Skip to content
Merged
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
17 changes: 17 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,23 @@ barrier. Canceling one caller still ends only its wait, not the shared physical
attempt. An operation that ignores cancellation can delay closure, and best-effort
observability callbacks remain outside this join guarantee.

The native client resolves the relay endpoint before asking the QUIC library to
allocate its owned UDP transport. Address failures therefore do not retain UDP
sockets until garbage collection. The numeric endpoint preserves mapped IPv6
addresses and zones; the already configured TLS ServerName still owns certificate
verification. Successful connections retain the library's single-use transport
and socket cleanup behavior.

The native TLS client closes its setup registration gate before cancellation and
waits for in-progress TCP/TLS/protocol setup to dispose of unreturned connections.
Concurrent client `Close` calls share the completion barrier. An application
closing an already returned stream remains registered until its socket close
finishes, so client closure also joins that close. This does not bind successfully
returned streams to their caller's setup context or change TCP half-close behavior.
An operation or TLS callback that ignores cancellation can delay closure.
TLS callbacks must not synchronously close the same client from inside a setup
that client closure needs to join.

## Web-cover TCP flow

The web-cover server binds TCP and UDP on the same numeric port. Its TCP side
Expand Down
195 changes: 195 additions & 0 deletions internal/tunnel/native_quic_addr_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//go:build linux || darwin

package tunnel

import (
"context"
"crypto/tls"
"errors"
"net"
"os"
"os/exec"
"runtime"
"runtime/debug"
"strings"
"syscall"
"testing"
"time"
)

const (
nativeQUICAddrResourceChild = "AUTOCAR_NATIVE_QUIC_ADDR_RESOURCE_CHILD"
nativeQUICAddrResourceScanLimit = 4096
)

// The child temporarily disables automatic GC, not the parent test process.
// This distinguishes synchronous ownership from eventual netFD finalization;
// it does not claim that the old error path permanently leaks descriptors.
func TestNativeQUICAddressErrorDoesNotRetainUDPDescriptors(t *testing.T) {
if os.Getenv(nativeQUICAddrResourceChild) == "1" {
nativeQUICAddrResourceChildTest(t)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, os.Args[0],
"-test.run=^TestNativeQUICAddressErrorDoesNotRetainUDPDescriptors$",
"-test.count=1", "-test.v", "-test.timeout=6s")
cmd.Env = append(os.Environ(), nativeQUICAddrResourceChild+"=1")
out, err := cmd.CombinedOutput()
for _, line := range strings.Split(strings.TrimSuffix(string(out), "\n"), "\n") {
t.Logf("isolated child | %s", line)
}
if ctx.Err() != nil {
t.Fatalf("isolated resource probe exceeded its budget: %v", ctx.Err())
}
if err != nil {
t.Fatalf("isolated resource probe failed: %v", err)
}
}

func nativeQUICAddrResourceChildTest(t *testing.T) {
const attempts = 32
// Listen/Close is local socket allocation only; it warms runtime netpoll
// before counting descriptors and sends no packet to any destination.
warm, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero})
if err != nil {
t.Fatal(err)
}
raw, rawErr := warm.SyscallConn()
var warmFD uintptr
var warmAddress syscall.Sockaddr
if rawErr == nil {
var addressErr error
rawErr = raw.Control(func(fd uintptr) {
warmFD = fd
warmAddress, addressErr = syscall.Getsockname(int(fd))
})
if rawErr == nil {
rawErr = addressErr
}
}
if err := warm.Close(); err != nil {
t.Fatal(err)
}
if rawErr != nil || warmFD >= nativeQUICAddrResourceScanLimit {
t.Fatalf("warm socket outside the bounded descriptor scan: fd=%d err=%v", warmFD, rawErr)
}
switch warmAddress.(type) {
case *syscall.SockaddrInet4, *syscall.SockaddrInet6:
default:
t.Fatalf("warm UDP socket is not an INET address: %T", warmAddress)
}
runtime.GC()
previousGC := debug.SetGCPercent(-1)
defer debug.SetGCPercent(previousGC)
before := nativeQUICAddrResourceSnapshot(t)
t.Logf("automatic_gc=disabled attempts=%d before_open=%d before_udp=%d", attempts, before.open, before.udp)

