# TailMux

TailMux is a macOS and Linux tool for accessing resources in multiple independent Tailscale tailnets simultaneously. It routes supported hostname-preserving connections by profile suffix while keeping profiles isolated and without repeatedly switching the official Tailscale client account.

The current target use case is:

- a work tailnet for company infrastructure
- a personal CQ Fabrication tailnet for private servers
- browser, API, SSH, and diagnostic access without switching the official Tailscale client account
- strict isolation: a hostname owned by one profile must never fall back to another profile

TailMux is application-layer routing, not a second native system-wide Tailscale VPN interface. See the limitations and routing model at https://tailmux.app/docs/limitations before assuming a protocol or workflow is supported.

TailMux is not affiliated with, endorsed by, or sponsored by Tailscale Inc.

## Current State

TailMux is implemented as a Go CLI plus local router. The recommended runtime is now embedded `tsnet` per profile:

```text
Browser / curl / SSH ProxyCommand
        |
        | HTTP_PROXY / HTTPS_PROXY / PAC / tailmux connect
        v
TailMux router 127.0.0.1:43100
        |
        | hostname suffix match
        v
profile-owned embedded tsnet dialer
        |
        v
selected tailnet
```

The older external `tailscaled` userspace backend is still supported for compatibility and debugging, but new local configs should use:

```yaml
profiles:
  personal:
    backend: "tsnet"
```

TailMux avoids system-route ambiguity by making the routing decision while the original hostname is still available. Its embedded profiles are separate from the official Tailscale client session and never alter that session.

## Implemented

- Go CLI with Cobra.
- YAML config loading, normalization, validation, and generated starter config.
- Profile-owned suffix routing with overlap rejection.
- HTTP proxy, HTTPS CONNECT, and SOCKS5 domain CONNECT support.
- Browser PAC generation and local PAC serving.
- Embedded `tsnet` backend with one profile identity/state directory per tailnet.
- External `tailscaled` userspace backend as compatibility path.
- `tailmux connect` for raw TCP tunnels through the selected profile.
- `tailmux tunnel` for loopback-only local TCP forwards, including RDP/SMB to explicit profile/IP targets.
- `tailmux run` and `tailmux env` for routing CLI HTTP clients (curl, git, npm) through the profile proxies.
- `tailmux diag path` for profile DNS, peer, DERP/direct, netcheck, and HTTP timing reports.
- `tailmux diag fetch` for router vs embedded vs direct timing comparisons.
- Local web GUI service and JSON API.
- Terminal dashboard (`tailmux tui`) for profiles and path reports.
- Tests for config, routing, proxying, embedded manager, diagnostics, GUI, and TUI.
- GitHub Actions for CI, release builds, changelog, and GoReleaser artifacts.

## Quick Start

Requirements:

- macOS for the primary workflow.
- Go `1.26.4` as declared in `go.mod`.
- Tailscale app or CLI available for the official direct-comparison checks.

Build and test:

```bash
make test
make build
```

Create a config:

```bash
tailmux init
tailmux config validate
```

Edit `~/.config/tailmux/config.yaml` and replace the example suffixes with the real tailnet DNS suffixes. Use `backend: "tsnet"` for new profiles.

Start TailMux:

```bash
tailmux up
```

Configure Safari/macOS to use the PAC URL:

```text
http://127.0.0.1:43180/proxy.pac
```

Run a path report:

```bash
tailmux diag path http://claudios-mac-mini-1.tail2c15f0.ts.net:13200/live-auctions --netcheck
```

Resolve a custom app or split-DNS name through one selected embedded profile:

```bash
tailmux dns query --profile work erp.apps.work.example.com
```

Expose RDP through an explicit loopback tunnel:

```bash
tailmux tunnel --profile work --listen 127.0.0.1:13389 10.10.20.15:3389
```

Then point the RDP client at `127.0.0.1:13389`. For SMB, tunnel to port 445 and mount from the loopback port (`smb://127.0.0.1:1445`). See `docs/08-system-wide-access.md`.

Route a terminal HTTP client (curl, git, npm) through a profile for an owned `.ts.net` host:

```bash
tailmux run curl https://grid.tail2c15f0.ts.net/
```

Compare fetch timings:

```bash
tailmux diag fetch \
  --modes router,embedded,direct \
  --samples 5 \
  --timeout 25s \
  --read-limit 4194304 \
  http://claudios-mac-mini-1.tail2c15f0.ts.net:13200/live-auctions
```

Open the local GUI:

```bash
tailmux gui serve
```

Use the terminal dashboard:

```bash
tailmux tui --target http://claudios-mac-mini-1.tail2c15f0.ts.net:13200/live-auctions --netcheck --watch
```

## Repository Map

