Ver Fonte

feat(vpn): implement AeroTUN high-speed encrypted VPN with ChaCha20-Poly1305 and Adaptive LZ4 compression

Proxmox Router há 1 semana atrás
pai
commit
0684307743

+ 4 - 2
go.mod

@@ -1,9 +1,11 @@
 module proxmox-router
 
-go 1.24.4
+go 1.26.0
 
 require (
+	github.com/pierrec/lz4/v4 v4.1.33 // indirect
 	github.com/vishvananda/netlink v1.3.1 // indirect
 	github.com/vishvananda/netns v0.0.5 // indirect
-	golang.org/x/sys v0.10.0 // indirect
+	golang.org/x/crypto v0.58.0 // indirect
+	golang.org/x/sys v0.49.0 // indirect
 )

+ 6 - 0
go.sum

@@ -1,7 +1,13 @@
+github.com/pierrec/lz4/v4 v4.1.33 h1:GjG1TJ1V4IzKP8L96muuuDNpTwd7D+l2ccXrjAbe014=
+github.com/pierrec/lz4/v4 v4.1.33/go.mod h1:7SE9MC2STkNtL4PIwGhjmyVwvILaGI9/COYQNBhKM/c=
 github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
 github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
 github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
 github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
+golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
+golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
+golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=

+ 268 - 0
internal/aerotun/device.go

@@ -0,0 +1,268 @@
+package aerotun
+
+import (
+	"encoding/binary"
+	"fmt"
+	"net"
+	"os"
+	"os/exec"
+	"syscall"
+	"unsafe"
+)
+
+type VirtualDevice interface {
+	Read(buf []byte) (int, error)
+	Write(buf []byte) (int, error)
+	Close() error
+	Name() string
+}
+
+// CreateDevice creates either a native Linux TUN device, or falls back to a high-speed
+// veth pair with AF_PACKET raw socket if running in an unprivileged container without /dev/net/tun.
+func CreateDevice(name string, mtu int, localCIDR string) (VirtualDevice, error) {
+	if mtu <= 0 {
+		mtu = DefaultMTU
+	}
+
+	if _, err := os.Stat("/dev/net/tun"); err == nil {
+		dev, err := createNativeTun(name, mtu, localCIDR)
+		if err == nil {
+			return dev, nil
+		}
+	}
+
+	// Universal container / Proxmox LXC fallback:
+	return createVethTun(name, mtu, localCIDR)
+}
+
+// ---------------------------------------------------------------------------
+// 1. Native Linux TUN Implementation (/dev/net/tun)
+// ---------------------------------------------------------------------------
+
+type nativeTun struct {
+	file *os.File
+	name string
+}
+
+func createNativeTun(name string, mtu int, localCIDR string) (*nativeTun, error) {
+	fd, err := syscall.Open("/dev/net/tun", syscall.O_RDWR, 0)
+	if err != nil {
+		return nil, err
+	}
+
+	type ifreq struct {
+		name  [16]byte
+		flags uint16
+		_pad  [22]byte
+	}
+
+	var ifr ifreq
+	copy(ifr.name[:], name)
+	// IFF_TUN = 0x0001, IFF_NO_PI = 0x1000
+	ifr.flags = 0x0001 | 0x1000
+
+	_, _, errno := syscall.Syscall(
+		syscall.SYS_IOCTL,
+		uintptr(fd),
+		uintptr(0x400454ca), // TUNSETIFF
+		uintptr(unsafe.Pointer(&ifr)),
+	)
+	if errno != 0 {
+		_ = syscall.Close(fd)
+		return nil, errno
+	}
+
+	// Configure MTU and IP
+	_ = exec.Command("ip", "link", "set", name, "mtu", fmt.Sprintf("%d", mtu)).Run()
+	_ = exec.Command("ip", "link", "set", name, "up").Run()
+
+	if localCIDR != "" {
+		_ = exec.Command("ip", "addr", "add", localCIDR, "dev", name).Run()
+	}
+
+	return &nativeTun{
+		file: os.NewFile(uintptr(fd), name),
+		name: name,
+	}, nil
+}
+
+func (t *nativeTun) Read(buf []byte) (int, error) {
+	return t.file.Read(buf)
+}
+
+func (t *nativeTun) Write(buf []byte) (int, error) {
+	return t.file.Write(buf)
+}
+
+func (t *nativeTun) Close() error {
+	err := t.file.Close()
+	_ = exec.Command("ip", "link", "delete", t.name).Run()
+	return err
+}
+
+func (t *nativeTun) Name() string {
+	return t.name
+}
+
+// ---------------------------------------------------------------------------
+// 2. High-Performance Container Fallback (VETH + AF_PACKET)
+// ---------------------------------------------------------------------------
+
+type vethTun struct {
+	name      string
+	epName    string
+	sockFd    int
+	ifIndex   int
+	hwAddr    [6]byte
+	epHwAddr  [6]byte
+	packetBuf [2048]byte
+}
+
+func createVethTun(name string, mtu int, localCIDR string) (*vethTun, error) {
+	epName := fmt.Sprintf("%s_ep", name)
+	if len(epName) > 15 {
+		epName = epName[:15]
+	}
+
+	// Clean up any stale interfaces
+	_ = exec.Command("ip", "link", "delete", name).Run()
+	_ = exec.Command("ip", "link", "delete", epName).Run()
+
+	// 1. Create veth pair
+	out, err := exec.Command("ip", "link", "add", name, "type", "veth", "peer", "name", epName).CombinedOutput()
+	if err != nil {
+		return nil, fmt.Errorf("failed to create veth pair %s: %s: %w", name, string(out), err)
+	}
+
+	// 2. Set Point-to-Point / ARP off behavior
+	_ = exec.Command("ip", "link", "set", name, "arp", "off").Run()
+	_ = exec.Command("ip", "link", "set", epName, "arp", "off").Run()
+	_ = exec.Command("ip", "link", "set", name, "mtu", fmt.Sprintf("%d", mtu)).Run()
+	_ = exec.Command("ip", "link", "set", epName, "mtu", fmt.Sprintf("%d", mtu)).Run()
+	_ = exec.Command("ip", "link", "set", name, "up").Run()
+	_ = exec.Command("ip", "link", "set", epName, "up").Run()
+
+	// 3. Assign IP to main device
+	if localCIDR != "" {
+		_ = exec.Command("ip", "addr", "add", localCIDR, "dev", name).Run()
+	}
+
+	// 4. Get endpoint interface index
+	epIface, err := net.InterfaceByName(epName)
+	if err != nil {
+		_ = exec.Command("ip", "link", "delete", name).Run()
+		return nil, fmt.Errorf("failed to lookup %s: %w", epName, err)
+	}
+
+	// 5. Open AF_PACKET raw socket bound to endpoint
+	// htons(ETH_P_ALL) = 0x0300
+	const ethPAll = 0x0300
+	sockFd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(htons(syscall.ETH_P_ALL)))
+	if err != nil {
+		_ = exec.Command("ip", "link", "delete", name).Run()
+		return nil, fmt.Errorf("failed to open raw AF_PACKET socket: %w", err)
+	}
+
+	addr := syscall.SockaddrLinklayer{
+		Protocol: htons(syscall.ETH_P_ALL),
+		Ifindex:  epIface.Index,
+	}
+	if err := syscall.Bind(sockFd, &addr); err != nil {
+		_ = syscall.Close(sockFd)
+		_ = exec.Command("ip", "link", "delete", name).Run()
+		return nil, fmt.Errorf("failed to bind raw socket to %s: %w", epName, err)
+	}
+
+	dev := &vethTun{
+		name:    name,
+		epName:  epName,
+		sockFd:  sockFd,
+		ifIndex: epIface.Index,
+	}
+
+	if len(epIface.HardwareAddr) >= 6 {
+		copy(dev.epHwAddr[:], epIface.HardwareAddr[:6])
+	}
+	if mainIface, err := net.InterfaceByName(name); err == nil && len(mainIface.HardwareAddr) >= 6 {
+		copy(dev.hwAddr[:], mainIface.HardwareAddr[:6])
+	}
+
+	return dev, nil
+}
+
+func (v *vethTun) Read(buf []byte) (int, error) {
+	for {
+		n, _, err := syscall.Recvfrom(v.sockFd, v.packetBuf[:], 0)
+		if err != nil {
+			return 0, err
+		}
+		if n <= 14 {
+			continue // Too small for ethernet frame
+		}
+
+		// Ethernet frame header: [Dst MAC (6)][Src MAC (6)][EtherType (2)]
+		etherType := binary.BigEndian.Uint16(v.packetBuf[12:14])
+		// Check for IPv4 (0x0800) or IPv6 (0x86DD)
+		if etherType != 0x0800 && etherType != 0x86DD {
+			continue // Ignore ARP, STP, etc.
+		}
+
+		ipPayload := v.packetBuf[14:n]
+		copied := copy(buf, ipPayload)
+		return copied, nil
+	}
+}
+
+func (v *vethTun) Write(buf []byte) (int, error) {
+	if len(buf) == 0 {
+		return 0, nil
+	}
+
+	// Build raw Ethernet frame to inject into endpoint
+	frameLen := 14 + len(buf)
+	var frame []byte
+	if frameLen <= len(v.packetBuf) {
+		frame = v.packetBuf[:frameLen]
+	} else {
+		frame = make([]byte, frameLen)
+	}
+
+	// Dst MAC: main interface, Src MAC: endpoint
+	copy(frame[0:6], v.hwAddr[:])
+	copy(frame[6:12], v.epHwAddr[:])
+
+	// Detect IP version
+	var etherType uint16 = 0x0800 // IPv4 default
+	if (buf[0] >> 4) == 6 {
+		etherType = 0x86DD // IPv6
+	}
+	binary.BigEndian.PutUint16(frame[12:14], etherType)
+	copy(frame[14:], buf)
+
+	addr := syscall.SockaddrLinklayer{
+		Protocol: htons(etherType),
+		Ifindex:  v.ifIndex,
+		Halen:    6,
+	}
+	copy(addr.Addr[:], v.hwAddr[:])
+
+	err := syscall.Sendto(v.sockFd, frame, 0, &addr)
+	if err != nil {
+		return 0, err
+	}
+	return len(buf), nil
+}
+
+func (v *vethTun) Close() error {
+	_ = syscall.Close(v.sockFd)
+	_ = exec.Command("ip", "link", "delete", v.name).Run()
+	return nil
+}
+
+func (v *vethTun) Name() string {
+	return v.name
+}
+
+func htons(n uint16) uint16 {
+	return (n<<8)&0xff00 | (n>>8)&0x00ff
+}

