Gateway/pkg/pairing/client.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

98 lines
2.7 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
// Package pairing handles the handshake between LetheanGateway and lthn.io.
//
// client := pairing.NewClient("https://lthn.io", "api-key", "charon", "eu-west", "10.69.69.165:51820")
// client.Pair()
// client.Heartbeat(stats)
package pairing
import (
"encoding/json"
"net/http"
core "dappco.re/go/core"
)
// Client manages the pairing lifecycle with lthn.io.
type Client struct {
apiURL string
apiKey string
name string
region string
wireguardEndpoint string
}
// NewClient creates a pairing client for the given gateway.
//
// client := pairing.NewClient("https://lthn.io", "key", "charon", "eu-west", "10.0.0.1:51820")
func NewClient(apiURL, apiKey, name, region, wireguardEndpoint string) *Client {
return &Client{
apiURL: apiURL,
apiKey: apiKey,
name: name,
region: region,
wireguardEndpoint: wireguardEndpoint,
}
}
// Pair registers this gateway with lthn.io.
//
// err := client.Pair()
func (c *Client) Pair() error {
body := map[string]interface{}{
"name": c.name,
"capabilities": []string{"vpn", "dns", "proxy"},
"region": c.region,
"bandwidth_mbps": 100,
"max_connections": 50,
"wireguard_endpoint": c.wireguardEndpoint,
}
data, _ := json.Marshal(body)
request, err := http.NewRequest("POST", c.apiURL+"/v1/gateway/pair", core.NewReader(string(data)))
if err != nil {
return core.E("pairing.Pair", "failed to create request", err)
}
request.Header.Set("Content-Type", "application/json")
if c.apiKey != "" {
request.Header.Set("Authorization", "Bearer "+c.apiKey)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
return core.E("pairing.Pair", "request failed", err)
}
defer response.Body.Close()
if response.StatusCode != 200 {
return core.E("pairing.Pair", core.Sprintf("status %d", response.StatusCode), nil)
}
return nil
}
// Heartbeat sends alive status + stats to lthn.io.
//
// err := client.Heartbeat(map[string]interface{}{"connections": 5, "load": 23})
func (c *Client) Heartbeat(stats map[string]interface{}) error {
stats["name"] = c.name
data, _ := json.Marshal(stats)
request, err := http.NewRequest("POST", c.apiURL+"/v1/gateway/heartbeat", core.NewReader(string(data)))
if err != nil {
return core.E("pairing.Heartbeat", "failed to create request", err)
}
request.Header.Set("Content-Type", "application/json")
if c.apiKey != "" {
request.Header.Set("Authorization", "Bearer "+c.apiKey)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
return core.E("pairing.Heartbeat", "request failed", err)
}
defer response.Body.Close()
return nil
}