Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New "resolver.source4" and "resolver.source6" properties #1203

Merged
6 commits merged into from Mar 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
New "resolver.source6" property
Use it to pass an IPv6 source address.
The "resolver.source" property can be used to pass either an IPv4 or an
IPv6 source address. Hence if you want to define both, use
"resolver.source" to store the IPv4 and "resolver.source6" to store the
IPv6.
  • Loading branch information
Alexandre Pion committed Mar 6, 2023
commit 5f49429b3260ccfb8450587682bfcde882e9d0e2
51 changes: 39 additions & 12 deletions lib/Zonemaster/Engine/Nameserver.pm
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use Zonemaster::Engine;
use Zonemaster::Engine::Packet;
use Zonemaster::Engine::Nameserver::Cache;
use Zonemaster::Engine::Recursor;
use Zonemaster::Engine::Constants ':misc';
use Zonemaster::Engine::Constants qw( :ip :misc );
use Zonemaster::LDNS;

use Net::IP::XS;
Expand All @@ -37,7 +37,8 @@ has 'dns' => ( is => 'ro' );
has 'cache' => ( is => 'ro' );
has 'times' => ( is => 'ro' );

has 'source_address' => ( is => 'ro' );
has 'source_address' => ( is => 'ro' );
has 'source_address6' => ( is => 'ro' );

