Skip to content

feat(network): #8 add AUTH handshake for binary protocol - #18

Merged
Saxy merged 6 commits into
mainfrom
feat/binary-auth
Jul 30, 2026
Merged

feat(network): #8 add AUTH handshake for binary protocol#18
Saxy merged 6 commits into
mainfrom
feat/binary-auth

Conversation

@Saxy

@Saxy Saxy commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Component: Networking/RESP, Client SDK, Build/CI

Type of Change: New feature

Related Issue: Closes #8

Technical Deep Dive & Context:

The binary protocol AUTH handshake follows the same design as the RESP AUTH from #17:

  • Zero-overhead path: When --require-pass is unset, requirePassHash is nil and connState.authenticated starts as true. The auth guard is a single boolean check on the hot path — no bcrypt, no allocations.
  • bcrypt at startup: NewServer hashes the password once with bcrypt.GenerateFromPassword. The hash is stored, never the plaintext. bcrypt.CompareHashAndPassword runs only on MsgAuth, never on data commands.
  • Wire format: MsgAuth payload is [2B usernameLen][username][2B passwordLen][password]. Parsed in processAuth by slicing directly into the gnet read buffer — zero copy, zero allocation.
  • Guard placement: Auth checking happens in handleDecryptedFrames / onTrafficPlaintext before the handler callback, so the handler never sees unauthenticated requests (except MsgPing, which is allow-listed like Redis).
  • ACL future-proofing: Username-aware AUTH <username> <password> is supported; only "default" user is accepted until an ACL system lands.

Performance & Benchmarks:

No measurable change on the hot path. The !st.authenticated check is a single boolean load; MsgAuth is never sent on the hot path.

BenchmarkGnetPlaintext-32             15828     72477 ns/op     672 B/op       2 allocs/op
BenchmarkGnetPlaintextParallel-32    168244      6817 ns/op     678 B/op       2 allocs/op
BenchmarkReadMessageZeroAlloc-32   435974060     2.678 ns/op       0 B/op       0 allocs/op

How Has This Been Tested:

  • go test ./... — all packages pass
  • go test -race ./... — all packages pass
  • task bench:go PKG=./internal/network/... — benchmarks pass
  • New tests: TestServerAuthFlow, TestServerAuthNoPassword, TestServerAuthWithUsername, TestServerAuthMalformedPayload
  • Existing RESP auth tests remain green

Checklist:

  • My code follows the existing code style of this project
  • I have added tests that prove my fix/feature works
  • New and existing tests pass locally (go test ./... and go test -race ./...)
  • I have updated the documentation (README, comments, or any relevant docs)
  • My changes generate no new go vet warnings
  • Any breaking changes are documented and communicated

Summary by CodeRabbit

  • New Features
    • Added client-side password authentication (“single-password mode”) with a new Auth flow that works for both plaintext and TLS.
    • When password protection is enabled, the server enforces authentication before non-ping traffic; malformed/invalid credentials produce explicit auth error responses and can close the connection after replies.
  • Developer Tools
    • Added a bench:go task to run Go benchmarks with configurable target packages, benchmark pattern, and iteration count.
  • Compatibility
    • If no password is configured, authentication behavior remains unchanged.

Implements connection-level authentication for the binary protocol
(port 9988) using the same --require-pass / TSD_REQUIRE_PASS config
as the RESP AUTH command.

- Add MsgAuth/MsgAuthOk/MsgAuthErr wire types with username+password
  payload format
- bcrypt password hashing at startup, comparison on first AUTH only
- connState gains authenticated flag (true when no password set)
- Auth guard rejects non-MsgPing commands before authentication
- Client.Auth() method for both internal/network and public client
- Full test coverage: auth flow, wrong/correct password, username,
  no-password no-op, malformed payload, per-connection isolation
- Add bench:go task to Taskfile for Go benchmark runs

Signed-off-by: Maximilian Hagen <git@saxy.dev>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Saxy, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cc9cd5f2-3f5d-474e-9250-34603b1ff9ff

📥 Commits

Reviewing files that changed from the base of the PR and between 5793202 and 88024b6.

📒 Files selected for processing (1)
  • internal/network/server.go
📝 Walkthrough

Walkthrough