+ 310 - 0
internal/aerotun/manager.go

@@ -0,0 +1,310 @@
+package aerotun
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+	"sync"
+	"time"
+)
+
+type Manager struct {
+	mu        sync.RWMutex
+	instances map[string]TunnelInstance
+	runners   map[string]*TunnelRunner
+	filePath  string
+}
+
+func NewManager() *Manager {
+	m := &Manager{
+		instances: make(map[string]TunnelInstance),
+		runners:   make(map[string]*TunnelRunner),
+		filePath:  StoragePath,
+	}
+
+	_ = os.MkdirAll(filepath.Dir(m.filePath), 0755)
+	if err := m.load(); err != nil {
+		log.Printf("[AeroTUN] Error loading saved instances: %v", err)
+	}
+
+	// Auto-start enabled tunnels
+	for id, inst := range m.instances {
+		if !inst.Disabled {
+			if err := m.startRunnerLocked(id); err != nil {
+				log.Printf("[AeroTUN] Failed to auto-start tunnel %s (%s): %v", inst.Name, id, err)
+			}
+		}
+	}
+
+	return m
+}
+
+func (m *Manager) load() error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	data, err := os.ReadFile(m.filePath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil
+		}
+		return err
+	}
+
+	var list []TunnelInstance
+	if err := json.Unmarshal(data, &list); err != nil {
+		return err
+	}
+
+	for _, inst := range list {
+		m.instances[inst.ID] = inst
+	}
+	return nil
+}
+
+func (m *Manager) saveLocked() error {
+	list := make([]TunnelInstance, 0, len(m.instances))
+	for _, inst := range m.instances {
+		// Do not persist volatile runtime counters
+		copyInst := inst
+		copyInst.Running = false
+		copyInst.ActivePeerEndpoint = ""
+		copyInst.LastHandshake = ""
+		copyInst.RxBytes = 0
+		copyInst.TxBytes = 0
+		copyInst.RawBytesIn = 0
+		copyInst.RawBytesOut = 0
+		copyInst.WireBytesIn = 0
+		copyInst.WireBytesOut = 0
+		copyInst.SavedBytes = 0
+		copyInst.CompressionRatio = ""
+		copyInst.PacketsRx = 0
+		copyInst.PacketsTx = 0
+		copyInst.CompressedPackets = 0
+		copyInst.RawPackets = 0
+		list = append(list, copyInst)
+	}
+
+	data, err := json.MarshalIndent(list, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	return os.WriteFile(m.filePath, data, 0600)
+}
+
+func (m *Manager) List() []TunnelInstance {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	result := make([]TunnelInstance, 0, len(m.instances))
+	for id, inst := range m.instances {
+		if runner, ok := m.runners[id]; ok && runner != nil {
+			tele := runner.GetTelemetry()
+			tele.Disabled = inst.Disabled
+			tele.Comment = inst.Comment
+			result = append(result, tele)
+		} else {
+			inst.Running = false
+			result = append(result, inst)
+		}
+	}
+	return result
+}
+
+func (m *Manager) Get(id string) (TunnelInstance, bool) {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	inst, exists := m.instances[id]
+	if !exists {
+		return TunnelInstance{}, false
+	}
+
+	if runner, ok := m.runners[id]; ok && runner != nil {
+		tele := runner.GetTelemetry()
+		tele.Disabled = inst.Disabled
+		tele.Comment = inst.Comment
+		return tele, true
+	}
+	return inst, true
+}
+
+func (m *Manager) Save(inst TunnelInstance) (TunnelInstance, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if inst.ID == "" {
+		inst.ID = fmt.Sprintf("aerotun-%d", time.Now().UnixNano())
+	}
+	if inst.Name == "" {
+		inst.Name = fmt.Sprintf("aerotun%d", len(m.instances))
+	}
+	if inst.MTU <= 0 {
+		inst.MTU = DefaultMTU
+	}
+	if inst.Mode == "server" && inst.ListenPort <= 0 {
+		inst.ListenPort = DefaultPort
+	}
+	if inst.CreatedAt.IsZero() {
+		inst.CreatedAt = time.Now()
+	}
+
+	// If existing runner is active, stop it before applying new config
+	if runner, exists := m.runners[inst.ID]; exists && runner != nil {
+		runner.Stop()
+		delete(m.runners, inst.ID)
+	}
+
+	m.instances[inst.ID] = inst
+	if err := m.saveLocked(); err != nil {
+		return inst, fmt.Errorf("failed to save configuration: %w", err)
+	}
+
+	// Restart if not disabled
+	if !inst.Disabled {
+		if err := m.startRunnerLocked(inst.ID); err != nil {
+			log.Printf("[AeroTUN] Warning: saved tunnel %s but failed to start: %v", inst.Name, err)
+		}
+	}
+
+	if runner, ok := m.runners[inst.ID]; ok && runner != nil {
+		return runner.GetTelemetry(), nil
+	}
+	return inst, nil
+}
+
+func (m *Manager) Delete(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if runner, exists := m.runners[id]; exists && runner != nil {
+		runner.Stop()
+		delete(m.runners, id)
+	}
+
+	delete(m.instances, id)
+	return m.saveLocked()
+}
+
+func (m *Manager) StartTunnel(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	inst, exists := m.instances[id]
+	if !exists {
+		return fmt.Errorf("tunnel not found: %s", id)
+	}
+
+	inst.Disabled = false
+	m.instances[id] = inst
+	_ = m.saveLocked()
+
+	return m.startRunnerLocked(id)
+}
+
+func (m *Manager) StopTunnel(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if runner, exists := m.runners[id]; exists && runner != nil {
+		runner.Stop()
+		delete(m.runners, id)
+	}
+	return nil
+}
+
+func (m *Manager) ToggleState(id string, disabled bool) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	inst, exists := m.instances[id]
+	if !exists {
+		return fmt.Errorf("tunnel not found: %s", id)
+	}
+
+	inst.Disabled = disabled
+	m.instances[id] = inst
+	_ = m.saveLocked()
+
+	if disabled {
+		if runner, exists := m.runners[id]; exists && runner != nil {
+			runner.Stop()
+			delete(m.runners, id)
+		}
+	} else {
+		return m.startRunnerLocked(id)
+	}
+
+	return nil
+}
+
+func (m *Manager) startRunnerLocked(id string) error {
+	inst, exists := m.instances[id]
+	if !exists {
+		return fmt.Errorf("tunnel not found: %s", id)
+	}
+
+	if runner, exists := m.runners[id]; exists && runner != nil {
+		runner.Stop()
+		delete(m.runners, id)
+	}
+
+	runner, err := NewTunnelRunner(inst)
+	if err != nil {
+		return err
+	}
+
+	runner.Start()
+	m.runners[id] = runner
+	return nil
+}
+
+func (m *Manager) StopAll() {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	for id, runner := range m.runners {
+		if runner != nil {
+			runner.Stop()
+		}
+		delete(m.runners, id)
+	}
+}
+
+func (m *Manager) GetSummaryStats() SummaryStats {
+	tunnels := m.List()
+	stats := SummaryStats{
+		TotalTunnels:      len(tunnels),
+		AlgorithmCrypto:   "ChaCha20-Poly1305 AEAD",
+		AlgorithmCompress: "Adaptive LZ4 (Dynamic Threshold)",
+	}
+
+	var totalRawOut, totalWireOut uint64
+	for _, t := range tunnels {
+		if t.Running {
+			stats.ActiveTunnels++
+		}
+		stats.TotalRxBytes += t.RxBytes
+		stats.TotalTxBytes += t.TxBytes
+		stats.TotalSavedBytes += t.SavedBytes
+		totalRawOut += t.RawBytesOut
+		totalWireOut += t.WireBytesOut
+	}
+
+	if totalRawOut > 0 && totalWireOut > 0 {
+		ratio := float64(totalRawOut) / float64(totalWireOut)
+		if ratio > 1.0 {
+			savedPct := (1.0 - (float64(totalWireOut) / float64(totalRawOut))) * 100.0
+			stats.AverageRatio = fmt.Sprintf("%.2fx (%.1f%% saved)", ratio, savedPct)
+		} else {
+			stats.AverageRatio = "1.00x (0% saved)"
+		}
+	} else {
+		stats.AverageRatio = "1.00x (0% saved)"
+	}
+
+	return stats
+}

