With a SIP003 plugin, every proxied connection is a loopback connection
from sslocal to the plugin. After the client half-closes, sslocal shuts
down its write side and waits for EOF from the plugin. Many plugins
(simple-obfs, v2ray-plugin, kcptun/smux, ...) never close their side, so
the socket stays in FIN_WAIT_2 forever (tcp_fin_timeout only applies to
orphaned sockets, keepalive is answered by the live plugin), leaking an
ephemeral port and fd per connection until local ports are exhausted.
- CopyBidirectional now fails with TimedOut if the remaining direction
stays idle for HALF_CLOSE_IDLE_TIMEOUT (60s) after the other direction
has been shut down. The timer restarts on every transferred byte, so
long downloads after a half-close keep working.
- Plugin::wait_started no longer spins on connect(), and rejects a
self-connected socket (src == dst) that would otherwise be mistaken
for a started plugin.
* feat(acl): add AclHandle to swap ACL rules atomically at runtime
AccessControl is immutable after loading, and ServiceContext used to store
it directly, so rules were frozen for the lifetime of the process.
Wrap the active rules in an ArcSwap-based AclHandle. Readers take a cheap
Arc snapshot which stays valid across a swap, enabling hot-reloading
without interrupting the service. reload() re-parses the retained file
path and only swaps the new rules in when parsing succeeds.
* feat(local): hot-reload the global ACL file through ServiceContext
Store the ACL as an AclHandle in ServiceContext. set_acl keeps its
signature and wraps the rules in a handle; cloned contexts share it, so
every listener sees the same swappable rules. acl() now returns an
Arc<AccessControl> snapshot instead of a reference, which cannot be
preserved across an atomic swap.
ServiceContext::reload_acl() re-reads the ACL file, swaps the new rules
in atomically and clears the DNS relay's reverse-lookup cache, which
caches decisions derived from the old rules. On failure the previous
rules stay active.
Server keeps the global context and exposes acl_reload_context() so
embedders can trigger reloads. Contexts with a private ACL keep their
own handle and are not covered by the global reload.
* feat(local): reload the ACL file on SIGUSR2 in sslocal
sslocal already reloads its server list from the config file on SIGUSR1.
Add a second reload trigger on SIGUSR2 for the ACL file, so routing
rules can be updated without restarting the process or interrupting
established connections.
Like the SIGUSR1 reloader, the task is Unix-only and compiles away on
Windows. ssservice's local mode shares this module and inherits it.
Document both signals in the README and cover the reload with an
integration test that verifies new connections are routed by the new
rules after each reload.
* refactor(acl): address review feedback for ACL hot-reload
- Rename `AclHandle` to `AccessControlHandle`, aligning the name with `AccessControl`
- Rename `Server::acl_reload_context` to `Server::context`, so the shared global context can be reused for other runtime operations
- Fold reload logging into `AccessControlHandle::reload`, keeping `ServiceContext::reload_acl` thin
- Record the config file and the ACL file as absolute paths at load time, so both SIGUSR1 and SIGUSR2 reloads keep working after daemonizing (which `chdir`s to `/`); symlinks are deliberately preserved by resolving with cwd instead of canonicalizing
* refactor(local): drop needless pending() in the non-Unix ACL reload task stub
The task may complete immediately: the main loop already tolerates early task exit, mirroring the existing Windows behavior of the SIGUSR1 reload task. This also releases the ServiceContext clone right away instead of holding it forever.
---------
Co-authored-by: ty <zonyitoo@gmail.com>
SIP022 requires that in a request header, either initial payload or
padding MUST be present. When the first payload is empty (e.g. handshake
triggered by an empty write for server-first protocols), the client
could pick padding 0 with probability 1/901, generating a header that
servers MUST reject per the spec, causing ~1/901 handshakes to fail
against ssserver built from the same crate.
Pick the padding size from 1..=AEAD2022_MAX_PADDING_SIZE instead.
fix#2184
EncryptedWriter.poll_write_encrypted keeps the encrypted buffer and its
on-wire offset across Poll::Pending, but returned the length of the
buffer passed on the *retried* call instead of the plaintext length that
was actually assembled into the pending frame.
Tokio's copy/copy_bidirectional may append freshly read data to the
input slice while the writer is Pending, so the reported count could
exceed what was actually encrypted and sent, silently dropping the
appended bytes.
Fixes#2175
smoltcp 0.13 (PR smoltcp-rs/smoltcp#1106) made automatic ICMP echo
replies opt-in via the "auto-icmp-echo-reply" feature flag. It is only
part of smoltcp's default features, which this crate disables with
default-features = false. Since the smoltcp 0.13 upgrade, the Tun local
silently drops ICMP Echo Requests and no longer responds to ping.
Fixes#2178
- Replace catch-all arm in OutboundProxyStream::try_into_tcp with
explicit cfg-gated variant arms, fixing unreachable_patterns warning
when local-http features are disabled
- Gate utils module behind local/server features, fixing dead_code
warning for ServerHandle when neither module is compiled
Add a dedicated config option for
inbound (listener) UDP sockets, instead of reusing the outbound option.
- SSConfig / SSServerExtConfig: new
JSON field (global + per-server)
- Config / ServerInstanceConfig: new field, defaults to false
- server/manager/local: accept_opts.udp.allow_fragmentation now sourced
from the inbound option; per-server override split into inbound/outbound
- README: document the new option in both JSON examples
- tests/udp.rs: fix unused import of AcceptOpts on non-Linux platforms
`net/sys/unix/bsd/mod.rs` already routes macos, ios, watchos and tvos
to `bsd/macos.rs`, and that module unconditionally imports
`net::udp::{BatchRecvMessage, BatchSendMessage}`. But the nine cfg
gates in `net/udp.rs` list only macos and ios among the Apple
platforms, so on watchos and tvos those items are configured out while
the importer is still compiled -- E0432, the crate does not build.
Add watchos and tvos to the nine gates so the two lists agree.
Switches the cryptographic backend so that ring is no longer pulled
into shadowsocks-rust on any supported target.
* shadowsocks-crypto: use the new aws-lc-rs backend in place of
the ring feature
* tokio-rustls: select the aws_lc_rs provider instead of ring.
* hickory-resolver: use tls-aws-lc-rs, https-aws-lc-rs and
h3-aws-lc-rs in place of the *-ring variants.
The only remaining ring path in the dependency tree is via quinn-proto on
wasm32-unknown-unknown, which aws-lc-rs cannot target and which
shadowsocks does not support.
Phase 2 of the outbound proxy refactor. The new
OutboundProxyDatagram type implements DatagramSocket / DatagramSend /
DatagramReceive on top of a freshly-bound local UDP socket plus one
SOCKS5 UDP ASSOCIATE per chain hop. Each datagram on the wire carries
N nested UdpAssociateHeader frames (one per hop), the inner-most one
addressing the ss-server's UDP external endpoint.
Per-hop control connections are built by reusing the existing TCP
chain builder (connect_chain_for_udp_associate) so all of the
shadowsocks DNS resolver / ConnectOpts behaviour still applies.
Integration:
* OutboundProxyClient::associate_udp + supports_udp().
* OutboundProxyClient::supports_udp() returns true iff every hop is
SOCKS5; HTTP/HTTPS hops cannot transport UDP and downgrade the
whole chain to 'unsupported'.
* local::net::udp::association::create_proxied_socket dispatches
based on supports_udp(): when true the per-association socket is
ProxySocket<OutboundProxyDatagram> wrapping the chained datagram;
otherwise we keep the previous direct ProxySocket<ShadowUdpSocket>.
* When the chain is configured but unable to relay UDP, sslocal /
ssserver emit a single startup WARN ('UDP traffic will bypass the
chain') instead of failing — runtime traffic transparently falls
back to a direct connection to the ss-server.
OutboundProxyStream gains try_into_tcp() so SOCKS5-only chains can
recover the bare TcpStream as keep-alive control connections —
Sync-friendly storage required because the HTTP CONNECT variant
internally holds a hyper::upgrade::Upgraded which is !Sync.
Replace the previous parallel outbound proxy implementations
(net::outbound_proxy + local::net::tcp::outbound_proxy) with a single
self-contained module at net::outbound, exposing OutboundProxyClient,
OutboundProxyStream, Socks5Auth, HttpProxyAuth and a TcpDialer trait.
Highlights:
* OutboundProxyStream is now a closed-set enum (Bypassed / Https / Http)
with statically-dispatched poll_* — no more Box<dyn ProxyStream>. The
HTTPS variant boxes its inner OutboundProxyStream solely to break the
recursive type instantiation required by the TLS libraries; this is
not dynamic dispatch.
* HTTP CONNECT client is rebuilt on top of hyper::client::conn::http1
+ hyper::upgrade. The hand-rolled status-line parser (which could
drop bytes that arrived in the same read() as the header terminator,
did not handle 100-Continue, etc.) is gone.
* Auth parameters are now strongly typed:
Socks5Auth::{None, UsernamePassword{..}}
HttpProxyAuth::{None, Basic{..}}
Both enums are #[non_exhaustive] for forward compatibility.
* AutoProxyClientStream's ProxiedViaChain variant is folded back into a
single Proxied(ProxyClientStream<MonProxyStream<OutboundTransport>>)
variant, with OutboundTransport = Direct(TcpStream) | Chained(...).
* ServiceContext (both server and local) now caches an
Arc<OutboundProxyClient> built once at set_outbound_proxies time
instead of re-parsing the chain on every connection.
* The standalone Socks5TcpClient / Socks5UdpClient implementations
previously duplicated across net::socks5_client and
local::socks::client::socks5 are unified into the latter location;
net::outbound::socks5 only keeps the chain-friendly Socks5Negotiator.
* When UDP is enabled but the configured chain contains a non-SOCKS5
hop, sslocal/ssserver emits a single startup warning. Phase 2 will
add UDP relay support for SOCKS5-only chains.
BREAKING CHANGE: the following items have moved or been removed:
* crate::net::outbound_proxy — removed; use crate::net::OutboundProxyClient
* crate::net::http_connect::HttpConnectClient — moved to
crate::net::HttpConnectClient (only available with the local-http feature)
* crate::net::Socks5TcpClient (the chain-mixing wrapper) — split: the
pure SOCKS5 client is now crate::local::socks::client::socks5::Socks5TcpClient;
the in-band negotiator is crate::net::Socks5Negotiator
* crate::local::net::tcp::outbound_proxy — removed
* ServiceContext::outbound_proxies() — replaced by outbound_client()
returning Option<&Arc<OutboundProxyClient>>
After an AEAD2022 handshake with a ServerUserManager, the authenticated
user's raw key is available inside DecryptedReader but not reachable
from the public API. Expose it via:
CryptoStream::user_key() -> Option<&[u8]>
ProxyServerStream::user_key() -> Option<&[u8]>
This lets callers (e.g. clash-rs) identify which multi-user identity
was authenticated without having to manually peek and re-derive the
identity subkey via BLAKE3 + AES-256-ECB before the handshake.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract HttpConnectClient to dedicated http_connect.rs module
- Add conduct_handshake_and_connect method to Socks5TcpClient for stream-based SOCKS5 negotiation
- Refactor outbound_proxy to use Socks5TcpClient and HttpConnectClient methods instead of direct protocol implementation
- Update module exports in net/mod.rs
- Move HTTP CONNECT logic to shared outbound_proxy helper module
- Simplify Socks5TcpClient to only handle SOCKS5 protocol, remove OutboundProxyAuth dependency
- Extract proxy chain utilities into separate net/outbound_proxy.rs with HTTP/HTTPS support
- Add UDP mode warnings in local and server startup when proxy chain is configured
- Share ProxyStream trait between modules for better code organization
- Gate HTTPS proxy support with feature flags (local-http-native-tls or local-http-rustls)