Skip to content

Commit 057090b

Browse files
committed
support outbound-bind-interface for Windows
1 parent 949310e commit 057090b

File tree

9 files changed

+67
-38
lines changed

9 files changed

+67
-38
lines changed

bin/sslocal.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ fn main() {
7979
(@arg OUTBOUND_RECV_BUFFER_SIZE: --("outbound-recv-buffer-size") +takes_value {validator::validate_u32} "Set outbound sockets' SO_RCVBUF option")
8080

8181
(@arg OUTBOUND_BIND_ADDR: --("outbound-bind-addr") +takes_value alias("bind-addr") {validator::validate_ip_addr} "Bind address, outbound socket will bind this address")
82+
(@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE / IP_BOUND_IF / IP_UNICAST_IF option for outbound socket")
8283
);
8384

8485
// FIXME: -6 is not a identifier, so we cannot build it with clap_app!
@@ -114,18 +115,10 @@ fn main() {
114115
#[cfg(any(target_os = "linux", target_os = "android"))]
115116
{
116117
app = clap_app!(@app (app)
117-
(@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE option for outbound socket")
118118
(@arg OUTBOUND_FWMARK: --("outbound-fwmark") +takes_value {validator::validate_u32} "Set SO_MARK option for outbound socket")
119119
);
120120
}
121121

122-
#[cfg(any(target_os = "macos", target_os = "ios"))]
123-
{
124-
app = clap_app!(@app (app)
125-
(@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set IP_BOUND_IF option for outbound socket")
126-
);
127-
}
128-
129122
#[cfg(feature = "local-redir")]
130123
{
131124
if RedirType::tcp_default() != RedirType::NotSupported {
@@ -414,7 +407,6 @@ fn main() {
414407
config.outbound_fwmark = Some(mark.parse::<u32>().expect("an unsigned integer for `outbound-fwmark`"));
415408
}
416409

417-
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))]
418410
if let Some(iface) = matches.value_of("OUTBOUND_BIND_INTERFACE") {
419411
config.outbound_bind_interface = Some(iface.to_owned());
420412
}

bin/ssmanager.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ fn main() {
5050
Servers defined will be created when process is started.")
5151

5252
(@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")
53+
(@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE / IP_BOUND_IF / IP_UNICAST_IF option for outbound socket")
5354
(@arg SERVER_HOST: -s --("server-host") +takes_value "Host name or IP address of your remote server")
5455

5556
(@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() {
105106
#[cfg(any(target_os = "linux", target_os = "android"))]
106107
{
107108
app = clap_app!(@app (app)
108-
(@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE option for outbound socket")
109109
(@arg OUTBOUND_FWMARK: --("outbound-fwmark") +takes_value {validator::validate_u32} "Set SO_MARK option for outbound socket")
110110
);
111111
}
112112

113-
#[cfg(any(target_os = "macos", target_os = "ios"))]
114-
{
115-
app = clap_app!(@app (app)
116-
(@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set IP_BOUND_IF option for outbound socket")
117-
);
118-
}
119-
120113
#[cfg(feature = "multi-threaded")]
121114
{
122115
app = clap_app!(@app (app)
@@ -180,7 +173,6 @@ fn main() {
180173
config.outbound_fwmark = Some(mark.parse::<u32>().expect("an unsigned integer for `outbound-fwmark`"));
181174
}
182175

183-
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))]
184176
if let Some(iface) = matches.value_of("OUTBOUND_BIND_INTERFACE") {
185177
config.outbound_bind_interface = Some(iface.to_owned());
186178
}

bin/ssserver.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ fn main() {
4444
(@arg CONFIG: -c --config +takes_value required_unless("SERVER_ADDR") "Shadowsocks configuration file (https://shadowsocks.org/en/config/quick-guide.html)")
4545

4646
(@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")
47+
(@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE / IP_BOUND_IF / IP_UNICAST_IF option for outbound socket")
4748

4849
(@arg SERVER_ADDR: -s --("server-addr") +takes_value {validator::validate_server_addr} requires[PASSWORD ENCRYPT_METHOD] "Server address")
4950
(@arg PASSWORD: -k --password +takes_value requires[SERVER_ADDR] "Server's password")
@@ -100,18 +101,10 @@ fn main() {
100101
#[cfg(any(target_os = "linux", target_os = "android"))]
101102
{
102103
app = clap_app!(@app (app)
103-
(@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set SO_BINDTODEVICE option for outbound socket")
104104
(@arg OUTBOUND_FWMARK: --("outbound-fwmark") +takes_value {validator::validate_u32} "Set SO_MARK option for outbound socket")
105105
);
106106
}
107107

108-
#[cfg(any(target_os = "macos", target_os = "ios"))]
109-
{
110-
app = clap_app!(@app (app)
111-
(@arg OUTBOUND_BIND_INTERFACE: --("outbound-bind-interface") +takes_value "Set IP_BOUND_IF option for outbound socket")
112-
);
113-
}
114-
115108
#[cfg(feature = "multi-threaded")]
116109
{
117110
app = clap_app!(@app (app)
@@ -214,7 +207,6 @@ fn main() {
214207
config.outbound_fwmark = Some(mark.parse::<u32>().expect("an unsigned integer for `outbound-fwmark`"));
215208
}
216209

217-
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))]
218210
if let Some(iface) = matches.value_of("OUTBOUND_BIND_INTERFACE") {
219211
config.outbound_bind_interface = Some(iface.to_owned());
220212
}

crates/shadowsocks-service/src/config.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -963,8 +963,7 @@ pub struct Config {
963963
/// Set `SO_MARK` socket option for outbound sockets
964964
#[cfg(any(target_os = "linux", target_os = "android"))]
965965
pub outbound_fwmark: Option<u32>,
966-
/// Set `SO_BINDTODEVICE` socket option for outbound sockets
967-
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))]
966+
/// Set `SO_BINDTODEVICE` (Linux), `IP_BOUND_IF` (BSD), `IP_UNICAST_IF` (Windows) socket option for outbound sockets
968967
pub outbound_bind_interface: Option<String>,
969968
/// Outbound sockets will `bind` to this address
970969
pub outbound_bind_addr: Option<IpAddr>,
@@ -1088,7 +1087,6 @@ impl Config {
10881087

10891088
#[cfg(any(target_os = "linux", target_os = "android"))]
10901089
outbound_fwmark: None,
1091-
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))]
10921090
outbound_bind_interface: None,
10931091
outbound_bind_addr: None,
10941092
#[cfg(target_os = "android")]

crates/shadowsocks-service/src/local/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,7 @@ pub async fn create(config: Config) -> io::Result<Server> {
104104
#[cfg(target_os = "android")]
105105
vpn_protect_path: config.outbound_vpn_protect_path,
106106

107-
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))]
108107
bind_interface: config.outbound_bind_interface,
109-
110108
bind_local_addr: config.outbound_bind_addr,
111109

112110
..Default::default()

crates/shadowsocks-service/src/manager/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ pub async fn run(config: Config) -> io::Result<()> {
4141
vpn_protect_path: config.outbound_vpn_protect_path,
4242

4343
bind_local_addr: config.outbound_bind_addr,
44-
45-
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))]
4644
bind_interface: config.outbound_bind_interface,
4745

4846
..Default::default()

crates/shadowsocks-service/src/server/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@ pub async fn run(config: Config) -> io::Result<()> {
5858
vpn_protect_path: config.outbound_vpn_protect_path,
5959

6060
bind_local_addr: config.outbound_bind_addr,
61-
62-
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios"))]
6361
bind_interface: config.outbound_bind_interface,
6462

6563
..Default::default()

crates/shadowsocks/src/net/option.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ pub struct ConnectOpts {
4141
pub bind_local_addr: Option<IpAddr>,
4242

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

4746
/// TCP options

crates/shadowsocks/src/net/sys/windows/mod.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::{
2+
ffi::CString,
23
io::{self, ErrorKind},
34
mem,
45
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
@@ -20,7 +21,10 @@ use winapi::{
2021
ctypes::{c_char, c_int},
2122
shared::{
2223
minwindef::{BOOL, DWORD, FALSE, LPDWORD, LPVOID},
23-
ws2def::IPPROTO_TCP,
24+
netioapi::if_nametoindex,
25+
ntdef::PCSTR,
26+
ws2def::{IPPROTO_IP, IPPROTO_IPV6, IPPROTO_TCP},
27+
ws2ipdef::IPV6_UNICAST_IF,
2428
},
2529
um::{
2630
mswsock::SIO_UDP_CONNRESET,
@@ -35,6 +39,10 @@ use crate::net::{sys::set_common_sockopt_for_connect, AddrFamily, ConnectOpts};
3539
// https://github.com/retep998/winapi-rs/issues/856
3640
const TCP_FASTOPEN: DWORD = 15;
3741

42+
// ws2ipdef.h
43+
// https://github.com/retep998/winapi-rs/pull/1007
44+
const IP_UNICAST_IF: DWORD = 31;
45+
3846
/// A `TcpStream` that supports TFO (TCP Fast Open)
3947
#[pin_project(project = TcpStreamProj)]
4048
pub enum TcpStream {
@@ -49,6 +57,11 @@ impl TcpStream {
4957
SocketAddr::V6(..) => TcpSocket::new_v6()?,
5058
};
5159

60+
// Binds to a specific network interface (device)
61+
if let Some(ref iface) = opts.bind_interface {
62+
set_ip_unicast_if(&socket, addr, iface)?;
63+
}
64+
5265
set_common_sockopt_for_connect(addr, &socket, opts)?;
5366

5467
if !opts.tcp.fastopen {
@@ -166,6 +179,51 @@ pub fn set_tcp_fastopen<S: AsRawSocket>(socket: &S) -> io::Result<()> {
166179
Ok(())
167180
}
168181

182+
fn set_ip_unicast_if<S: AsRawSocket>(socket: &S, addr: SocketAddr, iface: &str) -> io::Result<()> {
183+
let handle = socket.as_raw_socket() as SOCKET;
184+
185+
unsafe {
186+
// Windows if_nametoindex requires a C-string for interface name
187+
let ifname = CString::new(iface);
188+
189+
// https://docs.microsoft.com/en-us/previous-versions/windows/hardware/drivers/ff553788(v=vs.85)
190+
let if_index = if_nametoindex(ifname.as_ptr() as PCSTR);
191+
if if_name == 0 {
192+
// If the if_nametoindex function fails and returns zero, it is not possible to determine an error code.
193+
error!("if_nametoindex {} fails", iface);
194+
return Err(io::Error::new(ErrorKind::InvalidInput, "invalid interface name"));
195+
}
196+
197+
// https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
198+
let if_index = if_index as DWORD;
199+
200+
let ret = match addr {
201+
SocketAddr::V4(..) => setsockopt(
202+
handle,
203+
IPPROTO_IP as c_int,
204+
IP_UNICAST_IF,
205+
&if_index as *const _ as *const c_char,
206+
mem::size_of_val(&if_index) as c_int,
207+
),
208+
SocketAddr::V6(..) => setsockopt(
209+
handle,
210+
IPPROTO_IPV6 as c_int,
211+
IPV6_UNICAST_IF,
212+
&if_index as *const _ as *const c_char,
213+
mem::size_of_val(&if_index) as c_int,
214+
),
215+
};
216+
217+
if ret == SOCKET_ERROR {
218+
let err = io::Error::from_raw_os_error(WSAGetLastError());
219+
error!("set IP_UNICAST_IF / IPV6_UNICAST_IF error: {}", err);
220+
return Err(err);
221+
}
222+
}
223+
224+
Ok(())
225+
}
226+
169227
fn disable_connection_reset(socket: &UdpSocket) -> io::Result<()> {
170228
let handle = socket.as_raw_socket() as SOCKET;
171229

@@ -271,6 +329,10 @@ pub async fn create_outbound_udp_socket(af: AddrFamily, opts: &ConnectOpts) -> i
271329
let socket = UdpSocket::bind(bind_addr).await?;
272330
disable_connection_reset(&socket)?;
273331

332+
if let Some(ref iface) = opts.bind_interface {
333+
set_ip_unicast_if(&socket, bind_addr, iface)?;
334+
}
335+
274336
Ok(socket)
275337
}
276338

0 commit comments

Comments
 (0)