Skip to content

Commit 5173bce

Browse files
authored
fix: stop redirecting DERP and replicasync http requests (#10752)
Fixes an issue where setting CODER_REDIRECT_TO_ACCESS_URL breaks use of multiple Coder server replicas for DERP traffic.
1 parent 5c48cb4 commit 5173bce

File tree

5 files changed

+123
-13
lines changed

5 files changed

+123
-13
lines changed

cli/server.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1922,6 +1922,18 @@ func redirectToAccessURL(handler http.Handler, accessURL *url.URL, tunnel bool,
19221922
http.Redirect(w, r, accessURL.String(), http.StatusTemporaryRedirect)
19231923
}
19241924

1925+
// Exception: DERP
1926+
// We use this endpoint when creating a DERP-mesh in the enterprise version to directly
1927+
// dial other Coderd derpers. Redirecting to the access URL breaks direct dial since the
1928+
// access URL will be load-balanced in a multi-replica deployment.
1929+
//
1930+
// It's totally fine to access DERP over TLS, but we also don't need to redirect HTTP to
1931+
// HTTPS as DERP is itself an encrypted protocol.
1932+
if isDERPPath(r.URL.Path) {
1933+
handler.ServeHTTP(w, r)
1934+
return
1935+
}
1936+
19251937
// Only do this if we aren't tunneling.
19261938
// If we are tunneling, we want to allow the request to go through
19271939
// because the tunnel doesn't proxy with TLS.
@@ -1949,6 +1961,14 @@ func redirectToAccessURL(handler http.Handler, accessURL *url.URL, tunnel bool,
19491961
})
19501962
}
19511963

1964+
func isDERPPath(p string) bool {
1965+
segments := strings.SplitN(p, "/", 3)
1966+
if len(segments) < 2 {
1967+
return false
1968+
}
1969+
return segments[1] == "derp"
1970+
}
1971+
19521972
// IsLocalhost returns true if the host points to the local machine. Intended to
19531973
// be called with `u.Hostname()`.
19541974
func IsLocalhost(host string) bool {

cli/server_internal_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,56 @@ func TestRedirectHTTPToHTTPSDeprecation(t *testing.T) {
243243
})
244244
}
245245
}
246+
247+
func TestIsDERPPath(t *testing.T) {
248+
t.Parallel()
249+
250+
testcases := []struct {
251+
path string
252+
expected bool
253+
}{
254+
//{
255+
// path: "/derp",
256+
// expected: true,
257+
//},
258+
{
259+
path: "/derp/",
260+
expected: true,
261+
},
262+
{
263+
path: "/derp/latency-check",
264+
expected: true,
265+
},
266+
{
267+
path: "/derp/latency-check/",
268+
expected: true,
269+
},
270+
{
271+
path: "",
272+
expected: false,
273+
},
274+
{
275+
path: "/",
276+
expected: false,
277+
},
278+
{
279+
path: "/derptastic",
280+
expected: false,
281+
},
282+
{
283+
path: "/api/v2/derp",
284+
expected: false,
285+
},
286+
{
287+
path: "//",
288+
expected: false,
289+
},
290+
}
291+
for _, tc := range testcases {
292+
tc := tc
293+
t.Run(tc.path, func(t *testing.T) {
294+
t.Parallel()
295+
require.Equal(t, tc.expected, isDERPPath(tc.path))
296+
})
297+
}
298+
}

cli/server_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,12 @@ func TestServer(t *testing.T) {
683683
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
684684
require.Equal(t, c.expectRedirect, resp.Header.Get("Location"))
685685
}
686+
687+
// We should never redirect DERP
688+
respDERP, err := client.Request(ctx, http.MethodGet, "/derp", nil)
689+
require.NoError(t, err)
690+
defer respDERP.Body.Close()
691+
require.Equal(t, http.StatusUpgradeRequired, respDERP.StatusCode)
686692
}
687693

688694
// Verify TLS