for i := 0; i < attempts; i++ {
// The numeric out-of-range port is rejected before IP lookup. No DNS
// query, QUIC handshake, or connection to the valid TCP target occurs.
client, err := NewClient(ClientConfig{
ServerAddress: "127.0.0.1:65536", Token: testToken,
TLSConfig: &tls.Config{ServerName: "127.0.0.1"},
QUICDialTimeout: time.Second,
})
if err != nil {
t.Fatalf("attempt %d constructor rejected the deferred address case: %v", i, err)
}
dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Second)
conn, dialErr := client.DialContext(dialCtx, "tcp", "127.0.0.1:1")
cancelDial()
if conn != nil {
_ = conn.Close()
t.Errorf("attempt %d returned a connection for an invalid server port", i)
}
if err := client.Close(); err != nil {
t.Errorf("attempt %d client Close: %v", i, err)
}
var addressErr *net.AddrError
if !errors.As(dialErr, &addressErr) || addressErr.Err != "invalid port" || addressErr.Addr != "65536" {
t.Errorf("attempt %d did not preserve the numeric address error: %T %v", i, dialErr, dialErr)
}
}
// Close has joined every default physical attempt. Any UDP descriptor
// still retained here cannot be attributed to an unfinished dial worker.
afterClose := nativeQUICAddrResourceSnapshot(t)
retained := afterClose.udp - before.udp
t.Logf("after_client_close_open=%d after_client_close_udp=%d retained_udp=%d", afterClose.open, afterClose.udp, retained)
if retained != 0 {
t.Errorf("client Close left %d UDP descriptors retained after %d completed address failures; cleanup must not depend on GC", retained, attempts)
}

// Preserve this diagnostic even when the primary no-retention assertion
// fails: a subsequent GC may reclaim the old path's unreachable netFDs.
runtime.GC()
gcDeadline := time.Now().Add(2 * time.Second)
afterGC := nativeQUICAddrResourceSnapshot(t)
for afterGC.udp != before.udp && time.Now().Before(gcDeadline) {
runtime.Gosched()
time.Sleep(5 * time.Millisecond)
afterGC = nativeQUICAddrResourceSnapshot(t)
}
t.Logf("after_explicit_gc_open=%d after_explicit_gc_udp=%d post_gc_udp_delta=%d", afterGC.open, afterGC.udp, afterGC.udp-before.udp)
if afterGC.udp != before.udp {
t.Errorf("explicit GC did not restore UDP descriptor count within the diagnostic budget: before=%d after=%d", before.udp, afterGC.udp)
}
}

type nativeQUICAddrResources struct {
open int
udp int
}

func nativeQUICAddrResourceSnapshot(t *testing.T) nativeQUICAddrResources {
t.Helper()
var limit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil {
t.Fatal(err)
}
// The isolated child has only runtime/stdio descriptors plus at most 32
// sockets. Scan a fixed bounded range; do not raise or lower host limits.
bound := nativeQUICAddrResourceScanLimit
if limit.Cur < nativeQUICAddrResourceScanLimit {
bound = int(limit.Cur)
}
if bound < 128 {
t.Fatalf("descriptor limit too small for the bounded child probe: %d", bound)
}
var resources nativeQUICAddrResources
for fd := 0; fd < bound; fd++ {
_, _, errno := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), uintptr(syscall.F_GETFD), 0)
if errno == syscall.EBADF {
continue
}
if errno != 0 {
t.Fatalf("F_GETFD(%d): %v", fd, errno)
}
resources.open++
kind, err := syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_TYPE)
if err == nil && kind == syscall.SOCK_DGRAM {
address, err := syscall.Getsockname(fd)
// Explicit GC can run netFD finalizers between these read-only
// descriptor syscalls; a just-closed descriptor is not a failure.
if errors.Is(err, syscall.EBADF) {
continue
}
// SOCK_DGRAM does not imply INET UDP. Non-INET control or
// runtime descriptors may not support this address query; do
// not count them as UDP. The owned warm INET socket above
// verifies that its address query works without this filter.
if errors.Is(err, syscall.EOPNOTSUPP) || errors.Is(err, syscall.EAFNOSUPPORT) || errors.Is(err, syscall.ENOTSOCK) {
continue
}
if err != nil {
t.Fatalf("getsockname(%d): %v", fd, err)
}
switch address.(type) {
case *syscall.SockaddrInet4, *syscall.SockaddrInet6:
resources.udp++
}
}
}
return resources
}
83 changes: 83 additions & 0 deletions internal/tunnel/native_quic_addr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package tunnel

