3f170293f5
release / release (push) Successful in 1m19s
Keep the N always-on tunnel slots fixed but let each slot's WireGuard config
rotate through a larger pool, so a 10-concurrent provider cap (e.g. Proton) can
still cycle 50-100 profiles.
- lxc/rotate.sh + weircon-rotate.{service,timer}: round-robin one slot at a
time through wg-pool/, repointing a symlink and restarting only that slot.
- service: proxyManager tracks per-slot in-flight + drain/undrain state; a
localhost admin server (WEIRCON_ADMIN_LISTEN) lets rotate.sh drain a slot
before teardown and warm it back in after, so no request is routed to a
tunnel mid-rotation. Slots self-heal if undrain never arrives.
- GET /status: poll-friendly JSON of per-slot egress IP/state plus inferred
next-rotation slot + ETA, fed by a background egress-IP prober.
- docs + env examples for all new knobs.
68 lines
2.0 KiB
Go
68 lines
2.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
)
|
|
|
|
// newAdminHandler exposes the rotation control surface. It is intended to bind
|
|
// to localhost only (WEIRCON_ADMIN_LISTEN) and is called by rotate.sh:
|
|
//
|
|
// POST /drain?id=N take slot N out of service; blocks until in-flight
|
|
// drains (bounded by WEIRCON_DRAIN_TIMEOUT_SEC) + settle.
|
|
// 200 {"id":N,"drained":true,"inflight":0} on a clean drain,
|
|
// "drained":false with the residual count on timeout.
|
|
// POST /undrain?id=N return slot N to service after the warmup window.
|
|
// GET /status snapshot of every slot.
|
|
func newAdminHandler(cfg Config, mgr *proxyManager) http.Handler {
|
|
mux := http.NewServeMux()
|
|
|
|
slotID := func(w http.ResponseWriter, r *http.Request) (int, bool) {
|
|
raw := r.URL.Query().Get("id")
|
|
id, err := strconv.Atoi(raw)
|
|
if err != nil || id < 0 || id >= mgr.n {
|
|
http.Error(w, fmt.Sprintf("id must be 0-%d", mgr.n-1), http.StatusBadRequest)
|
|
return 0, false
|
|
}
|
|
return id, true
|
|
}
|
|
writeJSON := func(w http.ResponseWriter, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
mux.HandleFunc("/drain", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "POST only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
id, ok := slotID(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
residual := mgr.drain(id, cfg.DrainTimeout)
|
|
writeJSON(w, map[string]any{"id": id, "drained": residual == 0, "inflight": residual})
|
|
})
|
|
|
|
mux.HandleFunc("/undrain", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "POST only", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
id, ok := slotID(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
mgr.undrain(id)
|
|
writeJSON(w, map[string]any{"id": id, "warmup_sec": int(cfg.DrainWarmup.Seconds())})
|
|
})
|
|
|
|
mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, mgr.snapshot())
|
|
})
|
|
|
|
return mux
|
|
}
|