Initial vibeproxy implementation

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
This commit is contained in:
2026-08-17 10:28:59 -05:00
co-authored by Claude Sonnet 5
commit ba1f7c4c03
105 changed files with 9142 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
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)
}
}