diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1889f54..ac1fb15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,9 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v4 with: - version: latest + # Pinned so local `golangci-lint run` and CI report identical findings. + # Bump deliberately when the project upgrades; do not silently track latest. + version: v1.62.2 install-mode: goinstall security: diff --git a/CLAUDE.md b/CLAUDE.md index 989bb32..a8a2ea9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Portable TCP bridge over Tailscale/Headscale mesh networks using tsnet. - **Language:** Go 1.25+ - **Key dependency:** `tailscale.com/tsnet` v1.80.0 -- **Architecture:** Single-binary, single-file (`main.go` ~785 lines) +- **Architecture:** Single binary, multi-package — `main.go` (~270 lines orchestrator) + four `internal/` packages (see ADR-007). - **Config:** Environment variables only (no config files) — see `.env.example` - **Logging:** `log/slog` (structured, text or JSON) - **Metrics:** `sync/atomic` counters, JSON endpoint at `/metrics` @@ -15,14 +15,20 @@ Portable TCP bridge over Tailscale/Headscale mesh networks using tsnet. | Path | Purpose | |------|---------| -| `main.go` | All application code | -| `main_test.go` | Unit tests | -| `main_integration_test.go` | Integration tests (loopback, no tsnet) | -| `.env.example` | Configuration reference (2 required vars) | -| `scripts/client/` | Client launchers (run.sh, run.ps1, bootstrap) | -| `scripts/host/` | Host setup (setup.ps1, ts-bridge.service) | -| `.github/workflows/ci.yml` | CI pipeline (test, lint, security, shellcheck, build-matrix) | -| `.github/workflows/release.yml` | Automated releases via release-please | +| `main.go` | Orchestrator: flags, logger, signals, init, drain. Thin glue, no business logic. | +| `main_test.go` | Unit tests for main-package helpers (error diagnosis, etc.) | +| `main_integration_test.go` | Integration tests with mock Dialer (loopback, no tsnet) | +| `internal/config/` | Env-var parsing + `Config` struct + validation | +| `internal/proxy/` | `Dialer` interface, `AcceptLoop`, `handleConn`, `proxyConnections`, `idleConn`, `ReconnectDialer` | +| `internal/health/` | `/health/live`, `/health/ready`, `/metrics` HTTP server | +| `internal/telemetry/` | Atomic counters + read accessors | +| `specs/` (and `specs/archive/`) | Per-feature SDD folders (proposal + tasks + verification) — see §Workflow Rules | +| `.env.example` | Configuration reference (2 required vars + commented optionals) | +| `scripts/client/` | Client launchers (`run.sh`, `run.ps1`, `bootstrap.{sh,ps1}`) | +| `scripts/host/` | Host setup (`setup.ps1`, `ts-bridge.service`) | +| `scripts/tests/` | BATS tests for the launchers | +| `.github/workflows/ci.yml` | CI: test, lint, security (gosec), shellcheck, bats, build-matrix | +| `.github/workflows/release.yml` | Automated releases via release-please (PAT-driven) | ## Commands @@ -33,7 +39,8 @@ go build -o ts-bridge . # Test (always use race detector) go test -race -v ./... -# Lint +# Lint (CI pins this version — run the same locally to avoid drift) +go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 golangci-lint run # Security scan @@ -47,6 +54,8 @@ gosec ./... - **ADR-002:** Single binary, no config files, env-var driven - **ADR-004:** Atomic metrics, no mutexes +- **ADR-006:** `Dialer` interface for testability +- **ADR-007:** Multi-package split under `internal/` (this is the current layout — see Key Paths) - Full ADR index in vault: `knowledge/10_projects/ts-bridge/30-architecture/` ## Workflow Rules (read before first tool call) diff --git a/internal/proxy/reconnect.go b/internal/proxy/reconnect.go index 548b559..eefe715 100644 --- a/internal/proxy/reconnect.go +++ b/internal/proxy/reconnect.go @@ -121,13 +121,8 @@ func isPermanentDialError(err error) bool { return true } - msg := err.Error() // tsnet emits this string when the backend is in a terminal state // (Stopped, NeedsMachineAuth). Source: tsnet.Server.awaitRunning at // tailscale.com/tsnet@v1.80.0/tsnet.go:203. - if strings.HasPrefix(msg, "tsnet: backend in state ") { - return true - } - - return false + return strings.HasPrefix(err.Error(), "tsnet: backend in state ") }