Created
August 22, 2021 11:04
-
-
Save mosajjal/56a552f2d0a7a55f6106f91ba7e0ebe6 to your computer and use it in GitHub Desktop.
Automated Script to Use Cloudflare as a DNS server for Tailscale. Global alternative to Magic DNS
This file contains 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
""" | |
This script trys to add and delete records from cloudflare based on the current status of the account in tailscale, and eliminate the need for TailScale's magic DNS. | |
Each domain will be created under a subdomain "SUBDOMAIN", that way it'll be easier to find and detele records. | |
IMPORTANT: this script is not very well tested and it tries to DELETE some DNS records. Use it at your own risk, especially if your SUBDOMAIN value is generic and it's also used elsewhere in your domain. | |
""" | |
import os | |
import requests | |
TS_USERNAME = os.environ.get("TS_USERNAME") or "" # set env variable of TS_USERNAME for your Tailscale Username | |
TS_API_KEY = os.environ.get("TS_API_KEY") or "" # set env variable of TS_USERNAME for your Tailscale API Key | |
TS_API_URL = "https://api.tailscale.com/api/v2/" | |
CF_API_KEY = os.environ.get("CF_API_KEY") or "" # set env variable of CF_API_KEY for your Cloudflare API Key | |
CF_DOMAIN = os.environ.get("CF_DOMAIN") or "" # set env variable of CF_DOMAIN for the main domain in your Cloudflare zone. eg .example.com | |
CF_ZONE_ID = os.environ.get("CF_ZONE_ID") or "" # set env variable of CF_ZONE_ID for your Cloudflare domain Zone ID | |
CF_ACC_ID = os.environ.get("CF_ACC_ID") or "" # set env variable of CF_ACC_ID for your Cloudflare username, usually it'sa in email format | |
CF_API_URL = 'https://api.cloudflare.com/client/v4/' | |
DELIM = "-|_|-" # unique delimeter for the tailscale and cloudflare domains. | |
SUBDOMAIN = "tailscale" # creates the domains under tailscale "subdomain". eg host1.tailscale.example.com | |
# basic sanity check on the CF_DOMAIN | |
CF_DOMAIN = "." + CF_DOMAIN if CF_DOMAIN[0] != "." else CF_DOMAIN | |
def cf_grab_all_records_matching(zone_id, pattern): | |
out = dict() | |
headers = { | |
"X-Auth-Email": CF_ACC_ID, | |
"X-Auth-Key": CF_API_KEY, | |
"Content-Type": "application/json" | |
} | |
res = requests.get(f"{CF_API_URL}zones/{zone_id}/dns_records?per_page=100", headers=headers) | |
for item in res.json().get("result"): | |
if pattern in item.get("name"): | |
out[f"{item.get('name')}{DELIM}{item.get('content')}"] = {"id": item.get("id"), "type": item.get("type")} | |
return out | |
def cf_del_record(record_id): | |
headers = { | |
"X-Auth-Email": CF_ACC_ID, | |
"X-Auth-Key": CF_API_KEY, | |
"Content-Type": "application/json" | |
} | |
res = requests.delete(f"{CF_API_URL}zones/{CF_ZONE_ID}/dns_records/{record_id}", headers=headers) | |
return res.status_code == 200 | |
def cf_add_record(domain, value): | |
headers = { | |
"X-Auth-Email": CF_ACC_ID, | |
"X-Auth-Key": CF_API_KEY, | |
"Content-Type": "application/json" | |
} | |
data = { | |
"type": "AAAA" if ":" in value else "A", | |
"name": domain, | |
"content": value, | |
"ttl": 1, | |
"proxied": False | |
} | |
res = requests.post(f"{CF_API_URL}zones/{CF_ZONE_ID}/dns_records", headers=headers, json=data) | |
return res.status_code == 200 | |
def tailscale_current_status(suffix): | |
out = dict() | |
res = requests.get(f"{TS_API_URL}tailnet/{TS_USERNAME}/devices", auth=(TS_API_KEY,"")) | |
if res.status_code != 200: | |
return {"sucess":False, "message":f"Error: {res.json()}"} | |
else: | |
for item in res.json().get("devices"): | |
for address in item["addresses"]: | |
new_fqdn = item['name'].split(".")[0] + "." + suffix + CF_DOMAIN | |
type = "AAAA" if ":" in address else "A" | |
out[f"{new_fqdn}{DELIM}{address}"] = {"id": 0, "type": type} | |
return out | |
def main(): | |
ts_cf_zones = cf_grab_all_records_matching(CF_ZONE_ID, SUBDOMAIN + CF_DOMAIN) | |
ts_current = tailscale_current_status(SUBDOMAIN) | |
cf_to_del = [] | |
cf_to_add = [] | |
# items that are in ts_current and not in ts_cf_zones -> add | |
for item in list(set(ts_current.keys()) - set(ts_cf_zones.keys())): | |
cf_to_add.append((item.split(DELIM))) | |
# items that are in ts_cf_zones and not in ts_current -> delete | |
for item in list(set(ts_cf_zones.keys()) - set(ts_current.keys())): | |
cf_to_del.append(ts_cf_zones[item]['id']) | |
for item in cf_to_add: | |
res = cf_add_record(item[0], item[1]) | |
print(f"added {item[0]} with suceess: {res}") | |
for item in cf_to_del: | |
res = cf_del_record(item) | |
print(f"deleted {item} with suceess: {res}") | |
if len(cf_to_add) == 0 and len(cf_to_del) == 0: | |
print("Nothing to do") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment