@@ -52,6 +52,24 @@ impl AsyncRawSocket {
5252 self . socket . bind ( addr)
5353 }
5454
55+ /// Bind this socket to a specific network interface by OS index.
56+ ///
57+ /// Per-platform mechanism:
58+ ///
59+ /// | Platform | Option used | Notes |
60+ /// |----------|------------------------------------------|-----------------------------|
61+ /// | Linux | `IP_UNICAST_IF` / `IPV6_UNICAST_IF` | No `CAP_NET_RAW` required. |
62+ /// | macOS | `IP_BOUND_IF` / `IPV6_BOUND_IF` | Apple-specific socket opts. |
63+ /// | Windows | `IP_UNICAST_IF` / `IPV6_UNICAST_IF` | Via `ws2_32!setsockopt`. |
64+ ///
65+ /// Other platforms return [`std::io::ErrorKind::Unsupported`]. Taking a
66+ /// [`std::num::NonZeroU32`] makes it impossible to pass the
67+ /// "unbind / use default routing" sentinel by accident — drop the call
68+ /// site instead of binding to ifindex 0.
69+ pub fn bind_to_interface ( & self , family : socket2:: Domain , if_index : std:: num:: NonZeroU32 ) -> std:: io:: Result < ( ) > {
70+ bind_socket_to_interface ( & self . socket , family, if_index)
71+ }
72+
5573 pub async fn set_ttl ( & self , ttl : u32 ) -> std:: io:: Result < ( ) > {
5674 self . socket . set_ttl ( ttl)
5775 }
@@ -65,6 +83,132 @@ impl AsyncRawSocket {
6583 }
6684}
6785
86+ /// Bind a `socket2::Socket` to a specific interface index.
87+ ///
88+ /// Per-platform mechanism (`IP_UNICAST_IF` on Linux + Windows,
89+ /// `IP_BOUND_IF` on macOS — see [`AsyncRawSocket::bind_to_interface`] for
90+ /// the full table). Returns [`std::io::ErrorKind::Unsupported`] elsewhere.
91+ fn bind_socket_to_interface (
92+ socket : & Socket ,
93+ family : socket2:: Domain ,
94+ if_index : std:: num:: NonZeroU32 ,
95+ ) -> std:: io:: Result < ( ) > {
96+ let if_index = if_index. get ( ) ;
97+
98+ #[ cfg( any( target_os = "linux" , target_os = "macos" ) ) ]
99+ {
100+ use std:: os:: fd:: AsRawFd ;
101+
102+ // Resolve (level, name, value) per platform / family. Linux and
103+ // macOS share the libc::setsockopt call shape, only the constants
104+ // and IPv4 byte-order differ.
105+ let ( level, name, value) : ( libc:: c_int , libc:: c_int , u32 ) = match ( family, cfg ! ( target_os = "linux" ) ) {
106+ // Linux
107+ #[ cfg( target_os = "linux" ) ]
108+ ( socket2:: Domain :: IPV4 , _) => {
109+ // IPPROTO_IP, IP_UNICAST_IF; the kernel expects net-order.
110+ ( 0 , 50 , if_index. to_be ( ) )
111+ }
112+ #[ cfg( target_os = "linux" ) ]
113+ ( socket2:: Domain :: IPV6 , _) => {
114+ // IPPROTO_IPV6, IPV6_UNICAST_IF; host byte order.
115+ ( 41 , 76 , if_index)
116+ }
117+ // macOS
118+ #[ cfg( target_os = "macos" ) ]
119+ ( socket2:: Domain :: IPV4 , _) => {
120+ // IPPROTO_IP, IP_BOUND_IF
121+ ( 0 , 25 , if_index)
122+ }
123+ #[ cfg( target_os = "macos" ) ]
124+ ( socket2:: Domain :: IPV6 , _) => {
125+ // IPPROTO_IPV6, IPV6_BOUND_IF
126+ ( 41 , 125 , if_index)
127+ }
128+ _ => {
129+ return Err ( std:: io:: Error :: new (
130+ std:: io:: ErrorKind :: InvalidInput ,
131+ "interface bind only supported for IPv4 / IPv6 sockets" ,
132+ ) ) ;
133+ }
134+ } ;
135+
136+ let fd = socket. as_raw_fd ( ) ;
137+ // SAFETY: `fd` is a valid descriptor borrowed from `socket`;
138+ // `&value` is a stack pointer valid for the duration of the call.
139+ let ret = unsafe {
140+ libc:: setsockopt (
141+ fd,
142+ level,
143+ name,
144+ & value as * const u32 as * const libc:: c_void ,
145+ size_of :: < u32 > ( ) as libc:: socklen_t ,
146+ )
147+ } ;
148+ if ret == 0 {
149+ Ok ( ( ) )
150+ } else {
151+ Err ( std:: io:: Error :: last_os_error ( ) )
152+ }
153+ }
154+
155+ #[ cfg( target_os = "windows" ) ]
156+ {
157+ // Windows IP_UNICAST_IF (option 31) takes a u32: net-order for IPv4,
158+ // host-order for IPv6.
159+ use std:: os:: windows:: io:: AsRawSocket ;
160+ const IPPROTO_IP : i32 = 0 ;
161+ const IPPROTO_IPV6 : i32 = 41 ;
162+ const IP_UNICAST_IF : i32 = 31 ;
163+ const IPV6_UNICAST_IF : i32 = 31 ;
164+ // `SOCKET` is u64 in the Windows headers; on 32-bit Windows it
165+ // still fits in `usize` since SOCKET handles never exceed pointer
166+ // width.
167+ let raw: usize = usize:: try_from ( socket. as_raw_socket ( ) )
168+ . map_err ( |_| std:: io:: Error :: other ( "Windows socket handle does not fit in usize on this target" ) ) ?;
169+ let ( level, name, value) : ( i32 , i32 , u32 ) = if family == socket2:: Domain :: IPV4 {
170+ ( IPPROTO_IP , IP_UNICAST_IF , if_index. to_be ( ) )
171+ } else if family == socket2:: Domain :: IPV6 {
172+ ( IPPROTO_IPV6 , IPV6_UNICAST_IF , if_index)
173+ } else {
174+ return Err ( std:: io:: Error :: new (
175+ std:: io:: ErrorKind :: InvalidInput ,
176+ "interface bind only supported for IPv4 / IPv6 sockets" ,
177+ ) ) ;
178+ } ;
179+ // setsockopt takes optlen as i32; size_of::<u32>() is 4 — the cast
180+ // is infallible, but try_from documents intent and avoids a lint.
181+ let optlen = i32:: try_from ( size_of :: < u32 > ( ) ) . expect ( "size of u32 fits in i32" ) ;
182+ // SAFETY: setsockopt is invoked on a valid Windows socket handle
183+ // that outlives this call; `&value` lives on the stack across it.
184+ let ret = unsafe { windows_setsockopt ( raw, level, name, & value as * const u32 as * const u8 , optlen) } ;
185+ if ret == 0 {
186+ Ok ( ( ) )
187+ } else {
188+ Err ( std:: io:: Error :: last_os_error ( ) )
189+ }
190+ }
191+
192+ #[ cfg( not( any( target_os = "linux" , target_os = "macos" , target_os = "windows" ) ) ) ]
193+ {
194+ let _ = ( socket, family, if_index) ;
195+ Err ( std:: io:: Error :: new (
196+ std:: io:: ErrorKind :: Unsupported ,
197+ "interface bind not supported on this platform" ,
198+ ) )
199+ }
200+ }
201+
202+ #[ cfg( target_os = "windows" ) ]
203+ unsafe extern "system" {
204+ /// `setsockopt` from `ws2_32.dll`. We deliberately bind the symbol here
205+ /// instead of pulling in the full `windows-sys` crate so the only
206+ /// network-scanner-net Windows dependency stays within libstd's
207+ /// already-linked import library.
208+ #[ link_name = "setsockopt" ]
209+ fn windows_setsockopt ( s : usize , level : i32 , optname : i32 , optval : * const u8 , optlen : i32 ) -> i32 ;
210+ }
211+
68212impl < ' a > AsyncRawSocket {
69213 #[ tracing:: instrument( skip( self , buf) ) ]
70214 pub fn recv_from (
0 commit comments