1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package service
import (
"context"
"encoding/json"
"errors"
"net"
"net/http"
"net/netip"
"strings"
"testing"
"golang.org/x/net/dns/dnsmessage"
"github.com/armanlife/firefox-vpn-client/core/internal/resolver"
)
// startTestDNS answers every A query with ip.
func startTestDNS(t *testing.T, ip [4]byte) string {
t.Helper()
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { pc.Close() })
go func() {
buf := make([]byte, 1500)
for {
n, addr, err := pc.ReadFrom(buf)
if err != nil {
return
}
var msg dnsmessage.Message
if msg.Unpack(buf[:n]) != nil || len(msg.Questions) == 0 {
continue
}
reply := dnsmessage.Message{Header: dnsmessage.Header{ID: msg.Header.ID, Response: true}, Questions: msg.Questions}
if q := msg.Questions[0]; q.Type == dnsmessage.TypeA {
reply.Answers = []dnsmessage.Resource{{
Header: dnsmessage.ResourceHeader{Name: q.Name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 60},
Body: &dnsmessage.AResource{A: ip},
}}
}
packed, _ := reply.Pack()
_, _ = pc.WriteTo(packed, addr)
}
}()
return pc.LocalAddr().String()
}
func TestSettingsApplyDNS(t *testing.T) {
t.Cleanup(func() { resolver.Set(nil) })
d := newTestDaemon(t)
cfg := d.config()
cfg.DNS.Servers = []string{"https://1.1.1.1/dns-query", "tls://9.9.9.9"}
if _, err := d.updateSettings(cfg, nil); err != nil {
t.Fatal(err)
}
if got := resolver.Current().String(); !strings.Contains(got, "https://1.1.1.1/dns-query") || !strings.Contains(got, "tls://9.9.9.9:853") {
t.Fatalf("installed resolver = %s", got)
}
if view, _ := d.StatusView(); !strings.Contains(view.DNS, "1.1.1.1") {
t.Fatalf("status DNS = %q", view.DNS)
}
cfg.DNS.Servers = []string{"dns.google"}
if _, err := d.updateSettings(cfg, nil); err == nil {
t.Fatal("a host name without a scheme must be rejected")
}
cfg.DNS.Servers = []string{}
if _, err := d.updateSettings(cfg, nil); err != nil {
t.Fatal(err)
}
if !resolver.Current().System() {
t.Fatal("an empty list must restore the system resolver")
}
}
func TestDNSTestEndpointComparesWithSystem(t *testing.T) {
addr := startTestDNS(t, [4]byte{23, 235, 42, 9})
old := systemDNS
systemDNS = func(_ context.Context, host string) (resolver.Result, error) {
if host == "vpn.mozilla.org" {
return resolver.Result{}, errors.New("blocked")
}
return resolver.Result{Addrs: []netip.Addr{netip.MustParseAddr("10.10.34.35")}, Source: "system"}, nil
}
t.Cleanup(func() { systemDNS = old })
d := newTestDaemon(t)
rec := newDashboardClient(t, d).do(http.MethodPost, "/api/dns/test", map[string]any{"servers": []string{"udp://" + addr}})
if rec.Code != http.StatusOK {
t.Fatalf("got %d %s", rec.Code, rec.Body.String())
}
var out struct {
Results []dnsTestHost `json:"results"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatal(err)
}
if len(out.Results) != 4 {
t.Fatalf("expected 4 names, got %+v", out.Results)
}
for _, r := range out.Results {
if len(r.Addrs) != 1 || r.Addrs[0] != "23.235.42.9" || r.Error != "" {
t.Errorf("%s: configured answer %+v", r.Host, r)
}
switch r.Host {
case "vpn.mozilla.org":
if r.SystemError != "blocked" || r.Differs || r.Filtered {
t.Errorf("guardian system answer %+v", r)
}
default:
if !r.Differs || !r.Filtered {
t.Errorf("%s: a block-page system answer must be flagged: %+v", r.Host, r)
}
}
}
bad := newDashboardClient(t, d).do(http.MethodPost, "/api/dns/test", map[string]any{"servers": []string{"quic://1.1.1.1"}})
if bad.Code != http.StatusBadRequest {
t.Fatalf("an invalid server must be rejected, got %d", bad.Code)
}
}