f823c05aa3
Standalone Go service that routes SVC client traffic to per-server
backend voice endpoints, configured via pg LISTEN/NOTIFY (same channel
mc-router subscribes to). Each pg `servers` row with both
`voice_address` and `voice_proxy_port` set spawns a Valve: a public
UDP listener that maintains per-client ephemeral bridges to the
backend's SVC port.
Pieces:
cmd/svc-proxy/main.go entry; wires config, log fan-out,
bridge.Manager, pgsync, httpsrv
internal/config/ DATABASE_URL + BIND_HOST +
BRIDGE_IDLE_TTL (default 1m) +
HTTP_ADDR (default :8081)
internal/pgsync/ LISTEN automc_routes_changed; diff
desired/actual routes; emit Apply()
internal/bridge/ Valve per public port; per-client
bridge with atomic up/down byte counters;
idle eviction every 15s against TTL
internal/httpsrv/ operator UI — embedded single-page HTML
with active-connections table polled
every 1s + SSE log stream
(last 500 lines backlog on connect)
Reverse-proxied behind server-manager at /infra/svc-proxy/* — bind
internal-only addresses for production; auth is the dashboard's
Basic gate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
66 lines
2.1 KiB
Go
66 lines
2.1 KiB
Go
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
|
|
}
|