feat: added support for RR type 64 (SVCB)

* Added Service Binding RR support (type 64)

This is just using the exact same code as the HTTPS parser as they are formatted in the same manner.
This commit is contained in:
Justin Fisher 2025-07-08 21:34:48 -04:00 committed by LittleChest
parent ab3c5ca3f5
commit 577e88a9d7
3 changed files with 197 additions and 30 deletions

View File

@ -1838,6 +1838,8 @@ rhttpssvc.encodingLength = function (data) {
return len return len
} }
const rsvcb = rhttpssvc // SCVB is the same parser as HTTPS
exports.svcb = rsvcb
const renc = exports.record = function (type) { const renc = exports.record = function (type) {
switch (type.toUpperCase()) { switch (type.toUpperCase()) {
@ -1864,6 +1866,7 @@ const renc = exports.record = function (type) {
case 'DS': return rds case 'DS': return rds
case 'NAPTR': return rnaptr case 'NAPTR': return rnaptr
case 'TLSA': return rtlsa case 'TLSA': return rtlsa
case 'SVCB': return rsvcb
case 'HTTPS': return rhttpssvc case 'HTTPS': return rhttpssvc
} }
return runknown return runknown

210
test.js
View File

@ -654,13 +654,40 @@ const unhexlify = (hex) => {
// <HTTPS SVCB test cases> // <HTTPS SVCB test cases>
const debug_https = false const debug_https = false
const test_http_decode_encode = (t, testname, packetbuf, expected, skip_encode=false, skip_memcmp_bufs=false) => {
const test_scvb_decode_encode = (t, testname, packetbuf, expected, skip_encode=false, skip_memcmp_bufs=false) => {
const decoded = packet.svcb.decode(packetbuf, 0)
if (debug_https) {
console.log(`${testname}: decode:`)
console.log(JSON.stringify(decoded, null, 2))
}
t.ok(compare(t, decoded, expected), 'svcb ' + testname + ' decode')
const encoded = packet.svcb.encode(expected)
if (!skip_encode) {
if (debug_https) {
console.log(`${testname}: encode:`)
hexdump(encoded)
}
if (!skip_memcmp_bufs) {
t.ok(compare(t, packetbuf, encoded), 'svcb ' + testname + ' encode memcmp')
}
// now decode the encoded buffer and check for sameness
const recoded = packet.svcb.decode(encoded, 0)
if (debug_https) {
console.log(`${testname}: recode`)
console.log(JSON.stringify(decoded, null, 2))
}
t.ok(compare(t, recoded, expected), 'svcb ' + testname + ' recode')
}
}
const test_https_decode_encode = (t, testname, packetbuf, expected, skip_encode=false, skip_memcmp_bufs=false) => {
const decoded = packet.httpssvc.decode(packetbuf, 0) const decoded = packet.httpssvc.decode(packetbuf, 0)
if (debug_https) { if (debug_https) {
console.log(`${testname}: decode:`) console.log(`${testname}: decode:`)
console.log(JSON.stringify(decoded, null, 2)) console.log(JSON.stringify(decoded, null, 2))
} }
t.ok(compare(t, decoded, expected), testname + ' decode') t.ok(compare(t, decoded, expected), 'https ' + testname + ' decode')
const encoded = packet.httpssvc.encode(expected) const encoded = packet.httpssvc.encode(expected)
if (!skip_encode) { if (!skip_encode) {
if (debug_https) { if (debug_https) {
@ -668,7 +695,7 @@ const test_http_decode_encode = (t, testname, packetbuf, expected, skip_encode=f
hexdump(encoded) hexdump(encoded)
} }
if (!skip_memcmp_bufs) { if (!skip_memcmp_bufs) {
t.ok(compare(t, packetbuf, encoded), testname + ' encode memcmp') t.ok(compare(t, packetbuf, encoded), 'https ' + testname + ' encode memcmp')
} }
// now decode the encoded buffer and check for sameness // now decode the encoded buffer and check for sameness
const recoded = packet.httpssvc.decode(encoded, 0) const recoded = packet.httpssvc.decode(encoded, 0)
@ -676,16 +703,61 @@ const test_http_decode_encode = (t, testname, packetbuf, expected, skip_encode=f
console.log(`${testname}: recode`) console.log(`${testname}: recode`)
console.log(JSON.stringify(decoded, null, 2)) console.log(JSON.stringify(decoded, null, 2))
} }
t.ok(compare(t, recoded, expected), testname + ' recode') t.ok(compare(t, recoded, expected), 'https ' + testname + ' recode')
} }
return encoded return encoded
} }
const test_https_svcb_decode_encode = (t, testname, packetbuf, expected, skip_encode=false, skip_memcmp_bufs=false) => {
test_scvb_decode_encode(t, testname, packetbuf, expected, skip_encode, skip_memcmp_bufs)
return test_https_decode_encode(t, testname, packetbuf, expected, skip_encode, skip_memcmp_bufs)
}
tape('https svcb', function (t) { tape('https svcb', function (t) {
// see https://datatracker.ietf.org/doc/rfc9460/ for the test vectors // for the test vectors see:
// https://datatracker.ietf.org/doc/rfc9460/
// https://github.com/MikeBishop/dns-alt-svc/blob/main/draft-ietf-dnsop-svcb-https.md
testEncoder(t, packet.svcb, {
priority: 16,
name: "foo.example.org",
values: {
mandatory: [
"alpn",
"ipv4hint"
],
alpn: [
"h2",
"h3-19"
],
ipv4hint: [
"192.0.2.1"
]
}
}
)
testEncoder(t, packet.httpssvc, {
priority: 16,
name: "foo.example.org",
values: {
mandatory: [
"alpn",
"ipv4hint"
],
alpn: [
"h2",
"h3-19"
],
ipv4hint: [
"192.0.2.1"
]
}
}
)
// https AliasMode // https AliasMode
test_http_decode_encode(t, 'https rfc9460 case1 (AliasMode)', test_https_svcb_decode_encode(t, 'rfc9460 case1 (AliasMode)',
unhexlify( unhexlify(
"00 13" + // rdata len "00 13" + // rdata len
"00 00" + // priority "00 00" + // priority
@ -698,7 +770,7 @@ tape('https svcb', function (t) {
) )
// https target name is "." // https target name is "."
test_http_decode_encode(t, 'https rfc9460 case2 (target name ".")', test_https_svcb_decode_encode(t, 'rfc9460 case2 (target name ".")',
unhexlify( unhexlify(
"00 03" + // rdata len "00 03" + // rdata len
"00 01" + // priority "00 01" + // priority
@ -711,7 +783,7 @@ tape('https svcb', function (t) {
) )
// https port // https port
test_http_decode_encode(t, 'https rfc9460 case3 (port)', test_https_svcb_decode_encode(t, 'rfc9460 case3 (port)',
unhexlify( unhexlify(
"00 19" + // rdata len "00 19" + // rdata len
"00 10" + // priority "00 10" + // priority
@ -730,7 +802,7 @@ tape('https svcb', function (t) {
) )
// https generic key and value // https generic key and value
test_http_decode_encode(t, 'https rfc9460 case4 (generic key,val)', test_https_svcb_decode_encode(t, 'rfc9460 case4 (generic key,val)',
unhexlify( unhexlify(
"00 1c" + // rdata len "00 1c" + // rdata len
"00 01" + // priority "00 01" + // priority
@ -750,7 +822,7 @@ tape('https svcb', function (t) {
) )
// https generic key and value with decimal escape // https generic key and value with decimal escape
test_http_decode_encode(t, 'https rfc9460 case5 (generic key,val with escape)', test_https_svcb_decode_encode(t, 'rfc9460 case5 (generic key,val with escape)',
unhexlify( unhexlify(
"00 20" + // rdata len "00 20" + // rdata len
"00 01" + // priority "00 01" + // priority
@ -770,7 +842,7 @@ tape('https svcb', function (t) {
) )
// https two quoted ipv6 hints // https two quoted ipv6 hints
test_http_decode_encode(t, 'https rfc9460 case6 (ipv6hint)', test_https_svcb_decode_encode(t, 'rfc9460 case6 (ipv6hint)',
unhexlify( unhexlify(
"00 37" + // rdata len "00 37" + // rdata len
"00 01" + // priority "00 01" + // priority
@ -793,7 +865,7 @@ tape('https svcb', function (t) {
) )
// https ipv6 hint using embedded ipv4 syntax [2001:db8:122:344::192.0.2.33] // https ipv6 hint using embedded ipv4 syntax [2001:db8:122:344::192.0.2.33]
test_http_decode_encode(t, 'https rfc9460 case7 (ipv6hint v4 syntax)', test_https_svcb_decode_encode(t, 'rfc9460 case7 (ipv6hint v4 syntax)',
unhexlify( unhexlify(
"00 23" + // rdata len "00 23" + // rdata len
"00 01" + // priority "00 01" + // priority
@ -819,7 +891,7 @@ tape('https svcb', function (t) {
// //
// note: this may not encode to the same buffer due to internal js // note: this may not encode to the same buffer due to internal js
// ordering of the `values` object storing the params // ordering of the `values` object storing the params
test_http_decode_encode(t, 'https rfc9460 case8 (alpn,mandatory,ipv4hint)', test_https_svcb_decode_encode(t, 'rfc9460 case8 (alpn,mandatory,ipv4hint)',
unhexlify( unhexlify(
"00 30" + // rdata len "00 30" + // rdata len
"00 10" + // priority "00 10" + // priority
@ -859,27 +931,117 @@ tape('https svcb', function (t) {
true, // skip_memcmp_bufs: true, do not directly memcmp the resulting bufs, see comment above true, // skip_memcmp_bufs: true, do not directly memcmp the resulting bufs, see comment above
) )
testEncoder(t, packet.httpssvc, { t.end()
priority: 16, })
name: "foo.example.org",
values: { tape('cloudflare real world svcb/https', (t) => {
mandatory: [ const https_buf = unhexlify(
"alpn", "ef 23 81 80 00 01 00 01 00 00 00 01 09 63 6f 6d" +
"ipv4hint" "6d 75 6e 69 74 79 0a 63 6c 6f 75 64 66 6c 61 72" +
"65 03 63 6f 6d 00 00 41 00 01 09 63 6f 6d 6d 75" +
"6e 69 74 79 0a 63 6c 6f 75 64 66 6c 61 72 65 03" +
"63 6f 6d 00 00 41 00 01 00 00 00 3c 00 3d 00 01" +
"00 00 01 00 06 02 68 33 02 68 32 00 04 00 08 68" +
"12 02 43 68 12 03 43 00 06 00 20 26 06 47 00 00" +
"00 00 00 00 00 00 00 68 12 02 43 26 06 47 00 00" +
"00 00 00 00 00 00 00 68 12 03 43 00 00 29 02 00" +
"00 00 00 00 00 00"
)
const svcb_buf = unhexlify(
"ef 23 81 80 00 01 00 01 00 00 00 01 09 63 6f 6d" +
"6d 75 6e 69 74 79 0a 63 6c 6f 75 64 66 6c 61 72" +
"65 03 63 6f 6d 00 00 40 00 01 09 63 6f 6d 6d 75" +
"6e 69 74 79 0a 63 6c 6f 75 64 66 6c 61 72 65 03" +
"63 6f 6d 00 00 40 00 01 00 00 00 3c 00 3d 00 01" +
"00 00 01 00 06 02 68 33 02 68 32 00 04 00 08 68" +
"12 02 43 68 12 03 43 00 06 00 20 26 06 47 00 00" +
"00 00 00 00 00 00 00 68 12 02 43 26 06 47 00 00" +
"00 00 00 00 00 00 00 68 12 03 43 00 00 29 02 00" +
"00 00 00 00 00 00"
)
let expected = {
id: 61219,
type: "response",
flags: 384,
flag_qr: true,
opcode: "QUERY",
flag_aa: false,
flag_tc: false,
flag_rd: true,
flag_ra: true,
flag_z: false,
flag_ad: false,
flag_cd: false,
rcode: "NOERROR",
questions: [
{
name: "community.cloudflare.com",
type: "HTTPS",
class: "IN"
}
], ],
answers: [
{
name: "community.cloudflare.com",
type: "HTTPS",
ttl: 60,
class: "IN",
flush: false,
data: {
priority: 1,
name: ".",
values: {
alpn: [ alpn: [
"h2", "h3",
"h3-19" "h2"
], ],
ipv4hint: [ ipv4hint: [
"192.0.2.1" "104.18.2.67",
"104.18.3.67"
],
ipv6hint: [
"2606:4700::6812:243",
"2606:4700::6812:343"
] ]
} }
} }
) }
],
authorities: [],
additionals: [
{
name: ".",
type: "OPT",
udpPayloadSize: 512,
extendedRcode: 0,
ednsVersion: 0,
flags: 0,
flag_do: false,
options: []
}
]
}
const decoded_https = packet.decode(https_buf)
t.ok(decoded_https, "cloudflare real world https decoded")
if (debug_https) {
console.log(`cloudflare real world: decoded_https:`)
console.log(JSON.stringify(decoded_https, null, 2))
}
t.ok(compare(t, decoded_https, expected), "cloudflare real world https compare")
const decoded_svcb = packet.decode(svcb_buf)
t.ok(decoded_svcb, "cloudflare real world svcb decoded")
if (debug_https) {
console.log(`cloudflare real world: decoded_svcb:`)
console.log(JSON.stringify(decoded_svcb, null, 2))
}
expected.questions[0].type = expected.answers[0].type = "SVCB"
t.ok(compare(t, decoded_svcb, expected), "cloudflare real world svcb compare")
t.end() t.end()
}) })
// </HTTPS SVCB test cases> // </HTTPS SVCB test cases>

View File

@ -45,6 +45,7 @@ exports.toString = function (type) {
case 252: return 'AXFR' case 252: return 'AXFR'
case 251: return 'IXFR' case 251: return 'IXFR'
case 41: return 'OPT' case 41: return 'OPT'
case 64: return 'SVCB'
case 65: return 'HTTPS' case 65: return 'HTTPS'
case 255: return 'ANY' case 255: return 'ANY'
} }
@ -96,6 +97,7 @@ exports.toType = function (name) {
case 'AXFR': return 252 case 'AXFR': return 252
case 'IXFR': return 251 case 'IXFR': return 251
case 'OPT': return 41 case 'OPT': return 41
case 'SVCB': return 64
case 'HTTPS': return 65 case 'HTTPS': return 65
case 'ANY': return 255 case 'ANY': return 255
case '*': return 255 case '*': return 255