The beacon this replaces was a separate service wildcard-bound to 0.0.0.0:443 - it silently occupied port 443 on the reserved measurement addresses, voiding the IPv4 interception proof for as long as it ran, and it accepted a port report from anyone who could reach it. So this lives where the repo's own post-mortem said it belongs: POST on the control plane authenticated by the device credential, GET on the admin UI behind the existing apiAdmin helper. No new listener, no new port, no wildcard. Entries expire after 24h (ECHOLOT_ADB_ENDPOINT_RETENTION_H) on both write and read - a LAN address is a breadcrumb for driving a test device, not measurement data worth keeping. Also records the BLE peer-comparison design: the case for it is that BLE is out-of-band, which is what makes client isolation measurable at all - silence over IP cannot distinguish an isolating AP from an absent peer, and a peer confirming out-of-band that it was listening turns that silence into proof. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
4.1 KiB
Go
138 lines
4.1 KiB
Go
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package control
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"echo-lot.app/server/internal/store"
|
|
)
|
|
|
|
// devtoolsFixture is a server with one enrolled device, and that device's credential.
|
|
func devtoolsFixture(t *testing.T) (*Server, string) {
|
|
t.Helper()
|
|
st, err := store.Open(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st.SetADBEndpointRetention(24 * time.Hour)
|
|
tok, err := st.NewEnrollToken(time.Hour, "test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dev, err := st.Redeem(tok, "tablet")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &Server{Store: st}, dev.Credential
|
|
}
|
|
|
|
func postEndpoint(t *testing.T, s *Server, cred, body string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest("POST", "/v1/devtools/adb-endpoint", strings.NewReader(body))
|
|
if cred != "" {
|
|
req.Header.Set("Authorization", "Bearer "+cred)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
|
|
// The receiver this replaces took a port report from anyone who could reach it. This one does not.
|
|
func TestADBEndpointNeedsADeviceCredential(t *testing.T) {
|
|
s, _ := devtoolsFixture(t)
|
|
rec := postEndpoint(t, s, "", `{"host":"10.13.102.128","port":45305}`)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("no credential: code=%d, want 401", rec.Code)
|
|
}
|
|
rec = postEndpoint(t, s, "not-a-credential", `{"host":"10.13.102.128","port":45305}`)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("wrong credential: code=%d, want 401", rec.Code)
|
|
}
|
|
if got := s.Store.ADBEndpoints(); len(got) != 0 {
|
|
t.Fatalf("an unauthenticated report was stored: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestADBEndpointRejectsImplausibleHostAndPort(t *testing.T) {
|
|
s, cred := devtoolsFixture(t)
|
|
for _, body := range []string{
|
|
`{"host":"","port":45305}`,
|
|
`{"host":"10.13.102.128 && rm -rf /","port":45305}`,
|
|
`{"host":"not a host","port":45305}`,
|
|
`{"host":"-bad.example","port":45305}`,
|
|
`{"host":"10.13.102.128","port":0}`,
|
|
`{"host":"10.13.102.128","port":65536}`,
|
|
`{"host":"10.13.102.128","port":-1}`,
|
|
`not json at all`,
|
|
} {
|
|
rec := postEndpoint(t, s, cred, body)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("%s: code=%d, want 400 (%s)", body, rec.Code, strings.TrimSpace(rec.Body.String()))
|
|
}
|
|
}
|
|
if got := s.Store.ADBEndpoints(); len(got) != 0 {
|
|
t.Fatalf("a rejected report was stored anyway: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestADBEndpointStoresTheObservedSourceAndTheCallersDeviceID(t *testing.T) {
|
|
s, cred := devtoolsFixture(t)
|
|
rec := postEndpoint(t, s, cred,
|
|
`{"host":"10.13.102.128","port":45305,"device_name":"TB330FU","note":"wireless debugging"}`)
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("code=%d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
got := s.Store.ADBEndpoints()
|
|
if len(got) != 1 {
|
|
t.Fatalf("got %d rows, want 1", len(got))
|
|
}
|
|
e := got[0]
|
|
if e.Host != "10.13.102.128" || e.Port != 45305 || e.DeviceName != "TB330FU" {
|
|
t.Fatalf("report not stored as sent: %+v", e)
|
|
}
|
|
// The device id comes from the credential and the source IP from the connection, so neither is
|
|
// something the body can claim.
|
|
if e.Device == "" {
|
|
t.Fatal("the submitting device was not recorded")
|
|
}
|
|
if e.SourceIP != "192.0.2.1" { // httptest's RemoteAddr
|
|
t.Fatalf("source IP = %q, want the observed remote address", e.SourceIP)
|
|
}
|
|
if e.ReportedAt.IsZero() {
|
|
t.Fatal("no server-side timestamp was recorded")
|
|
}
|
|
}
|
|
|
|
func TestPlausibleHost(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
host string
|
|
want bool
|
|
}{
|
|
{"10.13.102.128", true},
|
|
{"192.168.1.1", true},
|
|
{"fe80::1", true},
|
|
{"[2001:db8::1]", true},
|
|
{"tablet.lan", true},
|
|
{"tablet", true},
|
|
{"a-b.example.net.", true},
|
|
{"", false},
|
|
{"not a host", false},
|
|
{"10.0.0.1:5555", false}, // the port is its own field; a host must not smuggle one
|
|
{"-lead.example", false},
|
|
{"trail-.example", false},
|
|
{"a..b", false},
|
|
{"http://10.0.0.1", false},
|
|
{strings.Repeat("x", 254), false},
|
|
} {
|
|
if got := plausibleHost(tc.host); got != tc.want {
|
|
t.Errorf("plausibleHost(%q) = %v, want %v", tc.host, got, tc.want)
|
|
}
|
|
}
|
|
}
|