Skip to content

Commit

Permalink
support outbound-bind-interface for Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
zonyitoo committed Oct 23, 2021
1 parent 949310e commit 057090b
Show file tree
Hide file tree
Showing 9 changed files with 67 additions and 38 deletions.
10 changes: 1 addition & 9 deletions bin/sslocal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -414,7 +407,6 @@ fn main() {
config.outbound_fwmark = Some(mark.parse::<u32>().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());
}
Expand Down
10 changes: 1 addition & 9 deletions bin/ssmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -180,7 +173,6 @@ fn main() {
config.outbound_fwmark = Some(mark.parse::<u32>().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());
}
Expand Down
10 changes: 1 addition & 9 deletions bin/ssserver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -214,7 +207,6 @@ fn main() {
config.outbound_fwmark = Some(mark.parse::<u32>().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());
}
Expand Down
4 changes: 1 addition & 3 deletions crates/shadowsocks-service/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>,
/// 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<String>,
/// Outbound sockets will `bind` to this address
pub outbound_bind_addr: Option<IpAddr>,
Expand Down Expand Up @@ -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")]
Expand Down
2 changes: 0 additions & 2 deletions crates/shadowsocks-service/src/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,7 @@ pub async fn create(config: Config) -> io::Result<Server> {
#[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()
Expand Down
2 changes: 0 additions & 2 deletions crates/shadowsocks-service/src/manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 0 additions & 2 deletions crates/shadowsocks-service/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 0 additions & 1 deletion crates/shadowsocks/src/net/option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ pub struct ConnectOpts {
pub bind_local_addr: Option<IpAddr>,

/// Outbound socket binds to interface
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))]
pub bind_interface: Option<String>,

/// TCP options
Expand Down
64 changes: 63 additions & 1 deletion crates/shadowsocks/src/net/sys/windows/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::{
ffi::CString,
io::{self, ErrorKind},
mem,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -166,6 +179,51 @@ pub fn set_tcp_fastopen<S: AsRawSocket>(socket: &S) -> io::Result<()> {
Ok(())
}

fn set_ip_unicast_if<S: AsRawSocket>(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;

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

Expand Down

0 comments on commit 057090b

Please sign in to comment.