arblant
firefox-vpn-client / core / internal / service / network.go
network.go116 lines · 3.7 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
package service

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/armanlife/firefox-vpn-client/core/internal/config"
	"github.com/armanlife/firefox-vpn-client/core/internal/controlplane"
	"github.com/armanlife/firefox-vpn-client/core/internal/logx"
	"github.com/armanlife/firefox-vpn-client/core/internal/resolver"
)

const apiRelayDialTimeout = 20 * time.Second

// resolverDialer dials through the configured DNS servers with a timeout.
type resolverDialer struct{ timeout time.Duration }

func (d resolverDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	ctx, cancel := context.WithTimeout(ctx, d.timeout)
	defer cancel()
	return resolver.Dial(ctx, network, address)
}

func (d resolverDialer) Dial(network, address string) (net.Conn, error) {
	return d.DialContext(context.Background(), network, address)
}

// configureDNS installs the configured DNS servers for every connection
// this program opens itself.
func configureDNS(cfg config.DNS) error {
	r, err := resolver.New(cfg.Servers, cfg.FallbackToSystem)
	if err != nil {
		return err
	}
	if r.System() {
		resolver.Set(nil)
		logx.Info("DNS: system resolver")
		return nil
	}
	resolver.Set(r)
	logx.Info("DNS: servers=%s", r)
	return nil
}

// configureAPIRelay sends the control-plane requests through the user's
// relay at raw (config.Network.APIRelay); empty sends them directly. Only
// the Firefox Accounts, Guardian, server-list, geo-file, and flag requests
// are affected; the VPN traffic keeps talking to Mozilla's proxy directly.
func configureAPIRelay(raw string) error {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		// Only a relay this function set is taken away: another transport
		// (a test's fixture) stays, so starting a daemon never sends a
		// test's requests to the real services.
		if apiRelayTransport != nil && controlplane.Transport() == apiRelayTransport {
			controlplane.SetTransport(nil)
		}
		apiRelayTransport = nil
		return nil
	}
	if err := config.ValidateRelay(raw); err != nil {
		return err
	}
	relay, _ := url.Parse(raw)
	dialer := resolverDialer{timeout: apiRelayDialTimeout}
	transport := &relayTransport{relay: relay, base: &http.Transport{
		DialContext:     dialer.DialContext,
		TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
	}}
	controlplane.SetTransport(transport)
	apiRelayTransport = transport
	logx.Info("control plane API requests sent through relay=%s", relay.Host)
	return nil
}

// apiRelayTransport is the transport configureAPIRelay installed; nil when
// no relay is set.
var apiRelayTransport http.RoundTripper

// relayTransport sends each request to the relay, with the target URL in
// the relay's url parameter (next to its own parameters, such as a key),
// and the method, headers, and body as they are; the relay forwards it and
// answers with the target's response. The response keeps the original
// request, so cookies and redirects resolve against the target. A relay
// that refuses (scripts/relay set X-Relay-Error) is an error that names
// the relay.
type relayTransport struct {
	relay *url.URL
	base  http.RoundTripper
}

func (t *relayTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	target := *t.relay
	query := target.Query()
	query.Set("url", req.URL.String())
	target.RawQuery = query.Encode()
	out := req.Clone(req.Context())
	out.URL = &target
	out.Host = ""
	resp, err := t.base.RoundTrip(out)
	if err != nil {
		return nil, fmt.Errorf("API relay %s: %w", t.relay.Host, err)
	}
	if reason := resp.Header.Get("X-Relay-Error"); reason != "" {
		resp.Body.Close()
		return nil, fmt.Errorf("API relay %s refused %s: %s", t.relay.Host, req.URL.Host, reason)
	}
	resp.Request = req
	return resp, nil
}