Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client_config.toml.simple
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ AUTO_DISABLE_TIMEOUT_SERVERS = true
# Clamped to [1s, 86400s].
AUTO_DISABLE_TIMEOUT_WINDOW_SECONDS = 30.0

# Android VpnService integrations only.
# When set, every upstream resolver/carrier socket fd is sent to this Unix socket
# for VpnService.protect(fd) before connect/bind.
# MASTERDNSVPN_PROTECT_PATH overrides this TOML value.
# FD_CONTROL_UNIX_SOCKET = ""

# If true, payload labels are base-encoded before tunneling.
# Usually keep false unless a specific resolver path behaves better with it.
BASE_ENCODE_DATA = false
Expand Down
2 changes: 1 addition & 1 deletion internal/client/async_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ func (c *Client) StartAsyncRuntime(parentCtx context.Context) error {
// 3. Open dedicated UDP sockets for each RX/TX worker.
conns := make([]*net.UDPConn, 0, c.tunnelRX_TX_Workers)
for i := 0; i < c.tunnelRX_TX_Workers; i++ {
conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
conn, err := c.listenUDPProtected(runtimeCtx, &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
for _, opened := range conns {
_ = opened.Close()
Expand Down
1 change: 1 addition & 0 deletions internal/client/dns_listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func (l *DNSListener) Start(ctx context.Context, ip string, port int) error {
if err != nil {
return err
}
// Local app-facing DNS listener; upstream resolver sockets are protected elsewhere.
conn, err := net.ListenUDP("udp", addr)
if err != nil {
return err
Expand Down
6 changes: 3 additions & 3 deletions internal/client/mtu.go
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ func (c *Client) recheckInactiveResolver(ctx context.Context, conn Connection) {
return
}

transport, err := newUDPQueryTransport(conn.ResolverLabel)
transport, err := c.newUDPQueryTransport(ctx, conn.ResolverLabel)
if err != nil {
return
}
Expand Down Expand Up @@ -734,7 +734,7 @@ func (c *Client) confirmResolverDown(conn *Connection, window time.Duration) boo
return true
}

transport, err := newUDPQueryTransport(conn.ResolverLabel)
transport, err := c.newUDPQueryTransport(context.Background(), conn.ResolverLabel)
if err != nil {
return true
}
Expand Down Expand Up @@ -890,7 +890,7 @@ func (c *Client) runConnectionMTUTest(ctx context.Context, conn Connection, serv
func (c *Client) probeConnectionMTU(ctx context.Context, conn Connection, maxUploadPayload int) (mtuConnectionProbeResult, mtuRejectReason) {
var result mtuConnectionProbeResult

probeTransport, err := newUDPQueryTransport(conn.ResolverLabel)
probeTransport, err := c.newUDPQueryTransport(ctx, conn.ResolverLabel)
if err != nil {
return result, mtuRejectUpload
}
Expand Down
94 changes: 94 additions & 0 deletions internal/client/protected_socket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package client

import (
"context"
"fmt"
"net"
"strings"
"syscall"

"masterdnsvpn-go/internal/sockprotect"
)

func (c *Client) protectPath() string {
if c == nil {
return ""
}
return strings.TrimSpace(c.cfg.FDControlUnixSocket)
}

func (c *Client) protectControl(network, address string, rc syscall.RawConn) error {
path := c.protectPath()
if path == "" {
return nil
}

var protectErr error
if err := rc.Control(func(fd uintptr) {
protectErr = sockprotect.ProtectFD(path, fd)
}); err != nil {
return fmt.Errorf("socket control failed for %s %s: %w", network, address, err)
}
if protectErr != nil {
return fmt.Errorf("failed to protect upstream socket for %s %s: %w", network, address, protectErr)
}
Comment on lines +20 to +34
return nil
}

func (c *Client) dialUDPResolver(ctx context.Context, resolverLabel string) (*net.UDPConn, error) {
if ctx == nil {
ctx = context.Background()
}
if c.protectPath() == "" {
var dialer net.Dialer
conn, err := dialer.DialContext(ctx, "udp", resolverLabel)
if err != nil {
return nil, err
}
udpConn, ok := conn.(*net.UDPConn)
if !ok {
_ = conn.Close()
return nil, fmt.Errorf("unexpected udp resolver connection type %T", conn)
}
return udpConn, nil
}

dialer := net.Dialer{
Control: c.protectControl,
}
conn, err := dialer.DialContext(ctx, "udp", resolverLabel)
if err != nil {
return nil, err
}

udpConn, ok := conn.(*net.UDPConn)
if !ok {
_ = conn.Close()
return nil, fmt.Errorf("unexpected udp resolver connection type %T", conn)
}
return udpConn, nil
}

func (c *Client) listenUDPProtected(ctx context.Context, addr *net.UDPAddr) (*net.UDPConn, error) {
if c.protectPath() == "" {
return net.ListenUDP("udp", addr)
}

if addr == nil {
addr = &net.UDPAddr{}
}
lc := net.ListenConfig{
Control: c.protectControl,
}
packetConn, err := lc.ListenPacket(ctx, "udp", addr.String())
if err != nil {
return nil, err
}

udpConn, ok := packetConn.(*net.UDPConn)
if !ok {
_ = packetConn.Close()
return nil, fmt.Errorf("unexpected udp listener connection type %T", packetConn)
}
return udpConn, nil
}
200 changes: 200 additions & 0 deletions internal/client/protected_socket_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//go:build unix

package client

import (
"context"
"fmt"
"net"
"os"
"syscall"
"testing"
"time"

"golang.org/x/sys/unix"

"masterdnsvpn-go/internal/config"
)

type clientProtectServerResult struct {
fdCount int
err error
}

func startClientStubProtectServer(t *testing.T, status byte) (string, <-chan clientProtectServerResult) {
t.Helper()

path := fmt.Sprintf("/tmp/masterdnsvpn-client-protect-%d.sock", time.Now().UnixNano())
_ = os.Remove(path)
listener, err := net.Listen("unix", path)
if err != nil {
t.Fatalf("Listen unix failed: %v", err)
}
t.Cleanup(func() {
_ = listener.Close()
_ = os.Remove(path)
})

resultCh := make(chan clientProtectServerResult, 4)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
go handleClientProtectConn(conn, status, resultCh)
}
}()

return path, resultCh
}

func handleClientProtectConn(conn net.Conn, status byte, resultCh chan<- clientProtectServerResult) {
defer conn.Close()

unixConn, ok := conn.(*net.UnixConn)
if !ok {
resultCh <- clientProtectServerResult{err: syscall.EINVAL}
return
}

rawConn, err := unixConn.SyscallConn()
if err != nil {
resultCh <- clientProtectServerResult{err: err}
return
}

var (
payload [1]byte
oobn int
readErr error
)
oob := make([]byte, unix.CmsgSpace(4))
if err := rawConn.Read(func(fd uintptr) bool {
_, oobn, _, _, readErr = unix.Recvmsg(int(fd), payload[:], oob, 0)
return true
}); err != nil {
resultCh <- clientProtectServerResult{err: err}
return
}
if readErr != nil {
resultCh <- clientProtectServerResult{err: readErr}
return
}

messages, err := unix.ParseSocketControlMessage(oob[:oobn])
if err != nil {
resultCh <- clientProtectServerResult{err: err}
return
}

fdCount := 0
for _, msg := range messages {
fds, err := unix.ParseUnixRights(&msg)
if err != nil {
resultCh <- clientProtectServerResult{err: err}
return
}
fdCount += len(fds)
for _, receivedFD := range fds {
_ = unix.Close(receivedFD)
}
}

_, err = conn.Write([]byte{status})
resultCh <- clientProtectServerResult{fdCount: fdCount, err: err}
}

func requireClientProtectResult(t *testing.T, resultCh <-chan clientProtectServerResult) clientProtectServerResult {
t.Helper()

select {
case result := <-resultCh:
if result.err != nil {
t.Fatalf("protect server failed: %v", result.err)
}
return result
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for protect server")
return clientProtectServerResult{}
}
}

func TestProtectedResolverQuerySendsFDToProtectServer(t *testing.T) {
resolverConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
if err != nil {
t.Fatalf("ListenUDP resolver failed: %v", err)
}
defer resolverConn.Close()

protectPath, protectResults := startClientStubProtectServer(t, 0x01)
c := New(config.ClientConfig{
FDControlUnixSocket: protectPath,
PacketDuplicationCount: 1,
SetupPacketDuplicationCount: 1,
RX_TX_Workers: 1,
}, nil, nil)

done := make(chan struct{})
go func() {
defer close(done)
buf := make([]byte, 512)
n, addr, err := resolverConn.ReadFromUDP(buf)
if err != nil || n < 2 {
return
}
_, _ = resolverConn.WriteToUDP([]byte{buf[0], buf[1], 0x81, 0x00}, addr)
}()

udpConn, err := c.getUDPConn(context.Background(), resolverConn.LocalAddr().String())
if err != nil {
t.Fatalf("getUDPConn returned error: %v", err)
}
defer udpConn.Close()

response, err := c.exchangeUDPQueryWithConn(udpConn, []byte{0x12, 0x34, 0x01}, time.Second)
if err != nil {
t.Fatalf("exchangeUDPQueryWithConn returned error: %v", err)
}
if len(response) < 2 || response[0] != 0x12 || response[1] != 0x34 {
t.Fatalf("unexpected resolver response: %v", response)
}

result := requireClientProtectResult(t, protectResults)
if result.fdCount != 1 {
t.Fatalf("expected one protected resolver fd, got %d", result.fdCount)
}

select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for resolver traffic")
}
}

func TestListenUDPProtectedSendsFDToProtectServer(t *testing.T) {
protectPath, protectResults := startClientStubProtectServer(t, 0x01)
c := New(config.ClientConfig{FDControlUnixSocket: protectPath}, nil, nil)

conn, err := c.listenUDPProtected(context.Background(), &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
t.Fatalf("listenUDPProtected returned error: %v", err)
}
defer conn.Close()

result := requireClientProtectResult(t, protectResults)
if result.fdCount != 1 {
t.Fatalf("expected one protected listener fd, got %d", result.fdCount)
}
}

func TestListenUDPProtectedFailsOnProtectFailure(t *testing.T) {
protectPath, _ := startClientStubProtectServer(t, 0x00)
c := New(config.ClientConfig{FDControlUnixSocket: protectPath}, nil, nil)

conn, err := c.listenUDPProtected(context.Background(), &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err == nil {
_ = conn.Close()
t.Fatal("expected listenUDPProtected to fail on protect failure")
}
}
1 change: 1 addition & 0 deletions internal/client/socks_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ func (c *Client) handleSocksUDPAssociate(ctx context.Context, conn net.Conn, cli
IP: net.IPv4zero,
Port: 0,
}
// Local SOCKS5 UDP ASSOCIATE relay socket; do not protect app-facing listeners.
udpConn, err := net.ListenUDP("udp", bindAddr)
if err != nil {
_ = c.sendSocksReply(conn, SOCKS5_REPLY_GENERAL_FAILURE, SOCKS5_ATYP_IPV4, net.IPv4zero, 0)
Expand Down
1 change: 1 addition & 0 deletions internal/client/tcp_listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func (l *TCPListener) Start(ctx context.Context, ip string, port int) error {

listeners := make([]net.Listener, 0, len(addrs))
for i, addr := range addrs {
// Local app-facing TCP/SOCKS listener; upstream resolver sockets are protected elsewhere.
listener, err := net.Listen("tcp", addr)
if err != nil {
if i == 0 {
Expand Down
Loading