rename: bridges/valves → tunnels (one term across types + API + UI)
CI / validate (push) Successful in 7s
CI / docker (push) Failing after 5s

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>
This commit is contained in:
2026-06-10 18:48:17 +02:00
parent 7fbe2555fb
commit 41a7e39754
5 changed files with 163 additions and 160 deletions
+38 -37
View File
@@ -5,61 +5,62 @@ import (
"time"
)
// counters is the per-bridge byte tally. Updated from the two hot paths
// counters is the per-tunnel byte tally. Updated from the two hot paths
// (readLoop client→backend, readBackend backend→client) — atomic to avoid
// locking the bridge for every datagram.
// locking the tunnel 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
// 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 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 {
// 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()
valves := make([]*Valve, 0, len(m.valves))
for _, v := range m.valves {
valves = append(valves, v)
listeners := make([]*Listener, 0, len(m.listeners))
for _, l := range m.listeners {
listeners = append(listeners, l)
}
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(),
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(),
})
}
v.mu.Unlock()
l.mu.Unlock()
}
return out
}