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 netcheck tells whether this device reaches the internet at all,
// outside the tunnel, so a failed connection to Mozilla can be told apart
// from a device without internet: a network that filters Mozilla answers
// these checks, one without internet does not.
//
// It asks the connectivity checks of Google, Microsoft, and Apple, which
// operating systems use for the same question, and Baidu, which answers in
// China where Google's does not, all at once over plain HTTP; any expected
// answer means online. A captive portal's redirect or page is not one. Each
// call checks anew: the network may have changed since the last one.
package netcheck
import (
"bytes"
"context"
"io"
"net"
"net/http"
"sync"
"time"
"github.com/armanlife/firefox-vpn-client/core/internal/egress"
)
// Result is what a check found.
type Result int
const (
// Unknown: no check was made (tests, or a check replaced).
Unknown Result = iota
// Online: at least one check site answered as expected.
Online
// Offline: none did within the timeout.
Offline
)
// Timeout bounds one check.
const Timeout = 5 * time.Second
// site is a check URL and the answer it gives on a working internet.
type site struct {
url string
ok func(status int, body []byte) bool
}
var sites = []site{
{"http://connectivitycheck.gstatic.com/generate_204", func(status int, _ []byte) bool { return status == http.StatusNoContent }},
{"http://www.msftconnecttest.com/connecttest.txt", func(status int, body []byte) bool {
return status == http.StatusOK && bytes.HasPrefix(body, []byte("Microsoft Connect Test"))
}},
{"http://captive.apple.com/hotspot-detect.html", func(status int, body []byte) bool {
return status == http.StatusOK && bytes.Contains(body, []byte("Success"))
}},
{"http://www.baidu.com/", func(status int, body []byte) bool {
return status == http.StatusOK && bytes.Contains(body, []byte("baidu"))
}},
}
// Check reports whether the internet is reachable; tests replace it.
var Check = check
var (
mu sync.Mutex
running chan struct{}
last Result
)
// check runs one check; callers that arrive while one runs share its
// result.
func check(ctx context.Context) Result {
mu.Lock()
if running != nil {
wait := running
mu.Unlock()
select {
case <-wait:
case <-ctx.Done():
return Unknown
}
mu.Lock()
defer mu.Unlock()
return last
}
done := make(chan struct{})
running = done
mu.Unlock()
result := probe(ctx, sites)
mu.Lock()
last, running = result, nil
mu.Unlock()
close(done)
return result
}
// probe asks every site at once, directly (never through a proxy), and
// returns Online at the first expected answer.
func probe(ctx context.Context, list []site) Result {
ctx, cancel := context.WithTimeout(ctx, Timeout)
defer cancel()
client := &http.Client{
Transport: &http.Transport{
Proxy: nil,
// Outside the TUN adapter while the TUN mode runs: the check is
// about the network itself.
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return egress.Dialer(Timeout).DialContext(ctx, network, address)
},
DisableKeepAlives: true,
},
// A redirect is a captive portal's, not the expected answer.
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
}
answers := make(chan bool, len(list))
for _, s := range list {
go func(s site) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.url, nil)
if err != nil {
answers <- false
return
}
resp, err := client.Do(req)
if err != nil {
answers <- false
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
answers <- s.ok(resp.StatusCode, body)
}(s)
}
for range list {
if <-answers {
return Online
}
}
return Offline
}