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
26 changes: 26 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
62 changes: 62 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -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?
22 changes: 22 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## Summary

<!-- Brief description of what this PR does and why -->

## 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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ logs/
# AI assistants
.claude/
.cursor/
CLAUDE.md
GEMINI.md
AGENTS.md
.aider*
Expand Down
42 changes: 42 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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
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
60 changes: 60 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,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" {
Expand All @@ -277,7 +277,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)
}
},
Expand Down Expand Up @@ -389,7 +389,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)
}
},
Expand Down
Loading