feat(socket): add Linux SO_MARK (fwmark) support for underlay sockets (#2288)

Adds a Linux-only socket_mark u32 config flag (CLI: --socket-mark, env:
ET_SOCKET_MARK, TOML/proto: flags.socket_mark, 0 = disabled) that is
applied as SO_MARK to every outbound underlay socket EasyTier creates:
TCP, UDP, QUIC, WebSocket, WireGuard connectors and listeners, plus the
FakeTCP decoy socket. Lets the host policy-route or filter EasyTier
underlay traffic with 'ip rule fwmark ...' or iptables -m mark.

Plumbing mirrors the existing bind_device pattern:
- FlagsInConfig.socket_mark (proto) + default 0 in gen_default_flags
- bind() builder gets a socket_mark arg; setup_socket2_ext calls
  apply_socket_mark which is a no-op for mark=0 and on non-Linux
- TunnelConnector trait gets set_socket_mark(u32) default-no-op method
- IP-based connectors override; create_listener_by_url and the connector
  factory pass mark from global_ctx flags
- QUIC threads mark through QuicEndpointManager::{server,connect}
- WebSocket/FakeTCP/TCP default-bind bypass paths apply mark via
  socket2::SockRef::from(&tokio_socket)
- ForeignNetworkEntry propagates parent socket_mark into its derived ctx

Includes a Linux smoke test plus a CAP_NET_ADMIN-gated test that does a
getsockopt(SO_MARK) round-trip to confirm the kernel applied the value.

SO_MARK requires CAP_NET_ADMIN; ignored silently on non-Linux. FakeTCP's
TUN-written segments are not covered (kernel doesn't tag raw TUN
writes); operators relying on fwmark for FakeTCP must apply an iptables
rule on the FakeTCP TUN device separately.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
X
2026-06-02 21:57:18 -04:00
committed by GitHub
parent df97f3a64d
commit e3ca7ffa54
17 changed files with 348 additions and 25 deletions
+15
View File
@@ -533,6 +533,17 @@ struct NetworkOptions {
)]
bind_device: Option<bool>,
// SO_MARK (fwmark) is a Linux-family kernel feature. Gate the flag out
// entirely on other targets so users on Windows/macOS/BSD don't see a
// `--socket-mark` they can't act on.
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
#[arg(
long,
env = "ET_SOCKET_MARK",
help = t!("core_clap.socket_mark").to_string()
)]
socket_mark: Option<u32>,
#[arg(
long,
env = "ET_ENABLE_KCP_PROXY",
@@ -1126,6 +1137,10 @@ impl NetworkOptions {
.into();
}
f.bind_device = self.bind_device.unwrap_or(f.bind_device);
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
{
f.socket_mark = self.socket_mark.or(f.socket_mark);
}
f.enable_kcp_proxy = self.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy);
f.disable_kcp_input = self.disable_kcp_input.unwrap_or(f.disable_kcp_input);
f.enable_quic_proxy = self.enable_quic_proxy.unwrap_or(f.enable_quic_proxy);