From 3dacd077571e964a0a81f8d2730e96ac047849a8 Mon Sep 17 00:00:00 2001 From: Dv2Corn <320380254+Dv2Corn@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:46:15 +0000 Subject: [PATCH] Accept ws and wss in X-Forwarded-Proto for the monitoring UI Some reverse proxies, such as Traefik, report WebSocket handshakes as ws or wss. These values were ignored, so behind a TLS-terminating proxy the scheme was assumed to be http: the origin check rejected the https origin of the dashboard, and accepted an http one. Map ws to http and wss to https. --- dnscrypt-proxy/monitoring_ui.go | 6 +-- dnscrypt-proxy/monitoring_ui_origin_test.go | 52 +++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 dnscrypt-proxy/monitoring_ui_origin_test.go diff --git a/dnscrypt-proxy/monitoring_ui.go b/dnscrypt-proxy/monitoring_ui.go index 4b6888bdda..7010b31b75 100644 --- a/dnscrypt-proxy/monitoring_ui.go +++ b/dnscrypt-proxy/monitoring_ui.go @@ -1022,16 +1022,16 @@ func (mc *MetricsCollector) GetMetrics() map[string]any { // requestScheme - Returns the scheme the client used, which a TLS-terminating // proxy reports in X-Forwarded-Proto. Browsers cannot forge that header on a -// WebSocket handshake. +// WebSocket handshake. Some proxies report these handshakes as ws or wss. func requestScheme(r *http.Request) string { forwarded := r.Header.Get("X-Forwarded-Proto") if comma := strings.IndexByte(forwarded, ','); comma >= 0 { forwarded = forwarded[:comma] } switch strings.ToLower(strings.TrimSpace(forwarded)) { - case "http": + case "http", "ws": return "http" - case "https": + case "https", "wss": return "https" } if r.TLS != nil { diff --git a/dnscrypt-proxy/monitoring_ui_origin_test.go b/dnscrypt-proxy/monitoring_ui_origin_test.go new file mode 100644 index 0000000000..b1e24dcd52 --- /dev/null +++ b/dnscrypt-proxy/monitoring_ui_origin_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "crypto/tls" + "net/http" + "net/http/httptest" + "testing" +) + +// TestWebSocketOriginBehindProxy checks the origin of WebSocket handshakes +// against the scheme reported by a reverse proxy, which can be ws or wss. +func TestWebSocketOriginBehindProxy(t *testing.T) { + ui := NewMonitoringUI(&Proxy{}) + if ui == nil { + t.Fatal("Failed to create monitoring UI") + } + for _, tt := range []struct { + name string + forwarded string + tls bool + origin string + want bool + }{ + {"direct http", "", false, "http://ui.example", true}, + {"direct https", "", true, "https://ui.example", true}, + {"direct https, http origin", "", true, "http://ui.example", false}, + {"https proxy", "https", false, "https://ui.example", true}, + {"https proxy, http origin", "https", false, "http://ui.example", false}, + {"wss proxy", "wss", false, "https://ui.example", true}, + {"wss proxy, uppercase", "WSS", false, "https://ui.example", true}, + {"wss proxy chain", "wss, ws", false, "https://ui.example", true}, + {"wss proxy, http origin", "wss", false, "http://ui.example", false}, + {"wss proxy, other host", "wss", false, "https://other.example", false}, + {"ws proxy", "ws", false, "http://ui.example", true}, + {"ws proxy to a TLS listener", "ws", true, "http://ui.example", true}, + {"ws proxy, https origin", "ws", true, "https://ui.example", false}, + } { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "http://ui.example/api/ws", nil) + r.Header.Set("Origin", tt.origin) + if tt.forwarded != "" { + r.Header.Set("X-Forwarded-Proto", tt.forwarded) + } + if tt.tls { + r.TLS = &tls.ConnectionState{} + } + if got := ui.upgrader.CheckOrigin(r); got != tt.want { + t.Errorf("origin %q with X-Forwarded-Proto %q accepted: %v, want %v", tt.origin, tt.forwarded, got, tt.want) + } + }) + } +}