Self-contained HTTP/HTTPS/TCP/UDP reverse proxy manager written in Go, with SQLite storage, a Bootstrap 5 admin UI (Go templates, no JS framework), and a REST API under /api/v1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
114 lines
2.5 KiB
Go
114 lines
2.5 KiB
Go
package streams
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"log/slog"
|
|
"reflect"
|
|
"sync"
|
|
|
|
"github.com/owen/vibeproxy/internal/database"
|
|
"github.com/owen/vibeproxy/internal/models"
|
|
)
|
|
|
|
type running struct {
|
|
model models.Stream
|
|
cancel context.CancelFunc
|
|
tcp *TCPProxy
|
|
udp *UDPProxy
|
|
}
|
|
|
|
// Manager owns the set of currently-listening TCP/UDP streams and can
|
|
// reconcile it against the database without disturbing streams that
|
|
// haven't changed, so existing connections on unaffected streams are never
|
|
// dropped by a reload.
|
|
type Manager struct {
|
|
db *sql.DB
|
|
log *slog.Logger
|
|
|
|
mu sync.Mutex
|
|
active map[int64]*running // stream ID -> running listener
|
|
baseCtx context.Context
|
|
}
|
|
|
|
func NewManager(db *sql.DB, log *slog.Logger) *Manager {
|
|
return &Manager{db: db, log: log, active: map[int64]*running{}, baseCtx: context.Background()}
|
|
}
|
|
|
|
// Reload fetches the current stream configuration and starts/stops
|
|
// listeners to match it. Streams whose configuration is unchanged are left
|
|
// running untouched.
|
|
func (m *Manager) Reload(ctx context.Context) error {
|
|
streams, err := database.ListStreams(m.db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
desired := map[int64]models.Stream{}
|
|
for _, s := range streams {
|
|
if s.Enabled {
|
|
desired[s.ID] = s
|
|
}
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// Stop streams that were removed or disabled.
|
|
for id, r := range m.active {
|
|
if _, ok := desired[id]; !ok {
|
|
m.stopLocked(id, r)
|
|
}
|
|
}
|
|
|
|
for id, s := range desired {
|
|
if r, ok := m.active[id]; ok {
|
|
if reflect.DeepEqual(r.model, s) {
|
|
continue // unchanged, leave the listener (and its connections) alone
|
|
}
|
|
m.stopLocked(id, r)
|
|
}
|
|
m.startLocked(id, s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) startLocked(id int64, s models.Stream) {
|
|
ctx, cancel := context.WithCancel(m.baseCtx)
|
|
r := &running{model: s, cancel: cancel}
|
|
var err error
|
|
switch s.Protocol {
|
|
case models.StreamTCP:
|
|
r.tcp = NewTCPProxy(s, m.log)
|
|
err = r.tcp.Start(ctx)
|
|
case models.StreamUDP:
|
|
r.udp = NewUDPProxy(s, m.log)
|
|
err = r.udp.Start(ctx)
|
|
}
|
|
if err != nil {
|
|
m.log.Error("failed to start stream", "id", id, "protocol", s.Protocol, "err", err)
|
|
cancel()
|
|
return
|
|
}
|
|
m.active[id] = r
|
|
}
|
|
|
|
func (m *Manager) stopLocked(id int64, r *running) {
|
|
r.cancel()
|
|
if r.tcp != nil {
|
|
r.tcp.Stop()
|
|
}
|
|
if r.udp != nil {
|
|
r.udp.Stop()
|
|
}
|
|
delete(m.active, id)
|
|
}
|
|
|
|
// StopAll shuts down every running stream (used on process shutdown).
|
|
func (m *Manager) StopAll() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for id, r := range m.active {
|
|
m.stopLocked(id, r)
|
|
}
|
|
}
|