Skip to content

Instantly share code, notes, and snippets.

@gwire
Last active August 10, 2022 22:18
Show Gist options
  • Save gwire/94f0d9f656194585022d7e71807c45a4 to your computer and use it in GitHub Desktop.
Save gwire/94f0d9f656194585022d7e71807c45a4 to your computer and use it in GitHub Desktop.
Tool to generate SVCB/HTTPS DNS records for tinydns
#!/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