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
package proxy
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"io"
"net"
"slices"
"strings"
"syscall"
"github.com/armanlife/firefox-vpn-client/core/internal/accounts"
"github.com/armanlife/firefox-vpn-client/core/internal/fxa"
"github.com/armanlife/firefox-vpn-client/core/internal/guardian"
)
// Failure codes say why a connection through Mozilla failed, so the apps
// can tell the user in their language what is wrong instead of always
// blaming the internet; the error text stays for the details. The status
// view's wake_error_code and the connection test's error_code carry them.
const (
// FailureNoNetwork: this device has no working network (no route, or
// names do not resolve).
FailureNoNetwork = "no_network"
// FailureUnreachable: Mozilla's servers did not answer or the
// connection was cut (timeouts, resets, failed TLS handshakes), as on
// a network that blocks them.
FailureUnreachable = "unreachable"
// FailureRefused: a Mozilla proxy server refused the tunnel.
FailureRefused = "refused"
// FailureLocationRefused: the server of a location the user chose
// kept refusing the tunnel (CONNECT 403 or 407) with a proxy pass
// fetched after the first refusal: Mozilla does not let this account
// use this location (LocationRefusedError).
FailureLocationRefused = "location_refused"
// FailureSignIn: the account must sign in again.
FailureSignIn = "sign_in"
// FailureQuota: the account's monthly data is used up.
FailureQuota = "quota"
// FailureNotEntitled: Mozilla does not let the account use the VPN.
FailureNotEntitled = "not_entitled"
// FailureRegion: Mozilla refuses requests from this region (HTTP 451).
FailureRegion = "region"
// FailureServer: Mozilla's service answered with a server error.
FailureServer = "server"
// FailureNoAccount: no account can be used now.
FailureNoAccount = "no_account"
// FailureTestSite: the tunnel opened, but the test site failed.
FailureTestSite = "test_site"
// FailureOther: anything else; the error text says what.
FailureOther = "other"
)
// FailureCode classifies err, a failure to reach a site through Mozilla,
// into one of the Failure codes; "" for nil.
func FailureCode(err error) string {
if err == nil {
return ""
}
var noAccount *accounts.NoUsableAccountError
var locationErr *LocationRefusedError
var connectErr *proxyConnectHTTPError
var dnsErr *net.DNSError
var errno syscall.Errno
var certErr *tls.CertificateVerificationError
var unknownAuthority x509.UnknownAuthorityError
var hostnameErr x509.HostnameError
var netErr net.Error
switch {
// The account's problems first: their errors may wrap the network
// error that a renewal also met.
case errors.Is(err, fxa.ErrReauthRequired):
return FailureSignIn
case errors.Is(err, guardian.ErrQuotaExceeded):
return FailureQuota
// Guardian's 403 also reads as an invalid token.
case errors.Is(err, guardian.ErrNotEntitled):
return FailureNotEntitled
case errors.Is(err, guardian.ErrTokenInvalid):
return FailureSignIn
case errors.Is(err, guardian.ErrRegionUnavailable):
return FailureRegion
case errors.As(err, &noAccount):
return FailureNoAccount
case errors.Is(err, guardian.ErrServer):
return FailureServer
case errors.As(err, &locationErr):
return FailureLocationRefused
// A rebuild that waits carries its cause, which decides.
case errors.As(err, &connectErr):
return FailureRefused
case errors.As(err, &dnsErr):
return FailureNoNetwork
case errors.As(err, &errno) && slices.Contains(noNetworkErrnos, errno):
return FailureNoNetwork
case errors.As(err, &errno) && slices.Contains(cutErrnos, errno),
errors.As(err, &certErr), errors.As(err, &unknownAuthority), errors.As(err, &hostnameErr),
errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF),
errors.Is(err, context.DeadlineExceeded),
errors.As(err, &netErr) && netErr.Timeout():
return FailureUnreachable
}
return FailureOther
}
// LocationRefusedError is a CONNECT refusal (403 or 407) by the server of a
// location the user chose, with a proxy pass fetched after the first
// refusal: a new pass did not help, so Mozilla most likely does not let
// this account use the location.
type LocationRefusedError struct {
Err error
}
const locationRefusedText = "Mozilla does not let this account use this location"
func (e *LocationRefusedError) Error() string {
return locationRefusedText + ": " + e.Err.Error()
}
func (e *LocationRefusedError) Unwrap() error { return e.Err }
// Redacted is the error without the response body, for logs that are not
// verbose.
func (e *LocationRefusedError) Redacted() string {
var connectErr *proxyConnectHTTPError
if errors.As(e.Err, &connectErr) {
return locationRefusedText + ": " + connectErr.Redacted()
}
return e.Error()
}
// SessionReplaced reports an error text of a request whose upstream
// session was closed under it because another replaced it (Go's HTTP/2
// client says "client connection force closed").
func SessionReplaced(text string) bool {
return strings.Contains(text, "client connection force closed") || strings.Contains(text, "use of closed network connection")
}