mirror of
https://github.com/syncthing/syncthing.git
synced 2026-06-01 04:45:12 -04:00
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>
211 lines
5.1 KiB
Go
211 lines
5.1 KiB
Go
// Copyright (C) 2015 The Syncthing Authors.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
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"
|
|
)
|
|
|
|
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{
|
|
DialContext: DialContext,
|
|
Proxy: http.ProxyFromEnvironment,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
}
|
|
|
|
// Defer this, so that logging gets set up.
|
|
go func() {
|
|
time.Sleep(500 * time.Millisecond)
|
|
slog.Info("Proxy settings detected")
|
|
if noFallback {
|
|
slog.Info("Proxy fallback disabled")
|
|
}
|
|
}()
|
|
} else {
|
|
go func() {
|
|
time.Sleep(500 * time.Millisecond)
|
|
slog.Debug("Dialer logging disabled, as no proxy was detected")
|
|
}()
|
|
}
|
|
}
|
|
|
|
// This is a rip off of proxy.FromURL for "socks" URL scheme
|
|
func socksDialerFunction(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
|
|
var auth *proxy.Auth
|
|
if u.User != nil {
|
|
auth = new(proxy.Auth)
|
|
auth.User = u.User.Username()
|
|
if p, ok := u.User.Password(); ok {
|
|
auth.Password = p
|
|
}
|
|
}
|
|
|
|
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.
|
|
type dialerConn struct {
|
|
net.Conn
|
|
|
|
addr net.Addr
|
|
}
|
|
|
|
func (c dialerConn) RemoteAddr() net.Addr {
|
|
return c.addr
|
|
}
|
|
|
|
func newDialerAddr(network, addr string) net.Addr {
|
|
netAddr, err := net.ResolveIPAddr(network, addr)
|
|
if err == nil {
|
|
return netAddr
|
|
}
|
|
return fallbackAddr{network, addr}
|
|
}
|
|
|
|
type fallbackAddr struct {
|
|
network string
|
|
addr string
|
|
}
|
|
|
|
func (a fallbackAddr) Network() string {
|
|
return a.network
|
|
}
|
|
|
|
func (a fallbackAddr) String() string {
|
|
return a.addr
|
|
}
|