# TailMux - Feasibility And Requirements

Date: 2026-05-05

## Executive Summary

TailMux is feasible, but only if routing remains hostname-owned and profile-owned. macOS routing is IP/CIDR based; TailMux's requirement is to route by FQDN suffix before DNS collapses a request into an address that may be ambiguous across tailnets.

The current architecture is:

1. Keep one isolated Tailscale identity per profile.
2. Prefer embedded `tsnet` for each profile.
3. Keep `tailscaled` userspace subprocesses as a compatibility backend.
4. Run one local TailMux router on loopback.
5. Route by profile-owned hostname suffix before DNS/IP ambiguity appears.
6. Use PAC, proxy environment variables, and explicit helpers for clients.
7. Fail closed on unknown suffixes, raw IPs, and profile failures.

This supports browser dashboards, APIs, SSE pages, Git over HTTPS, SSH via `tailmux connect`/`ProxyCommand`, and any developer tool that preserves the hostname through HTTP CONNECT or SOCKS5 domain connect.

## Problem Statement

Claudio needs to stay connected to two independent Tailscale tailnets at the same time:

- a work tailnet used to reach company infrastructure
- a personal CQ Fabrication tailnet used to reach private infrastructure

Traffic must be routed by hostname suffix. Example real-world suffixes used during development:

- `.taild63bef.ts.net` for work
- `.tail2c15f0.ts.net` for personal

The exact suffix values are local configuration. The product rule is stable: every suffix is owned by one profile, and no request may move from one profile to another after classification.

## Feasibility Verdict

### Supported Now

- Browser HTTP/HTTPS access through PAC.
- HTTP proxy and HTTPS CONNECT through the TailMux router.
- SOCKS5 domain CONNECT through the TailMux router.
- SSH with `ProxyCommand tailmux connect %h %p`.
- Git over HTTPS through HTTP proxy.
- Embedded profile routing through `tsnet`.
- External userspace `tailscaled` profile routing for compatibility.
- Profile status, login, logout, path reports, netcheck, timing probes, GUI, and TUI.
- Explicit custom-domain suffix aliases, because `suffixes` already accept non-`ts.net` domains.

### Not Supported By Design

- Two official Tailscale GUI accounts transmitting packets concurrently from one native macOS client.
- Kernel-level routing by URL or hostname suffix.
- Automatic inference for short names such as `server`.
- Raw IP routing without an explicit future static map.
- Transparent system-wide routing without proxy/PAC setup.
- UDP-heavy workflows through the current router.
- Bridging or sharing ACL/DNS/service context between profiles.

### Future But Separate Design

- macOS Network Extension or Packet Tunnel routing.
- Automatic macOS proxy/PAC setting changes.
- Launchd lifecycle installation.
- Signed native menu bar app.
- DNS export and provider publication for custom domains.
- Static IP maps with profile ownership.

## Product Goals

1. Keep at least two tailnets reachable concurrently from one Mac.
2. Route by fully qualified hostname suffix.
3. Keep tailnets isolated by construction.
4. Make Safari and browser access work through PAC.
5. Make developer tools work through proxy env, SOCKS5, or `tailmux connect`.
6. Make diagnosis obvious when a rule, profile, peer path, binary, or auth state is wrong.
7. Preserve performance for dynamic pages and long-lived streams.
8. Avoid invasive macOS network changes until a separate route-manager phase exists.

## Non-Goals

- Replacing the official Tailscale GUI client.
- Mutating or depending on the official GUI client's active tailnet identity.
- MITM inspection of TLS.
- Auto-approving devices in Tailscale admin consoles.
- Working around work tailnet policy restrictions.
- Sharing DNS, routes, ACL context, device keys, or service discovery between profiles.

## Core User Journeys

### First Install

1. Build or install `tailmux`.
2. Run `tailmux doctor`.
3. Generate or inspect `~/.config/tailmux/config.yaml`.
4. Use `backend: "tsnet"` for each new profile.
5. Validate suffix ownership with `tailmux config validate`.

### Configure Profiles

1. Define profile `work` with its work tailnet suffix.
2. Define profile `personal` with its personal tailnet suffix.
3. Give each profile a readable `hostname`.
4. Give each profile a unique `state_dir`.
5. Give each profile a fixed unique UDP `port` for stable peer-path diagnosis.

