140 lines
6.4 KiB
Go
140 lines
6.4 KiB
Go
// netcheck's Go tools bundle (novox/hq ADR 0188, ADR 0193): a process the node's runtime launches
|
|
// and speaks MCP over stdio to, through the Go SDK. It serves the two checks that are the machine's
|
|
// own sockets and resolver — a TCP connect, a DNS lookup (of the machine's resolver, or of one named),
|
|
// what listens here, and the private network's peers (hq issue 522) — and nothing that changes anything.
|
|
// Beside them it watches the tunnel and says on the bus when a peer's endpoint moves or its handshake goes
|
|
// stale or fresh again.
|
|
// The module's HTTP check is its TypeScript bundle; the runtime serves both under one module.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
func main() {
|
|
go watchTunnel(func(event string, body any) error { return stdio.Emit(event, body) })
|
|
// An empty name serves as the module the runtime names (MESH_SERVED_MODULE): netcheck.
|
|
if err := stdio.Serve("", tools()); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func tools() []stdio.Tool {
|
|
return []stdio.Tool{
|
|
{
|
|
Name: "netcheck_tunnel",
|
|
Description: "The private network's peers as this machine sees them (what wg show reads): each peer's machine where " +
|
|
"the mesh's names say it, its allowed ips, the endpoint (address:port) it was last heard from, its last handshake " +
|
|
"and that handshake's age, whether it is stale (older than three minutes, or never), and the bytes received and " +
|
|
"sent. Never a key. On the hub it says what address and port every machine reaches it from.",
|
|
Run: func(map[string]any) (any, error) {
|
|
cx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
return readTunnel(cx)
|
|
},
|
|
},
|
|
{
|
|
Name: "netcheck_listening",
|
|
Description: "What listens on this machine: every TCP socket in the listen state and every bound UDP socket, " +
|
|
"from the kernel's own tables (what ss -lunt reads), with the program holding it where this tool may see it. " +
|
|
"port narrows it to one port, e.g. 53 to see on which addresses the resolver answers.",
|
|
Input: map[string]any{
|
|
"port": map[string]any{"type": "integer", "description": "only this port (default every port)"},
|
|
},
|
|
Run: func(args map[string]any) (any, error) {
|
|
port := 0
|
|
if _, given := args["port"]; given {
|
|
p, err := whole(args, "port", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if p < 1 || p > 65535 {
|
|
return nil, fmt.Errorf("port %d is not a port (1-65535)", p)
|
|
}
|
|
port = p
|
|
}
|
|
return Listening("/proc", port), nil
|
|
},
|
|
},
|
|
{
|
|
Name: "netcheck_tcp",
|
|
Description: "Check whether a TCP port is reachable from this machine: opens one connection " +
|
|
"and closes it at once, sending nothing. Answers reachable, elapsed_ms and the error when not.",
|
|
Input: map[string]any{
|
|
"host": map[string]any{"type": "string", "description": "host name or IP address"},
|
|
"port": map[string]any{"type": "integer", "description": "TCP port, 1-65535"},
|
|
"timeout_ms": map[string]any{"type": "integer", "description": "give up after this long (default 3000, at most 30000)"},
|
|
},
|
|
Run: func(args map[string]any) (any, error) {
|
|
host, err := text(args, "host")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
port, err := whole(args, "port", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
timeout, err := timeoutOf(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return CheckTCP(host, port, timeout)
|
|
},
|
|
},
|
|
{
|
|
Name: "netcheck_request",
|
|
Description: "Make one HTTP request from this machine and answer what comes back: the status line, every response " +
|
|
"header and the body as text (base64 when it is not UTF-8), cut at 1 MiB with body_cut saying so. method is GET, " +
|
|
"HEAD or POST; headers and a body (at most 256 KiB) are sent as given. A redirect is reported (status and redirect), " +
|
|
"never followed. Only http and https; never this machine's own loopback, unspecified or link-local addresses (refused on the address dialled, so names and rebinding are caught too), and no proxy; TLS is verified against this machine's trust store. For what a service " +
|
|
"answers, not only whether it is up (that is netcheck_http).",
|
|
Input: map[string]any{
|
|
"url": map[string]any{"type": "string", "description": "http:// or https:// URL"},
|
|
"method": map[string]any{"type": "string", "enum": []string{"GET", "HEAD", "POST"}, "description": "default GET"},
|
|
"headers": map[string]any{"type": "object", "additionalProperties": map[string]any{"type": "string"}, "description": "request headers, name to value (optional)"},
|
|
"body": map[string]any{"type": "string", "description": "request body, at most 262144 bytes (optional)"},
|
|
"timeout_ms": map[string]any{"type": "integer", "description": "give up after this long (default 10000, at most 30000)"},
|
|
},
|
|
Run: runRequest,
|
|
},
|
|
{
|
|
Name: "netcheck_dns",
|
|
Description: "Look a name up with this machine's resolver (its /etc/resolv.conf and /etc/hosts), or — with server — ask " +
|
|
"that one resolver alone over UDP (or TCP), answering its response code (NOERROR, NXDOMAIN, REFUSED…). " +
|
|
"type is A, AAAA, CNAME, TXT or MX; answers the records found, or the error.",
|
|
Input: map[string]any{
|
|
"name": map[string]any{"type": "string", "description": "the name to look up"},
|
|
"type": map[string]any{"type": "string", "enum": []string{"A", "AAAA", "CNAME", "TXT", "MX"}, "description": "record type (default A)"},
|
|
"server": map[string]any{"type": "string", "description": "ask this resolver alone: an address, with :port or without (53), e.g. 10.10.0.1"},
|
|
"tcp": map[string]any{"type": "boolean", "description": "ask the server over TCP rather than UDP (default false)"},
|
|
"timeout_ms": map[string]any{"type": "integer", "description": "give up after this long (default 3000, at most 30000)"},
|
|
},
|
|
Run: func(args map[string]any) (any, error) {
|
|
name, err := text(args, "name")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
kind, _ := args["type"].(string)
|
|
timeout, err := timeoutOf(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
server, _ := args["server"].(string)
|
|
overTCP, _ := args["tcp"].(bool)
|
|
return AskDNS(name, kind, strings.TrimSpace(server), overTCP, timeout)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func say(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "[netcheck] "+format+"\n", args...)
|
|
}
|