mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 05:01:28 -10:00
* feat(daemon): TLS remote bridge — authenticated network access to the daemon Add internal/daemon/remote: an OPT-IN, TLS-only network bridge on top of the local daemon. A remote client drives the SAME SessionManager/Pool over the network behind bearer-token auth, reusing the daemon's framing and dispatch unchanged. The local Unix-socket daemon stays the default and is unchanged. - Bridge: per accepted TLS connection, authenticate THEN hand to the daemon's control dispatch via the new Server.ServeConn seam — a remote session runs under the same sandbox/risk model as a local one (never bypasses it). - Authenticator + TokenAuthenticator: constant-time bearer-token check, token from $ZERO_DAEMON_REMOTE_TOKEN or a file; optional attestation hook (no-op default). Auth happens before any control frame; failure closes the connection after a small backoff; tokens are never logged. - RemoteClient (DialRemote): verified TLS dial (never InsecureSkipVerify) + auth handshake, then the existing daemon.Client via the new NewClientConn seam, so remote and local share one protocol. - Fail-closed: TLS mandatory (refuses without cert/key), a configurable min-protocol-version floor, the 1 MiB frame cap reused, and a bounded concurrent-connection cap that refuses overflow. - CLI: `zero daemon serve-remote --addr --tls-cert --tls-key`; `run`/`attach` gain `--remote <addr> [--token --ca-cert --server-name]`. Daemon seams added (additive, behavior unchanged): Server.ServeConn, NewClientConn. SessionLink (event POST) and git-bundle repo transfer are documented follow-ups. Tests cover auth accept/reject (env + file), TLS-required, cert verification, version floor, run/status/attach round-trip, re-attach resumption, connection cap, and ListenAndServeTLS/Close. Gates: gofmt / vet / build (host+linux+ windows) / test -race / staticcheck (no new) / govulncheck (0) / deadcode (no new) all pass. * swarm: de-flake TestMailboxConcurrentSends on Windows CI The 200-way concurrent-send stress test inherited from the swarm package timed out on Windows CI (103/200 sends hit the 10s lock timeout): the lock's 20ms retry sleep starves waiters when Windows file ops are slow under heavy contention. Shorten the retry to 2ms (a freed lock is re-acquired promptly) and raise the test's lock timeout to 60s so a legitimately-slow CI never times a send out. Passes repeatedly under -race. (CI de-flake of a test that surfaced on #210's Windows smoke; functionally unrelated to the OAuth changes.) * remote: address CodeRabbit re-review (TLS dial + Close idempotency) - client: replace the deprecated, non-cancellation-aware tls.DialWithDialer with a context-aware tls.Dialer.DialContext (resolves the golangci-lint noctx CI blocker; the context bounds the dial + TLS handshake). - bridge: Close now clears b.listener under the lock, so a repeat Close is a no-op instead of returning a closed-listener error (matches its doc). Test asserts a double Close succeeds. Gates: gofmt/vet/build(host+linux+windows)/test -race/staticcheck(no new)/ govulncheck(0)/deadcode(no new) all pass.
68 lines
2.0 KiB
Go
68 lines
2.0 KiB
Go
package remote
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestTokenAuthenticator(t *testing.T) {
|
|
if _, err := NewTokenAuthenticator(" "); err == nil {
|
|
t.Fatal("empty token must be rejected (fail closed)")
|
|
}
|
|
a, err := NewTokenAuthenticator("s3cret")
|
|
if err != nil {
|
|
t.Fatalf("NewTokenAuthenticator: %v", err)
|
|
}
|
|
if err := a.Authenticate("s3cret"); err != nil {
|
|
t.Fatalf("matching token should authenticate: %v", err)
|
|
}
|
|
if err := a.Authenticate("wrong"); !errors.Is(err, ErrUnauthorized) {
|
|
t.Fatalf("mismatch err = %v, want ErrUnauthorized", err)
|
|
}
|
|
if err := a.Authenticate(""); !errors.Is(err, ErrUnauthorized) {
|
|
t.Fatalf("empty presented token err = %v, want ErrUnauthorized", err)
|
|
}
|
|
}
|
|
|
|
func TestTokenFromEnv(t *testing.T) {
|
|
// Clear both so the no-config path errors.
|
|
t.Setenv(EnvToken, "")
|
|
t.Setenv(EnvTokenFile, "")
|
|
if _, err := TokenFromEnv(); err == nil {
|
|
t.Fatal("TokenFromEnv with neither var set must error")
|
|
}
|
|
// Direct env.
|
|
t.Setenv(EnvToken, "from-env")
|
|
if tok, err := TokenFromEnv(); err != nil || tok != "from-env" {
|
|
t.Fatalf("TokenFromEnv(env) = %q, %v", tok, err)
|
|
}
|
|
// File (env takes precedence, so clear env first).
|
|
t.Setenv(EnvToken, "")
|
|
file := filepath.Join(t.TempDir(), "tok")
|
|
if err := os.WriteFile(file, []byte(" from-file\n"), 0o600); err != nil {
|
|
t.Fatalf("write token file: %v", err)
|
|
}
|
|
t.Setenv(EnvTokenFile, file)
|
|
if tok, err := TokenFromEnv(); err != nil || tok != "from-file" {
|
|
t.Fatalf("TokenFromEnv(file) = %q, %v", tok, err)
|
|
}
|
|
// Empty file fails closed.
|
|
if err := os.WriteFile(file, []byte(" \n"), 0o600); err != nil {
|
|
t.Fatalf("rewrite token file: %v", err)
|
|
}
|
|
if _, err := TokenFromEnv(); err == nil {
|
|
t.Fatal("empty token file must error")
|
|
}
|
|
}
|
|
|
|
func TestServerTLSConfigRequiresCertKey(t *testing.T) {
|
|
if _, err := ServerTLSConfig("", ""); err == nil {
|
|
t.Fatal("ServerTLSConfig must require a cert and key (TLS mandatory)")
|
|
}
|
|
if _, err := ServerTLSConfig("/nope/cert.pem", "/nope/key.pem"); err == nil {
|
|
t.Fatal("ServerTLSConfig must error on unreadable cert/key")
|
|
}
|
|
}
|