Files
syncthing/lib/dialer/internal.go
T

211 lines
5.1 KiB
Go
Raw Normal View History

2015-11-26 23:31:37 +00:00
// 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/.
2015-11-26 23:31:37 +00:00
package dialer
import (
"bufio"
"context"
"crypto/tls"
"encoding/base64"
"fmt"
2025-08-07 11:19:36 +02:00
"log/slog"
"net"
2015-11-26 23:31:37 +00:00
"net/http"
"net/url"
"os"
"sync"
2015-11-26 23:31:37 +00:00
"time"
"golang.org/x/net/proxy"
)
2024-11-19 11:32:56 +01:00
var noFallback = os.Getenv("ALL_PROXY_NO_FALLBACK") != ""
2015-11-26 23:31:37 +00:00
func init() {
proxy.RegisterDialerType("socks", socksDialerFunction)
proxy.RegisterDialerType("http", httpDialerFunction)
proxy.RegisterDialerType("https", httpDialerFunction)
2019-11-26 08:39:51 +01:00
if proxyDialer := proxy.FromEnvironment(); proxyDialer != proxy.Direct {
2015-11-26 23:31:37 +00:00
http.DefaultTransport = &http.Transport{
2019-11-26 08:39:51 +01:00
DialContext: DialContext,
2015-11-26 23:31:37 +00:00
Proxy: http.ProxyFromEnvironment,
TLSHandshakeTimeout: 10 * time.Second,
}
// Defer this, so that logging gets set up.
2015-11-26 23:31:37 +00:00
go func() {
time.Sleep(500 * time.Millisecond)
2025-08-07 11:19:36 +02:00
slog.Info("Proxy settings detected")
if noFallback {
2025-08-07 11:19:36 +02:00
slog.Info("Proxy fallback disabled")
}
2015-11-26 23:31:37 +00:00
}()
} else {
go func() {
time.Sleep(500 * time.Millisecond)
2025-08-07 11:19:36 +02:00
slog.Debug("Dialer logging disabled, as no proxy was detected")
2015-11-26 23:31:37 +00:00
}()
}
}
// 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
2025-10-21 22:00:44 +02:00
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
}