mesh/delivery merged: source.main moved to novox/mesh-controller@159db37618c2
mesh/check being carried to b7c3868980f3 on 159db37618c2 | from a4426190b0c7
Review of ddae6729: a scanner's random Host or SNI names each became a key, bounded only by the
bucket's lifetime and size, and every failed write was retried at each change and logged each time.
A refused name is now kept by name only under the registrable domain of a name the proxy serves, and
at most five new names a minute; every other refusal is counted in memory and written once a minute
under <node>._other (last minute, since start), which route_proxy_unrouted shows. A refused write of
that state is dropped and logged once until a write succeeds; trimming memory to 4096 names moved
from the request to the writer. The visitor's address is no longer kept: the journal has it.
Quick: drop unrouted keys from strangers by a fixed allow-list. Thorough: bound by the proxy's own
domains, by rate, and by never retrying. Chose thorough: the scan is constant on a public machine.
100 lines
4.0 KiB
Go
100 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
|
|
"route-proxy/internal/routestate"
|
|
)
|
|
|
|
// held is a state as the bus holds it, read-only.
|
|
type held map[string]any
|
|
|
|
func (h held) Keys() ([]string, error) {
|
|
out := []string{}
|
|
for k := range h {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|
|
|
|
func (h held) Get(key string) (*stdio.StateEntry, error) {
|
|
v, ok := h[key]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
raw, err := json.Marshal(v)
|
|
return &stdio.StateEntry{Key: key, Value: raw}, err
|
|
}
|
|
|
|
// The web front's tools answer from the bus, in one call each (issue 774): every site with its target, certificate
|
|
// expiry and last answer; one site in full, or that it is not served and when it was asked for; and the names the
|
|
// proxy answered "no route for".
|
|
func TestTheWebFrontsToolsAnswerFromTheBus(t *testing.T) {
|
|
now := time.Date(2026, 10, 30, 21, 0, 0, 0, time.UTC)
|
|
expires := now.Add(30 * 24 * time.Hour)
|
|
routes := held{
|
|
"anchor.grafana.example": routestate.Route{Name: "grafana.example", Proxy: "anchor", Reach: routestate.ReachPublic,
|
|
Targets: []routestate.Target{{Module: "grafana", Node: "home-server", Target: "http://10.0.0.2:3000"}},
|
|
Certificate: routestate.Certificate{Authority: routestate.ReachPublic, Issuer: "Let's Encrypt R11", NotAfter: &expires, Outcome: routestate.Issued},
|
|
LastAnswer: &routestate.Answer{Status: 502, At: now, Error: "dial tcp 10.0.0.2:3000: connect: connection refused"}},
|
|
"anchor.drive.example": routestate.Route{Name: "drive.example", Proxy: "anchor", Reach: routestate.ReachPublic,
|
|
Targets: []routestate.Target{{Module: "nextcloud", Node: "home-server", Target: "http://10.0.0.2:8080"}},
|
|
Certificate: routestate.Certificate{Authority: routestate.ReachPublic},
|
|
LastAnswer: &routestate.Answer{Status: 200, At: now}},
|
|
}
|
|
unrouted := held{
|
|
"anchor.old.example": routestate.Unrouted{Name: "old.example", Proxy: "anchor", AskedAt: now.Add(-time.Hour), Times: 3},
|
|
"anchor.wiki.example": routestate.Unrouted{Name: "wiki.example", Proxy: "anchor", AskedAt: now, Times: 1},
|
|
"anchor._other": routestate.Other{Proxy: "anchor", Minute: now, LastMinute: 40, SinceStart: 900},
|
|
}
|
|
tools := map[string]stdio.Tool{}
|
|
for _, tool := range (front{routes: routes, unrouted: unrouted, now: func() time.Time { return now }}).tools() {
|
|
tools[tool.Name] = tool
|
|
}
|
|
|
|
for _, row := range []struct {
|
|
tool string
|
|
args map[string]any
|
|
want []string // what the answer says, in its JSON
|
|
}{
|
|
{RoutesTool, nil, []string{`"served":2`, `"unreachable":1`,
|
|
`"name":"grafana.example"`, `"reachable":"no"`, `"expires_in_days":30`, `/ → grafana@home-server at http://10.0.0.2:3000`,
|
|
`"name":"drive.example"`, `"reachable":"yes"`, `public: none seen yet`}},
|
|
{RoutesTool, map[string]any{"node": "home-server"}, []string{`"served":0`}},
|
|
{RouteTool, map[string]any{"name": "Grafana.Example"}, []string{`"routed":true`, `"reachable":"no"`,
|
|
`connection refused`, `"issuer":"Let's Encrypt R11"`}},
|
|
{RouteTool, map[string]any{"name": "wiki.example"}, []string{`"routed":false`,
|
|
`answered \"no route\" for it, last at 2026-10-30T21:00:00Z`}},
|
|
{RouteTool, map[string]any{"name": "nothing.example"}, []string{`"routed":false`, `none was asked for it`}},
|
|
{UnroutedTool, nil, []string{`"names":2`, `"unrouted":[{"name":"wiki.example"`,
|
|
`"other_refusals":[{"proxy":"anchor"`, `"last_minute":40`, `"since_start":900`}},
|
|
} {
|
|
t.Run(row.tool, func(t *testing.T) {
|
|
tool, ok := tools[row.tool]
|
|
if !ok {
|
|
t.Fatalf("%s is not served", row.tool)
|
|
}
|
|
if !strings.Contains(tool.Description, "route-proxy") {
|
|
t.Errorf("%s's description does not name the web front, so a search does not find it", row.tool)
|
|
}
|
|
answer, err := tool.Run(row.args)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := json.Marshal(answer)
|
|
for _, w := range row.want {
|
|
if !strings.Contains(string(raw), w) {
|
|
t.Errorf("%s(%v) does not say %s:\n%s", row.tool, row.args, w, raw)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|