enterprise/replicasync/replicasync.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"net/http"
10+
"net/url"
1011
"os"
1112
"strings"
1213
"sync"
@@ -274,7 +275,19 @@ func (m *Manager) syncReplicas(ctx context.Context) error {
274275
wg.Add(1)
275276
go func(peer database.Replica) {
276277
defer wg.Done()
277-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, peer.RelayAddress, nil)
278+
ra, err := url.Parse(peer.RelayAddress)
279+
if err != nil {
280+
m.logger.Warn(ctx, "could not parse relay address",
281+
slog.F("relay_address", peer.RelayAddress), slog.Error(err))
282+
return
283+
}
284+
target, err := ra.Parse("/derp/latency-check")
285+
if err != nil {
286+
m.logger.Warn(ctx, "could not resolve /derp/latency-check endpoint",
287+
slog.F("relay_address", peer.RelayAddress), slog.Error(err))
288+
return
289+
}
290+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
278291
if err != nil {
279292
m.logger.Warn(ctx, "create http request for relay probe",
280293
slog.F("relay_address", peer.RelayAddress), slog.Error(err))

enterprise/replicasync/replicasync_test.go

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"net/http/httptest"
99
"sync"
10+
"sync/atomic"
1011
"testing"
1112
"time"
1213

@@ -55,9 +56,9 @@ func TestReplica(t *testing.T) {
5556
// Ensures that the replica reports a successful status for
5657
// accessing all of its peers.
5758
t.Parallel()
58-
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
59-
w.WriteHeader(http.StatusOK)
60-
}))
59+
dh := &derpyHandler{}
60+
defer dh.requireOnlyDERPPaths(t)
61+
srv := httptest.NewServer(dh)
6162
defer srv.Close()
6263
db, pubsub := dbtestutil.NewDB(t)
6364
peer, err := db.InsertReplica(context.Background(), database.InsertReplicaParams{
@@ -98,9 +99,9 @@ func TestReplica(t *testing.T) {
9899
ServerName: "hello.org",
99100
RootCAs: pool,
100101
}
101-
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102-
w.WriteHeader(http.StatusOK)
103-
}))
102+
dh := &derpyHandler{}
103+
defer dh.requireOnlyDERPPaths(t)
104+
srv := httptest.NewUnstartedServer(dh)
104105
srv.TLS = tlsConfig
105106
srv.StartTLS()
106107
defer srv.Close()
@@ -167,9 +168,9 @@ func TestReplica(t *testing.T) {
167168
server, err := replicasync.New(ctx, slogtest.Make(t, nil), db, pubsub, nil)
168169
require.NoError(t, err)
169170
defer server.Close()
170-
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
171-
w.WriteHeader(http.StatusOK)
172-
}))
171+
dh := &derpyHandler{}
172+
defer dh.requireOnlyDERPPaths(t)
173+
srv := httptest.NewServer(dh)
173174
defer srv.Close()
174175
peer, err := db.InsertReplica(ctx, database.InsertReplicaParams{
175176
ID: uuid.New(),
@@ -221,9 +222,9 @@ func TestReplica(t *testing.T) {
221222
db := dbmem.New()
222223
pubsub := pubsub.NewInMemory()
223224
logger := slogtest.Make(t, nil)
224-
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
225-
w.WriteHeader(http.StatusOK)
226-
}))
225+
dh := &derpyHandler{}
226+
defer dh.requireOnlyDERPPaths(t)
227+
srv := httptest.NewServer(dh)
227228
defer srv.Close()
228229
var wg sync.WaitGroup
229230
count := 20
@@ -255,3 +256,20 @@ func TestReplica(t *testing.T) {
255256
wg.Wait()
256257
})
257258
}
259+
260+
type derpyHandler struct {
261+
atomic.Uint32
262+
}
263+
264+
func (d *derpyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
265+
if r.URL.Path != "/derp/latency-check" {
266+
w.WriteHeader(http.StatusNotFound)
267+
d.Add(1)
268+
return
269+
}
270+
w.WriteHeader(http.StatusUpgradeRequired)
271+
}
272+
273+
func (d *derpyHandler) requireOnlyDERPPaths(t *testing.T) {
274+
require.Equal(t, uint32(0), d.Load())
275+
}

0 commit comments

Comments
 (0)