Skip to content

Instantly share code, notes, and snippets.

Last active May 8, 2023 05:37
Show Gist options
  • Save kylemanna/d43db65193467efcdea172907ca8360a to your computer and use it in GitHub Desktop.
Save kylemanna/d43db65193467efcdea172907ca8360a to your computer and use it in GitHub Desktop.
Tailscale Per Host Default Router Disabler
#!/usr/bin/env python3
""" Tailscale Per Host Default Router Disabler
Tailscale allows a single tailnet wide "Override local DNS" setting that works
great for mobile and random cloud machines I have. However, my more elaborate
setups require the ability to turn this off and use local DNS as the default
Unfortuantely with Tailscale as of v1.40.0 there's only two options:
1. Tailnet wide "local DNS override" (Turns out tailscale's internal resolver
is great DoH client actually with NextDNS).
2. Per Tailscale client `--accept-dns=false` option to fully disable the
default route and the MagicDNS routes.
This HACK listens via D-Bus for Tailscale to configure itself as a default
route and then immediately command systemd-resolved to set that to false.
The result: you get the secondary tailscale DNS benefits (split dns,
Magic DNS, etc) without forcing all your traffic this way.
To test, enable the default route manually then inspect
$ sudo resolvectl default-route tailscale0 true
$ resolvectl status tailscale0
Usage via command line:
Usage via systemd:
1. Install, enable, and run a service file before tailscaled.service starts.
import subprocess
import json
import socket
def is_tailscale_interface(ifindex: int):
name = socket.if_indextoname(ifindex)
return name.startswith('tailscale') if name else None
def handle_set_link_default_route(d: dict):
# Expected argument:
# {"type":"method_call","endian":"l","flags":0,"version":1,"cookie":2,"timestamp-realtime":1683503555104733,"sender":":1.547008","destination":"org.freedesktop.resolve1","path":"/org/freedesktop/resolve1","interface":"org.freedesktop.resolve1.Manager","member":"SetLinkDefaultRoute","payload":{"type":"ib","data":[106,true]}}
ifindex, default_route = d['payload']['data']
print(f"SetLinkDefaultRoute method called with arguments: interface_index={ifindex}, default_route={default_route}")
if default_route and is_tailscale_interface(ifindex):
# Call SetLinkDefaultRoute with False argument
subprocess.check_call(['busctl', 'call', 'org.freedesktop.resolve1',
'ib', str(ifindex), 'false'])
print(f"Disabled default route for interface {ifindex}")
def handle_set_link_domains(d: dict):
# Expected argument:
# {"type":"method_call","endian":"l","flags":0,"version":1,"cookie":2,"timestamp-realtime":1683522394899032,"sender":":1.739","destination":"org.freedesktop.resolve1","path":"/org/freedesktop/resolve1","interface":"org.freedesktop.resolve1.Manager","member":"SetLinkDomains","payload":{"type":"ia(sb)","data":[8,[["",false]]]}}
ifindex, domains = d['payload']['data']
print(f"SetLinkDomains method called with arguments: interface_index={ifindex}, domains={domains}")
if not is_tailscale_interface(ifindex):
# Nothing to do here
domains_clean = []
update = False
for i in domains:
if i[0] == '.' and i[1] == True:
print('Found default route search domain, dropping');
update = True
if update:
# Call SetLinkDomains with cleaned domains
cmd = ['busctl', 'call', 'org.freedesktop.resolve1',
'ia(sb)', str(ifindex), str(len(domains_clean))]
for i in domains_clean:
#print(f"Debug: {cmd=}")
def main():
# Match doesn't seem to filter to the exact method_call=SetLinkDefaultRoute
command = ["busctl", "monitor", "-q", "org.freedesktop.resolve1", "--match",
# Start the dbus-monitor subprocess and capture the output
process = subprocess.Popen(command, stdout=subprocess.PIPE)
if not process.stdout:
raise RuntimeError('stdout is broken')
# Continuously read the output and extract any SetLinkDefaultRoute method call arguments
for line in iter(process.stdout.readline, b''):
d = json.loads(line)
member = d.get('member')
if member == 'SetLinkDefaultRoute':
elif member == 'SetLinkDomains':
if __name__ == '__main__':
Description=Tailscale Per Host Default Router Disabler
ExecStart=/usr/bin/python3 -u /usr/local/bin/
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment