diff --git a/src/config/load.rs b/src/config/load.rs index f9e230c..b7bc9fa 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -1289,6 +1289,7 @@ impl ProxyConfig { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/config/types.rs b/src/config/types.rs index 7eb7702..e287246 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -1856,6 +1856,10 @@ pub enum UpstreamType { interface: Option, #[serde(default)] bind_addresses: Option>, + /// Linux-only hard interface pinning via `SO_BINDTODEVICE`. + /// Optional alias: `force_bind`. + #[serde(default, alias = "force_bind")] + bindtodevice: Option, }, Socks4 { address: String, diff --git a/src/network/probe.rs b/src/network/probe.rs index 1787b92..90484b3 100644 --- a/src/network/probe.rs +++ b/src/network/probe.rs @@ -97,6 +97,7 @@ pub async fn run_probe( let UpstreamType::Direct { interface, bind_addresses, + .. } = &upstream.upstream_type else { continue; diff --git a/src/proxy/tests/client_masking_blackhat_campaign_tests.rs b/src/proxy/tests/client_masking_blackhat_campaign_tests.rs index 917e799..962387a 100644 --- a/src/proxy/tests/client_masking_blackhat_campaign_tests.rs +++ b/src/proxy/tests/client_masking_blackhat_campaign_tests.rs @@ -31,6 +31,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_budget_security_tests.rs b/src/proxy/tests/client_masking_budget_security_tests.rs index 332451c..d356869 100644 --- a/src/proxy/tests/client_masking_budget_security_tests.rs +++ b/src/proxy/tests/client_masking_budget_security_tests.rs @@ -27,6 +27,7 @@ fn build_harness(config: ProxyConfig) -> PipelineHarness { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_diagnostics_security_tests.rs b/src/proxy/tests/client_masking_diagnostics_security_tests.rs index 67b797b..a2f44ce 100644 --- a/src/proxy/tests/client_masking_diagnostics_security_tests.rs +++ b/src/proxy/tests/client_masking_diagnostics_security_tests.rs @@ -11,6 +11,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_fragmented_classifier_security_tests.rs b/src/proxy/tests/client_masking_fragmented_classifier_security_tests.rs index 8fa2689..ae04c6a 100644 --- a/src/proxy/tests/client_masking_fragmented_classifier_security_tests.rs +++ b/src/proxy/tests/client_masking_fragmented_classifier_security_tests.rs @@ -11,6 +11,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_hard_adversarial_tests.rs b/src/proxy/tests/client_masking_hard_adversarial_tests.rs index c6b0e98..7e0c683 100644 --- a/src/proxy/tests/client_masking_hard_adversarial_tests.rs +++ b/src/proxy/tests/client_masking_hard_adversarial_tests.rs @@ -25,6 +25,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_http2_fragmented_preface_security_tests.rs b/src/proxy/tests/client_masking_http2_fragmented_preface_security_tests.rs index b5a8b4d..8aa6fb2 100644 --- a/src/proxy/tests/client_masking_http2_fragmented_preface_security_tests.rs +++ b/src/proxy/tests/client_masking_http2_fragmented_preface_security_tests.rs @@ -11,6 +11,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_prefetch_config_pipeline_integration_security_tests.rs b/src/proxy/tests/client_masking_prefetch_config_pipeline_integration_security_tests.rs index b3fd5cb..b992402 100644 --- a/src/proxy/tests/client_masking_prefetch_config_pipeline_integration_security_tests.rs +++ b/src/proxy/tests/client_masking_prefetch_config_pipeline_integration_security_tests.rs @@ -11,6 +11,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_prefetch_invariant_security_tests.rs b/src/proxy/tests/client_masking_prefetch_invariant_security_tests.rs index b57ad51..e9af94c 100644 --- a/src/proxy/tests/client_masking_prefetch_invariant_security_tests.rs +++ b/src/proxy/tests/client_masking_prefetch_invariant_security_tests.rs @@ -38,6 +38,7 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_probe_evasion_blackhat_tests.rs b/src/proxy/tests/client_masking_probe_evasion_blackhat_tests.rs index 9ab5f78..282465a 100644 --- a/src/proxy/tests/client_masking_probe_evasion_blackhat_tests.rs +++ b/src/proxy/tests/client_masking_probe_evasion_blackhat_tests.rs @@ -16,6 +16,7 @@ fn make_test_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_redteam_expected_fail_tests.rs b/src/proxy/tests/client_masking_redteam_expected_fail_tests.rs index 2b6f600..b4a79fe 100644 --- a/src/proxy/tests/client_masking_redteam_expected_fail_tests.rs +++ b/src/proxy/tests/client_masking_redteam_expected_fail_tests.rs @@ -39,6 +39,7 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> RedTeamHarness { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -229,6 +230,7 @@ async fn redteam_03_masking_duration_must_be_less_than_1ms_when_backend_down() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -470,6 +472,7 @@ async fn measure_invalid_probe_duration_ms(delay_ms: u64, tls_len: u16, body_sen upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -544,6 +547,7 @@ async fn capture_forwarded_probe_len(tls_len: u16, body_sent: usize) -> usize { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_replay_timing_security_tests.rs b/src/proxy/tests/client_masking_replay_timing_security_tests.rs index 97ed52a..8c829be 100644 --- a/src/proxy/tests/client_masking_replay_timing_security_tests.rs +++ b/src/proxy/tests/client_masking_replay_timing_security_tests.rs @@ -13,6 +13,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_shape_classifier_fuzz_redteam_expected_fail_tests.rs b/src/proxy/tests/client_masking_shape_classifier_fuzz_redteam_expected_fail_tests.rs index c4dd4db..14837bf 100644 --- a/src/proxy/tests/client_masking_shape_classifier_fuzz_redteam_expected_fail_tests.rs +++ b/src/proxy/tests/client_masking_shape_classifier_fuzz_redteam_expected_fail_tests.rs @@ -11,6 +11,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_shape_hardening_adversarial_tests.rs b/src/proxy/tests/client_masking_shape_hardening_adversarial_tests.rs index 2cf98c4..014180d 100644 --- a/src/proxy/tests/client_masking_shape_hardening_adversarial_tests.rs +++ b/src/proxy/tests/client_masking_shape_hardening_adversarial_tests.rs @@ -11,6 +11,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_shape_hardening_redteam_expected_fail_tests.rs b/src/proxy/tests/client_masking_shape_hardening_redteam_expected_fail_tests.rs index b0bf73e..49378f6 100644 --- a/src/proxy/tests/client_masking_shape_hardening_redteam_expected_fail_tests.rs +++ b/src/proxy/tests/client_masking_shape_hardening_redteam_expected_fail_tests.rs @@ -11,6 +11,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_shape_hardening_security_tests.rs b/src/proxy/tests/client_masking_shape_hardening_security_tests.rs index 7d2380b..6a64c6e 100644 --- a/src/proxy/tests/client_masking_shape_hardening_security_tests.rs +++ b/src/proxy/tests/client_masking_shape_hardening_security_tests.rs @@ -11,6 +11,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_masking_stress_adversarial_tests.rs b/src/proxy/tests/client_masking_stress_adversarial_tests.rs index 1c8b599..9ccd033 100644 --- a/src/proxy/tests/client_masking_stress_adversarial_tests.rs +++ b/src/proxy/tests/client_masking_stress_adversarial_tests.rs @@ -25,6 +25,7 @@ fn new_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_security_tests.rs b/src/proxy/tests/client_security_tests.rs index d585326..40d99ec 100644 --- a/src/proxy/tests/client_security_tests.rs +++ b/src/proxy/tests/client_security_tests.rs @@ -332,6 +332,7 @@ async fn relay_task_abort_releases_user_gate_and_ip_reservation() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -446,6 +447,7 @@ async fn relay_cutover_releases_user_gate_and_ip_reservation() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -570,6 +572,7 @@ async fn integration_route_cutover_and_quota_overlap_fails_closed_and_releases_s upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -740,6 +743,7 @@ async fn proxy_protocol_header_is_rejected_when_trust_list_is_empty() { upstream_type: crate::config::UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -817,6 +821,7 @@ async fn proxy_protocol_header_from_untrusted_peer_range_is_rejected_under_load( upstream_type: crate::config::UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -977,6 +982,7 @@ async fn short_tls_probe_is_masked_through_client_pipeline() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -1065,6 +1071,7 @@ async fn tls12_record_probe_is_masked_through_client_pipeline() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -1151,6 +1158,7 @@ async fn handle_client_stream_increments_connects_all_exactly_once() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -1244,6 +1252,7 @@ async fn running_client_handler_increments_connects_all_exactly_once() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -1334,6 +1343,7 @@ async fn idle_pooled_connection_closes_cleanly_in_generic_stream_path() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -1405,6 +1415,7 @@ async fn idle_pooled_connection_closes_cleanly_in_client_handler_path() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -1491,6 +1502,7 @@ async fn partial_tls_header_stall_triggers_handshake_timeout() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -1816,6 +1828,7 @@ async fn valid_tls_path_does_not_fall_back_to_mask_backend() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -1925,6 +1938,7 @@ async fn valid_tls_with_invalid_mtproto_falls_back_to_mask_backend() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -2032,6 +2046,7 @@ async fn client_handler_tls_bad_mtproto_is_forwarded_to_mask_backend() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -2154,6 +2169,7 @@ async fn alpn_mismatch_tls_probe_is_masked_through_client_pipeline() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -2247,6 +2263,7 @@ async fn invalid_hmac_tls_probe_is_masked_through_client_pipeline() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -2346,6 +2363,7 @@ async fn burst_invalid_tls_probes_are_masked_verbatim() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -3251,6 +3269,7 @@ async fn relay_connect_error_releases_user_and_ip_before_return() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -3812,6 +3831,7 @@ async fn untrusted_proxy_header_source_is_rejected() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -3882,6 +3902,7 @@ async fn empty_proxy_trusted_cidrs_rejects_proxy_header_by_default() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -3979,6 +4000,7 @@ async fn oversized_tls_record_is_masked_in_generic_stream_pipeline() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -4082,6 +4104,7 @@ async fn oversized_tls_record_is_masked_in_client_handler_pipeline() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -4199,6 +4222,7 @@ async fn tls_record_len_min_minus_1_is_rejected_in_generic_stream_pipeline() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -4302,6 +4326,7 @@ async fn tls_record_len_min_minus_1_is_rejected_in_client_handler_pipeline() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -4408,6 +4433,7 @@ async fn tls_record_len_16384_is_accepted_in_generic_stream_pipeline() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -4509,6 +4535,7 @@ async fn tls_record_len_16384_is_accepted_in_client_handler_pipeline() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_timing_profile_adversarial_tests.rs b/src/proxy/tests/client_timing_profile_adversarial_tests.rs index d8df19f..54ccbe2 100644 --- a/src/proxy/tests/client_timing_profile_adversarial_tests.rs +++ b/src/proxy/tests/client_timing_profile_adversarial_tests.rs @@ -24,6 +24,7 @@ fn make_test_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_tls_clienthello_size_security_tests.rs b/src/proxy/tests/client_tls_clienthello_size_security_tests.rs index 14c24b7..442412d 100644 --- a/src/proxy/tests/client_tls_clienthello_size_security_tests.rs +++ b/src/proxy/tests/client_tls_clienthello_size_security_tests.rs @@ -26,6 +26,7 @@ fn make_test_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_tls_clienthello_truncation_adversarial_tests.rs b/src/proxy/tests/client_tls_clienthello_truncation_adversarial_tests.rs index c757999..1243b83 100644 --- a/src/proxy/tests/client_tls_clienthello_truncation_adversarial_tests.rs +++ b/src/proxy/tests/client_tls_clienthello_truncation_adversarial_tests.rs @@ -27,6 +27,7 @@ fn make_test_upstream_manager(stats: Arc) -> Arc { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/client_tls_mtproto_fallback_security_tests.rs b/src/proxy/tests/client_tls_mtproto_fallback_security_tests.rs index a4d5df8..74ab347 100644 --- a/src/proxy/tests/client_tls_mtproto_fallback_security_tests.rs +++ b/src/proxy/tests/client_tls_mtproto_fallback_security_tests.rs @@ -41,6 +41,7 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/proxy/tests/direct_relay_security_tests.rs b/src/proxy/tests/direct_relay_security_tests.rs index e139923..5972204 100644 --- a/src/proxy/tests/direct_relay_security_tests.rs +++ b/src/proxy/tests/direct_relay_security_tests.rs @@ -1293,6 +1293,7 @@ async fn direct_relay_abort_midflight_releases_route_gauge() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -1400,6 +1401,7 @@ async fn direct_relay_cutover_midflight_releases_route_gauge() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -1522,6 +1524,7 @@ async fn direct_relay_cutover_storm_multi_session_keeps_generic_errors_and_relea upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, @@ -1758,6 +1761,7 @@ async fn negative_direct_relay_dc_connection_refused_fails_fast() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, selected_scope: String::new(), }], @@ -1849,6 +1853,7 @@ async fn adversarial_direct_relay_cutover_integrity() { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, selected_scope: String::new(), }], diff --git a/src/proxy/tests/proxy_shared_state_isolation_tests.rs b/src/proxy/tests/proxy_shared_state_isolation_tests.rs index 7887ef8..b174ee3 100644 --- a/src/proxy/tests/proxy_shared_state_isolation_tests.rs +++ b/src/proxy/tests/proxy_shared_state_isolation_tests.rs @@ -53,6 +53,7 @@ fn new_client_harness() -> ClientHarness { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, + bindtodevice: None, }, weight: 1, enabled: true, diff --git a/src/transport/middle_proxy/ping.rs b/src/transport/middle_proxy/ping.rs index bff088b..85888bd 100644 --- a/src/transport/middle_proxy/ping.rs +++ b/src/transport/middle_proxy/ping.rs @@ -67,6 +67,7 @@ pub fn format_sample_line(sample: &MePingSample) -> String { fn format_direct_with_config( interface: &Option, bind_addresses: &Option>, + bindtodevice: &Option, ) -> Option { let mut direct_parts: Vec = Vec::new(); if let Some(dev) = interface.as_deref().filter(|v| !v.is_empty()) { @@ -75,6 +76,9 @@ fn format_direct_with_config( if let Some(src) = bind_addresses.as_ref().filter(|v| !v.is_empty()) { direct_parts.push(format!("src={}", src.join(","))); } + if let Some(device) = bindtodevice.as_deref().filter(|v| !v.is_empty()) { + direct_parts.push(format!("bindtodevice={device}")); + } if direct_parts.is_empty() { None } else { @@ -231,8 +235,11 @@ pub async fn format_me_route( UpstreamType::Direct { interface, bind_addresses, + bindtodevice, } => { - if let Some(route) = format_direct_with_config(interface, bind_addresses) { + if let Some(route) = + format_direct_with_config(interface, bind_addresses, bindtodevice) + { route } else { detect_direct_route_details(reports, prefer_ipv6, v4_ok, v6_ok) diff --git a/src/transport/socket.rs b/src/transport/socket.rs index b751a30..58d3b97 100644 --- a/src/transport/socket.rs +++ b/src/transport/socket.rs @@ -158,6 +158,56 @@ pub fn create_outgoing_socket_bound(addr: SocketAddr, bind_addr: Option) Ok(socket) } +/// Pin an outgoing socket to a specific Linux network interface via SO_BINDTODEVICE. +#[cfg(target_os = "linux")] +pub fn bind_outgoing_socket_to_device(socket: &Socket, device: &str) -> Result<()> { + use std::io::{Error, ErrorKind}; + use std::os::fd::AsRawFd; + + let name = device.trim(); + if name.is_empty() { + return Err(Error::new( + ErrorKind::InvalidInput, + "bindtodevice must not be empty", + )); + } + + // The kernel expects an interface name buffer with a trailing NUL. + if name.len() >= libc::IFNAMSIZ { + return Err(Error::new( + ErrorKind::InvalidInput, + "bindtodevice exceeds IFNAMSIZ", + )); + } + let mut ifname = [0u8; libc::IFNAMSIZ]; + ifname[..name.len()].copy_from_slice(name.as_bytes()); + + let rc = unsafe { + libc::setsockopt( + socket.as_raw_fd(), + libc::SOL_SOCKET, + libc::SO_BINDTODEVICE, + ifname.as_ptr().cast::(), + (name.len() + 1) as libc::socklen_t, + ) + }; + if rc != 0 { + return Err(Error::last_os_error()); + } + debug!("Pinned outgoing socket to interface {}", name); + Ok(()) +} + +/// Stub for non-Linux targets where SO_BINDTODEVICE is unavailable. +#[cfg(not(target_os = "linux"))] +pub fn bind_outgoing_socket_to_device(_socket: &Socket, _device: &str) -> Result<()> { + use std::io::{Error, ErrorKind}; + Err(Error::new( + ErrorKind::Unsupported, + "bindtodevice is supported only on Linux", + )) +} + /// Get local address of a socket #[allow(dead_code)] pub fn get_local_addr(stream: &TcpStream) -> Option { diff --git a/src/transport/upstream.rs b/src/transport/upstream.rs index 674f0f0..2486b13 100644 --- a/src/transport/upstream.rs +++ b/src/transport/upstream.rs @@ -26,7 +26,9 @@ use crate::stats::Stats; use crate::transport::shadowsocks::{ ShadowsocksStream, connect_shadowsocks, sanitize_shadowsocks_url, }; -use crate::transport::socket::{create_outgoing_socket_bound, resolve_interface_ip}; +use crate::transport::socket::{ + bind_outgoing_socket_to_device, create_outgoing_socket_bound, resolve_interface_ip, +}; use crate::transport::socks::{connect_socks4, connect_socks5}; /// Number of Telegram datacenters @@ -928,6 +930,7 @@ impl UpstreamManager { UpstreamType::Direct { interface, bind_addresses, + bindtodevice, } => { let bind_ip = Self::resolve_bind_address( interface, @@ -943,6 +946,10 @@ impl UpstreamManager { } let socket = create_outgoing_socket_bound(target, bind_ip)?; + if let Some(device) = bindtodevice.as_deref().filter(|value| !value.is_empty()) { + bind_outgoing_socket_to_device(&socket, device).map_err(ProxyError::Io)?; + debug!(bindtodevice = %device, target = %target, "Pinned socket to interface"); + } if let Some(ip) = bind_ip { debug!(bind = %ip, target = %target, "Bound outgoing socket"); } else if interface.is_some() || bind_addresses.is_some() { @@ -1209,6 +1216,7 @@ impl UpstreamManager { UpstreamType::Direct { interface, bind_addresses, + bindtodevice, } => { let mut direct_parts = Vec::new(); if let Some(dev) = interface.as_deref().filter(|v| !v.is_empty()) { @@ -1217,6 +1225,9 @@ impl UpstreamManager { if let Some(src) = bind_addresses.as_ref().filter(|v| !v.is_empty()) { direct_parts.push(format!("src={}", src.join(","))); } + if let Some(device) = bindtodevice.as_deref().filter(|v| !v.is_empty()) { + direct_parts.push(format!("bindtodevice={device}")); + } if direct_parts.is_empty() { "direct".to_string() } else {