Compare commits

..

7 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
35 changed files with 2119 additions and 492 deletions

View File

@@ -15,12 +15,12 @@ jobs:
- uses: actions/checkout@v3
with:
fetch-depth: 1
- uses: actions/setup-go@v6
- uses: WillAbides/setup-go-faster@v1.8.0
with:
go-version: ${{ matrix.go }}
- run: "go test -race ./..."
- uses: dominikh/staticcheck-action@v1.4.0
- uses: dominikh/staticcheck-action@v1.3.1
with:
version: "2025.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

@@ -10,12 +10,11 @@ import (
hh "github.com/microsoft/wmi/pkg/hardware/host"
"github.com/Control-D-Inc/ctrld"
"github.com/Control-D-Inc/ctrld/internal/system"
)
// addExtraSplitDnsRule adds split DNS rule for domain if it's part of active directory.
func addExtraSplitDnsRule(cfg *ctrld.Config) bool {
domain, err := system.GetActiveDirectoryDomain()
domain, err := getActiveDirectoryDomain()
if err != nil {
mainLog.Load().Debug().Msgf("unable to get active directory domain: %v", err)
return false

View File

@@ -5,16 +5,14 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/Control-D-Inc/ctrld"
"github.com/Control-D-Inc/ctrld/internal/system"
"github.com/Control-D-Inc/ctrld/testhelper"
"github.com/stretchr/testify/assert"
)
func Test_getActiveDirectoryDomain(t *testing.T) {
start := time.Now()
domain, err := system.GetActiveDirectoryDomain()
domain, err := getActiveDirectoryDomain()
if err != nil {
t.Fatal(err)
}

View File

@@ -282,7 +282,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
}
p.mu.Unlock()
processLogAndCacheFlags(v, &cfg)
processLogAndCacheFlags()
// Log config do not have thing to validate, so it's safe to init log here,
// so it's able to log information in processCDFlags.
@@ -342,7 +342,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
updated := updateListenerConfig(&cfg, notifyExitToLogServer)
if cdUID != "" {
processLogAndCacheFlags(v, &cfg)
processLogAndCacheFlags()
}
if updated {
@@ -642,19 +642,13 @@ func processCDFlags(cfg *ctrld.Config) (*controld.ResolverConfig, error) {
logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID)
bo := backoff.NewBackoff("processCDFlags", logf, 30*time.Second)
bo.LogLongerThan = 30 * time.Second
ctx := context.Background()
req := &controld.ResolverConfigRequest{
RawUID: cdUID,
Version: rootCmd.Version,
Metadata: ctrld.SystemMetadata(ctx),
}
resolverConfig, err := controld.FetchResolverConfig(req, cdDev)
resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
for {
if errUrlNetworkError(err) {
bo.BackOff(ctx, err)
logger.Warn().Msg("could not fetch resolver using bootstrap DNS, retrying...")
resolverConfig, err = controld.FetchResolverConfig(req, cdDev)
resolverConfig, err = controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
continue
}
break
@@ -786,8 +780,7 @@ func processListenFlag() {
})
}
// processLogAndCacheFlags processes log and cache related flags
func processLogAndCacheFlags(v *viper.Viper, cfg *ctrld.Config) {
func processLogAndCacheFlags() {
if logPath != "" {
cfg.Service.LogPath = logPath
}
@@ -1522,12 +1515,7 @@ func cdUIDFromProvToken() string {
if customHostname != "" && !validHostname(customHostname) {
mainLog.Load().Fatal().Msgf("invalid custom hostname: %q", customHostname)
}
req := &controld.UtilityOrgRequest{
ProvToken: cdOrg,
Hostname: customHostname,
Metadata: ctrld.SystemMetadata(context.Background()),
}
req := &controld.UtilityOrgRequest{ProvToken: cdOrg, Hostname: customHostname}
// Process provision token if provided.
resolverConfig, err := controld.FetchResolverUID(req, rootCmd.Version, cdDev)
if err != nil {
@@ -1868,12 +1856,7 @@ func runningIface(s service.Service) *ifaceResponse {
// doValidateCdRemoteConfig fetches and validates custom config for cdUID.
func doValidateCdRemoteConfig(cdUID string, fatal bool) error {
req := &controld.ResolverConfigRequest{
RawUID: cdUID,
Version: rootCmd.Version,
Metadata: ctrld.SystemMetadata(context.Background()),
}
rc, err := controld.FetchResolverConfig(req, cdDev)
rc, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
if err != nil {
logger := mainLog.Load().Fatal()
if !fatal {

View File

@@ -217,12 +217,7 @@ func (p *prog) registerControlServerHandler() {
}
// Re-fetch pin code from API.
rcReq := &controld.ResolverConfigRequest{
RawUID: cdUID,
Version: rootCmd.Version,
Metadata: ctrld.SystemMetadata(context.Background()),
}
if rc, err := controld.FetchResolverConfig(rcReq, cdDev); rc != nil {
if rc, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev); rc != nil {
if rc.DeactivationPin != nil {
cdDeactivationPin.Store(*rc.DeactivationPin)
} else {

View File

@@ -954,13 +954,7 @@ func (p *prog) doSelfUninstall(answer *dns.Msg) {
logger := mainLog.Load().With().Str("mode", "self-uninstall").Logger()
if p.refusedQueryCount > selfUninstallMaxQueries {
p.checkingSelfUninstall = true
req := &controld.ResolverConfigRequest{
RawUID: cdUID,
Version: rootCmd.Version,
Metadata: ctrld.SystemMetadata(context.Background()),
}
_, err := controld.FetchResolverConfig(req, cdDev)
_, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
logger.Debug().Msg("maximum number of refused queries reached, checking device status")
selfUninstallCheck(err, p, logger)

View File

@@ -72,15 +72,7 @@ func setDNS(iface *net.Interface, nameservers []string) error {
SearchDomains: []dnsname.FQDN{},
}
if sds, err := searchDomains(); err == nil {
// Filter the root domain, since it's not allowed by systemd.
// See https://github.com/systemd/systemd/issues/9515
filteredSds := slices.DeleteFunc(sds, func(s dnsname.FQDN) bool {
return s == "" || s == "."
})
if len(filteredSds) != len(sds) {
mainLog.Load().Debug().Msg(`Removed root domain "." from search domains list`)
}
osConfig.SearchDomains = filteredSds
osConfig.SearchDomains = sds
} else {
mainLog.Load().Debug().Err(err).Msg("failed to get search domains list")
}

View File

@@ -213,8 +213,7 @@ func (p *prog) runWait() {
continue
}
if cdUID != "" {
rc, err := processCDFlags(newCfg)
if err != nil {
if rc, err := processCDFlags(newCfg); err != nil {
logger.Err(err).Msg("could not fetch ControlD config")
waitOldRunDone()
continue
@@ -226,10 +225,6 @@ func (p *prog) runWait() {
}
}
// Though the log configuration could not be changed during reloading, we still need to
// process the current flags here, so runtime internal logs can be used correctly.
processLogAndCacheFlags(v, newCfg)
waitOldRunDone()
p.mu.Lock()
@@ -325,12 +320,7 @@ func (p *prog) apiConfigReload() {
}
doReloadApiConfig := func(forced bool, logger zerolog.Logger) {
req := &controld.ResolverConfigRequest{
RawUID: cdUID,
Version: rootCmd.Version,
Metadata: ctrld.SystemMetadata(context.Background()),
}
resolverConfig, err := controld.FetchResolverConfig(req, cdDev)
resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
selfUninstallCheck(err, p, logger)
if err != nil {
logger.Warn().Err(err).Msg("could not fetch resolver config")

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"
@@ -555,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)
@@ -571,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

@@ -1,46 +0,0 @@
# Runtime Internal Logging
When no logging is configured (i.e., `log_path` is not set), ctrld automatically enables an internal logging system. This system stores logs in memory to provide troubleshooting information when problems occur.
## Purpose
The runtime internal logging system is designed primarily for **ctrld developers**, not end users. It captures detailed diagnostic information that can be useful for troubleshooting issues when they arise, especially in production environments where explicit logging may not be configured.
## When It's Enabled
Internal logging is automatically enabled when:
- ctrld is running in Control D mode (i.e., `--cd` flag is provided)
- No log file is configured (i.e., `log_path` is empty or not set)
If a log file is explicitly configured via `log_path`, internal logging will **not** be enabled, as the configured log file serves the logging purpose.
## How It Works
The internal logging system:
- Stores logs in **in-memory buffers** (not written to disk)
- Captures logs at **debug level** for normal operations and **warn level** for warnings
- Maintains separate buffers for normal logs and warning logs
- Automatically manages buffer size to prevent unbounded memory growth
- Preserves initialization logs even when buffers overflow
## Configuration
**Important**: The `log_level` configuration option does **not** affect the internal logging system. Internal logging always operates at debug level for normal logs and warn level for warnings, regardless of the `log_level` setting in the configuration file.
The `log_level` setting only affects:
- Console output (when running interactively)
- File-based logging (when `log_path` is configured)
## Accessing Internal Logs
Internal logs can be accessed through the control server API endpoints. This functionality is intended for developers and support personnel who need to diagnose issues.
## Notes
- Internal logging is **not** a replacement for proper log file configuration in production environments
- For production deployments, it is recommended to configure `log_path` to enable persistent file-based logging
- Internal logs are stored in memory and will be lost if the process terminates unexpectedly
- The internal logging system is automatically disabled when explicit logging is configured

5
doh.go
View File

@@ -122,6 +122,11 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
}
if err != nil {
err = wrapUrlError(err)
if r.isDoH3 {
if closer, ok := c.Transport.(io.Closer); ok {
closer.Close()
}
}
return nil, fmt.Errorf("could not perform request: %w", err)
}
defer resp.Body.Close()

44
go.mod
View File

@@ -1,12 +1,11 @@
module github.com/Control-D-Inc/ctrld
go 1.24
go 1.25.5
require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/ameshkov/dnsstamps v1.0.3
github.com/brunogui0812/sysprofiler v0.5.0
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
@@ -16,7 +15,6 @@ require (
github.com/hashicorp/golang-lru/v2 v2.0.1
github.com/illarion/gonotify/v2 v2.0.3
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2
github.com/jaypipes/ghw v0.21.0
github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
github.com/kardianos/service v1.2.1
@@ -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.57.1
github.com/quic-go/quic-go v0.54.0
github.com/rs/zerolog v1.28.0
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
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.11.1
github.com/stretchr/testify v1.9.0
github.com/vishvananda/netlink v1.2.1-beta.2
golang.org/x/net v0.43.0
golang.org/x/sync v0.16.0
golang.org/x/sys v0.35.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
)
@@ -55,12 +54,11 @@ require (
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // 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/groob/plist v0.0.0-20200425180238-0f631f258c01 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jaypipes/pcidb v1.1.1 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
@@ -79,28 +77,28 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/spakin/awk v1.0.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
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
github.com/yusufpapurcu/wmi v1.2.4 // 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.41.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.27.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.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
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect
)
replace github.com/mr-karan/doggo => github.com/Windscribe/doggo v0.0.0-20220919152748-2c118fc391f8

88
go.sum
View File

@@ -50,8 +50,6 @@ github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1O
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/brunogui0812/sysprofiler v0.5.0 h1:AUekplOKG/VKH6sPSBRxsKOA9Uv5OsI8qolXM73dXPU=
github.com/brunogui0812/sysprofiler v0.5.0/go.mod h1:lLd7gvylgd4nsTSC8exq1YY6qhLWXkgnalxjVzdlbEM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -64,9 +62,10 @@ 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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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=
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf/go.mod h1:G45410zMgmnSjLVKCq4f6GpbYAzoP2plX9rPwgx6C24=
@@ -93,7 +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-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
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=
@@ -136,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=
@@ -146,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=
@@ -168,8 +168,6 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/groob/plist v0.0.0-20200425180238-0f631f258c01 h1:0T3XGXebqLj7zSVLng9wX9axQzTEnvj/h6eT7iLfUas=
github.com/groob/plist v0.0.0-20200425180238-0f631f258c01/go.mod h1:itkABA+w2cw7x5nYUS/pLRef6ludkZKOigbROmCTaFw=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4=
@@ -186,13 +184,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
github.com/jaypipes/ghw v0.21.0 h1:ClG2xWtYY0c1ud9jZYwVGdSgfCI7AbmZmZyw3S5HHz8=
github.com/jaypipes/ghw v0.21.0/go.mod h1:GPrvwbtPoxYUenr74+nAnWbardIZq600vJDD5HnPsPE=
github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro=
github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8=
github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c h1:kbTQ8oGf+BVFvt/fM+ECI+NbZDCqoi0vtZTfB2p2hrI=
github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c/go.mod h1:k6+89xKz7BSMJ+DzIerBdtpEUeTlBMugO/hcVSzahog=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
@@ -269,10 +262,10 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo=
github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
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.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=
@@ -284,18 +277,16 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spakin/awk v1.0.0 h1:5ulBVgJhdN3XoFGNVv/MOHOIUfPVPvMCIlLH6O6ZqU4=
github.com/spakin/awk v1.0.0/go.mod h1:e7FnxcIEcRqdKwStPYWonox4n9DpharWk+3nnn1IqJs=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -311,8 +302,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
@@ -326,16 +317,14 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
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.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
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=
@@ -350,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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
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=
@@ -379,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=
@@ -387,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.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
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=
@@ -421,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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
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=
@@ -442,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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.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=
@@ -453,7 +444,6 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -493,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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.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=
@@ -505,8 +495,8 @@ 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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
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=
@@ -559,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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
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=
@@ -655,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=
@@ -669,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=
@@ -676,8 +668,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 h1:eeH1AIcPvSc0Z25ThsYF+Xoqbn0CI/YnXVYoTLFdGQw=
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9/go.mod h1:fyFX5Hj5tP1Mpk8obqA9MZgXT416Q5711SDT7dQLTLk=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

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"
)
@@ -71,23 +71,14 @@ func (u ErrorResponse) Error() string {
}
type utilityRequest struct {
UID string `json:"uid"`
ClientID string `json:"client_id,omitempty"`
Metadata map[string]string `json:"metadata"`
UID string `json:"uid"`
ClientID string `json:"client_id,omitempty"`
}
// UtilityOrgRequest contains request data for calling Org API.
type UtilityOrgRequest struct {
ProvToken string `json:"prov_token"`
Hostname string `json:"hostname"`
Metadata map[string]string `json:"metadata"`
}
// ResolverConfigRequest contains request data for fetching resolver config.
type ResolverConfigRequest struct {
RawUID string
Version string
Metadata map[string]string
ProvToken string `json:"prov_token"`
Hostname string `json:"hostname"`
}
// LogsRequest contains request data for sending runtime logs to API.
@@ -97,30 +88,26 @@ type LogsRequest struct {
}
// FetchResolverConfig fetch Control D config for given uid.
func FetchResolverConfig(req *ResolverConfigRequest, cdDev bool) (*ResolverConfig, error) {
uid, clientID := ParseRawUID(req.RawUID)
uReq := utilityRequest{
UID: uid,
Metadata: req.Metadata,
}
func FetchResolverConfig(rawUID, version string, cdDev bool) (*ResolverConfig, error) {
uid, clientID := ParseRawUID(rawUID)
req := utilityRequest{UID: uid}
if clientID != "" {
uReq.ClientID = clientID
req.ClientID = clientID
}
body, _ := json.Marshal(uReq)
return postUtilityAPI(req.Version, cdDev, false, bytes.NewReader(body))
body, _ := json.Marshal(req)
return postUtilityAPI(version, cdDev, false, bytes.NewReader(body))
}
// FetchResolverUID fetch resolver uid from a given request.
// FetchResolverUID fetch resolver uid from provision token.
func FetchResolverUID(req *UtilityOrgRequest, version string, cdDev bool) (*ResolverConfig, error) {
if req == nil {
return nil, errors.New("invalid request")
}
if req.Hostname == "" {
hostname, _ := os.Hostname()
req.Hostname = hostname
hostname := req.Hostname
if hostname == "" {
hostname, _ = os.Hostname()
}
body, _ := json.Marshal(req)
body, _ := json.Marshal(UtilityOrgRequest{ProvToken: req.ProvToken, Hostname: hostname})
return postUtilityAPI(version, cdDev, false, bytes.NewReader(body))
}
@@ -257,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

@@ -3,13 +3,10 @@
package controld
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/Control-D-Inc/ctrld"
)
func TestFetchResolverConfig(t *testing.T) {
@@ -23,18 +20,11 @@ func TestFetchResolverConfig(t *testing.T) {
{"valid dev", "p2", true, false},
{"invalid uid", "abcd1234", false, true},
}
ctx := context.Background()
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
req := &ResolverConfigRequest{
RawUID: tc.uid,
Version: "dev-test",
Metadata: ctrld.SystemMetadata(ctx),
}
got, err := FetchResolverConfig(ctx, req, tc.dev)
got, err := FetchResolverConfig(tc.uid, "dev-test", tc.dev)
require.False(t, (err != nil) != tc.wantErr, err)
if !tc.wantErr {
assert.NotEmpty(t, got.DOH)

View File

@@ -1,25 +0,0 @@
package system
import (
"errors"
"fmt"
"github.com/brunogui0812/sysprofiler"
)
// GetChassisInfo retrieves hardware information including machine model type and vendor from the system profiler.
func GetChassisInfo() (*ChassisInfo, error) {
hardwares, err := sysprofiler.Hardware()
if err != nil {
return nil, fmt.Errorf("failed to get hardware info: %w", err)
}
if len(hardwares) == 0 {
return nil, errors.New("no hardware info found")
}
hardware := hardwares[0]
info := &ChassisInfo{
Type: hardware.MachineModel,
Vendor: "Apple Inc.",
}
return info, nil
}

View File

@@ -1,20 +0,0 @@
//go:build !darwin
package system
import "github.com/jaypipes/ghw"
// GetChassisInfo retrieves hardware information including machine model type and vendor from the system profiler.
func GetChassisInfo() (*ChassisInfo, error) {
// Disable warnings from ghw, since these are undesirable but recoverable errors.
// With warnings enabled, ghw will emit unnecessary log messages.
chassis, err := ghw.Chassis(ghw.WithDisableWarnings())
if err != nil {
return nil, err
}
info := &ChassisInfo{
Type: chassis.TypeDescription,
Vendor: chassis.Vendor,
}
return info, nil
}

View File

@@ -1,7 +0,0 @@
package system
// ChassisInfo represents the structural framework of a device, specifying its type and manufacturer information.
type ChassisInfo struct {
Type string
Vendor string
}

View File

@@ -1,8 +0,0 @@
//go:build !windows
package system
// GetActiveDirectoryDomain returns AD domain name of this computer.
func GetActiveDirectoryDomain() (string, error) {
return "", nil
}

View File

@@ -1,74 +0,0 @@
package system
import (
"errors"
"fmt"
"io"
"log"
"os"
"strings"
"unsafe"
"github.com/microsoft/wmi/pkg/base/host"
hh "github.com/microsoft/wmi/pkg/hardware/host"
"golang.org/x/sys/windows"
)
// GetActiveDirectoryDomain returns AD domain name of this computer.
func GetActiveDirectoryDomain() (string, error) {
log.SetOutput(io.Discard)
defer log.SetOutput(os.Stderr)
// 1) Check environment variable
envDomain := os.Getenv("USERDNSDOMAIN")
if envDomain != "" {
return strings.TrimSpace(envDomain), nil
}
// 2) Query WMI via the microsoft/wmi library
whost := host.NewWmiLocalHost()
cs, err := hh.GetComputerSystem(whost)
if cs != nil {
defer cs.Close()
}
if err != nil {
return "", err
}
pod, err := cs.GetPropertyPartOfDomain()
if err != nil {
return "", err
}
if pod {
domainVal, err := cs.GetPropertyDomain()
if err != nil {
return "", fmt.Errorf("failed to get domain property: %w", err)
}
domainName := strings.TrimSpace(fmt.Sprintf("%v", domainVal))
if domainName == "" {
return "", errors.New("machine does not appear to have a domain set")
}
return domainName, nil
}
return "", nil
}
// DomainJoinedStatus returns the domain joined status of the current computer.
//
// 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
func DomainJoinedStatus() (uint32, error) {
var domain *uint16
var status uint32
if err := windows.NetGetJoinInformation(nil, &domain, &status); err != nil {
return 0, fmt.Errorf("failed to get domain join status: %w", err)
}
defer windows.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
return status, nil
}

View File

@@ -1,78 +0,0 @@
package ctrld
import (
"context"
"os"
"os/user"
"github.com/cuonglm/osinfo"
"github.com/Control-D-Inc/ctrld/internal/system"
)
const (
metadataOsKey = "os"
metadataChassisTypeKey = "chassis_type"
metadataChassisVendorKey = "chassis_vendor"
metadataUsernameKey = "username"
metadataDomainOrWorkgroupKey = "domain_or_workgroup"
metadataDomainKey = "domain"
)
var (
chassisType string
chassisVendor string
)
// SystemMetadata collects system and user-related SystemMetadata and returns it as a map.
func SystemMetadata(ctx context.Context) map[string]string {
m := make(map[string]string)
oi := osinfo.New()
m[metadataOsKey] = oi.String()
if chassisType == "" && chassisVendor == "" {
if ci, err := system.GetChassisInfo(); err == nil {
chassisType, chassisVendor = ci.Type, ci.Vendor
}
}
m[metadataChassisTypeKey] = chassisType
m[metadataChassisVendorKey] = chassisVendor
m[metadataUsernameKey] = currentLoginUser(ctx)
m[metadataDomainOrWorkgroupKey] = partOfDomainOrWorkgroup(ctx)
domain, err := system.GetActiveDirectoryDomain()
if err != nil {
ProxyLogger.Load().Debug().Err(err).Msg("Failed to get active directory domain name")
}
m[metadataDomainKey] = domain
return m
}
// currentLoginUser attempts to find the actual login user, even if the process is running as root.
func currentLoginUser(ctx context.Context) string {
// 1. Check SUDO_USER: This is the most reliable way to find the original user
// when a script is run via 'sudo'.
if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
return sudoUser
}
// 2. Check general user login variables. LOGNAME is often preferred over USER.
if logName := os.Getenv("LOGNAME"); logName != "" {
return logName
}
// 3. Fallback to USER variable.
if userEnv := os.Getenv("USER"); userEnv != "" {
return userEnv
}
// 4. Final fallback: Use the standard library function to get the *effective* user.
// This will return "root" if the process is running as root.
currentUser, err := user.Current()
if err != nil {
// Handle error gracefully, returning a placeholder
ProxyLogger.Load().Debug().Err(err).Msg("Failed to get current user")
return "unknown"
}
return currentUser.Username
}

View File

@@ -1,10 +0,0 @@
//go:build !windows
package ctrld
import "context"
// partOfDomainOrWorkgroup checks if the computer is part of a domain or workgroup and returns "true" or "false".
func partOfDomainOrWorkgroup(ctx context.Context) string {
return "false"
}

View File

@@ -1,11 +0,0 @@
package ctrld
import (
"context"
"testing"
)
func Test_metadata(t *testing.T) {
m := SystemMetadata(context.Background())
t.Logf("metadata: %v", m)
}

View File

@@ -1,22 +0,0 @@
package ctrld
import (
"context"
"github.com/Control-D-Inc/ctrld/internal/system"
)
// partOfDomainOrWorkgroup checks if the computer is part of a domain or workgroup and returns "true" or "false".
func partOfDomainOrWorkgroup(ctx context.Context) string {
status, err := system.DomainJoinedStatus()
if err != nil {
ProxyLogger.Load().Debug().Err(err).Msg("Failed to get domain join status")
return "false"
}
switch status {
case 2, 3:
return "true"
default:
return "false"
}
}

View File

@@ -20,8 +20,6 @@ import (
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/net/netmon"
"github.com/Control-D-Inc/ctrld/internal/system"
)
const (
@@ -130,7 +128,7 @@ func getDNSServers(ctx context.Context) ([]string, error) {
var dcServers []string
isDomain := checkDomainJoined()
if isDomain {
domainName, err := system.GetActiveDirectoryDomain()
domainName, err := getLocalADDomain()
if err != nil {
Log(context.Background(), logger.Debug(),
"Failed to get local AD domain: %v", err)
@@ -339,18 +337,75 @@ func currentNameserversFromResolvconf() []string {
func checkDomainJoined() bool {
logger := *ProxyLogger.Load()
status, err := system.DomainJoinedStatus()
if err != nil {
logger.Debug().Msgf("Failed to get domain joined status: %v", err)
var domain *uint16
var status uint32
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 (UnknownStatus=0, Unjoined=1, WorkgroupName=2, DomainName=3)",
domainName, status)
isDomain := status == syscall.NetSetupDomainName
logger.Debug().Msg("Domain join status: (UnknownStatus=0, Unjoined=1, WorkgroupName=2, DomainName=3)")
logger.Debug().Msgf("Is domain joined? status=%d, result=%v", status, isDomain)
Log(context.Background(), logger.Debug(), "Is domain joined? status=%d, result=%v", status, isDomain)
return isDomain
}
// getLocalADDomain uses Microsoft's WMI wrappers (github.com/microsoft/wmi/pkg/*)
// to query the Domain field from Win32_ComputerSystem instead of a direct go-ole call.
func getLocalADDomain() (string, error) {
log.SetOutput(io.Discard)
defer log.SetOutput(os.Stderr)
// 1) Check environment variable
envDomain := os.Getenv("USERDNSDOMAIN")
if envDomain != "" {
return strings.TrimSpace(envDomain), nil
}
// 2) Query WMI via the microsoft/wmi library
whost := host.NewWmiLocalHost()
q := query.NewWmiQuery("Win32_ComputerSystem")
instances, err := instance.GetWmiInstancesFromHost(whost, string(constant.CimV2), q)
if instances != nil {
defer instances.Close()
}
if err != nil {
return "", fmt.Errorf("WMI query failed: %v", err)
}
// If no results, return an error
if len(instances) == 0 {
return "", fmt.Errorf("no rows returned from Win32_ComputerSystem")
}
// We only care about the first row
domainVal, err := instances[0].GetProperty("Domain")
if err != nil {
return "", fmt.Errorf("machine does not appear to have a domain set: %v", err)
}
domainName := strings.TrimSpace(fmt.Sprintf("%v", domainVal))
if domainName == "" {
return "", fmt.Errorf("machine does not appear to have a domain set")
}
return domainName, nil
}
// validInterfaces returns a list of all physical interfaces.
// this is a duplicate of what is in net_windows.go, we should
// clean this up so there is only one version

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() {