// A reverse proxy, in the form the mesh expects one. // // **A route is a grant** (novox/hq ADR 0007, 08-connectivity §3). A module that must be reachable // declares it requires `route` and contributes the name it wants; the proxy provides `route` and // receives every consumer that asked. It is the mirror of a database grant: there the consumer // supplies a name and receives credentials, here it supplies a target and receives a name. // // **It is an example, not part of the control plane.** The control plane decides and never touches // a machine. A real deployment runs whatever proxy it likes — the contract is the file, not this // program. What lives here is that contract, written as something that runs so it can be read // rather than described. // // What it is given, written by the host from an ordinary declaration: // // $ROUTES every consumer, the name it asked for, and where the mesh says that machine is // // It re-reads on change rather than being restarted, for the same reason the provisioner does: // a route arriving or leaving is an ordinary event and must not drop the connections of every // other workload. package main import ( "encoding/json" "fmt" "log" "net" "net/http" "net/http/httputil" "net/url" "os" "sort" "strings" "sync" "time" ) // what the mesh writes: the contributions file, one entry per consumer. type given struct { Given []contribution `json:"given"` } type contribution struct { From string `json:"from"` Node string `json:"node"` // At is where that machine is on the private network. The mesh knows it; a proxy that had to // build it from the node name would be a naming convention copied into every provider. At string `json:"at"` Values map[string]any `json:"values"` } // table is what the proxy is currently serving, replaced whole whenever the file changes. // // Replaced rather than merged: the file is the whole truth about who has a route, so merging // would keep serving a name whose module was unassigned — which is the stale-route fault // 08-connectivity lists as open, reintroduced one level down. type table struct { mu sync.RWMutex to map[string]*httputil.ReverseProxy targets map[string]string } func (t *table) set(routes map[string]string) { made := map[string]*httputil.ReverseProxy{} for name, target := range routes { where, err := url.Parse(target) if err != nil { log.Printf("route %s points at %q, which is not a URL: %v", name, target, err) continue } made[name] = httputil.NewSingleHostReverseProxy(where) } t.mu.Lock() t.to, t.targets = made, routes t.mu.Unlock() } func (t *table) find(host string) (*httputil.ReverseProxy, bool) { // The port is not part of the name. A request to app.example:8080 is for app.example. if h, _, err := net.SplitHostPort(host); err == nil { host = h } t.mu.RLock() defer t.mu.RUnlock() p, ok := t.to[strings.ToLower(host)] return p, ok } func (t *table) names() []string { t.mu.RLock() defer t.mu.RUnlock() out := make([]string, 0, len(t.targets)) for name := range t.targets { out = append(out, name) } sort.Strings(out) return out } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "mesh-route-proxy: "+err.Error()) os.Exit(1) } } func run() error { path := strings.TrimSpace(os.Getenv("ROUTES")) if path == "" { return fmt.Errorf("ROUTES is not set, so this proxy does not know what it is routing") } listen := strings.TrimSpace(os.Getenv("LISTEN")) if listen == "" { listen = ":80" } held := newTable() read := func() { routes, err := routesFrom(path) if err != nil { // Kept serving what it had. A file being rewritten is momentarily unreadable, and // dropping every route because one read landed mid-write would turn an ordinary // event into an outage. log.Printf("cannot read %s, keeping what is already served: %v", path, err) return } held.set(routes) log.Printf("serving %d route(s): %s", len(routes), strings.Join(held.names(), ", ")) } read() go func() { for range time.Tick(2 * time.Second) { read() } }() return http.ListenAndServe(listen, handler(held)) } // newTable is an empty routing table. func newTable() *table { return &table{to: map[string]*httputil.ReverseProxy{}, targets: map[string]string{}} } // handler is the proxy itself, separated so it can be driven by a test without a listener. func handler(held *table) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { proxy, known := held.find(r.Host) if !known { // **Named, not a bare 404.** A route that was withdrawn and a name that never existed // are different things, and a proxy that says only "not found" makes an operator go // and read the mesh to tell them apart. What it is serving is the answer to both. w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "no route for %q in this mesh.\nserving: %s\n", r.Host, strings.Join(held.names(), ", ")) return } proxy.ServeHTTP(w, r) }) } // routesFrom reads what the mesh wrote and turns it into name → target. func routesFrom(path string) (map[string]string, error) { raw, err := os.ReadFile(path) if err != nil { return nil, err } var said given if err := json.Unmarshal(raw, &said); err != nil { return nil, err } out := map[string]string{} for _, c := range said.Given { name, _ := c.Values["name"].(string) if name == "" { log.Printf("%s on %s asked for a route and named nothing; skipped", c.From, c.Node) continue } port, ok := asPort(c.Values["port"]) if !ok { log.Printf("%s on %s asked for route %q and gave no usable port; skipped", c.From, c.Node, name) continue } // Where the mesh says that machine is. Empty means it is this one — a workload beside the // proxy is ordinary, and reaching it over loopback is both correct and the only thing // that works when there is no private network. at := c.At if at == "" { at = "127.0.0.1" } out[strings.ToLower(name)] = fmt.Sprintf("http://%s:%d", at, port) } return out, nil } // asPort accepts what JSON makes of a number, which is a float even when it was written 8080. func asPort(v any) (int, bool) { switch n := v.(type) { case float64: if n < 1 || n > 65535 { return 0, false } return int(n), true case int: if n < 1 || n > 65535 { return 0, false } return n, true } return 0, false }