a8fcb404be
internal/netscan/ discovers hosts on the network Maven is configured to look at:
a TCP-connect scan (net.DialTimeout, no raw sockets, no privileges) plus a read
of the kernel's ARP cache. Wired as a read-only query source, "network", so
"какие устройства в сети?" is answered by a scan instead of by whatever old note
happens to be nearest.
Scanning is a read, but an unbounded scanner on a home LAN is noisy and easy to
point somewhere it should not go, so the package is built around four bounds:
- Scan takes NO target argument. The range comes from the config block and
from nowhere else, so there is no exported way to scan an arbitrary prefix
and nothing an utterance, the router, or a scanned host says can retarget
it. That is asserted directly: the test watches every address handed to the
dialer and fails if one falls outside the configured prefix. The ARP cache —
the one input the network itself populates — is filtered to the configured
range for the same reason.
- Every configured CIDR must be private (RFC1918 / CGNAT / link-local) and no
larger than 1024 addresses. 8.8.8.0/24, 0.0.0.0/0 and 10.0.0.0/8 are refused
at config load, not after the packets have left.
- Rate-limited to a configured connections-per-second across the whole scan,
so it looks like background traffic rather than a portscan.
- Bounded in total by MaxHosts, a per-connection timeout, a 20s turn budget
and the context; a canceled scan stops dialing immediately.
Off unless configured: dark without "enabled": true, and applyDefaults
normalises a disabled block to nil. deploy/mavend.json carries it disabled.
BLUETOOTH IS NOT SHIPPED, AND IS BLOCKED, NOT SKIPPED. The plan's other half
(internal/bluetooth/, RSSI presence probes) needs a bluez stack that is not
here: bluetoothctl and hcitool are not installed, bluetoothd is not installed,
the bluetooth unit is inactive, and org.bluez is not on the system bus. hci0
exists as a kernel device and nothing can talk to it. The docker deploy is
further away still — it would need host networking, the D-Bus system socket
passed in, and CAP_NET_ADMIN. Writing an exec wrapper around a binary that does
not exist, against an output format nothing here can produce, would be a guess
dressed as a feature. It needs a decision about privileging the container before
any of it is worth writing.
Vikunja #257
112 lines
3.4 KiB
Go
112 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/kami/maven/internal/config"
|
|
)
|
|
|
|
func TestWireNetScanOffUnlessEnabled(t *testing.T) {
|
|
for name, cfg := range map[string]*config.Config{
|
|
"no block": {},
|
|
"written but dark": {NetScan: &config.NetScanConfig{
|
|
Subnets: []string{"192.168.1.0/24"},
|
|
}},
|
|
"enabled but nothing to scan": {NetScan: &config.NetScanConfig{Enabled: true}},
|
|
"enabled but public": {NetScan: &config.NetScanConfig{
|
|
Subnets: []string{"8.8.8.0/24"}, Enabled: true,
|
|
}},
|
|
"enabled but far too wide": {NetScan: &config.NetScanConfig{
|
|
Subnets: []string{"10.0.0.0/8"}, Enabled: true,
|
|
}},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if w := wireNetScan(cfg); w != nil {
|
|
t.Fatal("the scanner must not wire for this config")
|
|
}
|
|
})
|
|
}
|
|
|
|
var w *netWiring
|
|
if _, ok := w.scanSummary(context.Background()); ok {
|
|
t.Fatal("a nil wiring must not claim a query")
|
|
}
|
|
|
|
ok := wireNetScan(&config.Config{NetScan: &config.NetScanConfig{
|
|
Subnets: []string{"192.168.1.0/24"}, Enabled: true,
|
|
}})
|
|
if ok == nil {
|
|
t.Fatal("a valid enabled block should wire")
|
|
}
|
|
}
|
|
|
|
// A loopback /32 with nothing listening on the scanned port: the summary must
|
|
// come back honest rather than inventing a host. This also exercises the real
|
|
// dialer end to end without touching anything outside this box.
|
|
func TestScanSummaryOnAnEmptyRange(t *testing.T) {
|
|
w := wireNetScan(&config.Config{NetScan: &config.NetScanConfig{
|
|
// Port 1 on loopback: nothing listens and the connection is refused
|
|
// immediately, so the scan is fast and touches only this machine.
|
|
Subnets: []string{"127.0.0.1/32"}, Ports: []int{1}, Rate: 1000, Enabled: true,
|
|
}})
|
|
if w == nil {
|
|
t.Fatal("wireNetScan returned nil")
|
|
}
|
|
out, claimed := w.scanSummary(context.Background())
|
|
if !claimed {
|
|
t.Fatal("the summary did not claim the turn")
|
|
}
|
|
if out == "" {
|
|
t.Fatal("empty summary")
|
|
}
|
|
// Persona: feminine self-reference, informal address, no pet names.
|
|
low := strings.ToLower(out)
|
|
for _, bad := range []string{"нашёл", "не смог ", "вы ", "ваш", "милый", "дорогой"} {
|
|
if strings.Contains(low, bad) {
|
|
t.Errorf("persona violation %q in %q", bad, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHostWordAgreesWithTheCount(t *testing.T) {
|
|
for n, want := range map[int]string{
|
|
1: "устройство", 2: "устройства", 4: "устройства", 5: "устройств",
|
|
11: "устройств", 12: "устройств", 21: "устройство", 22: "устройства",
|
|
25: "устройств", 111: "устройств", 101: "устройство", 0: "устройств",
|
|
} {
|
|
if got := hostWord(n); got != want {
|
|
t.Errorf("hostWord(%d) = %q, want %q", n, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsNetworkQuery(t *testing.T) {
|
|
yes := []string{
|
|
"какие устройства в сети?",
|
|
"кто в сети?",
|
|
"просканируй сеть",
|
|
"покажи устройства в локальной сети",
|
|
"сколько машин в сети",
|
|
}
|
|
no := []string{
|
|
"",
|
|
"интернет не работает",
|
|
"сеть какая-то медленная",
|
|
"я в сети инстаграма",
|
|
"что включено дома?",
|
|
"напомни оплатить интернет",
|
|
}
|
|
for _, u := range yes {
|
|
if !isNetworkQuery(u) {
|
|
t.Errorf("isNetworkQuery(%q) = false, want true", u)
|
|
}
|
|
}
|
|
for _, u := range no {
|
|
if isNetworkQuery(u) {
|
|
t.Errorf("isNetworkQuery(%q) = true, want false", u)
|
|
}
|
|
}
|
|
}
|