package bridge import ( "sync/atomic" "time" ) // counters is the per-bridge byte tally. Updated from the two hot paths // (readLoop client→backend, readBackend backend→client) — atomic to avoid // locking the bridge for every datagram. type counters struct { bytesUp atomic.Uint64 // client → backend bytesDown atomic.Uint64 // backend → client } // ConnSnapshot is one row of the active-connections table the UI renders. // All times are wall-clock; sizes are total bytes since the bridge opened. type ConnSnapshot struct { Server string `json:"server"` // pg row name (e.g. "gtnh") Port int `json:"port"` // public UDP port (the valve) 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"` // bridge 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 client bridge across all valves. // Cheap-ish: takes the Manager lock + each Valve lock briefly, no per-bridge // lock (counters are atomic; LastSeen is read under the bridge lock). func (m *Manager) Snapshot() []ConnSnapshot { m.mu.Lock() valves := make([]*Valve, 0, len(m.valves)) for _, v := range m.valves { valves = append(valves, v) } m.mu.Unlock() now := time.Now() var out []ConnSnapshot for _, v := range valves { v.mu.Lock() for _, b := range v.bridges { b.mu.Lock() lastSeen := b.lastSeen opened := b.openedAt b.mu.Unlock() out = append(out, ConnSnapshot{ Server: v.route.Name, Port: v.route.Port, Backend: v.route.Address, Client: b.client.String(), BytesUp: b.counters.bytesUp.Load(), BytesDown: b.counters.bytesDown.Load(), OpenedAt: opened, LastSeen: lastSeen, IdleSeconds: now.Sub(lastSeen).Seconds(), }) } v.mu.Unlock() } return out }