fix(outbounds): use hostname for HTTPS proxy SNI (#1597)

The httpOutbound.dial() method incorrectly sets tls.Config.ServerName
to o.Addr, which is the "host:port" address string. TLS SNI must be a
pure hostname without a port number. Sending "host:port" as SNI causes
invalid TLS ClientHello messages and may prevent the proxy server from
correctly routing the connection.

Use o.ServerName instead, which is already correctly set to u.Hostname()
in NewHTTPOutbound.
This commit is contained in:
白日梦主义 2026-06-05 06:08:18 +08:00 committed by GitHub
parent f4c9de56cc
commit 829d125ea2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 89 additions and 1 deletions

View file

@ -88,7 +88,7 @@ func (o *httpOutbound) dial() (net.Conn, error) {
// Wrap the connection with TLS if the proxy is HTTPS.
conn = tls.Client(conn, &tls.Config{
InsecureSkipVerify: o.Insecure,
ServerName: o.Addr,
ServerName: o.ServerName,
})
}
return conn, nil

View file

@ -0,0 +1,88 @@
package outbounds
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"net"
"testing"
"time"
)
func TestHTTPOutboundHTTPSUsesConfiguredServerName(t *testing.T) {
cert, err := newHTTPOutboundTestCert()
if err != nil {
t.Fatal(err)
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
gotSNI := make(chan string, 1)
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
defer conn.Close()
tlsConn := tls.Server(conn, &tls.Config{
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
gotSNI <- hello.ServerName
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
},
})
_ = tlsConn.Handshake()
}()
outbound := &httpOutbound{
Dialer: &net.Dialer{Timeout: time.Second},
Addr: ln.Addr().String(),
HTTPS: true,
Insecure: true,
ServerName: "proxy.example.com",
}
conn, err := outbound.dial()
if err != nil {
t.Fatal(err)
}
defer conn.Close()
if err := conn.(*tls.Conn).Handshake(); err != nil {
t.Fatal(err)
}
select {
case got := <-gotSNI:
if got != outbound.ServerName {
t.Fatalf("SNI = %q, want %q", got, outbound.ServerName)
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for ClientHello")
}
}
func newHTTPOutboundTestCert() (tls.Certificate, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return tls.Certificate{}, err
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "localhost"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: []string{"localhost"},
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
return tls.Certificate{}, err
}
return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}, nil
}