Skip to content

Add opt-in fd protection for upstream UDP sockets (Android VpnService) - #196

Open
hawkff wants to merge 6 commits into
masterking32:mainfrom
hawkff:android-vpnservice-protect-hook
Open

hawkff wants to merge 6 commits into
masterking32:mainfrom
hawkff:android-vpnservice-protect-hook

Conversation

@hawkff

@hawkff hawkff commented Jun 17, 2026 •

Copy link
Copy Markdown

Summary

Adds opt-in fd protection for the client's upstream UDP sockets, for use when the client runs inside an Android VpnService host.

When FD_CONTROL_UNIX_SOCKET is set, the client sends the fd of every new upstream UDP socket to that Unix socket before connect/bind, so the host can call VpnService.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

FD_CONTROL_UNIX_SOCKET = "/path/to/protect.sock"

The key is also available as -fd-control-unix-socket and in JSON configs, like every other client option.

Which sockets

Sent to the protect server:

  • pooled UDP resolver sockets
  • UDP query transports used for session setup, MTU probes and resolver health checks
  • the RX/TX worker sockets

Never sent, because they face local apps and must stay on the VPN side:

  • the local TCP/SOCKS5 listener
  • the SOCKS5 UDP ASSOCIATE relay socket
  • the optional local DNS listener

Wire protocol

The client connects to the Unix SOCK_STREAM path, sends one fd via SCM_RIGHTS with a single 0x01 payload byte, and reads one status byte. 0x01 means 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_KEY is now required only when DATA_ENCRYPTION_METHOD is not 0. With method 0 the 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.

hawkff added 4 commits June 17, 2026 12:02
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.
@hawkff

hawkff commented Jun 18, 2026

Copy link
Copy Markdown
Author

Pushed an additional fix (d481d72): propagate context through the protected UDP resolver dial.

dialUDPResolver previously hardcoded context.Background(), so callers holding a live context (probeConnectionMTU, recheckInactiveResolver) couldn't cancel or time out socket creation — in the protected path a cancelled probe could hold a goroutine for up to the protect timeout (~15s). It now takes ctx (threaded via getUDPConn/newUDPQueryTransport), and the MTU probe/recheck paths pass their live context. Callers without one pass context.Background() (no behavior change); the unprotected path also switched to DialContext so it honors cancellation.

go build/vet/go test ./internal/client/ all clean.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/sockprotect with 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_KEY validation 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.

Comment thread internal/sockprotect/protect_unix.go Outdated
Comment thread internal/client/protected_socket.go
Comment thread internal/sockprotect/protect_unix.go Outdated
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.
@hawkff hawkff changed the title Add Android fd control for resolver sockets Add opt-in fd protection for upstream UDP sockets (Android VpnService) Sep 19, 2026
@masterking32
masterking32 requested a balanced review from Copilot October 20, 2026 11:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Android VpnService interoperability and cross-platform socket behavior still require human validation beyond the supplied stub tests.

No open issues 🎉

3 resolved since last review

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants