mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-03-25 23:30:41 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7904580ed | ||
|
|
593805bf6f | ||
|
|
ae37c56467 | ||
|
|
41597609c8 | ||
|
|
1f619a669a | ||
|
|
37c3331559 | ||
|
|
f334993f79 |
@@ -100,7 +100,7 @@ docker build -t controldns/ctrld . -f docker/Dockerfile
|
||||
|
||||
|
||||
# Usage
|
||||
The cli is self documenting, so free free to run `--help` on any sub-command to get specific usages.
|
||||
The cli is self documenting, so feel free to run `--help` on any sub-command to get specific usages.
|
||||
|
||||
## Arguments
|
||||
```
|
||||
|
||||
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"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ameshkov/dnsstamps"
|
||||
@@ -555,7 +556,24 @@ func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport {
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
_, port, _ := net.SplitHostPort(addr)
|
||||
if uc.BootstrapIP != "" {
|
||||
dialer := net.Dialer{Timeout: dialerTimeout, KeepAlive: dialerTimeout}
|
||||
// Create custom dialer with socket protection - matches working example pattern
|
||||
dialer := &net.Dialer{
|
||||
Timeout: dialerTimeout,
|
||||
KeepAlive: dialerTimeout,
|
||||
}
|
||||
// Access underlying socket fd before connecting to it
|
||||
dialer.Control = func(network, address string, c syscall.RawConn) error {
|
||||
return c.Control(func(fd uintptr) {
|
||||
Log(ctx, ProxyLogger.Load().Debug(), "Received DoH socket fd %d for %s", fd, address)
|
||||
i := int(fd)
|
||||
// Protect socket from VPN routing
|
||||
if err := ProtectSocket(i); err != nil {
|
||||
Log(ctx, ProxyLogger.Load().Warn(), "Failed to protect DoH socket fd=%d: %v", i, err)
|
||||
} else {
|
||||
Log(ctx, ProxyLogger.Load().Debug(), "Protected DoH socket fd=%d", i)
|
||||
}
|
||||
})
|
||||
}
|
||||
addr := net.JoinHostPort(uc.BootstrapIP, port)
|
||||
Log(ctx, ProxyLogger.Load().Debug(), "sending doh request to: %s", addr)
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
@@ -571,6 +589,21 @@ func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Protect DoH socket from VPN routing
|
||||
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
||||
if rawConn, err := tcpConn.SyscallConn(); err == nil {
|
||||
rawConn.Control(func(fd uintptr) {
|
||||
i := int(fd)
|
||||
if err := ProtectSocket(i); err != nil {
|
||||
Log(ctx, ProxyLogger.Load().Warn(), "Failed to protect DoH socket fd=%d: %v", i, err)
|
||||
} else {
|
||||
Log(ctx, ProxyLogger.Load().Debug(), "Protected DoH socket fd=%d", i)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Log(ctx, ProxyLogger.Load().Debug(), "sending doh request to: %s", conn.RemoteAddr())
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
28
go.mod
28
go.mod
@@ -1,13 +1,11 @@
|
||||
module github.com/Control-D-Inc/ctrld
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.7
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/ameshkov/dnsstamps v1.0.3
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/coreos/go-systemd/v22 v22.6.0
|
||||
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/frankban/quicktest v1.14.6
|
||||
@@ -36,10 +34,11 @@ require (
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/sync v0.12.0
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.42.0
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987
|
||||
tailscale.com v1.74.0
|
||||
)
|
||||
|
||||
@@ -55,7 +54,8 @@ require (
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
@@ -89,12 +89,14 @@ require (
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/mod v0.19.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf h1:40DHYsri+d1bnroFDU2FQAeq68f3kAlOzlQ93kCf26Q=
|
||||
@@ -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/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -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-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -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/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60 h1:MOzyaj0wu2xneBkzkg9LHNYjDBB4W5vP043A2SYQRPA=
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60/go.mod h1:th6VJvzjMbrYF8SduQY5rpD0HG0GleGxjadkqSxFs3k=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
@@ -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.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -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-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
@@ -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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
|
||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/certs"
|
||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/ddwrt"
|
||||
)
|
||||
@@ -244,8 +244,38 @@ func apiTransport(cdDev bool) *http.Transport {
|
||||
}
|
||||
|
||||
dial := func(ctx context.Context, network string, addrs []string) (net.Conn, error) {
|
||||
d := &ctrldnet.ParallelDialer{}
|
||||
return d.DialContext(ctx, network, addrs, ctrld.ProxyLogger.Load())
|
||||
// Create custom dialer with socket protection - matches working example pattern
|
||||
baseDialer := &net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
|
||||
// Access underlying socket fd before connecting to it
|
||||
baseDialer.Control = func(network, address string, c syscall.RawConn) error {
|
||||
return c.Control(func(fd uintptr) {
|
||||
ctrld.ProxyLogger.Load().Debug().Msgf("Received API socket fd %d for %s", fd, address)
|
||||
i := int(fd)
|
||||
// Protect socket from VPN routing
|
||||
if err := ctrld.ProtectSocket(i); err != nil {
|
||||
ctrld.ProxyLogger.Load().Warn().Err(err).Msgf("Failed to protect API socket fd=%d", i)
|
||||
} else {
|
||||
ctrld.ProxyLogger.Load().Debug().Msgf("Protected API socket fd=%d", i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Try each address with the protected dialer
|
||||
var lastErr error
|
||||
for _, addr := range addrs {
|
||||
ctrld.ProxyLogger.Load().Debug().Msgf("dialing to %s", addr)
|
||||
conn, err := baseDialer.DialContext(ctx, network, addr)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
lastErr = err
|
||||
ctrld.ProxyLogger.Load().Debug().Err(err).Msgf("failed to dial %s", addr)
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
_, port, _ := net.SplitHostPort(addr)
|
||||
|
||||
|
||||
21
resolver.go
21
resolver.go
@@ -62,8 +62,29 @@ var (
|
||||
or *osResolver
|
||||
defaultLocalIPv4 atomic.Value // holds net.IP (IPv4)
|
||||
defaultLocalIPv6 atomic.Value // holds net.IP (IPv6)
|
||||
|
||||
// socketProtector is a global function that can be set by mobile apps to protect
|
||||
// sockets from being routed through the VPN. This prevents routing loops.
|
||||
socketProtector atomic.Value // holds func(int) error
|
||||
)
|
||||
|
||||
// SetSocketProtector sets the global socket protection function.
|
||||
// This should be called by mobile VPN apps to prevent routing loops.
|
||||
func SetSocketProtector(protectFunc func(int) error) {
|
||||
socketProtector.Store(protectFunc)
|
||||
}
|
||||
|
||||
// ProtectSocket protects a socket using the globally set protector.
|
||||
// Returns nil if no protector is set.
|
||||
func ProtectSocket(fd int) error {
|
||||
if v := socketProtector.Load(); v != nil {
|
||||
if protectFunc, ok := v.(func(int) error); ok {
|
||||
return protectFunc(fd)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newLocalResolver() Resolver {
|
||||
var nss []string
|
||||
for _, addr := range Rfc1918Addresses() {
|
||||
|
||||
Reference in New Issue
Block a user