our
$VERSION
= (
qw$Id: SVCB.pm 2003 2025-01-21 12:06:06Z willem $
)[2];
my
%keybyname
= (
mandatory
=>
'key0'
,
alpn
=>
'key1'
,
'no-default-alpn'
=>
'key2'
,
port
=>
'key3'
,
ipv4hint
=>
'key4'
,
ech
=>
'key5'
,
ipv6hint
=>
'key6'
,
dohpath
=>
'key7'
,
ohttp
=>
'key8'
,
'tls-supported-groups'
=>
'key9'
,
);
my
%boolean
= (
'no-default-alpn'
=>
'key2'
,
ohttp
=>
'key8'
,
);
sub
_decode_rdata {
my
(
$self
,
$data
,
$offset
) =
@_
;
my
$limit
=
$self
->{rdlength};
my
$rdata
=
$self
->{rdata} =
substr
$$data
,
$offset
,
$limit
;
$self
->{SvcPriority} =
unpack
'n'
,
$rdata
;
(
$self
->{TargetName},
$offset
) = Net::DNS::DomainName->decode( \
$rdata
, 2 );
my
$params
=
$self
->{SvcParams} = [];
while
( (
my
$start
=
$offset
+ 4 ) <=
$limit
) {
my
(
$key
,
$size
) =
unpack
(
"\@$offset n2"
,
$rdata
);
my
$next
=
$start
+
$size
;
last
if
$next
>
$limit
;
push
@$params
, (
$key
,
substr
$rdata
,
$start
,
$size
);
$offset
=
$next
;
}
die
$self
->type .
': corrupt RDATA'
unless
$offset
==
$limit
;
return
;
}
sub
_encode_rdata {
my
$self
=
shift
;
return
$self
->{rdata}
if
$self
->{rdata};
my
@packed
=
pack
'n a*'
,
$self
->{SvcPriority},
$self
->{TargetName}->encode;
my
$params
=
$self
->{SvcParams} || [];
my
@params
=
@$params
;
while
(
@params
) {
my
$key
=
shift
@params
;
my
$val
=
shift
@params
;
push
@packed
,
pack
(
'n2a*'
,
$key
,
length
(
$val
),
$val
);
}
return
join
''
,
@packed
;
}
sub
_format_rdata {
my
$self
=
shift
;
my
@rdata
=
unpack
'H4'
,
pack
'n'
,
$self
->{SvcPriority};
my
$encode
=
$self
->{TargetName}->encode();
my
$length
= 2 +
length
$encode
;
my
@target
=
grep
{
length
}
split
/(\S{32})/,
unpack
'H*'
,
$encode
;
my
$target
=
substr
$self
->{TargetName}->string, 0, 40;
push
@rdata
,
join
''
,
shift
(
@target
),
"\t; $target\n"
;
push
@rdata
,
@target
;
my
$params
=
$self
->{SvcParams} || [];
my
@params
=
@$params
;
while
(
@params
) {
my
$key
=
shift
@params
;
my
$val
=
shift
@params
;
push
@rdata
,
"\n"
,
unpack
'H4H4'
,
pack
(
'n2'
,
$key
,
length
$val
);
my
@hex
=
grep
{
length
}
split
/(\S{32})/,
unpack
'H*'
,
$val
;
push
@rdata
,
shift
@hex
if
@hex
;
push
@rdata
,
"\t; key$key\n"
unless
$key
< 16;
push
@rdata
,
@hex
;
$length
+= 4 +
length
$val
;
}
if
(
$self
->{rdata} ) {
if
(
my
$corrupt
=
substr
$self
->{rdata},
$length
) {
my
(
$hex
,
@hex
) =
grep
{
length
}
split
/(\S{32})/,
unpack
'H*'
,
$corrupt
;
push
@rdata
,
"\n$hex\t; corrupt RDATA\n"
,
@hex
;
$length
+=
length
$corrupt
;
}
}
return
(
"\\# $length"
,
@rdata
);
}
sub
_parse_rdata {
my
(
$self
,
@argument
) =
@_
;
$self
->svcpriority(
shift
@argument
);
$self
->targetname(
shift
@argument
);
local
$SIG
{__WARN__} =
sub
{
die
@_
};
while
(
my
$svcparam
=
shift
@argument
) {
for
(
$svcparam
) {
my
@value
;
if
(/^key\d+=(.*)$/i) {
local
$_
=
length
($1) ? $1 :
shift
@argument
;
s/^
"([^"
]*)"$/$1/;
push
@value
,
$_
;
}
elsif
(/^[^=]+=(.*)$/) {
local
$_
=
length
($1) ? $1 :
shift
@argument
;
die
<<"Amen" if /\\092[,\\]/;
SVCB: Please use standard RFC1035 escapes
RFC9460 double-escape nonsense not implemented
Amen
s/^
"([^"
]*)"$/$1/;
s/\\,/\\044/g;
push
@value
,
split
/,/;
}
else
{
push
@value
,
''
unless
$keybyname
{
$_
};
}
m/^([^=]+)/;
my
$key
= $1;
push
@value
, 1
if
$boolean
{
$key
};
$key
=~ s/[-]/_/g;
$self
->
$key
(
@value
);
}
}
return
;
}
sub
_post_parse {
my
$self
=
shift
;
my
$paramref
=
$self
->{SvcParams} || [];
my
%svcparam
=
scalar
(
@$paramref
) ?
@$paramref
:
return
;
$self
->key0(
undef
);
if
(
defined
$svcparam
{0} ) {
my
%unique
;
foreach
(
grep
{ !
$unique
{
$_
}++ }
unpack
'n*'
,
$svcparam
{0} ) {
die
(
$self
->type .
qq[: unexpected "key0" in mandatory list]
)
if
$unique
{0};
die
(
$self
->type .
qq[: duplicate "key$_" in mandatory list]
)
if
--
$unique
{
$_
};
die
(
$self
->type .
qq[: mandatory "key$_" not present]
)
unless
defined
$svcparam
{
$_
};
}
$self
->mandatory(
keys
%unique
);
}
die
(
$self
->type .
qq[: expected alpn="..." not present]
)
if
defined
(
$svcparam
{2} ) && !
$svcparam
{1};
return
;
}
sub
_defaults {
my
$self
=
shift
;
$self
->_parse_rdata(
qw(0 .)
);
return
;
}
sub
svcpriority {
my
(
$self
,
@value
) =
@_
;
for
(
@value
) {
$self
->{SvcPriority} = 0 +
$_
}
return
$self
->{SvcPriority} || 0;
}
sub
targetname {
my
(
$self
,
@value
) =
@_
;
for
(
@value
) {
$self
->{TargetName} = Net::DNS::DomainName->new(
$_
) }
my
$target
=
$self
->{TargetName} ?
$self
->{TargetName}->name :
return
;
return
$target
unless
$self
->{SvcPriority};
return
(
$target
eq
'.'
) ?
$self
->owner :
$target
;
}
sub
mandatory {
my
(
$self
,
@value
) =
@_
;
my
@list
=
map
{
$keybyname
{
lc
$_
} ||
$_
}
map
{
split
/,/ }
@value
;
my
@keys
=
map
{ /(\d+)$/ ? $1 :
die
(
$self
->type .
qq[: unexpected "$_"]
) }
@list
;
return
$self
->key0( _integer16(
sort
{
$a
<=>
$b
}
@keys
) );
}
sub
alpn {
my
(
$self
,
@value
) =
@_
;
return
$self
->key1( _string(
@value
) );
}
sub
no_default_alpn {
my
(
$self
,
@value
) =
@_
;
return
$self
->key2( _boolean(
@value
) );
}
sub
port {
my
(
$self
,
@value
) =
@_
;
return
$self
->key3(
map
{ _integer16(
$_
) }
@value
);
}
sub
ipv4hint {
my
(
$self
,
@value
) =
@_
;
return
$self
->key4( _ipv4(
@value
) );
}
sub
ech {
my
(
$self
,
@value
) =
@_
;
return
$self
->key5(
map
{ _base64(
$_
) }
@value
);
}
sub
ipv6hint {
my
(
$self
,
@value
) =
@_
;
return
$self
->key6( _ipv6(
@value
) );
}
sub
dohpath {
my
(
$self
,
@value
) =
@_
;
return
$self
->key7(
@value
);
}
sub
ohttp {
my
(
$self
,
@value
) =
@_
;
return
$self
->key8( _boolean(
@value
) );
}
sub
tls_supported_groups {
my
(
$self
,
@value
) =
@_
;
return
$self
->key9( _integer16(
@value
) );
}
sub
_presentation {
my
@arg
=
@_
;
my
$raw
=
scalar
(
@arg
) ?
join
(
''
,
@arg
) :
return
();
return
Net::DNS::Text->decode( \
$raw
, 0,
length
(
$raw
) )->string;
}
sub
_boolean {
my
@arg
=
@_
;
return
@arg
unless
scalar
@arg
;
my
$arg
=
shift
@arg
;
return
$arg
unless
defined
$arg
;
return
(
$arg
?
''
:
undef
,
@arg
);
}
sub
_string {
my
@arg
=
@_
;
return
_presentation(
map
{ Net::DNS::Text->new(
$_
)->encode() }
@arg
);
}
sub
_base64 {
my
@arg
=
@_
;
return
_presentation(
map
{ MIME::Base64::decode(
$_
) }
@arg
);
}
sub
_integer16 {
my
@arg
=
@_
;
return
_presentation(
map
{
pack
(
'n'
,
$_
) }
@arg
);
}
sub
_ipv4 {
my
@arg
=
@_
;
return
_presentation(
map
{ Net::DNS::RR::A::address( {},
$_
) }
@arg
);
}
sub
_ipv6 {
my
@arg
=
@_
;
return
_presentation(
map
{ Net::DNS::RR::AAAA::address( {},
$_
) }
@arg
);
}
sub
AUTOLOAD {
my
(
$self
,
@argument
) =
@_
;
our
$AUTOLOAD
;
my
(
$method
) =
reverse
split
/::/,
$AUTOLOAD
;
my
$super
=
"SUPER::$method"
;
return
$self
->
$super
(
@argument
)
unless
$method
=~ /^key[0]*(\d+)$/i;
my
$key
= $1;
my
$paramsref
=
$self
->{SvcParams} || [];
my
%svcparams
=
@$paramsref
;
if
(
scalar
@argument
) {
my
$arg
=
shift
@argument
;
delete
$svcparams
{
$key
}
unless
defined
$arg
;
die
(
$self
->type .
qq[: duplicate SvcParam "key$key"]
)
if
defined
$svcparams
{
$key
};
die
(
$self
->type .
qq[: invalid SvcParam "key$key"]
)
if
$key
> 65534;
die
(
$self
->type .
qq[: unexpected "key$key" value]
)
if
scalar
@argument
;
delete
$self
->{rdata};
$svcparams
{
$key
} = Net::DNS::Text->new(
"$arg"
)->raw
if
defined
$arg
;
$self
->{SvcParams} = [
map
{ (
$_
,
$svcparams
{
$_
} ) }
sort
{
$a
<=>
$b
}
keys
%svcparams
];
}
else
{
die
(
$self
->type .
qq[: no value specified for "key$key"]
)
unless
defined
wantarray
;
}
return
$svcparams
{
$key
};
}
1;