package bridge import ( "sync/atomic" "time" ) // counters is the per-tunnel byte tally. Updated from the two hot paths // (readLoop client→backend, readBackend backend→client) — atomic to avoid // locking the tunnel for every datagram. type counters struct { bytesUp atomic.Uint64 // client → backend bytesDown atomic.Uint64 // backend → client } // TunnelSnapshot is one row of the tunnels table the UI renders. All times // are wall-clock; sizes are total bytes since the tunnel opened. type TunnelSnapshot struct { Server string `json:"server"` // pg row name (e.g. "gtnh") Port int `json:"port"` // public UDP port Backend string `json:"backend"` // backend addr Client string `json:"client"` // source IP:port BytesUp uint64 `json:"bytes_up"` // client → backend BytesDown uint64 `json:"bytes_down"` // backend → client OpenedAt time.Time `json:"opened_at"` // tunnel creation LastSeen time.Time `json:"last_seen"` // most-recent datagram either direction IdleSeconds float64 `json:"idle_seconds"` // derived; UI sorts by this } // Snapshot returns one row per active per-client tunnel across all // listeners. Cheap-ish: takes the Manager lock + each Listener lock briefly, // no per-tunnel lock (counters are atomic; LastSeen is read under the // tunnel lock). func (m *Manager) Snapshot() []TunnelSnapshot { m.mu.Lock() listeners := make([]*Listener, 0, len(m.listeners)) for _, l := range m.listeners { listeners = append(listeners, l) } m.mu.Unlock() now := time.Now() var out []TunnelSnapshot for _, l := range listeners { l.mu.Lock() for _, t := range l.tunnels { t.mu.Lock() lastSeen := t.lastSeen opened := t.openedAt t.mu.Unlock() out = append(out, TunnelSnapshot{ Server: l.route.Name, Port: l.route.Port, Backend: l.route.Address, Client: t.client.String(), BytesUp: t.counters.bytesUp.Load(), BytesDown: t.counters.bytesDown.Load(), OpenedAt: opened, LastSeen: lastSeen, IdleSeconds: now.Sub(lastSeen).Seconds(), }) } l.mu.Unlock() } return out }