eleven / messenger Public

Revert "server: replicate mesh-eligible routes to every live mesh peer"

This reverts commit c2caf045a0c149e82d80ef459fe35fb4c338ba05.
James Adam 1b4bd808771c462df20ce4dbfddc617b4fcaf896 parent c2caf04
7 files changed, +8 −497
  • control.go +0 −12
  • main.go +0 −3
  • mesh.go +5 −22
  • mesh_test.go +2 −26
  • meshreplicate.go +0 −274
  • meshreplicate_test.go +0 −148
  • router.go +1 −12
diff --git a/control.go b/control.go
index 9054101..e962630 100644
--- a/control.go
+++ b/control.go
@@ -287,18 +287,6 @@ func materializeInstance(reg *Registry, route Route, token string, opts addInsta
if err := reg.upsert(route); err != nil {
return nil, fmt.Errorf("mark ready: %v", err)
}
- if route.Hibernate {
- // Best-effort: give any live mesh peer this route before HRW could ever
- // place it there (meshreplicate.go). A no-op when the mesh is off or
- // this host is out of LCHAT_MESH_DOMAINS scope — self-hosters and
- // non-mesh boxes are unaffected. Never fails materialization: the
- // route and admin link stand regardless of replication's outcome.
- if hc, hcErr := hibernateConfigFromEnv(); hcErr != nil {
- log.Printf("mesh: skipping route replication for %s: %v", host, hcErr)
- } else {
- fanOutMeshRoute(hc, route)
- }
- }
if err := systemctl("reload", routerUnit); err != nil {
return nil, fmt.Errorf("reload %s: %v (route added; run: systemctl reload %s)", routerUnit, err, routerUnit)
}
diff --git a/main.go b/main.go
index c31f9c6..03f04fe 100644
--- a/main.go
+++ b/main.go
@@ -143,9 +143,6 @@ func main() {
case "add-instance":
runAddInstance(os.Args[2:])
return
- case "mesh-seed":
- runMeshSeed(os.Args[2:])
- return
case "deploy":
runDeploy(os.Args[2:])
return
diff --git a/mesh.go b/mesh.go
index 12197c8..b27ff0f 100644
--- a/mesh.go
+++ b/mesh.go
@@ -99,7 +99,6 @@ type meshSvc struct {
self string // this box's peer base URL — its mesh identity
store meshStore
domains []string // LCHAT_MESH_DOMAINS, lowercased — see inScope. Empty = unrestricted (test convenience only; meshFromEnv always supplies a non-empty list)
- token string // LCHAT_MESH_TOKEN — shared secret authenticating box-to-box route replication (meshreplicate.go); empty = unrestricted (test convenience only)
interval time.Duration
staleAfter time.Duration
now func() time.Time // a seam for the unit tests' clock
@@ -136,10 +135,7 @@ func newMesh(self string, store meshStore, domains []string) *meshSvc {
// comma-list shape as LCHAT_WILDCARD_DOMAINS, parsed by the same
// splitDomains): failing loudly here beats silently letting HRW "place" a
// host onto a box that has no registry row for it (a 404, not a graceful
-// wake) or that could never get it a matching cert — see inScope. And
-// LCHAT_MESH_TOKEN, the shared secret every mesh member authenticates
-// box-to-box route replication with (meshreplicate.go) — without it any box
-// could hand any other box arbitrary routes.
+// wake) or that could never get it a matching cert — see inScope.
func meshFromEnv(hc *hibernateConfig) (*meshSvc, error) {
self := strings.TrimSpace(os.Getenv("LCHAT_MESH_SELF"))
if self == "" {
@@ -152,10 +148,6 @@ func meshFromEnv(hc *hibernateConfig) (*meshSvc, error) {
if len(domains) == 0 {
return nil, errors.New("LCHAT_MESH_SELF set but LCHAT_MESH_DOMAINS missing — mesh placement needs an explicit domain allowlist so HRW never assigns a host to a box that has no registry row for it or can't obtain it a matching cert; set it to the domains every mesh member can serve (typically the same value as LCHAT_WILDCARD_DOMAINS)")
}
- token := strings.TrimSpace(os.Getenv("LCHAT_MESH_TOKEN"))
- if token == "" {
- return nil, errors.New("LCHAT_MESH_SELF set but LCHAT_MESH_TOKEN missing — route replication between mesh members needs a shared secret; generate one and set it identically on every mesh box")
- }
self = strings.TrimRight(self, "/")
u, err := url.Parse(self)
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") || u.Path != "" {
@@ -165,9 +157,7 @@ func meshFromEnv(hc *hibernateConfig) (*meshSvc, error) {
if err != nil {
return nil, fmt.Errorf("mesh heartbeat store: %w", err)
}
- m := newMesh(self, store, domains)
- m.token = token
- return m, nil
+ return newMesh(self, store, domains), nil
}
// start registers this box (one synchronous beat, so the first sweep and the
@@ -272,24 +262,17 @@ func (m *meshSvc) liveBoxes() []string {
// hosts under a configured LCHAT_MESH_DOMAINS domain are ever hashed across
// boxes; everything else always resolves to whichever box is asked — correct
// because registerInstance only ever replicates a route to the boxes covered
-// by those same domains (meshreplicate.go), so no other box has a row for it
+// by those same domains (control.go), so no other box has a row for it
// anyway, and this box (which was asked) is the only one that could possibly
// serve it. An empty domain list is unrestricted — a test-construction
// convenience (direct newMesh callers exercising bare HRW); meshFromEnv
// always supplies a non-empty list in production.
func (m *meshSvc) inScope(host string) bool {
- return hostInDomains(host, m.domains)
-}
-
-// hostInDomains is inScope's matching rule, factored out so `lchat mesh-seed`
-// (meshreplicate.go) can filter routes to replicate without needing a full
-// meshSvc: host equals a configured domain, or is a subdomain of one.
-func hostInDomains(host string, domains []string) bool {
- if len(domains) == 0 {
+ if len(m.domains) == 0 {
return true
}
host = strings.ToLower(host)
- for _, d := range domains {
+ for _, d := range m.domains {
if host == d || strings.HasSuffix(host, "."+d) {
return true
}
diff --git a/mesh_test.go b/mesh_test.go
index 95df128..eaf6d8f 100644
--- a/mesh_test.go
+++ b/mesh_test.go
@@ -424,7 +424,6 @@ func TestMeshDomainScopingBypassesHRW(t *testing.T) {
// no registry row for it (see the inScope doc comment).
func TestMeshFromEnvRequiresDomains(t *testing.T) {
t.Setenv("LCHAT_MESH_SELF", "https://box-a.internal")
- t.Setenv("LCHAT_MESH_TOKEN", "shared-secret")
t.Setenv("LCHAT_MESH_DOMAINS", "")
if _, err := meshFromEnv(&hibernateConfig{Bucket: "b", Region: "eu-west-1"}); err == nil {
t.Fatal("mesh without LCHAT_MESH_DOMAINS should be a config error")
@@ -440,27 +439,6 @@ func TestMeshFromEnvRequiresDomains(t *testing.T) {
}
}
-// meshFromEnv refuses to start the mesh without a shared route-replication
-// secret — an unauthenticated /api/mesh/route would let any caller hand this
-// box arbitrary routes (see meshreplicate.go).
-func TestMeshFromEnvRequiresToken(t *testing.T) {
- t.Setenv("LCHAT_MESH_SELF", "https://box-a.internal")
- t.Setenv("LCHAT_MESH_DOMAINS", "11ch.at")
- t.Setenv("LCHAT_MESH_TOKEN", "")
- if _, err := meshFromEnv(&hibernateConfig{Bucket: "b", Region: "eu-west-1"}); err == nil {
- t.Fatal("mesh without LCHAT_MESH_TOKEN should be a config error")
- }
-
- t.Setenv("LCHAT_MESH_TOKEN", "shared-secret")
- m, err := meshFromEnv(&hibernateConfig{Bucket: "b", Region: "eu-west-1"})
- if err != nil || m == nil {
- t.Fatalf("valid token: %v, %v", m, err)
- }
- if m.token != "shared-secret" {
- t.Fatalf("token = %q, want it read through", m.token)
- }
-}
-
// --- the proxy hop -------------------------------------------------------------
// Scheme/Host/SNI semantics of the peer hop: the instance hostname survives
@@ -638,11 +616,9 @@ func TestMeshOffTripwire(t *testing.T) {
// meshFromEnv: unset = off (nil, nil — the zero-change default); set without
// the hibernation bucket = a loud config error; malformed URLs rejected.
func TestMeshFromEnv(t *testing.T) {
- // A valid domain allowlist and token throughout —
- // TestMeshFromEnvRequiresDomains/TestMeshFromEnvRequiresToken cover their
- // missing cases on their own.
+ // A valid domain allowlist throughout — TestMeshFromEnvRequiresDomains
+ // covers the missing-domains case on its own.
t.Setenv("LCHAT_MESH_DOMAINS", "11ch.at")
- t.Setenv("LCHAT_MESH_TOKEN", "shared-secret")
t.Setenv("LCHAT_MESH_SELF", "")
if m, err := meshFromEnv(&hibernateConfig{Bucket: "b"}); err != nil || m != nil {
diff --git a/meshreplicate.go b/meshreplicate.go
deleted file mode 100644
index ab697af..0000000
--- a/meshreplicate.go
+++ /dev/null
@@ -1,274 +0,0 @@
-package main
-
-// Route replication across the activator mesh (mesh.go). registerInstance's
-// reg.upsert only ever writes ONE box's registry — but a host under
-// LCHAT_MESH_DOMAINS needs a routes row on every live mesh member before HRW
-// can honestly place it there (mesh.go's inScope doc comment: a box with no
-// row for a host 404s it, never wakes it). This file is the fan-out:
-//
-// - An admin RPC, POST /api/mesh/route (meshRouteHandler, wired by runRouter
-// only when the mesh is on), inserts or updates one route.
-// - registerInstance calls fanOutMeshRoute right after its local upsert,
-// best-effort against whichever peers are live right now.
-// - `lchat mesh-seed <peer>` (runMeshSeed) bulk-replicates every existing
-// in-scope route once — the catch-up a freshly joined box needs before it
-// can serve anything, since it joins with an empty registry.
-//
-// Both paths send only the fields a route's CONTENT actually needs (host,
-// kind, hibernate flag, per-instance config) — never Socket/DBPath, which are
-// box-local filesystem layout the receiving box must derive itself exactly as
-// registerInstance does for a route created locally, and never
-// Status/NextDue/CreatedAt, which are receiver-local lifecycle state.
-//
-// Honest gap: route MUTATIONS after creation (rename, remove, a hibernate
-// flip) are not replicated — only the initial registerInstance fan-out and
-// the one-time seed. A box whose peer never saw a later change either serves
-// stale config (rare per-instance fields) or, for a removed host, still has a
-// row an out-of-scope hash never assigns it anyway (mesh.go's inScope). Worth
-// closing before mesh placement is load-bearing for more than the initial
-// auto-provisioned space pool.
-
-import (
- "bytes"
- "context"
- "crypto/subtle"
- "crypto/tls"
- "encoding/json"
- "flag"
- "fmt"
- "io"
- "log"
- "net"
- "net/http"
- "net/url"
- "os"
- "path/filepath"
- "strings"
- "time"
-)
-
-// meshRoutePayload is the wire shape for one replicated route — deliberately
-// narrower than Route (see file doc comment).
-type meshRoutePayload struct {
- Host string
- Kind string
- Hibernate bool
- AppStoreURL string
- HomeOrigin string
- PushRelayURL string
- PushRelayToken string
- HomeRegion string
- Provider string
- Version string
-}
-
-func routeToPayload(r Route) meshRoutePayload {
- return meshRoutePayload{
- Host: r.Host, Kind: r.Kind, Hibernate: r.Hibernate,
- AppStoreURL: r.AppStoreURL, HomeOrigin: r.HomeOrigin,
- PushRelayURL: r.PushRelayURL, PushRelayToken: r.PushRelayToken,
- HomeRegion: r.HomeRegion, Provider: r.Provider, Version: r.Version,
- }
-}
-
-// toLocalRoute rebuilds a full Route from a peer's payload, deriving the
-// box-local Socket/DBPath itself — never trusting a peer's filesystem layout
-// — exactly as registerInstance does for a route created locally.
-func (p meshRoutePayload) toLocalRoute() (Route, error) {
- host := strings.ToLower(strings.TrimSpace(p.Host))
- if !validHost(host) {
- return Route{}, fmt.Errorf("%w: %q", errInvalidHost, host)
- }
- socket := filepath.Join(instanceSocketDir, host+".sock")
- if err := checkSocketLen(socket); err != nil {
- return Route{}, err
- }
- return Route{
- Host: host, Kind: p.Kind, Socket: socket,
- DBPath: filepath.Join(instanceDataDir, host+".db"),
- AppStoreURL: p.AppStoreURL,
- HomeOrigin: p.HomeOrigin,
- PushRelayURL: p.PushRelayURL,
- PushRelayToken: p.PushRelayToken,
- HomeRegion: p.HomeRegion,
- Provider: p.Provider,
- Hibernate: p.Hibernate,
- Version: p.Version,
- }, nil
-}
-
-// meshReplicateRoute POSTs one route to peer's /api/mesh/route, authenticated
-// with the mesh's shared token. Used both by fanOutMeshRoute and by
-// runMeshSeed's bulk seed.
-// The peer's own base URL (LCHAT_MESH_SELF, typically a bare address like
-// https://10.0.0.5) was never issued a certificate of its own — only
-// registered, wildcard-covered instance hostnames get one. So — exactly like
-// mesh.go's own newPeerProxy authenticates a proxied hop by the INSTANCE
-// hostname, never the peer's bare identity — this dials the peer's raw
-// address but performs TLS (and sends Host) as route.Host: a wildcard-covered
-// hostname the receiving box's wildcard manager hands back a valid cert for
-// regardless of whether that host is registered THERE yet (the "unknown
-// subdomain still completes TLS" property wildcard.go relies on). Callers
-// only ever pass an in-scope route (fanOutMeshRoute/runMeshSeed both check
-// first), so this is never asked to authenticate as an out-of-scope host.
-func meshReplicateRoute(ctx context.Context, peer, token string, route Route) error {
- peerURL, err := url.Parse(peer)
- if err != nil || peerURL.Host == "" {
- return fmt.Errorf("bad peer URL %q: %w", peer, err)
- }
- body, err := json.Marshal(routeToPayload(route))
- if err != nil {
- return err
- }
- reqURL := url.URL{Scheme: peerURL.Scheme, Host: route.Host, Path: "/api/mesh/route"}
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL.String(), bytes.NewReader(body))
- if err != nil {
- return err
- }
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Authorization", "Bearer "+token)
-
- dialAddr := peerDialAddr(peerURL)
- transport := &http.Transport{
- DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
- return (&net.Dialer{Timeout: 10 * time.Second}).DialContext(ctx, network, dialAddr)
- },
- }
- if peerURL.Scheme == "https" {
- transport.DialTLSContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
- conn, err := (&net.Dialer{Timeout: 10 * time.Second}).DialContext(ctx, network, dialAddr)
- if err != nil {
- return nil, err
- }
- tlsConn := tls.Client(conn, &tls.Config{ServerName: route.Host})
- if err := tlsConn.HandshakeContext(ctx); err != nil {
- conn.Close()
- return nil, err
- }
- return tlsConn, nil
- }
- }
- client := &http.Client{Transport: transport}
-
- resp, err := client.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
- return fmt.Errorf("peer %s: %s", peer, resp.Status)
- }
- return nil
-}
-
-// fanOutMeshRoute replicates route to every OTHER live mesh member,
-// best-effort: a peer that's down or errors just can't yet serve this host
-// (see the file doc comment's honest gap on mutations — creation retries only
-// via a later add-instance or an operator's mesh-seed rerun). A no-op — byte-
-// identical to today — whenever the mesh is off or the host is out of
-// LCHAT_MESH_DOMAINS scope, so self-hosters and non-mesh boxes are untouched.
-// Called right after registerInstance's local reg.upsert.
-func fanOutMeshRoute(hc *hibernateConfig, route Route) {
- mesh, err := meshFromEnv(hc)
- if err != nil || mesh == nil || !mesh.inScope(route.Host) {
- return
- }
- mesh.beat() // one synchronous PUT+LIST for a fresh membership view
- for _, peer := range mesh.liveBoxes() {
- if peer == mesh.self {
- continue
- }
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- err := meshReplicateRoute(ctx, peer, mesh.token, route)
- cancel()
- if err != nil {
- log.Printf("mesh: replicate %s to %s failed: %v — it can't serve this host until a later replication succeeds", route.Host, peer, err)
- }
- }
-}
-
-// meshRouteHandler is the receiving side: POST /api/mesh/route, wired by
-// runRouter only when the mesh is on. Bearer-authenticated with the same
-// shared token every mesh member is configured with — without this, any
-// caller could hand this box arbitrary routes.
-func meshRouteHandler(reg *Registry, rt *router, token string) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- const prefix = "Bearer "
- auth := r.Header.Get("Authorization")
- if !strings.HasPrefix(auth, prefix) ||
- subtle.ConstantTimeCompare([]byte(strings.TrimPrefix(auth, prefix)), []byte(token)) != 1 {
- http.Error(w, "unauthorized", http.StatusUnauthorized)
- return
- }
- var payload meshRoutePayload
- if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&payload); err != nil {
- http.Error(w, "bad request", http.StatusBadRequest)
- return
- }
- route, err := payload.toLocalRoute()
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- if err := reg.upsert(route); err != nil {
- log.Printf("mesh: route replication upsert %s failed: %v", route.Host, err)
- http.Error(w, "internal error", http.StatusInternalServerError)
- return
- }
- if err := rt.reload(); err != nil {
- log.Printf("mesh: route replication reload after %s failed: %v", route.Host, err)
- }
- w.WriteHeader(http.StatusNoContent)
- }
-}
-
-// runMeshSeed is `lchat mesh-seed <peer-base-url>`: bulk-replicates every
-// existing in-scope, hibernating route to one peer — the one-time catch-up a
-// freshly joined box needs (it starts with an empty registry) before HRW can
-// honestly place any host there. Reads LCHAT_MESH_DOMAINS/LCHAT_MESH_TOKEN
-// directly rather than through meshFromEnv: the CALLER (the already-running
-// box doing the seeding) need not have LCHAT_MESH_SELF set yet — seeding is
-// an operator step that runs before the mesh itself is flipped on (see
-// docs/instance-router.md).
-func runMeshSeed(args []string) {
- fs := flag.NewFlagSet("mesh-seed", flag.ExitOnError)
- registryPath := fs.String("registry", envOr("LCHAT_REGISTRY", "/data/registry.db"), "router registry SQLite path")
- _ = fs.Parse(args)
- peer := fs.Arg(0)
- if peer == "" {
- die("mesh-seed: usage: lchat mesh-seed <peer-base-url>")
- }
- domains := splitDomains(os.Getenv("LCHAT_MESH_DOMAINS"))
- if len(domains) == 0 {
- die("mesh-seed: LCHAT_MESH_DOMAINS must be set to the domains this seed covers")
- }
- token := strings.TrimSpace(os.Getenv("LCHAT_MESH_TOKEN"))
- if token == "" {
- die("mesh-seed: LCHAT_MESH_TOKEN must be set")
- }
-
- reg, err := openRegistry(*registryPath)
- if err != nil {
- die("mesh-seed: open registry: %v", err)
- }
- routes, err := reg.list()
- if err != nil {
- die("mesh-seed: list routes: %v", err)
- }
- seeded := 0
- for _, route := range routes {
- if !route.Hibernate || !hostInDomains(route.Host, domains) {
- continue
- }
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- err := meshReplicateRoute(ctx, peer, token, route)
- cancel()
- if err != nil {
- fmt.Printf(" %s: FAILED (%v)\n", route.Host, err)
- continue
- }
- fmt.Printf(" %s: seeded\n", route.Host)
- seeded++
- }
- fmt.Printf("seeded %d/%d in-scope hibernating route(s) to %s\n", seeded, len(routes), peer)
-}
diff --git a/meshreplicate_test.go b/meshreplicate_test.go
deleted file mode 100644
index f918699..0000000
--- a/meshreplicate_test.go
+++ /dev/null
@@ -1,148 +0,0 @@
-package main
-
-// Tests for meshreplicate.go: the trust boundary in toLocalRoute (only
-// content fields survive a peer's payload; Socket/DBPath are always derived
-// locally), the receiving handler's bearer auth, and the full round trip over
-// a plain HTTP peer (meshReplicateRoute's TLS-SNI-pinned dial for https peers
-// mirrors mesh.go's own newPeerProxy, already exercised by TestPeerProxyRewrite
-// — this file proves the request/response/auth/upsert plumbing, not the TLS
-// mechanics a second time).
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "path/filepath"
- "testing"
-)
-
-// toLocalRoute must never trust a peer's Socket/DBPath (box-local filesystem
-// layout) — it derives both itself from Host, exactly like registerInstance
-// does for a route created locally.
-func TestMeshRoutePayloadTrustBoundary(t *testing.T) {
- route := Route{
- Host: "space1.11ch.at", Kind: "instance", Socket: "/peer/only/space1.sock",
- DBPath: "/peer/only/space1.db", Hibernate: true, Version: "v1",
- AppStoreURL: "https://apps.example.com", HomeRegion: "eu",
- }
- payload := routeToPayload(route)
-
- // Round-trip through JSON, as it travels over the wire.
- body, err := json.Marshal(payload)
- if err != nil {
- t.Fatalf("marshal: %v", err)
- }
- var decoded meshRoutePayload
- if err := json.Unmarshal(body, &decoded); err != nil {
- t.Fatalf("unmarshal: %v", err)
- }
-
- local, err := decoded.toLocalRoute()
- if err != nil {
- t.Fatalf("toLocalRoute: %v", err)
- }
- if local.Socket == route.Socket || local.DBPath == route.DBPath {
- t.Fatalf("peer's Socket/DBPath leaked through: got %q / %q", local.Socket, local.DBPath)
- }
- if want := filepath.Join(instanceSocketDir, "space1.11ch.at.sock"); local.Socket != want {
- t.Fatalf("Socket = %q, want locally-derived %q", local.Socket, want)
- }
- if want := filepath.Join(instanceDataDir, "space1.11ch.at.db"); local.DBPath != want {
- t.Fatalf("DBPath = %q, want locally-derived %q", local.DBPath, want)
- }
- if local.Host != route.Host || local.Version != route.Version || local.AppStoreURL != route.AppStoreURL || local.HomeRegion != route.HomeRegion || !local.Hibernate {
- t.Fatalf("content fields did not survive: %+v", local)
- }
- if local.Status != "" || local.NextDue != 0 || local.CreatedAt != 0 {
- t.Fatalf("receiver-local lifecycle fields should be zero, got %+v", local)
- }
-}
-
-// An invalid host in the payload must be rejected, not silently coerced into
-// a filename/socket path.
-func TestMeshRoutePayloadRejectsBadHost(t *testing.T) {
- if _, err := (meshRoutePayload{Host: "not a host!!"}).toLocalRoute(); err == nil {
- t.Fatal("bad host should be rejected")
- }
-}
-
-// meshRouteHandler: wrong or missing bearer is 401 and never touches the
-// registry; a correctly authenticated request upserts the route and reloads
-// the router so it's immediately servable.
-func TestMeshRouteHandlerAuth(t *testing.T) {
- reg, err := openRegistry(filepath.Join(t.TempDir(), "registry.db"))
- if err != nil {
- t.Fatalf("open registry: %v", err)
- }
- rt, err := newRouter(reg, nil)
- if err != nil {
- t.Fatalf("new router: %v", err)
- }
- srv := httptest.NewServer(meshRouteHandler(reg, rt, "correct-token"))
- defer srv.Close()
-
- route := Route{Host: "space2.11ch.at", Kind: "instance", Hibernate: true}
- body, _ := json.Marshal(routeToPayload(route))
-
- post := func(bearer string) int {
- req, _ := http.NewRequest(http.MethodPost, srv.URL, bytes.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- if bearer != "" {
- req.Header.Set("Authorization", "Bearer "+bearer)
- }
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- t.Fatalf("post: %v", err)
- }
- defer resp.Body.Close()
- return resp.StatusCode
- }
-
- if code := post(""); code != http.StatusUnauthorized {
- t.Fatalf("no bearer: got %d, want 401", code)
- }
- if code := post("wrong-token"); code != http.StatusUnauthorized {
- t.Fatalf("wrong bearer: got %d, want 401", code)
- }
- if rt.allowed(route.Host) {
- t.Fatal("route must not exist after unauthorized attempts")
- }
-
- if code := post("correct-token"); code != http.StatusNoContent {
- t.Fatalf("correct bearer: got %d, want 204", code)
- }
- if !rt.allowed(route.Host) {
- t.Fatal("route should be servable immediately after a successful replication (reload)")
- }
-}
-
-// The full round trip over a plain HTTP peer: meshReplicateRoute dials the
-// peer's raw address and the handler on the other end upserts + reloads.
-func TestMeshReplicateRouteOverHTTP(t *testing.T) {
- reg, err := openRegistry(filepath.Join(t.TempDir(), "registry.db"))
- if err != nil {
- t.Fatalf("open registry: %v", err)
- }
- rt, err := newRouter(reg, nil)
- if err != nil {
- t.Fatalf("new router: %v", err)
- }
- mux := http.NewServeMux()
- mux.Handle("POST /api/mesh/route", meshRouteHandler(reg, rt, "shared-secret"))
- srv := httptest.NewServer(mux)
- defer srv.Close()
-
- route := testRoute("space3.11ch.at")
- if err := meshReplicateRoute(context.Background(), srv.URL, "shared-secret", route); err != nil {
- t.Fatalf("meshReplicateRoute: %v", err)
- }
- if !rt.allowed(route.Host) {
- t.Fatal("route was not replicated")
- }
-
- if err := meshReplicateRoute(context.Background(), srv.URL, "wrong-secret", route); err == nil {
- t.Fatal("wrong token should be rejected")
- }
-}
diff --git a/router.go b/router.go
index 89556b3..76a8173 100644
--- a/router.go
+++ b/router.go
@@ -405,22 +405,11 @@ func runRouter(args []string) {
http.Redirect(w, r, "https://"+host+r.URL.RequestURI(), http.StatusFound)
})
- // The mesh route-replication endpoint (meshreplicate.go) only exists when
- // the mesh is on — everything else keeps proxying straight through rt, so
- // a non-mesh box's HTTPS handler is byte-identical to before this existed.
- var httpsHandler http.Handler = rt
- if mesh != nil {
- mux := http.NewServeMux()
- mux.Handle("POST /api/mesh/route", meshRouteHandler(reg, rt, mesh.token))
- mux.Handle("/", rt)
- httpsHandler = mux
- }
-
// ReadHeaderTimeout guards the public front door against Slowloris-style slow-
// header connection exhaustion. No ReadTimeout/WriteTimeout — those would break
// long-lived proxied WebSockets.
httpSrv := &http.Server{Addr: *httpAddr, Handler: m.HTTPHandler(httpFallback), ReadHeaderTimeout: 10 * time.Second}
- httpsSrv := &http.Server{Addr: *httpsAddr, Handler: httpsHandler, TLSConfig: tlsCfg, ReadHeaderTimeout: 10 * time.Second}
+ httpsSrv := &http.Server{Addr: *httpsAddr, Handler: rt, TLSConfig: tlsCfg, ReadHeaderTimeout: 10 * time.Second}
// :80 serves the ACME HTTP-01 challenge and redirects everything else to https.
go func() {