+ 422 - 0
internal/aerotun/tunnel.go

@@ -0,0 +1,422 @@
+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)))
+}

+ 61 - 0
internal/aerotun/types.go

@@ -0,0 +1,61 @@
+package aerotun
+
+import "time"
+
+const (
+	ProtocolName      = "AeroTUN"
+	ProtocolVersion   = 1
+	DefaultMTU        = 1420
+	DefaultPort       = 51830
+	MagicHeader       = 0xAE70 // "AE"
+	FlagCompressedLZ4 = 0x01
+	FlagKeepalive     = 0x02
+	StoragePath       = "/etc/proxmox-router/vpn/aerotun_instances.json"
+)
+
+// TunnelInstance represents a configured AeroTUN interface and its live status
+type TunnelInstance struct {
+	ID             string    `json:"id"`
+	Name           string    `json:"name"`            // e.g. "aerotun0"
+	Mode           string    `json:"mode"`            // "server" (listen) or "client" (dial)
+	ListenPort     int       `json:"listen_port"`     // UDP port to bind
+	RemoteEndpoint string    `json:"remote_endpoint"` // "host:port" (used if client or static peer)
+	LocalAddress   string    `json:"local_address"`   // CIDR e.g. "10.99.0.1/24"
+	PeerAddress    string    `json:"peer_address"`    // Peer CIDR e.g. "10.99.0.2/24"
+	PSK            string    `json:"psk"`             // 32-byte hex/base64 pre-shared key
+	Compression    bool      `json:"compression"`     // Enable adaptive LZ4
+	MTU            int       `json:"mtu"`             // e.g. 1420
+	DefaultRoute   bool      `json:"default_route"`   // Route 0.0.0.0/0 via this tunnel
+	Disabled       bool      `json:"disabled"`        // MikroTik 'X' flag
+	Comment        string    `json:"comment,omitempty"`
+	CreatedAt      time.Time `json:"created_at"`
+
+	// Runtime state & telemetry
+	Running            bool    `json:"running"`
+	ActivePeerEndpoint string  `json:"active_peer_endpoint,omitempty"`
+	LastHandshake      string  `json:"last_handshake,omitempty"`
+	RxBytes            uint64  `json:"rx_bytes"`
+	TxBytes            uint64  `json:"tx_bytes"`
+	RawBytesIn         uint64  `json:"raw_bytes_in"`  // uncompressed RX payload
+	RawBytesOut        uint64  `json:"raw_bytes_out"` // uncompressed TX payload
+	WireBytesIn        uint64  `json:"wire_bytes_in"` // actual wire RX
+	WireBytesOut       uint64  `json:"wire_bytes_out"`// actual wire TX
+	SavedBytes         uint64  `json:"saved_bytes"`   // Bandwidth saved through compression
+	CompressionRatio   string  `json:"compression_ratio,omitempty"`
+	PacketsRx          uint64  `json:"packets_rx"`
+	PacketsTx          uint64  `json:"packets_tx"`
+	CompressedPackets  uint64  `json:"compressed_packets"`
+	RawPackets         uint64  `json:"raw_packets"`
+}
+
+// SummaryStats aggregates performance across all AeroTUN tunnels
+type SummaryStats struct {
+	TotalTunnels     int    `json:"total_tunnels"`
+	ActiveTunnels    int    `json:"active_tunnels"`
+	TotalRxBytes     uint64 `json:"total_rx_bytes"`
+	TotalTxBytes     uint64 `json:"total_tx_bytes"`
+	TotalSavedBytes  uint64 `json:"total_saved_bytes"`
+	AverageRatio     string `json:"average_ratio"`
+	AlgorithmCrypto  string `json:"algorithm_crypto"`
+	AlgorithmCompress string `json:"algorithm_compress"`
+}

