iOS Tailscale Clients Report HostName="localhost" — Breaks /etc/hosts Generators¶
Historical — the generator this describes has been retired (2026-07-20)
The fix target, /root/update_tailscale_hosts.sh on majordiscord, no longer exists (verified 2026-07-20: no such script, no cron/timer, no # TAILSCALE_START block in /etc/hosts). The homegrown /etc/hosts generator was removed, not patched — MagicDNS covers this now. This article is kept for the underlying insight (iOS clients can report HostName="localhost" in tailscale status --json, so any tool parsing that field must guard against it), but the specific remediation steps are no longer live anywhere on the fleet.
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:
/etc/hosts looked fine at the top — 127.0.0.1 localhost was still present — but inside the Tailscale-managed block:
# TAILSCALE_START
100.64.0.20 tttpod
100.64.0.30 majortoot
100.64.0.40 localhost <-- WRONG (this is an iPhone)
100.64.0.50 majormail
...
100.64.0.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:
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:
$ 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 <tailscale-ip> 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:
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 /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/hostsfiles are commonly skimmed top-down. The boguslocalhostline is buried in the Tailscale block, well below the legitimate127.0.0.1 localhostline, and looks superficially like a normal Tailscale entry.- Postfix's error message names the IP, not
localhost, so the connection to/etc/hostsisn't obvious. getent hosts localhostshows the first match (127.0.0.1), not the one Postfix's resolver actually picks forinet_interfaceslookup.
Verification Checklist¶
If you suspect this on any host using a similar generator script:
# 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