diff --git a/README.md b/README.md index d7be931..a68aa95 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Go dispatch proxy -A SOCKS5 load balancing proxy to combine multiple internet connections into one. Works on Windows and Linux (partial support, see below). [Reported to work on macOS](/~https://github.com/extremecoders-re/go-dispatch-proxy/issues/1). Written in pure Go with no additional dependencies. +A SOCKS5 load balancing proxy to combine multiple internet connections into one. Works on Windows and Linux (full support, see details below). [Reported to work on macOS](/~https://github.com/extremecoders-re/go-dispatch-proxy/issues/1). Written in pure Go with no additional dependencies. It can also be used as a transparent proxy to load balance multiple SSH tunnels. @@ -92,6 +92,20 @@ D:\> go-dispatch-proxy.exe -lport 5555 -tunnel 127.0.0.1:7777@1 127.0.0.1:7778@3 The `lport` if not specified defaults to 8080. This is the port where you need to point your web browser, download manager etc. Be sure to add this as a SOCKS v5 proxy. +## Full Linux Support [NEW] + +Go-dispatch-proxy now supports Linux in both normal mode and tunnel mode. On Linux, Go-dispatch-proxy uses the `SO_BINDTODEVICE` syscall to bind to the interface corresponding to the load balancer IPs. As a result, the binary must be run with `root` privilege OR by giving it the necessary capabilities as shown below. + +``` +$ sudo ./go-dispatch-proxy +``` + +OR (Recommended) + +``` +$ sudo setcap cap_net_raw=eip ./go-dispatch-proxy +$ ./go-dispatch-proxy +``` ## Compiling (For Development) @@ -117,16 +131,6 @@ $ GOOS=linux GOARCH=amd64 go build $ GOOS=darwin GOARCH=amd64 go build ``` -## Partial Linux Support - -Go-dispatch-proxy supports linux only if used as a transparent proxy (tunnel mode) to load balance SSH tunnels. - -Go-dispatch-proxy can't load balance internet connections on linux. You can run this tool on linux but it will not function as expected, traffic will not be load balanced across the available interfaces. This is because of how linux works. - -Go-dispatch-proxy works by specifying the IP address (binding) to be used for each outgoing connection. Unfortunately on linux **IP address binding != interface binding**. Linux uses the route which has the lowest metric inspite of specifying the source IP address. See [this](http://wiki.treck.com/Appendix_C:_Strong_End_System_Model_/_Weak_End_System_Model) for further information. - -One workaround on linux is to use [`SO_BINDTODEVICE`](https://linux.die.net/man/7/socket) while creating the socket. However this reqires root to work and currently not implemented. - ## Credits - [dispatch-proxy](/~https://github.com/Morhaus/dispatch-proxy): A SOCKS5/HTTP load balancing proxy written in NodeJS. diff --git a/main.go b/main.go index a03a3e5..87e6928 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( type load_balancer struct { address string + iface string contention_ratio int current_connections int } @@ -30,7 +31,7 @@ var mutex *sync.Mutex /* Get a load balancer according to contention ratio */ -func get_load_balancer() string { +func get_load_balancer() *load_balancer { mutex.Lock() lb := &lb_list[lb_index] lb.current_connections += 1 @@ -44,7 +45,7 @@ func get_load_balancer() string { } } mutex.Unlock() - return lb.address + return lb } /* @@ -70,43 +71,22 @@ func pipe_connections(local_conn, remote_conn net.Conn) { }() } -/* - Implements servers response of SOCKS5 -*/ -func server_response(local_conn net.Conn, address string) { - load_balancer_addr := get_load_balancer() - - local_addr, _ := net.ResolveTCPAddr("tcp4", load_balancer_addr) - remote_addr, _ := net.ResolveTCPAddr("tcp4", address) - remote_conn, err := net.DialTCP("tcp4", local_addr, remote_addr) - - if err != nil { - log.Println("[WARN]", address, "->", load_balancer_addr, fmt.Sprintf("{%s}", err)) - local_conn.Write([]byte{5, NETWORK_UNREACHABLE, 0, 1, 0, 0, 0, 0, 0, 0}) - local_conn.Close() - return - } - log.Println("[DEBUG]", address, "->", load_balancer_addr) - local_conn.Write([]byte{5, SUCCESS, 0, 1, 0, 0, 0, 0, 0, 0}) - pipe_connections(local_conn, remote_conn) -} - /* Handle connections in tunnel mode */ func handle_tunnel_connection(conn net.Conn) { - load_balancer_addr := get_load_balancer() + load_balancer := get_load_balancer() - remote_addr, _ := net.ResolveTCPAddr("tcp4", load_balancer_addr) + remote_addr, _ := net.ResolveTCPAddr("tcp4", load_balancer.address) remote_conn, err := net.DialTCP("tcp4", nil, remote_addr) if err != nil { - log.Println("[WARN]", load_balancer_addr, fmt.Sprintf("{%s}", err)) + log.Println("[WARN]", load_balancer.address, fmt.Sprintf("{%s}", err)) conn.Close() return } - log.Println("[DEBUG] Tunnelled to", load_balancer_addr) + log.Println("[DEBUG] Tunnelled to", load_balancer.address) pipe_connections(conn, remote_conn) } @@ -116,7 +96,7 @@ func handle_tunnel_connection(conn net.Conn) { func handle_connection(conn net.Conn, tunnel bool) { if tunnel { handle_tunnel_connection(conn) - } else if address, err := Handle_socks_connection(conn); err == nil { + } else if address, err := handle_socks_connection(conn); err == nil { server_response(conn, address) } } @@ -144,6 +124,29 @@ func detect_interfaces() { } +/* + Gets the interface associated with the IP +*/ +func get_iface_from_ip(ip string) string { + ifaces, _ := net.Interfaces() + + for _, iface := range ifaces { + if (iface.Flags&net.FlagUp == net.FlagUp) && (iface.Flags&net.FlagLoopback != net.FlagLoopback) { + addrs, _ := iface.Addrs() + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + if ipnet.IP.String() == ip { + return iface.Name + "\x00" + } + } + } + } + } + } + return "" +} + /* Parses the command line arguements to obtain the list of load balancers */ @@ -190,8 +193,13 @@ func parse_load_balancers(args []string, tunnel bool) { } } + iface := get_iface_from_ip(lb_ip) + if iface == "" { + log.Fatal("[FATAL] IP address not associated with an interface ", lb_ip) + } + log.Printf("[INFO] Load balancer %d: %s, contention ratio: %d\n", idx+1, lb_ip, cont_ratio) - lb_list[idx] = load_balancer{address: fmt.Sprintf("%s:%d", lb_ip, lb_port), contention_ratio: cont_ratio, current_connections: 0} + lb_list[idx] = load_balancer{address: fmt.Sprintf("%s:%d", lb_ip, lb_port), iface: iface, contention_ratio: cont_ratio, current_connections: 0} } } diff --git a/servers_response.go b/servers_response.go new file mode 100644 index 0000000..a749c2b --- /dev/null +++ b/servers_response.go @@ -0,0 +1,31 @@ +// +build !linux + +// servers_response.go +package main + +import ( + "fmt" + "log" + "net" +) + +/* + Implements servers response of SOCKS5 for non Linux systems +*/ +func server_response(local_conn net.Conn, remote_address string) { + load_balancer := get_load_balancer() + + local_tcpaddr, _ := net.ResolveTCPAddr("tcp4", load_balancer.address) + remote_tcpaddr, _ := net.ResolveTCPAddr("tcp4", remote_address) + remote_conn, err := net.DialTCP("tcp4", local_tcpaddr, remote_tcpaddr) + + if err != nil { + log.Println("[WARN]", remote_address, "->", load_balancer.address, fmt.Sprintf("{%s}", err)) + local_conn.Write([]byte{5, NETWORK_UNREACHABLE, 0, 1, 0, 0, 0, 0, 0, 0}) + local_conn.Close() + return + } + log.Println("[DEBUG]", remote_address, "->", load_balancer.address) + local_conn.Write([]byte{5, SUCCESS, 0, 1, 0, 0, 0, 0, 0, 0}) + pipe_connections(local_conn, remote_conn) +} diff --git a/servers_response_linux.go b/servers_response_linux.go new file mode 100644 index 0000000..5b5cbc8 --- /dev/null +++ b/servers_response_linux.go @@ -0,0 +1,42 @@ +// servers_response_linux.go +package main + +import ( + "fmt" + "log" + "net" + "syscall" +) + +/* + Implements servers response of SOCKS5 for linux systems +*/ +func server_response(local_conn net.Conn, remote_address string) { + load_balancer := get_load_balancer() + local_tcpaddr, _ := net.ResolveTCPAddr("tcp4", load_balancer.address) + + dialer := net.Dialer{ + LocalAddr: local_tcpaddr, + Control: func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + // NOTE: Run with root or use setcap to allow interface binding + // sudo setcap cap_net_raw=eip ./go-dispatch-proxy + if err := syscall.BindToDevice(int(fd), load_balancer.iface); err != nil { + log.Println("[WARN] Couldn't bind to interface", load_balancer.iface) + } + }) + }, + } + + remote_conn, err := dialer.Dial("tcp4", remote_address) + if err != nil { + log.Println("[WARN]", remote_address, "->", load_balancer.address, fmt.Sprintf("{%s}", err)) + local_conn.Write([]byte{5, NETWORK_UNREACHABLE, 0, 1, 0, 0, 0, 0, 0, 0}) + local_conn.Close() + return + } + + log.Println("[DEBUG]", remote_address, "->", load_balancer.address) + local_conn.Write([]byte{5, SUCCESS, 0, 1, 0, 0, 0, 0, 0, 0}) + pipe_connections(local_conn, remote_conn) +} diff --git a/socks.go b/socks.go index 96240bd..c802918 100644 --- a/socks.go +++ b/socks.go @@ -129,7 +129,7 @@ func client_conection_request(conn net.Conn) (string, error) { /* */ -func Handle_socks_connection(conn net.Conn) (string, error) { +func handle_socks_connection(conn net.Conn) (string, error) { if _, _, err := client_greeting(conn); err != nil { log.Println(err)