Skip to content

Commit

Permalink
Implement SO_BINDTODEVICE. Closes #6
Browse files Browse the repository at this point in the history
  • Loading branch information
extremecoders-re committed Mar 4, 2021
1 parent c685c43 commit 25613ab
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 41 deletions.
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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)

Expand All @@ -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.
Expand Down
66 changes: 37 additions & 29 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

type load_balancer struct {
address string
iface string
contention_ratio int
current_connections int
}
Expand All @@ -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
Expand All @@ -44,7 +45,7 @@ func get_load_balancer() string {
}
}
mutex.Unlock()
return lb.address
return lb
}

/*
Expand All @@ -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)
}

Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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}
}
}

Expand Down
31 changes: 31 additions & 0 deletions servers_response.go
Original file line number Diff line number Diff line change
@@ -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)
}
42 changes: 42 additions & 0 deletions servers_response_linux.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion socks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 25613ab

Please sign in to comment.