package vpn import ( "encoding/json" "os" "path/filepath" "strings" "testing" "time" ) // Real output of `wg show all dump`, from the two machines here: gate, where devices dial in, // and hamtv, which dials out through Mullvad. Keys are the real public ones, which are public. const gateDump = "wg0\tPRIVATEKEYWOULDBEHERE=\t4VwqgzHyelmVmNY+EANRWQggSSxvYYguJQMvUty9Sns=\t51820\toff\n" + "wg0\tNeWxo9wfqgipBQLuP676WFcMlW5RDrOcanc9y8sT7kU=\t3KWsu0mdVb2l5B/j0qHE03yE9LXi3FJ1WAjWMuxaLAw=\t10.0.0.33:63573\t172.21.46.3/32\t1790565744\t134192164\t415977404\toff\n" + "wg0\twUnRD990c6aglU0TCREtmfuUTPzqSmHrd3qTORpRJBg=\t4Pd5u2t6U9EdHlFlneg+hMr0G2v+IvoE/EqGfFdDPI8=\t(none)\t172.21.46.2/32\t0\t0\t0\toff\n" const hamtvDump = "wg0\tPRIVATEKEYWOULDBEHERE=\tF/rgcHi+ZiaWPoSQ9iPcldIUbbMotE1hwSB/74Y9Qi4=\t39281\toff\n" + "wg0\tzqsfGglzJPY657WMRxf/S4omG7+ZkSDIpDq+ggbc9yo=\t(none)\t23.234.72.2:51820\t0.0.0.0/0,::/0\t1790571246\t18206675520\t2687837240\t25\n" func TestParseServerDump(t *testing.T) { ts := parseDump(gateDump) if len(ts) != 1 { t.Fatalf("got %d tunnels, want 1", len(ts)) } w := ts[0] if w.Name != "wg0" || w.ListenPort != 51820 { t.Errorf("interface = %s:%d, want wg0:51820", w.Name, w.ListenPort) } if w.PublicKey != "4VwqgzHyelmVmNY+EANRWQggSSxvYYguJQMvUty9Sns=" { t.Errorf("public key = %q; the third column is the public one", w.PublicKey) } // The private key is column two of the interface line and must not appear anywhere. if w.PublicKey == "PRIVATEKEYWOULDBEHERE=" || w.FwMark == "PRIVATEKEYWOULDBEHERE=" { t.Error("the private key was kept") } if len(w.Peers) != 2 { t.Fatalf("got %d peers, want 2", len(w.Peers)) } p := w.Peers[0] if p.PublicKey != "NeWxo9wfqgipBQLuP676WFcMlW5RDrOcanc9y8sT7kU=" || p.Endpoint != "10.0.0.33:63573" { t.Errorf("first peer = %+v", p) } if !p.PSK { t.Error("first peer has a pre-shared key and should say so") } if got := p.AllowedIPs; len(got) != 1 || got[0] != "172.21.46.3/32" { t.Errorf("allowed ips = %v", got) } if p.Rx != 134192164 || p.Tx != 415977404 { t.Errorf("counters = %d/%d", p.Rx, p.Tx) } if p.Handshake == nil || p.Handshake.Unix() != 1790565744 { t.Errorf("handshake = %v", p.Handshake) } // The second peer has never connected: no endpoint, no handshake, nothing moved. if q := w.Peers[1]; q.Endpoint != "" || q.Handshake != nil || q.Rx != 0 { t.Errorf("second peer = %+v, want one that has never been heard from", q) } if kind, exit := classify(w); kind != "server" || exit != "" { t.Errorf("classify = %q/%q, want server", kind, exit) } } func TestParseClientDump(t *testing.T) { ts := parseDump(hamtvDump) if len(ts) != 1 || len(ts[0].Peers) != 1 { t.Fatalf("got %d tunnels", len(ts)) } w := ts[0] p := w.Peers[0] if len(p.AllowedIPs) != 2 || p.AllowedIPs[0] != "0.0.0.0/0" { t.Errorf("allowed ips = %v, want everything", p.AllowedIPs) } if p.Keepalive != 25 { t.Errorf("keepalive = %d, want 25", p.Keepalive) } if p.PSK { t.Error("this peer has no pre-shared key") } // It routes everything and has an endpoint, so this machine goes out through it. kind, exit := classify(w) if kind != "client" || exit != "23.234.72.2:51820" { t.Errorf("classify = %q/%q, want client through the endpoint", kind, exit) } } // A tunnel that is up but has no peers at all is neither, and must not be guessed at. func TestClassifyUndecided(t *testing.T) { if kind, _ := classify(Tunnel{Name: "wg0", ListenPort: 51820}); kind != "" { t.Errorf("classify with no peers = %q, want no opinion", kind) } } // Peers are named by the comment above them in a wg-quick file. The public key's own base64 // padding is an '=' too, which is the thing to get wrong. func TestConfNames(t *testing.T) { dir := t.TempDir() conf := `[Interface] Address = 172.21.46.1/24 PrivateKey = SECRETSECRETSECRETSECRETSECRETSECRETSECRET= ListenPort = 51820 # phone [Peer] PublicKey = NeWxo9wfqgipBQLuP676WFcMlW5RDrOcanc9y8sT7kU= AllowedIPs = 172.21.46.3/32 # the laptop [Peer] PublicKey = wUnRD990c6aglU0TCREtmfuUTPzqSmHrd3qTORpRJBg= AllowedIPs = 172.21.46.2/32 ` if err := os.WriteFile(filepath.Join(dir, "wg0.conf"), []byte(conf), 0o600); err != nil { t.Fatal(err) } old := confDir confDir = dir defer func() { confDir = old }() names := confNames() if got := names["NeWxo9wfqgipBQLuP676WFcMlW5RDrOcanc9y8sT7kU="]; got != "phone" { t.Errorf("first peer named %q, want phone", got) } if got := names["wUnRD990c6aglU0TCREtmfuUTPzqSmHrd3qTORpRJBg="]; got != "the laptop" { t.Errorf("second peer named %q, want the laptop", got) } for k := range names { if k == "SECRETSECRETSECRETSECRETSECRETSECRETSECRET=" { t.Error("the private key was read as if it were a peer") } } if len(names) != 2 { t.Errorf("%d names, want 2: %v", len(names), names) } } func TestOnlineIsRecentHandshake(t *testing.T) { for _, tc := range []struct { name string hs time.Time want bool }{ {"just now", time.Now().Add(-30 * time.Second), true}, {"a while back", time.Now().Add(-10 * time.Minute), false}, {"never", time.Time{}, false}, } { got := !tc.hs.IsZero() && time.Since(tc.hs) < onlineWithin if got != tc.want { t.Errorf("%s: online = %v, want %v", tc.name, got, tc.want) } } } // A peer that has never handshaken must carry no time at all, not the zero one. omitempty // does nothing for a time.Time, so this is the difference between the page saying "never // connected" and saying the device was last seen in the year one. func TestNeverConnectedHasNoTimestamp(t *testing.T) { w := parseDump(gateDump)[0] b, err := json.Marshal(w.Peers[1]) if err != nil { t.Fatal(err) } if strings.Contains(string(b), "handshake") { t.Errorf("json = %s, want no handshake field at all", b) } if strings.Contains(string(b), "0001-01-01") { t.Errorf("json = %s, want no year-one timestamp", b) } // and the one that has connected still carries its time if b, _ := json.Marshal(w.Peers[0]); !strings.Contains(string(b), "handshake") { t.Errorf("json = %s, want the handshake of a peer that has one", b) } }