[client] Send the relay hostname as Host when dialing a relay by IP (#8359)

This commit is contained in:
Tjeerd Ritsma
2026-10-27 16:57:23 +01:00
committed by GitHub
parent 09f1de12e8
commit 678aff07de
4 changed files with 122 additions and 3 deletions
@@ -8,8 +8,9 @@ import (
"github.com/coder/websocket" "github.com/coder/websocket"
) )
func createDialOptions(serverName string, underlyingOut *net.Conn) *websocket.DialOptions { func createDialOptions(serverName, host string, underlyingOut *net.Conn) *websocket.DialOptions {
return &websocket.DialOptions{ return &websocket.DialOptions{
HTTPClient: httpClientNbDialer(serverName, underlyingOut), HTTPClient: httpClientNbDialer(serverName, underlyingOut),
Host: host,
} }
} }
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"github.com/coder/websocket" "github.com/coder/websocket"
) )
func createDialOptions(_ string, _ *net.Conn) *websocket.DialOptions { func createDialOptions(_, _ string, _ *net.Conn) *websocket.DialOptions {
// WASM version doesn't support HTTPClient or custom TLS config. // WASM version doesn't support HTTPClient or custom TLS config.
return &websocket.DialOptions{} return &websocket.DialOptions{}
} }
+22 -1
View File
@@ -31,8 +31,13 @@ func (d Dialer) Dial(ctx context.Context, address, serverName string) (net.Conn,
return nil, err return nil, err
} }
host, err := requestHost(wsURL, serverName)
if err != nil {
return nil, err
}
var underlying net.Conn var underlying net.Conn
opts := createDialOptions(serverName, &underlying) opts := createDialOptions(serverName, host, &underlying)
wsConn, resp, err := websocket.Dial(ctx, wsURL, opts) wsConn, resp, err := websocket.Dial(ctx, wsURL, opts)
if err != nil { if err != nil {
@@ -78,6 +83,22 @@ func prepareURL(address string) (string, error) {
return parsed.String(), nil return parsed.String(), nil
} }
// requestHost keeps the server name in the Host header when dialing by IP, so
// proxies that route by host still match.
func requestHost(wsURL, serverName string) (string, error) {
if serverName == "" {
return "", nil
}
parsed, err := url.Parse(wsURL)
if err != nil {
return "", fmt.Errorf("parse relay URL %q: %w", wsURL, err)
}
if port := parsed.Port(); port != "" {
return net.JoinHostPort(serverName, port), nil
}
return serverName, nil
}
// httpClientNbDialer builds the http client used by the websocket library. // httpClientNbDialer builds the http client used by the websocket library.
// underlyingOut, when non-nil, is populated with the raw conn from the // underlyingOut, when non-nil, is populated with the raw conn from the
// transport's DialContext so the caller can read its RemoteAddr. // transport's DialContext so the caller can read its RemoteAddr.
+97
View File
@@ -1,7 +1,12 @@
package ws package ws
import ( import (
"context"
"net"
"net/http"
"net/http/httptest"
"testing" "testing"
"time"
) )
func TestPrepareURL(t *testing.T) { func TestPrepareURL(t *testing.T) {
@@ -74,3 +79,95 @@ func TestPrepareURL(t *testing.T) {
}) })
} }
} }
func TestRequestHost(t *testing.T) {
tests := []struct {
name string
wsURL string
serverName string
want string
}{
{
name: "no server name keeps the URL host",
wsURL: "wss://1.2.3.4:443/relay",
want: "",
},
{
name: "IP with port",
wsURL: "wss://1.2.3.4:443/relay",
serverName: "relay.example.com",
want: "relay.example.com:443",
},
{
name: "IP with non-standard port",
wsURL: "ws://1.2.3.4:45678/relay",
serverName: "relay.example.com",
want: "relay.example.com:45678",
},
{
name: "IP without port",
wsURL: "wss://1.2.3.4/relay",
serverName: "relay.example.com",
want: "relay.example.com",
},
{
name: "IPv6 with port",
wsURL: "wss://[2001:db8::1]:443/relay",
serverName: "relay.example.com",
want: "relay.example.com:443",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := requestHost(tt.wsURL, tt.serverName)
if err != nil {
t.Fatalf("requestHost(%q, %q) err = %v", tt.wsURL, tt.serverName, err)
}
if got != tt.want {
t.Errorf("requestHost(%q, %q) = %q, want %q", tt.wsURL, tt.serverName, got, tt.want)
}
})
}
}
func TestDialByIPSendsServerNameAsHost(t *testing.T) {
hosts := make(chan string, 1)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hosts <- r.Host
http.NotFound(w, r)
}))
defer srv.Close()
_, port, err := net.SplitHostPort(srv.Listener.Addr().String())
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
serverName string
want string
}{
{name: "dial by IP with server name", serverName: "relay.example.com", want: "relay.example.com:" + port},
{name: "dial without server name", want: "127.0.0.1:" + port},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if _, err := (Dialer{}).Dial(ctx, "rel://127.0.0.1:"+port, tt.serverName); err == nil {
t.Fatal("expected the handshake to fail against a plain HTTP server")
}
select {
case got := <-hosts:
if got != tt.want {
t.Errorf("server saw Host %q, want %q", got, tt.want)
}
case <-ctx.Done():
t.Fatal("server never received the handshake request")
}
})
}
}