diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 8d462615ac..33c559cddb 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -2098,6 +2098,17 @@ NFTSet=prefix:netdev:filter:eth_ipv4_prefix + + RequestAddress= + + Takes an IPv4 address. When specified, the Requested IP Address option (option code 50) is + added with it to the initial DHCPDISCOVER message sent by the DHCP client. Defaults to unset, and + an already assigned dynamic address to the interface is automatically picked. + + + + + SendHostname= @@ -2232,6 +2243,7 @@ NFTSet=prefix:netdev:filter:eth_ipv4_prefix are implied and these settings in the .network file are silently ignored. Also, Hostname=, MUDURL=, + RequestAddress, RequestOptions=, SendOption=, SendVendorOption=, diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c index f14e62588f..3e8038ede6 100644 --- a/src/network/networkd-dhcp4.c +++ b/src/network/networkd-dhcp4.c @@ -1383,15 +1383,15 @@ static int dhcp4_set_client_identifier(Link *link) { return 0; } -static int dhcp4_set_request_address(Link *link) { +static int dhcp4_find_dynamic_address(Link *link, struct in_addr *ret) { Address *a; assert(link); assert(link->network); - assert(link->dhcp_client); + assert(ret); if (!FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) - return 0; + return false; SET_FOREACH(a, link->addresses) { if (a->source != NETWORK_CONFIG_SOURCE_FOREIGN) @@ -1403,11 +1403,29 @@ static int dhcp4_set_request_address(Link *link) { } if (!a) + return false; + + *ret = a->in_addr.in; + return true; +} + +static int dhcp4_set_request_address(Link *link) { + struct in_addr a; + + assert(link); + assert(link->network); + assert(link->dhcp_client); + + a = link->network->dhcp_request_address; + + if (in4_addr_is_null(&a)) + (void) dhcp4_find_dynamic_address(link, &a); + + if (in4_addr_is_null(&a)) return 0; - log_link_debug(link, "DHCPv4 CLIENT: requesting " IPV4_ADDRESS_FMT_STR, IPV4_ADDRESS_FMT_VAL(a->in_addr.in)); - - return sd_dhcp_client_set_request_address(link->dhcp_client, &a->in_addr.in); + log_link_debug(link, "DHCPv4 CLIENT: requesting %s.", IN4_ADDR_TO_STRING(&a)); + return sd_dhcp_client_set_request_address(link->dhcp_client, &a); } static bool link_needs_dhcp_broadcast(Link *link) { @@ -1487,6 +1505,10 @@ static int dhcp4_configure(Link *link) { } if (!link->network->dhcp_anonymize) { + r = dhcp4_set_request_address(link); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set initial DHCPv4 address: %m"); + if (link->network->dhcp_use_mtu) { r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_MTU_INTERFACE); if (r < 0) @@ -1616,10 +1638,6 @@ static int dhcp4_configure(Link *link) { return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed set to lease lifetime: %m"); } - r = dhcp4_set_request_address(link); - if (r < 0) - return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set initial DHCPv4 address: %m"); - return dhcp4_set_client_identifier(link); } diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index ab456efb9e..42e989e116 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -214,6 +214,7 @@ NextHop.Family, config_parse_nexthop_family, NextHop.OnLink, config_parse_nexthop_onlink, 0, 0 NextHop.Blackhole, config_parse_nexthop_blackhole, 0, 0 NextHop.Group, config_parse_nexthop_group, 0, 0 +DHCPv4.RequestAddress, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_request_address) DHCPv4.ClientIdentifier, config_parse_dhcp_client_identifier, 0, offsetof(Network, dhcp_client_identifier) DHCPv4.UseDNS, config_parse_dhcp_use_dns, AF_INET, 0 DHCPv4.RoutesToDNS, config_parse_bool, 0, offsetof(Network, dhcp_routes_to_dns) diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index 34bc179d75..cc2cf36f5c 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -114,6 +114,7 @@ struct Network { /* DHCP Client Support */ AddressFamily dhcp; + struct in_addr dhcp_request_address; DHCPClientIdentifier dhcp_client_identifier; DUID dhcp_duid; uint32_t dhcp_iaid; diff --git a/test/test-network/conf/25-dhcp-client-ipv4-only.network b/test/test-network/conf/25-dhcp-client-ipv4-only.network index 653d7aa661..5e83bd24dc 100644 --- a/test/test-network/conf/25-dhcp-client-ipv4-only.network +++ b/test/test-network/conf/25-dhcp-client-ipv4-only.network @@ -8,6 +8,7 @@ IPv6AcceptRA=no Address=192.168.5.250/24 [DHCPv4] +RequestAddress=192.168.5.110 UseDomains=yes UseMTU=yes UseRoutes=yes diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 53deb63325..c00c135130 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -5204,6 +5204,16 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): self.assertRegex(output, r'inet 192.168.5.11[0-9]/24 metric 24 brd 192.168.5.255 scope global secondary dynamic noprefixroute test-label') self.assertNotIn('2600::', output) + output = check_output('ip -4 --json address show dev veth99') + for i in json.loads(output)[0]['addr_info']: + if i['label'] == 'test-label': + address1 = i['local'] + break + else: + self.assertFalse(True) + + self.assertRegex(address1, r'^192.168.5.11[0-9]$') + print('## ip route show table main dev veth99') output = check_output('ip route show table main dev veth99') print(output) @@ -5218,11 +5228,11 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): print('## ip route show table 211 dev veth99') output = check_output('ip route show table 211 dev veth99') print(output) - self.assertRegex(output, 'default via 192.168.5.1 proto dhcp src 192.168.5.11[0-9] metric 24') - self.assertRegex(output, '192.168.5.0/24 proto dhcp scope link src 192.168.5.11[0-9] metric 24') - self.assertRegex(output, '192.168.5.1 proto dhcp scope link src 192.168.5.11[0-9] metric 24') - self.assertRegex(output, '192.168.5.6 proto dhcp scope link src 192.168.5.11[0-9] metric 24') - self.assertRegex(output, '192.168.5.7 proto dhcp scope link src 192.168.5.11[0-9] metric 24') + self.assertRegex(output, f'default via 192.168.5.1 proto dhcp src {address1} metric 24') + self.assertRegex(output, f'192.168.5.0/24 proto dhcp scope link src {address1} metric 24') + self.assertRegex(output, f'192.168.5.1 proto dhcp scope link src {address1} metric 24') + self.assertRegex(output, f'192.168.5.6 proto dhcp scope link src {address1} metric 24') + self.assertRegex(output, f'192.168.5.7 proto dhcp scope link src {address1} metric 24') self.assertIn('10.0.0.0/8 via 192.168.5.1 proto dhcp', output) print('## link state file') @@ -5261,7 +5271,7 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): output = read_dnsmasq_log_file() print(output) self.assertIn('vendor class: FooBarVendorTest', output) - self.assertIn('DHCPDISCOVER(veth-peer) 12:34:56:78:9a:bc', output) + self.assertIn('DHCPDISCOVER(veth-peer) 192.168.5.110 12:34:56:78:9a:bc', output) self.assertIn('client provides name: test-hostname', output) self.assertIn('26:mtu', output) @@ -5275,7 +5285,7 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): # Sleep for 120 sec as the dnsmasq minimum lease time can only be set to 120 print('Wait for the DHCP lease to be expired') - self.wait_address_dropped('veth99', r'inet 192.168.5.11[0-9]*/24', ipv='-4', timeout_sec=120) + self.wait_address_dropped('veth99', f'inet {address1}/24', ipv='-4', timeout_sec=120) self.wait_address('veth99', r'inet 192.168.5.12[0-9]*/24', ipv='-4') self.wait_online(['veth99:routable', 'veth-peer:routable']) @@ -5285,10 +5295,20 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): print(output) self.assertIn('mtu 1492', output) self.assertIn('inet 192.168.5.250/24 brd 192.168.5.255 scope global veth99', output) - self.assertNotIn('192.168.5.11', output) + self.assertNotIn(f'{address1}', output) self.assertRegex(output, r'inet 192.168.5.12[0-9]/24 metric 24 brd 192.168.5.255 scope global secondary dynamic noprefixroute test-label') self.assertNotIn('2600::', output) + output = check_output('ip -4 --json address show dev veth99') + for i in json.loads(output)[0]['addr_info']: + if i['label'] == 'test-label': + address2 = i['local'] + break + else: + self.assertFalse(True) + + self.assertRegex(address2, r'^192.168.5.12[0-9]$') + print('## ip route show table main dev veth99') output = check_output('ip route show table main dev veth99') print(output) @@ -5303,12 +5323,12 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): print('## ip route show table 211 dev veth99') output = check_output('ip route show table 211 dev veth99') print(output) - self.assertRegex(output, 'default via 192.168.5.1 proto dhcp src 192.168.5.12[0-9] metric 24') - self.assertRegex(output, '192.168.5.0/24 proto dhcp scope link src 192.168.5.12[0-9] metric 24') - self.assertRegex(output, '192.168.5.1 proto dhcp scope link src 192.168.5.12[0-9] metric 24') + self.assertRegex(output, f'default via 192.168.5.1 proto dhcp src {address2} metric 24') + self.assertRegex(output, f'192.168.5.0/24 proto dhcp scope link src {address2} metric 24') + self.assertRegex(output, f'192.168.5.1 proto dhcp scope link src {address2} metric 24') self.assertNotIn('192.168.5.6', output) - self.assertRegex(output, '192.168.5.7 proto dhcp scope link src 192.168.5.12[0-9] metric 24') - self.assertRegex(output, '192.168.5.8 proto dhcp scope link src 192.168.5.12[0-9] metric 24') + self.assertRegex(output, f'192.168.5.7 proto dhcp scope link src {address2} metric 24') + self.assertRegex(output, f'192.168.5.8 proto dhcp scope link src {address2} metric 24') self.assertIn('10.0.0.0/8 via 192.168.5.1 proto dhcp', output) print('## link state file') @@ -5347,7 +5367,7 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): output = read_dnsmasq_log_file() print(output) self.assertIn('vendor class: FooBarVendorTest', output) - self.assertIn('DHCPDISCOVER(veth-peer) 192.168.5.11', output) + self.assertIn(f'DHCPDISCOVER(veth-peer) {address1} 12:34:56:78:9a:bc', output) self.assertIn('client provides name: test-hostname', output) self.assertIn('26:mtu', output) @@ -5387,7 +5407,7 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): state = get_dbus_dhcp4_client_state('veth99') print(f"State = {state}") - self.assertEqual(state, 'selecting') + self.assertEqual(state, 'rebooting') start_dnsmasq('--dhcp-option=option:dns-server,192.168.5.6,192.168.5.7', '--dhcp-option=option:domain-search,example.com',