has 'fake_delegations' => ( is => 'ro' );
has 'fake_ds' => ( is => 'ro' );
Expand All @@ -62,9 +63,10 @@ sub new {
my $attrs = shift;

my %lazy_attrs;
$lazy_attrs{source_address} = delete $attrs->{source_address} if exists $attrs->{source_address};
$lazy_attrs{dns} = delete $attrs->{dns} if exists $attrs->{dns};
$lazy_attrs{cache} = delete $attrs->{cache} if exists $attrs->{cache};
$lazy_attrs{source_address} = delete $attrs->{source_address} if exists $attrs->{source_address};
$lazy_attrs{source_address6} = delete $attrs->{source_address6} if exists $attrs->{source_address6};
$lazy_attrs{dns} = delete $attrs->{dns} if exists $attrs->{dns};
$lazy_attrs{cache} = delete $attrs->{cache} if exists $attrs->{cache};

# Required arguments
confess "Attribute \(address\) is required"
Expand Down Expand Up @@ -118,6 +120,9 @@ sub new {
confess "Argument must be a string or undef: source_address"
if exists $lazy_attrs{source_address}
&& ref $lazy_attrs{source_address} ne '';
confess "Argument must be a string or undef: source_address6"
if exists $lazy_attrs{source_address6}
&& ref $lazy_attrs{source_address6} ne '';
confess "Argument must be a Zonemaster::LDNS: dns"
if exists $lazy_attrs{dns}
&& ( !blessed $lazy_attrs{dns} || !$lazy_attrs{dns}->isa( 'Zonemaster::LDNS' ) );
Expand All @@ -132,9 +137,10 @@ sub new {
$attrs->{times} //= [];

my $obj = Class::Accessor::new( $class, $attrs );
$obj->{_source_address} = $lazy_attrs{source_address} if exists $lazy_attrs{source_address};
$obj->{_dns} = $lazy_attrs{dns} if exists $lazy_attrs{dns};
$obj->{_cache} = $lazy_attrs{cache} if exists $lazy_attrs{cache};
$obj->{_source_address} = $lazy_attrs{source_address} if exists $lazy_attrs{source_address};
$obj->{_source_address6} = $lazy_attrs{source_address6} if exists $lazy_attrs{source_address6};
$obj->{_dns} = $lazy_attrs{dns} if exists $lazy_attrs{dns};
$obj->{_cache} = $lazy_attrs{cache} if exists $lazy_attrs{cache};

Zonemaster::Engine->logger->add( NS_CREATED => { name => $name, ip => $obj->address->ip } );
$object_cache{$name}{$address} = $obj;
Expand All @@ -143,9 +149,9 @@ sub new {
}

sub source_address {
my $self = shift;
my ( $self, $ip_version ) = @_;

# Lazy default value
# Lazy default values
if ( !exists $self->{_source_address} ) {
my $value = Zonemaster::Engine::Profile->effective->get( q{resolver.source} );
if ( $value eq $RESOLVER_SOURCE_OS_DEFAULT ) {
Expand All @@ -155,6 +161,19 @@ sub source_address {
$self->{_source_address} = $value;
}
}
if ( !exists $self->{_source_address6} ) {
my $value = Zonemaster::Engine::Profile->effective->get( q{resolver.source6} );
if ( $value eq '' ) {
$self->{_source_address6} = undef;
}
else {
$self->{_source_address6} = $value;
}
}

if ( $ip_version == $IP_VERSION_6 and $self->{_source_address6} ) {
return $self->{_source_address6};
}

return $self->{_source_address};
}
Expand Down Expand Up @@ -197,8 +216,10 @@ sub _build_dns {
$res->edns_size( Zonemaster::Engine::Profile->effective->get( q{resolver.defaults.edns_size} ) );
$res->timeout( Zonemaster::Engine::Profile->effective->get( q{resolver.defaults.timeout} ) );

if ( $self->source_address ) {
$res->source( $self->source_address );
my $ip_version = Net::IP::XS::ip_get_version( $self->address->ip );
my $source_address = $self->source_address( $ip_version );
if ( $source_address ) {
$res->source( $source_address );
}

return $res;
Expand Down Expand Up @@ -752,6 +773,12 @@ A reference to a L<Zonemaster::Engine::Nameserver::Cache> object holding the cac
=item source_address

The source address all resolver objects should use when sending queries.
Depends on the IP version used to send the queries.

=item source_address6

The IPv6 source address all resolver objects should use when sending queries
over IPv6.

=item times

Expand Down
17 changes: 17 additions & 0 deletions lib/Zonemaster/Engine/Profile.pm
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ my %profile_properties_details = (
}
}
},
q{resolver.source6} => {
type => q{Str},
default => "",
test => sub {
if ( $_[0] ne '' and not Net::IP::XS::ip_is_ipv6( $_[0] ) ) {
die "Property resolver.source6 must be an IPv6 address or the empty string";
}
Net::IP::XS->new( $_[0] );
}
},
q{net.ipv4} => {
type => q{Bool}
},
Expand Down Expand Up @@ -627,6 +637,13 @@ The source address all resolver objects should use when sending queries.
If C<"os_default">, the OS default address is used.
Default C<"os_default">.

=head2 resolver.source6

A string that is an IPv6 address or the empty string or undefined.
The source address all resolver objects should use when sending queries over IPv6.
If the empty string of undefinded, use the OS default IPv6 address if available.
Default "" (empty string).

=head2 net.ipv4

A boolean. If true, resolver objects are allowed to send queries over
Expand Down
3 changes: 3 additions & 0 deletions share/profile_additional_properties.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"resolver" : {
"source6": "2001:db8::42"
},
"logfilter" : {
"BASIC" : {
"IPV6_ENABLED" : [
Expand Down
45 changes: 42 additions & 3 deletions t/profiles.t
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Readonly my $EXAMPLE_PROFILE_1 => q(
"retry": 123,
"retrans": 234
},
"source": "192.0.2.53"
"source": "192.0.2.53",
"source6": "2001:db8::42"
},
"net": {
"ipv4": true,
Expand Down Expand Up @@ -73,7 +74,8 @@ Readonly my $EXAMPLE_PROFILE_2 => q(
"retry": 99,
"retrans": 88
},
"source": "198.51.100.53"
"source": "198.51.100.53",
"source6": "2001:db8::cafe"
},
"net": {
"ipv4": false,
Expand Down Expand Up @@ -135,6 +137,7 @@ subtest 'new() returns a profile with all properties unset' => sub {
is $profile->get( 'resolver.defaults.igntc' ), undef, 'resolver.defaults.igntc is unset';
is $profile->get( 'resolver.defaults.fallback' ), undef, 'resolver.defaults.fallback is unset';
is $profile->get( 'resolver.source' ), undef, 'resolver.source is unset';
is $profile->get( 'resolver.source6' ), undef, 'resolver.source6 is unset';
is $profile->get( 'net.ipv4' ), undef, 'net.ipv4 is unset';
is $profile->get( 'net.ipv6' ), undef, 'net.ipv6 is unset';
is $profile->get( 'no_network' ), undef, 'no_network is unset';
Expand Down Expand Up @@ -168,6 +171,7 @@ subtest 'default() returns a profile with all properties set' => sub {
ok defined( $profile->get( 'resolver.defaults.retry' ) ), 'resolver.defaults.retry is set';
ok defined( $profile->get( 'resolver.defaults.retrans' ) ), 'resolver.defaults.retrans is set';
ok defined( $profile->get( 'resolver.source' ) ), 'resolver.source is set';
ok defined( $profile->get( 'resolver.source6' ) ), 'resolver.source6 is set';
ok defined( $profile->get( 'logfilter' ) ), 'logfilter is set';
ok defined( $profile->get( 'test_levels' ) ), 'test_levels is set';
ok defined( $profile->get( 'test_cases' ) ), 'test_cases is set';
Expand Down Expand Up @@ -196,6 +200,7 @@ subtest 'from_json("{}") returns a profile with all properties unset' => sub {
is $profile->get( 'resolver.defaults.retry' ), undef, 'resolver.defaults.retry is unset';
is $profile->get( 'resolver.defaults.retrans' ), undef, 'resolver.defaults.retrans is unset';
is $profile->get( 'resolver.source' ), undef, 'resolver.source is unset';
is $profile->get( 'resolver.source6' ), undef, 'resolver.source6 is unset';
is $profile->get( 'asnroots' ), undef, 'asnroots is unset';
is $profile->get( 'logfilter' ), undef, 'logfilter is unset';
is $profile->get( 'test_levels' ), undef, 'test_levels is unset';
Expand All @@ -216,6 +221,7 @@ subtest 'from_json() parses values from a string' => sub {
is $profile->get( 'resolver.defaults.retry' ), 123, 'resolver.defaults.retry was parsed from JSON';
is $profile->get( 'resolver.defaults.retrans' ), 234, 'resolver.defaults.retrans was parsed from JSON';
is $profile->get( 'resolver.source' ), '192.0.2.53', 'resolver.source was parsed from JSON';
is $profile->get( 'resolver.source6' ), '2001:db8::42', 'resolver.source6 was parsed from JSON';
eq_or_diff $profile->get( 'asnroots' ), ['example.com'], 'asnroots was parsed from JSON';
eq_or_diff $profile->get( 'logfilter' ), { Zone => { TAG => [ { when => { bananas => 0 }, set => 'WARNING' } ] } },
'logfilter was parsed from JSON';
Expand Down Expand Up @@ -266,6 +272,10 @@ subtest 'from_json() dies on illegal values' => sub {
"checks type of resolver.defaults.retrans";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"source":"example.com"}}' ); }
"checks type of resolver.source";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"source6":"example.com"}}' ); }
"checks type of resolver.source6";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"source6":"192.0.2.53"}}' ); }
"checks type of resolver.source6 (only IPv6)";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"asnroots":["[email protected]"]}' ); }
"checks type of asnroots";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"logfilter":[]}' ); } "checks type of logfilter";
Expand Down Expand Up @@ -382,6 +392,7 @@ subtest 'set() inserts values for unset properties' => sub {
$profile->set( 'resolver.defaults.retry', 123 );
$profile->set( 'resolver.defaults.retrans', 234 );
$profile->set( 'resolver.source', '192.0.2.53' );
$profile->set( 'resolver.source6', '2001:db8::42' );
$profile->set( 'asnroots', ['example.com'] );
$profile->set( 'logfilter', { Zone => { TAG => [ { when => { bananas => 0 }, set => 'WARNING' } ] } } );
$profile->set( 'test_levels', { Zone => { TAG => 'INFO' } } );
Expand All @@ -398,6 +409,7 @@ subtest 'set() inserts values for unset properties' => sub {
is $profile->get( 'resolver.defaults.retry' ), 123, 'resolver.defaults.retry can be given a value when unset';
is $profile->get( 'resolver.defaults.retrans' ), 234, 'resolver.defaults.retrans can be given a value when unset';
is $profile->get( 'resolver.source' ), '192.0.2.53', 'resolver.source can be given a value when unset';
is $profile->get( 'resolver.source6' ), '2001:db8::42', 'resolver.source6 can be given a value when unset';
eq_or_diff $profile->get( 'asnroots' ), ['example.com'], 'anroots can be given a value when unset';
eq_or_diff $profile->get( 'logfilter' ), { Zone => { TAG => [ { when => { bananas => 0 }, set => 'WARNING' } ] } },
'logfilter can be given a value when unset';
Expand All @@ -420,6 +432,7 @@ subtest 'set() updates values for set properties' => sub {
$profile->set( 'resolver.defaults.retry', 99 );
$profile->set( 'resolver.defaults.retrans', 88 );
$profile->set( 'resolver.source', '198.51.100.53' );
$profile->set( 'resolver.source6', '2001:db8::cafe' );
$profile->set( 'asnroots', [ 'asn1.example.com', 'asn2.example.com' ] );
$profile->set( 'logfilter', { Nameserver => { OTHER_TAG => [ { when => { apples => 1 }, set => 'INFO' } ] } } );
$profile->set( 'test_levels', { Nameserver => { OTHER_TAG => 'ERROR' } } );
Expand All @@ -435,6 +448,7 @@ subtest 'set() updates values for set properties' => sub {
is $profile->get( 'resolver.defaults.retry' ), 99, 'resolver.defaults.retry was updated';
is $profile->get( 'resolver.defaults.retrans' ), 88, 'resolver.defaults.retrans was updated';
is $profile->get( 'resolver.source' ), '198.51.100.53', 'resolver.source was updated';
is $profile->get( 'resolver.source6' ), '2001:db8::cafe', 'resolver.source6 was updated';
eq_or_diff $profile->get( 'asnroots' ), [ 'asn1.example.com', 'asn2.example.com' ], 'asnroots was updated';
eq_or_diff $profile->get( 'logfilter' ),
{ Nameserver => { OTHER_TAG => [ { when => { apples => 1 }, set => 'INFO' } ] } }, 'logfilter was updated';
Expand All @@ -456,6 +470,7 @@ subtest 'set() dies on attempts to unset properties' => sub {
throws_ok { $profile->set( 'resolver.defaults.retry', undef ); } qr/^.* can not be undef/, 'dies on attempt to unset resolver.defaults.retry';
throws_ok { $profile->set( 'resolver.defaults.retrans', undef ); } qr/^.* can not be undef/, 'dies on attempt to unset resolver.defaults.retans';
throws_ok { $profile->set( 'resolver.source', undef ); } qr/^.* can not be undef/, 'dies on attempt to unset resolver.source';
throws_ok { $profile->set( 'resolver.source6', undef ); } qr/^.* can not be undef/, 'dies on attempt to unset resolver.source6';
throws_ok { $profile->set( 'asnroots', undef ); } qr/^.* can not be undef/, 'dies on attempt to unset asnroots';
throws_ok { $profile->set( 'logfilter', undef ); } qr/^.* can not be undef/, 'dies on attempt to unset logfilter';
throws_ok { $profile->set( 'test_levels', undef ); } qr/^.* can not be undef/, 'dies on attempt to unset test_levels';
Expand Down Expand Up @@ -489,6 +504,7 @@ subtest 'set() dies on illegal value' => sub {
dies_ok { $profile->set( 'resolver.defaults.retrans', 256 ); } 'checks upper bound of resolver.defaults.retrans';
dies_ok { $profile->set( 'resolver.defaults.retrans', 1.5 ); } 'checks type of resolver.defaults.retrans';
dies_ok { $profile->set( 'resolver.source', ['192.0.2.53'] ); } 'checks type of resolver.source';
dies_ok { $profile->set( 'resolver.source6', ['2001:db8::42'] ); } 'checks type of resolver.source6';
dies_ok { $profile->set( 'asnroots', ['[email protected]'] ); } 'checks type of asnroots';
dies_ok { $profile->set( 'logfilter', [] ); } 'checks type of logfilter';
dies_ok { $profile->set( 'test_levels', [] ); } 'checks type of test_levels';
Expand All @@ -499,8 +515,10 @@ subtest 'set() accepts sentinel values' => sub {
my $profile = Zonemaster::Engine::Profile->new;

$profile->set( 'resolver.source', $RESOLVER_SOURCE_OS_DEFAULT );

is $profile->get( 'resolver.source' ), $RESOLVER_SOURCE_OS_DEFAULT, 'resolver.source was updated';

$profile->set( 'resolver.source6', '' );
is $profile->get( 'resolver.source6' ), '', 'resolver.source6 was updated';
};

subtest 'set() uses standard truthiness rules for boolean properties' => sub {
Expand Down Expand Up @@ -555,6 +573,7 @@ subtest 'merge() with a profile with all properties unset' => sub {
is $profile1->get( 'resolver.defaults.retry' ), 123, 'keeps value of resolver.defaults.retry';
is $profile1->get( 'resolver.defaults.retrans' ), 234, 'keeps value of resolver.defaults.retrans';
is $profile1->get( 'resolver.source' ), '192.0.2.53', 'keeps value of resolver.source';
is $profile1->get( 'resolver.source6' ), '2001:db8::42', 'keeps value of resolver.source6';
eq_or_diff $profile1->get( 'asnroots' ), ['example.com'], 'keeps value of asnroots';
eq_or_diff $profile1->get( 'logfilter' ), { Zone => { TAG => [ { when => { bananas => 0 }, set => 'WARNING' } ] } },
'keeps value of logfilter';
Expand All @@ -579,6 +598,7 @@ subtest 'merge() with a profile with all properties set' => sub {
is $profile1->get( 'resolver.defaults.retry' ), 99, 'updates resolver.defaults.retry';
is $profile1->get( 'resolver.defaults.retrans' ), 88, 'updates resolver.defaults.retrans';
is $profile1->get( 'resolver.source' ), '198.51.100.53', 'updates resolver.source';
is $profile1->get( 'resolver.source6' ), '2001:db8::cafe', 'updates resolver.source6';
eq_or_diff $profile1->get( 'asnroots' ), [ 'asn1.example.com', 'asn2.example.com' ], 'updates asnroots';
eq_or_diff $profile1->get( 'logfilter' ),
{ Nameserver => { OTHER_TAG => [ { when => { apples => 1 }, set => 'INFO' } ] } }, 'updates logfilter';
Expand All @@ -600,6 +620,7 @@ subtest 'merge() does not update the other profile' => sub {
is $profile2->get( 'resolver.defaults.igntc' ), undef, 'resolver.defaults.igntc was untouched in other';
is $profile2->get( 'resolver.defaults.fallback' ), undef, 'resolver.defaults.fallback was untouched in other';
is $profile2->get( 'resolver.source' ), undef, 'resolver.source was untouched in other';
is $profile2->get( 'resolver.source6' ), undef, 'resolver.source6 was untouched in other';
is $profile2->get( 'net.ipv4' ), undef, 'net.ipv4 was untouched in other';
is $profile2->get( 'net.ipv6' ), undef, 'net.ipv6 was untouched in other';
is $profile2->get( 'no_network' ), undef, 'no_network was untouched in other';
Expand Down Expand Up @@ -718,6 +739,24 @@ subtest 'to_json() serializes each property' => sub {
eq_or_diff decode_json( $json ), decode_json( qq({"resolver":{"source":"$RESOLVER_SOURCE_OS_DEFAULT"}}) );
};

subtest 'resolver.source6' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.source6', '2001:db8::42' );

my $json = $profile->to_json;

eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"source6":"2001:db8::42"}}' );
};

subtest 'resolver.source6 sentinel value' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.source6', '' );

my $json = $profile->to_json;

eq_or_diff decode_json( $json ), decode_json( qq({"resolver":{"source6":""}}) );
};

subtest 'asnroots' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'asnroots', ['example.com'] );
Expand Down