mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-04-07 12:32:04 +02:00
Compare commits
7 Commits
release-br
...
ip_stack
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7904580ed | ||
|
|
593805bf6f | ||
|
|
ae37c56467 | ||
|
|
41597609c8 | ||
|
|
1f619a669a | ||
|
|
37c3331559 | ||
|
|
f334993f79 |
@@ -100,7 +100,7 @@ docker build -t controldns/ctrld . -f docker/Dockerfile
|
|||||||
|
|
||||||
|
|
||||||
# Usage
|
# 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
|
## Arguments
|
||||||
```
|
```
|
||||||
|
|||||||
206
cmd/ctrld_library/netstack/README.md
Normal file
206
cmd/ctrld_library/netstack/README.md
Normal 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.
|
||||||
228
cmd/ctrld_library/netstack/dns_bridge.go
Normal file
228
cmd/ctrld_library/netstack/dns_bridge.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
324
cmd/ctrld_library/netstack/dns_filter.go
Normal file
324
cmd/ctrld_library/netstack/dns_filter.go
Normal 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
|
||||||
|
}
|
||||||
374
cmd/ctrld_library/netstack/netstack.go
Normal file
374
cmd/ctrld_library/netstack/netstack.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
115
cmd/ctrld_library/netstack/packet_handler.go
Normal file
115
cmd/ctrld_library/netstack/packet_handler.go
Normal 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)
|
||||||
|
}
|
||||||
123
cmd/ctrld_library/netstack/tcp_forwarder.go
Normal file
123
cmd/ctrld_library/netstack/tcp_forwarder.go
Normal 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
|
||||||
|
}
|
||||||
226
cmd/ctrld_library/netstack/udp_forwarder.go
Normal file
226
cmd/ctrld_library/netstack/udp_forwarder.go
Normal 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)
|
||||||
|
}
|
||||||
272
cmd/ctrld_library/packet_capture.go
Normal file
272
cmd/ctrld_library/packet_capture.go
Normal 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
|
||||||
|
}
|
||||||
35
config.go
35
config.go
@@ -21,6 +21,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ameshkov/dnsstamps"
|
"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) {
|
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
_, port, _ := net.SplitHostPort(addr)
|
_, port, _ := net.SplitHostPort(addr)
|
||||||
if uc.BootstrapIP != "" {
|
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)
|
addr := net.JoinHostPort(uc.BootstrapIP, port)
|
||||||
Log(ctx, ProxyLogger.Load().Debug(), "sending doh request to: %s", addr)
|
Log(ctx, ProxyLogger.Load().Debug(), "sending doh request to: %s", addr)
|
||||||
return dialer.DialContext(ctx, network, addr)
|
return dialer.DialContext(ctx, network, addr)
|
||||||
@@ -571,6 +589,21 @@ func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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())
|
Log(ctx, ProxyLogger.Load().Debug(), "sending doh request to: %s", conn.RemoteAddr())
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
28
go.mod
28
go.mod
@@ -1,13 +1,11 @@
|
|||||||
module github.com/Control-D-Inc/ctrld
|
module github.com/Control-D-Inc/ctrld
|
||||||
|
|
||||||
go 1.23.0
|
go 1.25.5
|
||||||
|
|
||||||
toolchain go1.23.7
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.2.1
|
github.com/Masterminds/semver/v3 v3.2.1
|
||||||
github.com/ameshkov/dnsstamps v1.0.3
|
github.com/ameshkov/dnsstamps v1.0.3
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0
|
github.com/coreos/go-systemd/v22 v22.6.0
|
||||||
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf
|
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
github.com/frankban/quicktest v1.14.6
|
github.com/frankban/quicktest v1.14.6
|
||||||
@@ -36,10 +34,11 @@ require (
|
|||||||
github.com/spf13/viper v1.16.0
|
github.com/spf13/viper v1.16.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2
|
github.com/vishvananda/netlink v1.2.1-beta.2
|
||||||
golang.org/x/net v0.38.0
|
golang.org/x/net v0.52.0
|
||||||
golang.org/x/sync v0.12.0
|
golang.org/x/sync v0.20.0
|
||||||
golang.org/x/sys v0.31.0
|
golang.org/x/sys v0.42.0
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||||
|
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987
|
||||||
tailscale.com v1.74.0
|
tailscale.com v1.74.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,7 +54,8 @@ require (
|
|||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // 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/google/uuid v1.6.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
@@ -89,12 +89,14 @@ require (
|
|||||||
go.uber.org/mock v0.5.0 // indirect
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
golang.org/x/crypto v0.36.0 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||||
golang.org/x/mod v0.19.0 // indirect
|
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/mod v0.34.0 // indirect
|
||||||
golang.org/x/tools v0.23.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
google.golang.org/protobuf v1.33.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/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
47
go.sum
47
go.sum
@@ -62,8 +62,9 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/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/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 h1:40DHYsri+d1bnroFDU2FQAeq68f3kAlOzlQ93kCf26Q=
|
||||||
@@ -133,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/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 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.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.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.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
@@ -143,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.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.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.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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 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.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=
|
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
@@ -336,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-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-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.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -365,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/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-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-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.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.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
@@ -373,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.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.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -407,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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -428,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-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-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.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -478,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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -490,11 +495,13 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
@@ -542,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-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -638,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.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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
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.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
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 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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
@@ -652,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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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-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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/certs"
|
"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"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/ddwrt"
|
"github.com/Control-D-Inc/ctrld/internal/router/ddwrt"
|
||||||
)
|
)
|
||||||
@@ -244,8 +244,38 @@ func apiTransport(cdDev bool) *http.Transport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dial := func(ctx context.Context, network string, addrs []string) (net.Conn, error) {
|
dial := func(ctx context.Context, network string, addrs []string) (net.Conn, error) {
|
||||||
d := &ctrldnet.ParallelDialer{}
|
// Create custom dialer with socket protection - matches working example pattern
|
||||||
return d.DialContext(ctx, network, addrs, ctrld.ProxyLogger.Load())
|
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)
|
_, port, _ := net.SplitHostPort(addr)
|
||||||
|
|
||||||
|
|||||||
21
resolver.go
21
resolver.go
@@ -62,8 +62,29 @@ var (
|
|||||||
or *osResolver
|
or *osResolver
|
||||||
defaultLocalIPv4 atomic.Value // holds net.IP (IPv4)
|
defaultLocalIPv4 atomic.Value // holds net.IP (IPv4)
|
||||||
defaultLocalIPv6 atomic.Value // holds net.IP (IPv6)
|
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 {
|
func newLocalResolver() Resolver {
|
||||||
var nss []string
|
var nss []string
|
||||||
for _, addr := range Rfc1918Addresses() {
|
for _, addr := range Rfc1918Addresses() {
|
||||||
|
|||||||
Reference in New Issue
Block a user