The co-op link was authenticated (HMAC token, never sent over the wire) but not encrypted. The hub now generates a self-signed cert on first run; the peer pins its exact fingerprint (no CA involved — there isn't one for a Tailscale/LAN address), delivered via a single invite-code paste that also carries the token, replacing today's separate IP+token copy. The peer link moves to its own TLS-only port (peer_listen, 47823) so the plain overlay/panel port (47822, OBS-facing) never needs to be exposed alongside it — today, opening the overlay port to a remote partner also exposes /deaths and the panel to anyone. Mandatory pinning, no insecure fallback: a half-configured peer (some but not all of hub/token/fingerprint, or a broken invite) fails loudly at startup rather than connecting unpinned. An unconfigured peer still runs fine as a local-only overlay, same as before. New: tlscert.go (cert generation/persistence), pin.go (fingerprint pinning), invite.go (invite-code encode/decode, host auto-detection), each with tests. main.go/config.go/duo.go/ws.go carry the wiring for this — the dual listener, new config keys, and the TLS-aware WebSocket dial — and were rewritten in English in the process, per the project's new English-only code convention (see CLAUDE.md).
57 lines
1.6 KiB
Go
57 lines
1.6 KiB
Go
package main
|
|
|
|
import "testing"
|
|
|
|
func validTestFingerprint() string {
|
|
certDER, _, _ := generateHubCert()
|
|
return certFingerprint(certDER)
|
|
}
|
|
|
|
func TestInviteRoundtrip(t *testing.T) {
|
|
fp := validTestFingerprint()
|
|
want := inviteCode{Host: "100.101.102.103", Port: "47823", Fingerprint: fp, Token: "sekrit"}
|
|
|
|
got, err := decodeInvite(encodeInvite(want))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != want {
|
|
t.Fatalf("roundtrip mismatch: got %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDecodeInviteRejectsMalformed(t *testing.T) {
|
|
fp := validTestFingerprint()
|
|
|
|
cases := map[string]string{
|
|
"not base64 at all": "!!! not base64 !!!",
|
|
"base64 but not json": "aGVsbG8gd29ybGQ", // "hello world"
|
|
"missing token": encodeInvite(inviteCode{Host: "h", Port: "1", Fingerprint: fp}),
|
|
"missing host": encodeInvite(inviteCode{Port: "1", Fingerprint: fp, Token: "t"}),
|
|
"bad fingerprint": encodeInvite(inviteCode{Host: "h", Port: "1", Fingerprint: "not-a-fingerprint", Token: "t"}),
|
|
}
|
|
for name, code := range cases {
|
|
if _, err := decodeInvite(code); err == nil {
|
|
t.Errorf("%s: expected an error, got none", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPickBestHost(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
addrs []string
|
|
want string
|
|
}{
|
|
{"tailscale preferred over LAN", []string{"192.168.1.5", "100.101.102.103"}, "100.101.102.103"},
|
|
{"LAN only", []string{"192.168.1.5"}, "192.168.1.5"},
|
|
{"nothing usable", []string{"8.8.8.8"}, ""},
|
|
{"empty", nil, ""},
|
|
}
|
|
for _, c := range cases {
|
|
if got := pickBestHost(c.addrs); got != c.want {
|
|
t.Errorf("%s: pickBestHost(%v) = %q, want %q", c.name, c.addrs, got, c.want)
|
|
}
|
|
}
|
|
}
|