Created
February 18, 2026 11:59
-
-
Save 0xBigBoss/ed55e9620c8a0fb663956f7303ff3a08 to your computer and use it in GitHub Desktop.
Workaround for Tailscale MagicDNS short names broken on macOS 26 (github.com/tailscale/tailscale/issues/17096)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>Label</key> | |
| <string>com.tailscale.hosts-sync</string> | |
| <key>ProgramArguments</key> | |
| <array> | |
| <string>/usr/local/bin/tailscale-hosts-sync</string> | |
| </array> | |
| <key>StartInterval</key> | |
| <integer>30</integer> | |
| <key>RunAtLoad</key> | |
| <true/> | |
| <key>StandardErrorPath</key> | |
| <string>/tmp/tailscale-hosts-sync.err</string> | |
| </dict> | |
| </plist> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # tailscale-hosts-sync — Syncs Tailscale peer short names into /etc/hosts. | |
| # | |
| # Workaround for macOS 26 breaking MagicDNS short name resolution. | |
| # macOS 26 no longer expands single-label hostnames using search domains | |
| # from Supplemental DNS resolvers (which is how Tailscale registers DNS). | |
| # See: https://github.com/tailscale/tailscale/issues/17096 | |
| # | |
| # This script parses `tailscale status --json`, extracts short DNS names | |
| # and IPv4 addresses, and writes them into a managed block in /etc/hosts. | |
| # A LaunchDaemon runs it every 30 seconds to keep entries in sync. | |
| # | |
| # Files: | |
| # /usr/local/bin/tailscale-hosts-sync (this script) | |
| # /Library/LaunchDaemons/com.tailscale.hosts-sync.plist (daemon) | |
| # /etc/hosts (managed block) | |
| # | |
| # Uninstall: | |
| # sudo launchctl bootout system/com.tailscale.hosts-sync | |
| # sudo rm /Library/LaunchDaemons/com.tailscale.hosts-sync.plist | |
| # sudo rm /usr/local/bin/tailscale-hosts-sync | |
| # sudo sed -i '' '/# BEGIN tailscale-hosts-sync/,/# END tailscale-hosts-sync/d' /etc/hosts | |
| set -euo pipefail | |
| TAILSCALE="/Applications/Tailscale.app/Contents/MacOS/Tailscale" | |
| HOSTS="/etc/hosts" | |
| BEGIN_MARKER="# BEGIN tailscale-hosts-sync" | |
| END_MARKER="# END tailscale-hosts-sync" | |
| if ! "$TAILSCALE" status --json &>/dev/null; then | |
| exit 0 | |
| fi | |
| entries=$("$TAILSCALE" status --json 2>/dev/null | python3 -c " | |
| import json, sys | |
| data = json.load(sys.stdin) | |
| lines = [] | |
| def add_node(node): | |
| dns_name = node.get('DNSName', '') | |
| ips = node.get('TailscaleIPs', []) | |
| if not dns_name or not ips: | |
| return | |
| short = dns_name.split('.')[0] | |
| if not short or short == 'localhost': | |
| return | |
| ipv4 = next((ip for ip in ips if '.' in ip), None) | |
| if ipv4: | |
| lines.append(f'{ipv4}\t{short}') | |
| self_node = data.get('Self') | |
| if self_node: | |
| add_node(self_node) | |
| for peer in data.get('Peer', {}).values(): | |
| add_node(peer) | |
| lines.sort(key=lambda l: l.split('\t')[1]) | |
| print('\n'.join(lines)) | |
| ") | |
| if [ -z "$entries" ]; then | |
| exit 0 | |
| fi | |
| block_file=$(mktemp) | |
| printf '%s\n%s\n%s\n' "$BEGIN_MARKER" "$entries" "$END_MARKER" > "$block_file" | |
| if grep -q "$BEGIN_MARKER" "$HOSTS"; then | |
| tmp=$(mktemp) | |
| python3 -c " | |
| import sys | |
| begin, end, block_path, hosts_path = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] | |
| with open(block_path) as f: | |
| block = f.read() | |
| with open(hosts_path) as f: | |
| lines = f.readlines() | |
| out = [] | |
| skip = False | |
| for line in lines: | |
| if line.rstrip() == begin: | |
| skip = True | |
| continue | |
| if skip and line.rstrip() == end: | |
| skip = False | |
| out.append(block) | |
| continue | |
| if not skip: | |
| out.append(line) | |
| print(''.join(out), end='') | |
| " "$BEGIN_MARKER" "$END_MARKER" "$block_file" "$HOSTS" > "$tmp" | |
| if ! cmp -s "$tmp" "$HOSTS"; then | |
| cat "$tmp" > "$HOSTS" | |
| fi | |
| rm -f "$tmp" | |
| else | |
| printf '\n' >> "$HOSTS" | |
| cat "$block_file" >> "$HOSTS" | |
| fi | |
| rm -f "$block_file" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment