nats: a WebSocket listener for a page in a browser, from the overlay only (hq research 036)
mesh/merge-gate head e6d5ead50dfd merged into trunk 44e7002d3c84 — fail: builds nats, sent nowhere; merging this needs a person's bus upgrade; 1 wait(s) fo…
mesh/repo-check head e6d5ead50dfd merged into trunk 44e7002d3c84 — fail: head e6d5ead50dfd does not merge cleanly into trunk 44e7002d3c84: in conflict modu…
mesh/delivery stopped: the pull request closed unmerged

The bus has no way for a browser to reach it (research 036, gap G1). The server now also listens
over WebSocket on 4223: published from the mesh like 4222, so the machine's filter admits it from
the overlay and nothing else; no TLS, acceptable only because the overlay is WireGuard and a
browser cannot be made to trust the mesh's own certificate; compression on. The user a page
connects as is the read-only view the controller composes (mesh-controller, `bus view-credential`).

A test parses the declared configuration with the server itself, holds the opening, the published
port and the listener to one port, and connects a client over WebSocket. The bus module's policy is
`record`: this reaches the bus's machine only through the planned step a person starts
(`bus upgrade`), as every bus change does.
This commit is contained in:
jochen
2026-10-10 16:00:52 +02:00
parent 1fcf46fbfc
commit e6d5ead50d
3 changed files with 210 additions and 1 deletions
@@ -0,0 +1,190 @@
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"math/big"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
)
// The WebSocket listener the manifest declares (novox/hq research 036, gap G1), read by the server the
// image runs: the module's own configuration, as declared, parsed by nats-server itself, with its TLS
// paths and store pointed at this test's directories and a user list of its own beside it — and a
// client connected over WebSocket to it, through the port the manifest opens from the mesh and the
// container publishes.
func TestTheDeclaredConfigurationListensForWebSocketFromTheMesh(t *testing.T) {
m := manifest(t)
// The three places the listener is declared agree on one port: the server's own configuration, the
// opening from the mesh (the machine's filter admits it from the overlay only), and the container's
// published port.
var opening map[string]any
for _, l := range m["listens"].([]any) {
if entry := l.(map[string]any); entry["name"] == "bus-websocket" {
opening = entry
}
}
if opening == nil {
t.Fatal("the manifest opens no bus-websocket port")
}
if opening["from"] != "mesh" {
t.Fatalf("the WebSocket listener is open from %q; it is open from the mesh only, because it has no TLS", opening["from"])
}
port := int(opening["port"].(float64))
published := false
for _, r := range m["resources"].([]any) {
if res := r.(map[string]any); res["id"] == "server" {
for _, p := range res["ports"].([]any) {
published = published || p.(string) == itoa(port)+":"+itoa(port)
}
}
}
if !published {
t.Fatalf("the container does not publish %d:%d", port, port)
}
dir := t.TempDir()
conf := serverConfOf(t, m)
conf = strings.ReplaceAll(conf, "/tls/", selfSignedInto(t, dir)+"/")
conf = strings.ReplaceAll(conf, `store_dir: "/data"`, `store_dir: "`+filepath.Join(dir, "data")+`"`)
if err := os.WriteFile(filepath.Join(dir, "nats.conf"), []byte(conf), 0o600); err != nil {
t.Fatal(err)
}
// A user list of this test's own, in the place the mesh writes it.
users := "accounts { MESH { jetstream: enabled\n users = [ { user: \"view\", password: \"view\", permissions: {\n" +
" publish: { allow: [\"probe\"] }\n subscribe: { allow: [\"probe\", \"_INBOX.view.>\"] } } } ]\n} }\n"
if err := os.WriteFile(filepath.Join(dir, "accounts.conf"), []byte(users), 0o600); err != nil {
t.Fatal(err)
}
opts, err := server.ProcessConfigFile(filepath.Join(dir, "nats.conf"))
if err != nil {
t.Fatalf("the server refuses the module's own configuration: %v", err)
}
if opts.Websocket.Port != port {
t.Fatalf("the server's WebSocket listener is on %d and the opening from the mesh is %d", opts.Websocket.Port, port)
}
if !opts.Websocket.NoTLS {
t.Fatal("the WebSocket listener wants TLS: a browser cannot be made to trust the mesh's own certificate, and the overlay is the tunnel")
}
if !opts.Websocket.Compression {
t.Fatal("the WebSocket listener does not compress")
}
if opts.Websocket.Host != "0.0.0.0" {
t.Fatalf("inside the container the listener binds %q; it binds every interface, and the machine publishes it from the mesh", opts.Websocket.Host)
}
// Run it, on ports the system chooses, so this runs beside a live bus.
opts.Host, opts.Port = "127.0.0.1", -1
opts.HTTPHost, opts.HTTPPort = "127.0.0.1", 0
opts.Websocket.Host, opts.Websocket.Port = "127.0.0.1", -1
opts.NoLog, opts.NoSigs = true, true
s, err := server.NewServer(opts)
if err != nil {
t.Fatal(err)
}
go s.Start()
if !s.ReadyForConnections(10 * time.Second) {
t.Fatal("the server did not start")
}
defer s.Shutdown()
nc, err := nats.Connect(s.WebsocketURL(), nats.UserInfo("view", "view"), nats.CustomInboxPrefix("_INBOX.view"), nats.Compression(true))
if err != nil {
t.Fatalf("a WebSocket client could not connect to %s: %v", s.WebsocketURL(), err)
}
defer nc.Close()
if !strings.HasPrefix(nc.ConnectedUrl(), "ws://") {
t.Fatalf("connected to %s, not over WebSocket", nc.ConnectedUrl())
}
sub, err := nc.SubscribeSync("probe")
if err != nil {
t.Fatal(err)
}
if err := nc.Publish("probe", []byte("round trip")); err != nil {
t.Fatal(err)
}
if msg, err := sub.NextMsg(5 * time.Second); err != nil || string(msg.Data) != "round trip" {
t.Fatalf("no round trip over WebSocket: %v", err)
}
// And the client port beside it still requires TLS: a plain connection is refused.
if _, err := nats.Connect(s.ClientURL(), nats.UserInfo("view", "view"), nats.Timeout(2*time.Second)); err == nil {
t.Fatal("the client port accepted a connection without TLS")
}
}
func itoa(n int) string { return strconv.Itoa(n) }
// manifest is the nats module's own definition, beside this program.
func manifest(t *testing.T) map[string]any {
t.Helper()
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
return m
}
func serverConfOf(t *testing.T, m map[string]any) string {
t.Helper()
for _, r := range m["resources"].([]any) {
if res := r.(map[string]any); res["id"] == "server-conf" {
return res["content"].(string)
}
}
t.Fatal("the nats module declares no server configuration")
return ""
}
// selfSignedInto writes a throwaway certificate and key as tls.crt and tls.key into a directory, as the
// mesh mounts the broker's at /tls, and returns that directory.
func selfSignedInto(t *testing.T, dir string) string {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "test bus"},
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
raw, err := x509.MarshalECPrivateKey(key)
if err != nil {
t.Fatal(err)
}
tlsDir := filepath.Join(dir, "tls")
if err := os.MkdirAll(tlsDir, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tlsDir, "tls.crt"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tlsDir, "tls.key"), pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: raw}), 0o600); err != nil {
t.Fatal(err)
}
return tlsDir
}