The filter blocks everything passing through the machine and then allows the machine's own containers back by naming the address ranges they sit on — two ranges fixed in the control plane and the rest typed after a flip had already cut a workstation off. A range describes one machine and goes stale in silence. Read the links carrying a default route instead, from /proc rather than by asking a program, and report them on every apply. A machine with no route off itself reports nothing, and the mesh composes no filter for it rather than writing a rule around a link with no name. novox/hq ADR 0140. The control plane does not read this yet.
148 lines
4.7 KiB
Go
148 lines
4.7 KiB
Go
// Package outward reads which of this machine's links face outside it (novox/hq ADR 0140).
|
|
//
|
|
// The filter the mesh derives constrains traffic arriving from outside the machine and says nothing
|
|
// about traffic that did not. To write that rule the mesh has to know which links "outside" arrives
|
|
// on, and that is a thing only the machine can say — so it says it, once per report, the way it
|
|
// already reports the kind of firewall it found and the tunnel it carried.
|
|
//
|
|
// **It replaces a list of addresses.** The filter used to allow the machine's own containers back
|
|
// through by naming the address ranges they sit on: two ranges fixed in the control plane's source
|
|
// and the rest typed by an operator. A range describes one machine and goes stale silently
|
|
// (novox/hq 04-ISSUES/137 and /141). A link that carries the default route is a fact the machine
|
|
// reads afresh every time, and it does not change when a module is added or removed.
|
|
//
|
|
// It reads the kernel's routing tables directly rather than asking a program. A module naming a
|
|
// program the machine does not have is how the mesh already reported success while doing nothing
|
|
// (novox/hq 04-ISSUES/136), and every machine has /proc.
|
|
package outward
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// ProcNet is where the kernel publishes its routing tables. A parameter so a test can hold a
|
|
// routing table without one.
|
|
const ProcNet = "/proc/net"
|
|
|
|
// Links are the interfaces carrying a default route, for both address families, sorted and without
|
|
// repeats.
|
|
//
|
|
// A machine may have more than one: a laptop with a cable and a radio has two, and both face
|
|
// outside. A machine with none — no route off itself — returns nothing, and the mesh refuses to
|
|
// compose a filter for it rather than writing a rule around a link with no name, which would be a
|
|
// rule set that does not load and a machine filtering nothing while its unit reports success.
|
|
func Links(procNet string) ([]string, error) {
|
|
if procNet == "" {
|
|
procNet = ProcNet
|
|
}
|
|
seen := map[string]bool{}
|
|
|
|
four, err := defaultsV4(filepath.Join(procNet, "route"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
six, err := defaultsV6(filepath.Join(procNet, "ipv6_route"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, name := range append(four, six...) {
|
|
if name != "" && name != "lo" {
|
|
seen[name] = true
|
|
}
|
|
}
|
|
|
|
out := make([]string, 0, len(seen))
|
|
for name := range seen {
|
|
out = append(out, name)
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|
|
|
|
// defaultsV4 reads /proc/net/route, whose columns are
|
|
//
|
|
// Iface Destination Gateway Flags RefCnt Use Metric Mask ...
|
|
//
|
|
// with addresses in hexadecimal. A default route is destination zero with mask zero — the mask
|
|
// matters, because a route to the zero address with a real mask is not a default route.
|
|
func defaultsV4(path string) ([]string, error) {
|
|
lines, err := rows(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []string
|
|
for _, fields := range lines {
|
|
if len(fields) < 8 {
|
|
continue
|
|
}
|
|
if isZeroHex(fields[1]) && isZeroHex(fields[7]) {
|
|
out = append(out, fields[0])
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// defaultsV6 reads /proc/net/ipv6_route, whose columns are
|
|
//
|
|
// dest destprefix src srcprefix nexthop metric refcnt use flags iface
|
|
//
|
|
// A default route is the zero destination with a zero prefix length.
|
|
func defaultsV6(path string) ([]string, error) {
|
|
lines, err := rows(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []string
|
|
for _, fields := range lines {
|
|
if len(fields) < 10 {
|
|
continue
|
|
}
|
|
if isZeroHex(fields[0]) && isZeroHex(fields[1]) {
|
|
out = append(out, fields[9])
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// rows reads a routing table into fields per line, skipping a header and blank lines. A table that
|
|
// is not there is not an error: a machine without the second address family has no file for it,
|
|
// and that is not a machine that cannot be filtered.
|
|
func rows(path string) ([][]string, error) {
|
|
file, err := os.Open(path)
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot read the routing table at %s: %w", path, err)
|
|
}
|
|
defer file.Close()
|
|
|
|
var out [][]string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "Iface") {
|
|
continue
|
|
}
|
|
out = append(out, strings.Fields(line))
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("cannot read the routing table at %s: %w", path, err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// isZeroHex is whether a hexadecimal field is all zeroes, whatever its width — the v4 table writes
|
|
// eight digits and the v6 table thirty-two, and a prefix length is two.
|
|
func isZeroHex(field string) bool {
|
|
if field == "" {
|
|
return false
|
|
}
|
|
return strings.Trim(strings.ToLower(field), "0") == ""
|
|
}
|