From 41a6530807eb85350ed389954d25c9a947bcace0 Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 7 Mar 2026 19:03:54 -0700 Subject: [PATCH 1/2] chore: add repo best practices (templates, linter config, editor config) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add PR template and YAML issue forms (bug report, feature request) - Add SECURITY.md with vulnerability reporting process - Add CLAUDE.md with project-level instructions for Claude Code - Add .golangci.yml to pin linter configuration - Add .editorconfig for cross-editor consistency - Fix stale Go version in CONTRIBUTING.md (1.21+ → 1.25+) - Unignore CLAUDE.md from .gitignore (project instructions should be tracked) --- .editorconfig | 26 +++++++++ .github/ISSUE_TEMPLATE/bug_report.yml | 62 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 23 ++++++++ .github/pull_request_template.md | 22 ++++++++ .gitignore | 1 - .golangci.yml | 35 ++++++++++++ CLAUDE.md | 60 +++++++++++++++++++++ CONTRIBUTING.md | 2 +- SECURITY.md | 22 ++++++++ 10 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/pull_request_template.md create mode 100644 .golangci.yml create mode 100644 CLAUDE.md create mode 100644 SECURITY.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b30fa2a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true + +[*.go] +indent_style = tab + +[*.{yml,yaml,json,md}] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab + +[*.sh] +indent_style = space +indent_size = 2 + +[*.ps1] +indent_style = space +indent_size = 4 +end_of_line = crlf diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..eda4f03 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,62 @@ +name: Bug Report +description: Report a bug or unexpected behavior +labels: ["bug"] +body: + - type: textarea + id: description + attributes: + label: Description + description: What happened? + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: How can we reproduce this? + value: | + 1. + 2. + 3. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What should have happened instead? + validations: + required: true + - type: input + id: version + attributes: + label: ts-bridge Version + description: "Output of `ts-bridge -version`" + placeholder: "v1.3.1 (abc1234)" + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating System + options: + - Windows + - Linux + - macOS + validations: + required: true + - type: dropdown + id: control-plane + attributes: + label: Control Plane + options: + - Tailscale (SaaS) + - Headscale (self-hosted) + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs + description: "Run with `-v` flag and paste relevant output" + render: text diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..330c156 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://mlorentedev.github.io/ts-bridge/ + about: Check the docs before opening an issue diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..6967bb1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,23 @@ +name: Feature Request +description: Suggest a new feature or improvement +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What problem does this solve? + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: How should it work? + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: What other approaches did you consider? diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..41d1b9e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,22 @@ +## Summary + + + +## Changes + +- + +## Testing + +- [ ] `go test -race ./...` passes +- [ ] `go vet ./...` clean +- [ ] Tested manually (if applicable) + +## Type + +- [ ] `feat` — New feature +- [ ] `fix` — Bug fix +- [ ] `refactor` — Code restructuring (no behavior change) +- [ ] `docs` — Documentation only +- [ ] `test` — Test additions/changes +- [ ] `chore` — Maintenance diff --git a/.gitignore b/.gitignore index 2a21577..98a0b8d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,6 @@ logs/ # AI assistants .claude/ .cursor/ -CLAUDE.md GEMINI.md AGENTS.md .aider* diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..38a6e44 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,35 @@ +run: + timeout: 5m + +linters: + enable: + - errcheck + - govet + - ineffassign + - staticcheck + - unused + - gosec + - goconst + - gocyclo + - misspell + - prealloc + - unconvert + - unparam + +linters-settings: + gocyclo: + min-complexity: 15 + goconst: + min-len: 3 + min-occurrences: 3 + gosec: + excludes: + - G117 + misspell: + locale: US + +issues: + exclude-dirs: + - site + max-issues-per-linter: 50 + max-same-issues: 5 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..866c9b6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,60 @@ +# ts-bridge + +Portable TCP bridge over Tailscale/Headscale mesh networks using tsnet. + +## Tech Stack + +- **Language:** Go 1.25+ +- **Key dependency:** `tailscale.com/tsnet` v1.80.0 +- **Architecture:** Single-binary, single-file (`main.go` ~785 lines) +- **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` + +## Key Paths + +| 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 | + +## Commands + +```sh +# Build +go build -o ts-bridge . + +# Test (always use race detector) +go test -race -v ./... + +# Lint +golangci-lint run + +# Security scan +gosec ./... + +# Run in dev mode +./scripts/dev.sh +``` + +## Architecture Decisions + +- **ADR-002:** Single binary, no config files, env-var driven +- **ADR-004:** Atomic metrics, no mutexes +- Full ADR index in vault: `knowledge/10_projects/ts-bridge/30-architecture/` + +## Conventions + +- Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`) +- GitHub Flow — all work via feature branches + PRs against `master` +- TDD — write failing tests first +- Table-driven tests with `t.Run` subtests +- Functions < 40 lines, cyclomatic complexity < 10 +- Error wrapping: `fmt.Errorf("context: %w", err)` +- No new dependencies without strong justification (zero-dep design goal) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 952240b..e492421 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ This project follows **GitHub Flow**: `master` is the protected default branch. ### Prerequisites -- Go 1.21+ +- Go 1.25+ - A Tailscale account (for integration testing) ### Quick Start diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..39e65d0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in ts-bridge, please report it responsibly. + +**Do not open a public issue.** Instead, email the maintainer directly or use [GitHub's private vulnerability reporting](https://github.com/mlorentedev/ts-bridge/security/advisories/new). + +## Scope + +ts-bridge handles sensitive data (Tailscale auth keys, network tunnels). The following are in scope: + +- Auth key leakage (logs, error messages, process environment) +- Unauthorized tunnel access or connection hijacking +- Denial of service via resource exhaustion +- State directory permission issues + +## Response Timeline + +- **Acknowledgment:** within 48 hours +- **Assessment:** within 7 days +- **Fix release:** as soon as practical, coordinated with reporter From 25ef3ec5eb618c0eeac9c3bd1a2364e065522832 Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 7 Mar 2026 19:10:40 -0700 Subject: [PATCH 2/2] fix: resolve golangci-lint issues (goconst, gocyclo, unparam) - Replace hardcoded "127.0.0.1:33389" with defaultLocalAddr constant in tests - Exclude test files from gocyclo (table-driven tests are legitimately complex) - Suppress unparam for acceptLoop (returns nil by design on clean shutdown) --- .golangci.yml | 7 +++++++ main_test.go | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 38a6e44..2254ba1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -31,5 +31,12 @@ linters-settings: issues: exclude-dirs: - site + exclude-rules: + - path: _test\.go + linters: + - gocyclo + - linters: + - unparam + text: "acceptLoop.*result 0.*is always nil" max-issues-per-linter: 50 max-same-issues: 5 diff --git a/main_test.go b/main_test.go index 403cb41..6526957 100644 --- a/main_test.go +++ b/main_test.go @@ -59,7 +59,7 @@ func TestLoadConfig(t *testing.T) { if cfg.AutoInstance { t.Error("expected manual mode to disable auto mode") } - if cfg.LocalAddr != "127.0.0.1:33389" { + if cfg.LocalAddr != defaultLocalAddr { t.Errorf("expected legacy local addr, got %s", cfg.LocalAddr) } if cfg.Hostname != "ts-bridge" { @@ -85,7 +85,7 @@ func TestLoadConfig(t *testing.T) { if cfg.AutoInstance { t.Error("expected explicit false auto flag to disable auto mode") } - if cfg.LocalAddr != "127.0.0.1:33389" { + if cfg.LocalAddr != defaultLocalAddr { t.Errorf("expected legacy local addr, got %s", cfg.LocalAddr) } }, @@ -197,7 +197,7 @@ func TestLoadConfig(t *testing.T) { if cfg.AutoInstance { t.Error("expected manual mode to take precedence over auto flag") } - if cfg.LocalAddr != "127.0.0.1:33389" { + if cfg.LocalAddr != defaultLocalAddr { t.Errorf("expected legacy local addr, got %s", cfg.LocalAddr) } },