arblant
firefox-vpn-client / core / internal / proxy / dns_test.go
dns_test.go160 lines · 4.6 KB
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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
package proxy

import (
	"context"
	"encoding/binary"
	"errors"
	"io"
	"net"
	"slices"
	"sync"
	"testing"
	"time"

	"golang.org/x/net/dns/dnsmessage"

	"github.com/armanlife/firefox-vpn-client/core/internal/routing"
)

func dnsQuery(t *testing.T, id uint16, host string, qtype dnsmessage.Type) []byte {
	t.Helper()
	name, err := dnsmessage.NewName(host + ".")
	if err != nil {
		t.Fatal(err)
	}
	msg, err := (&dnsmessage.Message{
		Header:    dnsmessage.Header{ID: id, RecursionDesired: true},
		Questions: []dnsmessage.Question{{Name: name, Type: qtype, Class: dnsmessage.ClassINET}},
	}).Pack()
	if err != nil {
		t.Fatal(err)
	}
	return msg
}

// askDNS sends one message over DNS over TCP and reads the answer.
func askDNS(t *testing.T, conn net.Conn, msg []byte) []byte {
	t.Helper()
	_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
	framed := binary.BigEndian.AppendUint16(nil, uint16(len(msg)))
	if _, err := conn.Write(append(framed, msg...)); err != nil {
		t.Fatal(err)
	}
	var size [2]byte
	if _, err := io.ReadFull(conn, size[:]); err != nil {
		t.Fatal(err)
	}
	answer := make([]byte, binary.BigEndian.Uint16(size[:]))
	if _, err := io.ReadFull(conn, answer); err != nil {
		t.Fatal(err)
	}
	return answer
}

// DNS sent through the proxy goes to DNS over HTTPS (exchange), never to
// Mozilla's port 53, which it refuses: queries of any type, several over
// one connection, get the DNS server's answers as they are.
func TestDNSThroughTheProxyIsAnsweredHere(t *testing.T) {
	t.Parallel()

	upstream := &recordingOpener{}
	routed := NewRoutedOpener(upstream)
	defer routed.Close()
	var mu sync.Mutex
	var asked [][]byte
	routed.exchange = func(_ context.Context, query []byte) ([]byte, error) {
		mu.Lock()
		asked = append(asked, query)
		mu.Unlock()
		return append([]byte("answer:"), query...), nil
	}

	conn, err := routed.OpenTunnelForClient("1.1.1.1:53", "192.168.1.20")
	if err != nil {
		t.Fatal(err)
	}
	defer conn.Close()
	for i, qtype := range []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeMX} {
		query := dnsQuery(t, uint16(100+i), "example.com", qtype)
		if got := askDNS(t, conn, query); string(got) != "answer:"+string(query) {
			t.Fatalf("answer %q", got)
		}
	}
	mu.Lock()
	n := len(asked)
	mu.Unlock()
	upstream.mu.Lock()
	targets := slices.Clone(upstream.targets)
	upstream.mu.Unlock()
	if n != 2 || slices.Contains(targets, "1.1.1.1:53") {
		t.Fatalf("queries asked %d, tunnels %v", n, targets)
	}
	if c := routed.Counts(); c.Proxy != 1 {
		t.Fatalf("counts %+v", c)
	}
}

// A query the DNS servers do not answer gets SERVFAIL for the same ID and
// question, so the client tries again or reports it at once; what is not
// a DNS query ends the connection.
func TestAFailedDNSQueryGetsServfail(t *testing.T) {
	t.Parallel()

	client, server := net.Pipe()
	defer client.Close()
	go serveDNS(server, func(context.Context, []byte) ([]byte, error) {
		return nil, errors.New("no DNS server answered")
	})
	answer := askDNS(t, client, dnsQuery(t, 7, "example.com", dnsmessage.TypeAAAA))
	var msg dnsmessage.Message
	if err := msg.Unpack(answer); err != nil {
		t.Fatal(err)
	}
	if !msg.Response || msg.ID != 7 || msg.RCode != dnsmessage.RCodeServerFailure ||
		len(msg.Questions) != 1 || msg.Questions[0].Type != dnsmessage.TypeAAAA {
		t.Fatalf("answer %+v", msg)
	}

	// An answer sent as a query is no query.
	notQuery, _ := (&dnsmessage.Message{Header: dnsmessage.Header{ID: 8, Response: true}}).Pack()
	_ = client.SetDeadline(time.Now().Add(5 * time.Second))
	if _, err := client.Write(append(binary.BigEndian.AppendUint16(nil, uint16(len(notQuery))), notQuery...)); err != nil {
		t.Fatal(err)
	}
	if _, err := client.Read(make([]byte, 1)); err == nil {
		t.Fatal("the connection stayed open after a message that is no query")
	}
}

// A routing rule that sends port 53 directly still does: only DNS that
// would go to Mozilla is answered here.
func TestDNSRoutedDirectlyStaysDirect(t *testing.T) {
	t.Parallel()

	routed := NewRoutedOpener(&recordingOpener{})
	defer routed.Close()
	var dialed []string
	routed.dial = func(_ context.Context, _, address string) (net.Conn, error) {
		dialed = append(dialed, address)
		a, b := net.Pipe()
		_ = b.Close()
		return a, nil
	}
	routed.exchange = func(context.Context, []byte) ([]byte, error) {
		t.Error("a direct DNS server was answered here")
		return nil, errors.New("unexpected")
	}
	router, err := routing.New(routing.Config{Direct: []string{"10.0.0.0/8"}})
	if err != nil {
		t.Fatal(err)
	}
	routed.SetRouter(router)
	conn, err := routed.OpenTunnel("10.1.2.3:53")
	if err != nil {
		t.Fatal(err)
	}
	_ = conn.Close()
	if len(dialed) != 1 || dialed[0] != "10.1.2.3:53" {
		t.Fatalf("direct dials %v", dialed)
	}
}