feat(dialer): add HTTP/HTTPS proxy support via CONNECT (#10572)
Register HTTP and HTTPS proxy dialers and implement CONNECT-based tunneling for HTTP proxies. The new dialer supports: - Plain HTTP proxies using CONNECT - HTTPS proxies by performing a TLS handshake before CONNECT - Optional basic authentication via Proxy-Authorization (with a warning when creds are used over cleartext HTTP) This allows all_proxy to be set to http:// or https:// URLs, enabling data transfer through HTTP(S) proxies. ### Purpose Allow peers to connect using HTTP Proxies (CONNECT) ### Testing Tested with both HTTP and HTTPS proxy connection, using both no auth and plain authentication. ### Screenshots No visual change ### Documentation https://github.com/syncthing/docs/pull/987 ## Authorship Your name and email will be added automatically to the AUTHORS file based on the commit metadata. --------- Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com> Signed-off-by: Jakob Borg <jakob@kastelo.net> Co-authored-by: Jakob Borg <jakob@kastelo.net>
This commit is contained in:
committed by
GitHub
parent
987e631176
commit
84c6b37913
@@ -7,11 +7,17 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
@@ -21,6 +27,8 @@ var noFallback = os.Getenv("ALL_PROXY_NO_FALLBACK") != ""
|
||||
|
||||
func init() {
|
||||
proxy.RegisterDialerType("socks", socksDialerFunction)
|
||||
proxy.RegisterDialerType("http", httpDialerFunction)
|
||||
proxy.RegisterDialerType("https", httpDialerFunction)
|
||||
|
||||
if proxyDialer := proxy.FromEnvironment(); proxyDialer != proxy.Direct {
|
||||
http.DefaultTransport = &http.Transport{
|
||||
@@ -59,6 +67,114 @@ func socksDialerFunction(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error)
|
||||
return proxy.SOCKS5("tcp", u.Host, auth, forward)
|
||||
}
|
||||
|
||||
type httpProxyDialer struct {
|
||||
proxyURL *url.URL
|
||||
forwardDialer proxy.Dialer
|
||||
}
|
||||
|
||||
func httpDialerFunction(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
|
||||
return &httpProxyDialer{
|
||||
proxyURL: u,
|
||||
forwardDialer: forward,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *httpProxyDialer) Dial(network, addr string) (net.Conn, error) {
|
||||
return h.DialContext(context.Background(), network, addr)
|
||||
}
|
||||
|
||||
// bufferedConn wraps a bufio.Reader (needed by http.ReadResponse) while
|
||||
// providing the other net.Conn methods via embedding.
|
||||
type bufferedConn struct {
|
||||
net.Conn
|
||||
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
func (c *bufferedConn) Read(b []byte) (int, error) {
|
||||
return c.reader.Read(b)
|
||||
}
|
||||
|
||||
var warnCleartextProxyAuthOnce sync.Once
|
||||
|
||||
func (h *httpProxyDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
||||
return nil, fmt.Errorf("unsupported network for http proxy: %s", network)
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
var err error
|
||||
if cd, ok := h.forwardDialer.(proxy.ContextDialer); ok {
|
||||
conn, err = cd.DialContext(ctx, "tcp", h.proxyURL.Host)
|
||||
} else {
|
||||
conn, err = h.forwardDialer.Dial("tcp", h.proxyURL.Host)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial proxy %s: %w", h.proxyURL.Host, err)
|
||||
}
|
||||
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("set proxy connection deadline: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.SetDeadline(time.Time{}) }()
|
||||
}
|
||||
|
||||
if h.proxyURL.Scheme == "https" {
|
||||
tlsConn := tls.Client(conn, &tls.Config{
|
||||
ServerName: h.proxyURL.Hostname(),
|
||||
})
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("https proxy handshake: %w", err)
|
||||
}
|
||||
conn = tlsConn
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Host: addr},
|
||||
Host: addr,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
if u := h.proxyURL.User; u != nil {
|
||||
if h.proxyURL.Scheme == "http" {
|
||||
warnCleartextProxyAuthOnce.Do(func() {
|
||||
slog.WarnContext(ctx,
|
||||
"Using basic auth over cleartext HTTP proxy",
|
||||
"proxy", h.proxyURL.Redacted(),
|
||||
)
|
||||
})
|
||||
}
|
||||
password, _ := u.Password()
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(u.Username() + ":" + password))
|
||||
req.Header.Set("Proxy-Authorization", "Basic "+auth)
|
||||
}
|
||||
|
||||
if err := req.Write(conn); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("write proxy CONNECT request: %w", err)
|
||||
}
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
resp, err := http.ReadResponse(br, req)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("read proxy CONNECT response: %w", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("http proxy CONNECT failed: %s", resp.Status)
|
||||
}
|
||||
|
||||
return &bufferedConn{Conn: conn, reader: br}, nil
|
||||
}
|
||||
|
||||
// dialerConn is needed because proxy dialed connections have RemoteAddr() pointing at the proxy,
|
||||
// which then screws up various things such as IsLAN checks, and "let's populate the relay invitation address from
|
||||
// existing connection" shenanigans.
|
||||
|
||||
Reference in New Issue
Block a user