Report what an adopted node holds, its firewall and what is reachable, and speak unasked when that changes (hq ADR 0100)
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
// Package reachable reads what can be reached on this machine now: every listening socket, and
|
||||
// every container port the runtime publishes (novox/hq ADR 0100).
|
||||
//
|
||||
// It is what converging an adopted node previews — each port, whether a module declares it or it
|
||||
// will close — and what a converged genesis counts before refusing a machine in use. It reads; it
|
||||
// never decides what is the mesh's.
|
||||
package reachable
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-host/internal/link"
|
||||
"github.com/novox/mesh-host/internal/system"
|
||||
)
|
||||
|
||||
// Runner executes a command.
|
||||
type Runner = system.Runner
|
||||
|
||||
// Reach is one thing reachable on this machine, in the words the report carries.
|
||||
type Reach = link.Reach
|
||||
|
||||
// Collect reads the machine's listening sockets and the runtime's published ports. A published
|
||||
// port is reported once, as published, rather than again as the runtime's proxy listening for it.
|
||||
func Collect(ctx context.Context, run Runner) ([]Reach, error) {
|
||||
out, err := run(ctx, "ss", "-Hltunp")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading this machine's listening sockets: %w", err)
|
||||
}
|
||||
sockets := Sockets(out)
|
||||
|
||||
var published []Reach
|
||||
if ps, err := run(ctx, "docker", "ps", "--format", "{{.Names}}\t{{.Ports}}"); err == nil {
|
||||
published = Published(ps)
|
||||
}
|
||||
return Merge(sockets, published), nil
|
||||
}
|
||||
|
||||
var process = regexp.MustCompile(`users:\(\("([^"]+)"`)
|
||||
|
||||
// Sockets parses `ss -Hltunp`: each line a netid, a state, two queues, the local address and
|
||||
// port, the peer, and the process when ss may name it.
|
||||
func Sockets(out string) []Reach {
|
||||
var reached []Reach
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 {
|
||||
continue
|
||||
}
|
||||
protocol := fields[0]
|
||||
if protocol != "tcp" && protocol != "udp" {
|
||||
continue
|
||||
}
|
||||
address, port, ok := splitLocal(fields[4])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
r := Reach{Protocol: protocol, Address: address, Port: port}
|
||||
if m := process.FindStringSubmatch(line); m != nil {
|
||||
r.By = m[1]
|
||||
}
|
||||
reached = append(reached, r)
|
||||
}
|
||||
return reached
|
||||
}
|
||||
|
||||
// splitLocal reads "127.0.0.1:53", "[::]:22", "*:22" and "[fe80::1]%veth0:123".
|
||||
func splitLocal(local string) (string, int, bool) {
|
||||
i := strings.LastIndex(local, ":")
|
||||
if i < 0 {
|
||||
return "", 0, false
|
||||
}
|
||||
port, err := strconv.Atoi(local[i+1:])
|
||||
if err != nil {
|
||||
return "", 0, false
|
||||
}
|
||||
address := local[:i]
|
||||
if at := strings.Index(address, "%"); at >= 0 {
|
||||
address = address[:at]
|
||||
}
|
||||
address = strings.TrimSuffix(strings.TrimPrefix(address, "["), "]")
|
||||
if address == "*" {
|
||||
address = "0.0.0.0"
|
||||
}
|
||||
return address, port, true
|
||||
}
|
||||
|
||||
// Published parses `docker ps --format '{{.Names}}\t{{.Ports}}'`. Only what is published on the
|
||||
// machine counts; a port a container exposes and nothing publishes is not reachable from outside it.
|
||||
func Published(out string) []Reach {
|
||||
var reached []Reach
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
name, ports, ok := strings.Cut(strings.TrimSpace(line), "\t")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, mapping := range strings.Split(ports, ",") {
|
||||
reached = append(reached, mappingOf(name, strings.TrimSpace(mapping))...)
|
||||
}
|
||||
}
|
||||
return reached
|
||||
}
|
||||
|
||||
// mappingOf reads "0.0.0.0:9000-9001->9000-9001/tcp" into one reach per port.
|
||||
func mappingOf(name, mapping string) []Reach {
|
||||
outer, inner, ok := strings.Cut(mapping, "->")
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
inner, protocol, ok := strings.Cut(inner, "/")
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
i := strings.LastIndex(outer, ":")
|
||||
if i < 0 {
|
||||
return nil
|
||||
}
|
||||
address := strings.TrimSuffix(strings.TrimPrefix(outer[:i], "["), "]")
|
||||
from, to, ok := portRange(outer[i+1:])
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
cfrom, _, ok := portRange(inner)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var reached []Reach
|
||||
for p := from; p <= to; p++ {
|
||||
reached = append(reached, Reach{Protocol: protocol, Address: address, Port: p, By: name,
|
||||
Published: true, ContainerPort: cfrom + (p - from)})
|
||||
}
|
||||
return reached
|
||||
}
|
||||
|
||||
func portRange(s string) (int, int, bool) {
|
||||
a, b, isRange := strings.Cut(s, "-")
|
||||
from, err := strconv.Atoi(a)
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
if !isRange {
|
||||
return from, from, true
|
||||
}
|
||||
to, err := strconv.Atoi(b)
|
||||
if err != nil || to < from {
|
||||
return 0, 0, false
|
||||
}
|
||||
return from, to, true
|
||||
}
|
||||
|
||||
// Merge puts the published ports beside the sockets, dropping the runtime proxy's own socket for a
|
||||
// port that is reported as published already, and sorts the whole by port.
|
||||
func Merge(sockets, published []Reach) []Reach {
|
||||
key := func(r Reach) string { return r.Protocol + " " + r.Address + " " + strconv.Itoa(r.Port) }
|
||||
isPublished := map[string]bool{}
|
||||
for _, p := range published {
|
||||
isPublished[key(p)] = true
|
||||
}
|
||||
var out []Reach
|
||||
for _, s := range sockets {
|
||||
if s.By == "docker-proxy" && isPublished[key(s)] {
|
||||
continue
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
out = append(out, published...)
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if out[i].Port != out[j].Port {
|
||||
return out[i].Port < out[j].Port
|
||||
}
|
||||
if out[i].Protocol != out[j].Protocol {
|
||||
return out[i].Protocol < out[j].Protocol
|
||||
}
|
||||
return out[i].Address < out[j].Address
|
||||
})
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user