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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user