feat: initial commit

This commit is contained in:
PorridgePi
2023-10-08 02:45:45 +08:00
parent bc39503175
commit bc85ea63ab
2 changed files with 279 additions and 0 deletions

20
config-sample.yaml Normal file
View File

@@ -0,0 +1,20 @@
identifier: com.example.doh
individualConfig: true
devices:
- name: iPhone
uuid: e124199a-3777-4652-b75f-d87dfe407031
dns:
- name: Cloudflare
url: https://cloudflare-dns.com/dns-query
includeDeviceInfo: false
formatDeviceInfo: true
- name: Quad9
url: https://dns.quad9.net/dns-query
includeDeviceInfo: false
formatDeviceInfo: true
excluded_ssids:
- MyWifiSSID

259
doh-mobileconfig.py Normal file
View File

@@ -0,0 +1,259 @@
# requires httpx, dnspython
import re, uuid, yaml, requests, argparse, urllib.parse, base64, httpx, dns.message, dns.query, dns.rdatatype
parser = argparse.ArgumentParser()
parser.add_argument("-p", "--print", action="store_true", help="print formatted device information and URLs")
parser.add_argument("--suffix", action="store", help="sets a suffix to device information, defaults to '-wan'")
parser.add_argument("-c", "--config", action="store", help="specify custom location to config.yaml")
args = parser.parse_args()
# https://stackoverflow.com/a/54254115
def isValidUUID(val):
try:
uuid.UUID(str(val))
return True
except ValueError:
return False
# https://stackoverflow.com/a/7160778
def isValidURL(url):
regex = re.compile(
r'^(?:http|ftp)s?://' # http:// or https://
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain...
r'localhost|' #localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
return(re.match(regex, str(url)) is not None)
mobileConfigTemplate = """
<?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>PayloadContent</key>
<array>
REPLACE_DNS_RECORDS
</array>
<key>PayloadDescription</key>
<string>Adds encrypted DNS configurations (DNS-over-HTTPS or DNS-over-TLS) to macOS 11.0 Big Sur (or newer) and iOS 14 (or newer) based systems</string>
<key>PayloadDisplayName</key>
<string>Personal DoH Settings</string>
<key>PayloadIdentifier</key>
<string>REPLACE_IDENTIFIER</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>REPLACE_UUID</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
"""
EDNSRecordTemplate = """
<dict>
<key>DNSSettings</key>
<dict>
<key>DNSProtocol</key>
<string>HTTPS</string>
<key>ServerAddresses</key>
<array/>
<key>ServerURL</key>
<string>REPLACE_SERVER_URL</string>
</dict>
<key>OnDemandRules</key>
<array>
<dict>
<key>Action</key>
<string>Disconnect</string>
<key>SSIDMatch</key>
<array>
REPLACE_EXCLUDED_SSID
</array>
</dict>
<dict>
<key>Action</key>
<string>Connect</string>
<key>InterfaceTypeMatch</key>
<string>WiFi</string>
</dict>
<dict>
<key>Action</key>
<string>Connect</string>
<key>InterfaceTypeMatch</key>
<string>Cellular</string>
</dict>
<dict>
<key>Action</key>
<string>Disconnect</string>
</dict>
</array>
<key>PayloadDescription</key>
<string>Configures device to use REPLACE_DNS_NAME DNS-over-HTTPS</string>
<key>PayloadDisplayName</key>
<string>REPLACE_DNS_NAME DoH</string>
<key>PayloadIdentifier</key>
<string>com.apple.dnsSettings.managed.REPLACE_UUID</string>
<key>PayloadType</key>
<string>com.apple.dnsSettings.managed</string>
<key>PayloadUUID</key>
<string>REPLACE_UUID</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>ProhibitDisablement</key>
<false/>
</dict>
"""
if (args.config):
with open(args.config, 'r') as file:
config = yaml.safe_load(file)
else:
with open('config.yaml', 'r') as file:
config = yaml.safe_load(file)
def isValidDOH(url):
try:
# resp = requests.get(
# url = url,
# params = {"name": "one.one.one.one", "type": "A"},
# headers = {"accept": "application/dns-json"}
# )
# for ans in resp.json()["Answer"]:
# if ans["data"] == "1.1.1.1" or ans["data"] == "1.0.0.1":
# return True
# reqRaw = dns.message.make_query("one.one.one.one", 'A', id=0).to_wire()
# reqStr = base64.urlsafe_b64encode(reqRaw).decode('utf-8').rstrip('=')
# resp = requests.get(
# url = url,
# params = {"dns": reqStr},
# headers = {"accept": "application/dns-message"}
# )
# respDecoded = str(dns.message.from_wire(resp.content))
# if ("1.1.1.1" in respDecoded or "1.0.0.1" in respDecoded):
# return True
# else:
# print(resp)
# print(respDecoded)
# return False
with httpx.Client(follow_redirects=True) as client:
q = dns.message.make_query("one.one.one.one", dns.rdatatype.A)
r = dns.query.https(q, url, session=client)
for answer in r.answer:
if ("1.1.1.1" in str(answer) or "1.0.0.1" in str(answer)):
return True
else:
print(answer)
return False
except Exception as e:
print(e)
return False
def formatDeviceInfo(deviceName, deviceUUID, toFormat = True, addSuffix = True):
if (args.suffix is not None):
suffix = args.suffix
suffix = re.sub(r"[^\w\s]", '', suffix)
suffix = re.sub(r"\s+", '-', suffix)
else:
suffix = "wan"
if (not isValidUUID(deviceUUID)):
print("ERROR: Invalid device UUID!")
print(" Device:", deviceName)
print(" UUID:", device["uuid"])
exit()
if (toFormat):
deviceName = deviceName.lower()
# https://stackoverflow.com/a/1007615
# Remove all non-word characters (everything except numbers and letters)
deviceName = re.sub(r"[^\w\s]", '', deviceName)
# Replace all runs of whitespace with a single dash
deviceName = re.sub(r"\s+", '-', deviceName)
if (addSuffix):
return deviceUUID + '-' + deviceName + '-' + suffix
else:
return deviceUUID + '-' + deviceName
else:
return urllib.parse.quote(deviceName + ' (' + deviceUUID + ')', safe='')
def newUUID4(match):
return str(uuid.uuid4())
def main():
newRecords = []
for device in config["devices"]:
if (config["individualConfig"]):
newRecords = []
for dns in config["dns"]:
url = dns["url"]
if (not isValidURL(url)):
print("ERROR: Invalid URL!")
print(" URL:", url)
exit()
if (url[-1] == '/'):
url = url[:-1]
if (dns["includeDeviceInfo"]):
url += '/' if (url[-1] != '/') else ''
urlPath = formatDeviceInfo(device["name"], device["uuid"], dns["formatDeviceInfo"])
url = url + urlPath
if (not isValidDOH(url)):
print("ERROR: Invalid DOH URL!")
print(" URL:", url)
exit()
print(url)
excludedSSIDs = []
for ssid in config["excluded_ssids"]:
excludedSSIDs.append(" <string>" + ssid + "</string>")
excludedSSIDs = '\n'.join(excludedSSIDs)
newRecord = EDNSRecordTemplate
newRecord = re.sub(r"REPLACE_EXCLUDED_SSID", excludedSSIDs, newRecord)
newRecord = re.sub(r"REPLACE_SERVER_URL", url, newRecord)
if (dns["includeDeviceInfo"]):
newRecord = re.sub(r"REPLACE_DNS_NAME", dns["name"] + ' (' + device["name"] + ')', newRecord)
newRecords.append(newRecord)
else:
newRecord = re.sub(r"REPLACE_DNS_NAME", dns["name"], newRecord)
if (newRecord not in newRecords):
newRecords.append(newRecord)
if (config["individualConfig"]):
newRecords = "\n".join(newRecords)
mobileConfig = mobileConfigTemplate
mobileConfig = re.sub(r"REPLACE_IDENTIFIER", config["identifier"] + '-' + formatDeviceInfo(device["name"], device["uuid"], True, False), mobileConfig)
mobileConfig = re.sub(r"REPLACE_DNS_RECORDS", newRecords, mobileConfig)
mobileConfig = re.sub(r"REPLACE_UUID", newUUID4, mobileConfig)
with open(formatDeviceInfo(device["name"], device["uuid"], True, False) + '-' + "DoH.mobileconfig", "w") as file:
file.write(mobileConfig)
if (not config["individualConfig"]):
newRecords = "\n".join(newRecords)
mobileConfig = mobileConfigTemplate
mobileConfig = re.sub(r"REPLACE_IDENTIFIER", config["identifier"], mobileConfig)
mobileConfig = re.sub(r"REPLACE_DNS_RECORDS", newRecords, mobileConfig)
mobileConfig = re.sub(r"REPLACE_UUID", newUUID4, mobileConfig)
with open("DoH.mobileconfig", "w") as file:
file.write(mobileConfig)
if (args.print):
for device in config["devices"]:
print(formatDeviceInfo(device["name"], device["uuid"], True))
else:
main()