Core packages: config validation, network renderers, root helper, auth, SQLite store, capture engine

Capture engine streams dumpcap pcapng into verbatim, hashed segments with
per-segment tshark summaries; display filters, PDML dissection with verified
byte ranges, export and import. Tests run in a Debian trixie container.
This commit is contained in:
Andy Dixon
2026-10-09 17:34:32 +01:00
commit 52da3c13f6
26 changed files with 4335 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
// Package helper defines the request protocol between the unprivileged core and
// the root pilens-helper. Every operation is fixed and typed; there is no generic
// command execution.
package helper
import (
"context"
"encoding/json"
"errors"
"net"
"time"
"pilens.dev/pilens/internal/config"
)
const DefaultSocket = "/run/pilens/helper.sock"
type Request struct {
Op string `json:"op"`
Network *config.Network `json:"network,omitempty"`
}
type Response struct {
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// Operation names.
const (
OpApplyNetwork = "apply_network"
OpStatus = "status"
OpStations = "stations"
OpLeases = "leases"
)
type ApplyResult struct {
Applied bool `json:"applied"`
RolledBack bool `json:"rolled_back"`
Steps []string `json:"steps"`
Error string `json:"error,omitempty"`
}
type Status struct {
TableLoaded bool `json:"table_loaded"`
APState string `json:"ap_state"` // NM GENERAL.STATE or "absent"
DnsmasqActive bool `json:"dnsmasq_active"`
IPForward bool `json:"ip_forward"`
Regdomain string `json:"regdomain"`
Configured bool `json:"configured"`
LastError string `json:"last_error,omitempty"`
}
type Station struct {
MAC string `json:"mac"`
SignalDBm *int `json:"signal_dbm,omitempty"`
RxBytes uint64 `json:"rx_bytes"`
TxBytes uint64 `json:"tx_bytes"`
ConnectedS int64 `json:"connected_s"`
InactiveMS int64 `json:"inactive_ms"`
}
type Lease struct {
Expiry int64 `json:"expiry"` // unix seconds; 0 = infinite
MAC string `json:"mac"`
IP string `json:"ip"`
Hostname string `json:"hostname"`
}
// Client talks to the helper socket.
type Client struct{ Socket string }
func (c Client) Call(ctx context.Context, req Request, out any) error {
d := net.Dialer{}
conn, err := d.DialContext(ctx, "unix", c.Socket)
if err != nil {
return errors.New("network helper unavailable: " + err.Error())
}
defer conn.Close()
dl, ok := ctx.Deadline()
if !ok {
dl = time.Now().Add(2 * time.Minute)
}
conn.SetDeadline(dl)
if err := json.NewEncoder(conn).Encode(req); err != nil {
return err
}
var resp Response
if err := json.NewDecoder(conn).Decode(&resp); err != nil {
return err
}
if out != nil && len(resp.Data) > 0 {
if err := json.Unmarshal(resp.Data, out); err != nil {
return err
}
}
if !resp.OK {
return errors.New(resp.Error)
}
return nil
}
+75
View File
@@ -0,0 +1,75 @@
package helper
import (
"bufio"
"strconv"
"strings"
)
// ParseStationDump parses `iw dev X station dump` output.
func ParseStationDump(s string) []Station {
var out []Station
var cur *Station
sc := bufio.NewScanner(strings.NewReader(s))
for sc.Scan() {
line := sc.Text()
if strings.HasPrefix(line, "Station ") {
f := strings.Fields(line)
if len(f) >= 2 {
out = append(out, Station{MAC: strings.ToLower(f[1])})
cur = &out[len(out)-1]
}
continue
}
if cur == nil {
continue
}
k, v, ok := strings.Cut(strings.TrimSpace(line), ":")
if !ok {
continue
}
f := strings.Fields(v)
if len(f) == 0 {
continue
}
n, err := strconv.ParseInt(f[0], 10, 64)
if err != nil {
continue
}
switch k {
case "inactive time":
cur.InactiveMS = n
case "rx bytes":
cur.RxBytes = uint64(n)
case "tx bytes":
cur.TxBytes = uint64(n)
case "signal":
v := int(n)
cur.SignalDBm = &v
case "connected time":
cur.ConnectedS = n
}
}
return out
}
// ParseLeases parses a dnsmasq lease file: "expiry mac ip hostname clientid".
func ParseLeases(s string) []Lease {
var out []Lease
for _, line := range strings.Split(s, "\n") {
f := strings.Fields(line)
if len(f) < 4 {
continue
}
exp, err := strconv.ParseInt(f[0], 10, 64)
if err != nil {
continue
}
h := f[3]
if h == "*" {
h = ""
}
out = append(out, Lease{Expiry: exp, MAC: strings.ToLower(f[1]), IP: f[2], Hostname: h})
}
return out
}
+25
View File
@@ -0,0 +1,25 @@
package helper
import "testing"
func TestParseStationDump(t *testing.T) {
s := ParseStationDump(`Station AA:bb:cc:dd:ee:ff (on wlan0)
inactive time: 1200 ms
rx bytes: 5000
tx bytes: 7000
signal: -47 [-47, -49] dBm
connected time: 33 seconds
Station 11:22:33:44:55:66 (on wlan0)
rx bytes: 1
`)
if len(s) != 2 || s[0].MAC != "aa:bb:cc:dd:ee:ff" || *s[0].SignalDBm != -47 || s[0].RxBytes != 5000 || s[0].ConnectedS != 33 || s[1].SignalDBm != nil {
t.Fatalf("%+v", s)
}
}
func TestParseLeases(t *testing.T) {
l := ParseLeases("1700000000 AA:BB:CC:DD:EE:FF 192.168.50.101 phone 01:aa\n1700000001 11:22:33:44:55:66 192.168.50.102 * *\ngarbage\n")
if len(l) != 2 || l[0].Hostname != "phone" || l[1].Hostname != "" || l[0].MAC != "aa:bb:cc:dd:ee:ff" {
t.Fatalf("%+v", l)
}
}