Adds binary-protocol password authentication across protocol constants, clients, server connection handling, configuration wiring, and integration tests. It also adds a configurable Go benchmark task and updates constructor call sites.

Changes

Binary protocol authentication

Layer / File(s) Summary
Authentication protocol and client API
internal/network/protocol.go, internal/network/client.go, client/client.go
Adds AUTH message types, authentication error responses, and client methods that serialize credentials and validate authentication responses.
Server authentication enforcement
internal/network/server.go, server/server.go
Adds bcrypt-backed password configuration, per-connection authentication state, AUTH parsing, plaintext/TLS request gating, failed-attempt handling, worker shutdown, and configuration wiring.
Authentication coverage and constructor updates
internal/network/server_test.go, internal/network/*_test.go, internal/metrics/metrics_test.go
Tests authentication flows, no-password behavior, usernames, malformed payloads, connection isolation, deletion handling, and updated server constructor calls.

Go benchmark task

Layer / File(s) Summary
Configurable Go benchmark command
Taskfile.yml
Adds bench:go with configurable package, benchmark selector, and run count variables.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClientAPI
  participant NetworkClient
  participant NetworkServer
  participant AuthWorker
  participant ApplicationHandler
  ClientAPI->>NetworkClient: Auth(password, scratchBuf)
  NetworkClient->>NetworkServer: Send MsgAuth credentials
  NetworkServer->>AuthWorker: Dispatch bcrypt verification
  AuthWorker-->>NetworkServer: Write MsgAuthOk or MsgAuthErr
  ClientAPI->>NetworkClient: Send application request
  NetworkClient->>NetworkServer: Send authenticated frame
  NetworkServer->>ApplicationHandler: Forward request
  ApplicationHandler-->>ClientAPI: Return protocol response
Loading

Possibly related PRs

  • Saxy/Tellstone#17: Wires the shared require-pass configuration into the RESP authentication path while this PR adds it to the binary protocol path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding binary protocol AUTH support.
Description check ✅ Passed It fills the required template sections with component, change type, related issue, implementation details, benchmarks, testing, and checklist.
Linked Issues check ✅ Passed The PR adds MsgAuth and auth state, bcrypt verification, client Auth, default-user handling, ping bypass, and no-password compatibility for #8.
Out of Scope Changes check ✅ Passed The added benchmark task and test updates support the AUTH feature and do not introduce unrelated functional scope.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/binary-auth

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/network/server_test.go (1)

168-326: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add end-to-end coverage for TLS authentication.

These tests exercise only plaintext connections, while TLS uses an independent authentication-gating branch. Add a TLS case covering pre-auth rejection and successful AUTH.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/network/server_test.go` around lines 168 - 326, Extend the
authentication tests with a TLS end-to-end case using the existing
startAuthServer and connection helpers, configuring TLS and dialing through a
TLS client. Verify a protected request before AUTH returns MsgAuthErr, then
authenticate with the correct password and verify MsgAuthOk followed by a
successful request, covering the TLS-specific authentication gate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/network/server_test.go`:
- Around line 56-58: Update the fake DELETE handler’s OpDelete case to remove
the key from the store rather than storing nil. Use the store’s existing
deletion operation, preserving the current successful response behavior.

In `@internal/network/server.go`:
- Around line 247-248: Update the MsgAuth handling in OnTraffic and processAuth
so bcrypt verification never runs synchronously on the gnet event loop; dispatch
authentication checks through a bounded worker pool or rate limiter, then return
results to the connection safely. Track failed AUTH attempts and enforce a limit
by throttling or closing clients after repeated failures, while preserving
successful authentication behavior for both plaintext and TLS paths.

---

Nitpick comments:
In `@internal/network/server_test.go`:
- Around line 168-326: Extend the authentication tests with a TLS end-to-end
case using the existing startAuthServer and connection helpers, configuring TLS
and dialing through a TLS client. Verify a protected request before AUTH returns
MsgAuthErr, then authenticate with the correct password and verify MsgAuthOk
followed by a successful request, covering the TLS-specific authentication gate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d2bcc9e3-a203-45cd-8d1b-78c122e81dde

📥 Commits

Reviewing files that changed from the base of the PR and between 9608176 and ed1832e.

📒 Files selected for processing (10)
  • Taskfile.yml
  • client/client.go
  • internal/metrics/metrics_test.go
  • internal/network/benchmark_tls_test.go
  • internal/network/client.go
  • internal/network/protocol.go
  • internal/network/protocol_test.go
  • internal/network/server.go
  • internal/network/server_test.go
  • server/server.go

Comment thread internal/network/server_test.go
Comment thread internal/network/server.go Outdated
Saxy added 3 commits July 30, 2026 07:32
Move bcrypt.CompareHashAndPassword off the gnet event loop by
dispatching AUTH verification through a bounded goroutine pool,
preventing bcrypt's slow path from blocking all connections on
the event-loop goroutine.

Signed-off-by: Maximilian Hagen <git@saxy.dev>
…re closing channel

Signed-off-by: Maximilian Hagen <git@saxy.dev>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/network/server.go (1)

272-301: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate AUTH-handling logic between TLS and plaintext paths.

The msg.Type == MsgAuth block (fast-OK on no password, username fast-reject, dispatchAuth + pending handling) is duplicated almost verbatim in handleDecryptedFrames (Lines 272-301) and onTrafficPlaintext (Lines 383-412), differing only in how the "pending" outcome is signaled to the caller. Extracting a shared helper (e.g. s.handleAuthMessage(msg) (payload []byte, respType MessageType, pending bool)) would prevent the two copies drifting apart on future auth changes (e.g. one path getting a fix the other misses).

♻️ Suggested extraction shape
func (s *Server) tryAuth(c gnet.Conn, st *connState, msg *Message) (payload []byte, respType MessageType, pending, skipHandler bool) {
	if s.requirePassHash == nil {
		return ResponseOK, MsgAuthOk, false, true
	}
	username, password := parseAuthPayload(msg.Value)
	if len(username) > 0 && string(username) != "default" {
		return s.authFailed(st), MsgAuthErr, false, true
	}
	passwordCopy := make([]byte, len(password))
	copy(passwordCopy, password)
	if s.dispatchAuth(c, passwordCopy) {
		st.authPending = true
		return nil, 0, true, true
	}
	return s.authFailed(st), MsgAuthErr, false, true
}

Both call sites then branch on pending to decide whether to advance/discard or break/return gnet.None.

Also applies to: 383-412

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/network/server.go` around lines 272 - 301, Extract the duplicated
AUTH processing from handleDecryptedFrames and onTrafficPlaintext into a shared
Server helper that handles password bypass, username rejection, password
copying, dispatchAuth, and authPending state, returning response values plus a
pending indicator. Replace both inline msg.Type == MsgAuth branches with calls
to this helper, preserving each caller’s existing pending control flow and
skip-handler behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/network/server.go`:
- Around line 494-518: Update the AUTH success and failure response writes in
the surrounding authentication flow to capture and handle Write errors
consistently with the other response paths, closing the connection when a write
fails. Also record the bytes written in s.bytesWritten and the per-shard
AddBytesWritten metric for both MsgAuthOk and MsgAuthErr writes, preserving the
TLS-versus-plain connection selection.
- Around line 525-534: Update both authentication call sites using dispatchAuth
so a false result from worker-pool saturation is not passed to authFailed or
counted as a credential failure; instead use the existing retry/rejection
behavior appropriate for unavailable capacity. Also update parseAuthPayload to
distinguish malformed payloads from valid empty/default credentials, and
fast-reject malformed AUTH frames before dispatching bcrypt work while
preserving normal handling for valid credentials.
- Around line 500-519: The failed AUTH response path in the auth worker must
wake the connection so buffered frames are processed immediately. In the branch
handling failed attempts around st.authFails and st.closeAfterReply, call
c.Wake(nil) after writing the failure response when the connection is not being
closed; preserve the existing close behavior for st.closeAfterReply and match
the successful AUTH response path.

---

Nitpick comments:
In `@internal/network/server.go`:
- Around line 272-301: Extract the duplicated AUTH processing from
handleDecryptedFrames and onTrafficPlaintext into a shared Server helper that
handles password bypass, username rejection, password copying, dispatchAuth, and
authPending state, returning response values plus a pending indicator. Replace
both inline msg.Type == MsgAuth branches with calls to this helper, preserving
each caller’s existing pending control flow and skip-handler behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c61d266-4924-4e92-86dc-0bf71e3ca7b6

📥 Commits

Reviewing files that changed from the base of the PR and between f631bbb and 178757e.

📒 Files selected for processing (1)
  • internal/network/server.go

Comment thread internal/network/server.go Outdated
Comment thread internal/network/server.go
Comment thread internal/network/server.go
Signed-off-by: Maximilian Hagen <git@saxy.dev>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/network/server.go (1)

486-488: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Gate the auth worker pool on requirePass.

NewServer creates authJobs and starts the four authWorker goroutines even when requirePass is empty, so an auth-disabled server still owns idle goroutines/channel infrastructure for its lifetime. Skip both allocations/spawns unless requirePass != "".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/network/server.go` around lines 486 - 488, Update NewServer and the
authWorker startup path so authJobs is allocated and authWorker goroutines are
spawned only when requirePass is non-empty; leave the existing worker behavior
unchanged when password authentication is enabled.
🧹 Nitpick comments (1)
internal/network/server.go (1)

486-548: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Write-error handling, byte metrics, and post-failure Wake now resolve prior feedback.

authWorker now checks writeErr and closes on failure, updates s.bytesWritten/per-shard metrics for the AUTH response, and calls c.Wake(nil) uniformly after both success and failure paths (guarded by closeAfterReply) — this addresses the previously flagged missing metrics/write-error handling and the missing wake-on-failure re-trigger.

Separately: the failure branch (lines 507-518) re-implements authFails++ / warn-log / closeAfterReply threshold logic inline, while handleAuthMessage (Line 472) calls a separate s.authFailed(st) helper that — per the change description — does the same accounting. Having two independent implementations of the same failure-counting/threshold policy risks them drifting (e.g., if maxAuthFails handling or log fields are updated in one but not the other).

♻️ Suggested consolidation
 		} else {
-			st.authFails++
-			respPayload, respType = ResponseAuthErr, MsgAuthErr
-			if s.logger.Enabled(log.LevelWarn) {
-				s.logger.Log(log.LevelWarn, "network: failed AUTH attempt",
-					log.String("remote_addr", st.remoteAddr),
-					log.Int("attempts", st.authFails),
-				)
-			}
-			if st.authFails >= maxAuthFails {
-				st.closeAfterReply = true
-			}
+			respPayload = s.authFailed(st)
+			respType = MsgAuthErr
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/network/server.go` around lines 486 - 548, Consolidate the
failed-authentication handling in authWorker by reusing the existing authFailed
helper instead of duplicating authFails incrementing, warning logs, and
maxAuthFails closeAfterReply logic inline. Preserve the existing failure
response selection and ensure authFailed remains the single source of truth for
failure accounting and threshold behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/network/server.go`:
- Around line 375-382: Handle the error returned by c.Discard in the MsgAuth
dispatched branch of the server message loop, matching the existing
discard-error handling used near the other loop path: increment s.protocolErrors
and emit the corresponding warning when Discard fails, while preserving the
current return behavior.

---

Outside diff comments:
In `@internal/network/server.go`:
- Around line 486-488: Update NewServer and the authWorker startup path so
authJobs is allocated and authWorker goroutines are spawned only when
requirePass is non-empty; leave the existing worker behavior unchanged when
password authentication is enabled.

---

Nitpick comments:
In `@internal/network/server.go`:
- Around line 486-548: Consolidate the failed-authentication handling in
authWorker by reusing the existing authFailed helper instead of duplicating
authFails incrementing, warning logs, and maxAuthFails closeAfterReply logic
inline. Preserve the existing failure response selection and ensure authFailed
remains the single source of truth for failure accounting and threshold
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 89ca737d-d155-4760-8dde-9714178a00a0

📥 Commits

Reviewing files that changed from the base of the PR and between 178757e and 5793202.

📒 Files selected for processing (1)
  • internal/network/server.go

Comment thread internal/network/server.go
Signed-off-by: Maximilian Hagen <git@saxy.dev>
@Saxy
Saxy merged commit ce5bb5c into main Jul 30, 2026
9 checks passed
@Saxy
Saxy deleted the feat/binary-auth branch July 30, 2026 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AUTH Handshake for Binary Protocol

1 participant