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-03 09:57:18 +08:00
committed by GitHub
co-authored by Claude
parent df97f3a64d
commit e3ca7ffa54
17 changed files with 348 additions and 25 deletions
+27 -2
View File
@@ -81,6 +81,7 @@ static TRUSTED_PROXIES: LazyLock<Vec<IpNetwork>> = LazyLock::new(|| {
pub struct WsTunnelListener {
addr: url::Url,
listener: Option<TcpListener>,
socket_mark: Option<u32>,
}
impl WsTunnelListener {
@@ -88,9 +89,14 @@ impl WsTunnelListener {
WsTunnelListener {
addr,
listener: None,
socket_mark: None,
}
}
pub fn set_socket_mark(&mut self, socket_mark: Option<u32>) {
self.socket_mark = socket_mark;
}
async fn try_accept(&self, stream: TcpStream) -> Result<Box<dyn Tunnel>, TunnelError> {
let peer_addr = stream.peer_addr()?;
let mut remote_addr =
@@ -164,7 +170,11 @@ impl TunnelListener for WsTunnelListener {
self.listener = None;
let addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?;
let listener = bind::<TcpListener>().addr(addr).only_v6(true).call()?;
let listener = bind::<TcpListener>()
.addr(addr)
.only_v6(true)
.maybe_socket_mark(self.socket_mark)
.call()?;
self.addr
.set_port(Some(listener.local_addr()?.port()))
@@ -201,6 +211,7 @@ pub struct WsTunnelConnector {
resolved_addr: Option<SocketAddr>,
bind_addrs: Vec<SocketAddr>,
socket_mark: Option<u32>,
}
impl WsTunnelConnector {
@@ -211,6 +222,7 @@ impl WsTunnelConnector {
resolved_addr: None,
bind_addrs: vec![],
socket_mark: None,
}
}
@@ -274,6 +286,10 @@ impl WsTunnelConnector {
} else {
TcpSocket::new_v6()?
};
crate::tunnel::common::apply_socket_mark(
&socket2::SockRef::from(&socket),
self.socket_mark,
)?;
Self::connect_with(self.addr.clone(), addr, socket).await
}
@@ -285,7 +301,12 @@ impl WsTunnelConnector {
for bind_addr in self.bind_addrs.iter() {
tracing::info!(?bind_addr, ?addr, "bind addr");
match bind().addr(*bind_addr).only_v6(true).call() {
match bind()
.addr(*bind_addr)
.only_v6(true)
.maybe_socket_mark(self.socket_mark)
.call()
{
Ok(socket) => futures.push(Self::connect_with(self.addr.clone(), addr, socket)),
Err(error) => {
tracing::error!(?bind_addr, ?addr, ?error, "bind addr fail");
@@ -327,6 +348,10 @@ impl TunnelConnector for WsTunnelConnector {
fn set_resolved_addr(&mut self, addr: SocketAddr) {
self.resolved_addr = Some(addr);
}
fn set_socket_mark(&mut self, socket_mark: Option<u32>) {
self.socket_mark = socket_mark;
}
}
#[cfg(test)]