Gateway/cmd/gateway/main.go
Claude b33b5548ec
feat: LetheanGateway binary scaffold
Replaces Python+HAProxy gateway stack with a single Go binary:
- pkg/pairing: handshake with lthn.io (pair + heartbeat)
- pkg/meter: per-session bandwidth metering
- cmd/gateway: main binary with admin API on :8880
- Uses core/api for HTTP, pairs via /v1/gateway/* endpoints

Eliminates: Python lvpn, HAProxy config templating, legacy session management.
Keeps: WireGuard (managed via go-process when ready).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 04:54:43 +01:00

103 lines
2.7 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
// LetheanGateway is the companion binary for .lthn gateway node operators.
// It replaces the legacy Python+HAProxy stack with a single Go service that:
//
// - Pairs with lthn.io using the node's chain alias identity
// - Manages WireGuard tunnels for VPN customers
// - Routes proxy traffic for residential/mobile/SEO services
// - Meters bandwidth usage per session
// - Reports heartbeat + stats to lthn.io for dispatch
//
// Usage:
//
// lethean-gateway --name charon --api https://api.lthn.io --region eu-west
package main
import (
"context"
"encoding/json"
"net/http"
"time"
core "dappco.re/go/core"
"lethean.io/gateway/pkg/meter"
"lethean.io/gateway/pkg/pairing"
)
var (
apiURL = envDefault("LTHN_API", "https://lthn.io")
apiKey = envDefault("LTHN_API_KEY", "")
name = envDefault("LTHN_GATEWAY_NAME", "")
region = envDefault("LTHN_REGION", "unknown")
wgPort = envDefault("WG_PORT", "51820")
wgHost = envDefault("WG_HOST", "0.0.0.0")
)
func envDefault(key, fallback string) string {
if value := core.Env(key); value != "" {
return value
}
return fallback
}
func main() {
core.Println("Lethean Gateway")
core.Println(core.Sprintf(" Name: %s.lthn", name))
core.Println(core.Sprintf(" API: %s", apiURL))
core.Println(core.Sprintf(" Region: %s", region))
core.Println("")
if name == "" {
core.Println("ERROR: LTHN_GATEWAY_NAME required (your .lthn alias)")
return
}
// Bandwidth meter
bandwidth := meter.New()
// Pair with lthn.io
client := pairing.NewClient(apiURL, apiKey, name, region, wgHost+":"+wgPort)
err := client.Pair()
if err != nil {
core.Println(core.Sprintf("Pairing failed: %v", err))
core.Println("Retrying in 30s...")
} else {
core.Println("Paired with lthn.io")
}
// Heartbeat loop
go func() {
for {
time.Sleep(60 * time.Second)
stats := bandwidth.Snapshot()
heartbeatErr := client.Heartbeat(stats)
if heartbeatErr != nil {
core.Println(core.Sprintf("Heartbeat failed: %v — will retry", heartbeatErr))
}
}
}()
// Local admin API
http.HandleFunc("/health", func(writer http.ResponseWriter, request *http.Request) {
json.NewEncoder(writer).Encode(map[string]interface{}{
"name": name + ".lthn",
"region": region,
"status": "online",
"sessions": bandwidth.ActiveSessions(),
"bytes_total": bandwidth.TotalBytes(),
})
})
http.HandleFunc("/stats", func(writer http.ResponseWriter, request *http.Request) {
json.NewEncoder(writer).Encode(bandwidth.Snapshot())
})
core.Println("Admin API listening on :8880")
http.ListenAndServe(":8880", nil)
}
// Placeholder — will be replaced by core/api RouteGroup when ready
func init() {
_ = context.Background
}