forked from fossabot/clash
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: bind socket to interface by native API on Windows (#2662)
- Loading branch information
Showing
6 changed files
with
224 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package dialer | ||
|
||
import ( | ||
"encoding/binary" | ||
"net" | ||
"strings" | ||
"syscall" | ||
"unsafe" | ||
|
||
"github.com/Dreamacro/clash/component/iface" | ||
|
||
"golang.org/x/sys/windows" | ||
) | ||
|
||
const ( | ||
IP_UNICAST_IF = 31 | ||
IPV6_UNICAST_IF = 31 | ||
) | ||
|
||
type controlFn = func(network, address string, c syscall.RawConn) error | ||
|
||
func bindControl(ifaceIdx int, chain controlFn) controlFn { | ||
return func(network, address string, c syscall.RawConn) (err error) { | ||
defer func() { | ||
if err == nil && chain != nil { | ||
err = chain(network, address, c) | ||
} | ||
}() | ||
|
||
ipStr, _, err := net.SplitHostPort(address) | ||
if err == nil { | ||
ip := net.ParseIP(ipStr) | ||
if ip != nil && !ip.IsGlobalUnicast() { | ||
return | ||
} | ||
} | ||
|
||
var innerErr error | ||
err = c.Control(func(fd uintptr) { | ||
if ipStr == "" && strings.HasPrefix(network, "udp") { | ||
// When listening udp ":0", we should bind socket to interface4 and interface6 at the same time | ||
// and ignore the error of bind6 | ||
_ = bindSocketToInterface6(windows.Handle(fd), ifaceIdx) | ||
innerErr = bindSocketToInterface4(windows.Handle(fd), ifaceIdx) | ||
return | ||
} | ||
switch network { | ||
case "tcp4", "udp4": | ||
innerErr = bindSocketToInterface4(windows.Handle(fd), ifaceIdx) | ||
case "tcp6", "udp6": | ||
innerErr = bindSocketToInterface6(windows.Handle(fd), ifaceIdx) | ||
} | ||
}) | ||
|
||
if innerErr != nil { | ||
err = innerErr | ||
} | ||
|
||
return | ||
} | ||
} | ||
|
||
func bindSocketToInterface4(handle windows.Handle, ifaceIdx int) error { | ||
// MSDN says for IPv4 this needs to be in net byte order, so that it's like an IP address with leading zeros. | ||
// Ref: https://learn.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options | ||
var bytes [4]byte | ||
binary.BigEndian.PutUint32(bytes[:], uint32(ifaceIdx)) | ||
index := *(*uint32)(unsafe.Pointer(&bytes[0])) | ||
err := windows.SetsockoptInt(handle, windows.IPPROTO_IP, IP_UNICAST_IF, int(index)) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func bindSocketToInterface6(handle windows.Handle, ifaceIdx int) error { | ||
return windows.SetsockoptInt(handle, windows.IPPROTO_IPV6, IPV6_UNICAST_IF, ifaceIdx) | ||
} | ||
|
||
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error { | ||
ifaceObj, err := iface.ResolveInterface(ifaceName) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
dialer.Control = bindControl(ifaceObj.Index, dialer.Control) | ||
return nil | ||
} | ||
|
||
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) { | ||
ifaceObj, err := iface.ResolveInterface(ifaceName) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
lc.Control = bindControl(ifaceObj.Index, lc.Control) | ||
return address, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package dialer | ||
|
||
import ( | ||
"net" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/Dreamacro/clash/component/iface" | ||
) | ||
|
||
func lookupLocalAddr(ifaceName string, network string, destination net.IP, port int) (net.Addr, error) { | ||
ifaceObj, err := iface.ResolveInterface(ifaceName) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var addr *net.IPNet | ||
switch network { | ||
case "udp4", "tcp4": | ||
addr, err = ifaceObj.PickIPv4Addr(destination) | ||
case "tcp6", "udp6": | ||
addr, err = ifaceObj.PickIPv6Addr(destination) | ||
default: | ||
if destination != nil { | ||
if destination.To4() != nil { | ||
addr, err = ifaceObj.PickIPv4Addr(destination) | ||
} else { | ||
addr, err = ifaceObj.PickIPv6Addr(destination) | ||
} | ||
} else { | ||
addr, err = ifaceObj.PickIPv4Addr(destination) | ||
} | ||
} | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if strings.HasPrefix(network, "tcp") { | ||
return &net.TCPAddr{ | ||
IP: addr.IP, | ||
Port: port, | ||
}, nil | ||
} else if strings.HasPrefix(network, "udp") { | ||
return &net.UDPAddr{ | ||
IP: addr.IP, | ||
Port: port, | ||
}, nil | ||
} | ||
|
||
return nil, iface.ErrAddrNotFound | ||
} | ||
|
||
func fallbackBindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error { | ||
if !destination.IsGlobalUnicast() { | ||
return nil | ||
} | ||
|
||
local := uint64(0) | ||
if dialer.LocalAddr != nil { | ||
_, port, err := net.SplitHostPort(dialer.LocalAddr.String()) | ||
if err == nil { | ||
local, _ = strconv.ParseUint(port, 10, 16) | ||
} | ||
} | ||
|
||
addr, err := lookupLocalAddr(ifaceName, network, destination, int(local)) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
dialer.LocalAddr = addr | ||
|
||
return nil | ||
} | ||
|
||
func fallbackBindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) { | ||
_, port, err := net.SplitHostPort(address) | ||
if err != nil { | ||
port = "0" | ||
} | ||
|
||
local, _ := strconv.ParseUint(port, 10, 16) | ||
|
||
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local)) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return addr.String(), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters