majorwiki/05-troubleshooting/networking/tailscale-status-json-hostname-localhost-ios.md
majorlinux 4126656c05 wiki: update fail2ban digest + netdata docker health + 3 new articles
- fail2ban-digest-mode-fleet: recidive-only email model, sshd now silent,
  defaults-debian.conf gotcha added
- netdata-docker-health-alarm-tuning: 30m/10m config, tuning history table
- New: wp-fail2ban-logpath-debian-ubuntu, lora-adapter-gguf-conversion-fails,
  tailscale-status-json-hostname-localhost-ios
- Various article updates and nav index refreshes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 14:58:07 -04:00

3.7 KiB

title domain category tags status created updated
iOS Tailscale Clients Report HostName="localhost" — Breaks /etc/hosts Generators troubleshooting networking
tailscale
ios
postfix
etc-hosts
jq
published 2026-04-29 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:

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'