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
package proxy
import (
"bufio"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
"github.com/armanlife/firefox-vpn-client/core/internal/egress"
"github.com/armanlife/firefox-vpn-client/core/internal/logx"
)
// The SOCKS5 port also serves HTTP proxy clients (the Windows system proxy
// and applications that only know HTTP proxies): a SOCKS5 client starts
// with byte 5, an HTTP client with a request line. HTTPS goes through
// CONNECT tunnels; plain http:// requests are forwarded one per
// connection. Both use the same login, routing, and refused-port notes as
// SOCKS5.
// bufferedConn reads through the reader that peeked at the first byte, so
// nothing the client sent is lost.
type bufferedConn struct {
net.Conn
r *bufio.Reader
}
func (c *bufferedConn) Read(p []byte) (int, error) { return c.r.Read(p) }
// CloseWrite keeps half-close working for the relay.
func (c *bufferedConn) CloseWrite() error {
if cw, ok := c.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
// hopHeaders are meant for the proxy, not for the destination.
var hopHeaders = []string{"Connection", "Proxy-Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "Te", "Trailer", "Transfer-Encoding", "Upgrade"}
func (s *SOCKSServer) serveHTTP(client *bufferedConn, clientAddr string, start time.Time) {
req, err := http.ReadRequest(client.r)
if err != nil {
logx.Debug("HTTP proxy request unreadable client=%s err=%v", clientAddr, err)
return
}
if s.Auth != nil && !s.Auth.matchBasic(req.Header.Get("Proxy-Authorization")) {
logx.Warn("HTTP proxy client rejected: %s client=%s", errSOCKSAuth, clientAddr)
writeHTTPError(client, http.StatusProxyAuthRequired, "The proxy needs the username and password set in Settings.", `Basic realm="Firefox VPN"`)
return
}
if s.handshakeTimeout > 0 {
_ = client.SetDeadline(time.Time{})
}
if req.Method == http.MethodConnect {
target := withDefaultPort(req.Host, "443")
upstream, err := s.openTarget(client, target, clientAddr)
if err != nil {
writeHTTPError(client, httpStatusFor(err), httpErrorText(target, err), "")
return
}
defer upstream.Close()
if _, err := io.WriteString(client, "HTTP/1.1 200 Connection established\r\n\r\n"); err != nil {
return
}
proxyBidirectional(client, upstream, s.idleTimeout)
logx.Debug("HTTP proxy client disconnected client=%s target=%s duration=%s", clientAddr, logx.Target(target), time.Since(start).Round(time.Millisecond))
return
}
if req.URL == nil || !strings.EqualFold(req.URL.Scheme, "http") || req.URL.Host == "" {
writeHTTPError(client, http.StatusBadRequest, "This is a proxy: send a CONNECT request or an absolute http:// URL.", "")
return
}
target := withDefaultPort(req.URL.Host, "80")
upstream, err := s.openTarget(client, target, clientAddr)
if err != nil {
writeHTTPError(client, httpStatusFor(err), httpErrorText(target, err), "")
return
}
defer upstream.Close()
for _, h := range hopHeaders {
req.Header.Del(h)
}
req.Close = true
if err := req.Write(upstream); err != nil {
logx.Debug("HTTP proxy request forwarding failed client=%s target=%s err=%v", clientAddr, logx.Target(target), err)
return
}
resp, err := http.ReadResponse(bufio.NewReader(upstream), req)
if err != nil {
writeHTTPError(client, http.StatusBadGateway, "The site sent no valid answer.", "")
return
}
defer resp.Body.Close()
resp.Close = true
_ = resp.Write(client)
}
// openTarget opens a tunnel to target for a SOCKS5 or HTTP client and
// notes and logs a port Mozilla's proxy refuses, and a loop.
func (s *SOCKSServer) openTarget(client net.Conn, target, clientAddr string) (net.Conn, error) {
if egress.OwnDial(target) {
if loop, warn := loops.note(target, time.Now()); loop {
host, _, _ := net.SplitHostPort(target)
s.Notices.record(NoticeLoop, "", host)
if warn {
logx.Warn("a loop: this program's own connections come back to its proxy as a client's (client=%s target=%s), so nothing connects; a program sends this program's own traffic into the proxy: in PassWall2 turn Localhost Proxy off, in another VPN program's TUN mode let firefox-vpn-core connect directly", clientAddr, logx.Target(target))
}
}
}
var conn net.Conn
var err error
if affinityUpstream, ok := s.upstream.(clientAffinityTunnelOpener); ok {
conn, err = affinityUpstream.OpenTunnelForClient(target, clientAffinityKey(client.RemoteAddr()))
} else {
conn, err = s.upstream.OpenTunnel(target)
}
if err == nil {
logx.Debug("upstream tunnel open succeeded client=%s target=%s", clientAddr, logx.Target(target))
return conn, nil
}
if portRefused(target, err) {
host, port, _ := net.SplitHostPort(target)
s.Notices.record(NoticePort, port, host)
logx.Warn("Mozilla's proxy does not connect to port %s; it connects to ports %s only client=%s target=%s", port, UpstreamPortsText, clientAddr, logx.Target(target))
} else {
logx.Warn("upstream tunnel open failed client=%s target=%s err=%s", clientAddr, logx.Target(target), logx.Err(err))
}
return nil, err
}
func withDefaultPort(host, port string) string {
if _, _, err := net.SplitHostPort(host); err == nil {
return host
}
return net.JoinHostPort(strings.Trim(host, "[]"), port)
}
func httpStatusFor(err error) int {
var connectErr *proxyConnectHTTPError
switch {
case errors.Is(err, ErrBlockedByRule):
return http.StatusForbidden
case errors.Is(err, ErrNoUsableSession):
return http.StatusServiceUnavailable
case errors.As(err, &connectErr) && connectErr.statusCode == http.StatusBadGateway:
return http.StatusForbidden
}
return http.StatusBadGateway
}
func httpErrorText(target string, err error) string {
switch {
case portRefused(target, err):
_, port, _ := net.SplitHostPort(target)
return fmt.Sprintf("Mozilla's proxy does not connect to port %s; it connects to ports %s only.", port, UpstreamPortsText)
case errors.Is(err, ErrThisMachine):
return "The proxy does not connect to this computer itself."
case errors.Is(err, ErrBlockedByRule):
return "Refused by a routing rule (Settings, Routing)."
case errors.Is(err, ErrNoUsableSession):
return "No account can supply a connection right now; see the dashboard."
}
return "The connection through Mozilla's proxy failed."
}
func writeHTTPError(w io.Writer, status int, text, authenticate string) {
body := text + "\n"
header := fmt.Sprintf("HTTP/1.1 %d %s\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: %d\r\nConnection: close\r\n", status, http.StatusText(status), len(body))
if authenticate != "" {
header += "Proxy-Authenticate: " + authenticate + "\r\n"
}
_, _ = io.WriteString(w, header+"\r\n"+body)
}
// matchBasic checks a Proxy-Authorization header with Basic credentials.
func (c *socksCredentials) matchBasic(header string) bool {
scheme, encoded, ok := strings.Cut(header, " ")
if !ok || !strings.EqualFold(scheme, "Basic") {
return false
}
decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded))
if err != nil {
return false
}
user, pass, ok := strings.Cut(string(decoded), ":")
userOK := subtle.ConstantTimeCompare([]byte(user), []byte(c.username)) == 1
passOK := subtle.ConstantTimeCompare([]byte(pass), []byte(c.password)) == 1
return ok && userOK && passOK
}