enrollment: the server mints the §2.1 bootstrap link, the app consumes it
POST /admin/enroll-tokens now returns the whole link, not just the token: echolot://enroll?v=1&u=<control URL>&p=pin-sha256:<b64>&t=<token> The server is the only party that knows all three parts at once, and the part an operator gets wrong by hand is the base64 pin — which does not fail loudly, it just never matches, surfacing days later as an inscrutable TLS error. The app takes the link from a paste or from an echolot:// deep link (QR scan), and writes URL, pin and credential together or not at all. One trap the tests pin: an unencoded "+" in a query string decodes to a space, so a hand-assembled link arrives with a pin wrong by one character. Base64 has no spaces, so they are restored — unambiguous, and it cannot damage a correctly encoded pin. Also fixes a spec divergence: §2.1 names the field device_credential and the first implementation shipped "credential". Both are sent now and the client prefers the spec's; the alias goes once nothing reads it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8166611af1
commit
ad85f3bfcd
@@ -31,6 +31,7 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -145,12 +146,13 @@ func serve(cfg *config.Config) error {
|
||||
Store: st, Sessions: sessions, Name: cfg.Name,
|
||||
UDPPort: mustPort(firstAddr(cfg.UDPListen)), TCPPort: mustPort(firstAddr(cfg.TCPListen)),
|
||||
StunPort: mustPort(firstAddr(cfg.StunListen)), PinB64: pin, CertChain: cert.Certificate,
|
||||
DelayedEcho: dp.SendDelayedEcho,
|
||||
DownTrain: dp.DownTrain,
|
||||
BigSend: dp.BigSend,
|
||||
TCPRecent: func(ip string) any { return tcpSrv.RecentFor(ip) },
|
||||
Runs: runStore,
|
||||
AppRange: appRange,
|
||||
DelayedEcho: dp.SendDelayedEcho,
|
||||
DownTrain: dp.DownTrain,
|
||||
BigSend: dp.BigSend,
|
||||
TCPRecent: func(ip string) any { return tcpSrv.RecentFor(ip) },
|
||||
Runs: runStore,
|
||||
AppRange: appRange,
|
||||
PublicControlURL: publicControlURL(cfg),
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
@@ -446,3 +448,24 @@ func loadOrCreateCert(cfg *config.Config) (tls.Certificate, error) {
|
||||
slog.Info("generated self-signed certificate", "cert", certPath)
|
||||
return tls.X509KeyPair(certPem, keyPem)
|
||||
}
|
||||
|
||||
// publicControlURL is where clients should reach this server's control plane.
|
||||
//
|
||||
// Configured wins; otherwise the first control listen address is used, which is correct for the
|
||||
// plain case (bind an address, hand out that address). A wildcard bind has no single right answer,
|
||||
// so it is left to the operator rather than guessed — a link pointing at 0.0.0.0 is worse than a
|
||||
// link the operator was told to configure.
|
||||
func publicControlURL(cfg *config.Config) string {
|
||||
if cfg.PublicControlURL != "" {
|
||||
return strings.TrimRight(cfg.PublicControlURL, "/")
|
||||
}
|
||||
addr := firstAddr(cfg.ControlListen)
|
||||
if addr == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(addr, ":") || strings.HasPrefix(addr, "0.0.0.0:") || strings.HasPrefix(addr, "[::]:") {
|
||||
slog.Warn("control plane is bound to a wildcard address; set ECHOLOT_PUBLIC_URL "+
|
||||
"so enrollment links point somewhere reachable", "listen", addr)
|
||||
}
|
||||
return "https://" + addr
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user