import (
"context"
"net/netip"
"testing"
)

func TestResolveNativeQUICAddress(t *testing.T) {
for _, tc := range []struct {
name string
address string
want string
zone string
mapped bool
empty bool
}{
{name: "ipv4", address: "127.0.0.1:443", want: "[::ffff:127.0.0.1]:443", mapped: true},
{name: "ipv6", address: "[::1]:443", want: "[::1]:443"},
{name: "ipv6_canonical", address: "[2001:DB8:0:0:0:0:0:1]:443", want: "[2001:db8::1]:443"},
{name: "mapped_ipv6", address: "[::ffff:127.0.0.1]:443", want: "[::ffff:127.0.0.1]:443", mapped: true},
{name: "ipv6_zone", address: "[fe80::1%audit-zone]:443", want: "[fe80::1%audit-zone]:443", zone: "audit-zone"},
{name: "mapped_ipv6_zone", address: "[::ffff:127.0.0.1%audit-zone]:443", want: "[::ffff:127.0.0.1%audit-zone]:443", zone: "audit-zone", mapped: true},
{name: "empty_host", address: ":443", want: ":443", empty: true},
{name: "empty_address", address: "", want: ":0", empty: true},
// This uses a literal IP and the standard local UDP service name;
// neither an external hostname nor an interface is resolved.
{name: "named_port", address: "127.0.0.1:domain", want: "[::ffff:127.0.0.1]:53", mapped: true},
{name: "ipv4_zero_port", address: "127.0.0.1:0", want: "[::ffff:127.0.0.1]:0", mapped: true},
{name: "ipv6_zero_port", address: "[::1]:0", want: "[::1]:0"},
{name: "empty_host_zero_port", address: ":0", want: ":0", empty: true},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := resolveNativeQUICAddress(tc.address)
if err != nil {
t.Fatal(err)
}
if got != tc.want {
t.Errorf("numeric authority = %q, want %q", got, tc.want)
}
if tc.empty {
return
}
// Literal parsing proves that the second DialAddr resolution need
// not perform DNS. In particular IPv4%zone is not a valid literal.
endpoint, err := netip.ParseAddrPort(got)
if err != nil {
t.Fatalf("resolved authority is not a literal endpoint: %v", err)
}
if endpoint.Addr().Zone() != tc.zone || endpoint.Addr().Is4In6() != tc.mapped {
t.Errorf("family/zone changed: endpoint=%v mapped=%t zone=%q", endpoint, endpoint.Addr().Is4In6(), endpoint.Addr().Zone())
}
})
}
for _, tc := range []struct{ name, address string }{
{"missing_port", "127.0.0.1"},
{"unbracketed_ipv6", "::1:443"},
{"high_port", "127.0.0.1:65536"},
{"negative_port", "127.0.0.1:-1"},
{"extra_port_separator", "127.0.0.1:443:1"},
{"missing_ipv6_bracket", "[::1:443"},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := resolveNativeQUICAddress(tc.address)
if err == nil || got != "" {
t.Errorf("malformed address resolved: got=%q err=%v", got, err)
}
})
}
}

func TestDialNativeQUICAddrRejectsNilTLS(t *testing.T) {
// The default NewClient path always supplies a verifying TLS config.
// This private-wrapper boundary is rejected before allocating a socket,
// without dialing or observing any external peer.
conn, err := dialNativeQUICAddr(context.Background(), "127.0.0.1:1", nil, nil)
if conn != nil || err == nil || err.Error() != "quic: tls.Config not set" {
if conn != nil {
_ = conn.CloseWithError(applicationShutdown, "unexpected nil TLS result")
}
t.Fatalf("nil TLS result: conn=%v err=%v", conn, err)
}
}
Loading
Loading