41a7e39754
API shape:
GET /api/connections → GET /api/tunnels
body: {"connections": […]} → {"tunnels": […]}
Type rename (package stays "bridge" — internal):
Valve → Listener
clientBridge → tunnel
ConnSnapshot → TunnelSnapshot
Log messages mirror the new vocab ("listener open/close", "tunnel
open/idle evict/forward failed"). UI header is now "Active tunnels"
and the empty state reads "no active tunnels".
server-manager's dashboard polls /infra/svc-proxy/api/tunnels and
shows "N tunnels" on the svc-proxy infra card.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
67 lines
2.1 KiB
Go
67 lines
2.1 KiB
Go
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
|
|
}
|