Something went wrong. Try again.
Cloudflare Tunnel client with NetBSD support
Something went wrong. Try again.
18 kB · 581 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582package quicktunnelauth
import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "testing/iotest" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require")
func TestNewQuickTunnelAuthSessionManager(t *testing.T) { t.Parallel()
first, err := NewQuickTunnelAuthSessionManager() require.NoError(t, err) require.NotNil(t, first) assert.Len(t, first.sessionCookieSigningKey, quickTunnelAuthSessionSigningKeySize) assert.NotNil(t, first.random) assert.NotNil(t, first.now)
second, err := NewQuickTunnelAuthSessionManager() require.NoError(t, err) require.NotNil(t, second) assert.NotEqual(t, first.sessionCookieSigningKey, second.sessionCookieSigningKey)}
func TestQuickTunnelAuthSessionManagerSign(t *testing.T) { t.Parallel()
key := bytes.Repeat([]byte{0x42}, quickTunnelAuthSessionSigningKeySize) manager := &QuickTunnelAuthSessionManager{sessionCookieSigningKey: key} payload := []byte("session payload")
signature, err := manager.sign(payload) require.NoError(t, err)
expectedMAC := hmac.New(sha256.New, key) _, err = expectedMAC.Write(payload) require.NoError(t, err) assert.Equal(t, expectedMAC.Sum(nil), signature) assert.Len(t, signature, sha256.Size)}
func TestQuickTunnelAuthSessionManagerSignRequiresKey(t *testing.T) { t.Parallel()
tests := []struct { name string manager *QuickTunnelAuthSessionManager }{ {name: "nil manager"}, {name: "empty key", manager: &QuickTunnelAuthSessionManager{}}, }
for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel()
_, err := test.manager.sign([]byte("payload")) require.EqualError(t, err, "session cookie signing key is not initialized") }) }}
func TestQuickTunnelAuthSessionManagerIssueSession(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC) tests := []struct { name string identityExpiresAt time.Time expectedExpiresAt time.Time }{ { name: "identity expires first", identityExpiresAt: now.Add(2 * time.Hour), expectedExpiresAt: now.Add(2 * time.Hour), }, { name: "session TTL expires first", identityExpiresAt: now.Add(48 * time.Hour), expectedExpiresAt: now.Add(4 * time.Hour), }, }
for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel()
randomBytes := bytes.Repeat([]byte{0x24}, quickTunnelAuthSessionNonceSize) manager := newTestQuickTunnelAuthSessionManager(now, 0x42, bytes.NewReader(randomBytes))
cookie, err := manager.IssueSession(test.identityExpiresAt) require.NoError(t, err) require.NotNil(t, cookie) assert.Equal(t, quickTunnelAuthSessionCookieName, cookie.Name) assert.Equal(t, quickTunnelAuthSessionCookiePath, cookie.Path) assert.Empty(t, cookie.Domain) assert.True(t, cookie.Secure) assert.True(t, cookie.HttpOnly) assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite) assert.True(t, cookie.Expires.Equal(test.expectedExpiresAt))
payloadBytes, signature, payload := decodeQuickTunnelAuthSessionCookie(t, cookie) assert.Equal(t, base64.RawURLEncoding.EncodeToString(randomBytes), payload.Nonce) assert.Equal(t, test.expectedExpiresAt.Unix(), payload.ExpiresAt)
expectedSignature, err := manager.sign(payloadBytes) require.NoError(t, err) assert.True(t, hmac.Equal(expectedSignature, signature)) }) }}
func TestQuickTunnelAuthSessionManagerIssueSessionRejectsExpiredIdentity(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC) tests := []struct { name string identityExpiresAt time.Time }{ {name: "at current time", identityExpiresAt: now}, {name: "before current time", identityExpiresAt: now.Add(-time.Second)}, }
for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel()
manager := newTestQuickTunnelAuthSessionManager(now, 0x42, bytes.NewReader(make([]byte, quickTunnelAuthSessionNonceSize))) cookie, err := manager.IssueSession(test.identityExpiresAt) require.EqualError(t, err, "issue authentication session: identity has expired") assert.Nil(t, cookie) }) }}
func TestQuickTunnelAuthSessionManagerIssueSessionRejectsIdentityExpiringWithinCurrentSecond(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC).Add(100 * time.Millisecond) manager := newTestQuickTunnelAuthSessionManager( now, 0x42, bytes.NewReader(make([]byte, quickTunnelAuthSessionNonceSize)), )
cookie, err := manager.IssueSession(now.Add(500 * time.Millisecond)) require.EqualError(t, err, "issue authentication session: identity expires too soon") assert.Nil(t, cookie)}
func TestQuickTunnelAuthSessionManagerIssueSessionRequiresInitializedManager(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC) tests := []struct { name string manager *QuickTunnelAuthSessionManager }{ {name: "nil manager"}, {name: "zero-value manager", manager: &QuickTunnelAuthSessionManager{}}, }
for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel()
cookie, err := test.manager.IssueSession(now.Add(time.Hour)) require.EqualError(t, err, "authentication session manager is not initialized") assert.Nil(t, cookie) }) }}
func TestQuickTunnelAuthSessionManagerIssueSessionErrors(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC)
t.Run("random source failure", func(t *testing.T) { t.Parallel()
manager := newTestQuickTunnelAuthSessionManager(now, 0x42, iotest.ErrReader(assert.AnError)) cookie, err := manager.IssueSession(now.Add(time.Hour)) require.EqualError(t, err, "generate authentication session nonce: assert.AnError general error for testing") assert.Nil(t, cookie) })
t.Run("missing signing key", func(t *testing.T) { t.Parallel()
manager := newTestQuickTunnelAuthSessionManager(now, 0, bytes.NewReader(make([]byte, quickTunnelAuthSessionNonceSize))) manager.sessionCookieSigningKey = nil cookie, err := manager.IssueSession(now.Add(time.Hour)) require.EqualError(t, err, "issue authentication session: session cookie signing key is not initialized") assert.Nil(t, cookie) })}
func TestQuickTunnelAuthSessionManagerValidateSession(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC) manager := newTestQuickTunnelAuthSessionManager( now, 0x42, bytes.NewReader(bytes.Repeat([]byte{0x24}, quickTunnelAuthSessionNonceSize)), ) cookie, err := manager.IssueSession(now.Add(time.Hour)) require.NoError(t, err)
request := requestWithQuickTunnelAuthSession(cookie.Value) valid, err := manager.ValidateSession(request) require.NoError(t, err) assert.True(t, valid)}
func TestQuickTunnelAuthSessionManagerValidateSessionRejectsDuplicateCookies(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC) manager := newTestQuickTunnelAuthSessionManager( now, 0x42, bytes.NewReader(bytes.Repeat([]byte{0x24}, quickTunnelAuthSessionNonceSize)), ) cookie, err := manager.IssueSession(now.Add(time.Hour)) require.NoError(t, err)
request := requestWithQuickTunnelAuthSession(cookie.Value) request.AddCookie(cookie)
valid, err := manager.ValidateSession(request) require.NoError(t, err) assert.False(t, valid)}
func TestQuickTunnelAuthSessionManagerValidateSessionRemovesAuthenticationCookies(t *testing.T) { t.Parallel()
const state = "test-state" now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC) manager := newTestQuickTunnelAuthSessionManager( now, 0x42, bytes.NewReader(bytes.Repeat([]byte{0x24}, quickTunnelAuthSessionNonceSize)), ) cookie, err := manager.IssueSession(now.Add(time.Hour)) require.NoError(t, err)
request := requestWithQuickTunnelAuthSession(cookie.Value) request.AddCookie(&http.Cookie{ Name: quickTunnelAuthStateCookieName(state), Value: "state-value", Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, }) request.AddCookie(&http.Cookie{ Name: "origin-session", Value: "origin-value", Path: "/", Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, })
valid, err := manager.ValidateSession(request) require.NoError(t, err) assert.True(t, valid) assert.Empty(t, request.CookiesNamed(quickTunnelAuthSessionCookieName)) assert.Empty(t, request.CookiesNamed(quickTunnelAuthStateCookieName(state)))
originCookies := request.CookiesNamed("origin-session") require.Len(t, originCookies, 1) assert.Equal(t, "origin-value", originCookies[0].Value)}
func TestQuickTunnelAuthSessionManagerValidateSessionExpirationBoundaries(t *testing.T) { t.Parallel()
expiresAt := time.Date(2026, time.September, 3, 10, 0, 0, 0, time.UTC) validNonce := base64.RawURLEncoding.EncodeToString(make([]byte, quickTunnelAuthSessionNonceSize)) tests := []struct { name string now time.Time expiresAt time.Time valid bool }{ {name: "immediately before expiry", now: expiresAt.Add(-time.Nanosecond), expiresAt: expiresAt, valid: true}, {name: "at expiry", now: expiresAt, expiresAt: expiresAt}, {name: "after expiry", now: expiresAt.Add(time.Nanosecond), expiresAt: expiresAt}, {name: "at maximum TTL", now: expiresAt, expiresAt: expiresAt.Add(quickTunnelAuthSessionTTL), valid: true}, }
for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel()
manager := newTestQuickTunnelAuthSessionManager(test.now, 0x42, bytes.NewReader(nil)) payloadBytes, err := json.Marshal(quickTunnelAuthSessionCookiePayload{ Nonce: validNonce, ExpiresAt: test.expiresAt.Unix(), }) require.NoError(t, err) cookie := signedQuickTunnelAuthSessionCookie(t, manager, payloadBytes)
valid, err := manager.ValidateSession(requestWithQuickTunnelAuthSession(cookie.Value)) require.NoError(t, err) assert.Equal(t, test.valid, valid) }) }}
func TestQuickTunnelAuthSessionManagerValidateSessionRejectsMalformedCookies(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC) manager := newTestQuickTunnelAuthSessionManager(now, 0x42, bytes.NewReader(nil)) tests := []struct { name string cookieValue string }{ {name: "empty"}, {name: "missing separator", cookieValue: "payload"}, {name: "empty payload", cookieValue: ".signature"}, {name: "empty signature", cookieValue: "payload."}, {name: "extra separator", cookieValue: "payload.signature.extra"}, {name: "invalid payload encoding", cookieValue: "*.AA"}, {name: "invalid signature encoding", cookieValue: "e30.!"}, }
for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel()
valid, err := manager.ValidateSession(requestWithQuickTunnelAuthSession(test.cookieValue)) require.NoError(t, err) assert.False(t, valid) }) }}
func TestQuickTunnelAuthSessionManagerValidateSessionRejectsInvalidPayloads(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC) validNonce := base64.RawURLEncoding.EncodeToString(make([]byte, quickTunnelAuthSessionNonceSize)) nonCanonicalNonce := validNonce[:len(validNonce)-1] + "B" tests := []struct { name string rawPayload []byte payload *quickTunnelAuthSessionCookiePayload }{ {name: "invalid JSON", rawPayload: []byte("{")}, { name: "invalid nonce encoding", payload: &quickTunnelAuthSessionCookiePayload{ Nonce: "not-base64!", ExpiresAt: now.Add(time.Hour).Unix(), }, }, { name: "invalid nonce size", payload: &quickTunnelAuthSessionCookiePayload{ Nonce: base64.RawURLEncoding.EncodeToString(make([]byte, quickTunnelAuthSessionNonceSize-1)), ExpiresAt: now.Add(time.Hour).Unix(), }, }, { name: "non-canonical nonce", payload: &quickTunnelAuthSessionCookiePayload{ Nonce: nonCanonicalNonce, ExpiresAt: now.Add(time.Hour).Unix(), }, }, { name: "expired at boundary", payload: &quickTunnelAuthSessionCookiePayload{ Nonce: validNonce, ExpiresAt: now.Unix(), }, }, { name: "expired before boundary", payload: &quickTunnelAuthSessionCookiePayload{ Nonce: validNonce, ExpiresAt: now.Add(-time.Second).Unix(), }, }, }
for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel()
manager := newTestQuickTunnelAuthSessionManager(now, 0x42, bytes.NewReader(nil)) payloadBytes := test.rawPayload if test.payload != nil { var err error payloadBytes, err = json.Marshal(test.payload) require.NoError(t, err) } cookie := signedQuickTunnelAuthSessionCookie(t, manager, payloadBytes)
valid, err := manager.ValidateSession(requestWithQuickTunnelAuthSession(cookie.Value)) require.NoError(t, err) assert.False(t, valid) }) }}
func TestQuickTunnelAuthSessionManagerValidateSessionRejectsTampering(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC) tests := []struct { name string tamperPayload bool tamperSignature bool }{ {name: "payload", tamperPayload: true}, {name: "signature", tamperSignature: true}, }
for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel()
manager := newTestQuickTunnelAuthSessionManager( now, 0x42, bytes.NewReader(bytes.Repeat([]byte{0x24}, quickTunnelAuthSessionNonceSize)), ) cookie, err := manager.IssueSession(now.Add(time.Hour)) require.NoError(t, err)
payloadBytes, signature, _ := decodeQuickTunnelAuthSessionCookie(t, cookie) if test.tamperPayload { payloadBytes[0] ^= 0xff } if test.tamperSignature { signature[0] ^= 0xff } cookie.Value = encodeQuickTunnelAuthSessionCookieValue(payloadBytes, signature)
valid, err := manager.ValidateSession(requestWithQuickTunnelAuthSession(cookie.Value)) require.NoError(t, err) assert.False(t, valid) }) }}
func TestQuickTunnelAuthSessionManagerValidateSessionRejectsPreviousProcess(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC) firstManager := newTestQuickTunnelAuthSessionManager( now, 0x11, bytes.NewReader(bytes.Repeat([]byte{0x24}, quickTunnelAuthSessionNonceSize)), ) cookie, err := firstManager.IssueSession(now.Add(time.Hour)) require.NoError(t, err)
restartedManager := newTestQuickTunnelAuthSessionManager(now, 0x22, bytes.NewReader(nil)) valid, err := restartedManager.ValidateSession(requestWithQuickTunnelAuthSession(cookie.Value)) require.NoError(t, err) assert.False(t, valid)}
func TestQuickTunnelAuthSessionManagerValidateSessionErrors(t *testing.T) { t.Parallel()
now := time.Date(2026, time.September, 2, 10, 0, 0, 0, time.UTC) manager := newTestQuickTunnelAuthSessionManager(now, 0x42, bytes.NewReader(nil))
t.Run("nil request", func(t *testing.T) { t.Parallel()
valid, err := manager.ValidateSession(nil) require.EqualError(t, err, "validate authentication session: request is nil") assert.False(t, valid) })
t.Run("missing cookie", func(t *testing.T) { t.Parallel()
request := httptest.NewRequest(http.MethodGet, "https://example.com", nil) valid, err := manager.ValidateSession(request) require.NoError(t, err) assert.False(t, valid) })
t.Run("uninitialized manager", func(t *testing.T) { t.Parallel()
var uninitialized *QuickTunnelAuthSessionManager valid, err := uninitialized.ValidateSession(httptest.NewRequest(http.MethodGet, "https://example.com", nil)) require.EqualError(t, err, "authentication session manager is not initialized") assert.False(t, valid) })
t.Run("missing signing key", func(t *testing.T) { t.Parallel()
unsignedManager := newTestQuickTunnelAuthSessionManager(now, 0, bytes.NewReader(nil)) unsignedManager.sessionCookieSigningKey = nil payloadBytes := []byte(`{}`) signature := make([]byte, sha256.Size) request := requestWithQuickTunnelAuthSession(encodeQuickTunnelAuthSessionCookieValue(payloadBytes, signature))
valid, err := unsignedManager.ValidateSession(request) require.EqualError(t, err, "verify authentication session signature: session cookie signing key is not initialized") assert.False(t, valid) })}
func newTestQuickTunnelAuthSessionManager(now time.Time, keyByte byte, random io.Reader) *QuickTunnelAuthSessionManager { return &QuickTunnelAuthSessionManager{ sessionCookieSigningKey: bytes.Repeat([]byte{keyByte}, quickTunnelAuthSessionSigningKeySize), random: random, now: func() time.Time { return now }, }}
func signedQuickTunnelAuthSessionCookie(t *testing.T, manager *QuickTunnelAuthSessionManager, payloadBytes []byte) *http.Cookie { t.Helper()
signature, err := manager.sign(payloadBytes) require.NoError(t, err) return &http.Cookie{ Name: quickTunnelAuthSessionCookieName, Value: encodeQuickTunnelAuthSessionCookieValue(payloadBytes, signature), Path: quickTunnelAuthSessionCookiePath, Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, }}
func decodeQuickTunnelAuthSessionCookie(t *testing.T, cookie *http.Cookie) ([]byte, []byte, quickTunnelAuthSessionCookiePayload) { t.Helper()
encodedPayload, encodedSignature, found := strings.Cut(cookie.Value, quickTunnelAuthSessionCookieSeparator) require.True(t, found) payloadBytes, err := base64.RawURLEncoding.DecodeString(encodedPayload) require.NoError(t, err) signature, err := base64.RawURLEncoding.DecodeString(encodedSignature) require.NoError(t, err)
var payload quickTunnelAuthSessionCookiePayload require.NoError(t, json.Unmarshal(payloadBytes, &payload)) return payloadBytes, signature, payload}
func requestWithQuickTunnelAuthSession(cookieValue string) *http.Request { request := httptest.NewRequest(http.MethodGet, "https://example.com", nil) request.AddCookie(&http.Cookie{ Name: quickTunnelAuthSessionCookieName, Value: cookieValue, Path: quickTunnelAuthSessionCookiePath, Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, }) return request}