From 057090b6a3991c07a377cf05b5472ad7a86bdbbc Mon Sep 17 00:00:00 2001 From: zonyitoo Date: Sat, 23 Oct 2021 17:17:03 +0800 Subject: [PATCH] support outbound-bind-interface for Windows --- bin/sslocal.rs | 10 +-- bin/ssmanager.rs | 10 +-- bin/ssserver.rs | 10 +-- crates/shadowsocks-service/src/config.rs | 4 +- crates/shadowsocks-service/src/local/mod.rs | 2 - crates/shadowsocks-service/src/manager/mod.rs | 2 - crates/shadowsocks-service/src/server/mod.rs | 2 - crates/shadowsocks/src/net/option.rs | 1 - crates/shadowsocks/src/net/sys/windows/mod.rs | 64 ++++++++++++++++++- 9 files changed, 67 insertions(+), 38 deletions(-) diff --git a/bin/sslocal.rs b/bin/sslocal.rs index 0e4477ea1371..b63b79b6f3e1 100644 --- a/bin/sslocal.rs +++ b/bin/sslocal.rs @@ -79,6 +79,7 @@ fn main() { (@arg OUTBOUND_RECV_BUFFER_SIZE: --("outbound-recv-buffer-size") +takes_value {validator::validate_u32} "Set outbound sockets' SO_RCVBUF option") (@arg OUTBOUND_BIND_ADDR: --("outbound-bind-addr") +takes_value alias("bind-addr") {validator::validate_ip_addr} "Bind address, outbound socket will bind this address") + (@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE / IP_BOUND_IF / IP_UNICAST_IF option for outbound socket") ); // FIXME: -6 is not a identifier, so we cannot build it with clap_app! @@ -114,18 +115,10 @@ fn main() { #[cfg(any(target_os = "linux", target_os = "android"))] { app = clap_app!(@app (app) - (@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE option for outbound socket") (@arg OUTBOUND_FWMARK: --("outbound-fwmark") +takes_value {validator::validate_u32} "Set SO_MARK option for outbound socket") ); } - #[cfg(any(target_os = "macos", target_os = "ios"))] - { - app = clap_app!(@app (app) - (@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set IP_BOUND_IF option for outbound socket") - ); - } - #[cfg(feature = "local-redir")] { if RedirType::tcp_default() != RedirType::NotSupported { @@ -414,7 +407,6 @@ fn main() { config.outbound_fwmark = Some(mark.parse::().expect("an unsigned integer for `outbound-fwmark`")); } - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))] if let Some(iface) = matches.value_of("OUTBOUND_BIND_INTERFACE") { config.outbound_bind_interface = Some(iface.to_owned()); } diff --git a/bin/ssmanager.rs b/bin/ssmanager.rs index 1817b5bd24d4..d637d029ff78 100644 --- a/bin/ssmanager.rs +++ b/bin/ssmanager.rs @@ -50,6 +50,7 @@ fn main() { Servers defined will be created when process is started.") (@arg OUTBOUND_BIND_ADDR: -b --("outbound-bind-addr") +takes_value alias("bind-addr") {validator::validate_ip_addr} "Bind address, outbound socket will bind this address") + (@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE / IP_BOUND_IF / IP_UNICAST_IF option for outbound socket") (@arg SERVER_HOST: -s --("server-host") +takes_value "Host name or IP address of your remote server") (@arg MANAGER_ADDR: --("manager-addr") +takes_value alias("manager-address") {validator::validate_manager_addr} "ShadowSocks Manager (ssmgr) address, could be ip:port, domain:port or /path/to/unix.sock") @@ -105,18 +106,10 @@ fn main() { #[cfg(any(target_os = "linux", target_os = "android"))] { app = clap_app!(@app (app) - (@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE option for outbound socket") (@arg OUTBOUND_FWMARK: --("outbound-fwmark") +takes_value {validator::validate_u32} "Set SO_MARK option for outbound socket") ); } - #[cfg(any(target_os = "macos", target_os = "ios"))] - { - app = clap_app!(@app (app) - (@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set IP_BOUND_IF option for outbound socket") - ); - } - #[cfg(feature = "multi-threaded")] { app = clap_app!(@app (app) @@ -180,7 +173,6 @@ fn main() { config.outbound_fwmark = Some(mark.parse::().expect("an unsigned integer for `outbound-fwmark`")); } - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))] if let Some(iface) = matches.value_of("OUTBOUND_BIND_INTERFACE") { config.outbound_bind_interface = Some(iface.to_owned()); } diff --git a/bin/ssserver.rs b/bin/ssserver.rs index 2ef9caafee8a..4663d90e668f 100644 --- a/bin/ssserver.rs +++ b/bin/ssserver.rs @@ -44,6 +44,7 @@ fn main() { (@arg CONFIG: -c --config +takes_value required_unless("SERVER_ADDR") "Shadowsocks configuration file (https://shadowsocks.org/en/config/quick-guide.html)") (@arg OUTBOUND_BIND_ADDR: -b --("outbound-bind-addr") +takes_value alias("bind-addr") {validator::validate_ip_addr} "Bind address, outbound socket will bind this address") + (@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE / IP_BOUND_IF / IP_UNICAST_IF option for outbound socket") (@arg SERVER_ADDR: -s --("server-addr") +takes_value {validator::validate_server_addr} requires[PASSWORD ENCRYPT_METHOD] "Server address") (@arg PASSWORD: -k --password +takes_value requires[SERVER_ADDR] "Server's password") @@ -100,18 +101,10 @@ fn main() { #[cfg(any(target_os = "linux", target_os = "android"))] { app = clap_app!(@app (app) - (@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE option for outbound socket") (@arg OUTBOUND_FWMARK: --("outbound-fwmark") +takes_value {validator::validate_u32} "Set SO_MARK option for outbound socket") ); } - #[cfg(any(target_os = "macos", target_os = "ios"))] - { - app = clap_app!(@app (app) - (@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set IP_BOUND_IF option for outbound socket") - ); - } - #[cfg(feature = "multi-threaded")] { app = clap_app!(@app (app) @@ -214,7 +207,6 @@ fn main() { config.outbound_fwmark = Some(mark.parse::().expect("an unsigned integer for `outbound-fwmark`")); } - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))] if let Some(iface) = matches.value_of("OUTBOUND_BIND_INTERFACE") { config.outbound_bind_interface = Some(iface.to_owned()); } diff --git a/crates/shadowsocks-service/src/config.rs b/crates/shadowsocks-service/src/config.rs index 0054323137ee..f1fc5a7fab86 100644 --- a/crates/shadowsocks-service/src/config.rs +++ b/crates/shadowsocks-service/src/config.rs @@ -963,8 +963,7 @@ pub struct Config { /// Set `SO_MARK` socket option for outbound sockets #[cfg(any(target_os = "linux", target_os = "android"))] pub outbound_fwmark: Option, - /// Set `SO_BINDTODEVICE` socket option for outbound sockets - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))] + /// Set `SO_BINDTODEVICE` (Linux), `IP_BOUND_IF` (BSD), `IP_UNICAST_IF` (Windows) socket option for outbound sockets pub outbound_bind_interface: Option, /// Outbound sockets will `bind` to this address pub outbound_bind_addr: Option, @@ -1088,7 +1087,6 @@ impl Config { #[cfg(any(target_os = "linux", target_os = "android"))] outbound_fwmark: None, - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))] outbound_bind_interface: None, outbound_bind_addr: None, #[cfg(target_os = "android")] diff --git a/crates/shadowsocks-service/src/local/mod.rs b/crates/shadowsocks-service/src/local/mod.rs index a2507241afca..caffc01427df 100644 --- a/crates/shadowsocks-service/src/local/mod.rs +++ b/crates/shadowsocks-service/src/local/mod.rs @@ -104,9 +104,7 @@ pub async fn create(config: Config) -> io::Result { #[cfg(target_os = "android")] vpn_protect_path: config.outbound_vpn_protect_path, - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))] bind_interface: config.outbound_bind_interface, - bind_local_addr: config.outbound_bind_addr, ..Default::default() diff --git a/crates/shadowsocks-service/src/manager/mod.rs b/crates/shadowsocks-service/src/manager/mod.rs index ffdb36f74210..d00a96a21225 100644 --- a/crates/shadowsocks-service/src/manager/mod.rs +++ b/crates/shadowsocks-service/src/manager/mod.rs @@ -41,8 +41,6 @@ pub async fn run(config: Config) -> io::Result<()> { vpn_protect_path: config.outbound_vpn_protect_path, bind_local_addr: config.outbound_bind_addr, - - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))] bind_interface: config.outbound_bind_interface, ..Default::default() diff --git a/crates/shadowsocks-service/src/server/mod.rs b/crates/shadowsocks-service/src/server/mod.rs index 4b5d669f3b52..39a87875fed5 100644 --- a/crates/shadowsocks-service/src/server/mod.rs +++ b/crates/shadowsocks-service/src/server/mod.rs @@ -58,8 +58,6 @@ pub async fn run(config: Config) -> io::Result<()> { vpn_protect_path: config.outbound_vpn_protect_path, bind_local_addr: config.outbound_bind_addr, - - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))] bind_interface: config.outbound_bind_interface, ..Default::default() diff --git a/crates/shadowsocks/src/net/option.rs b/crates/shadowsocks/src/net/option.rs index 398d33141829..de8ada2ec75c 100644 --- a/crates/shadowsocks/src/net/option.rs +++ b/crates/shadowsocks/src/net/option.rs @@ -41,7 +41,6 @@ pub struct ConnectOpts { pub bind_local_addr: Option, /// Outbound socket binds to interface - #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))] pub bind_interface: Option, /// TCP options diff --git a/crates/shadowsocks/src/net/sys/windows/mod.rs b/crates/shadowsocks/src/net/sys/windows/mod.rs index 574f3747412b..f6ae68aa31a6 100644 --- a/crates/shadowsocks/src/net/sys/windows/mod.rs +++ b/crates/shadowsocks/src/net/sys/windows/mod.rs @@ -1,4 +1,5 @@ use std::{ + ffi::CString, io::{self, ErrorKind}, mem, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, @@ -20,7 +21,10 @@ use winapi::{ ctypes::{c_char, c_int}, shared::{ minwindef::{BOOL, DWORD, FALSE, LPDWORD, LPVOID}, - ws2def::IPPROTO_TCP, + netioapi::if_nametoindex, + ntdef::PCSTR, + ws2def::{IPPROTO_IP, IPPROTO_IPV6, IPPROTO_TCP}, + ws2ipdef::IPV6_UNICAST_IF, }, um::{ mswsock::SIO_UDP_CONNRESET, @@ -35,6 +39,10 @@ use crate::net::{sys::set_common_sockopt_for_connect, AddrFamily, ConnectOpts}; // https://github.com/retep998/winapi-rs/issues/856 const TCP_FASTOPEN: DWORD = 15; +// ws2ipdef.h +// https://github.com/retep998/winapi-rs/pull/1007 +const IP_UNICAST_IF: DWORD = 31; + /// A `TcpStream` that supports TFO (TCP Fast Open) #[pin_project(project = TcpStreamProj)] pub enum TcpStream { @@ -49,6 +57,11 @@ impl TcpStream { SocketAddr::V6(..) => TcpSocket::new_v6()?, }; + // Binds to a specific network interface (device) + if let Some(ref iface) = opts.bind_interface { + set_ip_unicast_if(&socket, addr, iface)?; + } + set_common_sockopt_for_connect(addr, &socket, opts)?; if !opts.tcp.fastopen { @@ -166,6 +179,51 @@ pub fn set_tcp_fastopen(socket: &S) -> io::Result<()> { Ok(()) } +fn set_ip_unicast_if(socket: &S, addr: SocketAddr, iface: &str) -> io::Result<()> { + let handle = socket.as_raw_socket() as SOCKET; + + unsafe { + // Windows if_nametoindex requires a C-string for interface name + let ifname = CString::new(iface); + + // https://docs.microsoft.com/en-us/previous-versions/windows/hardware/drivers/ff553788(v=vs.85) + let if_index = if_nametoindex(ifname.as_ptr() as PCSTR); + if if_name == 0 { + // If the if_nametoindex function fails and returns zero, it is not possible to determine an error code. + error!("if_nametoindex {} fails", iface); + return Err(io::Error::new(ErrorKind::InvalidInput, "invalid interface name")); + } + + // https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options + let if_index = if_index as DWORD; + + let ret = match addr { + SocketAddr::V4(..) => setsockopt( + handle, + IPPROTO_IP as c_int, + IP_UNICAST_IF, + &if_index as *const _ as *const c_char, + mem::size_of_val(&if_index) as c_int, + ), + SocketAddr::V6(..) => setsockopt( + handle, + IPPROTO_IPV6 as c_int, + IPV6_UNICAST_IF, + &if_index as *const _ as *const c_char, + mem::size_of_val(&if_index) as c_int, + ), + }; + + if ret == SOCKET_ERROR { + let err = io::Error::from_raw_os_error(WSAGetLastError()); + error!("set IP_UNICAST_IF / IPV6_UNICAST_IF error: {}", err); + return Err(err); + } + } + + Ok(()) +} + fn disable_connection_reset(socket: &UdpSocket) -> io::Result<()> { let handle = socket.as_raw_socket() as SOCKET; @@ -271,6 +329,10 @@ pub async fn create_outbound_udp_socket(af: AddrFamily, opts: &ConnectOpts) -> i let socket = UdpSocket::bind(bind_addr).await?; disable_connection_reset(&socket)?; + if let Some(ref iface) = opts.bind_interface { + set_ip_unicast_if(&socket, bind_addr, iface)?; + } + Ok(socket) }