package aerotun import ( "encoding/json" "fmt" "log" "os" "path/filepath" "sort" "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) } } sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) 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 }