+ 118 - 0
internal/api/aerotun_handlers.go

@@ -0,0 +1,118 @@
+package api
+
+import (
+	"encoding/json"
+	"net/http"
+	"proxmox-router/internal/aerotun"
+)
+
+func (s *Server) handleListAeroTUNTunnels(w http.ResponseWriter, r *http.Request) {
+	tunnels := s.vpnMgr.AeroTUN.List()
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(tunnels)
+}
+
+func (s *Server) handleGetAeroTUNStats(w http.ResponseWriter, r *http.Request) {
+	stats := s.vpnMgr.AeroTUN.GetSummaryStats()
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(stats)
+}
+
+func (s *Server) handleGenerateAeroTUNKey(w http.ResponseWriter, r *http.Request) {
+	key, err := aerotun.GenerateSecureKey()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(map[string]string{"key": key})
+}
+
+func (s *Server) handleSaveAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
+	var inst aerotun.TunnelInstance
+	if err := json.NewDecoder(r.Body).Decode(&inst); err != nil {
+		http.Error(w, "invalid request body", http.StatusBadRequest)
+		return
+	}
+
+	saved, err := s.vpnMgr.AeroTUN.Save(inst)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(saved)
+}
+
+func (s *Server) handleDeleteAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		http.Error(w, "missing tunnel ID", http.StatusBadRequest)
+		return
+	}
+
+	if err := s.vpnMgr.AeroTUN.Delete(id); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
+}
+
+func (s *Server) handleSetAeroTUNTunnelState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		http.Error(w, "missing tunnel ID", http.StatusBadRequest)
+		return
+	}
+
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, "invalid JSON", http.StatusBadRequest)
+		return
+	}
+
+	if err := s.vpnMgr.AeroTUN.ToggleState(id, req.Disabled); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
+}
+
+func (s *Server) handleStartAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		http.Error(w, "missing tunnel ID", http.StatusBadRequest)
+		return
+	}
+
+	if err := s.vpnMgr.AeroTUN.StartTunnel(id); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
+}
+
+func (s *Server) handleStopAeroTUNTunnel(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		http.Error(w, "missing tunnel ID", http.StatusBadRequest)
+		return
+	}
+
+	if err := s.vpnMgr.AeroTUN.StopTunnel(id); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
+}

+ 10 - 0
internal/api/api.go

