Last active
August 10, 2022 22:18
-
-
Save gwire/94f0d9f656194585022d7e71807c45a4 to your computer and use it in GitHub Desktop.
Tool to generate SVCB/HTTPS DNS records for tinydns
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
#!/usr/bin/env python3 | |
# tinysvcb - generate a RR type 64 SVCB or 65 HTTPS records in tinydns wire format | |
# | |
# example: ./tinysvcb.py --https --domain example.com --priority 0 --target host.example.com | |
# | |
# Based on https://datatracker.ietf.org/doc/draft-ietf-dnsop-svcb-https/10/ | |
# | |
# 2022 Lee Maguire | |
import sys | |
import getopt | |
import ipaddress | |
import base64 | |
rrtype = "65" | |
domain = "example.com" | |
priority = "0" | |
target = "host.example.com" | |
parameters = "" | |
ttl = "86400" | |
def tinySVCBRecord( rrtype, domain, priority, target, parameters, ttl ): | |
output = ":" + escapeText( domain ) + ":" + rrtype + ":" | |
output += octNboInt( 2, priority ) | |
output += lengthPrefixedLabels( target ) | |
if len(parameters) > 0: | |
output += encodeParameters( parameters ) | |
output += ":" + ttl | |
return( output ) | |
def octNboInt( length, number ): | |
## octal representation of an integer in network byte order | |
## if input "2, 256", output should be "\001\000" | |
## "network byte order" is big-endian | |
output = "" | |
intbytes = int(number).to_bytes(length, "big") | |
for i in range(length): | |
output += "\\{0:03o}".format(intbytes[i]) | |
return( output ) | |
def lengthPrefixedLabels( domain ): | |
## if input "example.com", output should be "\007example\003com\000" | |
## if input ".", output should be "\000" | |
## TODO: account for pre-escaped text | |
output = "" | |
#print(domain) | |
if len(domain) > 1: | |
for label in domain.split("."): | |
output += "\\{0:03o}".format(int(len(label))) | |
output += escapeText(label) | |
output += "\\{0:03o}".format(int(0)) | |
return( output ) | |
def charString( text ): | |
## Appendix A in the spec | |
output = text ## TODO: actually implement | |
return( output ) | |
def escapeText( text ): | |
## escape characters that aren't valid in tinydns records | |
output = "" | |
for c in text: | |
if c in ["\r","\n","\t",":"," ","\\","/"]: | |
output = output + "\\{0:03o}".format(ord(c)) | |
else: | |
output = output + c | |
return( output ); | |
def octIPv6( ipv6addr ): | |
ip6_bytes = ipaddress.IPv6Address(ipv6addr).packed | |
output = "" | |
for j in range(16): | |
output += "\\{0:03o}".format(ip6_bytes[j]) | |
return( output ) | |
def octIPv4( ipv4addr ): | |
ip4_bytes = ipaddress.IPv4Address(ipv4addr).packed | |
output = "" | |
for j in range(4): | |
output += "\\{0:03o}".format(ip4_bytes[j]) | |
return( output ) | |
def getParamId( key ): | |
if key in "mandatory": | |
return( 0 ) | |
elif key in "alpn": | |
return( 1 ) | |
elif key in "no-default-alpn": | |
return( 2 ) | |
elif key in "port": | |
return( 3 ) | |
elif key in "ipv4hint": | |
return( 4 ) | |
elif key in "ech": | |
return( 5 ) | |
elif key in "ipv6hint": | |
return( 6 ) | |
elif key.startswith('key'): | |
return( int(key[3:]) ) | |
def encodeParameters( parameters ): | |
output = "" | |
paramdict = {} | |
## put parameter ids and raw values into a dictonary | |
for param in parameters.split(" "): | |
if param in "no-default-alpn": | |
svcint = getParamId( param ) | |
paramdict[svcint] = "empty" ## value is ignored | |
else: | |
key,value = param.split("=") | |
value = value.strip('\"') | |
value = value.strip('\'') | |
svcint = getParamId( key ) | |
paramdict[svcint] = value | |
## go through the dictionary in id order | |
##print(paramdict) | |
for svcid in sorted(paramdict): | |
output += octNboInt(2, svcid) | |
if svcid == 0: # mandatory | |
mandatory_ids = [] | |
mandatory = paramdict[svcid].split(",") | |
for mandsvc in mandatory: | |
mandatory_ids.append( getParamId(mandsvc) ) | |
output += octNboInt(2, len(mandatory_ids) * 2) # length is fixed 2 bytes per item | |
## go through array in id order | |
for i in sorted(mandatory_ids): | |
output += octNboInt(2, i) | |
elif svcid == 1: # alpn | |
alpns = paramdict[svcid].split(",") | |
alpn_output = "" | |
alpn_length = 0 | |
for alpn in alpns: | |
alpn_length = alpn_length + len(alpn) + 1 | |
alpn_output += octNboInt(1, len(alpn)) ## TODO: account for escaped text | |
alpn_output += escapeText(alpn) | |
output += octNboInt(2, alpn_length) | |
output += alpn_output | |
elif svcid == 3: # port | |
output += octNboInt(2, 2) # length is fixed 2 bytes | |
output += octNboInt(2, paramdict[svcid] ) # port value is just an int | |
elif svcid == 4: # ipv4hint | |
addresses = paramdict[svcid].split(",") | |
output += octNboInt(2, len(addresses) * 4) # length is fixed 4 bytes per item | |
for ipv4addr in addresses: | |
output += octIPv4( ipv4addr ) | |
elif svcid == 5: # ech | |
ech = base64.b64decode(paramdict[svcid]) | |
output += octNboInt(2, len(ech)) | |
for i in range(len(ech)): | |
output += "\\{0:03o}".format(ech[i]) | |
elif svcid == 6: # ipv6hint | |
addresses = paramdict[svcid].split(",") | |
output += octNboInt(2, len(addresses) * 16) # length is fixed 16 bytes per item | |
for ipv6addr in addresses: | |
output += octIPv6( ipv6addr ) | |
elif svcid > 6: | |
key_val = charString(paramdict[svcid]) | |
output += octNboInt(2, len(key_val)) | |
output += escapeText(key_val) | |
return( output ) | |
opts, args = getopt.getopt(sys.argv[1:],"hd:p:t:a:l:",["help","https","svcb","priority=","target=","domain=","ttl=","parameters="]) | |
for opt, arg in opts: | |
if opt in ("-h", "--help"): | |
print('Usage: tinysvcb.py --https --domain example.com --priority 0 --target host.example.com') | |
print(' --svcb (Use RR 64)') | |
print(' --https (Use RR 65)') | |
print(' --priority int (0 for alias)') | |
print(' --domain hostname (domain hostname)') | |
print(' --target hostname (service hostname)') | |
print(' --parameters "key=value key=value" (parameter list)') | |
print(' --ttl int (dns ttl)') | |
sys.exit() | |
elif opt in ("--svcb"): | |
rrtype = "64" | |
elif opt in ("--https"): | |
rrtype = "65" | |
elif opt in ("-d", "--domain"): | |
domain = arg | |
elif opt in ("-p", "--priority"): | |
priority = arg | |
elif opt in ("-t", "--target"): | |
target = arg | |
elif opt in ("-a", "--parameters"): | |
parameters = arg | |
elif opt in ("-l", "--ttl"): | |
ttl = arg | |
line = tinySVCBRecord( rrtype, domain, priority, target, parameters, ttl) | |
sys.stdout.write( line + "\n") | |
sys.exit(0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment