Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
29 changes: 19 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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

Expand All @@ -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/[email protected]
golangci-lint run

# Security scan
Expand All @@ -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)
Expand Down
7 changes: 1 addition & 6 deletions internal/proxy/reconnect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]/tsnet.go:203.
if strings.HasPrefix(msg, "tsnet: backend in state ") {
return true
}

return false
return strings.HasPrefix(err.Error(), "tsnet: backend in state ")
}
Loading