A computed module says what its machine opens, so a hub can be filtered
The machine that most needed a firewall was the one that could not have one. A hub is dialled by every node at other sites and needs its port open; a machine that is not a hub dials out and needs nothing open. They are the same module, and `listens` in a manifest is one answer for every machine that runs it — so the machine a static answer gets wrong is the one facing the public internet. A generator can now say what it opens, in a second interface rather than a method on every generator: most have nothing to say here, and requiring an empty method of each would be a cost paid everywhere for one caller. The port is the one in the endpoint, which is where the interface takes its ListenPort from. One source, so a rule set cannot open a port the interface is not on. Open to everywhere and deliberately: a node at another site is not on the private network until this port lets it on, so restricting it to the mesh would be a rule that can never be satisfied by the thing it exists for. And a generator that cannot say is refused rather than read as silence. Closing a port on the evidence of a failure to look is how a machine is severed by a fault somewhere else — and the machine it would sever is the hub, whose only route to being fixed is the network it just closed.
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
package overlay
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/novox/mesh-control/internal/catalogue"
|
||||
)
|
||||
|
||||
// The private network as a module rather than as code beside the module system.
|
||||
//
|
||||
@@ -195,3 +201,46 @@ func DomainManifest() map[string]any {
|
||||
// reaches the broker, which is what being in the mesh is — so it answers "not part of this" for
|
||||
// everyone instead of refusing for want of a hub.
|
||||
func Empty() *Generator { return &Generator{graph: Graph{}} }
|
||||
|
||||
// Listens is the port this node accepts the private network on, which only a hub has.
|
||||
//
|
||||
// **A fact about this machine's place in the mesh, not about the module.** Every machine on the
|
||||
// network runs the same module; a hub is dialled by every node at other sites and needs its port
|
||||
// open, and a machine that is not a hub dials out and needs nothing open at all. A static field in
|
||||
// a manifest is one answer for every machine that runs it, so it cannot say this — and the machine
|
||||
// it would get wrong is the one facing the public internet, which is the machine that most needs
|
||||
// filtering.
|
||||
//
|
||||
// The port is the one in the endpoint, which is also where the interface takes its ListenPort
|
||||
// from. One source, so a rule set cannot open a port the interface is not on.
|
||||
func (g *Generator) Listens(node string) ([]catalogue.Listening, error) {
|
||||
for _, n := range g.nodes {
|
||||
if n.Name != node {
|
||||
continue
|
||||
}
|
||||
if !n.Reachable() {
|
||||
// It dials out and nothing dials it. Opening a port here would be opening one on a
|
||||
// machine nothing connects to, which is not harmless — it is a rule with no source
|
||||
// that somebody later has to work out the reason for.
|
||||
return nil, nil
|
||||
}
|
||||
port := portOf(n.Endpoint)
|
||||
if port == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"%s is reachable at %q and no port can be read from it, so what it must accept "+
|
||||
"the private network on is unknown", node, n.Endpoint)
|
||||
}
|
||||
number, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s is reachable at %q, and %q is not a port", node, n.Endpoint, port)
|
||||
}
|
||||
return []catalogue.Listening{{
|
||||
Port: number, Protocol: "udp", From: catalogue.FromEverywhere,
|
||||
// From everywhere, and deliberately: a node at another site is not on the private
|
||||
// network until this port lets it on, so restricting this to the mesh would be a
|
||||
// rule that can never be satisfied by the thing it exists for.
|
||||
Why: "the private network — a node at another site has no other way in",
|
||||
}}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package overlay
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-control/internal/catalogue"
|
||||
)
|
||||
|
||||
func networkOf(t *testing.T, nodes []Node) *Generator {
|
||||
t.Helper()
|
||||
made, err := From(nodes, "10.42.0.0/16", "/var/lib/mesh-host/overlay.key")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return made
|
||||
}
|
||||
|
||||
// A hub is dialled by every node at other sites, and needs its port open. A machine that is not a
|
||||
// hub dials out and needs nothing open at all.
|
||||
//
|
||||
// They are the same module, which is why a static field in a manifest cannot say it — and the
|
||||
// machine it gets wrong is the one facing the public internet, which is the machine that most
|
||||
// needs filtering.
|
||||
func TestOnlyAMachineThatCanBeDialledOpensTheOverlaysPort(t *testing.T) {
|
||||
network := networkOf(t, []Node{
|
||||
{Name: "anchor", Key: "a", Endpoint: "198.51.100.10:51820", Site: "one", Hub: true},
|
||||
{Name: "laptop", Key: "b", Site: "one"},
|
||||
})
|
||||
|
||||
opens, err := network.Listens("anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(opens) != 1 {
|
||||
t.Fatalf("the machine every other one dials opens %d ports", len(opens))
|
||||
}
|
||||
if opens[0].Port != 51820 || opens[0].At() != "udp" {
|
||||
t.Fatalf("the port is not the one the interface listens on: %+v", opens[0])
|
||||
}
|
||||
// From everywhere, and deliberately: a node at another site is not on the private network
|
||||
// until this port lets it on, so restricting it to the mesh would be a rule that can never be
|
||||
// satisfied by the thing it exists for.
|
||||
if opens[0].From != catalogue.FromEverywhere {
|
||||
t.Fatalf("the way onto the private network is restricted to the private network: %+v", opens[0])
|
||||
}
|
||||
if opens[0].Why == "" {
|
||||
t.Fatal("a port is opened and nothing says why, which is what makes a rule set unreadable")
|
||||
}
|
||||
|
||||
// And the machine nothing dials opens nothing. Not harmless to get wrong: a rule with no
|
||||
// reason is one somebody later has to work out the reason for.
|
||||
quiet, err := network.Listens("laptop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(quiet) != 0 {
|
||||
t.Fatalf("a machine nothing dials opened %d port(s)", len(quiet))
|
||||
}
|
||||
}
|
||||
|
||||
// The port comes from the endpoint, which is where the interface takes its ListenPort from. One
|
||||
// source, so a rule set cannot open a port the interface is not on.
|
||||
func TestTheOpenedPortIsTheOneTheInterfaceListensOn(t *testing.T) {
|
||||
network := networkOf(t, []Node{
|
||||
{Name: "anchor", Key: "a", Endpoint: "198.51.100.10:60000", Site: "one", Hub: true},
|
||||
})
|
||||
opens, err := network.Listens("anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(opens) != 1 || opens[0].Port != 60000 {
|
||||
t.Fatalf("the rule set would open a port the interface is not on: %+v", opens)
|
||||
}
|
||||
|
||||
written, err := Declaration(Node{Name: "anchor", Key: "a", Address: "10.42.0.1",
|
||||
Endpoint: "198.51.100.10:60000", Hub: true}, nil, "/k")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(written), "ListenPort = 60000") {
|
||||
t.Fatalf("the interface and the rule set disagree about the port:\n%s", written)
|
||||
}
|
||||
}
|
||||
|
||||
// An endpoint with no port is refused rather than treated as a machine that opens nothing.
|
||||
//
|
||||
// A generator that cannot say what a machine opens is not one that says it opens nothing, and
|
||||
// closing a port on the evidence of a failure to look is how a machine is severed by a fault
|
||||
// somewhere else entirely.
|
||||
func TestAnEndpointWithNoPortIsRefusedRatherThanTakenAsSilence(t *testing.T) {
|
||||
network := networkOf(t, []Node{
|
||||
{Name: "anchor", Key: "a", Endpoint: "198.51.100.10", Site: "one", Hub: true},
|
||||
})
|
||||
if _, err := network.Listens("anchor"); err == nil {
|
||||
t.Fatal("a machine whose port could not be read was treated as opening nothing")
|
||||
}
|
||||
}
|
||||
|
||||
// A machine the network has never heard of opens nothing, and that is an answer rather than an
|
||||
// error: a node assigned the module before it is placed is in exactly that state.
|
||||
func TestAMachineNotOnTheNetworkOpensNothing(t *testing.T) {
|
||||
network := networkOf(t, []Node{
|
||||
{Name: "anchor", Key: "a", Endpoint: "198.51.100.10:51820", Site: "one", Hub: true},
|
||||
})
|
||||
opens, err := network.Listens("a-stranger")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(opens) != 0 {
|
||||
t.Fatalf("a machine not on the network opened %d port(s)", len(opens))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user