### Authenticate Profiles

1. Run `tailmux profile login personal`.
2. Approve the generated TailMux device in the personal tailnet.
3. Run `tailmux profile login work`.
4. Approve the generated TailMux device in the work tailnet.
5. Verify both with `tailmux status`.

For embedded `tsnet` profiles, `--reset` is not supported. Use profile logout and, if needed during development, rotate/delete only that profile's state directory after confirming the device can be recreated.

### Start Routing

1. Run `tailmux up`.
2. TailMux starts embedded `tsnet` profiles and the local router.
3. TailMux serves PAC at `http://127.0.0.1:43180/proxy.pac`.
4. Safari/macOS uses the PAC URL.
5. Matching suffixes go through TailMux; public hosts remain direct.

### Diagnose A Slow Page Or SSE

1. Run `tailmux diag path <url> --netcheck`.
2. Check the selected profile and matched suffix.
3. Check whether `tailscale ping` reports direct, DERP, or peer relay.
4. Check HTTP timing through the router.
5. Run `tailmux diag fetch --modes router,embedded,direct --samples 5 <url>` for repeated timing.
6. For SSE, add `--first-byte-only`.

## Functional Requirements

### Config

- Load YAML config from `~/.config/tailmux/config.yaml` by default.
- Expand `~` in paths.
- Validate profile names, hostnames, suffixes, listeners, ports, and state directories.
- Reject overlapping suffixes assigned to different profiles.
- Reject cross-profile fallback.
- Keep listeners on loopback.

### Profile Backends

- `tsnet`: one embedded `tsnet.Server` per profile.
- `tailscaled`: one external userspace daemon per profile.
- Backends share the same routing contract.
- Profile login/logout/status commands must target the selected profile only.
- State directories must never be shared.

### Router

- Listen on loopback only.
- Accept HTTP proxy requests.
- Accept HTTPS CONNECT requests.
- Accept SOCKS5 domain CONNECT requests.
- Route by hostname suffix.
- Preserve TLS end-to-end.
- Deny unknown `.ts.net` hostnames.
- Deny raw IP literals by default.
- Never fallback between profiles.

### Browser Support

- Generate PAC text from config.
- Serve PAC from a loopback endpoint.
- PAC routes only configured profile-owned suffixes to TailMux.
- PAC returns `DIRECT` for all other hosts.
- Safari setup remains manual until a confirmed explicit macOS settings command exists.

### Diagnostics And UI

- `tailmux doctor`: local environment and config checks.
- `tailmux status`: profile health.
- `tailmux test <host>`: suffix ownership decision.
- `tailmux diag path`: selected profile, status, peer, ping path, netcheck, HTTP timing.
- `tailmux diag fetch`: repeated URL timing across router, embedded, and direct modes.
- `tailmux gui serve`: local GUI and JSON API.
- `tailmux tui`: terminal dashboard and optional path report.

## Non-Functional Requirements

### Security

- Default deny for unknown `.ts.net`.
- Default deny for raw IP.
- No cross-profile retries.
- Loopback-only proxy/control listeners.
- User-only state/config/runtime directories.
- No auth keys in normal logs.
- No transparent bridge between tailnets.

### Performance

The router must stream dynamic and unknown-length responses immediately. SSE, chunked responses, and large dynamic pages cannot wait for the full upstream response before forwarding to the browser.

Current local finding: after the streaming fix, TailMux router samples were faster than official direct-client samples for the tested live-auctions page and SSE first-event measurement. This is an observation, not a permanent guarantee; re-measure with `diag fetch` when changing proxy, embedded, or Tailscale dependencies.

### Reliability

- Recover cleanly from profile auth/state errors.
- Detect port conflicts.
- Detect missing or mismatched binaries.
- Keep the router running if one profile fails; requests to that profile fail closed.
- Make Tailscale CLI mismatch visible and fixable.

## Decision Record

- Official fast user switching is not enough because only one account is active for packet transmission.
- Full macOS Network Extension routing is powerful but not the current product path because it introduces entitlements, signing, system routing, and hostname-to-flow attribution risks.
- VM/container per tailnet is feasible but heavier and less ergonomic.
- Hostname-preserving proxy/PAC routing is the lowest-risk design that satisfies the isolation requirement today.
