|
| 1 | +# Agent Development Guide |
| 2 | + |
| 3 | +This guide provides coding agents with essential information for working with the tailrelay codebase. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +**tailrelay** is a Docker container that combines Tailscale, Caddy, and socat to expose local services (particularly Start9 services) to a Tailscale network. The project includes: |
| 8 | + |
| 9 | +- Docker image building with Tailscale, Caddy, and socat |
| 10 | +- Shell script orchestration for service startup |
| 11 | +- Python-based integration testing |
| 12 | +- Docker Compose for development and testing |
| 13 | + |
| 14 | +## Build, Test & Development Commands |
| 15 | + |
| 16 | +### Build Docker Image |
| 17 | + |
| 18 | +```bash |
| 19 | +# Build development image |
| 20 | +docker buildx build -t sudocarlos/tailrelay:dev --load . |
| 21 | + |
| 22 | +# Build production image (with version tag) |
| 23 | +docker buildx build -t sudocarlos/tailrelay:latest . |
| 24 | +``` |
| 25 | + |
| 26 | +### Run Tests |
| 27 | + |
| 28 | +```bash |
| 29 | +# Full integration test suite (Python) |
| 30 | +python docker-compose-test.py |
| 31 | + |
| 32 | +# Full integration test suite (Bash) |
| 33 | +./docker-compose-test.sh |
| 34 | + |
| 35 | +# Prerequisites: Create .env file from template |
| 36 | +cp .env.example .env |
| 37 | +# Edit .env with your TAILRELAY_HOST, TAILNET_DOMAIN, COMPOSE_FILE |
| 38 | +``` |
| 39 | + |
| 40 | +### Development Environment |
| 41 | + |
| 42 | +```bash |
| 43 | +# Start test environment |
| 44 | +docker compose -f compose-test.yml up -d |
| 45 | + |
| 46 | +# View logs |
| 47 | +docker compose -f compose-test.yml logs tailrelay-test |
| 48 | + |
| 49 | +# Stop test environment |
| 50 | +docker compose -f compose-test.yml down |
| 51 | + |
| 52 | +# Check listening ports |
| 53 | +docker exec -it tailrelay-test netstat -tulnp | grep LISTEN |
| 54 | + |
| 55 | +# Manual health checks |
| 56 | +curl -sSL http://tailrelay-dev:8080 # Test HTTP proxy |
| 57 | +curl -sSL http://tailrelay-dev:9002/healthz # Health endpoint |
| 58 | +curl -sSL http://tailrelay-dev:9002/metrics # Metrics endpoint |
| 59 | +``` |
| 60 | + |
| 61 | +### Running Single Tests |
| 62 | + |
| 63 | +Since this project uses integration tests rather than unit tests, there's no single test runner. Tests are curl-based health checks defined in the test scripts: |
| 64 | + |
| 65 | +```bash |
| 66 | +# Run a single health check manually |
| 67 | +curl -sSL http://${TAILRELAY_HOST}:8080 && echo success || echo fail |
| 68 | +curl -sSL http://${TAILRELAY_HOST}:9002/healthz && echo success || echo fail |
| 69 | +``` |
| 70 | + |
| 71 | +## Code Style Guidelines |
| 72 | + |
| 73 | +### Shell Scripts (Bash/sh) |
| 74 | + |
| 75 | +**File Extensions & Shebangs:** |
| 76 | +- Use `.sh` extension for shell scripts |
| 77 | +- Use `#!/usr/bin/env bash` for Bash scripts |
| 78 | +- Use `#!/bin/ash` for Alpine Linux scripts (Dockerfile entrypoint) |
| 79 | + |
| 80 | +**Style Conventions:** |
| 81 | +- Use 4-space indentation (not tabs) |
| 82 | +- Variable names in UPPER_SNAKE_CASE for environment variables |
| 83 | +- Variable names in lower_snake_case for local variables |
| 84 | +- Use `${VAR}` syntax for variable expansion (prefer braces) |
| 85 | +- Quote all variables: `"$VAR"` unless word splitting is intended |
| 86 | +- Use `set -e` for error handling (fail fast) |
| 87 | +- Use `set -x` for debugging (show commands) |
| 88 | + |
| 89 | +**Error Handling:** |
| 90 | +```bash |
| 91 | +# Check command exit codes |
| 92 | +if [ $? -ne 0 ]; then |
| 93 | + echo "failed!" |
| 94 | + exit 1 |
| 95 | +fi |
| 96 | + |
| 97 | +# Alternative with conditional execution |
| 98 | +command || { echo "failed!"; exit 1; } |
| 99 | +``` |
| 100 | + |
| 101 | +**Comments:** |
| 102 | +- Use `#` for single-line comments |
| 103 | +- Add descriptive comments for complex logic |
| 104 | +- Document environment variables at the top of scripts |
| 105 | + |
| 106 | +### Python Scripts |
| 107 | + |
| 108 | +**Imports:** |
| 109 | +- Standard library imports first |
| 110 | +- Third-party imports second |
| 111 | +- Separate groups with blank lines |
| 112 | +- Alphabetically sorted within groups when practical |
| 113 | + |
| 114 | +```python |
| 115 | +import subprocess |
| 116 | +import time |
| 117 | +import sys |
| 118 | +from pathlib import Path |
| 119 | +from typing import List, Tuple |
| 120 | + |
| 121 | +from dotenv import load_dotenv |
| 122 | +``` |
| 123 | + |
| 124 | +**Type Hints:** |
| 125 | +- Use type hints for function parameters and return values |
| 126 | +- Use `typing` module types: `List`, `Tuple`, `Dict`, etc. |
| 127 | +- Example: `def run(cmd: str, *, capture_output=False) -> Tuple[int, str, str]:` |
| 128 | + |
| 129 | +**Naming Conventions:** |
| 130 | +- Variables and functions: `lower_snake_case` |
| 131 | +- Constants: `UPPER_SNAKE_CASE` |
| 132 | +- Classes: `PascalCase` |
| 133 | +- Private functions/vars: prefix with `_` |
| 134 | + |
| 135 | +**String Formatting:** |
| 136 | +- Prefer f-strings for string interpolation |
| 137 | +- Example: `f"❌ Build failed:\n{err}"` |
| 138 | + |
| 139 | +**Error Handling:** |
| 140 | +- Use try-except blocks for subprocess timeouts |
| 141 | +- Return error codes instead of raising exceptions when appropriate |
| 142 | +- Provide descriptive error messages with emoji indicators (✅, ❌, ⚠️) |
| 143 | + |
| 144 | +**Docstrings:** |
| 145 | +- Use triple-quoted strings for function documentation |
| 146 | +- Include parameter and return value descriptions |
| 147 | + |
| 148 | +```python |
| 149 | +def run(cmd: str, *, capture_output=False, timeout=None) -> Tuple[int, str, str]: |
| 150 | + """Run a shell command and return (returncode, stdout, stderr). |
| 151 | + If the process times out, return rc=124 and a timeout message instead of raising.""" |
| 152 | +``` |
| 153 | + |
| 154 | +### Dockerfile |
| 155 | + |
| 156 | +**Best Practices:** |
| 157 | +- Use `ARG` for build-time variables |
| 158 | +- Use `ENV` for runtime variables |
| 159 | +- Combine `RUN` commands to reduce layers |
| 160 | +- Use `--no-cache` with apk/apt for smaller images |
| 161 | +- Add `LABEL` for maintainer information |
| 162 | +- Use multi-stage builds when appropriate |
| 163 | + |
| 164 | +**Version Pinning:** |
| 165 | +- Pin versions for base images: `tailscale/tailscale:$TAILSCALE_VERSION` |
| 166 | +- Pin versions for Alpine packages when stability is critical |
| 167 | + |
| 168 | +### Caddyfile Configuration |
| 169 | + |
| 170 | +**Style:** |
| 171 | +- Use tabs for indentation (Caddy convention) |
| 172 | +- One site block per listening address |
| 173 | +- Group related directives together |
| 174 | +- Always specify full domain with port: `host.domain.ts.net:port` |
| 175 | + |
| 176 | +**Common Patterns:** |
| 177 | +``` |
| 178 | +hostname.tailnet.ts.net:port { |
| 179 | + reverse_proxy target:port { |
| 180 | + header_up Host {upstream_hostport} |
| 181 | + trusted_proxies private_ranges |
| 182 | + } |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +## Environment Variables |
| 187 | + |
| 188 | +**Required Variables:** |
| 189 | +- `TS_HOSTNAME` - Tailscale machine name (must match Caddyfile) |
| 190 | +- `TS_STATE_DIR` - Tailscale state directory (default: `/var/lib/tailscale/`) |
| 191 | + |
| 192 | +**Optional Variables:** |
| 193 | +- `RELAY_LIST` - Comma-separated socat relay definitions: `port:host:port` |
| 194 | + - Example: `50001:electrs.embassy:50001,21004:lnd.embassy:10009` |
| 195 | +- `TS_EXTRA_FLAGS` - Additional Tailscale flags |
| 196 | +- `TS_AUTH_ONCE` - Authenticate once (default: `true`) |
| 197 | +- `TS_ENABLE_METRICS` - Enable metrics endpoint (default: `true`) |
| 198 | +- `TS_ENABLE_HEALTH_CHECK` - Enable health check endpoint (default: `true`) |
| 199 | + |
| 200 | +**Test Environment Variables (.env file):** |
| 201 | +- `TAILRELAY_HOST` - Test container hostname |
| 202 | +- `TAILNET_DOMAIN` - Tailscale domain for testing |
| 203 | +- `COMPOSE_FILE` - Path to docker-compose test file |
| 204 | + |
| 205 | +## File Structure |
| 206 | + |
| 207 | +``` |
| 208 | +. |
| 209 | +├── Dockerfile # Multi-service container definition |
| 210 | +├── start.sh # Container entrypoint script |
| 211 | +├── Caddyfile.example # Example Caddy configuration |
| 212 | +├── docker-compose-test.py # Python integration tests |
| 213 | +├── docker-compose-test.sh # Bash integration tests |
| 214 | +├── compose-test.yml # Docker Compose test configuration |
| 215 | +├── requirements.txt # Python dependencies (python-dotenv) |
| 216 | +├── .env.example # Environment variable template |
| 217 | +└── README.md # User documentation |
| 218 | +``` |
| 219 | + |
| 220 | +## Testing Strategy |
| 221 | + |
| 222 | +**Integration Tests:** |
| 223 | +- Build Docker image with dev tag |
| 224 | +- Start containers with docker-compose |
| 225 | +- Wait for services to initialize (3 second delay) |
| 226 | +- Run health checks via curl |
| 227 | +- Verify listening ports |
| 228 | +- Clean up containers |
| 229 | + |
| 230 | +**Health Check Endpoints:** |
| 231 | +- HTTP proxy: `:8080`, `:8081` (proxied services) |
| 232 | +- HTTPS with TLS: `:8443` |
| 233 | +- Tailscale health: `:9002/healthz` |
| 234 | +- Tailscale metrics: `:9002/metrics` |
| 235 | + |
| 236 | +## Common Pitfalls & Notes |
| 237 | + |
| 238 | +1. **File Persistence**: Start9 removes files on reboot - always backup `/home/start9/tailscale` |
| 239 | +2. **Hostname Matching**: `TS_HOSTNAME` must match the hostname in Caddyfile |
| 240 | +3. **Tailnet Domain**: Must be exact Tailnet name from Tailscale admin console |
| 241 | +4. **RELAY_LIST Format**: Strict format `port:host:port` - parsing is fragile |
| 242 | +5. **Docker Network**: Use `--net start9` for Start9 deployments |
| 243 | +6. **TLS Certificates**: Require HTTPS enabled in Tailscale admin console |
| 244 | +7. **Container Execution**: Uses `exec` to replace shell with containerboot (PID 1 handling) |
| 245 | + |
| 246 | +## Version Information |
| 247 | + |
| 248 | +- Current Version: `v0.1.1` (defined in Dockerfile ARG and start.sh) |
| 249 | +- Tailscale Base: `v1.92.5` (Dockerfile ARG) |
| 250 | +- Alpine Linux with Caddy and socat |
| 251 | + |
| 252 | +## Making Changes |
| 253 | + |
| 254 | +When modifying the project: |
| 255 | + |
| 256 | +1. Update version in both `Dockerfile` ARG and `start.sh` |
| 257 | +2. Test with both Python and Bash test scripts |
| 258 | +3. Verify all health check endpoints respond |
| 259 | +4. Update README.md if user-facing changes |
| 260 | +5. Rebuild image before testing: `docker buildx build` |
| 261 | +6. Test in isolated network: use compose-test.yml |
0 commit comments