From d3bbce40c26778934713051f7f03faca5886e2e6 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 15 Mar 2026 06:49:58 +0100 Subject: [PATCH 1/5] docs: add open source documentation and community files Add missing documentation for open source readiness: - CODE_OF_CONDUCT.md (Contributor Covenant v2.1) - CHANGELOG.md (full version history from v0.0.1-beta to v0.1.9) - .github/ISSUE_TEMPLATE/bug_report.yml - .github/ISSUE_TEMPLATE/feature_request.yml - .github/PULL_REQUEST_TEMPLATE.md - docs/cli-reference.md (complete CLI reference) - docs/api-reference.md (REST API with curl examples) - docs/tunnel-security.md (JWT, HTTP auth, IP allowlisting) - docs/transports.md (QUIC vs WebSocket vs H2 guide) - docs/configuration.md (.localup.yml format and usage) - docs/reverse-tunnels.md (agent mode and private service access) Fix Cargo.toml repository URL to point to correct GitHub repo. --- .github/ISSUE_TEMPLATE/bug_report.yml | 107 ++++ .github/ISSUE_TEMPLATE/feature_request.yml | 69 +++ .github/PULL_REQUEST_TEMPLATE.md | 39 ++ CHANGELOG.md | 112 ++++ CODE_OF_CONDUCT.md | 56 ++ Cargo.toml | 2 +- docs/api-reference.md | 582 +++++++++++++++++++++ docs/cli-reference.md | 520 ++++++++++++++++++ docs/configuration.md | 285 ++++++++++ docs/reverse-tunnels.md | 266 ++++++++++ docs/transports.md | 194 +++++++ docs/tunnel-security.md | 219 ++++++++ 12 files changed, 2450 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 docs/api-reference.md create mode 100644 docs/cli-reference.md create mode 100644 docs/configuration.md create mode 100644 docs/reverse-tunnels.md create mode 100644 docs/transports.md create mode 100644 docs/tunnel-security.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..3ea9f00 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,107 @@ +name: Bug Report +description: Report a bug in localup +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug! Please fill out the sections below to help us reproduce and fix the issue. + + - type: input + id: version + attributes: + label: localup version + description: Run `localup --version` to get this + placeholder: "0.1.9" + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating System + options: + - macOS (Apple Silicon) + - macOS (Intel) + - Linux (x86_64) + - Linux (ARM64) + - Windows (x86_64) + - Other + validations: + required: true + + - type: dropdown + id: protocol + attributes: + label: Protocol + description: Which tunnel protocol are you using? + options: + - HTTP + - HTTPS + - TCP + - TLS/SNI + - Not applicable + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: A clear description of the bug + placeholder: What happened? What did you expect to happen? + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: How can we reproduce this issue? + placeholder: | + 1. Start relay with `localup relay --http-addr 0.0.0.0:18080` + 2. Create tunnel with `localup --port 3000 --protocol http` + 3. Send request to tunnel URL + 4. See error... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen? + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened? + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Logs + description: | + Relevant log output. Run with `--log-level debug` for verbose logs. + render: shell + + - type: textarea + id: config + attributes: + label: Configuration + description: | + Any relevant configuration (relay flags, tunnel flags, environment variables). + Make sure to redact any secrets or tokens. + render: shell + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Any other context, screenshots, or information about the problem. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..a9ccbf7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,69 @@ +name: Feature Request +description: Suggest a new feature or improvement +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting a feature! Please describe what you'd like and why. + + - type: textarea + id: problem + attributes: + label: Problem + description: What problem does this feature solve? What pain point are you experiencing? + placeholder: "I'm always frustrated when..." + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: How would you like this to work? Be as specific as possible. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: What alternative solutions or workarounds have you considered? + + - type: dropdown + id: area + attributes: + label: Area + description: Which part of localup does this relate to? + options: + - CLI + - Client Library (Rust) + - Relay / Exit Node + - HTTP/HTTPS Tunnels + - TCP Tunnels + - TLS/SNI Tunnels + - Authentication / JWT + - Dashboard / Web UI + - Desktop App + - Node.js SDK + - Documentation + - Other + validations: + required: true + + - type: dropdown + id: importance + attributes: + label: Importance + description: How important is this feature to you? + options: + - Nice to have + - Important - impacts my workflow + - Critical - blocking my use case + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Any other context, mockups, or examples that help explain the feature. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..fa5f6a0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,39 @@ +## Summary + + + +## Changes + + + +- + +## Related Issues + + + +## Type of Change + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update +- [ ] Refactoring (no functional changes) +- [ ] CI/CD or build changes + +## Checklist + +- [ ] `cargo fmt --all -- --check` passes +- [ ] `cargo clippy --all-targets --all-features -- -D warnings` passes +- [ ] `cargo test` passes +- [ ] I have added tests for my changes +- [ ] I have updated documentation as needed +- [ ] My changes don't introduce new warnings + +## Test Plan + + + +## Screenshots / Logs + + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1dab441 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,112 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.9] - 2026-03-07 + +### Fixed + +- Force `Connection: close` on proxied HTTP/1.1 requests to prevent connection reuse issues +- Keep QUIC send half alive during H2 passthrough streaming + +## [0.1.8] - 2026-03-07 + +### Fixed + +- Rewrite H2 proxy as true bidirectional passthrough for reliable HTTP/2 streaming + +## [0.1.7] - 2026-03-06 + +### Added + +- Device authentication and OAuth support +- Exit node portal improvements + +### Fixed + +- Chunked HTTP/2 proxy handling + +## [0.1.6] - 2026-02-21 + +### Fixed + +- Rewrite Host and Origin headers in raw HTTP requests +- Rewrite Host header in transparent streaming + +### Changed + +- Replace inline test server with dedicated script + +## [0.1.5] - 2026-02-16 + +### Fixed + +- Use transparent byte streaming for all HTTP/1.1 requests + +## [0.1.4] - 2026-02-08 + +### Added + +- HTTP/2 support with ALPN negotiation + +## [0.1.3] - 2026-02-08 + +### Fixed + +- Use HttpProxy for concurrent HTTP request handling + +## [0.1.2] - 2026-02-08 + +### Fixed + +- Skip body reading for HTTP 1xx/204/304 and HEAD responses + +## [0.1.1] - 2026-02-08 + +### Added + +- Wildcard domain tunnels +- Subdomain access restrictions in JWT claims +- Tauri desktop application (macOS, Linux) +- Tunnel editing in dashboard UI +- HTTP/2 support in HTTPS relay +- TCP connection management + +### Fixed + +- 5-second latency on chunked HTTP responses +- Generate unique tunnel IDs for each protocol configuration + +## [0.0.1-beta] - 2025-10-26 to 2026-02-07 + +Initial beta period with 70 beta releases. Key milestones: + +- Core QUIC-based tunnel protocol +- TCP, TLS/SNI, HTTP, and HTTPS tunnel support +- JWT authentication +- ACME/Let's Encrypt certificate management +- Geo-distributed exit node architecture +- CLI tool and client library +- SeaORM database layer (SQLite, PostgreSQL) +- REST API with OpenAPI/Swagger documentation +- Dashboard web application +- Node.js SDK +- Multi-platform release builds (Linux, macOS) +- Docker support + +[Unreleased]: https://github.com/localup-dev/localup/compare/v0.1.9...HEAD +[0.1.9]: https://github.com/localup-dev/localup/compare/v0.1.8...v0.1.9 +[0.1.8]: https://github.com/localup-dev/localup/compare/v0.1.7...v0.1.8 +[0.1.7]: https://github.com/localup-dev/localup/compare/v0.1.6...v0.1.7 +[0.1.6]: https://github.com/localup-dev/localup/compare/v0.1.5...v0.1.6 +[0.1.5]: https://github.com/localup-dev/localup/compare/v0.1.4...v0.1.5 +[0.1.4]: https://github.com/localup-dev/localup/compare/v0.1.3...v0.1.4 +[0.1.3]: https://github.com/localup-dev/localup/compare/v0.1.2...v0.1.3 +[0.1.2]: https://github.com/localup-dev/localup/compare/v0.1.1...v0.1.2 +[0.1.1]: https://github.com/localup-dev/localup/compare/v0.0.1-beta70...v0.1.1 +[0.0.1-beta]: https://github.com/localup-dev/localup/releases/tag/v0.0.1-beta1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..85f27c6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,56 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a positive experience for everyone, regardless of background or +identity. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +* Being respectful and considerate of differing viewpoints +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best for the overall community + +Examples of unacceptable behavior: + +* Trolling, insulting or derogatory comments, and personal attacks +* Public or private harassment +* Publishing others' private information without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of unacceptable behavior may be reported to the project maintainers at +**team@tunnel.io**. + +All reports will be reviewed and investigated promptly and fairly. All community +leaders are obligated to respect the privacy and security of the reporter. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/Cargo.toml b/Cargo.toml index 01f52b8..6df240a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ version = "0.1.4" edition = "2021" license = "MIT OR Apache-2.0" authors = ["Tunnel Team "] -repository = "https://github.com/tunnel/geo-tunnel" +repository = "https://github.com/localup-dev/localup" [workspace.dependencies] # Async runtime diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..05331c2 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,582 @@ +# REST API Reference + +The localup relay exposes a REST API for managing tunnels, inspecting traffic, managing domains, and authentication. The API is auto-documented with OpenAPI/Swagger. + +--- + +## Getting Started + +### Enable the API + +The API server is enabled by default when running a relay. Configure the bind address: + +```bash +localup relay http \ + --api-http-addr 0.0.0.0:8080 \ + --jwt-secret "my-secret" +``` + +For HTTPS: + +```bash +localup relay http \ + --api-https-addr 0.0.0.0:8443 \ + --api-tls-cert /path/to/cert.pem \ + --api-tls-key /path/to/key.pem +``` + +To disable the API: + +```bash +localup relay http --no-api +``` + +### OpenAPI / Swagger UI + +- **OpenAPI spec**: `GET /api/openapi.json` +- **Swagger UI**: `GET /swagger-ui` + +### Authentication + +Most endpoints require a session token obtained via login: + +```bash +# Login +curl -X POST http://relay:8080/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email": "admin@example.com", "password": "mypassword"}' + +# Response includes a session token +# {"token": "session-token-here", "expires_at": "..."} + +# Use the session token for subsequent requests +curl http://relay:8080/api/tunnels \ + -H "Authorization: Bearer session-token-here" +``` + +You can also use API keys (auth tokens) for programmatic access: + +```bash +# Create an API key +curl -X POST http://relay:8080/api/auth-tokens \ + -H "Authorization: Bearer session-token-here" \ + -H "Content-Type: application/json" \ + -d '{"name": "CI/CD", "description": "Token for CI pipeline"}' + +# Use the API key directly +curl http://relay:8080/api/tunnels \ + -H "Authorization: Bearer api-key-here" +``` + +--- + +## Endpoints + +### Health Check + +#### `GET /api/health` + +No authentication required. + +```bash +curl http://relay:8080/api/health +``` + +```json +{ + "status": "ok", + "version": "0.1.9", + "active_tunnels": 3 +} +``` + +### Protocol Discovery + +#### `GET /.well-known/localup-protocols` + +No authentication required. Returns available transport protocols. + +```bash +curl http://relay:8080/.well-known/localup-protocols +``` + +--- + +## Tunnel Management + +### List Tunnels + +#### `GET /api/tunnels` + +```bash +# Active tunnels only +curl http://relay:8080/api/tunnels \ + -H "Authorization: Bearer $TOKEN" + +# Include disconnected tunnels +curl "http://relay:8080/api/tunnels?include_inactive=true" \ + -H "Authorization: Bearer $TOKEN" + +# Admin: see all users' tunnels +curl "http://relay:8080/api/tunnels?scope=all" \ + -H "Authorization: Bearer $TOKEN" +``` + +### Get Tunnel + +#### `GET /api/tunnels/{id}` + +```bash +curl http://relay:8080/api/tunnels/myapp \ + -H "Authorization: Bearer $TOKEN" +``` + +### Delete Tunnel + +#### `DELETE /api/tunnels/{id}` + +```bash +curl -X DELETE http://relay:8080/api/tunnels/myapp \ + -H "Authorization: Bearer $TOKEN" +``` + +### Tunnel Metrics + +#### `GET /api/tunnels/{id}/metrics` + +```bash +curl http://relay:8080/api/tunnels/myapp/metrics \ + -H "Authorization: Bearer $TOKEN" +``` + +```json +{ + "total_requests": 1523, + "requests_per_minute": 12.5, + "avg_latency_ms": 45.2, + "error_rate": 0.02, + "total_bandwidth_bytes": 5242880 +} +``` + +--- + +## Traffic Inspection + +### List Captured Requests + +#### `GET /api/requests` + +Query HTTP requests captured by the relay. + +```bash +# All requests for a tunnel +curl "http://relay:8080/api/requests?localup_id=myapp" \ + -H "Authorization: Bearer $TOKEN" + +# Filter by method and status +curl "http://relay:8080/api/requests?localup_id=myapp&method=POST&status_min=400&status_max=599" \ + -H "Authorization: Bearer $TOKEN" + +# Pagination +curl "http://relay:8080/api/requests?localup_id=myapp&offset=0&limit=50" \ + -H "Authorization: Bearer $TOKEN" +``` + +**Query Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `localup_id` | string | Filter by tunnel ID | +| `method` | string | Filter by HTTP method (GET, POST, etc.) | +| `path` | string | Filter by request path (partial match) | +| `status` | u16 | Exact status code | +| `status_min` | u16 | Minimum status code | +| `status_max` | u16 | Maximum status code | +| `offset` | usize | Pagination offset (default: 0) | +| `limit` | usize | Page size (default: 100, max: 1000) | +| `scope` | string | `"mine"` (default) or `"all"` (admin) | + +### Get Captured Request + +#### `GET /api/requests/{id}` + +```bash +curl http://relay:8080/api/requests/550e8400-e29b-41d4-a716-446655440000 \ + -H "Authorization: Bearer $TOKEN" +``` + +Returns full request and response details including headers and body. + +### Replay Request + +#### `POST /api/requests/{id}/replay` + +Re-send a previously captured request through the tunnel. + +```bash +curl -X POST http://relay:8080/api/requests/550e8400-e29b-41d4-a716-446655440000/replay \ + -H "Authorization: Bearer $TOKEN" +``` + +### List TCP Connections + +#### `GET /api/tcp-connections` + +For non-HTTP tunnels (TCP, TLS). + +```bash +curl "http://relay:8080/api/tcp-connections?localup_id=my-tcp-tunnel" \ + -H "Authorization: Bearer $TOKEN" +``` + +--- + +## Custom Domains + +### List Domains + +#### `GET /api/domains` + +```bash +curl http://relay:8080/api/domains \ + -H "Authorization: Bearer $TOKEN" +``` + +### Upload Certificate + +#### `POST /api/domains` + +Upload a custom TLS certificate for a domain. + +```bash +curl -X POST http://relay:8080/api/domains \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "domain": "app.example.com", + "cert_pem": "'$(base64 < cert.pem)'", + "key_pem": "'$(base64 < key.pem)'", + "auto_renew": true + }' +``` + +### Get Domain Details + +#### `GET /api/domains/{domain}` + +```bash +curl http://relay:8080/api/domains/app.example.com \ + -H "Authorization: Bearer $TOKEN" +``` + +### Delete Domain + +#### `DELETE /api/domains/{domain}` + +```bash +curl -X DELETE http://relay:8080/api/domains/app.example.com \ + -H "Authorization: Bearer $TOKEN" +``` + +### Certificate Details + +#### `GET /api/domains/{domain}/certificate-details` + +Get X.509 certificate details (serial, issuer, expiration, SANs). + +```bash +curl http://relay:8080/api/domains/app.example.com/certificate-details \ + -H "Authorization: Bearer $TOKEN" +``` + +--- + +## ACME / Let's Encrypt + +Automated certificate provisioning via ACME. The relay must be started with `--acme-email`. + +### Initiate Challenge + +#### `POST /api/domains/challenge/initiate` + +```bash +curl -X POST http://relay:8080/api/domains/challenge/initiate \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"domain": "app.example.com", "challenge_type": "http-01"}' +``` + +Supported challenge types: `http-01`, `dns-01`. + +### Pre-Validate Challenge + +#### `POST /api/domains/challenge/pre-validate` + +Check if your challenge setup is correct before ACME submission. + +### Complete Challenge + +#### `POST /api/domains/challenge/complete` + +```bash +curl -X POST http://relay:8080/api/domains/challenge/complete \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"domain": "app.example.com", "challenge_id": "challenge-uuid"}' +``` + +### Request Certificate + +#### `POST /api/domains/{domain}/certificate` + +After challenge validation, request the certificate from Let's Encrypt. + +```bash +curl -X POST http://relay:8080/api/domains/app.example.com/certificate \ + -H "Authorization: Bearer $TOKEN" +``` + +--- + +## Authentication + +### Register + +#### `POST /api/auth/register` + +Registration must be enabled with `--allow-signup`. + +```bash +curl -X POST http://relay:8080/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "email": "user@example.com", + "password": "securepassword", + "full_name": "Jane Doe" + }' +``` + +### Login + +#### `POST /api/auth/login` + +```bash +curl -X POST http://relay:8080/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email": "user@example.com", "password": "securepassword"}' +``` + +### Current User + +#### `GET /api/auth/me` + +```bash +curl http://relay:8080/api/auth/me \ + -H "Authorization: Bearer $TOKEN" +``` + +### Logout + +#### `POST /api/auth/logout` + +```bash +curl -X POST http://relay:8080/api/auth/logout \ + -H "Authorization: Bearer $TOKEN" +``` + +### Auth Configuration + +#### `GET /api/auth/config` + +No auth required. Returns what auth methods are available. + +```bash +curl http://relay:8080/api/auth/config +``` + +```json +{ + "signup_enabled": true, + "social_providers": ["google", "github"], + "magic_link_enabled": true, + "device_auth_enabled": true +} +``` + +--- + +## OAuth (Social Login) + +### Get OAuth URL + +#### `GET /api/auth/oauth/{provider}/url` + +```bash +curl "http://relay:8080/api/auth/oauth/github/url?redirect_uri=http://localhost:3000/callback" +``` + +Providers: `google`, `github`. + +### OAuth Callback + +#### `POST /api/auth/oauth/{provider}/callback` + +```bash +curl -X POST http://relay:8080/api/auth/oauth/github/callback \ + -H "Content-Type: application/json" \ + -d '{"code": "auth-code", "state": "csrf-state", "redirect_uri": "http://localhost:3000/callback"}' +``` + +--- + +## Device Authorization (RFC 8628) + +For CLI and desktop app login without browser redirect. + +### Initiate + +#### `POST /api/device/authorize` + +```bash +curl -X POST http://relay:8080/api/device/authorize \ + -H "Content-Type: application/json" \ + -d '{"client_id": "localup-cli"}' +``` + +```json +{ + "device_code": "device-code-here", + "user_code": "ABCD-1234", + "verification_uri": "https://relay.example.com/device", + "expires_in": 900, + "interval": 5 +} +``` + +### Poll for Token + +#### `POST /api/device/token` + +```bash +curl -X POST http://relay:8080/api/device/token \ + -H "Content-Type: application/json" \ + -d '{ + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + "device_code": "device-code-here", + "client_id": "localup-cli" + }' +``` + +Returns `authorization_pending` until the user approves, then returns the access token. + +### Approve (Browser) + +#### `POST /api/device/verify` + +User approves the device from the browser. + +```bash +curl -X POST http://relay:8080/api/device/verify \ + -H "Authorization: Bearer $SESSION_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"user_code": "ABCD-1234"}' +``` + +--- + +## API Keys (Auth Tokens) + +Long-lived tokens for programmatic access. + +### List API Keys + +#### `GET /api/auth-tokens` + +```bash +curl http://relay:8080/api/auth-tokens \ + -H "Authorization: Bearer $TOKEN" +``` + +### Create API Key + +#### `POST /api/auth-tokens` + +```bash +curl -X POST http://relay:8080/api/auth-tokens \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name": "CI/CD Pipeline", "description": "For automated deployments", "expires_in_days": 90}' +``` + +The response includes the token value -- **save it immediately**, it's only shown once. + +### Update API Key + +#### `PATCH /api/auth-tokens/{id}` + +```bash +curl -X PATCH http://relay:8080/api/auth-tokens/token-uuid \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name": "Updated Name", "is_active": false}' +``` + +### Delete API Key + +#### `DELETE /api/auth-tokens/{id}` + +```bash +curl -X DELETE http://relay:8080/api/auth-tokens/token-uuid \ + -H "Authorization: Bearer $TOKEN" +``` + +--- + +## Database Configuration + +The relay stores captured requests and user data in a database. Configure with `--database-url`: + +```bash +# In-memory SQLite (default, data lost on restart) +localup relay http + +# File-based SQLite +localup relay http --database-url "sqlite://./tunnel.db?mode=rwc" + +# PostgreSQL +localup relay http --database-url "postgres://user:pass@localhost/localup_db" + +# PostgreSQL with TimescaleDB (recommended for production) +localup relay http --database-url "postgres://user:pass@localhost/localup_db" +``` + +Migrations run automatically on startup. + +--- + +## Error Responses + +All errors follow RFC 7807 Problem Details format: + +```json +{ + "type": "https://httpstatuses.io/404", + "title": "Not Found", + "status": 404, + "detail": "Tunnel 'myapp' not found" +} +``` + +Common status codes: + +| Status | Meaning | +|--------|---------| +| 200 | Success | +| 201 | Created | +| 400 | Bad request (invalid input) | +| 401 | Unauthorized (missing/invalid token) | +| 403 | Forbidden (insufficient permissions) | +| 404 | Not found | +| 409 | Conflict (duplicate resource) | +| 500 | Internal server error | diff --git a/docs/cli-reference.md b/docs/cli-reference.md new file mode 100644 index 0000000..db37fce --- /dev/null +++ b/docs/cli-reference.md @@ -0,0 +1,520 @@ +# CLI Reference + +Complete reference for all `localup` commands, flags, and environment variables. + +## Quick Overview + +``` +localup [OPTIONS] # Run a single tunnel +localup init # Create .localup.yml config +localup up [--tunnels name1,name2] # Start tunnels from config +localup down # Stop tunnels from config +localup status # Show tunnel status +localup relay # Run a relay server +localup agent # Run a reverse tunnel agent +localup agent-server # Run a standalone agent server +localup connect # Connect through a reverse tunnel +localup generate-token # Generate JWT tokens +localup daemon # Manage the daemon +localup service # Manage the system service +localup config # Manage stored configuration +``` + +--- + +## Standalone Tunnel + +Run a single tunnel without a configuration file. + +```bash +localup [OPTIONS] +``` + +### Connection Options + +| Flag | Short | Env Var | Default | Description | +|------|-------|---------|---------|-------------| +| `--port ` | `-p` | | | Local port to expose | +| `--address ` | | | | Local address to expose (alternative to `--port`) | +| `--protocol ` | | | `http` | Protocol: `http`, `https`, `tcp`, `tls` | +| `--relay
` | `-r` | `RELAY` | | Relay server address | +| `--token ` | `-t` | `TUNNEL_AUTH_TOKEN` | | Authentication token | +| `--transport ` | | | `quic` | Transport: `quic`, `h2`, `websocket` | +| `--subdomain ` | `-s` | | | Subdomain for HTTP/HTTPS tunnels | +| `--custom-domain ` | | | | Custom domain (repeatable, supports wildcards) | +| `--remote-port ` | | | | Remote port for TCP/TLS tunnels | +| `--http-port ` | | | | HTTP backend port for TLS passthrough | + +### Security Options + +| Flag | Description | +|------|-------------| +| `--basic-auth ` | HTTP Basic Auth credentials (repeatable) | +| `--auth-token ` | HTTP Bearer token for tunnel access (repeatable) | +| `--allow-ip ` | Allowed IP addresses/ranges (repeatable) | + +### Other Options + +| Flag | Default | Description | +|------|---------|-------------| +| `--log-level ` | `info` | Log level: `trace`, `debug`, `info`, `warn`, `error` | +| `--metrics-port ` | `9090` | Metrics web dashboard port | +| `--no-metrics` | | Disable metrics collection | + +### Examples + +```bash +# HTTP tunnel +localup -p 3000 -r relay.example.com:4443 -t $TOKEN + +# HTTPS tunnel with subdomain +localup -p 3000 --protocol https -s myapp -r relay.example.com:4443 -t $TOKEN + +# TCP tunnel on specific remote port +localup -p 5432 --protocol tcp --remote-port 15432 -r relay.example.com:4443 -t $TOKEN + +# HTTP tunnel with Basic Auth +localup -p 3000 --basic-auth "admin:secret" --basic-auth "user:pass" -r relay.example.com:4443 -t $TOKEN + +# HTTP tunnel with IP allowlisting +localup -p 3000 --allow-ip 192.168.1.0/24 --allow-ip 10.0.0.5 -r relay.example.com:4443 -t $TOKEN + +# TLS tunnel with custom domain and HTTP passthrough +localup -p 9443 --protocol tls --custom-domain "*.example.com" --http-port 9080 -r relay.example.com:4443 -t $TOKEN + +# Use WebSocket transport (firewall-friendly) +localup -p 3000 --transport websocket -r relay.example.com:4443 -t $TOKEN +``` + +--- + +## Project Config Commands + +### `localup init` + +Create a new `.localup.yml` config file in the current directory. + +```bash +localup init +``` + +### `localup up` + +Start tunnels defined in `.localup.yml`. + +```bash +# Start all enabled tunnels +localup up + +# Start specific tunnels only +localup up --tunnels web,api +``` + +### `localup down` + +Stop all tunnels started from `.localup.yml`. + +```bash +localup down +``` + +### `localup status` + +Show status of running tunnels. + +```bash +localup status +``` + +--- + +## Tunnel Config Management + +### `localup add ` + +Add a tunnel to `.localup.yml`. + +```bash +localup add web -p 3000 --protocol http -s myapp +localup add api -p 8080 --protocol https -s api --allow-ip 10.0.0.0/8 +localup add db -p 5432 --protocol tcp --remote-port 15432 +``` + +Accepts the same flags as standalone mode plus: + +| Flag | Description | +|------|-------------| +| `--enabled` | Auto-start with daemon | + +### `localup list` + +List all tunnel configurations. + +### `localup show ` + +Show details of a specific tunnel. + +### `localup remove ` + +Remove a tunnel configuration. + +### `localup enable ` / `localup disable ` + +Enable or disable auto-start with daemon. + +--- + +## Daemon Commands + +### `localup daemon start` + +Start the daemon in foreground. + +```bash +localup daemon start +localup daemon start -c /path/to/.localup.yml +``` + +### `localup daemon stop` + +Stop the running daemon. + +### `localup daemon status` + +Show daemon status and running tunnels. + +### `localup daemon list` + +List all configured tunnels. + +### `localup daemon reload` + +Reload all tunnel configurations. + +### `localup daemon tunnel-start ` + +Start a specific tunnel by name. + +### `localup daemon tunnel-stop ` + +Stop a specific tunnel by name. + +### `localup daemon tunnel-reload ` + +Reload a specific tunnel (stop + start with new config). + +### `localup daemon add ` / `localup daemon remove ` + +Add or remove a tunnel from the daemon configuration. + +```bash +localup daemon add frontend -p 3000 --protocol https -s frontend +localup daemon remove frontend +``` + +--- + +## System Service Commands + +Install localup as a system service (launchd on macOS, systemd on Linux). + +```bash +localup service install # Install service +localup service uninstall # Uninstall service +localup service start # Start service +localup service stop # Stop service +localup service restart # Restart service +localup service status # Check service status +localup service logs # View logs (default: 50 lines) +localup service logs -n 100 # View last 100 lines +``` + +--- + +## Relay Server Commands + +Run a relay (exit node) server. See [Relay Configuration](custom-relay-config.md) for advanced setup. + +### `localup relay tcp` + +TCP relay with port-based routing. + +```bash +localup relay tcp \ + --localup-addr 0.0.0.0:4443 \ + --tcp-port-range 10000-20000 \ + --domain relay.example.com \ + --jwt-secret "my-secret" \ + --database-url "sqlite://./tunnel.db?mode=rwc" +``` + +### `localup relay tls` + +TLS/SNI relay with SNI-based routing (no TLS termination). + +```bash +localup relay tls \ + --localup-addr 0.0.0.0:4443 \ + --tls-addr 0.0.0.0:443 \ + --domain relay.example.com \ + --jwt-secret "my-secret" +``` + +### `localup relay http` + +HTTP/HTTPS relay with host-based routing and TLS termination. + +```bash +localup relay http \ + --localup-addr 0.0.0.0:4443 \ + --http-addr 0.0.0.0:80 \ + --https-addr 0.0.0.0:443 \ + --tls-cert /path/to/cert.pem \ + --tls-key /path/to/key.pem \ + --domain relay.example.com \ + --jwt-secret "my-secret" +``` + +### Common Relay Options + +| Flag | Env Var | Default | Description | +|------|---------|---------|-------------| +| `--localup-addr ` | | `0.0.0.0:4443` | QUIC control plane address | +| `--domain ` | | `localhost` | Public domain name | +| `--jwt-secret ` | `JWT_SECRET` | | JWT validation secret | +| `--tls-cert ` | | auto-generated | TLS certificate | +| `--tls-key ` | | auto-generated | TLS private key | +| `--database-url ` | `DATABASE_URL` | in-memory SQLite | Database connection | +| `--log-level ` | | `info` | Log level | +| `--transport ` | | `quic` | Control plane transport | + +### API Server Options (All Relay Types) + +| Flag | Env Var | Description | +|------|---------|-------------| +| `--api-http-addr ` | `API_HTTP_ADDR` | HTTP API server address | +| `--api-https-addr ` | `API_HTTPS_ADDR` | HTTPS API server address | +| `--api-tls-cert ` | `API_TLS_CERT` | TLS cert for API server | +| `--api-tls-key ` | `API_TLS_KEY` | TLS key for API server | +| `--no-api` | | Disable API server | + +### User Management Options (All Relay Types) + +| Flag | Env Var | Description | +|------|---------|-------------| +| `--admin-email ` | `ADMIN_EMAIL` | Create admin user on startup | +| `--admin-password ` | `ADMIN_PASSWORD` | Admin password | +| `--admin-username ` | `ADMIN_USERNAME` | Admin username | +| `--allow-signup` | `ALLOW_SIGNUP` | Allow public user registration | + +### HTTP Relay Extras + +| Flag | Env Var | Description | +|------|---------|-------------| +| `--acme-email ` | `ACME_EMAIL` | Let's Encrypt email | +| `--acme-staging` | | Use Let's Encrypt staging | +| `--acme-cert-dir ` | | ACME cert directory (default: `/opt/localup/certs/acme`) | +| `--websocket-path ` | | WebSocket endpoint (default: `/localup`) | +| `--smtp-host ` | `SMTP_HOST` | SMTP server for email features | +| `--smtp-port ` | `SMTP_PORT` | SMTP port (default: `587`) | +| `--smtp-username ` | `SMTP_USERNAME` | SMTP username | +| `--smtp-password ` | `SMTP_PASSWORD` | SMTP password | +| `--smtp-from ` | `SMTP_FROM` | Sender email address | + +--- + +## Agent & Reverse Tunnel Commands + +See [Reverse Tunnels](reverse-tunnels.md) for detailed usage guide. + +### `localup agent` + +Run as a reverse tunnel agent, exposing a private service through the relay. + +```bash +localup agent \ + --relay relay.example.com:4443 \ + --token $TOKEN \ + --target-address 192.168.1.100:8080 \ + --agent-id my-service +``` + +| Flag | Env Var | Default | Description | +|------|---------|---------|-------------| +| `--relay ` | `LOCALUP_RELAY_ADDR` | `localhost:4443` | Relay server | +| `--token ` | `LOCALUP_AUTH_TOKEN` | | Auth token (required) | +| `--target-address ` | `LOCALUP_TARGET_ADDRESS` | | Target to forward to (required) | +| `--agent-id ` | `LOCALUP_AGENT_ID` | auto-generated | Agent identifier | +| `--insecure` | `LOCALUP_INSECURE` | | Skip TLS verification | +| `--jwt-secret ` | `LOCALUP_JWT_SECRET` | | JWT secret for client auth | +| `--log-level ` | `RUST_LOG` | `info` | Log level | + +### `localup connect` + +Connect through a reverse tunnel to access a private service. + +```bash +localup connect \ + --relay relay.example.com:4443 \ + --agent-id my-service \ + --remote-address 192.168.1.100:8080 \ + --local-address localhost:18080 \ + --token $TOKEN +``` + +| Flag | Env Var | Default | Description | +|------|---------|---------|-------------| +| `--relay ` | `LOCALUP_RELAY` | | Relay server (required) | +| `--agent-id ` | | | Agent ID to route through (required) | +| `--remote-address ` | | | Remote address to connect to (required) | +| `--local-address ` | | `localhost:0` | Local address to bind | +| `--token ` | `LOCALUP_AUTH_TOKEN` | | Auth token for relay | +| `--agent-token ` | `LOCALUP_AGENT_TOKEN` | | Auth token for agent | +| `--insecure` | | | Skip TLS verification | + +### `localup agent-server` + +Run a standalone agent server (combined relay + agent). + +```bash +localup agent-server \ + --listen 0.0.0.0:4443 \ + --jwt-secret "my-secret" \ + --target-address 192.168.1.100:8080 +``` + +| Flag | Env Var | Default | Description | +|------|---------|---------|-------------| +| `--listen ` | `LOCALUP_LISTEN` | `0.0.0.0:4443` | QUIC listen address | +| `--cert ` | `LOCALUP_CERT` | auto-generated | TLS certificate | +| `--key ` | `LOCALUP_KEY` | auto-generated | TLS key | +| `--jwt-secret ` | `LOCALUP_JWT_SECRET` | | JWT authentication secret | +| `--target-address ` | `LOCALUP_TARGET_ADDRESS` | | Backend service address | +| `--relay-addr ` | `LOCALUP_RELAY_ADDR` | | Upstream relay address | +| `--relay-id ` | `LOCALUP_RELAY_ID` | | Server ID on upstream relay | +| `--relay-token ` | `LOCALUP_RELAY_TOKEN` | | Auth token for upstream relay | +| `--verbose` | | | Enable verbose logging | + +--- + +## Token Generation + +### `localup generate-token` + +Generate JWT tokens for client authentication. + +```bash +# Basic token (24-hour validity) +localup generate-token --secret "my-secret" --sub myapp + +# 48-hour token +localup generate-token --secret "my-secret" --sub myapp --hours 48 + +# Token with subdomain restrictions +localup generate-token --secret "my-secret" --sub myapp --allowed-subdomain "myapp-*" + +# Token for reverse tunnel access +localup generate-token --secret "my-secret" --sub myapp --reverse-tunnel --agent my-agent + +# Script-friendly (token only, no extra output) +localup generate-token --secret "my-secret" --sub myapp --token-only +``` + +| Flag | Env Var | Description | +|------|---------|-------------| +| `--secret ` | `TUNNEL_JWT_SECRET` | JWT signing secret (required) | +| `--sub ` | | Subject/tunnel ID (auto-generated UUID if omitted) | +| `--user-id ` | | User ID who owns token | +| `--hours ` | | Token validity (default: `24`) | +| `--reverse-tunnel` | | Enable reverse tunnel access | +| `--agent ` | | Allowed agent IDs (repeatable) | +| `--allowed-address ` | | Allowed target addresses (repeatable) | +| `--allowed-subdomain ` | | Allowed subdomain patterns (repeatable, glob) | +| `--token-only` | | Output only JWT token | + +--- + +## Config Commands + +### `localup config set-token ` + +Store a default authentication token. + +### `localup config get-token` + +Display the stored authentication token. + +### `localup config clear-token` + +Remove the stored authentication token. + +--- + +## Environment Variables + +### Client + +| Variable | Description | +|----------|-------------| +| `TUNNEL_AUTH_TOKEN` | Default authentication token | +| `RELAY` | Default relay server address | + +### Relay Server + +| Variable | Description | +|----------|-------------| +| `JWT_SECRET` | JWT validation secret | +| `DATABASE_URL` | Database connection string | +| `API_HTTP_ADDR` | HTTP API server address | +| `API_HTTPS_ADDR` | HTTPS API server address | +| `API_TLS_CERT` | API TLS certificate path | +| `API_TLS_KEY` | API TLS key path | +| `ADMIN_EMAIL` | Auto-create admin email | +| `ADMIN_PASSWORD` | Auto-create admin password | +| `ADMIN_USERNAME` | Auto-create admin username | +| `ALLOW_SIGNUP` | Allow public user registration | +| `ACME_EMAIL` | Let's Encrypt email | +| `SMTP_HOST` | SMTP server hostname | +| `SMTP_PORT` | SMTP port | +| `SMTP_USERNAME` | SMTP username | +| `SMTP_PASSWORD` | SMTP password | +| `SMTP_FROM` | Sender email address | + +### Agent + +| Variable | Description | +|----------|-------------| +| `LOCALUP_RELAY_ADDR` | Relay server address | +| `LOCALUP_AUTH_TOKEN` | Auth token | +| `LOCALUP_AGENT_TOKEN` | Agent-specific auth token | +| `LOCALUP_TARGET_ADDRESS` | Target address to forward to | +| `LOCALUP_AGENT_ID` | Agent identifier | +| `LOCALUP_INSECURE` | Skip TLS verification | +| `LOCALUP_JWT_SECRET` | JWT secret | + +### Agent Server + +| Variable | Description | +|----------|-------------| +| `LOCALUP_LISTEN` | QUIC listen address | +| `LOCALUP_CERT` | TLS certificate path | +| `LOCALUP_KEY` | TLS key path | +| `LOCALUP_RELAY_ID` | Server ID on upstream relay | +| `LOCALUP_RELAY_TOKEN` | Upstream relay auth token | + +### Logging + +| Variable | Description | +|----------|-------------| +| `RUST_LOG` | Log level filter (e.g., `info`, `localup=debug`) | + +--- + +## File Locations + +| Path | Description | +|------|-------------| +| `.localup.yml` | Project tunnel configuration (discovered hierarchically) | +| `~/.localup/config.json` | Stored tokens and IPC socket info | +| `~/.localup/` | Auto-generated certificates (if not provided) | +| `/opt/localup/certs/acme` | ACME certificate directory (default) | diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..8c5f167 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,285 @@ +# Configuration Guide + +localup supports three levels of configuration: + +1. **CLI flags** - Per-invocation settings +2. **Project config** (`.localup.yml`) - Multi-tunnel definitions per project +3. **Stored config** (`~/.localup/config.json`) - Persistent defaults (tokens) + +--- + +## Project Configuration (`.localup.yml`) + +Define multiple tunnels in a single file and manage them together. + +### Creating a Config File + +```bash +localup init +``` + +This creates a `.localup.yml` in the current directory with a skeleton structure. + +### File Format + +```yaml +# Default settings applied to all tunnels +defaults: + relay: relay.example.com:4443 + token: ${TUNNEL_AUTH_TOKEN} # Environment variable expansion + transport: quic # quic, h2, websocket + local_host: localhost + timeout_seconds: 30 + +# Tunnel definitions +tunnels: + - name: web + port: 3000 + protocol: http + subdomain: myapp + enabled: true + + - name: api + port: 8080 + protocol: https + subdomain: api + allow_ips: + - 10.0.0.0/8 + - 192.168.1.0/24 + + - name: database + port: 5432 + protocol: tcp + remote_port: 15432 + enabled: false # Won't start with `localup up` + + - name: tls-service + port: 9443 + protocol: tls + custom_domain: service.example.com + sni_hostnames: + - service.example.com + - "*.service.example.com" + http_port: 9080 # HTTP backend for TLS passthrough + + - name: staging + port: 3001 + protocol: http + subdomain: staging + relay: staging-relay.example.com:4443 # Override default relay + token: ${STAGING_TOKEN} # Override default token + transport: websocket # Override default transport + local_host: 0.0.0.0 # Override default local host +``` + +### Tunnel Fields + +| Field | Required | Default | Description | +|-------|----------|---------|-------------| +| `name` | Yes | | Tunnel name (alphanumeric, hyphens, underscores) | +| `port` | Yes | | Local port to expose | +| `protocol` | No | `http` | `http`, `https`, `tcp`, `tls` | +| `subdomain` | No | | Subdomain for HTTP/HTTPS/TLS tunnels | +| `custom_domain` | No | | Custom domain (supports wildcards) | +| `remote_port` | No | | Remote port for TCP tunnels | +| `sni_hostnames` | No | | SNI hostnames for TLS tunnels (list) | +| `http_port` | No | | HTTP backend port for TLS passthrough | +| `relay` | No | from defaults | Override relay address | +| `token` | No | from defaults | Override auth token | +| `transport` | No | from defaults | Override transport protocol | +| `local_host` | No | from defaults | Override local host | +| `enabled` | No | `true` | Auto-start with `localup up` | +| `allow_ips` | No | | IP allowlist (CIDR format) | + +### Environment Variable Expansion + +Token and other string values support `${ENV_VAR}` syntax: + +```yaml +defaults: + token: ${TUNNEL_AUTH_TOKEN} + +tunnels: + - name: web + port: 3000 + token: ${WEB_TUNNEL_TOKEN} +``` + +### Config File Discovery + +localup searches for `.localup.yml` (or `.localup.yaml`) starting from the current directory and walking up to the filesystem root: + +``` +/home/user/projects/myapp/.localup.yml # Found first +/home/user/projects/.localup.yml # Checked second +/home/user/.localup.yml # Checked third +/home/.localup.yml # ... +``` + +### Using the Config + +```bash +# Start all enabled tunnels +localup up + +# Start specific tunnels only +localup up --tunnels web,api + +# Stop all tunnels +localup down + +# Check status +localup status +``` + +### Managing Tunnels + +```bash +# Add a tunnel to config +localup add frontend -p 3000 --protocol http -s frontend + +# List all tunnels +localup list + +# Show tunnel details +localup show frontend + +# Remove a tunnel +localup remove frontend + +# Enable/disable auto-start +localup enable frontend +localup disable frontend +``` + +--- + +## Daemon Mode + +For long-running tunnel management, use the daemon. It reads from `.localup.yml` and keeps tunnels alive. + +```bash +# Start daemon (foreground) +localup daemon start + +# Start with specific config +localup daemon start -c /path/to/.localup.yml + +# Check status +localup daemon status + +# Manage individual tunnels +localup daemon tunnel-start web +localup daemon tunnel-stop web +localup daemon tunnel-reload web + +# Reload all configs +localup daemon reload + +# Stop daemon +localup daemon stop +``` + +### System Service + +Install as a system service for automatic startup: + +```bash +# Install (launchd on macOS, systemd on Linux) +localup service install + +# Manage +localup service start +localup service stop +localup service restart +localup service status +localup service logs -n 100 + +# Uninstall +localup service uninstall +``` + +--- + +## Stored Configuration + +localup stores persistent configuration in `~/.localup/config.json`. + +### Managing Tokens + +```bash +# Store a default token +localup config set-token "eyJ0eXAiOiJKV1Qi..." + +# View stored token +localup config get-token + +# Clear stored token +localup config clear-token +``` + +The stored token is used when no `--token` flag or `TUNNEL_AUTH_TOKEN` env var is provided. + +### Priority Order + +Configuration values are resolved in this order (highest priority first): + +1. CLI flags (`--token`, `--relay`, etc.) +2. Environment variables (`TUNNEL_AUTH_TOKEN`, `RELAY`) +3. Per-tunnel overrides in `.localup.yml` +4. Defaults section in `.localup.yml` +5. Stored config (`~/.localup/config.json`) + +--- + +## Example: Full Project Setup + +```bash +# 1. Initialize config +cd ~/projects/myapp +localup init + +# 2. Add tunnels +localup add web -p 3000 --protocol http -s myapp +localup add api -p 8080 --protocol https -s myapp-api +localup add db -p 5432 --protocol tcp --remote-port 15432 + +# 3. Set default token +localup config set-token "$(localup generate-token --secret my-secret --sub myapp --token-only)" + +# 4. Start all tunnels +localup up + +# 5. Check status +localup status + +# 6. Stop when done +localup down +``` + +## Example: Team Setup with `.localup.yml` in Git + +```yaml +# .localup.yml - Commit this to your repo +defaults: + relay: relay.mycompany.com:4443 + token: ${TUNNEL_AUTH_TOKEN} # Each dev sets their own token + +tunnels: + - name: frontend + port: 3000 + protocol: http + subdomain: ${USER}-frontend # Unique per developer + + - name: backend + port: 8080 + protocol: http + subdomain: ${USER}-backend +``` + +Each developer sets their own token: + +```bash +export TUNNEL_AUTH_TOKEN="their-personal-token" +localup up +``` diff --git a/docs/reverse-tunnels.md b/docs/reverse-tunnels.md new file mode 100644 index 0000000..a83465a --- /dev/null +++ b/docs/reverse-tunnels.md @@ -0,0 +1,266 @@ +# Reverse Tunnels + +Reverse tunnels let you access private services (behind firewalls, NAT, or on private networks) through a relay server. Unlike standard tunnels that expose a local port to the internet, reverse tunnels let you reach into a remote network. + +--- + +## How It Works + +``` +You (Client) --> Relay Server --> Agent --> Private Service + localup localup localup 192.168.1.100:5432 + connect relay agent +``` + +1. An **Agent** runs on the private network, connected to the relay +2. A **Client** connects to the relay and requests access to the agent +3. The relay bridges the connection between client and agent +4. The agent forwards traffic to the private service + +--- + +## Quick Start + +### Step 1: Start a Relay + +```bash +localup relay http \ + --localup-addr 0.0.0.0:4443 \ + --http-addr 0.0.0.0:8080 \ + --jwt-secret "my-secret" +``` + +### Step 2: Generate Tokens + +```bash +# Token for the agent +AGENT_TOKEN=$(localup generate-token --secret "my-secret" --sub agent-1 --token-only) + +# Token for the client (with reverse tunnel access) +CLIENT_TOKEN=$(localup generate-token --secret "my-secret" --sub client-1 \ + --reverse-tunnel --agent private-db --token-only) +``` + +### Step 3: Run the Agent (on the private network) + +```bash +localup agent \ + --relay relay.example.com:4443 \ + --token "$AGENT_TOKEN" \ + --target-address 192.168.1.100:5432 \ + --agent-id private-db +``` + +The agent connects to the relay and registers with ID `private-db`. It will forward all traffic to `192.168.1.100:5432`. + +### Step 4: Connect (from anywhere) + +```bash +localup connect \ + --relay relay.example.com:4443 \ + --token "$CLIENT_TOKEN" \ + --agent-id private-db \ + --remote-address 192.168.1.100:5432 \ + --local-address localhost:15432 +``` + +Now you can access the private database at `localhost:15432`: + +```bash +psql -h localhost -p 15432 -U myuser mydb +``` + +--- + +## Use Cases + +### Access a Private Database + +```bash +# Agent (on database network) +localup agent \ + --relay relay.example.com:4443 \ + --token "$TOKEN" \ + --target-address db-server:5432 \ + --agent-id prod-db + +# Client (your laptop) +localup connect \ + --relay relay.example.com:4443 \ + --token "$TOKEN" \ + --agent-id prod-db \ + --remote-address db-server:5432 \ + --local-address localhost:15432 +``` + +### SSH into a Private Server + +```bash +# Agent (on the private network) +localup agent \ + --relay relay.example.com:4443 \ + --token "$TOKEN" \ + --target-address 10.0.1.50:22 \ + --agent-id office-server + +# Client +localup connect \ + --relay relay.example.com:4443 \ + --token "$TOKEN" \ + --agent-id office-server \ + --remote-address 10.0.1.50:22 \ + --local-address localhost:2222 + +# Then SSH through the tunnel +ssh -p 2222 user@localhost +``` + +### Access an Internal Web App + +```bash +# Agent (on internal network) +localup agent \ + --relay relay.example.com:4443 \ + --token "$TOKEN" \ + --target-address internal-app.corp:80 \ + --agent-id intranet + +# Client +localup connect \ + --relay relay.example.com:4443 \ + --token "$TOKEN" \ + --agent-id intranet \ + --remote-address internal-app.corp:80 \ + --local-address localhost:18080 + +# Access at http://localhost:18080 +``` + +--- + +## Agent Server (Standalone) + +For simpler setups where you don't need a separate relay, use `agent-server`. It combines relay and agent functionality in a single process. + +```bash +# On the private network (acts as both relay and agent) +localup agent-server \ + --listen 0.0.0.0:4443 \ + --jwt-secret "my-secret" \ + --target-address 192.168.1.100:8080 + +# Connect from anywhere +localup connect \ + --relay agent-host.example.com:4443 \ + --remote-address 192.168.1.100:8080 \ + --local-address localhost:18080 \ + --token "$TOKEN" +``` + +### Agent Server with Upstream Relay + +The agent server can also register with an upstream relay for public accessibility: + +```bash +localup agent-server \ + --listen 0.0.0.0:4443 \ + --target-address 192.168.1.100:8080 \ + --relay-addr relay.example.com:4443 \ + --relay-id my-agent-server \ + --relay-token "$RELAY_TOKEN" +``` + +--- + +## Security + +### Token Restrictions + +Use JWT claims to control what clients can access: + +```bash +# Allow access to specific agent only +localup generate-token --secret "my-secret" --sub client-1 \ + --reverse-tunnel \ + --agent private-db + +# Allow access to specific addresses only +localup generate-token --secret "my-secret" --sub client-1 \ + --reverse-tunnel \ + --allowed-address "192.168.1.100:5432" \ + --allowed-address "192.168.1.101:5432" +``` + +### Agent-Side Authentication + +The agent can validate client tokens independently: + +```bash +localup agent \ + --relay relay.example.com:4443 \ + --token "$AGENT_TOKEN" \ + --target-address 192.168.1.100:5432 \ + --agent-id private-db \ + --jwt-secret "agent-secret" # Validates client tokens +``` + +### TLS Verification + +In production, always use valid TLS certificates. For development: + +```bash +# Skip TLS verification (development only!) +localup agent --insecure ... +localup connect --insecure ... +``` + +--- + +## Environment Variables + +All agent and connect flags can be set via environment variables: + +```bash +# Agent +export LOCALUP_RELAY_ADDR="relay.example.com:4443" +export LOCALUP_AUTH_TOKEN="agent-token" +export LOCALUP_TARGET_ADDRESS="192.168.1.100:5432" +export LOCALUP_AGENT_ID="private-db" +localup agent + +# Connect +export LOCALUP_RELAY="relay.example.com:4443" +export LOCALUP_AUTH_TOKEN="client-token" +localup connect --agent-id private-db --remote-address 192.168.1.100:5432 +``` + +--- + +## Troubleshooting + +### Agent won't connect to relay + +```bash +# Enable debug logging +localup agent --log-level debug ... + +# Check TLS - try with insecure for testing +localup agent --insecure ... +``` + +Verify the agent token is signed with the relay's JWT secret. + +### Client can't reach agent + +1. Verify the agent is connected: check relay logs or `localup status` +2. Verify the agent ID matches between `--agent-id` flags +3. Verify the client token has `--reverse-tunnel` permission +4. Check that `--remote-address` matches the agent's `--target-address` + +### Connection drops + +The agent and client automatically attempt reconnection. If connections are unstable: + +- Check network quality between agent and relay +- Try a different transport (`--transport websocket` for firewalled networks) +- Increase timeout in `.localup.yml` (`timeout_seconds: 60`) diff --git a/docs/transports.md b/docs/transports.md new file mode 100644 index 0000000..e996add --- /dev/null +++ b/docs/transports.md @@ -0,0 +1,194 @@ +# Transport Protocols + +localup supports three transport protocols for the connection between client and relay. All transports provide encryption and multiplexing, but differ in performance, compatibility, and firewall traversal. + +--- + +## Overview + +| | QUIC | WebSocket | HTTP/2 | +|---|---|---|---| +| **Protocol** | UDP | TCP (wss://) | TCP (h2) | +| **Default Port** | 4443/UDP | 443/TCP | 443/TCP | +| **Encryption** | TLS 1.3 (built-in) | TLS 1.2/1.3 | TLS 1.2/1.3 | +| **Multiplexing** | Native | Custom framing | Native (HTTP/2 streams) | +| **Firewall** | May be blocked | Passes all firewalls | Passes all firewalls | +| **Performance** | Best | Good | Good | +| **Head-of-line blocking** | No (per-stream) | Yes (TCP) | Yes (TCP) | +| **0-RTT reconnection** | Yes | No | No | +| **Best for** | Default / mobile | Corporate networks | Mixed HTTP traffic | + +--- + +## QUIC (Default) + +QUIC is the default and recommended transport. It provides the best performance with native multiplexing, 0-RTT reconnection, and per-stream flow control. + +### When to Use + +- Default choice for most deployments +- Mobile or unreliable networks (handles network switching) +- Low-latency requirements +- High-throughput scenarios + +### When to Avoid + +- Corporate firewalls blocking UDP +- Networks that only allow TCP port 443 +- Strict proxy environments + +### Usage + +```bash +# Explicit (same as default) +localup -p 3000 --transport quic -r relay.example.com:4443 + +# Relay-side +localup relay http --localup-addr 0.0.0.0:4443 --transport quic +``` + +--- + +## WebSocket + +WebSocket runs over TCP port 443, making it compatible with virtually all networks and firewalls. It uses a custom framing protocol for stream multiplexing on top of a single WebSocket connection. + +### When to Use + +- Corporate networks with restrictive firewalls +- Environments that block UDP +- Behind HTTP proxies +- When maximum compatibility is needed + +### When to Avoid + +- When lowest latency is critical (TCP overhead) +- When you need per-stream flow control + +### Usage + +```bash +# Client +localup -p 3000 --transport websocket -r relay.example.com:443 + +# Relay-side +localup relay http \ + --localup-addr 0.0.0.0:4443 \ + --transport websocket \ + --websocket-path /localup # Default endpoint path +``` + +The WebSocket endpoint is served at the path configured with `--websocket-path` (default: `/localup`). + +--- + +## HTTP/2 (H2) + +HTTP/2 uses native HTTP/2 streams for multiplexing, running over TCP port 443. It's a standard protocol universally supported by CDNs, proxies, and load balancers. + +### When to Use + +- Environments with HTTP/2-aware proxies or CDNs +- When mixing tunnel traffic with HTTP services +- Standard protocol compliance requirements +- Behind load balancers that speak HTTP/2 + +### When to Avoid + +- When lowest latency is critical (TCP head-of-line blocking) +- Simple deployments where QUIC works fine + +### Usage + +```bash +# Client +localup -p 3000 --transport h2 -r relay.example.com:443 + +# Relay-side +localup relay http --localup-addr 0.0.0.0:4443 --transport h2 +``` + +--- + +## Auto-Discovery + +When no `--transport` flag is provided, the client can auto-discover available transports by querying the relay's well-known endpoint: + +``` +GET /.well-known/localup-protocols +``` + +The response lists available transports with their configuration: + +```json +{ + "quic": { "port": 4443 }, + "websocket": { "port": 443, "path": "/localup" }, + "h2": { "port": 443 } +} +``` + +The client selects the best available transport automatically, preferring QUIC when available. + +--- + +## Configuration + +### In `.localup.yml` + +Set a default transport or override per-tunnel: + +```yaml +defaults: + transport: quic # Default for all tunnels + +tunnels: + - name: web + port: 3000 + transport: websocket # Override for this tunnel (behind firewall) + + - name: api + port: 8080 # Uses default (quic) +``` + +### Via Environment + +The transport can also be influenced by the relay address. If the relay URL includes a scheme: + +- `wss://relay.example.com` implies WebSocket +- `https://relay.example.com` implies H2 +- `relay.example.com:4443` (no scheme) implies QUIC + +--- + +## Troubleshooting + +### "Connection refused" or "Connection timed out" + +Your network may be blocking the transport's port/protocol. + +```bash +# Try WebSocket (most compatible) +localup -p 3000 --transport websocket -r relay.example.com:443 + +# Enable debug logging to see transport negotiation +localup -p 3000 --log-level debug -r relay.example.com:4443 +``` + +### Slow performance + +If tunnels feel slow: + +1. Check if you're using QUIC (best performance) +2. For TCP-based transports (WebSocket, H2), latency is higher due to TCP head-of-line blocking +3. Try switching transports to see if performance improves + +### Transport mismatch + +The client and relay must support the same transport. If the relay only has QUIC configured but the client requests WebSocket, the connection will fail. + +Check available transports: + +```bash +curl https://relay.example.com/.well-known/localup-protocols +``` diff --git a/docs/tunnel-security.md b/docs/tunnel-security.md new file mode 100644 index 0000000..f9e0565 --- /dev/null +++ b/docs/tunnel-security.md @@ -0,0 +1,219 @@ +# Tunnel Security + +localup provides multiple layers of security to protect your tunnels. This guide covers JWT authentication, HTTP authentication, and IP allowlisting. + +--- + +## JWT Authentication (Relay Access) + +JWT tokens control who can create tunnels on a relay server. Every tunnel connection requires a valid token signed with the relay's secret. + +### Generating Tokens + +```bash +# Basic token (24-hour validity) +localup generate-token --secret "my-relay-secret" --sub myapp + +# Custom validity period +localup generate-token --secret "my-relay-secret" --sub myapp --hours 48 + +# Restrict to specific subdomains +localup generate-token --secret "my-relay-secret" --sub myapp \ + --allowed-subdomain "myapp" \ + --allowed-subdomain "myapp-*" + +# Token for reverse tunnel access +localup generate-token --secret "my-relay-secret" --sub myapp \ + --reverse-tunnel \ + --agent my-agent-id + +# Script-friendly output (token only) +TOKEN=$(localup generate-token --secret "my-relay-secret" --sub myapp --token-only) +``` + +### Using Tokens + +```bash +# Via CLI flag +localup -p 3000 -r relay.example.com:4443 -t "$TOKEN" + +# Via environment variable +export TUNNEL_AUTH_TOKEN="$TOKEN" +localup -p 3000 -r relay.example.com:4443 + +# Via stored config +localup config set-token "$TOKEN" +localup -p 3000 -r relay.example.com:4443 +``` + +### Relay-Side Setup + +The relay validates tokens using the same secret: + +```bash +localup relay http \ + --jwt-secret "my-relay-secret" \ + --http-addr 0.0.0.0:80 +``` + +The secret must match exactly between token generation and relay validation. + +### Token Claims + +Tokens can restrict what the bearer is allowed to do: + +| Claim | Purpose | +|-------|---------| +| `sub` | Tunnel identifier | +| `exp` | Expiration timestamp | +| `allowed-subdomain` | Glob patterns for allowed subdomains | +| `reverse-tunnel` | Whether reverse tunnel access is permitted | +| `agent` | Which agent IDs the token can access | +| `allowed-address` | Which target addresses are permitted | + +--- + +## HTTP Authentication (Tunnel Access) + +Protect your tunnel endpoints so that only authorized users can access them. This is independent of JWT authentication -- JWT controls who creates the tunnel, HTTP auth controls who accesses it. + +### Basic Authentication + +Add username/password protection to your tunnel: + +```bash +# Single user +localup -p 3000 --basic-auth "admin:secretpassword" -r relay.example.com:4443 -t $TOKEN + +# Multiple users +localup -p 3000 \ + --basic-auth "admin:adminpass" \ + --basic-auth "developer:devpass" \ + --basic-auth "viewer:viewpass" \ + -r relay.example.com:4443 -t $TOKEN +``` + +When enabled, visitors to your tunnel URL will see a browser login prompt. Requests without valid credentials receive a `401 Unauthorized` response. + +### Bearer Token Authentication + +Protect your tunnel with bearer tokens (useful for API endpoints): + +```bash +# Single token +localup -p 3000 --auth-token "my-api-key-123" -r relay.example.com:4443 -t $TOKEN + +# Multiple tokens +localup -p 3000 \ + --auth-token "production-key" \ + --auth-token "staging-key" \ + -r relay.example.com:4443 -t $TOKEN +``` + +Clients must include the `Authorization: Bearer ` header: + +```bash +curl -H "Authorization: Bearer my-api-key-123" https://myapp.relay.example.com/api/data +``` + +### Combining Auth Methods + +You can combine Basic Auth, Bearer tokens, and IP allowlisting. A request passes if it satisfies **any** of the configured auth methods: + +```bash +localup -p 3000 \ + --basic-auth "admin:secret" \ + --auth-token "api-key-123" \ + --allow-ip 10.0.0.0/8 \ + -r relay.example.com:4443 -t $TOKEN +``` + +--- + +## IP Allowlisting + +Restrict tunnel access to specific IP addresses or CIDR ranges. + +### Usage + +```bash +# Single IP +localup -p 3000 --allow-ip 203.0.113.50 -r relay.example.com:4443 -t $TOKEN + +# CIDR range +localup -p 3000 --allow-ip 10.0.0.0/8 -r relay.example.com:4443 -t $TOKEN + +# Multiple rules +localup -p 3000 \ + --allow-ip 192.168.1.0/24 \ + --allow-ip 10.0.0.0/8 \ + --allow-ip 203.0.113.50 \ + -r relay.example.com:4443 -t $TOKEN +``` + +### In Configuration File + +```yaml +tunnels: + - name: internal-api + port: 8080 + protocol: http + subdomain: internal + allow_ips: + - 10.0.0.0/8 # Private network + - 192.168.0.0/16 # Private network + - 172.16.0.0/12 # Private network + - 203.0.113.50 # Office public IP +``` + +Requests from IPs not in the allowlist receive a `403 Forbidden` response. + +--- + +## Transport Security + +All tunnel traffic is encrypted regardless of the transport protocol: + +| Transport | Encryption | Port | +|-----------|-----------|------| +| QUIC | TLS 1.3 (built-in) | 4443/UDP | +| WebSocket | TLS 1.2/1.3 (wss://) | 443/TCP | +| HTTP/2 | TLS 1.2/1.3 (h2) | 443/TCP | + +### TLS Certificate Verification + +By default, clients verify the relay's TLS certificate. For development with self-signed certificates: + +```bash +# Skip verification (development only!) +localup -p 3000 -r localhost:4443 --insecure +``` + +Never use `--insecure` in production. + +--- + +## Security Best Practices + +1. **Use strong JWT secrets** - At least 32 characters, random. Never commit secrets to git. + +2. **Set short token expiry** - Use the shortest practical validity period. + +3. **Use HTTPS tunnels** when exposing web services - This provides TLS termination at the relay, encrypting traffic between external clients and the relay. + +4. **Combine security layers** - Use JWT + HTTP Auth + IP allowlisting together for defense in depth. + +5. **Use environment variables for secrets**: + ```bash + export TUNNEL_AUTH_TOKEN="..." + export JWT_SECRET="..." + ``` + +6. **Rotate tokens regularly** - Generate new tokens and update clients periodically. + +7. **Use subdomain restrictions** in JWT claims to prevent token misuse: + ```bash + localup generate-token --secret "$SECRET" --sub myapp --allowed-subdomain "myapp" + ``` + +8. **Monitor tunnel access** - Use the REST API or dashboard to review captured requests and connections. From b457519ae6965a8fedd870fd2d1fd136cf8a95ec Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 15 Mar 2026 06:55:47 +0100 Subject: [PATCH 2/5] chore: add desktop app release script Local release workflow for the Tauri desktop app with version bumping, building, optional Apple notarization, Tauri auto-updater support, and GitHub Release creation. --- scripts/release-desktop.sh | 691 +++++++++++++++++++++++++++++++++++++ 1 file changed, 691 insertions(+) create mode 100755 scripts/release-desktop.sh diff --git a/scripts/release-desktop.sh b/scripts/release-desktop.sh new file mode 100755 index 0000000..1c07391 --- /dev/null +++ b/scripts/release-desktop.sh @@ -0,0 +1,691 @@ +#!/bin/bash +set -e + +# ============================================================================= +# LocalUp Desktop Release Script +# ============================================================================= +# +# Complete local release workflow for the Tauri desktop app: +# 1. Bump version across all config files +# 2. Build frontend (Vite + React) +# 3. Build Tauri app (creates .app, .dmg, updater artifacts) +# 4. Optionally notarize DMG with Apple +# 5. Generate latest.json for Tauri auto-updater +# 6. Create GitHub Release with all artifacts via `gh` +# +# Usage: +# ./scripts/release-desktop.sh [options] +# +# Arguments: +# Version to release (e.g. 0.2.0, 0.2.0-beta.1) +# +# Options: +# --skip-build Skip the build step (use existing artifacts) +# --skip-notarize Skip Apple notarization (default: skipped unless creds set) +# --skip-tests Skip running tests before building +# --draft Create the GitHub release as a draft +# --dry-run Show what would be done without making changes +# --no-push Build and prepare artifacts but don't create GitHub release +# +# Required Environment Variables (for notarization): +# APPLE_ID Apple ID for notarization +# APPLE_ID_PASSWORD App-specific password +# APPLE_TEAM_ID Apple Developer Team ID +# +# Optional Environment Variables: +# TAURI_SIGNING_PRIVATE_KEY Tauri updater signing key +# TAURI_SIGNING_PRIVATE_KEY_PASSWORD Password for Tauri signing key +# +# Examples: +# ./scripts/release-desktop.sh 0.2.0 +# ./scripts/release-desktop.sh 0.2.0 --skip-tests --draft +# ./scripts/release-desktop.sh 0.2.0 --dry-run +# ./scripts/release-desktop.sh 0.2.0 --skip-build # reuse last build +# +# ============================================================================= + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +print_header() { + echo "" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${CYAN} $1${NC}" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} +print_step() { echo -e " ${BLUE}▸${NC} $1"; } +print_success() { echo -e " ${GREEN}✓${NC} $1"; } +print_warning() { echo -e " ${YELLOW}⚠${NC} $1"; } +print_error() { echo -e " ${RED}✗${NC} $1"; } + +# ── Parse arguments ────────────────────────────────────────────────────────── + +VERSION="" +SKIP_BUILD=false +SKIP_NOTARIZE=true # default: skip unless Apple creds are present +SKIP_TESTS=false +DRAFT=false +DRY_RUN=false +NO_PUSH=false + +usage() { + head -42 "$0" | tail -37 + exit 1 +} + +while [[ $# -gt 0 ]]; do + case $1 in + --skip-build) SKIP_BUILD=true; shift ;; + --skip-notarize) SKIP_NOTARIZE=true; shift ;; + --skip-tests) SKIP_TESTS=true; shift ;; + --draft) DRAFT=true; shift ;; + --dry-run) DRY_RUN=true; shift ;; + --no-push) NO_PUSH=true; shift ;; + -h|--help) usage ;; + -*) + print_error "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + *) + if [ -z "$VERSION" ]; then + VERSION="$1" + else + print_error "Unexpected argument: $1" + exit 1 + fi + shift + ;; + esac +done + +if [ -z "$VERSION" ]; then + print_error "Version argument is required" + echo "" + usage +fi + +# Validate version format +if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.]+)?$ ]]; then + print_error "Invalid version format: $VERSION" + echo " Expected: MAJOR.MINOR.PATCH (e.g. 0.2.0) or MAJOR.MINOR.PATCH-PRE (e.g. 0.2.0-beta.1)" + exit 1 +fi + +TAG="v$VERSION" + +# Auto-enable notarization if Apple credentials are available +if [ -n "$APPLE_ID" ] && [ -n "$APPLE_ID_PASSWORD" ] && [ -n "$APPLE_TEAM_ID" ]; then + SKIP_NOTARIZE=false +fi + +# Detect pre-release +IS_PRERELEASE=false +if [[ "$VERSION" =~ (alpha|beta|rc|-[a-zA-Z]) ]]; then + IS_PRERELEASE=true +fi + +# ── Change to project root ─────────────────────────────────────────────────── + +cd "$(dirname "$0")/.." +PROJECT_ROOT=$(pwd) +DESKTOP_DIR="$PROJECT_ROOT/apps/localup-desktop" +TAURI_DIR="$DESKTOP_DIR/src-tauri" + +# ── Architecture detection ─────────────────────────────────────────────────── + +ARCH=$(uname -m) +case "$ARCH" in + arm64|aarch64) RUST_TARGET="aarch64-apple-darwin"; ARCH_LABEL="aarch64" ;; + x86_64) RUST_TARGET="x86_64-apple-darwin"; ARCH_LABEL="x86_64" ;; + *) + print_error "Unsupported architecture: $ARCH" + exit 1 + ;; +esac + +# ── Paths ──────────────────────────────────────────────────────────────────── + +# Tauri bundles go to workspace root target (since it's a workspace member) +BUNDLE_DIR="$PROJECT_ROOT/target/$RUST_TARGET/release/bundle" +# Fallback: sometimes Tauri puts bundles under the app's own target +BUNDLE_DIR_ALT="$TAURI_DIR/target/$RUST_TARGET/release/bundle" + +OUTPUT_DIR="$PROJECT_ROOT/release/$VERSION" + +# ============================================================================= +# Validate Environment +# ============================================================================= +print_header "Validating Environment" + +# Check we're on macOS +if [ "$(uname)" != "Darwin" ]; then + print_error "This script only runs on macOS (Tauri desktop build)" + exit 1 +fi +print_success "Running on macOS ($ARCH)" + +# Check required tools +MISSING_TOOLS=() +for cmd in bun cargo gh; do + if ! command -v "$cmd" &> /dev/null; then + MISSING_TOOLS+=("$cmd") + fi +done + +if [ ${#MISSING_TOOLS[@]} -ne 0 ]; then + print_error "Missing required tools: ${MISSING_TOOLS[*]}" + echo "" + echo " Install with:" + echo " bun → curl -fsSL https://bun.sh/install | bash" + echo " cargo → curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" + echo " gh → brew install gh" + exit 1 +fi +print_success "Required tools: bun, cargo, gh" + +# Check gh is authenticated +if ! gh auth status &> /dev/null; then + print_error "GitHub CLI is not authenticated. Run: gh auth login" + exit 1 +fi +print_success "GitHub CLI authenticated" + +# Check Tauri CLI is available +if ! (cd "$DESKTOP_DIR" && bun run tauri --version) &> /dev/null; then + print_warning "Tauri CLI not found, will install via bun" +fi + +# Notarization credentials check +if [ "$SKIP_NOTARIZE" = false ]; then + if [ -z "$APPLE_ID" ] || [ -z "$APPLE_ID_PASSWORD" ] || [ -z "$APPLE_TEAM_ID" ]; then + print_error "Apple credentials required for notarization" + echo " Set: APPLE_ID, APPLE_ID_PASSWORD, APPLE_TEAM_ID" + echo " Or notarization will be auto-skipped" + SKIP_NOTARIZE=true + else + print_success "Apple credentials configured" + fi +fi + +# Tauri updater signing key check +HAS_SIGNING_KEY=false +if [ -n "$TAURI_SIGNING_PRIVATE_KEY" ]; then + HAS_SIGNING_KEY=true + print_success "Tauri signing key configured" +else + print_warning "No TAURI_SIGNING_PRIVATE_KEY set - updater signatures will be missing" +fi + +# Check if tag already exists +if git rev-parse "$TAG" &> /dev/null; then + print_error "Tag $TAG already exists" + echo "" + echo " To delete it: git tag -d $TAG && git push origin :refs/tags/$TAG" + exit 1 +fi +print_success "Tag $TAG is available" + +# Check for uncommitted changes +if [ -n "$(git status -s)" ]; then + print_warning "You have uncommitted changes" + if [ "$DRY_RUN" = false ]; then + read -p " Continue anyway? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo " Aborted." + exit 1 + fi + fi +fi + +# Summary +echo "" +echo -e " ${BOLD}Release configuration:${NC}" +echo " Version: $VERSION ($TAG)" +echo " Pre-release: $IS_PRERELEASE" +echo " Architecture: $ARCH_LABEL ($RUST_TARGET)" +echo " Skip Build: $SKIP_BUILD" +echo " Skip Tests: $SKIP_TESTS" +echo " Notarize: $([ "$SKIP_NOTARIZE" = true ] && echo 'No' || echo 'Yes')" +echo " Signing Key: $([ "$HAS_SIGNING_KEY" = true ] && echo 'Yes' || echo 'No')" +echo " Draft Release: $DRAFT" +echo " Dry Run: $DRY_RUN" +echo " No Push: $NO_PUSH" + +if [ "$DRY_RUN" = false ]; then + echo "" + read -p " Proceed? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo " Aborted." + exit 0 + fi +fi + +# ============================================================================= +# Step 1: Bump Version +# ============================================================================= +print_header "Step 1: Bump Version to $VERSION" + +# Files that contain the version +TAURI_CONF="$TAURI_DIR/tauri.conf.json" +DESKTOP_CARGO="$TAURI_DIR/Cargo.toml" +DESKTOP_PKG="$DESKTOP_DIR/package.json" + +bump_version_in_file() { + local file="$1" + local description="$2" + + if [ "$DRY_RUN" = true ]; then + echo " [DRY RUN] Would update $description" + return + fi + + case "$file" in + *.json) + # Use a temp file approach for JSON version bumping + if [[ "$file" == *tauri.conf.json ]]; then + # tauri.conf.json: "version": "X.Y.Z" at top level + sed -i '' 's/"version": "[^"]*"/"version": "'"$VERSION"'"/' "$file" + else + # package.json: "version": "X.Y.Z" (first occurrence) + sed -i '' '0,/"version": "[^"]*"/{s/"version": "[^"]*"/"version": "'"$VERSION"'"/}' "$file" + fi + ;; + *.toml) + # Cargo.toml: version = "X.Y.Z" (first occurrence under [package]) + sed -i '' '0,/^version = "[^"]*"/{s/^version = "[^"]*"/version = "'"$VERSION"'"/}' "$file" + ;; + esac + + print_success "Updated $description" +} + +bump_version_in_file "$TAURI_CONF" "tauri.conf.json" +bump_version_in_file "$DESKTOP_CARGO" "src-tauri/Cargo.toml" +bump_version_in_file "$DESKTOP_PKG" "package.json" + +# Verify versions match +if [ "$DRY_RUN" = false ]; then + V_TAURI=$(grep '"version"' "$TAURI_CONF" | head -1 | sed 's/.*"version": "\([^"]*\)".*/\1/') + V_CARGO=$(grep '^version' "$DESKTOP_CARGO" | head -1 | sed 's/version = "\([^"]*\)"/\1/') + V_PKG=$(grep '"version"' "$DESKTOP_PKG" | head -1 | sed 's/.*"version": "\([^"]*\)".*/\1/') + + if [ "$V_TAURI" != "$VERSION" ] || [ "$V_CARGO" != "$VERSION" ] || [ "$V_PKG" != "$VERSION" ]; then + print_error "Version mismatch after bump!" + echo " tauri.conf.json: $V_TAURI" + echo " Cargo.toml: $V_CARGO" + echo " package.json: $V_PKG" + exit 1 + fi + print_success "All versions set to $VERSION" +fi + +# ============================================================================= +# Step 2: Run Tests +# ============================================================================= +if [ "$SKIP_TESTS" = false ] && [ "$SKIP_BUILD" = false ]; then + print_header "Step 2: Running Tests" + + if [ "$DRY_RUN" = true ]; then + echo " [DRY RUN] Would run: cargo test --workspace --quiet" + else + print_step "Running workspace tests..." + if cargo test --workspace --quiet 2>&1; then + print_success "All tests passed" + else + print_error "Tests failed. Fix them before releasing." + exit 1 + fi + fi +else + print_header "Step 2: Tests (Skipped)" +fi + +# ============================================================================= +# Step 3: Build Tauri App +# ============================================================================= +if [ "$SKIP_BUILD" = false ]; then + print_header "Step 3: Building Tauri Desktop App" + + if [ "$DRY_RUN" = true ]; then + echo " [DRY RUN] Would run: bun install && bun run tauri build --target $RUST_TARGET" + else + print_step "Installing frontend dependencies..." + (cd "$DESKTOP_DIR" && bun install --frozen-lockfile 2>/dev/null || bun install) + print_success "Frontend dependencies installed" + + print_step "Building Tauri app (this may take a while)..." + (cd "$DESKTOP_DIR" && bun run tauri build --target "$RUST_TARGET") + print_success "Tauri build completed" + fi +else + print_header "Step 3: Build (Skipped)" + print_warning "Using existing build artifacts" +fi + +# ============================================================================= +# Step 4: Locate and Verify Artifacts +# ============================================================================= +print_header "Step 4: Locating Build Artifacts" + +# Determine actual bundle directory +if [ -d "$BUNDLE_DIR" ]; then + ACTUAL_BUNDLE_DIR="$BUNDLE_DIR" +elif [ -d "$BUNDLE_DIR_ALT" ]; then + ACTUAL_BUNDLE_DIR="$BUNDLE_DIR_ALT" +else + if [ "$DRY_RUN" = true ]; then + echo " [DRY RUN] Would look for bundles in:" + echo " $BUNDLE_DIR" + echo " $BUNDLE_DIR_ALT" + ACTUAL_BUNDLE_DIR="$BUNDLE_DIR" + else + print_error "Bundle directory not found" + echo " Checked:" + echo " $BUNDLE_DIR" + echo " $BUNDLE_DIR_ALT" + exit 1 + fi +fi + +# Find DMG +DMG_FILE="" +if [ "$DRY_RUN" = false ]; then + # Tauri names DMGs like: LocalUp_0.2.0_aarch64.dmg + for f in "$ACTUAL_BUNDLE_DIR/dmg/"*.dmg; do + if [ -f "$f" ]; then + DMG_FILE="$f" + break + fi + done + + if [ -z "$DMG_FILE" ]; then + print_error "No .dmg found in $ACTUAL_BUNDLE_DIR/dmg/" + exit 1 + fi + DMG_SIZE=$(ls -lh "$DMG_FILE" | awk '{print $5}') + print_success "DMG: $(basename "$DMG_FILE") ($DMG_SIZE)" +fi + +# Find updater artifacts (.tar.gz and .sig in macos/ dir) +TAR_GZ_FILE="" +SIG_FILE="" +if [ "$DRY_RUN" = false ]; then + for f in "$ACTUAL_BUNDLE_DIR/macos/"*.tar.gz; do + if [ -f "$f" ]; then + TAR_GZ_FILE="$f" + break + fi + done + for f in "$ACTUAL_BUNDLE_DIR/macos/"*.tar.gz.sig; do + if [ -f "$f" ]; then + SIG_FILE="$f" + break + fi + done + + if [ -n "$TAR_GZ_FILE" ]; then + TAR_SIZE=$(ls -lh "$TAR_GZ_FILE" | awk '{print $5}') + print_success "Updater tar.gz: $(basename "$TAR_GZ_FILE") ($TAR_SIZE)" + else + print_warning "No updater .tar.gz found (auto-update won't work)" + fi + + if [ -n "$SIG_FILE" ]; then + print_success "Updater signature: $(basename "$SIG_FILE")" + else + print_warning "No updater .sig found (auto-update won't work)" + fi +fi + +# ============================================================================= +# Step 5: Notarize DMG (macOS only) +# ============================================================================= +print_header "Step 5: Notarize DMG" + +if [ "$SKIP_NOTARIZE" = true ]; then + print_warning "Notarization skipped" + echo " Users may need to run: xattr -cr /Applications/LocalUp.app" +elif [ "$DRY_RUN" = true ]; then + echo " [DRY RUN] Would notarize: $(basename "$DMG_FILE")" +else + print_step "Submitting to Apple notary service..." + xcrun notarytool submit "$DMG_FILE" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_ID_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + + print_step "Stapling notarization ticket..." + xcrun stapler staple "$DMG_FILE" + print_success "Notarization complete" +fi + +# ============================================================================= +# Step 6: Prepare Release Directory +# ============================================================================= +print_header "Step 6: Preparing Release Artifacts" + +mkdir -p "$OUTPUT_DIR" + +# Standardized artifact names for the release +RELEASE_DMG_NAME="LocalUp-macos-${ARCH_LABEL}.dmg" +RELEASE_TAR_NAME="LocalUp-macos-${ARCH_LABEL}.app.tar.gz" + +if [ "$DRY_RUN" = true ]; then + echo " [DRY RUN] Would copy artifacts to $OUTPUT_DIR" +else + # Copy DMG + cp "$DMG_FILE" "$OUTPUT_DIR/$RELEASE_DMG_NAME" + print_success "Prepared: $RELEASE_DMG_NAME" + + # Copy updater tar.gz + if [ -n "$TAR_GZ_FILE" ]; then + cp "$TAR_GZ_FILE" "$OUTPUT_DIR/$RELEASE_TAR_NAME" + print_success "Prepared: $RELEASE_TAR_NAME" + fi + + # Copy signature + if [ -n "$SIG_FILE" ]; then + SIGNATURE=$(cat "$SIG_FILE") + echo "$SIGNATURE" > "$OUTPUT_DIR/${RELEASE_TAR_NAME}.sig" + print_success "Prepared: ${RELEASE_TAR_NAME}.sig" + else + SIGNATURE="" + fi + + # Generate checksums + (cd "$OUTPUT_DIR" && shasum -a 256 *.dmg *.tar.gz 2>/dev/null > SHA256SUMS.txt || true) + print_success "Generated: SHA256SUMS.txt" +fi + +# ============================================================================= +# Step 7: Generate latest.json for Tauri Auto-Updater +# ============================================================================= +print_header "Step 7: Generating latest.json" + +PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +# GitHub release download URL +REPO_URL=$(gh repo view --json url -q '.url' 2>/dev/null || echo "https://github.com/localup-dev/localup") +RELEASE_URL="$REPO_URL/releases/download/$TAG" + +LATEST_JSON="$OUTPUT_DIR/latest.json" + +# Build platforms object based on current architecture +# (CI builds for all architectures; local builds only for current machine) +if [ "$ARCH_LABEL" = "aarch64" ]; then + PLATFORM_KEY="darwin-aarch64" +else + PLATFORM_KEY="darwin-x86_64" +fi + +if [ "$DRY_RUN" = true ]; then + echo " [DRY RUN] Would generate latest.json with:" + echo " version: $VERSION" + echo " platform: $PLATFORM_KEY" + echo " url: $RELEASE_URL/$RELEASE_DMG_NAME" +else + cat > "$LATEST_JSON" << EOF +{ + "version": "$VERSION", + "notes": "See $REPO_URL/releases/tag/$TAG for release notes.", + "pub_date": "$PUB_DATE", + "platforms": { + "$PLATFORM_KEY": { + "url": "$RELEASE_URL/$RELEASE_DMG_NAME", + "signature": "$SIGNATURE" + } + } +} +EOF + + print_success "Generated: latest.json" + echo "" + cat "$LATEST_JSON" +fi + +# ============================================================================= +# Step 8: Create Git Tag +# ============================================================================= +print_header "Step 8: Git Tag" + +if [ "$DRY_RUN" = true ]; then + echo " [DRY RUN] Would create tag: $TAG" +elif [ "$NO_PUSH" = true ]; then + print_warning "Skipping git tag (--no-push)" +else + # Commit version bump if there are changes + if [ -n "$(git status -s)" ]; then + print_step "Committing version bump..." + git add \ + "$TAURI_CONF" \ + "$DESKTOP_CARGO" \ + "$DESKTOP_PKG" + git commit -m "chore: bump desktop app version to $VERSION" + print_success "Version bump committed" + fi + + print_step "Creating tag $TAG..." + git tag -a "$TAG" -m "Release $TAG" + print_success "Tag $TAG created" + + print_step "Pushing commit and tag..." + git push origin HEAD + git push origin "$TAG" + print_success "Pushed to remote" +fi + +# ============================================================================= +# Step 9: Create GitHub Release +# ============================================================================= +print_header "Step 9: GitHub Release" + +if [ "$NO_PUSH" = true ]; then + print_warning "Skipping GitHub release (--no-push)" + echo "" + echo " Artifacts are in: $OUTPUT_DIR/" + echo "" + echo " To create the release manually:" + echo " gh release create $TAG $OUTPUT_DIR/* --title \"Release $TAG\"" +elif [ "$DRY_RUN" = true ]; then + echo " [DRY RUN] Would create GitHub release $TAG with:" + echo " $RELEASE_DMG_NAME" + [ -n "$TAR_GZ_FILE" ] && echo " $RELEASE_TAR_NAME" + [ -n "$SIG_FILE" ] && echo " ${RELEASE_TAR_NAME}.sig" + echo " latest.json" + echo " SHA256SUMS.txt" +else + print_step "Creating GitHub release..." + + # Build file list + RELEASE_FILES=() + RELEASE_FILES+=("$OUTPUT_DIR/$RELEASE_DMG_NAME") + [ -f "$OUTPUT_DIR/$RELEASE_TAR_NAME" ] && RELEASE_FILES+=("$OUTPUT_DIR/$RELEASE_TAR_NAME") + [ -f "$OUTPUT_DIR/${RELEASE_TAR_NAME}.sig" ] && RELEASE_FILES+=("$OUTPUT_DIR/${RELEASE_TAR_NAME}.sig") + [ -f "$OUTPUT_DIR/latest.json" ] && RELEASE_FILES+=("$OUTPUT_DIR/latest.json") + [ -f "$OUTPUT_DIR/SHA256SUMS.txt" ] && RELEASE_FILES+=("$OUTPUT_DIR/SHA256SUMS.txt") + + # Build gh flags + GH_FLAGS=() + GH_FLAGS+=("--title" "Release $TAG") + [ "$DRAFT" = true ] && GH_FLAGS+=("--draft") + [ "$IS_PRERELEASE" = true ] && GH_FLAGS+=("--prerelease") + + NOTARIZE_NOTE="" + if [ "$SKIP_NOTARIZE" = true ]; then + NOTARIZE_NOTE='> **Note for macOS users:** The app is not yet notarized. If you see _"LocalUp is damaged and can'\''t be opened"_, run: +> ```bash +> xattr -cr /Applications/LocalUp.app +> ``` +> Then open the app again.' + fi + + gh release create "$TAG" \ + "${RELEASE_FILES[@]}" \ + "${GH_FLAGS[@]}" \ + --notes "$(cat < Date: Sun, 15 Mar 2026 06:55:57 +0100 Subject: [PATCH 3/5] chore: add SQLite journal/WAL files to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 369710b..31ff5b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ /target *.db +*.db-journal +*.db-wal +*.db-shm node_modules .vscode From 1c0493045ab605afc40063ce04610d2189081cb7 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 15 Mar 2026 07:01:19 +0100 Subject: [PATCH 4/5] docs: fix inaccuracies found during testing CLI reference: - Remove --transport from Common Relay Options (only exists on relay http) - Add TLS Relay Extras section (--http-redirect-addr, --https-redirect-port, --http-passthrough-addr) - Add --oauth-client and --transport to HTTP Relay Extras - Add OAUTH_CLIENTS env var to relay server section - Document daemon add flags with correct default (https, not http) - Add --custom-domain flag to daemon add API reference: - Add 11 missing endpoints: magic link send/verify, device info/deny, ACME challenge list/cancel/restart, GET auth-token by ID, ACME HTTP-01 challenge serving, teams list --- docs/api-reference.md | 114 ++++++++++++++++++++++++++++++++++++++++++ docs/cli-reference.md | 26 +++++++++- 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 05331c2..5b3d977 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -326,6 +326,39 @@ curl -X POST http://relay:8080/api/domains/challenge/complete \ -d '{"domain": "app.example.com", "challenge_id": "challenge-uuid"}' ``` +### List Pending Challenges + +#### `GET /api/domains/{domain}/challenges` + +List pending ACME challenges for a domain. + +```bash +curl http://relay:8080/api/domains/app.example.com/challenges \ + -H "Authorization: Bearer $TOKEN" +``` + +### Cancel Challenge + +#### `POST /api/domains/{domain}/challenge/cancel` + +Cancel an ongoing ACME challenge. + +```bash +curl -X POST http://relay:8080/api/domains/app.example.com/challenge/cancel \ + -H "Authorization: Bearer $TOKEN" +``` + +### Restart Challenge + +#### `POST /api/domains/{domain}/challenge/restart` + +Restart a failed ACME challenge. + +```bash +curl -X POST http://relay:8080/api/domains/app.example.com/challenge/restart \ + -H "Authorization: Bearer $TOKEN" +``` + ### Request Certificate #### `POST /api/domains/{domain}/certificate` @@ -337,6 +370,12 @@ curl -X POST http://relay:8080/api/domains/app.example.com/certificate \ -H "Authorization: Bearer $TOKEN" ``` +### ACME HTTP-01 Challenge + +#### `GET /.well-known/acme-challenge/{token}` + +Serves ACME HTTP-01 challenge tokens. Called by Let's Encrypt servers during domain validation. No authentication required. + --- ## Authentication @@ -430,6 +469,32 @@ curl -X POST http://relay:8080/api/auth/oauth/github/callback \ --- +## Magic Link (Passwordless Login) + +### Send Magic Link + +#### `POST /api/auth/magic-link/send` + +Send a passwordless login email. Requires SMTP to be configured on the relay. + +```bash +curl -X POST http://relay:8080/api/auth/magic-link/send \ + -H "Content-Type: application/json" \ + -d '{"email": "user@example.com"}' +``` + +### Verify Magic Link + +#### `GET /api/auth/magic-link/verify` + +Verify the magic link token from the email and receive a session token. + +```bash +curl "http://relay:8080/api/auth/magic-link/verify?token=magic-link-token-here" +``` + +--- + ## Device Authorization (RFC 8628) For CLI and desktop app login without browser redirect. @@ -470,6 +535,16 @@ curl -X POST http://relay:8080/api/device/token \ Returns `authorization_pending` until the user approves, then returns the access token. +### Device Info + +#### `GET /api/device/info` + +Get information about a pending device authorization (used by the verification page). + +```bash +curl "http://relay:8080/api/device/info?user_code=ABCD-1234" +``` + ### Approve (Browser) #### `POST /api/device/verify` @@ -483,6 +558,19 @@ curl -X POST http://relay:8080/api/device/verify \ -d '{"user_code": "ABCD-1234"}' ``` +### Deny (Browser) + +#### `POST /api/device/deny` + +User denies the device authorization from the browser. + +```bash +curl -X POST http://relay:8080/api/device/deny \ + -H "Authorization: Bearer $SESSION_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"user_code": "ABCD-1234"}' +``` + --- ## API Keys (Auth Tokens) @@ -498,6 +586,17 @@ curl http://relay:8080/api/auth-tokens \ -H "Authorization: Bearer $TOKEN" ``` +### Get API Key + +#### `GET /api/auth-tokens/{id}` + +Get details for a specific API key (token value is not returned). + +```bash +curl http://relay:8080/api/auth-tokens/token-uuid \ + -H "Authorization: Bearer $TOKEN" +``` + ### Create API Key #### `POST /api/auth-tokens` @@ -533,6 +632,21 @@ curl -X DELETE http://relay:8080/api/auth-tokens/token-uuid \ --- +## Teams + +### List Teams + +#### `GET /api/teams` + +List teams for the current user. + +```bash +curl http://relay:8080/api/teams \ + -H "Authorization: Bearer $TOKEN" +``` + +--- + ## Database Configuration The relay stores captured requests and user data in a database. Configure with `--database-url`: diff --git a/docs/cli-reference.md b/docs/cli-reference.md index db37fce..d3460cb 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -209,9 +209,21 @@ Add or remove a tunnel from the daemon configuration. ```bash localup daemon add frontend -p 3000 --protocol https -s frontend +localup daemon add api -p 8080 --protocol http -s api --custom-domain api.example.com localup daemon remove frontend ``` +Available flags for `daemon add`: + +| Flag | Default | Description | +|------|---------|-------------| +| `--port ` | | Local port (required) | +| `--protocol ` | `https` | Protocol: `http`, `https`, `tcp`, `tls` | +| `--subdomain ` | | Subdomain for HTTP/HTTPS tunnels | +| `--custom-domain ` | | Custom domain (repeatable, supports wildcards) | + +Note: `daemon add` defaults to `https` protocol, unlike `localup add` which defaults to `http`. + --- ## System Service Commands @@ -277,6 +289,8 @@ localup relay http \ ### Common Relay Options +These flags are available on all relay subcommands (`tcp`, `tls`, `http`): + | Flag | Env Var | Default | Description | |------|---------|---------|-------------| | `--localup-addr ` | | `0.0.0.0:4443` | QUIC control plane address | @@ -286,7 +300,14 @@ localup relay http \ | `--tls-key ` | | auto-generated | TLS private key | | `--database-url ` | `DATABASE_URL` | in-memory SQLite | Database connection | | `--log-level ` | | `info` | Log level | -| `--transport ` | | `quic` | Control plane transport | + +### TLS Relay Extras + +| Flag | Description | +|------|-------------| +| `--http-redirect-addr ` | Optional HTTP server that redirects to HTTPS | +| `--https-redirect-port ` | HTTPS port to redirect to (default: `443`) | +| `--http-passthrough-addr ` | Optional HTTP passthrough server for plain HTTP traffic | ### API Server Options (All Relay Types) @@ -311,10 +332,12 @@ localup relay http \ | Flag | Env Var | Description | |------|---------|-------------| +| `--transport ` | | Control plane transport: `quic`, `h2`, `websocket` (default: `quic`) | | `--acme-email ` | `ACME_EMAIL` | Let's Encrypt email | | `--acme-staging` | | Use Let's Encrypt staging | | `--acme-cert-dir ` | | ACME cert directory (default: `/opt/localup/certs/acme`) | | `--websocket-path ` | | WebSocket endpoint (default: `/localup`) | +| `--oauth-client ` | `OAUTH_CLIENTS` | Register OAuth client for Device Authorization Grant (RFC 8628) | | `--smtp-host ` | `SMTP_HOST` | SMTP server for email features | | `--smtp-port ` | `SMTP_PORT` | SMTP port (default: `587`) | | `--smtp-username ` | `SMTP_USERNAME` | SMTP username | @@ -479,6 +502,7 @@ Remove the stored authentication token. | `SMTP_USERNAME` | SMTP username | | `SMTP_PASSWORD` | SMTP password | | `SMTP_FROM` | Sender email address | +| `OAUTH_CLIENTS` | OAuth client registrations for Device Authorization Grant | ### Agent From 4d241ce68590194217e33e9103def4d4be8d8888 Mon Sep 17 00:00:00 2001 From: David Viejo Date: Sun, 15 Mar 2026 07:35:37 +0100 Subject: [PATCH 5/5] fix(ci): add system deps, Bun, and fix Node.js SDK lint errors CI pipeline fixes: - Add GTK/glib/WebKit system dependencies for Tauri desktop build - Add Bun to lint, test, build, and E2E jobs (needed by localup-client build script) Node.js SDK lint fixes: - Sort imports alphabetically (biome organizeImports) - Use Number.parseInt instead of global parseInt - Use string literals instead of unnecessary template literals - Add trailing commas per biome formatter - Break long lines per biome line width (100) - Disable noExplicitAny for quic.ts (optional dynamic imports) --- .github/workflows/ci.yml | 40 ++++++++++++++++++++++ sdks/nodejs/biome.json | 14 +++++++- sdks/nodejs/src/client.ts | 46 +++++++++++++++----------- sdks/nodejs/src/protocol/codec.ts | 11 +++--- sdks/nodejs/src/protocol/types.ts | 8 +---- sdks/nodejs/src/transport/h2.ts | 17 ++++++---- sdks/nodejs/src/transport/quic.ts | 19 ++++------- sdks/nodejs/src/transport/websocket.ts | 11 +++--- 8 files changed, 111 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dffd0ef..27846c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,16 @@ jobs: name: webapps-ci path: webapps/ + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libglib2.0-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: @@ -122,6 +132,16 @@ jobs: name: webapps-ci path: webapps/ + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libglib2.0-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: @@ -156,6 +176,16 @@ jobs: name: webapps-ci path: webapps/ + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libglib2.0-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: @@ -186,6 +216,16 @@ jobs: name: webapps-ci path: webapps/ + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libglib2.0-dev libsoup-3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: diff --git a/sdks/nodejs/biome.json b/sdks/nodejs/biome.json index 27cae74..dd6a46e 100644 --- a/sdks/nodejs/biome.json +++ b/sdks/nodejs/biome.json @@ -28,7 +28,19 @@ "semicolons": "always" } }, + "overrides": [ + { + "include": ["src/transport/quic.ts"], + "linter": { + "rules": { + "suspicious": { + "noExplicitAny": "off" + } + } + } + } + ], "files": { - "ignore": ["dist", "node_modules"] + "ignore": ["dist/**", "node_modules/**"] } } diff --git a/sdks/nodejs/src/client.ts b/sdks/nodejs/src/client.ts index c6b92b9..37a3847 100644 --- a/sdks/nodejs/src/client.ts +++ b/sdks/nodejs/src/client.ts @@ -16,16 +16,16 @@ * ``` */ +import { EventEmitter } from "node:events"; import * as http from "node:http"; import * as https from "node:https"; import * as net from "node:net"; -import { EventEmitter } from "node:events"; -import type { TransportConnection, TransportStream, TransportConnector } from "./transport/base.ts"; -import { WebSocketConnector } from "./transport/websocket.ts"; -import { H2Connector } from "./transport/h2.ts"; -import { QuicConnector, isQuicAvailable, getQuicUnavailableReason } from "./transport/quic.ts"; -import type { TunnelMessage, Protocol, Endpoint, TunnelConfig } from "./protocol/types.ts"; +import type { Endpoint, Protocol, TunnelConfig, TunnelMessage } from "./protocol/types.ts"; import { createDefaultTunnelConfig } from "./protocol/types.ts"; +import type { TransportConnection, TransportConnector, TransportStream } from "./transport/base.ts"; +import { H2Connector } from "./transport/h2.ts"; +import { QuicConnector, getQuicUnavailableReason, isQuicAvailable } from "./transport/quic.ts"; +import { WebSocketConnector } from "./transport/websocket.ts"; import { logger } from "./utils/logger.ts"; // ============================================================================ @@ -129,7 +129,7 @@ function parseRelayAddress(relay: string): { host: string; port: number } { if (parts.length === 2) { return { host: parts[0]!, - port: parseInt(parts[1]!, 10), + port: Number.parseInt(parts[1]!, 10), }; } return { @@ -215,7 +215,7 @@ class LocalupListener extends EventEmitter implements Listener { endpoints: Endpoint[], localAddr: string, localPort: number, - localHttps: boolean + localHttps: boolean, ) { super(); this.connection = connection; @@ -228,7 +228,11 @@ class LocalupListener extends EventEmitter implements Listener { // Connection-pooling agents (matches Rust HttpProxy MAX_POOL_SIZE = 10) this.httpAgent = new http.Agent({ keepAlive: true, maxSockets: 10 }); - this.httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 10, rejectUnauthorized: false }); + this.httpsAgent = new https.Agent({ + keepAlive: true, + maxSockets: 10, + rejectUnauthorized: false, + }); this.closePromise = new Promise((resolve) => { this.closeResolve = resolve; @@ -259,9 +263,9 @@ class LocalupListener extends EventEmitter implements Listener { logger.debug(`Received ping (timestamp: ${msg.timestamp}), sending pong...`); await this.controlStream.sendMessage({ type: "Pong", - timestamp: msg.timestamp + timestamp: msg.timestamp, }); - logger.debug(`Sent pong response`); + logger.debug("Sent pong response"); break; case "Disconnect": logger.info(`Relay disconnected: ${msg.reason}`); @@ -402,7 +406,7 @@ class LocalupListener extends EventEmitter implements Listener { private async handleHttpRequest( stream: TransportStream, - msg: Extract + msg: Extract, ): Promise { const { method, uri, headers, body, streamId } = msg; @@ -501,7 +505,7 @@ class LocalupListener extends EventEmitter implements Listener { private async handleHttpStream( stream: TransportStream, - msg: Extract + msg: Extract, ): Promise { const { streamId, initialData } = msg; @@ -551,7 +555,7 @@ class LocalupListener extends EventEmitter implements Listener { private async handleTcpConnect( stream: TransportStream, - msg: Extract + msg: Extract, ): Promise { const { streamId } = msg; @@ -605,7 +609,7 @@ class LocalupListener extends EventEmitter implements Listener { }, () => { resolve(socket); - } + }, ); socket.on("error", reject); @@ -663,16 +667,18 @@ export async function forward(options: ForwardOptions): Promise { const parts = options.addr.split(":"); if (parts.length === 2) { localHost = parts[0]!; - localPort = parseInt(parts[1]!, 10); + localPort = Number.parseInt(parts[1]!, 10); } else { - localPort = parseInt(options.addr, 10); + localPort = Number.parseInt(options.addr, 10); } } // Get auth token const authToken = options.authtoken ?? process.env.LOCALUP_AUTHTOKEN ?? ""; if (!authToken) { - throw new Error("Authentication token is required. Set authtoken option or LOCALUP_AUTHTOKEN env."); + throw new Error( + "Authentication token is required. Set authtoken option or LOCALUP_AUTHTOKEN env.", + ); } // Get relay address @@ -710,7 +716,7 @@ export async function forward(options: ForwardOptions): Promise { }); } else { throw new Error( - `QUIC transport requested but not available. ${getQuicUnavailableReason()}` + `QUIC transport requested but not available. ${getQuicUnavailableReason()}`, ); } break; @@ -787,7 +793,7 @@ export async function forward(options: ForwardOptions): Promise { response.endpoints, localHost, localPort, - proto === "https" + proto === "https", ); } diff --git a/sdks/nodejs/src/protocol/codec.ts b/sdks/nodejs/src/protocol/codec.ts index 8d7e8c6..ebf4eb8 100644 --- a/sdks/nodejs/src/protocol/codec.ts +++ b/sdks/nodejs/src/protocol/codec.ts @@ -13,14 +13,14 @@ */ import { - type TunnelMessage, - type Protocol, - type TunnelConfig, + type AgentMetadata, type Endpoint, type ExitNodeConfig, - type AgentMetadata, - MessageDiscriminant, MAX_FRAME_SIZE, + MessageDiscriminant, + type Protocol, + type TunnelConfig, + type TunnelMessage, } from "./types.ts"; // ============================================================================ @@ -832,6 +832,7 @@ export class FrameAccumulator { readAllMessages(): TunnelMessage[] { const messages: TunnelMessage[] = []; let msg: TunnelMessage | null; + // biome-ignore lint/suspicious/noAssignInExpressions: standard read-loop pattern while ((msg = this.tryReadMessage()) !== null) { messages.push(msg); } diff --git a/sdks/nodejs/src/protocol/types.ts b/sdks/nodejs/src/protocol/types.ts index 9c42a36..ef321bb 100644 --- a/sdks/nodejs/src/protocol/types.ts +++ b/sdks/nodejs/src/protocol/types.ts @@ -28,13 +28,7 @@ export type Protocol = /** * Geographic regions for exit node selection */ -export type Region = - | "UsEast" - | "UsWest" - | "EuWest" - | "EuCentral" - | "AsiaPacific" - | "SouthAmerica"; +export type Region = "UsEast" | "UsWest" | "EuWest" | "EuCentral" | "AsiaPacific" | "SouthAmerica"; /** * Exit node configuration diff --git a/sdks/nodejs/src/transport/h2.ts b/sdks/nodejs/src/transport/h2.ts index b10e66d..44e338e 100644 --- a/sdks/nodejs/src/transport/h2.ts +++ b/sdks/nodejs/src/transport/h2.ts @@ -6,14 +6,14 @@ */ import * as http2 from "node:http2"; -import * as tls from "node:tls"; +import type * as tls from "node:tls"; +import { FrameAccumulator, encodeMessage } from "../protocol/codec.ts"; import type { TunnelMessage } from "../protocol/types.ts"; -import { encodeMessage, FrameAccumulator } from "../protocol/codec.ts"; import type { - TransportStream, + ConnectionStats, TransportConnection, TransportConnector, - ConnectionStats, + TransportStream, } from "./base.ts"; import { TransportError, TransportErrorCode } from "./base.ts"; @@ -283,7 +283,7 @@ export class H2Connector implements TransportConnector { private rejectUnauthorized: boolean; constructor( - options: { useTls?: boolean; alpnProtocol?: string; rejectUnauthorized?: boolean } = {} + options: { useTls?: boolean; alpnProtocol?: string; rejectUnauthorized?: boolean } = {}, ) { this.useTls = options.useTls ?? true; this.alpnProtocol = options.alpnProtocol ?? "localup-v1"; @@ -320,7 +320,12 @@ export class H2Connector implements TransportConnector { session.on("error", (err) => { clearTimeout(timeout); - reject(new TransportError(`Connection failed: ${err.message}`, TransportErrorCode.ConnectionFailed)); + reject( + new TransportError( + `Connection failed: ${err.message}`, + TransportErrorCode.ConnectionFailed, + ), + ); }); }); } diff --git a/sdks/nodejs/src/transport/quic.ts b/sdks/nodejs/src/transport/quic.ts index 737f443..0fcf433 100644 --- a/sdks/nodejs/src/transport/quic.ts +++ b/sdks/nodejs/src/transport/quic.ts @@ -18,21 +18,19 @@ * If QUIC is not available, use WebSocket or HTTP/2 transport instead. */ +import * as crypto from "node:crypto"; +import { FrameAccumulator, encodeMessage } from "../protocol/codec.ts"; import type { TunnelMessage } from "../protocol/types.ts"; -import { encodeMessage, FrameAccumulator } from "../protocol/codec.ts"; import type { - TransportStream, + ConnectionStats, TransportConnection, TransportConnector, - ConnectionStats, + TransportStream, } from "./base.ts"; import { TransportError, TransportErrorCode } from "./base.ts"; -import * as crypto from "node:crypto"; -// We use dynamic imports and 'any' types to make @matrixai/quic optional -// eslint-disable-next-line @typescript-eslint/no-explicit-any +// Dynamic imports with 'any' types to make @matrixai/quic optional let QUICClient: any = null; -// eslint-disable-next-line @typescript-eslint/no-explicit-any let EventQUICConnectionStream: any = null; let quicLoadAttempted = false; let quicLoadError: Error | null = null; @@ -387,10 +385,7 @@ export class QuicConnector implements TransportConnector { async connect(host: string, port: number, serverName?: string): Promise { const available = await loadQuicModule(); if (!available || !QUICClient) { - throw new TransportError( - getQuicUnavailableReason(), - TransportErrorCode.ConnectionFailed - ); + throw new TransportError(getQuicUnavailableReason(), TransportErrorCode.ConnectionFailed); } try { @@ -419,7 +414,7 @@ export class QuicConnector implements TransportConnector { throw new TransportError( `QUIC connection to ${host}:${port} failed: ${errMsg}`, TransportErrorCode.ConnectionFailed, - err as Error + err as Error, ); } } diff --git a/sdks/nodejs/src/transport/websocket.ts b/sdks/nodejs/src/transport/websocket.ts index b453ef5..d61d477 100644 --- a/sdks/nodejs/src/transport/websocket.ts +++ b/sdks/nodejs/src/transport/websocket.ts @@ -14,13 +14,13 @@ * 3 = WindowUpdate */ +import { decodeMessagePayload, encodeMessagePayload } from "../protocol/codec.ts"; import type { TunnelMessage } from "../protocol/types.ts"; -import { encodeMessagePayload, decodeMessagePayload } from "../protocol/codec.ts"; import type { - TransportStream, + ConnectionStats, TransportConnection, TransportConnector, - ConnectionStats, + TransportStream, } from "./base.ts"; import { TransportError, TransportErrorCode } from "./base.ts"; @@ -403,7 +403,10 @@ export class WebSocketConnector implements TransportConnector { ws.onerror = (event: Event) => { clearTimeout(timeout); reject( - new TransportError(`Connection failed: ${event.type}`, TransportErrorCode.ConnectionFailed) + new TransportError( + `Connection failed: ${event.type}`, + TransportErrorCode.ConnectionFailed, + ), ); }; });