# TailMux - Security And Isolation Model

Date: 2026-05-05

## Security Objective

TailMux must allow one Mac to access multiple separate tailnets without allowing one tailnet to receive traffic intended for another.

The rule is fail closed:

- unknown hostname: deny
- unknown `.ts.net`: deny
- raw IP: deny by default
- selected profile down: deny
- selected backend error: deny
- no fallback to another profile

## Isolation Boundaries

### Profile Identity Boundary

Every profile has its own Tailscale identity.

For `backend: "tsnet"`:

- one `tsnet.Server` per profile
- unique state directory
- unique node key
- unique hostname
- optional profile-specific auth key environment variable
- advertised subnet/app-connector routes disabled unless the profile explicitly sets `accept_routes: true`

For `backend: "tailscaled"`:

- one external userspace daemon per profile
- unique state directory
- unique daemon socket
- unique profile HTTP/SOCKS proxy listeners
- unique UDP port unless explicitly using auto port `0`

Control:

- config validation rejects duplicate state directories, sockets, ports, and unsafe listener addresses where applicable
- profile commands are dispatched through the selected backend only
- no command should call the default official Tailscale daemon for profile state

### State Boundary

Example:

```text
~/Library/Application Support/TailMux/tailnets/work/
~/Library/Application Support/TailMux/tailnets/personal/
```

Controls:

- create directories as user-only
- never copy profile state between tailnets
- use readable hostnames for approval pages
- treat deleting a profile state directory as a destructive re-registration action

### Routing Boundary

Routing is based on hostname suffix ownership.

Controls:

- match before DNS resolution whenever possible
- require FQDNs for tailnet traffic
- reject overlapping suffix ownership
- deny raw IPs by default
- never try another profile after a route failure

### Local Listener Boundary

TailMux listeners must stay on loopback.

Typical endpoints:

```text
router: 127.0.0.1:43100
PAC:    127.0.0.1:43180
```

Compatibility backend profile listeners:

```text
work HTTP:      127.0.0.1:43110
work SOCKS5:    127.0.0.1:43111
personal HTTP:  127.0.0.1:43120
personal SOCKS: 127.0.0.1:43121
```

Controls:

- reject non-loopback binds
- inspect listeners in diagnostics
- never expose TailMux proxy ports to the LAN

## Threat Model

### Accidental Cross-Tailnet Fallback

Scenario: work profile is down and a work hostname is requested. TailMux tries personal after work fails.

Impact:

- request metadata leaks to the wrong profile path
- tailnet policy expectations are violated

Control:

- classifier returns one profile or an error
- connection failure ends the request
- tests assert no secondary dial

### Broad Suffix Captures Too Much

Scenario: `.ts.net` is assigned to personal and `.taild63bef.ts.net` is assigned to work.

Impact:

- a broad rule can capture traffic that belongs to a specific profile

Control:

- overlap validation rejects broad/specific suffix ownership conflicts
- custom domains follow the same rule

### Raw IP Loses Profile Context

Scenario: user requests `http://100.x.y.z`.

Impact:

- TailMux cannot prove which tailnet owns the address
- two tailnets can have unrelated peers in the same Tailscale address space
- subnet routes can overlap across separate tailnets

Control:

- deny unconfigured IP literals
- allow only explicit profile-owned `ip_routes`
- reject overlapping `ip_routes` across profiles at config validation
- reject default routes, loopback, multicast, and unspecified ranges
- never fall back to another profile after an IP route failure
- for GUI clients such as RDP, use `tailmux tunnel --profile <name> --listen 127.0.0.1:<port> <ip:port>` so the profile choice is explicit
- tunnel listeners must bind to loopback only

Safe example:

```yaml
profiles:
  work:
    accept_routes: true
    ip_routes:
      - "10.10.20.0/24"
      - "100.64.0.10"
```

Unsafe example:

```yaml
profiles:
  work:
    ip_routes:
      - "10.0.0.0/16"
  personal:
    ip_routes:
      - "10.0.20.0/24"
```

