Compare commits

...

19 Commits

Author SHA1 Message Date
Ginder Singh
d7904580ed remove unused code. 2026-03-19 15:16:44 -04:00
Ginder Singh
593805bf6f ios support. 2026-03-19 03:55:25 -04:00
Ginder Singh
ae37c56467 quic block 2026-03-19 00:49:09 -04:00
Ginder Singh
41597609c8 tcp/ip stack + firewall mode. 2026-03-19 00:24:35 -04:00
Ginder Singh
1f619a669a tcp/ip stack + firewall mode. 2026-03-19 00:24:07 -04:00
Cuong Manh Le
37c3331559 Merge pull request #285 from Control-D-Inc/cuonglm-patch-1 2026-03-06 22:16:47 +07:00
Cuong Manh Le
f334993f79 Fix typo in README usage section 2026-01-22 22:15:02 +07:00
Cuong Manh Le
3ca559e5a4 Merge pull request #264 from Control-D-Inc/release-branch-v1.4.7
Release branch v1.4.7
2025-10-07 01:02:39 +07:00
Cuong Manh Le
0e3f764299 feat: add --rfc1918 flag for explicit LAN client support
Make RFC1918 listener spawning opt-in via --rfc1918 flag instead of automatic behavior.
This allows users to explicitly control when ctrld listens on private network addresses
to receive DNS queries from LAN clients, improving security and configurability.

Refactor network interface detection to better distinguish between physical and virtual
interfaces, ensuring only real hardware interfaces are used for RFC1918 address binding.
2025-09-25 16:45:56 +07:00
Cuong Manh Le
e52402eb0c Upgrade quic-go to v0.54.0 2025-09-25 16:45:05 +07:00
Cuong Manh Le
2133f31854 docs: add known issues documentation for Darwin 15.5 upgrade issue
Documents the self-upgrade issue on macOS Darwin 15.5 affecting
ctrld v1.4.2+ and provides workarounds for affected users.
2025-09-25 16:44:54 +07:00
Ginder Singh
a198a5cd65 start mobile library with provision id and custom hostname. 2025-09-25 16:44:39 +07:00
Cuong Manh Le
eb2b231bd2 Merge pull request #254 from Control-D-Inc/release-branch-v1.4.6
Release branch v1.4.6
2025-08-22 04:08:56 +07:00
Jared Quick
7af29cfbc0 Add OPNsense new lease file
Signed-off-by: Jared Quick <jared.quick@salesforce.com>
2025-08-20 18:19:35 +07:00
Cuong Manh Le
ce1a165348 .github/workflows: bump go version to 1.24.x 2025-08-15 23:33:23 +07:00
Cuong Manh Le
fd48e6d795 fix: ensure upstream health checks can handle large DNS responses
- Add UpstreamConfig.VerifyMsg() method with proper EDNS0 support
- Replace hardcoded DNS messages in health checks with standardized verification method
- Set EDNS0 buffer size to 4096 bytes to handle large DNS responses
- Add test case for legacy resolver with extensive extra sections
2025-08-15 22:55:47 +07:00
Cuong Manh Le
d71d1341b6 refactor(prog): move network monitoring outside listener loop
Move the network monitoring goroutine initialization outside the listener
loop to prevent it from being started multiple times. Previously, the
network monitoring was started once per listener during first run, which
was unnecessary and could lead to multiple monitoring instances.

The change ensures network monitoring is started only once per program
execution cycle, improving efficiency and preventing potential resource
waste from duplicate monitoring goroutines.

- Extract network monitoring goroutine from listener loop
- Start network monitoring once per run cycle instead of per listener
- Maintain same functionality while improving resource usage
2025-08-12 16:49:05 +07:00
Cuong Manh Le
21855df4af fix: correct Windows API constants to fix domain join detection
The function was incorrectly identifying domain-joined status due to wrong
constant values, potentially causing false negatives for domain-joined machines.
2025-08-12 16:48:10 +07:00
Cuong Manh Le
66e2d3a40a refactor: move network monitoring to separate goroutine
- Move network monitoring initialization out of serveDNS() function
- Start network monitoring in a separate goroutine during program startup
- Remove context parameter from monitorNetworkChanges() as it's not used
- Simplify serveDNS() function signature by removing unused context parameter
- Ensure network monitoring starts only once during initial run, not on reload

This change improves separation of concerns by isolating network monitoring
from DNS serving logic, and prevents potential issues with multiple
monitoring goroutines if starting multiple listeners.
2025-08-12 16:46:57 +07:00
34 changed files with 2308 additions and 126 deletions

View File