- `docs/00-getting-started.md`: onboarding runbook for new contributors.
- `docs/01-feasibility-and-requirements.md`: validated feasibility, product scope, and requirements.
- `docs/02-architecture.md`: current runtime architecture, package boundaries, and data flows.
- `docs/03-implementation-plan.md`: delivered work, active backlog, and acceptance criteria.
- `docs/04-security-isolation.md`: isolation model, threat model, and verification plan.
- `docs/05-research-sources.md`: official sources and local findings used for decisions.
- `docs/06-macos-login-procedure.md`: macOS profile login, PAC setup, troubleshooting, and performance checks.
- `docs/07-custom-domain-dns-plan.md`: custom-domain routing alias plan.
- `docs/08-system-wide-access.md`: system-wide access model (PAC, `tailmux run`/`env`, `tailmux tunnel`) and the deny-by-default caveat.
- `docs/11-macos-local-dev-session.md`: canonical, guarded Production ↔ local Dev menu-bar testing runbook.
- `examples/tailmux.example.yaml`: recommended config shape.
- `examples/browser.pac`: static PAC example.
- `scripts/package-macos-local.sh`: build the ad-hoc-signed macOS `.dmg` and `.pkg` (see "macOS app package" below).
- `cmd/tailmux`: CLI entrypoint and command implementations.
- `internal/embedded`: embedded `tsnet` profile manager.
- `internal/proxy`: local routing proxy.
- `internal/pathdiag`: path diagnostics and recommendations.
- `internal/manager`: profile/status service used by GUI and TUI.

## Development

```bash
make test
make test-race
make vet
make lint
make build
```

To test local macOS menu-bar changes against the real profiles and activation on
this Mac, use the sequential runtime switch:

```bash
make run-macos-dev
# Production is now stopped; test the local Debug build.
make restore-macos-production
```

The Dev build is compiled before the installed app is stopped, uses the same
config/state/ports only after Production has released them, and defaults to the
production license endpoint so it reuses the current Polar device activation.
Its bundle identifier is `com.cqfabrication.tailmux.menu.dev`, keeping
LaunchServices and quit events separate from the installed Production app.
It is launched directly from `.derivedData` and never overwrites the app in
`/Applications`. A failed Dev startup automatically restores Production. See
`docs/11-macos-local-dev-session.md` for the complete procedure, verification
evidence, rollback behavior, and recovery rules.

If the installed `golangci-lint` does not support the Go version in `go.mod`, use the latest v2 runner:

```bash
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run ./...
```

Release builds are handled by GoReleaser on `v*` tags. The release workflow builds macOS and Linux binaries for `amd64` and `arm64`, uploads archives and checksums, and generates release notes from conventional commits.

## macOS app package (internal distribution)

`make package-macos-local` builds an ad-hoc-signed `.dmg` **and** `.pkg` for internal
testing — no Apple Developer ID required. It compiles the app in Release, builds the
`tailmux` CLI, assembles them, and writes both installers under `dist/`. In CI (with
Developer ID certs present) both artifacts are signed and notarized.

```bash
# Universal (Intel + Apple Silicon) — for distributing to the team (~22 MB)
make package-macos-local

# Apple Silicon only — smaller/faster for a quick local test (~13 MB)
ARCH=arm64 ./scripts/package-macos-local.sh
```

Knobs (environment variables): `ARCH` = `universal` (default), `arm64`, or `amd64`;
`VERSION` (default `0.1.0`); `GO` (path to the Go toolchain).

Two installers are produced:

- **`.dmg` (primary install)** — open it and drag **TailMux.app** into the
  **Applications** folder. This is the classic drag-to-install layout. `.dmg` users
  get the CLI from the app: **Settings → Install command-line tool**.
- **`.pkg` (scripted / MDM install)** — installs the app and auto-symlinks the CLI
  onto the `PATH`, so it is best for unattended/MDM deployments.

The `.pkg` installs:

- `/Applications/TailMux.app` — the menu bar app.
- `/usr/local/bin/tailmux` — a **symlink** to the app's bundled CLI (one physical
  copy), so `tailmux run` / `env` / `tunnel` work from any terminal.

Size: the CLI embeds the full Tailscale userspace stack (~22 MB/arch after
stripping) and dominates the package. The script keeps it small by stripping
symbols (`-ldflags="-s -w" -trimpath`), shipping a single copy symlinked onto the
PATH instead of duplicating it, and supporting an arch-specific `ARCH=arm64` build.
GoReleaser release builds are already stripped.

Install and run:

```bash
# Drag-to-install: open the .dmg, then drag TailMux.app to Applications.
open dist/<stamp>/TailMux-0.1.0-macos-<arch>.dmg

# Or the scripted installer:
sudo installer -pkg dist/<stamp>/TailMux-0.1.0-macos-<arch>.pkg -target /
```

Local builds are ad-hoc-signed and **not notarized**, so on first launch Gatekeeper
blocks the app: right-click the app → **Open** (or System Settings → Privacy &
Security → **Open Anyway**). If another TailMux instance is already running, quit it
first to avoid a port clash on 43100/43200.
