Skip to content

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:

postfix: fatal: parameter inet_interfaces: no local interface found for 100.64.0.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.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:

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:

$ 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/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:

# 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'