@@ -9,7 +9,7 @@ jobs:
fail-fast: false
matrix:
os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
go: ["1.23.x"]
go: ["1.24.x"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
@@ -21,6 +21,6 @@ jobs:
- run: "go test -race ./..."
- uses: dominikh/staticcheck-action@v1.3.1
with:
version: "2024.1.1"
version: "2025.1"
install-go: false
cache-key: ${{ matrix.go }}

View File

@@ -100,7 +100,7 @@ docker build -t controldns/ctrld . -f docker/Dockerfile
# Usage
The cli is self documenting, so free free to run `--help` on any sub-command to get specific usages.
The cli is self documenting, so feel free to run `--help` on any sub-command to get specific usages.
## Arguments
```

View File

@@ -178,7 +178,15 @@ func RunMobile(appConfig *AppConfig, appCallback *AppCallback, stopCh chan struc
noConfigStart = false
homedir = appConfig.HomeDir
verbose = appConfig.Verbose
cdUID = appConfig.CdUID
if appConfig.ProvisionID != "" {
cdOrg = appConfig.ProvisionID
}
if appConfig.CustomHostname != "" {
customHostname = appConfig.CustomHostname
}
if appConfig.CdUID != "" {
cdUID = appConfig.CdUID
}
cdUpstreamProto = appConfig.UpstreamProto
logPath = appConfig.LogPath
run(appCallback, stopCh)

View File

@@ -189,6 +189,7 @@ func initRunCmd() *cobra.Command {
runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
_ = runCmd.Flags().MarkHidden("iface")
runCmd.Flags().StringVarP(&cdUpstreamProto, "proto", "", ctrld.ResolverTypeDOH, `Control D upstream type, either "doh" or "doh3"`)
runCmd.Flags().BoolVarP(&rfc1918, "rfc1918", "", false, "Listen on RFC1918 addresses when 127.0.0.1 is the only listener")
runCmd.FParseErrWhitelist = cobra.FParseErrWhitelist{UnknownFlags: true}
rootCmd.AddCommand(runCmd)
@@ -531,6 +532,7 @@ NOTE: running "ctrld start" without any arguments will start already installed c
startCmd.Flags().BoolVarP(&skipSelfChecks, "skip_self_checks", "", false, `Skip self checks after installing ctrld service`)
startCmd.Flags().BoolVarP(&startOnly, "start_only", "", false, "Do not install new service")
_ = startCmd.Flags().MarkHidden("start_only")
startCmd.Flags().BoolVarP(&rfc1918, "rfc1918", "", false, "Listen on RFC1918 addresses when 127.0.0.1 is the only listener")
routerCmd := &cobra.Command{
Use: "setup",

View File

@@ -84,13 +84,7 @@ type upstreamForResult struct {
srcAddr string
}
func (p *prog) serveDNS(mainCtx context.Context, listenerNum string) error {
// Start network monitoring
if err := p.monitorNetworkChanges(mainCtx); err != nil {
mainLog.Load().Error().Err(err).Msg("Failed to start network monitoring")
// Don't return here as we still want DNS service to run
}
func (p *prog) serveDNS(listenerNum string) error {
listenerConfig := p.cfg.Listener[listenerNum]
// make sure ip is allocated
if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil {
@@ -213,8 +207,8 @@ func (p *prog) serveDNS(mainCtx context.Context, listenerNum string) error {
return nil
})
}
// When we spawn a listener on 127.0.0.1, also spawn listeners on the RFC1918
// addresses of the machine. So ctrld could receive queries from LAN clients.
// When we spawn a listener on 127.0.0.1, also spawn listeners on the RFC1918 addresses of the machine
// if explicitly set via setting rfc1918 flag, so ctrld could receive queries from LAN clients.
if needRFC1918Listeners(listenerConfig) {
g.Go(func() error {
for _, addr := range ctrld.Rfc1918Addresses() {
@@ -1045,7 +1039,7 @@ func (p *prog) queryFromSelf(ip string) bool {
// needRFC1918Listeners reports whether ctrld need to spawn listener for RFC 1918 addresses.
// This is helpful for non-desktop platforms to receive queries from LAN clients.
func needRFC1918Listeners(lc *ctrld.ListenerConfig) bool {
return lc.IP == "127.0.0.1" && lc.Port == 53 && !ctrld.IsDesktopPlatform()
return rfc1918 && lc.IP == "127.0.0.1" && lc.Port == 53
}
// ipFromARPA parses a FQDN arpa domain and return the IP address if valid.
@@ -1187,7 +1181,7 @@ func FlushDNSCache() error {
}
// monitorNetworkChanges starts monitoring for network interface changes
func (p *prog) monitorNetworkChanges(ctx context.Context) error {
func (p *prog) monitorNetworkChanges() error {
mon, err := netmon.New(func(format string, args ...any) {
// Always fetch the latest logger (and inject the prefix)
mainLog.Load().Printf("netmon: "+format, args...)
@@ -1406,9 +1400,6 @@ func (p *prog) checkUpstreamOnce(upstream string, uc *ctrld.UpstreamConfig) erro
return err
}
msg := new(dns.Msg)
msg.SetQuestion(".", dns.TypeNS)
timeout := 1000 * time.Millisecond
if uc.Timeout > 0 {
timeout = time.Millisecond * time.Duration(uc.Timeout)
@@ -1422,6 +1413,7 @@ func (p *prog) checkUpstreamOnce(upstream string, uc *ctrld.UpstreamConfig) erro
mainLog.Load().Debug().Msgf("Rebootstrapping resolver for upstream: %s", upstream)
start := time.Now()
msg := uc.VerifyMsg()
_, err = resolver.Resolve(ctx, msg)
duration := time.Since(start)

View File

@@ -18,11 +18,13 @@ type AppCallback struct {
// AppConfig allows overwriting ctrld cli flags from mobile platforms.
type AppConfig struct {
CdUID string
HomeDir string
UpstreamProto string
Verbose int
LogPath string
CdUID string
ProvisionID string
CustomHostname string
HomeDir string
UpstreamProto string
Verbose int
LogPath string
}
const (

View File

@@ -39,6 +39,7 @@ var (
skipSelfChecks bool
cleanup bool
startOnly bool
rfc1918 bool
mainLog atomic.Pointer[zerolog.Logger]
consoleWriter zerolog.ConsoleWriter

View File

@@ -530,6 +530,15 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
go p.watchLinkState(ctx)
}
if !reload {
go func() {
// Start network monitoring
if err := p.monitorNetworkChanges(); err != nil {
mainLog.Load().Error().Err(err).Msg("Failed to start network monitoring")
}
}()
}
for listenerNum := range p.cfg.Listener {
p.cfg.Listener[listenerNum].Init()
if !reload {
@@ -541,7 +550,7 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
}
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
mainLog.Load().Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, addr)
if err := p.serveDNS(ctx, listenerNum); err != nil {
if err := p.serveDNS(listenerNum); err != nil {
mainLog.Load().Fatal().Err(err).Msgf("unable to start dns proxy on listener.%s", listenerNum)
}
mainLog.Load().Debug().Msgf("end of serveDNS listener.%s: %s", listenerNum, addr)

View File

@@ -28,15 +28,17 @@ type AppCallback interface {
// Start configures utility with config.toml from provided directory.
// This function will block until Stop is called
// Check port availability prior to calling it.
func (c *Controller) Start(CdUID string, HomeDir string, UpstreamProto string, logLevel int, logPath string) {
func (c *Controller) Start(CdUID string, ProvisionID string, CustomHostname string, HomeDir string, UpstreamProto string, logLevel int, logPath string) {
if c.stopCh == nil {
c.stopCh = make(chan struct{})
c.Config = cli.AppConfig{
CdUID: CdUID,
HomeDir: HomeDir,
UpstreamProto: UpstreamProto,
Verbose: logLevel,
LogPath: logPath,
CdUID: CdUID,
ProvisionID: ProvisionID,
CustomHostname: CustomHostname,
HomeDir: HomeDir,
UpstreamProto: UpstreamProto,
Verbose: logLevel,
LogPath: logPath,
}
appCallback := mapCallback(c.AppCallback)
cli.RunMobile(&c.Config, &appCallback, c.stopCh)

View File

@@ -0,0 +1,206 @@
# Netstack - Full Packet Capture for Mobile VPN
Complete TCP/UDP/DNS packet capture implementation using gVisor netstack for Android and iOS.
## Overview
Provides full packet capture for mobile VPN applications:
- **DNS filtering** through ControlD proxy
- **TCP forwarding** for all TCP traffic
- **UDP forwarding** with session tracking
- **Socket protection** to prevent routing loops
- **QUIC blocking** for better content filtering
## Architecture
```
Mobile Apps → VPN TUN Interface → PacketHandler → gVisor Netstack
├─→ DNS Filter (Port 53)
│ └─→ ControlD DNS Proxy
├─→ TCP Forwarder
│ └─→ net.Dial("tcp") + protect(fd)
└─→ UDP Forwarder
└─→ net.Dial("udp") + protect(fd)
Real Network (Protected Sockets)
```
## Components
### DNS Filter (`dns_filter.go`)
Detects DNS packets on port 53 and routes to ControlD proxy.
### DNS Bridge (`dns_bridge.go`)
Tracks DNS queries by transaction ID with 5-second timeout.
### TCP Forwarder (`tcp_forwarder.go`)
Forwards TCP connections using gVisor's `tcp.NewForwarder()`.
### UDP Forwarder (`udp_forwarder.go`)
Forwards UDP packets with session tracking and 60-second idle timeout.
### Packet Handler (`packet_handler.go`)
Interface for TUN I/O and socket protection.
### Netstack Controller (`netstack.go`)
Manages gVisor stack and coordinates all components.
## Socket Protection
Critical for preventing routing loops:
```go
// Protection happens BEFORE connect()
dialer.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
protectSocket(int(fd))
})
}
```
**All protected sockets:**
- TCP/UDP forwarder sockets (user traffic)
- ControlD API sockets (api.controld.com)
- DoH upstream sockets (freedns.controld.com)
## Platform Configuration
### Android
```kotlin
Builder()
.addAddress("10.0.0.2", 24)
.addRoute("0.0.0.0", 0)
.addDnsServer("10.0.0.1")
.setMtu(1500)
override fun protectSocket(fd: Long) {
protect(fd.toInt()) // VpnService.protect()
}
// DNS Proxy: 0.0.0.0:5354
```
### iOS
```swift
NEIPv4Settings(addresses: ["10.0.0.2"], ...)
NEDNSSettings(servers: ["10.0.0.1"])
includedRoutes = [NEIPv4Route.default()]
func protectSocket(_ fd: Int) throws {
// No action needed - iOS Network Extension sockets
// automatically bypass VPN tunnel
}
// DNS Proxy: 127.0.0.1:53
```
**Note:** iOS Network Extensions run in separate process - sockets automatically bypass VPN.
## Protocol Support
| Protocol | Support |
|----------|---------|
| DNS (UDP/TCP port 53) | ✅ Full |
| TCP (all ports) | ✅ Full |
| UDP (except 53, 80, 443) | ✅ Full |
| QUIC (UDP/443, UDP/80) | 🚫 Blocked |
| ICMP | ⚠️ Partial |
| IPv4 | ✅ Full |
| IPv6 | ✅ Full |
## QUIC Blocking
Drops UDP packets on ports 443 and 80 to force TCP fallback:
**Why:**
- Better DNS filtering (QUIC bypasses DNS)
- Visible SNI for content filtering
- Consistent ControlD policy enforcement
**Result:**
- Apps automatically fallback to TCP/TLS
- No user-visible errors
- Slightly slower initial connection, then normal
## Usage (Android)
```kotlin
// Create callback
val callback = object : PacketAppCallback {
override fun readPacket(): ByteArray { ... }
override fun writePacket(packet: ByteArray) { ... }
override fun protectSocket(fd: Long) {
protect(fd.toInt())
}
override fun closePacketIO() { ... }
override fun exit(s: String) { ... }
override fun hostname(): String = "android-device"
override fun lanIp(): String = "10.0.0.2"
override fun macAddress(): String = "00:00:00:00:00:00"
}
// Create controller
val controller = Ctrld_library.newPacketCaptureController(callback)
// Start
controller.startWithPacketCapture(
callback,
cdUID,
"", "",
filesDir.absolutePath,
"doh",
2,
"$filesDir/ctrld.log"
)
// Stop
controller.stop(false, 0)
```
## Usage (iOS)
```swift
// DNS-only mode
let proxy = LocalProxy()
proxy.start(
cUID: cdUID,
deviceName: UIDevice.current.name,
upstreamProto: "doh",
logLevel: 3,
provisionID: ""
)
// Firewall mode (full capture)
let proxy = LocalProxy()
proxy.startFirewall(
cUID: cdUID,
deviceName: UIDevice.current.name,
upstreamProto: "doh",
logLevel: 3,
provisionID: "",
packetFlow: packetFlow
)
```
## Requirements
- **Android**: API 24+ (Android 7.0+)
- **iOS**: iOS 12.0+
- **Go**: 1.23+
- **gVisor**: v0.0.0-20240722211153-64c016c92987
## Files
- `packet_handler.go` - TUN I/O interface
- `netstack.go` - gVisor controller
- `dns_filter.go` - DNS packet detection
- `dns_bridge.go` - Transaction tracking
- `tcp_forwarder.go` - TCP forwarding
- `udp_forwarder.go` - UDP forwarding
## License
Same as parent ctrld project.

View File

@@ -0,0 +1,228 @@
package netstack
import (
"fmt"
"sync"
"time"
"github.com/miekg/dns"
)
// DNSBridge provides a bridge between the netstack DNS filter and the existing ctrld DNS proxy.
// It allows DNS queries captured from packets to be processed by the same logic as traditional DNS queries.
type DNSBridge struct {
// Channel for sending DNS queries
queryCh chan *DNSQuery
// Channel for receiving DNS responses
responseCh chan *DNSResponse
// Map to track pending queries by transaction ID
pendingQueries map[uint16]*PendingQuery
mu sync.RWMutex
// Timeout for DNS queries
queryTimeout time.Duration
// Running state
running bool
stopCh chan struct{}
wg sync.WaitGroup
}
// DNSQuery represents a DNS query to be processed
type DNSQuery struct {
ID uint16 // Transaction ID for matching response
Query []byte // Raw DNS query bytes
RespCh chan []byte // Response channel
SrcIP string // Source IP for logging
SrcPort uint16 // Source port
}
// DNSResponse represents a DNS response
type DNSResponse struct {
ID uint16
Response []byte
}
// PendingQuery tracks a query waiting for response
type PendingQuery struct {
Query *DNSQuery
Timestamp time.Time
}
// NewDNSBridge creates a new DNS bridge
func NewDNSBridge() *DNSBridge {
return &DNSBridge{
queryCh: make(chan *DNSQuery, 100),
responseCh: make(chan *DNSResponse, 100),
pendingQueries: make(map[uint16]*PendingQuery),
queryTimeout: 5 * time.Second,
stopCh: make(chan struct{}),
}
}
// Start starts the DNS bridge
func (b *DNSBridge) Start() {
b.mu.Lock()
if b.running {
b.mu.Unlock()
return
}
b.running = true
b.mu.Unlock()
// Start response handler
b.wg.Add(1)
go b.handleResponses()
// Start timeout checker
b.wg.Add(1)
go b.checkTimeouts()
}
// Stop stops the DNS bridge
func (b *DNSBridge) Stop() {
b.mu.Lock()
if !b.running {
b.mu.Unlock()
return
}
b.running = false
b.mu.Unlock()
close(b.stopCh)
b.wg.Wait()
// Clean up pending queries
b.mu.Lock()
for _, pending := range b.pendingQueries {
close(pending.Query.RespCh)
}
b.pendingQueries = make(map[uint16]*PendingQuery)
b.mu.Unlock()
}
// ProcessQuery processes a DNS query and waits for response
func (b *DNSBridge) ProcessQuery(query []byte, srcIP string, srcPort uint16) ([]byte, error) {
if len(query) < 12 {
return nil, fmt.Errorf("invalid DNS query: too short")
}
// Parse DNS message to get transaction ID
msg := new(dns.Msg)
if err := msg.Unpack(query); err != nil {
return nil, fmt.Errorf("failed to parse DNS query: %v", err)
}
// Create response channel
respCh := make(chan []byte, 1)
// Create query
dnsQuery := &DNSQuery{
ID: msg.Id,
Query: query,
RespCh: respCh,
SrcIP: srcIP,
SrcPort: srcPort,
}
// Store as pending
b.mu.Lock()
b.pendingQueries[msg.Id] = &PendingQuery{
Query: dnsQuery,
Timestamp: time.Now(),
}
b.mu.Unlock()
// Send query
select {
case b.queryCh <- dnsQuery:
case <-time.After(time.Second):
b.mu.Lock()
delete(b.pendingQueries, msg.Id)
b.mu.Unlock()
return nil, fmt.Errorf("query channel full")
}
// Wait for response with timeout
select {
case response := <-respCh:
b.mu.Lock()
delete(b.pendingQueries, msg.Id)
b.mu.Unlock()
return response, nil
case <-time.After(b.queryTimeout):
b.mu.Lock()
delete(b.pendingQueries, msg.Id)
b.mu.Unlock()
return nil, fmt.Errorf("DNS query timeout")
}
}
// GetQueryChannel returns the channel for receiving DNS queries
func (b *DNSBridge) GetQueryChannel() <-chan *DNSQuery {
return b.queryCh
}
// SendResponse sends a DNS response back to the waiting query
func (b *DNSBridge) SendResponse(id uint16, response []byte) error {
b.mu.RLock()
pending, exists := b.pendingQueries[id]
b.mu.RUnlock()
if !exists {
return fmt.Errorf("no pending query for ID %d", id)
}
select {
case pending.Query.RespCh <- response:
return nil
case <-time.After(time.Second):
return fmt.Errorf("failed to send response: channel blocked")
}
}
// handleResponses handles incoming responses
func (b *DNSBridge) handleResponses() {
defer b.wg.Done()
for {
select {
case <-b.stopCh:
return
case resp := <-b.responseCh:
if err := b.SendResponse(resp.ID, resp.Response); err != nil {
// Log error but continue
}
}
}
}
// checkTimeouts periodically checks for and removes timed out queries
func (b *DNSBridge) checkTimeouts() {
defer b.wg.Done()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-b.stopCh:
return
case <-ticker.C:
now := time.Now()
b.mu.Lock()
for id, pending := range b.pendingQueries {
if now.Sub(pending.Timestamp) > b.queryTimeout {
close(pending.Query.RespCh)
delete(b.pendingQueries, id)
}
}
b.mu.Unlock()
}
}
}

View File

@@ -0,0 +1,324 @@
package netstack
import (
"encoding/binary"
"fmt"
"net"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
// DNSFilter intercepts and processes DNS packets.
type DNSFilter struct {
dnsHandler func([]byte) ([]byte, error)
}
// NewDNSFilter creates a new DNS filter with the given handler.
func NewDNSFilter(handler func([]byte) ([]byte, error)) *DNSFilter {
return &DNSFilter{
dnsHandler: handler,
}
}
// ProcessPacket checks if a packet is a DNS query and processes it.
// Returns:
// - isDNS: true if this is a DNS packet
// - response: DNS response packet (if handled), nil otherwise
// - error: any error that occurred
func (df *DNSFilter) ProcessPacket(packet []byte) (isDNS bool, response []byte, err error) {
if len(packet) < header.IPv4MinimumSize {
return false, nil, nil
}
// Parse IP version
ipVersion := packet[0] >> 4
switch ipVersion {
case 4:
return df.processIPv4(packet)
case 6:
return df.processIPv6(packet)
default:
return false, nil, nil
}
}
// processIPv4 processes an IPv4 packet and checks if it's DNS.
func (df *DNSFilter) processIPv4(packet []byte) (bool, []byte, error) {
if len(packet) < header.IPv4MinimumSize {
return false, nil, nil
}
// Parse IPv4 header
ipHdr := header.IPv4(packet)
if !ipHdr.IsValid(len(packet)) {
return false, nil, nil
}
// Check if it's UDP
if ipHdr.TransportProtocol() != header.UDPProtocolNumber {
return false, nil, nil
}
// Get IP header length
ihl := int(ipHdr.HeaderLength())
if len(packet) < ihl+header.UDPMinimumSize {
return false, nil, nil
}
// Parse UDP header
udpHdr := header.UDP(packet[ihl:])
srcPort := udpHdr.SourcePort()
dstPort := udpHdr.DestinationPort()
// Check if destination port is 53 (DNS)
if dstPort != 53 {
return false, nil, nil
}
srcIP := ipHdr.SourceAddress()
dstIP := ipHdr.DestinationAddress()
// Extract DNS payload
udpPayloadOffset := ihl + header.UDPMinimumSize
if len(packet) <= udpPayloadOffset {
return true, nil, fmt.Errorf("invalid UDP packet length")
}
dnsQuery := packet[udpPayloadOffset:]
if len(dnsQuery) == 0 {
return true, nil, fmt.Errorf("empty DNS query")
}
// Process DNS query
if df.dnsHandler == nil {
return true, nil, fmt.Errorf("no DNS handler configured")
}
dnsResponse, err := df.dnsHandler(dnsQuery)
if err != nil {
return true, nil, fmt.Errorf("DNS handler error: %v", err)
}
// Build response packet
responsePacket := df.buildIPv4UDPPacket(
dstIP.As4(), // Swap src/dst
srcIP.As4(),
dstPort, // Swap ports
srcPort,
dnsResponse,
)
return true, responsePacket, nil
}
// processIPv6 processes an IPv6 packet and checks if it's DNS.
func (df *DNSFilter) processIPv6(packet []byte) (bool, []byte, error) {
if len(packet) < header.IPv6MinimumSize {
return false, nil, nil
}
// Parse IPv6 header
ipHdr := header.IPv6(packet)
if !ipHdr.IsValid(len(packet)) {
return false, nil, nil
}
// Check if it's UDP
if ipHdr.TransportProtocol() != header.UDPProtocolNumber {
return false, nil, nil
}
// IPv6 header is fixed size
if len(packet) < header.IPv6MinimumSize+header.UDPMinimumSize {
return false, nil, nil
}
// Parse UDP header
udpHdr := header.UDP(packet[header.IPv6MinimumSize:])
srcPort := udpHdr.SourcePort()
dstPort := udpHdr.DestinationPort()
// Check if destination port is 53 (DNS)
if dstPort != 53 {
return false, nil, nil
}
// Extract DNS payload
udpPayloadOffset := header.IPv6MinimumSize + header.UDPMinimumSize
if len(packet) <= udpPayloadOffset {
return true, nil, fmt.Errorf("invalid UDP packet length")
}
dnsQuery := packet[udpPayloadOffset:]
if len(dnsQuery) == 0 {
return true, nil, fmt.Errorf("empty DNS query")
}
// Process DNS query
if df.dnsHandler == nil {
return true, nil, fmt.Errorf("no DNS handler configured")
}
dnsResponse, err := df.dnsHandler(dnsQuery)
if err != nil {
return true, nil, fmt.Errorf("DNS handler error: %v", err)
}
// Build response packet
srcIP := ipHdr.SourceAddress()
dstIP := ipHdr.DestinationAddress()
responsePacket := df.buildIPv6UDPPacket(
dstIP.As16(), // Swap src/dst
srcIP.As16(),
dstPort, // Swap ports
srcPort,
dnsResponse,
)
return true, responsePacket, nil
}
// buildIPv4UDPPacket builds a complete IPv4/UDP packet with the given payload.
func (df *DNSFilter) buildIPv4UDPPacket(srcIP, dstIP [4]byte, srcPort, dstPort uint16, payload []byte) []byte {
// Calculate lengths
udpLen := header.UDPMinimumSize + len(payload)
ipLen := header.IPv4MinimumSize + udpLen
packet := make([]byte, ipLen)
// Build IPv4 header
ipHdr := header.IPv4(packet)
ipHdr.Encode(&header.IPv4Fields{
TotalLength: uint16(ipLen),
TTL: 64,
Protocol: uint8(header.UDPProtocolNumber),
SrcAddr: tcpip.AddrFrom4(srcIP),
DstAddr: tcpip.AddrFrom4(dstIP),
})
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
// Build UDP header
udpHdr := header.UDP(packet[header.IPv4MinimumSize:])
udpHdr.Encode(&header.UDPFields{
SrcPort: srcPort,
DstPort: dstPort,
Length: uint16(udpLen),
})
// Copy payload
copy(packet[header.IPv4MinimumSize+header.UDPMinimumSize:], payload)
// Calculate UDP checksum
xsum := header.PseudoHeaderChecksum(
header.UDPProtocolNumber,
tcpip.AddrFrom4(srcIP),
tcpip.AddrFrom4(dstIP),
uint16(udpLen),
)
xsum = checksum(payload, xsum)
udpHdr.SetChecksum(^udpHdr.CalculateChecksum(xsum))
return packet
}
// buildIPv6UDPPacket builds a complete IPv6/UDP packet with the given payload.
func (df *DNSFilter) buildIPv6UDPPacket(srcIP, dstIP [16]byte, srcPort, dstPort uint16, payload []byte) []byte {
// Calculate lengths
udpLen := header.UDPMinimumSize + len(payload)
ipLen := header.IPv6MinimumSize + udpLen
packet := make([]byte, ipLen)
// Build IPv6 header
ipHdr := header.IPv6(packet)
ipHdr.Encode(&header.IPv6Fields{
PayloadLength: uint16(udpLen),
TransportProtocol: header.UDPProtocolNumber,
HopLimit: 64,
SrcAddr: tcpip.AddrFrom16(srcIP),
DstAddr: tcpip.AddrFrom16(dstIP),
})
// Build UDP header
udpHdr := header.UDP(packet[header.IPv6MinimumSize:])
udpHdr.Encode(&header.UDPFields{
SrcPort: srcPort,
DstPort: dstPort,
Length: uint16(udpLen),
})
// Copy payload
copy(packet[header.IPv6MinimumSize+header.UDPMinimumSize:], payload)
// Calculate UDP checksum
xsum := header.PseudoHeaderChecksum(
header.UDPProtocolNumber,
tcpip.AddrFrom16(srcIP),
tcpip.AddrFrom16(dstIP),
uint16(udpLen),
)
xsum = checksum(payload, xsum)
udpHdr.SetChecksum(^udpHdr.CalculateChecksum(xsum))
return packet
}
// checksum calculates the checksum for the given data.
func checksum(buf []byte, initial uint16) uint16 {
v := uint32(initial)
l := len(buf)
if l&1 != 0 {
l--
v += uint32(buf[l]) << 8
}
for i := 0; i < l; i += 2 {
v += (uint32(buf[i]) << 8) + uint32(buf[i+1])
}
return reduceChecksum(v)
}
// reduceChecksum reduces a 32-bit checksum to 16 bits.
func reduceChecksum(v uint32) uint16 {
v = (v >> 16) + (v & 0xffff)
v = (v >> 16) + (v & 0xffff)
return uint16(v)
}
// IPv4Address is a helper to create an IPv4 address from a byte array.
func IPv4Address(b [4]byte) net.IP {
return net.IPv4(b[0], b[1], b[2], b[3])
}
// IPv6Address is a helper to create an IPv6 address from a byte array.
func IPv6Address(b [16]byte) net.IP {
return net.IP(b[:])
}
// parseIPv4 extracts source and destination IPs from an IPv4 packet.
func parseIPv4(packet []byte) (srcIP, dstIP [4]byte, ok bool) {
if len(packet) < header.IPv4MinimumSize {
return
}
ipHdr := header.IPv4(packet)
if !ipHdr.IsValid(len(packet)) {
return
}
srcAddr := ipHdr.SourceAddress().As4()
dstAddr := ipHdr.DestinationAddress().As4()
copy(srcIP[:], srcAddr[:])
copy(dstIP[:], dstAddr[:])
ok = true
return
}
// parseUDP extracts UDP header information.
func parseUDP(udpHeader []byte) (srcPort, dstPort uint16, ok bool) {
if len(udpHeader) < header.UDPMinimumSize {
return
}
srcPort = binary.BigEndian.Uint16(udpHeader[0:2])
dstPort = binary.BigEndian.Uint16(udpHeader[2:4])
ok = true
return
}

View File

@@ -0,0 +1,374 @@
package netstack
import (
"context"
"fmt"
"net"
"net/netip"
"sync"
"time"
"github.com/Control-D-Inc/ctrld"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)
const (
// Default MTU for the TUN interface
defaultMTU = 1500
// NICID is the ID of the network interface
NICID = 1
// Channel capacity for packet buffers
channelCapacity = 256
)
// NetstackController manages the gVisor netstack integration for mobile packet capture.
type NetstackController struct {
stack *stack.Stack
linkEP *channel.Endpoint
packetHandler PacketHandler
dnsFilter *DNSFilter
tcpForwarder *TCPForwarder
udpForwarder *UDPForwarder
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
started bool
mu sync.Mutex
}
// Config holds configuration for NetstackController.
type Config struct {
// MTU is the maximum transmission unit
MTU uint32
// TUNIPv4 is the IPv4 address assigned to the TUN interface
TUNIPv4 netip.Addr
// TUNIPv6 is the IPv6 address assigned to the TUN interface (optional)
TUNIPv6 netip.Addr
// DNSHandler is the function to process DNS queries
DNSHandler func([]byte) ([]byte, error)
// UpstreamInterface is the real network interface for routing non-DNS traffic
UpstreamInterface *net.Interface
}
// NewNetstackController creates a new netstack controller.
func NewNetstackController(handler PacketHandler, cfg *Config) (*NetstackController, error) {
if handler == nil {
return nil, fmt.Errorf("packet handler cannot be nil")
}
if cfg == nil {
cfg = &Config{
MTU: defaultMTU,
TUNIPv4: netip.MustParseAddr("10.0.0.1"),
}
}
if cfg.MTU == 0 {
cfg.MTU = defaultMTU
}
ctx, cancel := context.WithCancel(context.Background())
// Create gVisor stack
s := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{
ipv4.NewProtocol,
ipv6.NewProtocol,
},
TransportProtocols: []stack.TransportProtocolFactory{
tcp.NewProtocol,
udp.NewProtocol,
},
})
// Create link endpoint
linkEP := channel.New(channelCapacity, cfg.MTU, "")
// Create DNS filter
dnsFilter := NewDNSFilter(cfg.DNSHandler)
// Create TCP forwarder
tcpForwarder := NewTCPForwarder(s, handler.ProtectSocket, ctx)
// Create UDP forwarder
udpForwarder := NewUDPForwarder(s, handler.ProtectSocket, ctx)
// Create NIC
if err := s.CreateNIC(NICID, linkEP); err != nil {
cancel()
return nil, fmt.Errorf("failed to create NIC: %v", err)
}
// Enable spoofing to allow packets with any source IP
if err := s.SetSpoofing(NICID, true); err != nil {
cancel()
return nil, fmt.Errorf("failed to enable spoofing: %v", err)
}
// Enable promiscuous mode to accept all packets
if err := s.SetPromiscuousMode(NICID, true); err != nil {
cancel()
return nil, fmt.Errorf("failed to enable promiscuous mode: %v", err)
}
// Add IPv4 address
protocolAddr := tcpip.ProtocolAddress{
Protocol: ipv4.ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: tcpip.AddrFromSlice(cfg.TUNIPv4.AsSlice()),
PrefixLen: 24,
},
}
if err := s.AddProtocolAddress(NICID, protocolAddr, stack.AddressProperties{}); err != nil {
cancel()
return nil, fmt.Errorf("failed to add IPv4 address: %v", err)
}
// Add IPv6 address if provided
if cfg.TUNIPv6.IsValid() {
protocolAddr6 := tcpip.ProtocolAddress{
Protocol: ipv6.ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: tcpip.AddrFromSlice(cfg.TUNIPv6.AsSlice()),
PrefixLen: 64,
},
}
if err := s.AddProtocolAddress(NICID, protocolAddr6, stack.AddressProperties{}); err != nil {
cancel()
return nil, fmt.Errorf("failed to add IPv6 address: %v", err)
}
}
// Add default routes
s.SetRouteTable([]tcpip.Route{
{
Destination: header.IPv4EmptySubnet,
NIC: NICID,
},
{
Destination: header.IPv6EmptySubnet,
NIC: NICID,
},
})
// Register forwarders with the stack
s.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.forwarder.HandlePacket)
s.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.forwarder.HandlePacket)
nc := &NetstackController{
stack: s,
linkEP: linkEP,
packetHandler: handler,
dnsFilter: dnsFilter,
tcpForwarder: tcpForwarder,
udpForwarder: udpForwarder,
ctx: ctx,
cancel: cancel,
started: false,
}
ctrld.ProxyLogger.Load().Info().Msg("[Netstack] Controller created with TCP/UDP forwarders")
return nc, nil
}
// Start starts the netstack controller and begins processing packets.
func (nc *NetstackController) Start() error {
nc.mu.Lock()
defer nc.mu.Unlock()
if nc.started {
return fmt.Errorf("netstack controller already started")
}
nc.started = true
// Start packet reader goroutine (TUN -> netstack)
nc.wg.Add(1)
go nc.readPackets()
// Start packet writer goroutine (netstack -> TUN)
nc.wg.Add(1)
go nc.writePackets()
ctrld.ProxyLogger.Load().Info().Msg("[Netstack] Packet processing started (read/write goroutines)")
return nil
}
// Stop stops the netstack controller and waits for all goroutines to finish.
func (nc *NetstackController) Stop() error {
nc.mu.Lock()
if !nc.started {
nc.mu.Unlock()
return nil
}
nc.mu.Unlock()
nc.cancel()
nc.wg.Wait()
// Close UDP forwarder
if nc.udpForwarder != nil {
nc.udpForwarder.Close()
}
if err := nc.packetHandler.Close(); err != nil {
return fmt.Errorf("failed to close packet handler: %v", err)
}
nc.mu.Lock()
nc.started = false
nc.mu.Unlock()
return nil
}
// readPackets reads packets from the TUN interface and injects them into the netstack.
func (nc *NetstackController) readPackets() {
defer nc.wg.Done()
for {
select {
case <-nc.ctx.Done():
return
default:
}
// Read packet from TUN
packet, err := nc.packetHandler.ReadPacket()
if err != nil {
if nc.ctx.Err() != nil {
return
}
time.Sleep(10 * time.Millisecond)
continue
}
if len(packet) == 0 {
continue
}
// Check if this is a DNS packet
isDNS, response, err := nc.dnsFilter.ProcessPacket(packet)
if err != nil {
continue
}
if isDNS && response != nil {
// DNS packet was handled, send response back to TUN
nc.packetHandler.WritePacket(response)
ctrld.ProxyLogger.Load().Debug().Msgf("[Netstack] DNS response sent (%d bytes)", len(response))
continue
}
if isDNS {
continue
}
// Not a DNS packet - check if it's an OUTBOUND packet (source = 10.0.0.x)
// We should ONLY inject outbound packets, not return packets
if len(packet) >= 20 {
// Check if source is in our VPN subnet (10.0.0.x)
isOutbound := packet[12] == 10 && packet[13] == 0 && packet[14] == 0
if !isOutbound {
// This is a return packet (server -> mobile)
// Drop it - return packets come through forwarder's upstream connection
continue
}
// Block QUIC protocol (UDP on port 443)
// QUIC runs over UDP and bypasses DNS, so we block it to force HTTP/2 or HTTP/3 over TCP
protocol := packet[9]
if protocol == 17 { // UDP
// Get IP header length
ihl := int(packet[0]&0x0f) * 4
if len(packet) >= ihl+4 {
// Parse UDP destination port (bytes 2-3 of UDP header)
dstPort := uint16(packet[ihl+2])<<8 | uint16(packet[ihl+3])
if dstPort == 443 || dstPort == 80 {
// Block QUIC (UDP/443) and HTTP/3 (UDP/80)
// Apps will fallback to TCP automatically
dstIP := net.IPv4(packet[16], packet[17], packet[18], packet[19])
ctrld.ProxyLogger.Load().Debug().Msgf("[Netstack] Blocked QUIC packet to %s:%d", dstIP, dstPort)
continue
}
}
}
}
// Create packet buffer
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(packet),
})
// Determine protocol number
var proto tcpip.NetworkProtocolNumber
if len(packet) > 0 {
version := packet[0] >> 4
switch version {
case 4:
proto = header.IPv4ProtocolNumber
case 6:
proto = header.IPv6ProtocolNumber
default:
pkt.DecRef()
continue
}
} else {
pkt.DecRef()
continue
}
// Inject into netstack - TCP/UDP forwarders will handle it
nc.linkEP.InjectInbound(proto, pkt)
}
}
// writePackets reads packets from netstack and writes them to the TUN interface.
func (nc *NetstackController) writePackets() {
defer nc.wg.Done()
for {
select {
case <-nc.ctx.Done():
return
default:
}
// Read packet from netstack
pkt := nc.linkEP.ReadContext(nc.ctx)
if pkt == nil {
continue
}
// Convert packet to bytes
vv := pkt.ToView()
packet := vv.AsSlice()
// Write to TUN
if err := nc.packetHandler.WritePacket(packet); err != nil {
// Log error
continue
}
pkt.DecRef()
}
}

View File

@@ -0,0 +1,115 @@
package netstack
import (
"fmt"
"sync"
)
// PacketHandler defines the interface for reading and writing raw IP packets
// from/to the mobile TUN interface.
type PacketHandler interface {
// ReadPacket reads a raw IP packet from the TUN interface.
// This should be a blocking call.
ReadPacket() ([]byte, error)
// WritePacket writes a raw IP packet back to the TUN interface.
WritePacket(packet []byte) error
// Close closes the packet handler and releases resources.
Close() error
// ProtectSocket protects a socket file descriptor from being routed through the VPN.
// This is required on Android/iOS to prevent routing loops.
// Returns nil if successful, error otherwise.
ProtectSocket(fd int) error
}
// MobilePacketHandler implements PacketHandler using callbacks from mobile platforms.
// This bridges Go Mobile interface with the netstack implementation.
type MobilePacketHandler struct {
readFunc func() ([]byte, error)
writeFunc func([]byte) error
closeFunc func() error
protectFunc func(int) error
mu sync.Mutex
closed bool
}
// NewMobilePacketHandler creates a new packet handler with mobile callbacks.
func NewMobilePacketHandler(
readFunc func() ([]byte, error),
writeFunc func([]byte) error,
closeFunc func() error,
protectFunc func(int) error,
) *MobilePacketHandler {
return &MobilePacketHandler{
readFunc: readFunc,
writeFunc: writeFunc,
closeFunc: closeFunc,
protectFunc: protectFunc,
closed: false,
}
}
// ReadPacket reads a packet from mobile TUN interface.
func (m *MobilePacketHandler) ReadPacket() ([]byte, error) {
m.mu.Lock()
closed := m.closed
m.mu.Unlock()
if closed {
return nil, fmt.Errorf("packet handler is closed")
}
if m.readFunc == nil {
return nil, fmt.Errorf("read function not set")
}
return m.readFunc()
}
// WritePacket writes a packet back to mobile TUN interface.
func (m *MobilePacketHandler) WritePacket(packet []byte) error {
m.mu.Lock()
closed := m.closed
m.mu.Unlock()
if closed {
return fmt.Errorf("packet handler is closed")
}
if m.writeFunc == nil {
return fmt.Errorf("write function not set")
}
return m.writeFunc(packet)
}
// Close closes the packet handler.
func (m *MobilePacketHandler) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
if m.closed {
return nil
}
m.closed = true
if m.closeFunc != nil {
return m.closeFunc()
}
return nil
}
// ProtectSocket protects a socket file descriptor from VPN routing.
func (m *MobilePacketHandler) ProtectSocket(fd int) error {
if m.protectFunc == nil {
// No protect function provided - this is okay for non-VPN scenarios
return nil
}
return m.protectFunc(fd)
}

View File

@@ -0,0 +1,123 @@
package netstack
import (
"context"
"io"
"net"
"syscall"
"time"
"github.com/Control-D-Inc/ctrld"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/waiter"
)
// TCPForwarder handles TCP connections from the TUN interface
type TCPForwarder struct {
protectSocket func(fd int) error
ctx context.Context
forwarder *tcp.Forwarder
}
// NewTCPForwarder creates a new TCP forwarder
func NewTCPForwarder(s *stack.Stack, protectSocket func(fd int) error, ctx context.Context) *TCPForwarder {
f := &TCPForwarder{
protectSocket: protectSocket,
ctx: ctx,
}
// Create gVisor TCP forwarder with handler callback
// rcvWnd=0 (default), maxInFlight=1024
f.forwarder = tcp.NewForwarder(s, 0, 1024, f.handleRequest)
return f
}
// GetForwarder returns the underlying gVisor forwarder
func (f *TCPForwarder) GetForwarder() *tcp.Forwarder {
return f.forwarder
}
// handleRequest handles an incoming TCP connection request
func (f *TCPForwarder) handleRequest(req *tcp.ForwarderRequest) {
// Get the endpoint ID
id := req.ID()
// Create waiter queue
var wq waiter.Queue
// Create endpoint from request
ep, err := req.CreateEndpoint(&wq)
if err != nil {
req.Complete(true) // Send RST
return
}
// Accept the connection
req.Complete(false)
// Cast to TCP endpoint
tcpEP, ok := ep.(*tcp.Endpoint)
if !ok {
ep.Close()
return
}
// Handle in goroutine
go f.handleConnection(tcpEP, &wq, id)
}
func (f *TCPForwarder) handleConnection(ep *tcp.Endpoint, wq *waiter.Queue, id stack.TransportEndpointID) {
// Convert endpoint to Go net.Conn
tunConn := gonet.NewTCPConn(wq, ep)
defer tunConn.Close()
// In gVisor's TransportEndpointID for an inbound connection:
// - LocalAddress/LocalPort = the destination (where packet is going TO)
// - RemoteAddress/RemotePort = the source (where packet is coming FROM)
// We want to dial the DESTINATION (LocalAddress/LocalPort)
dstAddr := net.TCPAddr{
IP: net.IP(id.LocalAddress.AsSlice()),
Port: int(id.LocalPort),
}
// Create outbound connection with socket protection DURING dial
dialer := &net.Dialer{
Timeout: 30 * time.Second,
}
// CRITICAL: Protect socket BEFORE connect() is called
if f.protectSocket != nil {
dialer.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
f.protectSocket(int(fd))
})
}
}
upstreamConn, err := dialer.DialContext(f.ctx, "tcp", dstAddr.String())
if err != nil {
return
}
defer upstreamConn.Close()
// Log successful TCP connection
srcAddr := net.IP(id.RemoteAddress.AsSlice())
ctrld.ProxyLogger.Load().Debug().Msgf("[TCP] %s:%d -> %s:%d", srcAddr, id.RemotePort, dstAddr.IP, dstAddr.Port)
// Bidirectional copy
done := make(chan struct{}, 2)
go func() {
io.Copy(upstreamConn, tunConn)
done <- struct{}{}
}()
go func() {
io.Copy(tunConn, upstreamConn)
done <- struct{}{}
}()
// Wait for one direction to finish
<-done
}

View File

@@ -0,0 +1,226 @@
package netstack
import (
"context"
"fmt"
"net"
"sync"
"syscall"
"time"
"github.com/Control-D-Inc/ctrld"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"gvisor.dev/gvisor/pkg/waiter"
)
// UDPForwarder handles UDP packets from the TUN interface
type UDPForwarder struct {
protectSocket func(fd int) error
ctx context.Context
forwarder *udp.Forwarder
// Track UDP "connections" (address pairs)
connections map[string]*udpConn
mu sync.Mutex
}
type udpConn struct {
tunEP *gonet.UDPConn
upstreamConn *net.UDPConn
cancel context.CancelFunc
}
// NewUDPForwarder creates a new UDP forwarder
func NewUDPForwarder(s *stack.Stack, protectSocket func(fd int) error, ctx context.Context) *UDPForwarder {
f := &UDPForwarder{
protectSocket: protectSocket,
ctx: ctx,
connections: make(map[string]*udpConn),
}
// Create gVisor UDP forwarder with handler callback
f.forwarder = udp.NewForwarder(s, f.handlePacket)
return f
}
// GetForwarder returns the underlying gVisor forwarder
func (f *UDPForwarder) GetForwarder() *udp.Forwarder {
return f.forwarder
}
// handlePacket handles an incoming UDP packet
func (f *UDPForwarder) handlePacket(req *udp.ForwarderRequest) {
// Get the endpoint ID
id := req.ID()
// Create connection key (source -> destination)
connKey := fmt.Sprintf("%s:%d->%s:%d",
net.IP(id.RemoteAddress.AsSlice()),
id.RemotePort,
net.IP(id.LocalAddress.AsSlice()),
id.LocalPort,
)
f.mu.Lock()
conn, exists := f.connections[connKey]
if !exists {
// Create new connection
conn = f.createConnection(req, connKey)
if conn == nil {
f.mu.Unlock()
return
}
f.connections[connKey] = conn
// Log new UDP session
srcAddr := net.IP(id.RemoteAddress.AsSlice())
dstAddr := net.IP(id.LocalAddress.AsSlice())
ctrld.ProxyLogger.Load().Debug().Msgf("[UDP] New session: %s:%d -> %s:%d (total: %d)",
srcAddr, id.RemotePort, dstAddr, id.LocalPort, len(f.connections))
}
f.mu.Unlock()
}
func (f *UDPForwarder) createConnection(req *udp.ForwarderRequest, connKey string) *udpConn {
id := req.ID()
// Create waiter queue
var wq waiter.Queue
// Create endpoint from request
ep, err := req.CreateEndpoint(&wq)
if err != nil {
return nil
}
// Convert to Go UDP conn
tunConn := gonet.NewUDPConn(&wq, ep)
// Extract destination address
// LocalAddress/LocalPort = destination (where packet is going TO)
// RemoteAddress/RemotePort = source (where packet is coming FROM)
dstAddr := &net.UDPAddr{
IP: net.IP(id.LocalAddress.AsSlice()),
Port: int(id.LocalPort),
}
// Create dialer with socket protection DURING dial
dialer := &net.Dialer{}
// CRITICAL: Protect socket BEFORE connect() is called
if f.protectSocket != nil {
dialer.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
f.protectSocket(int(fd))
})
}
}
// Create outbound UDP connection
dialConn, dialErr := dialer.Dial("udp", dstAddr.String())
if dialErr != nil {
tunConn.Close()
return nil
}
upstreamConn, ok := dialConn.(*net.UDPConn)
if !ok {
dialConn.Close()
tunConn.Close()
return nil
}
// Create connection context
ctx, cancel := context.WithCancel(f.ctx)
udpConnection := &udpConn{
tunEP: tunConn,
upstreamConn: upstreamConn,
cancel: cancel,
}
// Start forwarding goroutines
go f.forwardTunToUpstream(udpConnection, ctx)
go f.forwardUpstreamToTun(udpConnection, ctx, connKey)
return udpConnection
}
func (f *UDPForwarder) forwardTunToUpstream(conn *udpConn, ctx context.Context) {
buffer := make([]byte, 65535)
for {
select {
case <-ctx.Done():
return
default:
}
// Read from TUN
n, err := conn.tunEP.Read(buffer)
if err != nil {
return
}
// Write to upstream
_, err = conn.upstreamConn.Write(buffer[:n])
if err != nil {
return
}
}
}
func (f *UDPForwarder) forwardUpstreamToTun(conn *udpConn, ctx context.Context, connKey string) {
defer func() {
conn.tunEP.Close()
conn.upstreamConn.Close()
f.mu.Lock()
delete(f.connections, connKey)
f.mu.Unlock()
}()
buffer := make([]byte, 65535)
// Set read timeout
conn.upstreamConn.SetReadDeadline(time.Now().Add(30 * time.Second))
for {
select {
case <-ctx.Done():
return
default:
}
// Read from upstream
n, err := conn.upstreamConn.Read(buffer)
if err != nil {
return
}
// Reset read deadline
conn.upstreamConn.SetReadDeadline(time.Now().Add(30 * time.Second))
// Write to TUN
_, err = conn.tunEP.Write(buffer[:n])
if err != nil {
return
}
}
}
// Close closes all UDP connections
func (f *UDPForwarder) Close() {
f.mu.Lock()
defer f.mu.Unlock()
for _, conn := range f.connections {
conn.cancel()
conn.tunEP.Close()
conn.upstreamConn.Close()
}
f.connections = make(map[string]*udpConn)
}

View File

@@ -0,0 +1,272 @@
package ctrld_library
import (
"fmt"
"net/netip"
"runtime"
"time"
"github.com/Control-D-Inc/ctrld"
"github.com/Control-D-Inc/ctrld/cmd/cli"
"github.com/Control-D-Inc/ctrld/cmd/ctrld_library/netstack"
"github.com/miekg/dns"
)
// PacketAppCallback extends AppCallback with packet read/write capabilities.
// Mobile platforms implementing full packet capture should use this interface.
type PacketAppCallback interface {
AppCallback
// ReadPacket reads a raw IP packet from the TUN interface.
// This should be a blocking call that returns when a packet is available.
ReadPacket() ([]byte, error)
// WritePacket writes a raw IP packet back to the TUN interface.
WritePacket(packet []byte) error
// ClosePacketIO closes packet I/O resources.
ClosePacketIO() error
// ProtectSocket protects a socket file descriptor from being routed through the VPN.
// On Android, this calls VpnService.protect() to prevent routing loops.
// On iOS, this marks the socket to bypass the VPN.
// Returns nil on success, error on failure.
ProtectSocket(fd int) error
}
// PacketCaptureController holds state for packet capture mode
type PacketCaptureController struct {
baseController *Controller
// Packet capture mode fields
netstackCtrl *netstack.NetstackController
dnsBridge *netstack.DNSBridge
packetStopCh chan struct{}
}
// NewPacketCaptureController creates a new packet capture controller
func NewPacketCaptureController(appCallback PacketAppCallback) *PacketCaptureController {
return &PacketCaptureController{
baseController: &Controller{AppCallback: appCallback},
packetStopCh: make(chan struct{}),
}
}
// StartWithPacketCapture starts ctrld in full packet capture mode for mobile.
// This method enables full IP packet processing with DNS filtering and upstream routing.
// It requires a PacketAppCallback that provides packet read/write capabilities.
func (pc *PacketCaptureController) StartWithPacketCapture(
packetCallback PacketAppCallback,
CdUID string,
ProvisionID string,
CustomHostname string,
HomeDir string,
UpstreamProto string,
logLevel int,
logPath string,
) error {
if pc.baseController.stopCh != nil {
return fmt.Errorf("controller already running")
}
// Set up configuration
pc.baseController.Config = cli.AppConfig{
CdUID: CdUID,
ProvisionID: ProvisionID,
CustomHostname: CustomHostname,
HomeDir: HomeDir,
UpstreamProto: UpstreamProto,
Verbose: logLevel,
LogPath: logPath,
}
pc.baseController.AppCallback = packetCallback
// Set global socket protector for HTTP client sockets (API calls, etc)
// This prevents routing loops when ctrld makes HTTP requests to api.controld.com
ctrld.SetSocketProtector(packetCallback.ProtectSocket)
// Create DNS bridge for communication between netstack and DNS proxy
pc.dnsBridge = netstack.NewDNSBridge()
pc.dnsBridge.Start()
// Create packet handler that wraps the mobile callbacks
packetHandler := netstack.NewMobilePacketHandler(
packetCallback.ReadPacket,
packetCallback.WritePacket,
packetCallback.ClosePacketIO,
packetCallback.ProtectSocket,
)
// Create DNS handler that uses the bridge
dnsHandler := func(query []byte) ([]byte, error) {
// Extract source IP from query context if available
// For now, use a placeholder
return pc.dnsBridge.ProcessQuery(query, "10.0.0.2", 0)
}
// Auto-detect platform and use appropriate TUN IP
// Android: TUN=10.0.0.1, Device=10.0.0.2
// iOS: TUN=10.0.0.2, Device=10.0.0.1
tunIP := "10.0.0.1" // Default for Android
// Check if running on iOS (no reliable way, so we'll make it configurable)
// For now, use Android config. iOS should update their VPN settings to match.
tunIPv4, err := netip.ParseAddr(tunIP)
if err != nil {
return fmt.Errorf("failed to parse TUN IPv4: %v", err)
}
netstackCfg := &netstack.Config{
MTU: 1500,
TUNIPv4: tunIPv4,
DNSHandler: dnsHandler,
UpstreamInterface: nil, // Will use default interface
}
ctrld.ProxyLogger.Load().Info().Msgf("[PacketCapture] Netstack TUN IP: %s", tunIP)
// Create netstack controller
netstackCtrl, err := netstack.NewNetstackController(packetHandler, netstackCfg)
if err != nil {
pc.dnsBridge.Stop()
return fmt.Errorf("failed to create netstack controller: %v", err)
}
pc.netstackCtrl = netstackCtrl
// Start netstack processing
if err := pc.netstackCtrl.Start(); err != nil {
pc.dnsBridge.Stop()
return fmt.Errorf("failed to start netstack: %v", err)
}
// Start regular ctrld DNS processing in background
// This allows us to use existing DNS filtering logic
pc.baseController.stopCh = make(chan struct{})
// Start DNS query processor that receives queries from the bridge
// and sends them to the ctrld DNS proxy
go pc.processDNSQueries()
// Start the main ctrld mobile runner
go func() {
appCallback := mapCallback(pc.baseController.AppCallback)
cli.RunMobile(&pc.baseController.Config, &appCallback, pc.baseController.stopCh)
}()
// Log platform detection for DNS proxy port
dnsPort := "5354"
if runtime.GOOS == "ios" || runtime.GOOS == "darwin" {
dnsPort = "53"
}
ctrld.ProxyLogger.Load().Info().Msgf("[PacketCapture] Platform: %s, DNS proxy port: %s", runtime.GOOS, dnsPort)
return nil
}
// processDNSQueries processes DNS queries from the bridge using the ctrld DNS proxy
func (pc *PacketCaptureController) processDNSQueries() {
queryCh := pc.dnsBridge.GetQueryChannel()
for {
select {
case <-pc.packetStopCh:
return
case <-pc.baseController.stopCh:
return
case query := <-queryCh:
go pc.handleDNSQuery(query)
}
}
}
// handleDNSQuery handles a single DNS query
func (pc *PacketCaptureController) handleDNSQuery(query *netstack.DNSQuery) {
// Parse DNS message
msg := new(dns.Msg)
if err := msg.Unpack(query.Query); err != nil {
return
}
// Determine DNS proxy port based on platform
// Android: 0.0.0.0:5354
// iOS: 127.0.0.1:53
dnsProxyAddr := "127.0.0.1:5354" // Default for Android
if runtime.GOOS == "ios" || runtime.GOOS == "darwin" {
// iOS uses port 53
dnsProxyAddr = "127.0.0.1:53"
}
// Send query to actual DNS proxy
client := &dns.Client{
Net: "udp",
Timeout: 3 * time.Second,
}
response, _, err := client.Exchange(msg, dnsProxyAddr)
if err != nil {
// Create SERVFAIL response
response = new(dns.Msg)
response.SetReply(msg)
response.Rcode = dns.RcodeServerFailure
}
// Pack response
responseBytes, err := response.Pack()
if err != nil {
return
}
// Send response back through bridge
pc.dnsBridge.SendResponse(query.ID, responseBytes)
}
// Stop stops the packet capture controller
func (pc *PacketCaptureController) Stop(restart bool, pin int64) int {
var errorCode = 0
// Clear global socket protector
ctrld.SetSocketProtector(nil)
// Stop DNS bridge
if pc.dnsBridge != nil {
pc.dnsBridge.Stop()
pc.dnsBridge = nil
}
// Stop netstack
if pc.netstackCtrl != nil {
if err := pc.netstackCtrl.Stop(); err != nil {
// Log error but continue shutdown
fmt.Printf("Error stopping netstack: %v\n", err)
}
pc.netstackCtrl = nil
}
// Close packet stop channel
if pc.packetStopCh != nil {
close(pc.packetStopCh)
pc.packetStopCh = make(chan struct{})
}
// Stop base controller
if !restart {
errorCode = cli.CheckDeactivationPin(pin, pc.baseController.stopCh)
}
if errorCode == 0 && pc.baseController.stopCh != nil {
close(pc.baseController.stopCh)
pc.baseController.stopCh = nil
}
return errorCode
}
// IsRunning returns true if the controller is running
func (pc *PacketCaptureController) IsRunning() bool {
return pc.baseController.stopCh != nil
}
// IsPacketMode returns true (always in packet mode for this controller)
func (pc *PacketCaptureController) IsPacketMode() bool {
return true
}

View File

@@ -21,6 +21,7 @@ import (
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/ameshkov/dnsstamps"
@@ -358,6 +359,15 @@ func (uc *UpstreamConfig) Init() {
}
}
// VerifyMsg creates and returns a new DNS message could be used for testing upstream health.
func (uc *UpstreamConfig) VerifyMsg() *dns.Msg {
msg := new(dns.Msg)
msg.RecursionDesired = true
msg.SetQuestion(".", dns.TypeNS)
msg.SetEdns0(4096, false) // ensure handling of large DNS response
return msg
}
// VerifyDomain returns the domain name that could be resolved by the upstream endpoint.
// It returns empty for non-ControlD upstream endpoint.
func (uc *UpstreamConfig) VerifyDomain() string {
@@ -546,7 +556,24 @@ func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport {
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
_, port, _ := net.SplitHostPort(addr)
if uc.BootstrapIP != "" {
dialer := net.Dialer{Timeout: dialerTimeout, KeepAlive: dialerTimeout}
// Create custom dialer with socket protection - matches working example pattern
dialer := &net.Dialer{
Timeout: dialerTimeout,
KeepAlive: dialerTimeout,
}
// Access underlying socket fd before connecting to it
dialer.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
Log(ctx, ProxyLogger.Load().Debug(), "Received DoH socket fd %d for %s", fd, address)
i := int(fd)
// Protect socket from VPN routing
if err := ProtectSocket(i); err != nil {
Log(ctx, ProxyLogger.Load().Warn(), "Failed to protect DoH socket fd=%d: %v", i, err)
} else {
Log(ctx, ProxyLogger.Load().Debug(), "Protected DoH socket fd=%d", i)
}
})
}
addr := net.JoinHostPort(uc.BootstrapIP, port)
Log(ctx, ProxyLogger.Load().Debug(), "sending doh request to: %s", addr)
return dialer.DialContext(ctx, network, addr)
@@ -562,6 +589,21 @@ func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport {
if err != nil {
return nil, err
}
// Protect DoH socket from VPN routing
if tcpConn, ok := conn.(*net.TCPConn); ok {
if rawConn, err := tcpConn.SyscallConn(); err == nil {
rawConn.Control(func(fd uintptr) {
i := int(fd)
if err := ProtectSocket(i); err != nil {
Log(ctx, ProxyLogger.Load().Warn(), "Failed to protect DoH socket fd=%d: %v", i, err)
} else {
Log(ctx, ProxyLogger.Load().Debug(), "Protected DoH socket fd=%d", i)
}
})
}
}
Log(ctx, ProxyLogger.Load().Debug(), "sending doh request to: %s", conn.RemoteAddr())
return conn, nil
}

View File

@@ -36,7 +36,7 @@ func (uc *UpstreamConfig) setupDOH3Transport() {
func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper {
rt := &http3.Transport{}
rt.TLSClientConfig = &tls.Config{RootCAs: uc.certPool}
rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
_, port, _ := net.SplitHostPort(addr)
// if we have a bootstrap ip set, use it to avoid DNS lookup
if uc.BootstrapIP != "" {
@@ -96,14 +96,14 @@ func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper {
// - quic dialer is different with net.Dialer
// - simplification for quic free version
type parallelDialerResult struct {
conn quic.EarlyConnection
conn *quic.Conn
err error
}
type quicParallelDialer struct{}
// Dial performs parallel dialing to the given address list.
func (d *quicParallelDialer) Dial(ctx context.Context, addrs []string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
func (d *quicParallelDialer) Dial(ctx context.Context, addrs []string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
if len(addrs) == 0 {
return nil, errors.New("empty addresses")
}

42
docs/known-issues.md Normal file
View File

@@ -0,0 +1,42 @@
# Known Issues
This document outlines known issues with ctrld and their current status, workarounds, and recommendations.
## macOS (Darwin) Issues
### Self-Upgrade Issue on Darwin 15.5
**Issue**: ctrld self-upgrading functionality may not work on macOS Darwin 15.5.
**Status**: Under investigation
**Description**: Users on macOS Darwin 15.5 may experience issues when ctrld attempts to perform automatic self-upgrades. The upgrade process would be triggered, but ctrld won't be upgraded.
**Workarounds**:
1. **Recommended**: Upgrade your macOS system to Darwin 15.6 or later, which has been tested and verified to work correctly with ctrld self-upgrade functionality.
2. **Alternative**: Run `ctrld upgrade prod` directly to manually upgrade ctrld to the latest version on Darwin 15.5.
**Affected Versions**: ctrld v1.4.2 and later on macOS Darwin 15.5
**Last Updated**: 05/09/2025
---
## Contributing to Known Issues
If you encounter an issue not listed here, please:
1. Check the [GitHub Issues](https://github.com/Control-D-Inc/ctrld/issues) to see if it's already reported
2. If not reported, create a new issue with:
- Detailed description of the problem
- Steps to reproduce
- Expected vs actual behavior
- System information (OS, version, architecture)
- ctrld version
## Issue Status Legend
- **Under investigation**: Issue is confirmed and being analyzed
- **Workaround available**: Temporary solution exists while permanent fix is developed
- **Fixed**: Issue has been resolved in a specific version
- **Won't fix**: Issue is acknowledged but will not be addressed due to technical limitations or design decisions

View File

@@ -142,7 +142,7 @@ func (s *testQUICServer) serve(t *testing.T) {
}
// handleConnection manages an individual QUIC connection by accepting and handling incoming streams in separate goroutines.
func (s *testQUICServer) handleConnection(t *testing.T, conn quic.Connection) {
func (s *testQUICServer) handleConnection(t *testing.T, conn *quic.Conn) {
for {
stream, err := conn.AcceptStream(context.Background())
if err != nil {
@@ -154,7 +154,7 @@ func (s *testQUICServer) handleConnection(t *testing.T, conn quic.Connection) {
}
// handleStream processes a single QUIC stream, reads DNS messages, generates a response, and sends it back to the client.
func (s *testQUICServer) handleStream(t *testing.T, stream quic.Stream) {
func (s *testQUICServer) handleStream(t *testing.T, stream *quic.Stream) {
defer stream.Close()
// Read length (2 bytes)

35
go.mod
View File

@@ -1,13 +1,11 @@
module github.com/Control-D-Inc/ctrld
go 1.23.0
toolchain go1.23.7
go 1.25.5
require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/ameshkov/dnsstamps v1.0.3
github.com/coreos/go-systemd/v22 v22.5.0
github.com/coreos/go-systemd/v22 v22.6.0
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf
github.com/docker/go-units v0.5.0
github.com/frankban/quicktest v1.14.6
@@ -29,17 +27,18 @@ require (
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/client_model v0.5.0
github.com/prometheus/prom2json v1.3.3
github.com/quic-go/quic-go v0.48.2
github.com/quic-go/quic-go v0.54.0
github.com/rs/zerolog v1.28.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.9.0
github.com/vishvananda/netlink v1.2.1-beta.2
golang.org/x/net v0.38.0
golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0
golang.org/x/net v0.52.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.42.0
golang.zx2c4.com/wireguard/windows v0.5.3
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987
tailscale.com v1.74.0
)
@@ -54,10 +53,9 @@ require (
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -74,7 +72,6 @@ require (
github.com/mdlayher/packet v1.1.2 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@@ -89,15 +86,17 @@ require (
github.com/subosito/gotenv v1.4.2 // indirect
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect
github.com/vishvananda/netns v0.0.4 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.23.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

67
go.sum
View File

@@ -62,8 +62,9 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf h1:40DHYsri+d1bnroFDU2FQAeq68f3kAlOzlQ93kCf26Q=
@@ -91,8 +92,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
@@ -103,8 +102,6 @@ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/j
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
@@ -137,6 +134,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -147,8 +146,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -162,8 +161,6 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -242,10 +239,6 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
@@ -271,8 +264,8 @@ github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcET
github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -330,8 +323,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
@@ -346,8 +339,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -375,6 +368,8 @@ golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPI
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60 h1:MOzyaj0wu2xneBkzkg9LHNYjDBB4W5vP043A2SYQRPA=
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60/go.mod h1:th6VJvzjMbrYF8SduQY5rpD0HG0GleGxjadkqSxFs3k=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@@ -383,8 +378,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -417,8 +412,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -438,8 +433,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -488,8 +483,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -500,13 +495,13 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -554,8 +549,8 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -650,8 +645,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -664,6 +659,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -16,4 +16,5 @@ var clientInfoFiles = map[string]ctrld.LeaseFileFormat{
"/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense
"/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla
"/var/lib/kea/dhcp4.leases": ctrld.KeaDHCP4, // Pfsense
"/var/db/dnsmasq.leases": ctrld.Dnsmasq, // OPNsense
}

View File

@@ -74,7 +74,6 @@ func (m *mdns) lookupIPByHostname(name string, v6 bool) string {
if value == name {
if addr, err := netip.ParseAddr(key.(string)); err == nil && addr.Is6() == v6 {
ip = addr.String()
//lint:ignore S1008 This is used for readable.
if addr.IsLoopback() { // Continue searching if this is loopback address.
return true
}

View File

@@ -104,7 +104,6 @@ func (p *ptrDiscover) lookupIPByHostname(name string, v6 bool) string {
if value == name {
if addr, err := netip.ParseAddr(key.(string)); err == nil && addr.Is6() == v6 {
ip = addr.String()
//lint:ignore S1008 This is used for readable.
if addr.IsLoopback() { // Continue searching if this is loopback address.
return true
}
@@ -120,8 +119,7 @@ func (p *ptrDiscover) lookupIPByHostname(name string, v6 bool) string {
// is reachable, set p.serverDown to false, so p.lookupHostname can continue working.
func (p *ptrDiscover) checkServer() {
bo := backoff.NewBackoff("ptrDiscover", func(format string, args ...any) {}, time.Minute*5)
m := new(dns.Msg)
m.SetQuestion(".", dns.TypeNS)
m := (&ctrld.UpstreamConfig{}).VerifyMsg()
ping := func() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

View File

@@ -13,11 +13,11 @@ import (
"os"
"runtime"
"strings"
"syscall"
"time"
"github.com/Control-D-Inc/ctrld"
"github.com/Control-D-Inc/ctrld/internal/certs"
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
"github.com/Control-D-Inc/ctrld/internal/router"
"github.com/Control-D-Inc/ctrld/internal/router/ddwrt"
)
@@ -244,8 +244,38 @@ func apiTransport(cdDev bool) *http.Transport {
}
dial := func(ctx context.Context, network string, addrs []string) (net.Conn, error) {
d := &ctrldnet.ParallelDialer{}
return d.DialContext(ctx, network, addrs, ctrld.ProxyLogger.Load())
// Create custom dialer with socket protection - matches working example pattern
baseDialer := &net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}
// Access underlying socket fd before connecting to it
baseDialer.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
ctrld.ProxyLogger.Load().Debug().Msgf("Received API socket fd %d for %s", fd, address)
i := int(fd)
// Protect socket from VPN routing
if err := ctrld.ProtectSocket(i); err != nil {
ctrld.ProxyLogger.Load().Warn().Err(err).Msgf("Failed to protect API socket fd=%d", i)
} else {
ctrld.ProxyLogger.Load().Debug().Msgf("Protected API socket fd=%d", i)
}
})
}
// Try each address with the protected dialer
var lastErr error
for _, addr := range addrs {
ctrld.ProxyLogger.Load().Debug().Msgf("dialing to %s", addr)
conn, err := baseDialer.DialContext(ctx, network, addr)
if err == nil {
return conn, nil
}
lastErr = err
ctrld.ProxyLogger.Load().Debug().Err(err).Msgf("failed to dial %s", addr)
}
return nil, lastErr
}
_, port, _ := net.SplitHostPort(addr)

View File

@@ -5,9 +5,12 @@ import (
"bytes"
"encoding/hex"
"net"
"net/netip"
"os"
"strings"
"tailscale.com/net/netmon"
"github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile"
)
@@ -128,3 +131,25 @@ func virtualInterfaces() set {
}
return s
}
// validInterfacesMap returns a set containing non virtual interfaces.
// TODO: deduplicated with cmd/cli/net_linux.go in v2.
func validInterfaces() set {
m := make(map[string]struct{})
vis := virtualInterfaces()
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
if _, existed := vis[i.Name]; existed {
return
}
m[i.Name] = struct{}{}
})
// Fallback to default route interface if found nothing.
if len(m) == 0 {
defaultRoute, err := netmon.DefaultRoute()
if err != nil {
return m
}
m[defaultRoute.InterfaceName] = struct{}{}
}
return m
}

View File

@@ -23,20 +23,17 @@ import (
)
const (
maxDNSAdapterRetries = 5
retryDelayDNSAdapter = 1 * time.Second
defaultDNSAdapterTimeout = 10 * time.Second
minDNSServers = 1 // Minimum number of DNS servers we want to find
NetSetupUnknown uint32 = 0
NetSetupWorkgroup uint32 = 1
NetSetupDomain uint32 = 2
NetSetupCloudDomain uint32 = 3
DS_FORCE_REDISCOVERY = 0x00000001
DS_DIRECTORY_SERVICE_REQUIRED = 0x00000010
DS_BACKGROUND_ONLY = 0x00000100
DS_IP_REQUIRED = 0x00000200
DS_IS_DNS_NAME = 0x00020000
DS_RETURN_DNS_NAME = 0x40000000
maxDNSAdapterRetries = 5
retryDelayDNSAdapter = 1 * time.Second
defaultDNSAdapterTimeout = 10 * time.Second
minDNSServers = 1 // Minimum number of DNS servers we want to find
DS_FORCE_REDISCOVERY = 0x00000001
DS_DIRECTORY_SERVICE_REQUIRED = 0x00000010
DS_BACKGROUND_ONLY = 0x00000100
DS_IP_REQUIRED = 0x00000200
DS_IS_DNS_NAME = 0x00020000
DS_RETURN_DNS_NAME = 0x40000000
)
type DomainControllerInfo struct {
@@ -158,7 +155,7 @@ func getDNSServers(ctx context.Context) ([]string, error) {
0, // DomainGuid - not needed
0, // SiteName - not needed
uintptr(flags), // Flags
uintptr(unsafe.Pointer(&info))) // DomainControllerInfo - output
uintptr(unsafe.Pointer(&info))) // DomainControllerInfo - output
if ret != 0 {
switch ret {
@@ -343,27 +340,28 @@ func checkDomainJoined() bool {
var domain *uint16
var status uint32
err := windows.NetGetJoinInformation(nil, &domain, &status)
if err != nil {
Log(context.Background(), logger.Debug(),
"Failed to get domain join status: %v", err)
if err := windows.NetGetJoinInformation(nil, &domain, &status); err != nil {
Log(context.Background(), logger.Debug(), "Failed to get domain join status: %v", err)
return false
}
defer windows.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
// NETSETUP_JOIN_STATUS constants from Microsoft Windows API
// See: https://learn.microsoft.com/en-us/windows/win32/api/lmjoin/ne-lmjoin-netsetup_join_status
//
// NetSetupUnknownStatus uint32 = 0 // The status is unknown
// NetSetupUnjoined uint32 = 1 // The computer is not joined to a domain or workgroup
// NetSetupWorkgroupName uint32 = 2 // The computer is joined to a workgroup
// NetSetupDomainName uint32 = 3 // The computer is joined to a domain
//
// We only care about NetSetupDomainName.
domainName := windows.UTF16PtrToString(domain)
Log(context.Background(), logger.Debug(),
"Domain join status: domain=%s status=%d (Unknown=0, Workgroup=1, Domain=2, CloudDomain=3)",
"Domain join status: domain=%s status=%d (UnknownStatus=0, Unjoined=1, WorkgroupName=2, DomainName=3)",
domainName, status)
// Consider domain or cloud domain as domain-joined
isDomain := status == NetSetupDomain || status == NetSetupCloudDomain
Log(context.Background(), logger.Debug(),
"Is domain joined? status=%d, traditional=%v, cloud=%v, result=%v",
status,
status == NetSetupDomain,
status == NetSetupCloudDomain,
isDomain)
isDomain := status == syscall.NetSetupDomainName
Log(context.Background(), logger.Debug(), "Is domain joined? status=%d, result=%v", status, isDomain)
return isDomain
}

35
net_darwin.go Normal file
View File

@@ -0,0 +1,35 @@
package ctrld
import (
"bufio"
"bytes"
"io"
"os/exec"
"strings"
)
// validInterfaces returns a set of all valid hardware ports.
// TODO: deduplicated with cmd/cli/net_darwin.go in v2.
func validInterfaces() map[string]struct{} {
b, err := exec.Command("networksetup", "-listallhardwareports").Output()
if err != nil {
return nil
}
return parseListAllHardwarePorts(bytes.NewReader(b))
}
// parseListAllHardwarePorts parses output of "networksetup -listallhardwareports"
// and returns map presents all hardware ports.
func parseListAllHardwarePorts(r io.Reader) map[string]struct{} {
m := make(map[string]struct{})
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
after, ok := strings.CutPrefix(line, "Device: ")
if !ok {
continue
}
m[after] = struct{}{}
}
return m
}

View File

@@ -1,4 +1,4 @@
package cli
package ctrld
import (
"maps"

15
net_others.go Normal file
View File

@@ -0,0 +1,15 @@
//go:build !darwin && !windows && !linux
package ctrld
import "tailscale.com/net/netmon"
// validInterfaces returns a set containing only default route interfaces.
// TODO: deuplicated with cmd/cli/net_others.go in v2.
func validInterfaces() map[string]struct{} {
defaultRoute, err := netmon.DefaultRoute()
if err != nil {
return nil
}
return map[string]struct{}{defaultRoute.InterfaceName: {}}
}

View File

@@ -62,8 +62,29 @@ var (
or *osResolver
defaultLocalIPv4 atomic.Value // holds net.IP (IPv4)
defaultLocalIPv6 atomic.Value // holds net.IP (IPv6)
// socketProtector is a global function that can be set by mobile apps to protect
// sockets from being routed through the VPN. This prevents routing loops.
socketProtector atomic.Value // holds func(int) error
)
// SetSocketProtector sets the global socket protection function.
// This should be called by mobile VPN apps to prevent routing loops.
func SetSocketProtector(protectFunc func(int) error) {
socketProtector.Store(protectFunc)
}
// ProtectSocket protects a socket using the globally set protector.
// Returns nil if no protector is set.
func ProtectSocket(fd int) error {
if v := socketProtector.Load(); v != nil {
if protectFunc, ok := v.(func(int) error); ok {
return protectFunc(fd)
}
}
return nil
}
func newLocalResolver() Resolver {
var nss []string
for _, addr := range Rfc1918Addresses() {
@@ -729,10 +750,15 @@ func newResolverWithNameserver(nameservers []string) *osResolver {
return r
}
// Rfc1918Addresses returns the list of local interfaces private IP addresses
// Rfc1918Addresses returns the list of local physical interfaces private IP addresses
func Rfc1918Addresses() []string {
vis := validInterfaces()
var res []string
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
// Skip virtual interfaces.
if _, existed := vis[i.Name]; !existed {
return
}
addrs, _ := i.Addrs()
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)

View File

@@ -282,6 +282,35 @@ func Test_Edns0_CacheReply(t *testing.T) {
}
}
// https://github.com/Control-D-Inc/ctrld/issues/255
func Test_legacyResolverWithBigExtraSection(t *testing.T) {
lanPC, err := net.ListenPacket("udp", "127.0.0.1:0") // 127.0.0.1 is considered LAN (loopback)
if err != nil {
t.Fatalf("failed to listen on LAN address: %v", err)
}
lanServer, lanAddr, err := runLocalPacketConnTestServer(t, lanPC, bigExtraSectionHandler())
if err != nil {
t.Fatalf("failed to run LAN test server: %v", err)
}
defer lanServer.Shutdown()
uc := &UpstreamConfig{
Name: "Legacy",
Type: ResolverTypeLegacy,
Endpoint: lanAddr,
}
uc.Init()
r, err := NewResolver(uc)
if err != nil {
t.Fatal(err)
}
_, err = r.Resolve(context.Background(), uc.VerifyMsg())
if err != nil {
t.Fatal(err)
}
}
func Test_upstreamTypeFromEndpoint(t *testing.T) {
tests := []struct {
name string
@@ -370,6 +399,68 @@ func countHandler(call *atomic.Int64) dns.HandlerFunc {
}
}
func mustRR(s string) dns.RR {
r, err := dns.NewRR(s)
if err != nil {
panic(err)
}
return r
}
func bigExtraSectionHandler() dns.HandlerFunc {
return func(w dns.ResponseWriter, msg *dns.Msg) {
m := &dns.Msg{
Answer: []dns.RR{
mustRR(". 7149 IN NS m.root-servers.net."),
mustRR(". 7149 IN NS c.root-servers.net."),
mustRR(". 7149 IN NS e.root-servers.net."),
mustRR(". 7149 IN NS j.root-servers.net."),
mustRR(". 7149 IN NS g.root-servers.net."),
mustRR(". 7149 IN NS k.root-servers.net."),
mustRR(". 7149 IN NS l.root-servers.net."),
mustRR(". 7149 IN NS d.root-servers.net."),
mustRR(". 7149 IN NS h.root-servers.net."),
mustRR(". 7149 IN NS b.root-servers.net."),
mustRR(". 7149 IN NS a.root-servers.net."),
mustRR(". 7149 IN NS f.root-servers.net."),
mustRR(". 7149 IN NS i.root-servers.net."),
},
Extra: []dns.RR{
mustRR("m.root-servers.net. 656 IN A 202.12.27.33"),
mustRR("m.root-servers.net. 656 IN AAAA 2001:dc3::35"),
mustRR("c.root-servers.net. 656 IN A 192.33.4.12"),
mustRR("c.root-servers.net. 656 IN AAAA 2001:500:2::c"),
mustRR("e.root-servers.net. 656 IN A 192.203.230.10"),
mustRR("e.root-servers.net. 656 IN AAAA 2001:500:a8::e"),
mustRR("j.root-servers.net. 656 IN A 192.58.128.30"),
mustRR("j.root-servers.net. 656 IN AAAA 2001:503:c27::2:30"),
mustRR("g.root-servers.net. 656 IN A 192.112.36.4"),
mustRR("g.root-servers.net. 656 IN AAAA 2001:500:12::d0d"),
mustRR("k.root-servers.net. 656 IN A 193.0.14.129"),
mustRR("k.root-servers.net. 656 IN AAAA 2001:7fd::1"),
mustRR("l.root-servers.net. 656 IN A 199.7.83.42"),
mustRR("l.root-servers.net. 656 IN AAAA 2001:500:9f::42"),
mustRR("d.root-servers.net. 656 IN A 199.7.91.13"),
mustRR("d.root-servers.net. 656 IN AAAA 2001:500:2d::d"),
mustRR("h.root-servers.net. 656 IN A 198.97.190.53"),
mustRR("h.root-servers.net. 656 IN AAAA 2001:500:1::53"),
mustRR("b.root-servers.net. 656 IN A 170.247.170.2"),
mustRR("b.root-servers.net. 656 IN AAAA 2801:1b8:10::b"),
mustRR("a.root-servers.net. 656 IN A 198.41.0.4"),
mustRR("a.root-servers.net. 656 IN AAAA 2001:503:ba3e::2:30"),
mustRR("f.root-servers.net. 656 IN A 192.5.5.241"),
mustRR("f.root-servers.net. 656 IN AAAA 2001:500:2f::f"),
mustRR("i.root-servers.net. 656 IN A 192.36.148.17"),
mustRR("i.root-servers.net. 656 IN AAAA 2001:7fe::53"),
},
}
m.Compress = true
m.SetReply(msg)
w.WriteMsg(m)
}
}
func generateEdns0ClientCookie() string {
cookie := make([]byte, 8)
if _, err := rand.Read(cookie); err != nil {