The unsafe example must fail validation because TailMux cannot safely classify `10.0.20.15` without a profile signal.

### Local Proxy Exposed To LAN

Scenario: TailMux binds `0.0.0.0:43100`.

Impact:

- another LAN device can use Claudio's tailnet access

Control:

- config validation rejects non-loopback listeners
- defaults are loopback-only

### Shared Profile State

Scenario: work and personal use the same state directory.

Impact:

- identity contamination
- device approval confusion
- node-key collision

Control:

- validation rejects duplicate state paths
- docs require clear hostnames
- state rotation is profile-specific only

### Official Client Confusion

Scenario: the official Tailscale app is connected to one tailnet and Safari appears to work without PAC in another environment.

Impact:

- tests are attributed to the wrong path
- direct client and TailMux results are mixed

Control:

- direct comparison commands explicitly label `router`, `embedded`, and `direct`
- Safari PAC path must be verified separately
- TailMux does not use the official GUI identity for profile routing

### DNS Cache Or Client Resolves Early

Scenario: a tool resolves a hostname before using a proxy and then connects to a raw IP.

Impact:

- profile ownership signal is lost

Control:

- use HTTP CONNECT, PAC, or SOCKS5 domain connect
- prefer `socks5h` semantics where applicable
- resolve embedded profile hostnames through the selected profile's Tailscale DNS view
- deny raw IP unless it matches explicit profile-owned `ip_routes`

### Secret Leakage

Scenario: logs contain auth keys or auth URLs.

Impact:

- local logs could leak authentication material

Control:

- read auth keys from environment variables instead of config
- redact sensitive values where possible
- avoid logging full URLs with query strings when not needed
- keep logs local under user-owned directories

## Security Defaults

```yaml
security:
  require_loopback_listeners: true
  allow_cross_profile_fallback: false
  allow_ip_literals: false
  allow_unknown_tsnet: false
```

Expected request outcomes:

```text
unknown profile-owned suffix -> 502/deny with no upstream dial
selected profile unavailable -> 503/deny with no fallback
raw IP literal               -> 403/deny by default
```

The same no-fallback rule applies when the CLI dials through an already-running
service: `tailmux connect` classifies the host locally before any dialing (an
unclassified host is never handed to anything, including the running router),
and once the router's SOCKS5 listener has accepted the connection its verdict
is final — a denial or dial failure is an error, never a retry through a
freshly started embedded profile.

## Verification Plan

### Automated

```bash
go test ./...
go test -race -shuffle=on ./...
```

Required coverage areas:

- rule selection
- suffix overlap rejection
- raw IP denial
- unknown suffix denial
- CONNECT/HTTP/SOCKS routing
- SSE and chunk flushing
- embedded profile argument parsing
- path diagnostic parsing
- GUI/TUI manager behavior

### Manual

```bash
tailmux doctor
tailmux config validate
tailmux up
tailmux status
lsof -nP -iTCP:43100 -iTCP:43180
netstat -rn
```

Expected:

- listeners are on `127.0.0.1`
- no TailMux-created broad kernel route
- profiles have distinct state directories
- profile statuses are independent

### Negative

1. Stop or logout the work profile.
2. Request a work hostname.
3. Confirm it fails.
4. Confirm personal path receives no traffic.

Repeat in reverse.

Then:

1. Request an unknown `.ts.net`.
2. Confirm deny.
3. Request a raw IP.
4. Confirm deny.

## Operational Rules

- Do not run TailMux as root.
- Do not configure TailMux as an exit node.
- Do not advertise subnet routes from TailMux profiles.
- Do not accept or inject system routes in the current runtime.
- Do not use short MagicDNS names for routing.
- Do not store auth keys directly in YAML.
- Prefer FQDNs under configured suffixes.

## Future Review Gate

Before any transparent routing, Network Extension, route manager, or static IP map:

- write a separate design document
- prove hostname-to-flow attribution
- prove no fallback/cross-profile behavior
- define required Apple entitlements and signing path
- define explicit user consent for system network changes
- add negative tests before implementation
