--- title: iOS Tailscale Clients Report HostName="localhost" — Breaks /etc/hosts Generators domain: troubleshooting category: networking tags: - tailscale - ios - postfix - etc-hosts - jq status: published created: 2026-04-29 updated: 2026-04-29 --- # iOS Tailscale Clients Report HostName="localhost" — Breaks /etc/hosts Generators ## Problem A homegrown script that builds an `/etc/hosts` block from `tailscale status --json` silently corrupted the file the moment any iOS device joined the tailnet. After the next run, services bound to `localhost` started failing. On the affected host (`majordiscord`), Postfix refused to start with: ``` postfix: fatal: parameter inet_interfaces: no local interface found for 100.127.114.10 ``` `/etc/hosts` looked fine at the top — `127.0.0.1 localhost` was still present — but inside the Tailscale-managed block: ``` # TAILSCALE_START 100.84.42.102 tttpod 100.110.197.17 majortoot 100.95.55.40 localhost <-- WRONG (this is an iPhone) 100.84.165.52 majormail ... 100.127.114.10 localhost <-- WRONG (this is an iPad) # TAILSCALE_END ``` When Postfix resolved `localhost` (because `inet_interfaces = localhost` in `main.cf`), the **last matching entry** in `/etc/hosts` won — a Tailscale IP that doesn't exist on this host — and the daemon died on bind. ## Root Cause The script used `.HostName` from the Tailscale JSON: ```bash tailscale status --json \ | jq -r '.Peer[] | "\(.TailscaleIPs[0]) \(.HostName)"' \ >> "$TEMP_HOSTS" ``` iOS Tailscale clients (iPhone, iPad) **always report `HostName: "localhost"`** in the JSON. iOS doesn't expose the real device name to apps the way macOS/Linux/Windows do, so the Tailscale client falls back to the literal string `localhost`. Inspect it directly: ```bash $ tailscale status --json | jq '.Peer[] | select(.OS == "iOS") | {DNSName, HostName, OS}' { "DNSName": "iphone171.tail7f2d9.ts.net.", "HostName": "localhost", "OS": "iOS" } { "DNSName": "ipad166.tail7f2d9.ts.net.", "HostName": "localhost", "OS": "iOS" } ``` Every iOS device contributes a line ` localhost` to `/etc/hosts`, hijacking the `localhost` lookup. ## Fix Use `.DNSName` (the unique tailnet DNS name) and take the first dotted component instead of `.HostName`: ```bash tailscale status --json \ | jq -r '.Peer[] | "\(.TailscaleIPs[0]) \(.DNSName | rtrimstr(".") | split(".")[0])"' \ >> "$TEMP_HOSTS" ``` `DNSName` is always set, always unique, and produces clean labels like `iphone171`, `ipad166`, `majorlab`, etc. After patching the script and re-running it: ```bash $ bash /root/update_tailscale_hosts.sh $ systemctl restart postfix $ systemctl is-active postfix active ``` ## Why It's Hard to Spot - The corruption only triggers when an iOS device is in the tailnet — so the script "worked" for months. - `/etc/hosts` files are commonly skimmed top-down. The bogus `localhost` line is buried in the Tailscale block, well below the legitimate `127.0.0.1 localhost` line, and looks superficially like a normal Tailscale entry. - Postfix's error message names the IP, not `localhost`, so the connection to `/etc/hosts` isn't obvious. - `getent hosts localhost` shows the *first* match (`127.0.0.1`), not the one Postfix's resolver actually picks for `inet_interfaces` lookup. ## Verification Checklist If you suspect this on any host using a similar generator script: ```bash # Any non-loopback "localhost" entries are bugs grep -nE '^[0-9]+\..* localhost\s*$' /etc/hosts # Look at iOS peers' HostName field tailscale status --json | jq '.Peer[] | select(.OS == "iOS") | .HostName' ``` ## Related - [[majordiscord]] — affected host (incident logged 2026-04-29) - [[Network Overview]] — Tailscale fleet topology