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

222
test.js
View File

@ -654,13 +654,40 @@ const unhexlify = (hex) => {
// <HTTPS SVCB test cases>
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)
if (debug_https) {
console.log(`${testname}: decode:`)
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)
if (!skip_encode) {
if (debug_https) {
@ -668,7 +695,7 @@ const test_http_decode_encode = (t, testname, packetbuf, expected, skip_encode=f
hexdump(encoded)
}
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
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(JSON.stringify(decoded, null, 2))
}
t.ok(compare(t, recoded, expected), testname + ' recode')
t.ok(compare(t, recoded, expected), 'https ' + testname + ' recode')
}
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) {
// 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
test_http_decode_encode(t, 'https rfc9460 case1 (AliasMode)',
test_https_svcb_decode_encode(t, 'rfc9460 case1 (AliasMode)',
unhexlify(
"00 13" + // rdata len
"00 00" + // priority
@ -698,7 +770,7 @@ tape('https svcb', function (t) {
)
// 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(
"00 03" + // rdata len
"00 01" + // priority
@ -711,7 +783,7 @@ tape('https svcb', function (t) {
)
// https port
test_http_decode_encode(t, 'https rfc9460 case3 (port)',
test_https_svcb_decode_encode(t, 'rfc9460 case3 (port)',
unhexlify(
"00 19" + // rdata len
"00 10" + // priority
@ -730,7 +802,7 @@ tape('https svcb', function (t) {
)
// 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(
"00 1c" + // rdata len
"00 01" + // priority
@ -750,7 +822,7 @@ tape('https svcb', function (t) {
)
// 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(
"00 20" + // rdata len
"00 01" + // priority
@ -770,7 +842,7 @@ tape('https svcb', function (t) {
)
// https two quoted ipv6 hints
test_http_decode_encode(t, 'https rfc9460 case6 (ipv6hint)',
test_https_svcb_decode_encode(t, 'rfc9460 case6 (ipv6hint)',
unhexlify(
"00 37" + // rdata len
"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]
test_http_decode_encode(t, 'https rfc9460 case7 (ipv6hint v4 syntax)',
test_https_svcb_decode_encode(t, 'rfc9460 case7 (ipv6hint v4 syntax)',
unhexlify(
"00 23" + // rdata len
"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
// 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(
"00 30" + // rdata len
"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
)
testEncoder(t, packet.httpssvc, {
priority: 16,
name: "foo.example.org",
values: {
mandatory: [
"alpn",
"ipv4hint"
],
alpn: [
"h2",
"h3-19"
],
ipv4hint: [
"192.0.2.1"
]
}
}
t.end()
})
tape('cloudflare real world svcb/https', (t) => {
const https_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 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: [
"h3",
"h2"
],
ipv4hint: [
"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()
})
// </HTTPS SVCB test cases>

View File

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