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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
package proxy
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/armanlife/firefox-vpn-client/core/internal/egress"
"github.com/armanlife/firefox-vpn-client/core/internal/logx"
"github.com/armanlife/firefox-vpn-client/core/internal/resolver"
"github.com/armanlife/firefox-vpn-client/core/internal/routing"
)
// ErrBlockedByRule is a connection refused by a routing rule.
var ErrBlockedByRule = errors.New("refused by a routing rule")
// ErrThisMachine is a direct connection from another device to this machine
// itself, which the proxy refuses whatever the rules say (refuseThisMachine).
var ErrThisMachine = fmt.Errorf("%w: the proxy does not connect to this machine itself", ErrBlockedByRule)
// directDialTimeout bounds a direct connection.
const directDialTimeout = 15 * time.Second
// RouteCounts counts connections by route since the proxy started.
type RouteCounts struct {
Proxy int64 `json:"proxy"`
Direct int64 `json:"direct"`
Blocked int64 `json:"blocked"`
}
// RoutedOpener opens each tunnel through Mozilla's proxy, directly through
// the local network, or not at all, as its router decides. Without a
// router everything goes through the proxy. The router can be replaced
// while connections are open.
type RoutedOpener struct {
upstream tunnelOpener
router atomic.Pointer[routing.Router]
// dial connects directly for clients on this machine, dialRemote for
// clients on other devices; tests replace them.
dial, dialRemote func(ctx context.Context, network, address string) (net.Conn, error)
// lookup resolves names for the router's IP rules (routing
// resolve_domains) through Mozilla; tests replace it.
lookup routing.LookupFunc
// exchange answers the DNS a client sends through the proxy, which
// Mozilla does not carry (DNSPort), with DNS over HTTPS through
// Mozilla; tests replace it.
exchange ExchangeFunc
dnsNoted atomic.Bool
proxied, direct, blocked atomic.Int64
meter *trafficMeter
}
// directDial opens the connections routed directly with the local
// network's own DNS, not dns.servers: they go to the local country's
// sites, whose names its DNS answers best (with the nearest servers), and a
// filtering DNS only hijacks foreign names. While the TUN mode runs, both
// the connection and the DNS go outside the TUN adapter (egress).
// remoteDirectDial does the same for clients on other devices, but never
// connects to this machine itself (refuseThisMachine).
func directDial(ctx context.Context, network, address string) (net.Conn, error) {
return egress.ClientDialer(0).DialContext(ctx, network, address)
}
func remoteDirectDial(ctx context.Context, network, address string) (net.Conn, error) {
d := egress.ClientDialer(0)
d.Control = func(network, address string, c syscall.RawConn) error {
if err := refuseThisMachine(network, address, c); err != nil {
return err
}
return egress.Control(network, address, c)
}
return d.DialContext(ctx, network, address)
}
// refuseThisMachine keeps clients on other devices, when the proxy is
// shared, off this machine's own services (the dashboard among them) and
// off link-local addresses (a cloud server's metadata service,
// 169.254.169.254): a direct route never connects to a loopback,
// unspecified, or link-local address, whatever name led there. It runs on
// the resolved address. A client on this machine reaches those anyway.
func refuseThisMachine(_, address string, _ syscall.RawConn) error {
host, _, err := net.SplitHostPort(address)
if err != nil {
return err
}
if ip, err := netip.ParseAddr(host); err == nil {
if ip = ip.Unmap(); ip.IsLoopback() || ip.IsUnspecified() || ip.IsLinkLocalUnicast() {
return ErrThisMachine
}
}
return nil
}
// remoteClient reports whether a client key (the client's IP, see
// clientAffinityKey) is another device: not loopback. The core's own calls
// carry no key.
func remoteClient(clientKey string) bool {
if clientKey == "" {
return false
}
ip, err := netip.ParseAddr(clientKey)
return err != nil || !ip.Unmap().IsLoopback()
}
// NewRoutedOpener routes the tunnels of upstream; Close stops its speed
// sampling.
func NewRoutedOpener(upstream tunnelOpener) *RoutedOpener {
o := &RoutedOpener{upstream: upstream, dial: directDial, dialRemote: remoteDirectDial, meter: newTrafficMeter()}
// Names are looked up with DNS over HTTPS inside tunnels through
// Mozilla: the local network's DNS may be filtered and answer a
// blocked name with its block page's address (10.10.34.35 in Iran),
// which IP rules such as geoip:private would then send directly.
// These tunnels are not counted as client connections.
names := resolver.NewThrough(func(_ context.Context, _, address string) (net.Conn, error) {
return upstream.OpenTunnel(address)
})
o.lookup = func(ctx context.Context, host string) ([]netip.Addr, error) {
result, err := names.Lookup(ctx, host)
return result.Addrs, err
}
o.exchange = names.Exchange
return o
}
// Lookup resolves host the way the router's IP rules see it: through
// Mozilla.
func (o *RoutedOpener) Lookup(ctx context.Context, host string) ([]netip.Addr, error) {
return o.lookup(ctx, host)
}
// Close stops measuring the speed.
func (o *RoutedOpener) Close() { o.meter.close() }
// Traffic returns the bytes and speed so far.
func (o *RoutedOpener) Traffic() Traffic { return o.meter.traffic() }
// SetRouter installs r; nil sends everything through the proxy.
func (o *RoutedOpener) SetRouter(r *routing.Router) { o.router.Store(r) }
// Counts returns the connections by route so far.
func (o *RoutedOpener) Counts() RouteCounts {
return RouteCounts{Proxy: o.proxied.Load(), Direct: o.direct.Load(), Blocked: o.blocked.Load()}
}
func (o *RoutedOpener) OpenTunnel(authority string) (net.Conn, error) {
return o.OpenTunnelForClient(authority, "")
}
// throughMozilla is authority as it can go through Mozilla, which connects
// to web ports only: git over SSH to GitHub (github.com:22) goes to
// ssh.github.com:443, GitHub's SSH server on the HTTPS port, with the same
// protocol and host keys.
func throughMozilla(authority string) string {
if strings.EqualFold(authority, githubSSH) {
logx.Debug("GitHub's SSH through port 443 target=%s", logx.Target(authority))
return "ssh.github.com:443"
}
return authority
}
const githubSSH = "github.com:22"
// Carried reports whether a connection to host:port can go through
// Mozilla's proxy: one of its ports, or GitHub's SSH (throughMozilla).
func Carried(host, port string) bool {
return PortAllowed(port) || strings.EqualFold(net.JoinHostPort(host, port), githubSSH)
}
// OpenTunnelForClient keeps the client affinity of the upstream pool.
func (o *RoutedOpener) OpenTunnelForClient(authority, clientKey string) (net.Conn, error) {
if r := o.router.Load(); r != nil {
host, _, err := net.SplitHostPort(authority)
if err == nil {
switch r.Decide(context.Background(), host, o.lookup) {
case routing.Direct:
o.direct.Add(1)
logx.Debug("route direct target=%s", logx.Target(authority))
ctx, cancel := context.WithTimeout(context.Background(), directDialTimeout)
defer cancel()
dial := o.dial
if remoteClient(clientKey) && o.dialRemote != nil {
dial = o.dialRemote
}
conn, err := dial(ctx, "tcp", authority)
if errors.Is(err, ErrThisMachine) {
o.direct.Add(-1)
o.blocked.Add(1)
logx.Debug("route blocked: this machine itself target=%s", logx.Target(authority))
}
if err != nil {
return nil, err
}
return countConn(conn, &o.meter.directUp, &o.meter.directDown), nil
case routing.Block:
o.blocked.Add(1)
logx.Debug("route blocked target=%s", logx.Target(authority))
return nil, ErrBlockedByRule
}
}
}
o.proxied.Add(1)
if _, port, err := net.SplitHostPort(authority); err == nil && port == DNSPort && o.exchange != nil {
if o.dnsNoted.CompareAndSwap(false, true) {
logx.Info("DNS a client sends through the proxy (port 53, which Mozilla does not connect to) is answered here, with DNS over HTTPS through Mozilla")
}
logx.Debug("DNS through the proxy answered here target=%s", logx.Target(authority))
client, server := net.Pipe()
go serveDNS(server, o.exchange)
return countConn(client, &o.meter.up, &o.meter.down), nil
}
authority = throughMozilla(authority)
var conn net.Conn
var err error
if affinity, ok := o.upstream.(clientAffinityTunnelOpener); ok && clientKey != "" {
conn, err = affinity.OpenTunnelForClient(authority, clientKey)
} else {
conn, err = o.upstream.OpenTunnel(authority)
}
if err != nil {
return nil, err
}
return countConn(conn, &o.meter.up, &o.meter.down), nil
}