Updatesr get_https_rrecord()

- quote vars (hoping it'll resolve the Mac runner issue)
- make sure CNAMEs are properly parsed
- end get_https_rrecord() earlier when there's no record but DNS binaries are "HTTPS record aware"
- while loop was redundant
- better comments

Elsewhere:

make sure get_https_rrecord is called with a trailing dot for the NODE
This commit is contained in:
Dirk Wetter
2026-06-01 16:14:29 +02:00
parent ba7d9604a9
commit 84bd9dd1a3
+76 -73
View File
@@ -2359,7 +2359,7 @@ hex2binary() {
# convert 414243 into ABC
hex2ascii() {
hex2binary $1
hex2binary "$1"
}
# arg1: text string
@@ -22622,7 +22622,8 @@ get_caa_rrecord() {
#
get_https_rrecord() {
local raw_https=""
local hash="" len line=""
local hash="" line=""
local len # better fix as integer?
local len_alpnID=""
local alpnID=""
local alpnID_wire=""
@@ -22634,42 +22635,36 @@ get_https_rrecord() {
[[ -n "$NODNS" ]] && return 2 # if minimum DNS lookup was instructed, leave here
"$HAS_DIG_NOIDNOUT" && noidnout="+noidnout"
# There's a) the possibility to query HTTPS RR records directly like "dig +short HTTPS dev.testssl.sh",
# "drill HTTPS FQDN" or "nslookup -type=HTTPS FQDN". This works for newer binaries only, unfortunately.
# On top of that b) there's also an extended format which e.g. cloudflare uses:
# $ host -t type65 testssl.net
# testssl.net has TYPE65 record \# 136 00010000010006026833026832000400086815229AAC43CDE7000500 470045FE0D0041A70020002057F87361C7B5A3B8CD3C028892690D35 2863623DAD4E03D33B231A4C3C8BB02B0004000100010012636C6F75 64666C6172652D6563682E636F6D0000000600202606470030310000 00000000AC43CDE72606470030360000000000006815229A
# $ host -t HTTPS testssl.net
# testssl.net has HTTPS record 1 . alpn="h3,h2" ipv4hint=104.21.34.154,172.67.205.231 ech=AEX+DQBBpwAgACBX+HNhx7WjuM08AoiSaQ01KGNiPa1OA9M7IxpMPIuwKwAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA= ipv6hint=2606:4700:3031::ac43:cde7,2606:4700:3036::6815:229a
# ECH is the encrypted client hello --> for esni (https://datatracker.ietf.org/doc/draft-ietf-tls-esni/)
# Nice description: https://www.netmeister.org/blog/https-rrs.html
# Thus we try first whether we can query the HTTPS records directly as this gives us that already
# in clear text and also we can avoid to parse the encoded format. We'll do that as a fallback but
# at this moment we're trying to scrape only the values alpn from it, if they come first.
# There's the possibility to query HTTPS RR records directly like "dig +short HTTPS dev.testssl.sh",
# "drill HTTPS FQDN" or "nslookup -type=HTTPS FQDN". This works for new binaries only. Thus we try first
# whether we can query the HTTPS records directly as this gives us that already everything we want in
# in clear text and also we can avoid to parse the encoded formats.
# "tail -1" and the awk commands make sure we use the right lines when we encounter a CNAME
OPENSSL_CONF=""
if "$HAS_DIG_HTTPS"; then
text_httpsrr=$(dig +short +search +timeout=3 +tries=3 $noidnout HTTPS "$1" 2>/dev/null)
text_httpsrr="$(dig +short +search +timeout=3 +tries=3 $noidnout HTTPS "$1" 2>/dev/null | tail -1)"
elif "$HAS_DRILL_HTTPS"; then
text_httpsrr=$(drill -Q HTTPS $1 2>/dev/null)
text_httpsrr="$(drill -Q HTTPS "$1" 2>/dev/null | tail -1)"
elif "$HAS_HOST_HTTPS"; then
text_httpsrr=$(host -t HTTPS $1 2>/dev/null)
text_httpsrr=${text_httpsrr#*record }
elif "$HAS_NSLOOKUP_HTTPS"; then # from 4th field onwards \/
text_httpsrr=$(nslookup -type=HTTPS $1 | awk '/'"^${1}"'.*rdata_65// { print substr($0,index($0,$4)) }')
text_httpsrr="$(host -t HTTPS "$1" 2>/dev/null | awk -F'HTTP service bindings ' '/HTTP service bindings /{print $2}')"
elif "$HAS_NSLOOKUP_HTTPS"; then
text_httpsrr="$(nslookup -type=HTTPS "$1" | awk -F'rdata_65 = ' '/rdata_65 =/{print $2}' )"
fi
if [[ -n "$text_httpsrr" ]]; then
safe_echo "$text_httpsrr"
OPENSSL_CONF="$saved_openssl_conf" # see https://github.com/drwetter/testssl.sh/issues/134
return 0
elif "$HAS_DIG_HTTPS" || "$HAS_DRILL_HTTPS" || "$HAS_HOST_HTTPS" || "$HAS_NSLOOKUP_HTTPS"; then
# no record despite binaries are "HTTPS record aware"
OPENSSL_CONF="$saved_openssl_conf"
return 0
fi
# Now we need to try parsing the raw output
# Format probably: https://www.rfc-editor.org/rfc/rfc3597 (plus updates)
# If there's a type65 record there are 2x3 output formats, mostly depending on age of distribution
# -- roughly that's the difference between text and binary format -- and the type of DNS client
# As we didn't succeed yet, we need to try parsing the raw output. First is to get the TYPE65 record
# as text. These days (2026) it's not that common anymore. Mac is the party pooper as it normally returns
# a hex stream only --in 2026. Here's how output of old+ancient client DNS binaries may look like with TYPE65
# for host:
# 1) 'google.com has HTTPS record 1 . alpn="h2,h3" '
@@ -22687,10 +22682,10 @@ get_https_rrecord() {
raw_https="$(dig $DIG_R +short +search +timeout=3 +tries=3 $noidnout type65 "$1" 2>/dev/null)"
# empty if there's no such record
elif "$HAS_DRILL"; then
raw_https="$(drill $1 type65 | grep -v '^;;' | awk '/'"^${1}"'.*TYPE65/ { print substr($0,index($0,$5)) }' )" # from 5th field onwards
raw_https="$(drill "$1" type65 | grep -v '^;;' | awk '/'"^${1}"'.*TYPE65/ { print substr($0,index($0,$5)) }' )" # from 5th field onwards
# empty if there's no such record
elif "$HAS_HOST"; then
raw_https="$(host -t type65 $1)"
raw_https="$(host -t type65 "$1")"
if [[ "$raw_https" =~ "has no HTTPS|has no TYPE65" ]]; then
raw_https=""
else
@@ -22698,76 +22693,82 @@ get_https_rrecord() {
raw_https="${raw_https/$1 has TYPE65 record /}"
fi
elif "$HAS_NSLOOKUP"; then
raw_https="$(strip_lf "$(nslookup -type=type65 $1 | awk '/'"^${1}"'.*rdata_65/ { print substr($0,index($0,$4)) }' )")"
raw_https="$(strip_lf "$(nslookup -type=type65 "$1" | awk '/'"^${1}"'.*rdata_65/ { print substr($0,index($0,$4)) }' )")"
# empty if there's no such record
else
return 1
# No dig, drill, host, or nslookup --> complaint was elsewhere already
fi
OPENSSL_CONF="$saved_openssl_conf" # see https://github.com/drwetter/testssl.sh/issues/134
OPENSSL_CONF="$saved_openssl_conf" # We're done now with openssl, see https://github.com/drwetter/testssl.sh/issues/134
# dig +short HTTPS dev.testssl.sh / dig +short type65 dev.testssl.sh
# 1 . alpn="h2" port=443 ipv6hint=2a01:238:4308:a920:1000:0:b:1337
#
# 36 000100000100030268320003000201BB000600102A0102384308A920 10000000000B1337
# alpn| L h 2 443 2a010238... L=len
#
# -----------------
# testssl.net (split over a couple of lines)
#
# 1. alpn="h3,h2" ipv4hint=104.21.34.154,172.67.205.231
# 136 00010000010006026833026832000400086815229AAC43CDE7000500 470045FE0D0041F3002000202BD0935ED66980C1862F2570C0D6014D
# alpn| L h 3 L h 2 |IPv4#1||IPv4#2|
# Now comes the third, more tricky part and the last straw --> parsing the hex stream which was returned if it was returned.
# Format is like: https://www.rfc-editor.org/rfc/rfc3597 (plus updates)
# ech=AEX+DQBBzgAgACBQGA9EFbz+PkJAXSXtcqJluxLlhxIgzhJ+GhTtRd4nJQAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA= ipv6hint=2606:4700:3031::ac43:cde7,2606:4700:3036::6815:229a
# 733A7CFAAEA5E4DD9CA43D4C24199E330004000100010012636C6F75 64666C6172652D6563682E636F6D0000000600202606470030310000 00000000AC43CDE72606470030360000000000006815229A
# | cloudflare-ech.com | IPv6#1 #IPv6#2
# dev.testssl.sh:
# 1 . alpn="h2" port=443 ipv6hint=2a01:238:4308:a920:1000:0:b:1337
#
# 36 000100000100030268320003000201BB000600102A0102384308A920 10000000000B1337
# TL alpn| L h 2 443 2a010238... L=len of alpn entries, TL=total length of the following by, excluding spaces
#
# -----------------
# testssl.net (here hown over a couple of lines):
# 1 . alpn="h3,h2" ipv4hint=104.21.34.154,172.67.205.231 ech=AEX+DQBBpwAgACBX+HNhx7WjuM08AoiSaQ01KGNiPa1OA9M7IxpMPIuwKwAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA= ipv6hint=2606:4700:3031::ac43:cde7,2606:4700:3036::6815:229a
# ECH is the encrypted client hello --> for esni (https://datatracker.ietf.org/doc/draft-ietf-tls-esni/)
# Nice description: https://www.netmeister.org/blog/https-rrs.html
#
# 1. alpn="h3,h2" ipv4hint=104.21.34.154,172.67.205.231
# 136 00010000010006026833026832000400086815229AAC43CDE7000500 470045FE0D0041F3002000202BD0935ED66980C1862F2570C0D6014D
# TL alpn| L h 3 L h 2 |IPv4#1||IPv4#2|
#
# ech=AEX+DQBBzgAgACBQGA9EFbz+PkJAXSXtcqJluxLlhxIgzhJ+GhTtRd4nJQAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA= ipv6hint=2606:4700:3031::ac43:cde7,2606:4700:3036::6815:229a
# 733A7CFAAEA5E4DD9CA43D4C24199E330004000100010012636C6F75 64666C6172652D6563682E636F6D0000000600202606470030310000 00000000AC43CDE72606470030360000000000006815229A
# | cloudflare-ech.com | IPv6#1 #IPv6#2
# Now comes the last straw, decoding og the stream. It only works for short entries like 1. alpn=h2,h3
# Be aware that all variables are strings here! Therefore we use double quotes so that e.g. 03 won't become 3
# For now the following only works for short entries like 1. alpn=h2,h3
if [[ -z "$raw_https" ]]; then
return 1
elif [[ "$raw_https" =~ \#\ [0-9][0-9] ]]; then
while read hash len line ;do
# signals that we're on the right track with type65 interpretation
read hash len line <<< "$raw_https"
# \# 10 00010000010003026832
# \# 36 000100000100030268320003000201BB000600102A01023842816755 10000000000B1337
[[ $DEBUG -eq 1 ]] && echo "$hash $len $line"
if [[ "${line:0:4}" == 0001 ]]; then # marker to proceed, belongs to SvcPriority, see rfc9460, 2.4.3
svc_priority=$(printf "%0d" "$((10#${line:2:2}))") # 1 is most often, 0 is alias
if [[ $svc_priority == 1 ]]; then
if [[ $svc_priority == "1" ]]; then
# mock text representation
svc_priority="$svc_priority . "
svc_priority+=" . "
alpnID="${alpnID}${svc_priority}"
fi
if [[ ${line:8:2} == 01 ]]; then # Then comes SvcParamKeys, see rfc 14.3.2 which should be alpn=1
alpnID="${alpnID}alpn=\"" # double quote for clear text
if [[ ${line:8:2} == "01" ]]; then # Then comes SvcParamKeys, see rfc 14.3.2 which should be alpn=1
alpnID+="\"" # double quote for clear text
else
continue # If the 1st element is not alpn, next iteration of loop will fail.
continue # If the 1st element is not alpn, next iteration of loop will fail for now
fi # Should we care as SvcParamKey!=alpn doesn't seems not very common?
len_alpnID=${line:12:2} # length of alpn entries (1st?)
alpnID_wire=${line:16:4} # value of first entry
alpnID=${alpnID}$(hex2ascii $alpnID_wire)
# from here it only works for one simple entry (like h2 or h2,h3)
if [[ "$len_alpnID" != "03" ]]; then # 06 would be another entry e.g. h3, quote the rhs!
alpnID_wire=${line:22:4} #FIXME: we can't cope with three entries yet
alpnID="${alpnID},$(hex2ascii $alpnID_wire)"
len_alpnID="${line:12:2}" # length of alpn entries, e.g. 03 or 06
alpnID_wire="${line:16:4}" # value of first entry
alpnID="${alpnID}$(hex2ascii "$alpnID_wire")"
localPTR=20
if [[ "$len_alpnID" == "06" ]]; then # 06 would be another entry e.g. h3, quote the rhs!
alpnID_wire="${line:22:4}" #FIXME: we can't cope with three entries yet
alpnID="${alpnID},$(hex2ascii "$alpnID_wire")"
localPTR=26
fi
[[ ${line:8:2} == 01 ]] && alpnID="${alpnID}\"" # if alpn add trailing double quote
[[ ${line:8:2} == "01" ]] && alpnID+="\"" # if alpn and we're done add trailing double quote
# best is to check len and then stop now
if [[ $len -eq 10 ]]; then
[[ $DEBUG -eq 1 ]] && echo "end 10"
break
fi
# done if localPTR / 2 = len or localPTR not empty
echo "key: ${line:localPTR:4}"
# key=1: alpn
# key=3: port
# key=4: IPv4
# key=6: IPv6
# len_alpnID=$((len_alpnID*2)) # =>word! Now get name from 4th and value from 4th+len position...
# alpnID="$(hex2ascii ${line:4:$len_alpnID})"
# alpnID_wire="$(hex2ascii "${line:$((4+len_alpnID)):100}")"
else
out "please report unknown HTTPS RR $line with flag @ $NODE"
return 7
fi
done <<< "$raw_https"
safe_echo "$alpnID"
fi
return 0
@@ -23602,6 +23603,7 @@ dns_https_rr () {
local jsonID="DNS_HTTPS_rrecord"
local https_rr=""
local indent=""
local https_rr_node="$NODE"
out "$indent"; pr_bold " DNS HTTPS RR"; out " (expt.): "
if [[ -n "$NODNS" ]]; then
@@ -23611,9 +23613,11 @@ dns_https_rr () {
out "(instructed to use the proxy for DNS only)"
fileout "${jsonID}" "INFO" "check skipped as instructed (proxy)"
else
https_rr="$(get_https_rrecord $NODE)"
# append a dot if there was none
[[ $https_rr_node =~ '.'$ ]] || https_rr_node+="."
https_rr="$(get_https_rrecord $https_rr_node)"
if [[ -n "$https_rr" ]]; then
pr_svrty_good "yes" ; out ": "
pr_svrty_good "yes" ; out ": "
prln_italic "$(out_row_aligned_max_width "$https_rr" "$indent " $TERM_WIDTH)"
fileout "${jsonID}" "OK" "$https_rr"
else
@@ -23624,7 +23628,6 @@ dns_https_rr () {
}
# Check messages which needed to be processed. I.e. those which would have destroyed the nice
# screen output and thus havve been postponed. This is just an idea and is only used once
# but can be extended in the future. An array might be more handy