Repository navigation
Conversation
Add opt-in Unix socket fd protection for upstream client sockets so Android hosts can call VpnService.protect before resolver traffic is used.
DATA_ENCRYPTION_METHOD=0 (None) is valid no-crypto, but finalizeClientConfig unconditionally rejected an empty ENCRYPTION_KEY, so a None-encryption client (for example a profile that defaults to method 0) failed to start. Require the key only when the method is not 0. Add tests for both cases.
ProtectFD ran the full handshake inside RawConn.Control with only a receive timeout, so a protect server that accepted but stalled during connect or send could block upstream socket creation indefinitely. Perform a non-blocking connect bounded by a deadline (poll for writability, then check SO_ERROR) and set SO_SNDTIMEO/SO_RCVTIMEO so every blocking step is time-bounded.
dialUDPResolver hardcoded context.Background(), so callers holding a live context (probeConnectionMTU, recheckInactiveResolver) could not cancel or time out socket creation. In the protected path a cancelled probe would hold a goroutine for up to protectTimeout (~15s total) after cancellation. Thread ctx through dialUDPResolver -> getUDPConn / newUDPQueryTransport and wire the live context from the MTU probe/recheck paths. Callers without a context (sendOneWayDNSQuery, exchangeDNSOverConnection, confirmResolverDown) pass context.Background() (no behavior change). The unprotected path now also uses DialContext so it honors cancellation too. Addresses Greptile review (PR #2). go build/vet/test clean.
|
Pushed an additional fix (d481d72): propagate context through the protected UDP resolver dial.
|
There was a problem hiding this comment.
Pull request overview
This PR adds optional Android VpnService-style FD protection for upstream UDP resolver sockets by sending newly-created UDP socket file descriptors to a Unix-domain “protect” server before connect/bind, controlled via FD_CONTROL_UNIX_SOCKET (or MASTERDNSVPN_PROTECT_PATH env override).
Changes:
- Introduces
internal/sockprotectwith a Unix fd-passing client (SCM_RIGHTS) plus time-bounded connect/send/read handshake and platform stubs. - Wires FD protection into upstream UDP socket creation paths (resolver UDP pool, UDP query transports, async runtime worker sockets).
- Adds config support + documentation + tests for the new setting; also refines
ENCRYPTION_KEYvalidation to only require it when encryption is enabled.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/sockprotect/protect_unix.go | Unix-domain protect handshake implementation (connect + SCM_RIGHTS + status byte). |
| internal/sockprotect/protect_unix_test.go | Verifies fd passing and failure status handling for the protect protocol. |
| internal/sockprotect/protect_other.go | Non-unix stub that errors when protection is configured. |
| internal/sockprotect/cloexec_flag_supported.go | Uses SOCK_CLOEXEC on platforms that support it. |
| internal/sockprotect/cloexec_flag_fallback.go | Fallback when SOCK_CLOEXEC isn’t available. |
| internal/config/client.go | Adds FD_CONTROL_UNIX_SOCKET + env override; adjusts encryption-key requirement logic. |
| internal/config/client_test.go | Tests FD control config parsing/override and updated encryption-key requirement behavior. |
| internal/client/protected_socket.go | Adds protected UDP dial/listen helpers and net.Dialer/ListenConfig wiring. |
| internal/client/protected_socket_unix_test.go | Validates that protected socket creation sends fds and fails when protection fails. |
| internal/client/tunnel_runtime.go | Threads context into UDP resolver pooling + uses protected resolver dialing. |
| internal/client/mtu.go | Uses protected UDP query transport creation for MTU/recheck flows. |
| internal/client/async_runtime.go | Uses protected UDP listener for async RX/TX worker sockets. |
| internal/client/tcp_listener.go | Clarifying comment that local listener sockets are not protected. |
| internal/client/socks_manager.go | Clarifying comment that SOCKS UDP associate sockets are not protected. |
| internal/client/dns_listener.go | Clarifying comment that local DNS listener sockets are not protected. |
| client_config.toml.simple | Documents FD_CONTROL_UNIX_SOCKET and env override behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Replace the raw socket/connect/poll sequence with net.Dialer, WriteMsgUnix and one deadline for the whole handshake. The net package already sets CLOEXEC, which removes the two build-tagged flag files. The error for a rejected fd now includes the status byte. On the client side, dialUDPResolver and listenUDPProtected always go through one Control hook that is a no-op when FD_CONTROL_UNIX_SOCKET is empty, instead of keeping separate protected and unprotected paths.
The -fd-control-unix-socket flag and the JSON config already cover per-launch overrides, so the environment variable only duplicated them. Remove the unused json struct tag, make the key an active entry in the sample config, and add it to the client configuration tables in all README translations. Note that ENCRYPTION_KEY is optional when DATA_ENCRYPTION_METHOD is 0.



Summary
Adds opt-in fd protection for the client's upstream UDP sockets, for use when the client runs inside an Android
VpnServicehost.When
FD_CONTROL_UNIX_SOCKETis set, the client sends the fd of every new upstream UDP socket to that Unix socket before connect/bind, so the host can callVpnService.protect(fd). Without this, resolver traffic from a client running inside the VPN process loops back into the tunnel. When the key is empty (the default), socket creation is unchanged.Configuration
The key is also available as
-fd-control-unix-socketand in JSON configs, like every other client option.Which sockets
Sent to the protect server:
Never sent, because they face local apps and must stay on the VPN side:
Wire protocol
The client connects to the Unix
SOCK_STREAMpath, sends one fd viaSCM_RIGHTSwith a single0x01payload byte, and reads one status byte.0x01means success. Any other status, a short read, a timeout or a connect/send error fails socket creation; there is no fallback to an unprotected socket. One deadline of 5 seconds bounds the whole handshake. This is the framing used by libneko/Matsuri protect servers.Also in this PR
ENCRYPTION_KEYis now required only whenDATA_ENCRYPTION_METHODis not0. With method0the codec does not use the key, but config loading still rejected an empty value.Compatibility
Default-off. Desktop and server setups see no change. On platforms without Unix fd passing (Windows), setting the key makes upstream socket creation fail with a clear error.