| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- package aerotun
- import (
- "crypto/cipher"
- "crypto/rand"
- "crypto/sha256"
- "encoding/binary"
- "fmt"
- "log"
- "net"
- "os/exec"
- "sync"
- "sync/atomic"
- "time"
- "github.com/pierrec/lz4/v4"
- "golang.org/x/crypto/chacha20poly1305"
- )
- type TunnelRunner struct {
- mu sync.RWMutex
- cfg TunnelInstance
- dev VirtualDevice
- udpConn *net.UDPConn
- aead cipher.AEAD
- remoteAddr *net.UDPAddr
- txCounter uint64
- rxMaxCounter uint64
- replayWindow uint64
- stopChan chan struct{}
- closed bool
- // Metrics
- rxBytes uint64
- txBytes uint64
- rawBytesIn uint64
- rawBytesOut uint64
- wireBytesIn uint64
- wireBytesOut uint64
- savedBytes uint64
- packetsRx uint64
- packetsTx uint64
- compressedPackets uint64
- rawPackets uint64
- lastHandshake time.Time
- }
- func NewTunnelRunner(cfg TunnelInstance) (*TunnelRunner, error) {
- // 1. Derive 32-byte ChaCha20-Poly1305 key from PSK
- if cfg.PSK == "" {
- return nil, fmt.Errorf("pre-shared key (PSK) is required")
- }
- key := sha256.Sum256([]byte(cfg.PSK))
- aead, err := chacha20poly1305.New(key[:])
- if err != nil {
- return nil, fmt.Errorf("failed to initialize ChaCha20-Poly1305: %w", err)
- }
- // 2. Create Virtual Network Device (TUN or VETH fallback)
- dev, err := CreateDevice(cfg.Name, cfg.MTU, cfg.LocalAddress)
- if err != nil {
- return nil, fmt.Errorf("failed to create virtual network device %s: %w", cfg.Name, err)
- }
- // 3. Resolve UDP socket / remote address
- var localUDPAddr *net.UDPAddr
- if cfg.ListenPort > 0 {
- localUDPAddr = &net.UDPAddr{Port: cfg.ListenPort}
- } else if stringsEqual(cfg.Mode, "server") {
- localUDPAddr = &net.UDPAddr{Port: DefaultPort}
- }
- udpConn, err := net.ListenUDP("udp", localUDPAddr)
- if err != nil {
- _ = dev.Close()
- return nil, fmt.Errorf("failed to bind UDP socket on port %d: %w", cfg.ListenPort, err)
- }
- var remoteAddr *net.UDPAddr
- if cfg.RemoteEndpoint != "" {
- resolved, err := net.ResolveUDPAddr("udp", cfg.RemoteEndpoint)
- if err == nil {
- remoteAddr = resolved
- }
- }
- runner := &TunnelRunner{
- cfg: cfg,
- dev: dev,
- udpConn: udpConn,
- aead: aead,
- remoteAddr: remoteAddr,
- stopChan: make(chan struct{}),
- }
- return runner, nil
- }
- func (t *TunnelRunner) Start() {
- go t.txLoop()
- go t.rxLoop()
- go t.keepaliveLoop()
- if t.cfg.DefaultRoute {
- _ = exec.Command("ip", "route", "replace", "default", "dev", t.cfg.Name).Run()
- }
- log.Printf("[AeroTUN] Tunnel %s started in %s mode (listen port %d, MTU %d)", t.cfg.Name, t.cfg.Mode, t.cfg.ListenPort, t.cfg.MTU)
- }
- func (t *TunnelRunner) Stop() {
- t.mu.Lock()
- if t.closed {
- t.mu.Unlock()
- return
- }
- t.closed = true
- close(t.stopChan)
- t.mu.Unlock()
- _ = t.udpConn.Close()
- _ = t.dev.Close()
- log.Printf("[AeroTUN] Tunnel %s stopped", t.cfg.Name)
- }
- // txLoop: Reads raw IP packets from TUN, compresses with LZ4, encrypts with ChaCha20-Poly1305, sends via UDP
- func (t *TunnelRunner) txLoop() {
- tunBuf := make([]byte, 2048)
- lz4Buf := make([]byte, 2048)
- udpPacket := make([]byte, 2048)
- for {
- select {
- case <-t.stopChan:
- return
- default:
- }
- n, err := t.dev.Read(tunBuf)
- if err != nil {
- select {
- case <-t.stopChan:
- return
- default:
- time.Sleep(10 * time.Millisecond)
- continue
- }
- }
- if n == 0 {
- continue
- }
- rawIP := tunBuf[:n]
- atomic.AddUint64(&t.rawBytesOut, uint64(n))
- atomic.AddUint64(&t.txBytes, uint64(n))
- atomic.AddUint64(&t.packetsTx, 1)
- // 1. Adaptive LZ4 Compression
- var flags byte = 0
- payload := rawIP
- if t.cfg.Compression && n > 40 {
- compressedSize, err := lz4.CompressBlock(rawIP, lz4Buf, nil)
- // Only compress if saving at least 16 bytes
- if err == nil && compressedSize > 0 && compressedSize < n-16 {
- flags |= FlagCompressedLZ4
- payload = lz4Buf[:compressedSize]
- saved := uint64(n - compressedSize)
- atomic.AddUint64(&t.savedBytes, saved)
- atomic.AddUint64(&t.compressedPackets, 1)
- } else {
- atomic.AddUint64(&t.rawPackets, 1)
- }
- } else {
- atomic.AddUint64(&t.rawPackets, 1)
- }
- // 2. Build 12-byte Authenticated Header:
- // [Magic (2B)][Version (1B)][Flags (1B)][Counter (8B)]
- counter := atomic.AddUint64(&t.txCounter, 1)
- binary.BigEndian.PutUint16(udpPacket[0:2], MagicHeader)
- udpPacket[2] = ProtocolVersion
- udpPacket[3] = flags
- binary.BigEndian.PutUint64(udpPacket[4:12], counter)
- // 3. Construct 12-byte Nonce
- var nonce [12]byte
- copy(nonce[0:4], udpPacket[0:4])
- binary.BigEndian.PutUint64(nonce[4:12], counter)
- // 4. AEAD Seal: authenticate header [0:12] as AAD, encrypt payload
- encrypted := t.aead.Seal(udpPacket[:12], nonce[:], payload, udpPacket[:12])
- // 5. Transmit UDP packet
- t.mu.RLock()
- target := t.remoteAddr
- t.mu.RUnlock()
- if target != nil {
- _, _ = t.udpConn.WriteToUDP(encrypted, target)
- atomic.AddUint64(&t.wireBytesOut, uint64(len(encrypted)))
- }
- }
- }
- // rxLoop: Reads UDP datagrams, verifies AEAD & anti-replay, decompresses LZ4, writes raw IP to TUN
- func (t *TunnelRunner) rxLoop() {
- udpBuf := make([]byte, 2048)
- decompBuf := make([]byte, 2048)
- for {
- select {
- case <-t.stopChan:
- return
- default:
- }
- n, srcAddr, err := t.udpConn.ReadFromUDP(udpBuf)
- if err != nil {
- select {
- case <-t.stopChan:
- return
- default:
- time.Sleep(10 * time.Millisecond)
- continue
- }
- }
- if n < 28 { // 12-byte header + 16-byte Poly1305 tag minimum
- continue
- }
- packet := udpBuf[:n]
- atomic.AddUint64(&t.wireBytesIn, uint64(n))
- // 1. Verify Magic Header & Protocol Version
- magic := binary.BigEndian.Uint16(packet[0:2])
- if magic != MagicHeader || packet[2] != ProtocolVersion {
- continue
- }
- flags := packet[3]
- counter := binary.BigEndian.Uint64(packet[4:12])
- // 2. Anti-Replay Sliding Window Check
- if !t.checkAndUpdateReplay(counter) {
- continue // Drop replayed packet
- }
- // 3. Construct 12-byte Nonce & Verify AEAD
- var nonce [12]byte
- copy(nonce[0:4], packet[0:4])
- binary.BigEndian.PutUint64(nonce[4:12], counter)
- decrypted, err := t.aead.Open(nil, nonce[:], packet[12:], packet[:12])
- if err != nil {
- continue // Authentication failed
- }
- // 4. Dynamic Roaming: Learn remote peer address in server mode
- t.mu.Lock()
- if t.remoteAddr == nil || (t.cfg.Mode == "server" && !udpAddrsEqual(t.remoteAddr, srcAddr)) {
- t.remoteAddr = srcAddr
- t.lastHandshake = time.Now()
- }
- t.lastHandshake = time.Now()
- t.mu.Unlock()
- // 5. Handle Keepalive
- if flags&FlagKeepalive != 0 {
- continue
- }
- // 6. LZ4 Decompress if flagged
- var ipPayload []byte
- if flags&FlagCompressedLZ4 != 0 {
- uncompressedSize, err := lz4.UncompressBlock(decrypted, decompBuf)
- if err != nil || uncompressedSize == 0 {
- continue
- }
- ipPayload = decompBuf[:uncompressedSize]
- atomic.AddUint64(&t.rawBytesIn, uint64(uncompressedSize))
- } else {
- ipPayload = decrypted
- atomic.AddUint64(&t.rawBytesIn, uint64(len(decrypted)))
- }
- // 7. Inject IP packet into TUN device
- _, _ = t.dev.Write(ipPayload)
- atomic.AddUint64(&t.rxBytes, uint64(len(ipPayload)))
- atomic.AddUint64(&t.packetsRx, 1)
- }
- }
- func (t *TunnelRunner) keepaliveLoop() {
- ticker := time.NewTicker(20 * time.Second)
- defer ticker.Stop()
- keepaliveBuf := make([]byte, 32)
- for {
- select {
- case <-t.stopChan:
- return
- case <-ticker.C:
- t.mu.RLock()
- target := t.remoteAddr
- t.mu.RUnlock()
- if target == nil {
- continue
- }
- counter := atomic.AddUint64(&t.txCounter, 1)
- binary.BigEndian.PutUint16(keepaliveBuf[0:2], MagicHeader)
- keepaliveBuf[2] = ProtocolVersion
- keepaliveBuf[3] = FlagKeepalive
- binary.BigEndian.PutUint64(keepaliveBuf[4:12], counter)
- var nonce [12]byte
- copy(nonce[0:4], keepaliveBuf[0:4])
- binary.BigEndian.PutUint64(nonce[4:12], counter)
- encrypted := t.aead.Seal(keepaliveBuf[:12], nonce[:], nil, keepaliveBuf[:12])
- _, _ = t.udpConn.WriteToUDP(encrypted, target)
- }
- }
- }
- // 64-packet Sliding Window Anti-Replay algorithm
- func (t *TunnelRunner) checkAndUpdateReplay(counter uint64) bool {
- t.mu.Lock()
- defer t.mu.Unlock()
- if counter == 0 {
- return false
- }
- if counter > t.rxMaxCounter {
- diff := counter - t.rxMaxCounter
- if diff >= 64 {
- t.replayWindow = 1
- } else {
- t.replayWindow = (t.replayWindow << diff) | 1
- }
- t.rxMaxCounter = counter
- return true
- }
- diff := t.rxMaxCounter - counter
- if diff >= 64 {
- return false // Packet is too old
- }
- mask := uint64(1) << diff
- if (t.replayWindow & mask) != 0 {
- return false // Duplicate packet!
- }
- t.replayWindow |= mask
- return true
- }
- func (t *TunnelRunner) GetTelemetry() TunnelInstance {
- t.mu.RLock()
- peerStr := ""
- if t.remoteAddr != nil {
- peerStr = t.remoteAddr.String()
- }
- lastHandshakeStr := "Never"
- if !t.lastHandshake.IsZero() {
- lastHandshakeStr = time.Since(t.lastHandshake).Truncate(time.Second).String() + " ago"
- }
- t.mu.RUnlock()
- inst := t.cfg
- inst.Running = !t.closed
- inst.ActivePeerEndpoint = peerStr
- inst.LastHandshake = lastHandshakeStr
- inst.RxBytes = atomic.LoadUint64(&t.rxBytes)
- inst.TxBytes = atomic.LoadUint64(&t.txBytes)
- inst.RawBytesIn = atomic.LoadUint64(&t.rawBytesIn)
- inst.RawBytesOut = atomic.LoadUint64(&t.rawBytesOut)
- inst.WireBytesIn = atomic.LoadUint64(&t.wireBytesIn)
- inst.WireBytesOut = atomic.LoadUint64(&t.wireBytesOut)
- inst.SavedBytes = atomic.LoadUint64(&t.savedBytes)
- inst.PacketsRx = atomic.LoadUint64(&t.packetsRx)
- inst.PacketsTx = atomic.LoadUint64(&t.packetsTx)
- inst.CompressedPackets = atomic.LoadUint64(&t.compressedPackets)
- inst.RawPackets = atomic.LoadUint64(&t.rawPackets)
- // Calculate compression ratio
- if inst.RawBytesOut > 0 && inst.WireBytesOut > 0 {
- ratio := float64(inst.RawBytesOut) / float64(inst.WireBytesOut)
- if ratio > 1.0 {
- savedPct := (1.0 - (float64(inst.WireBytesOut) / float64(inst.RawBytesOut))) * 100.0
- inst.CompressionRatio = fmt.Sprintf("%.2fx (%.1f%% saved)", ratio, savedPct)
- } else {
- inst.CompressionRatio = "1.00x (0% saved)"
- }
- } else {
- inst.CompressionRatio = "1.00x (0% saved)"
- }
- return inst
- }
- func GenerateSecureKey() (string, error) {
- key := make([]byte, 32)
- if _, err := rand.Read(key); err != nil {
- return "", err
- }
- return fmt.Sprintf("%x", key), nil
- }
- func udpAddrsEqual(a, b *net.UDPAddr) bool {
- if a == nil || b == nil {
- return a == b
- }
- return a.IP.Equal(b.IP) && a.Port == b.Port
- }
- func stringsEqual(a, b string) bool {
- return len(a) == len(b) && (a == b || (len(a) > 0 && (a[0]|0x20) == (b[0]|0x20)))
- }
|