@@ -206,6 +206,16 @@ func (s *Server) registerRoutes() {
 	s.mux.HandleFunc("DELETE /api/vpn/eoip/{id}", s.handleDeleteEoIP)
 	s.mux.HandleFunc("POST /api/vpn/eoip/{id}/state", s.handleSetEoIPState)
 
+	// AeroTUN (High-speed ChaCha20-Poly1305 + LZ4 Encrypted UDP Tunnel)
+	s.mux.HandleFunc("GET /api/vpn/aerotun/tunnels", s.handleListAeroTUNTunnels)
+	s.mux.HandleFunc("POST /api/vpn/aerotun/tunnels", s.handleSaveAeroTUNTunnel)
+	s.mux.HandleFunc("DELETE /api/vpn/aerotun/tunnels/{id}", s.handleDeleteAeroTUNTunnel)
+	s.mux.HandleFunc("POST /api/vpn/aerotun/tunnels/{id}/state", s.handleSetAeroTUNTunnelState)
+	s.mux.HandleFunc("POST /api/vpn/aerotun/tunnels/{id}/start", s.handleStartAeroTUNTunnel)
+	s.mux.HandleFunc("POST /api/vpn/aerotun/tunnels/{id}/stop", s.handleStopAeroTUNTunnel)
+	s.mux.HandleFunc("GET /api/vpn/aerotun/genkey", s.handleGenerateAeroTUNKey)
+	s.mux.HandleFunc("GET /api/vpn/aerotun/stats", s.handleGetAeroTUNStats)
+
 	// PPP Profiles (/ppp profile)
 	s.mux.HandleFunc("GET /api/vpn/ppp/profiles", s.handleListPPPProfiles)
 	s.mux.HandleFunc("POST /api/vpn/ppp/profiles", s.handleSavePPPProfile)

+ 11 - 1
internal/vpn/manager.go

@@ -1,5 +1,7 @@
 package vpn
 
+import "proxmox-router/internal/aerotun"
+
 type Manager struct {
 	PPPoE     *PPPoEManager
 	WireGuard *WireGuardManager
@@ -8,6 +10,7 @@ type Manager struct {
 	IPSec     *IPSecManager
 	EoIP      *EoIPManager
 	PPPServer *PPPServerManager
+	AeroTUN   *aerotun.Manager
 }
 
 func NewManager() *Manager {
@@ -25,6 +28,7 @@ func NewManager() *Manager {
 		IPSec:     NewIPSecManager(),
 		EoIP:      NewEoIPManager(),
 		PPPServer: pppServer,
+		AeroTUN:   aerotun.NewManager(),
 	}
 }
 
@@ -80,13 +84,15 @@ func (m *Manager) GetOverallStatus() OverallVPNStatus {
 		}
 	}
 
+	aeroStats := m.AeroTUN.GetSummaryStats()
+
 	pppoeSrv := m.PPPServer.GetPPPoEServer()
 	pptpSrv := m.PPPServer.GetPPTPServer()
 	l2tpSrv := m.PPPServer.GetL2TPServer()
 	ipsecSrv := m.PPPServer.GetIPSecServer()
 	pppSessions := len(m.PPPServer.ListActiveSessions())
 
-	totalActive := activePPPoE + activeWG + activePPTP + activeL2TP + activeIPSec + activeEoIP + pppSessions
+	totalActive := activePPPoE + activeWG + activePPTP + activeL2TP + activeIPSec + activeEoIP + aeroStats.ActiveTunnels + pppSessions
 
 	return OverallVPNStatus{
 		TotalPPPoE:          len(pppoeList),
@@ -102,6 +108,10 @@ func (m *Manager) GetOverallStatus() OverallVPNStatus {
 		ActiveIPSec:         activeIPSec,
 		TotalEoIP:           len(eoipList),
 		ActiveEoIP:          activeEoIP,
+		TotalAeroTUN:        aeroStats.TotalTunnels,
+		ActiveAeroTUN:       aeroStats.ActiveTunnels,
+		AeroTUNSavedBytes:   aeroStats.TotalSavedBytes,
+		AeroTUNRatio:        aeroStats.AverageRatio,
 		TotalPPPSessions:    pppSessions,
 		PPPoEServerRunning:  pppoeSrv.Running,
 		PPTPServerRunning:   pptpSrv.Running,

+ 7 - 3
internal/vpn/types.go

@@ -13,9 +13,13 @@ type OverallVPNStatus struct {
 	ActiveL2TP          int  `json:"active_l2tp"`
 	TotalIPSec          int  `json:"total_ipsec"`
 	ActiveIPSec         int  `json:"active_ipsec"`
-	TotalEoIP           int  `json:"total_eoip"`
-	ActiveEoIP          int  `json:"active_eoip"`
-	TotalPPPSessions    int  `json:"total_ppp_sessions"`
+	TotalEoIP           int    `json:"total_eoip"`
+	ActiveEoIP          int    `json:"active_eoip"`
+	TotalAeroTUN        int    `json:"total_aerotun"`
+	ActiveAeroTUN       int    `json:"active_aerotun"`
+	AeroTUNSavedBytes   uint64 `json:"aerotun_saved_bytes"`
+	AeroTUNRatio        string `json:"aerotun_ratio"`
+	TotalPPPSessions    int    `json:"total_ppp_sessions"`
 	PPPoEServerRunning  bool `json:"pppoe_server_running"`
 	PPTPServerRunning   bool `json:"pptp_server_running"`
 	L2TPServerRunning   bool `json:"l2tp_server_running"`

+ 333 - 0
internal/web/static/app.js

@@ -113,6 +113,7 @@ function navigate(tabName) {
     'dynamic-routing': { title: 'BGP Routing (/routing bgp)', sub: 'BIRD 2 dynamic routing engine' },
     ppp: { title: 'PPP Servers & Secrets', sub: 'Subscriber authentication, dynamic IP pools and VPN access concentrators' },
     eoip: { title: 'EoIP Tunnels', sub: 'Ethernet over IP (EoIP / GRETAP) Layer 2 encapsulation & transparent network bridging' },
+    aerotun: { title: 'AeroTUN VPN (/interface aerotun)', sub: 'Ultra-fast Layer 3 encrypted tunnel with ChaCha20-Poly1305 and live Adaptive LZ4 compression' },
     pppoe: { title: 'PPPoE Client WAN', sub: 'Point-to-Point Protocol over Ethernet WAN access' },
     wireguard: { title: 'WireGuard VPN', sub: 'High-performance cryptographic Layer 3 tunnels with ChaCha20-Poly1305 key exchange' },
     ipsec: { title: 'IPSec (IKEv2) strongSwan', sub: 'Enterprise Site-to-Site & Roadwarrior IPSec controller' },
@@ -152,6 +153,7 @@ function navigate(tabName) {
   if (tabName === 'dynamic-routing') loadDynamicRouting();
   if (tabName === 'ppp') loadPPP();
   if (tabName === 'eoip') loadEoIP();
+  if (tabName === 'aerotun') loadAeroTUN();
   if (tabName === 'persistence') loadDebianConfig();
   if (tabName === 'pppoe') loadPPPoE();
   if (tabName === 'wireguard') loadWireGuard();
@@ -232,6 +234,7 @@ async function fetchData(isManual = false) {
     if (state.activeTab === 'dynamic-routing' || state.activeTab === 'bgp' || state.activeTab === 'ospf') loadDynamicRouting(false);
     if (state.activeTab === 'ppp') loadPPP(false);
     if (state.activeTab === 'eoip') loadEoIP(false);
+    if (state.activeTab === 'aerotun') loadAeroTUN(false);
     if (state.activeTab === 'pppoe') loadPPPoE(false);
     if (state.activeTab === 'wireguard') loadWireGuard(false);
     if (state.activeTab === 'ipsec') loadIPSec(false);
@@ -442,6 +445,8 @@ function updateBadges() {
     const v = state.vpnStatus;
     const bEoIP = document.getElementById('badge-eoip-count');
     if (bEoIP) bEoIP.innerText = v.total_eoip || 0;
+    const bAero = document.getElementById('badge-aerotun-count');
+    if (bAero) bAero.innerText = v.total_aerotun || 0;
     const bPPPSess = document.getElementById('badge-ppp-sessions');
     if (bPPPSess) bPPPSess.innerText = v.total_ppp_sessions || 0;
     setDotBadge('badge-pppoe-status', v.active_pppoe > 0 || v.pppoe_server_running);
@@ -6531,6 +6536,334 @@ async function deleteEoIP(id) {
   }
 }
 
+// ======================== AeroTUN (Custom Encrypted VPN with Adaptive LZ4) ========================
+async function loadAeroTUN(populate = true) {
+  try {
+    const [tunnelsRes, statsRes] = await Promise.all([
+      fetch(`${API_BASE}/vpn/aerotun/tunnels`),
+      fetch(`${API_BASE}/vpn/aerotun/stats`)
+    ]);
+
+    if (tunnelsRes.ok) {
+      const data = await tunnelsRes.json();
+      state.aerotunTunnels = Array.isArray(data) ? data : [];
+    }
+
+    if (statsRes.ok) {
+      state.aerotunStats = await statsRes.json();
+    }
+
+    const countBadge = document.getElementById('aerotun-count-badge');
+    if (countBadge) countBadge.innerText = `${(state.aerotunTunnels || []).length} Tunnels`;
+
+    const sideBadge = document.getElementById('badge-aerotun-count');
+    if (sideBadge) sideBadge.innerText = (state.aerotunTunnels || []).length;
+
+    renderAeroTUN();
+  } catch (err) {
+    console.error('Failed to load AeroTUN:', err);
+  }
+}
+
+function renderAeroTUN() {
+  const tbody = document.getElementById('aerotun-table-body');
+  const empty = document.getElementById('aerotun-empty');
+
+  // Update summary cards
+  const stats = state.aerotunStats || {};
+  const tunnels = state.aerotunTunnels || [];
+
+  const statActive = document.getElementById('aerotun-stat-active');
+  if (statActive) {
+    const activeCount = tunnels.filter(t => t.running).length;
+    statActive.innerText = `${activeCount} / ${tunnels.length}`;
+  }
+
+  const statSaved = document.getElementById('aerotun-stat-saved');
+  if (statSaved) {
+    statSaved.innerText = formatBytes(stats.total_saved_bytes || 0);
+  }
+
+  const statRatio = document.getElementById('aerotun-stat-ratio');
+  if (statRatio) {
+    statRatio.innerText = stats.average_ratio || '1.00x (0% saved)';
+  }
+
+  const statTraffic = document.getElementById('aerotun-stat-traffic');
+  if (statTraffic) {
+    statTraffic.innerText = `↓ ${formatBytes(stats.total_rx_bytes || 0)} / ↑ ${formatBytes(stats.total_tx_bytes || 0)}`;
+  }
+
+  if (!tbody) return;
+  tbody.innerHTML = '';
+
+  if (tunnels.length === 0) {
+    empty?.classList.remove('hidden');
+    return;
+  }
+  empty?.classList.add('hidden');
+
+  tunnels.forEach(t => {
+    const tr = document.createElement('tr');
+    tr.className = `hover:bg-slate-800/40 transition text-xs ${t.disabled ? 'opacity-50' : ''}`;
+
+    const modeBadge = t.mode === 'server'
+      ? `<span class="px-2 py-0.5 rounded text-[10px] font-mono bg-sky-500/20 text-sky-300 border border-sky-500/30 font-semibold uppercase">Server</span>`
+      : `<span class="px-2 py-0.5 rounded text-[10px] font-mono bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 font-semibold uppercase">Client</span>`;
+
+    const endpointInfo = t.mode === 'server'
+      ? `<span class="text-sky-300 font-mono">Port :${t.listen_port || 51830}</span>`
+      : `<span class="text-indigo-300 font-mono">${escapeHtml(t.remote_endpoint || '(not set)')}</span>`;
+
+    const compBadge = t.compression
+      ? `<span class="px-1.5 py-0.5 rounded text-[10px] font-mono bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 font-semibold" title="Adaptive LZ4 active">${t.compression_ratio || 'LZ4'}</span>`
+      : `<span class="text-slate-500 text-[10px]">Off</span>`;
+
+    const activePeer = t.active_peer_endpoint
+      ? `<span class="text-emerald-400 font-mono text-[11px]" title="Last seen: ${t.last_handshake || 'just now'}">● ${escapeHtml(t.active_peer_endpoint)}</span>`
+      : `<span class="text-slate-500 text-[11px]">${t.mode === 'server' ? 'Waiting for peer...' : 'Connecting...'}</span>`;
+
+    const rawRx = formatBytes(t.rx_bytes || 0);
+    const rawTx = formatBytes(t.tx_bytes || 0);
+    const wireRx = formatBytes(t.wire_bytes_in || 0);
+    const wireTx = formatBytes(t.wire_bytes_out || 0);
+
+    tr.innerHTML = `
+      <td class="py-2.5 px-3 text-center">${renderMikroTikFlag(t.running, t.disabled)}</td>
+      <td class="py-2.5 px-3 font-bold text-white font-mono flex items-center gap-1.5">
+        <span>${escapeHtml(t.name)}</span>
+        ${t.default_route ? '<span class="text-[9px] px-1 rounded bg-amber-500/20 text-amber-300 border border-amber-500/30">DEF</span>' : ''}
+      </td>
+      <td class="py-2.5 px-3">${modeBadge}</td>
+      <td class="py-2.5 px-3 text-xs font-mono">${endpointInfo}</td>
+      <td class="py-2.5 px-3 font-semibold text-emerald-400 font-mono">${escapeHtml(t.local_address || '-')}</td>
+      <td class="py-2.5 px-3">${compBadge}</td>
+      <td class="py-2.5 px-3">${activePeer}</td>
+      <td class="py-2.5 px-3 font-mono text-[11px]">
+        <span class="text-emerald-400">↓ ${rawRx}</span>
+        <span class="text-cyan-400 ml-1">↑ ${rawTx}</span>
+      </td>
+      <td class="py-2.5 px-3 font-mono text-[11px]">
+        <span class="text-slate-400">↓ ${wireRx}</span>
+        <span class="text-slate-400 ml-1">↑ ${wireTx}</span>
+      </td>
+      <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[120px]" title="${escapeHtml(t.comment || '')}">${escapeHtml(t.comment || '-')}</td>
+      <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
+        ${t.running
+          ? `<button onclick="stopAeroTUNTunnel('${t.id}')" class="text-xs text-amber-400 hover:text-amber-300 font-medium">Stop</button>`
+          : `<button onclick="startAeroTUNTunnel('${t.id}')" class="text-xs text-emerald-400 hover:text-emerald-300 font-medium">Start</button>`
+        }
+        <button onclick="toggleAeroTUNState('${t.id}', ${!t.disabled})" class="text-xs ${t.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${t.disabled ? 'Enable' : 'Disable'}</button>
+        <button onclick="openEditAeroTUNModal('${t.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
+        <button onclick="deleteAeroTUN('${t.id}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
+      </td>
+    `;
+    tbody.appendChild(tr);
+  });
+}
+
+function onAeroTUNModeChange() {
+  const mode = document.getElementById('aerotun-modal-mode').value;
+  const endpointInput = document.getElementById('aerotun-modal-endpoint');
+  const portInput = document.getElementById('aerotun-modal-port');
+
+  if (mode === 'server') {
+    if (!portInput.value) portInput.value = '51830';
+    endpointInput.placeholder = 'Optional / learned roaming peer';
+  } else {
+    endpointInput.placeholder = 'vpn.example.com:51830 (required)';
+    if (!portInput.value) portInput.value = '';
+  }
+}
+
+async function generateAeroTUNKey() {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/aerotun/genkey`);
+    if (res.ok) {
+      const data = await res.json();
+      document.getElementById('aerotun-modal-psk').value = data.key;
+      showToast('Key Generated', 'Cryptographic 256-bit PSK generated', 'success');
+    }
+  } catch (err) {
+    const arr = new Uint8Array(32);
+    crypto.getRandomValues(arr);
+    const hex = Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
+    document.getElementById('aerotun-modal-psk').value = hex;
+  }
+}
+
+function openAddAeroTUNModal() {
+  const tunnels = state.aerotunTunnels || [];
+  document.getElementById('aerotun-modal-title').innerText = 'Add AeroTUN Interface';
+  document.getElementById('aerotun-modal-id').value = '';
+  document.getElementById('aerotun-modal-name').value = `aerotun${tunnels.length}`;
+  document.getElementById('aerotun-modal-mode').value = 'server';
+  document.getElementById('aerotun-modal-port').value = 51830 + tunnels.length;
+  document.getElementById('aerotun-modal-endpoint').value = '';
+  document.getElementById('aerotun-modal-local').value = `10.99.${tunnels.length}.1/24`;
+  document.getElementById('aerotun-modal-peer').value = `10.99.${tunnels.length}.2/24`;
+  document.getElementById('aerotun-modal-psk').value = '';
+  document.getElementById('aerotun-modal-mtu').value = '1420';
+  document.getElementById('aerotun-modal-comment').value = '';
+  document.getElementById('aerotun-modal-compression').checked = true;
+  document.getElementById('aerotun-modal-defroute').checked = false;
+  document.getElementById('aerotun-modal-disabled').checked = false;
+
+  generateAeroTUNKey();
+  onAeroTUNModeChange();
+  document.getElementById('aerotun-modal').classList.remove('hidden');
+}
+
+function openEditAeroTUNModal(id) {
+  const t = (state.aerotunTunnels || []).find(tun => tun.id === id);
+  if (!t) return;
+
+  document.getElementById('aerotun-modal-title').innerText = `Edit AeroTUN Interface (${t.name})`;
+  document.getElementById('aerotun-modal-id').value = t.id;
+  document.getElementById('aerotun-modal-name').value = t.name;
+  document.getElementById('aerotun-modal-mode').value = t.mode || 'server';
+  document.getElementById('aerotun-modal-port').value = t.listen_port || '';
+  document.getElementById('aerotun-modal-endpoint').value = t.remote_endpoint || '';
+  document.getElementById('aerotun-modal-local').value = t.local_address || '';
+  document.getElementById('aerotun-modal-peer').value = t.peer_address || '';
+  document.getElementById('aerotun-modal-psk').value = t.psk || '';
+  document.getElementById('aerotun-modal-mtu').value = t.mtu || 1420;
+  document.getElementById('aerotun-modal-comment').value = t.comment || '';
+  document.getElementById('aerotun-modal-compression').checked = !!t.compression;
+  document.getElementById('aerotun-modal-defroute').checked = !!t.default_route;
+  document.getElementById('aerotun-modal-disabled').checked = !!t.disabled;
+
+  onAeroTUNModeChange();
+  document.getElementById('aerotun-modal').classList.remove('hidden');
+}
+
+function closeAeroTUNModal() {
+  document.getElementById('aerotun-modal').classList.add('hidden');
+}
+
+async function submitSaveAeroTUN(e) {
+  e.preventDefault();
+  const id = document.getElementById('aerotun-modal-id').value;
+  const name = document.getElementById('aerotun-modal-name').value.trim();
+  const mode = document.getElementById('aerotun-modal-mode').value;
+  const port = parseInt(document.getElementById('aerotun-modal-port').value) || 0;
+  const endpoint = document.getElementById('aerotun-modal-endpoint').value.trim();
+  const local = document.getElementById('aerotun-modal-local').value.trim();
+  const peer = document.getElementById('aerotun-modal-peer').value.trim();
+  const psk = document.getElementById('aerotun-modal-psk').value.trim();
+  const mtu = parseInt(document.getElementById('aerotun-modal-mtu').value) || 1420;
+  const comment = document.getElementById('aerotun-modal-comment').value.trim();
+  const compression = document.getElementById('aerotun-modal-compression').checked;
+  const defaultRoute = document.getElementById('aerotun-modal-defroute').checked;
+  const disabled = document.getElementById('aerotun-modal-disabled').checked;
+
+  if (mode === 'client' && !endpoint) {
+    showToast('Validation Error', 'Remote endpoint (host:port) is required in client mode', 'error');
+    return;
+  }
+  if (!psk) {
+    showToast('Validation Error', 'Pre-shared key (PSK) is required', 'error');
+    return;
+  }
+
+  try {
+    const res = await fetch(`${API_BASE}/vpn/aerotun/tunnels`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        id,
+        name,
+        mode,
+        listen_port: port,
+        remote_endpoint: endpoint,
+        local_address: local,
+        peer_address: peer,
+        psk,
+        mtu,
+        compression,
+        default_route: defaultRoute,
+        comment,
+        disabled
+      })
+    });
+
+    if (res.ok) {
+      showToast('AeroTUN Saved', `Tunnel ${name} configured successfully`, 'success');
+      closeAeroTUNModal();
+      loadAeroTUN(false);
+      fetchData(false);
+    } else {
+      const err = await res.text();
+      showToast('Error Saving Tunnel', err, 'error');
+    }
+  } catch (err) {
+    showToast('Network Error', err.message, 'error');
+  }
+}
+
+async function startAeroTUNTunnel(id) {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/aerotun/tunnels/${encodeURIComponent(id)}/start`, { method: 'POST' });
+    if (res.ok) {
+      showToast('AeroTUN Started', 'Tunnel interface is active', 'success');
+      loadAeroTUN(false);
+      fetchData(false);
+    } else {
+      const err = await res.text();
+      showToast('Failed to Start', err, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function stopAeroTUNTunnel(id) {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/aerotun/tunnels/${encodeURIComponent(id)}/stop`, { method: 'POST' });
+    if (res.ok) {
+      showToast('AeroTUN Stopped', 'Tunnel stopped', 'info');
+      loadAeroTUN(false);
+      fetchData(false);
+    } else {
+      const err = await res.text();
+      showToast('Failed to Stop', err, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function toggleAeroTUNState(id, disabled) {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/aerotun/tunnels/${encodeURIComponent(id)}/state`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ disabled })
+    });
+    if (res.ok) {
+      showToast('State Updated', disabled ? 'Tunnel disabled' : 'Tunnel enabled', 'info');
+      loadAeroTUN(false);
+      fetchData(false);
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function deleteAeroTUN(id) {
+  if (!confirm('Are you sure you want to delete this AeroTUN tunnel?')) return;
+  try {
+    const res = await fetch(`${API_BASE}/vpn/aerotun/tunnels/${encodeURIComponent(id)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Deleted', 'AeroTUN tunnel removed', 'success');
+      loadAeroTUN(false);
+      fetchData(false);
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
 // ======================== PPP Central Hub & Servers ========================
 function switchPPPSubTab(tab) {
   state.pppSubTab = tab;

+ 222 - 1
internal/web/static/index.html

@@ -268,6 +268,20 @@
           <span id="badge-eoip-count" class="text-[10px] bg-slate-800 text-slate-300 px-1.5 py-0.2 rounded-full font-mono">0</span>
         </a>
 
+        <a href="#aerotun" onclick="navigate('aerotun')" id="nav-aerotun"
+           class="nav-link flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium transition-all group text-slate-300 hover:bg-slate-800/80 hover:text-white">
+          <div class="flex items-center space-x-2.5">
+            <svg class="w-4 h-4 text-sky-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
+            </svg>
+            <span class="flex items-center gap-1.5">
+              <span>AeroTUN</span>
+              <span class="text-[9px] px-1 py-0.2 rounded bg-sky-500/20 text-sky-300 border border-sky-500/30 font-semibold font-mono">LZ4</span>
+            </span>
+          </div>
+          <span id="badge-aerotun-count" class="text-[10px] bg-slate-800 text-slate-300 px-1.5 py-0.2 rounded-full font-mono">0</span>
+        </a>
+
         <!-- CATEGORY: DIAGNOSTICS & TOOLS -->
         <div class="px-3 pt-3 pb-1 text-[10px] font-bold text-slate-500 uppercase tracking-wider">Diagnostics</div>
 
@@ -1452,6 +1466,116 @@
           </div>
         </section>
 
+        <!-- ================= SECTION: AeroTUN (/interface aerotun) ================= -->
+        <section id="section-aerotun" class="hidden space-y-6">
+          <div class="bg-gradient-to-r from-sky-950/50 via-slate-900 to-slate-900 border border-sky-800/40 rounded-xl p-5 flex flex-wrap items-center justify-between gap-4">
+            <div>
+              <div class="flex items-center gap-2 mb-1">
+                <span class="px-2 py-0.5 rounded text-[10px] font-mono bg-sky-500/20 text-sky-300 border border-sky-500/30 font-semibold">/interface aerotun</span>
+                <h2 class="text-base font-bold text-white flex items-center gap-2">
+                  <span>AeroTUN High-Speed VPN</span>
+                  <span class="px-2 py-0.5 text-[10px] rounded-full bg-cyan-500/20 text-cyan-300 border border-cyan-500/40 font-mono font-normal">ChaCha20-Poly1305 + LZ4</span>
+                </h2>
+              </div>
+              <p class="text-xs text-slate-400">Custom ultra-lightweight Layer 3 IP tunnel over UDP with dynamic adaptive LZ4 compression, sliding window anti-replay protection, and roaming NAT traversal.</p>
+            </div>
+            <div class="flex items-center gap-2.5">
+              <button onclick="loadAeroTUN(true)" class="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 transition flex items-center gap-1.5" title="Refresh AeroTUN Status">
+                <span>🔄 Refresh</span>
+              </button>
+              <button onclick="openAddAeroTUNModal()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-sky-600 hover:bg-sky-500 text-white shadow-lg shadow-sky-500/20 transition flex items-center gap-1.5">
+                <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
+                <span>+ Add AeroTUN Tunnel</span>
+              </button>
+            </div>
+          </div>
+
+          <!-- Summary Metric Cards -->
+          <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
+            <div class="bg-slate-900/90 border border-slate-800 rounded-xl p-4 flex items-center justify-between">
+              <div>
+                <div class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Active Tunnels</div>
+                <div id="aerotun-stat-active" class="text-xl font-bold text-white font-mono mt-1">0 / 0</div>
+                <div class="text-[10px] text-slate-500 mt-0.5">Instances online</div>
+              </div>
+              <div class="w-10 h-10 rounded-xl bg-sky-500/10 border border-sky-500/20 flex items-center justify-center text-sky-400">
+                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
+              </div>
+            </div>
+
+            <div class="bg-slate-900/90 border border-slate-800 rounded-xl p-4 flex items-center justify-between">
+              <div>
+                <div class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Compression Savings</div>
+                <div id="aerotun-stat-saved" class="text-xl font-bold text-emerald-400 font-mono mt-1">0 B</div>
+                <div id="aerotun-stat-ratio" class="text-[10px] text-emerald-300 font-mono mt-0.5">1.00x (0% saved)</div>
+              </div>
+              <div class="w-10 h-10 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400">
+                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/></svg>
+              </div>
+            </div>
+
+            <div class="bg-slate-900/90 border border-slate-800 rounded-xl p-4 flex items-center justify-between">
+              <div>
+                <div class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Wire Traffic</div>
+                <div id="aerotun-stat-traffic" class="text-sm font-bold text-white font-mono mt-1">↓ 0 B / ↑ 0 B</div>
+                <div class="text-[10px] text-slate-500 mt-0.5">UDP Wire Transmit / Receive</div>
+              </div>
+              <div class="w-10 h-10 rounded-xl bg-cyan-500/10 border border-cyan-500/20 flex items-center justify-center text-cyan-400">
+                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"/></svg>
+              </div>
+            </div>
+
+            <div class="bg-slate-900/90 border border-slate-800 rounded-xl p-4 flex items-center justify-between">
+              <div>
+                <div class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">AEAD Cipher & Anti-Replay</div>
+                <div class="text-xs font-bold text-sky-300 font-mono mt-1">ChaCha20-Poly1305</div>
+                <div class="text-[10px] text-slate-400 mt-0.5">64-pkt Sliding Window Active</div>
+              </div>
+              <div class="w-10 h-10 rounded-xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center text-indigo-400">
+                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
+              </div>
+            </div>
+          </div>
+
+          <!-- AeroTUN Interfaces Table -->
+          <div class="bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
+            <div class="p-4 border-b border-slate-800 flex items-center justify-between">
+              <div class="flex items-center gap-2">
+                <h3 class="text-sm font-bold text-white">Configured AeroTUN Tunnels</h3>
+                <span id="aerotun-count-badge" class="text-xs font-mono bg-slate-800 text-slate-300 px-2.5 py-0.5 rounded-full">0 Tunnels</span>
+              </div>
+              <div class="text-xs text-slate-400">
+                <span class="font-mono text-emerald-400">R</span> = Running &nbsp;|&nbsp; <span class="font-mono text-rose-400">X</span> = Disabled
+              </div>
+            </div>
+
+            <div class="overflow-x-auto">
+              <table class="w-full text-left text-xs font-mono">
+                <thead class="text-slate-400 bg-slate-950/60 border-b border-slate-800 uppercase tracking-wider text-[10px] font-sans">
+                  <tr>
+                    <th class="py-2.5 px-3 w-12 text-center" title="Flags: R=Running, X=Disabled">Flag</th>
+                    <th class="py-2.5 px-3">Name</th>
+                    <th class="py-2.5 px-3">Mode</th>
+                    <th class="py-2.5 px-3">Port / Remote Endpoint</th>
+                    <th class="py-2.5 px-3">Tunnel IP (CIDR)</th>
+                    <th class="py-2.5 px-3">Compression</th>
+                    <th class="py-2.5 px-3">Active Peer</th>
+                    <th class="py-2.5 px-3">Payload Traffic</th>
+                    <th class="py-2.5 px-3">Wire Traffic</th>
+                    <th class="py-2.5 px-3">Comment</th>
+                    <th class="py-2.5 px-3 text-right font-sans">Actions</th>
+                  </tr>
+                </thead>
+                <tbody id="aerotun-table-body" class="divide-y divide-slate-800/80"></tbody>
+              </table>
+            </div>
+
+            <div id="aerotun-empty" class="hidden p-10 text-center text-slate-500 text-xs font-sans">
+              No AeroTUN interfaces configured. Click "+ Add AeroTUN Tunnel" to create a fast encrypted tunnel.
+            </div>
+          </div>
+        </section>
+
         <!-- ================= SECTION: FIREWALL (/ip firewall) ================= -->
         <section id="section-firewall" class="hidden space-y-6">
           <div class="bg-gradient-to-r from-emerald-950/40 via-slate-900 to-slate-900 border border-emerald-900/30 rounded-xl p-5 flex flex-wrap items-center justify-between gap-4">
@@ -7342,6 +7466,103 @@
     </div>
   </div>
 
+  <!-- ================= MODAL: ADD/EDIT AeroTUN TUNNEL ================= -->
+  <div id="aerotun-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/40">
+        <div>
+          <h3 id="aerotun-modal-title" class="text-sm font-bold text-white">Add AeroTUN Interface</h3>
+          <p class="text-[11px] text-slate-400">ChaCha20-Poly1305 AEAD + Adaptive LZ4 Tunnel</p>
+        </div>
+        <button onclick="closeAeroTUNModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+      <form onsubmit="submitSaveAeroTUN(event)" class="p-6 space-y-4 text-xs font-sans">
+        <input type="hidden" id="aerotun-modal-id">
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Interface Name *</label>
+            <input type="text" id="aerotun-modal-name" placeholder="aerotun0" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-sky-500">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Operating Mode *</label>
+            <select id="aerotun-modal-mode" onchange="onAeroTUNModeChange()" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-sky-500">
+              <option value="server">Server (Listen for incoming peers)</option>
+              <option value="client">Client (Initiate connection to peer)</option>
+            </select>
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Listen UDP Port</label>
+            <input type="number" id="aerotun-modal-port" placeholder="51830" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-sky-500">
+            <span class="text-[10px] text-slate-400">Default: 51830</span>
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Remote Endpoint (Host:Port)</label>
+            <input type="text" id="aerotun-modal-endpoint" placeholder="peer.example.com:51830" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-sky-500">
+            <span class="text-[10px] text-slate-400">Required in Client mode</span>
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Local Tunnel IP (CIDR) *</label>
+            <input type="text" id="aerotun-modal-local" placeholder="10.99.0.1/24" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-sky-500">
+            <span class="text-[10px] text-slate-400">Overlay IPv4 assigned to interface</span>
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Peer Tunnel IP (Optional CIDR)</label>
+            <input type="text" id="aerotun-modal-peer" placeholder="10.99.0.2/24" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-sky-500">
+            <span class="text-[10px] text-slate-400">Expected peer overlay IP</span>
+          </div>
+        </div>
+
+        <div>
+          <div class="flex items-center justify-between mb-1.5">
+            <label class="block font-semibold text-slate-300">Pre-Shared Key (PSK) *</label>
+            <button type="button" onclick="generateAeroTUNKey()" class="text-[11px] text-sky-400 hover:text-sky-300 underline font-mono">⚡ Generate Key</button>
+          </div>
+          <input type="text" id="aerotun-modal-psk" placeholder="32-byte hexadecimal pre-shared secret" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-sky-500">
+          <span class="text-[10px] text-slate-400">Both ends must use identical 256-bit symmetric key for ChaCha20-Poly1305</span>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">MTU</label>
+            <input type="number" id="aerotun-modal-mtu" value="1420" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-sky-500">
+            <span class="text-[10px] text-slate-400">Default: 1420 bytes</span>
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
+            <input type="text" id="aerotun-modal-comment" placeholder="High-speed interconnect" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
+          </div>
+        </div>
+
+        <div class="flex flex-wrap items-center justify-between pt-2 border-t border-slate-800/80 gap-3">
+          <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
+            <input type="checkbox" id="aerotun-modal-compression" checked class="rounded bg-slate-800 border-slate-700 text-sky-600">
+            <span class="font-medium">Adaptive LZ4 Compression</span>
+          </label>
+          <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
+            <input type="checkbox" id="aerotun-modal-defroute" class="rounded bg-slate-800 border-slate-700 text-sky-600">
+            <span class="font-medium">Default Route</span>
+          </label>
+          <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
+            <input type="checkbox" id="aerotun-modal-disabled" class="rounded bg-slate-800 border-slate-700 text-rose-600">
+            <span class="font-medium">Disabled (X)</span>
+          </label>
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
+          <button type="button" onclick="closeAeroTUNModal()" class="px-4 py-2 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
+          <button type="submit" class="px-5 py-2 rounded-lg font-semibold bg-sky-600 hover:bg-sky-500 text-white shadow-lg shadow-sky-500/20">Save AeroTUN</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
   <!-- ================= MODAL: ADD/EDIT PPP SECRET (USER) ================= -->
   <div id="ppp-secret-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
     <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
@@ -9357,6 +9578,6 @@
   </div>
 
   <datalist id="fw-addrlist-datalist"></datalist>
-  <script src="app.js?v=7.1"></script>
+  <script src="app.js?v=8.0"></script>
 </body>
 </html>