Add TLS + certificate pinning for the peer link
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).
This commit is contained in:
1 parent
e1f7e6f529
commit
e9fe10f0f7
12 files changed
+1198
-465
No files matched your search
@@ -0,0 +1,56 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user