route-proxy: serve an internal name to the private network only

The proxy answers public and internal names on the same listeners, so
serving an internal-only route made it reachable from the internet by
anyone sending its name. Requests and handshakes for an internal name
from outside the mesh range, loopback or a container bridge are now
answered as an unrouted name, and the 404 no longer lists them
(novox/hq issue 191, ADR 0138 insight of 2026-10-02).
This commit is contained in:
2026-10-02 01:10:13 +02:00
parent 9acb5f1292
commit 0fcea460da
2 changed files with 354 additions and 16 deletions
+183
View File
@@ -0,0 +1,183 @@
package main
import (
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
// behind is a workload the proxy can send to, and a table routing one public name and one
// internal-only name to it, with the private network set to inside.
func behind(t *testing.T, inside string) *table {
t.Helper()
workload := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "the workload")
}))
t.Cleanup(workload.Close)
at, _ := url.Parse(workload.URL)
host, port, _ := net.SplitHostPort(at.Host)
routes, public, err := routesFrom(write(t, fmt.Sprintf(`{"given":[
{"from":"app","node":"anchor","at":%q,
"values":{"name":"app.example","internal-name":"app.anchor.internal","port":%s}},
{"from":"admin","node":"anchor","at":%q,
"values":{"internal-name":"admin.anchor.internal","port":%s}}
]}`, host, port, host, port)))
if err != nil {
t.Fatal(err)
}
held := newTable()
held.inside, err = sourcesFrom(inside)
if err != nil {
t.Fatal(err)
}
held.set(routes, public)
return held
}
// askFrom is what the proxy answers a request for host coming from remote.
func askFrom(held *table, host, remote string) (int, string) {
r := httptest.NewRequest(http.MethodGet, "http://"+host+"/", nil)
r.RemoteAddr = remote
w := httptest.NewRecorder()
handler(held).ServeHTTP(w, r)
return w.Code, w.Body.String()
}
// **An internal-only name is served to the private network and to nobody else** (novox/hq ADR
// 0138, issue 191). The proxy answers public names on the same listeners, so without this a name
// being internal kept nobody out: a request from the internet only had to carry it.
func TestAnInternalOnlyNameIsServedOnlyInsideThePrivateNetwork(t *testing.T) {
held := behind(t, "10.10.0.0/24")
if code, body := askFrom(held, "admin.anchor.internal", "10.10.0.7:51000"); code != http.StatusOK ||
body != "the workload" {
t.Errorf("a request from the private network was not served: %d %q", code, body)
}
if code, body := askFrom(held, "admin.anchor.internal", "127.0.0.1:51000"); code != http.StatusOK {
t.Errorf("a request from the machine itself was not served: %d %q", code, body)
}
code, body := askFrom(held, "admin.anchor.internal", "203.0.113.9:51000")
if code != http.StatusNotFound {
t.Fatalf("a request from outside the private network reached an internal-only name: %d %q",
code, body)
}
// Answered as a name never routed, and the list of what is served does not name it either —
// otherwise the refusal would tell an outsider exactly what to ask for from inside.
if strings.Contains(strings.SplitN(body, "\n", 2)[1], "admin.anchor.internal") {
t.Errorf("the refusal names the internal-only route to an outsider: %q", body)
}
if !strings.Contains(body, "app.example") {
t.Errorf("the refusal stopped listing the public names: %q", body)
}
}
// The internal name of a route that also has a public one is internal too: served inside, and to
// an outsider only under the public name. Nothing is lost — the outsider has the public name — and a
// name stays one thing whichever route it came from.
func TestAnInternalAliasOfAPublicRouteIsServedInsideOnly(t *testing.T) {
held := behind(t, "10.10.0.0/24")
if code, body := askFrom(held, "app.anchor.internal", "10.10.0.7:51000"); code != http.StatusOK {
t.Errorf("the internal alias stopped answering the private network: %d %q", code, body)
}
if code, _ := askFrom(held, "app.anchor.internal", "203.0.113.9:51000"); code != http.StatusNotFound {
t.Errorf("the internal alias was served to an outsider: %d", code)
}
if code, _ := askFrom(held, "app.example", "203.0.113.9:51000"); code != http.StatusOK {
t.Errorf("the public name was refused to an outsider: %d", code)
}
}
// A container on this machine reaches the proxy from its bridge's range, and is the machine itself
// (novox/hq ADR 0144): inside, though it is neither loopback nor the mesh.
func TestAContainerOnThisMachineIsInside(t *testing.T) {
held := behind(t, "10.10.0.0/24")
_, bridge, _ := net.ParseCIDR("172.18.0.1/16")
bridge.IP = net.ParseIP("172.18.0.1")
_, other, _ := net.ParseCIDR("192.168.1.20/24")
other.IP = net.ParseIP("192.168.1.20")
held.setBridges(bridgesFrom(map[string][]net.Addr{
"br-0123456789ab": {bridge},
"eth0": {other},
}))
if code, _ := askFrom(held, "admin.anchor.internal", "172.18.0.5:51000"); code != http.StatusOK {
t.Errorf("a container on this machine was refused: %d", code)
}
// The machine's own network is not a container bridge: a neighbour there is not the machine.
if code, _ := askFrom(held, "admin.anchor.internal", "192.168.1.30:51000"); code != http.StatusNotFound {
t.Errorf("a neighbour on the machine's network was served an internal-only name: %d", code)
}
}
// With no private network said, only the machine itself is inside — refused to everyone else,
// never served to everyone.
func TestWithNoPrivateNetworkSaidAnInternalOnlyNameIsServedToTheMachineAlone(t *testing.T) {
held := behind(t, "")
if code, _ := askFrom(held, "admin.anchor.internal", "10.10.0.7:51000"); code != http.StatusNotFound {
t.Errorf("an internal-only name was served with no private network said: %d", code)
}
if code, _ := askFrom(held, "admin.anchor.internal", "[::1]:51000"); code != http.StatusOK {
t.Errorf("an internal-only name was refused to the machine itself: %d", code)
}
}
type from struct {
net.Conn
remote net.Addr
}
func (c from) RemoteAddr() net.Addr { return c.remote }
// The handshake refuses an internal-only name to an outsider too: the certificate would name it,
// and serving it would answer the question the routing refuses to.
func TestTheHandshakeRefusesAnInternalOnlyNameToAnOutsider(t *testing.T) {
held := behind(t, "10.10.0.0/24")
served := &tls.Certificate{}
pick := certificateFor(held, func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return served, nil }, nil)
hello := func(name, remote string) *tls.ClientHelloInfo {
addr, _ := net.ResolveTCPAddr("tcp", remote)
return &tls.ClientHelloInfo{ServerName: name, Conn: from{remote: addr}}
}
if _, err := pick(hello("admin.anchor.internal", "203.0.113.9:443")); err == nil {
t.Error("an outsider was handed a certificate for an internal-only name")
}
if got, err := pick(hello("admin.anchor.internal", "10.10.0.7:443")); err != nil || got != served {
t.Errorf("a client on the private network was refused: %v", err)
}
if got, err := pick(hello("app.example", "203.0.113.9:443")); err != nil || got != served {
t.Errorf("a public name was refused to an outsider: %v", err)
}
}
// A range the proxy cannot read stops it, rather than serving internal names to fewer machines
// than the mesh said, or to a typo.
func TestAPrivateNetworkThatDoesNotParseIsRefused(t *testing.T) {
if _, err := sourcesFrom("10.10.0.0/24, not-a-range"); err == nil {
t.Error("a range that does not parse was accepted")
}
inside, err := sourcesFrom("10.10.0.0/24 fd00::/8")
if err != nil {
t.Fatal(err)
}
for remote, want := range map[string]bool{
"10.10.0.200:1": true,
"[::ffff:10.10.0.3]:1": true,
"[fd00::1]:1": true,
"10.11.0.1:1": false,
"192.168.1.10:1": false,
"not-an-address": false,
} {
if inside.holds(remote) != want {
t.Errorf("%s inside the private network: got %v, want %v", remote, !want, want)
}
}
}