From 80922ebbc58768304d5dd503a0eac1b9a923ef15 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 17 Aug 2026 03:42:11 +0900 Subject: [PATCH 1/6] network: unify IPv4ProxyARPAddress= and IPv6ProxyNDPAddress= It is not necessary to provide dedicated settings for IPv4 and IPv6 proxy neighbor entries. Let's unify them. The IPv4ProxyARPAddress= setting was introduced after v261. Hence, we can freely rename it. --- src/network/networkd-neighbor-proxy.c | 43 +++++++------------ src/network/networkd-network-gperf.gperf | 4 +- .../conf/25-ipv4-proxy-arp-disabled.network | 4 +- .../conf/25-ipv4-proxy-arp.network | 10 ++--- .../conf/25-ipv6-proxy-ndp.network | 10 ++--- test/test-network/systemd-networkd-tests.py | 4 +- 6 files changed, 31 insertions(+), 44 deletions(-) diff --git a/src/network/networkd-neighbor-proxy.c b/src/network/networkd-neighbor-proxy.c index de01f38f2a..a2b050f34b 100644 --- a/src/network/networkd-neighbor-proxy.c +++ b/src/network/networkd-neighbor-proxy.c @@ -190,62 +190,49 @@ int config_parse_neighbor_proxy_address( void *data, void *userdata) { - _cleanup_free_ struct in_addr_data *address = NULL; - Network *network = ASSERT_PTR(userdata); - int family = ltype; - union in_addr_union buffer = {}; + Set **neighbor_proxy_addresses = ASSERT_PTR(data); int r; - assert(IN_SET(family, AF_INET, AF_INET6)); assert(filename); assert(lvalue); assert(rvalue); if (isempty(rvalue)) { - /* Drop only entries belonging to this family, so that - * IPv4ProxyARPAddress= and IPv6ProxyNDPAddress= can be reset independently. */ - network_drop_neighbor_proxy_addresses(network, family); + *neighbor_proxy_addresses = set_free(*neighbor_proxy_addresses); return 0; } - r = in_addr_from_string(family, rvalue, &buffer); - if (r < 0) { - log_syntax(unit, LOG_WARNING, filename, line, r, - "Failed to parse %s, ignoring: %s", lvalue, rvalue); - return 0; - } + struct in_addr_data a = {}; + r = in_addr_from_string_auto(rvalue, &a.family, &a.address); + if (r < 0) + return log_syntax_parse_error(unit, filename, line, r, lvalue, rvalue); - if (in_addr_is_null(family, &buffer)) { + if (in_addr_is_null(a.family, &a.address)) { log_syntax(unit, LOG_WARNING, filename, line, 0, - "%s cannot be the ANY address, ignoring: %s", lvalue, rvalue); + "%s= cannot be the ANY address, ignoring: %s", lvalue, rvalue); return 0; } /* Reject address classes that do not qualify as proxy targets and that the kernel would * reject: multicast for both families, plus the IPv4 limited broadcast 255.255.255.255. */ - if (in_addr_is_multicast(family, &buffer) > 0) { + if (in_addr_is_multicast(a.family, &a.address) > 0) { log_syntax(unit, LOG_WARNING, filename, line, 0, - "%s cannot be a multicast address, ignoring: %s", lvalue, rvalue); + "%s= cannot be a multicast address, ignoring: %s", lvalue, rvalue); return 0; } - if (family == AF_INET && buffer.in.s_addr == htobe32(INADDR_BROADCAST)) { + if (a.family == AF_INET && a.address.in.s_addr == htobe32(INADDR_BROADCAST)) { log_syntax(unit, LOG_WARNING, filename, line, 0, - "%s cannot be the limited broadcast address, ignoring: %s", + "%s= cannot be the limited broadcast address, ignoring: %s", lvalue, rvalue); return 0; } - address = new(struct in_addr_data, 1); - if (!address) + struct in_addr_data *copied = newdup(struct in_addr_data, &a, 1); + if (!copied) return log_oom(); - *address = (struct in_addr_data) { - .family = family, - .address = buffer, - }; - - r = set_ensure_consume(&network->neighbor_proxy_addresses, &in_addr_data_hash_ops_free, TAKE_PTR(address)); + r = set_ensure_consume(neighbor_proxy_addresses, &in_addr_data_hash_ops_free, copied); if (r < 0) return log_oom(); diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 0a58c728a8..93711c02f4 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -176,8 +176,8 @@ Network.PrimarySlave, config_parse_bool, Network.IPv4ProxyARP, config_parse_tristate, 0, offsetof(Network, proxy_arp) Network.IPv4ProxyARPPrivateVLAN, config_parse_tristate, 0, offsetof(Network, proxy_arp_pvlan) Network.ProxyARP, config_parse_tristate, 0, offsetof(Network, proxy_arp) -Network.IPv4ProxyARPAddress, config_parse_neighbor_proxy_address, AF_INET, 0 -Network.IPv6ProxyNDPAddress, config_parse_neighbor_proxy_address, AF_INET6, 0 +Network.IPv6ProxyNDPAddress, config_parse_neighbor_proxy_address, 0, offsetof(Network, neighbor_proxy_addresses) /* deprecated */ +Network.ProxyNeighbor, config_parse_neighbor_proxy_address, 0, offsetof(Network, neighbor_proxy_addresses) Network.IPv4ReversePathFilter, config_parse_ip_reverse_path_filter, 0, offsetof(Network, ipv4_rp_filter) Network.MulticastIGMPVersion, config_parse_ipv4_force_igmp_version, 0, offsetof(Network, ipv4_force_igmp_version) Network.MPLSRouting, config_parse_tristate, 0, offsetof(Network, mpls_input) diff --git a/test/test-network/conf/25-ipv4-proxy-arp-disabled.network b/test/test-network/conf/25-ipv4-proxy-arp-disabled.network index 3f0f615ea5..08e4e8ccc7 100644 --- a/test/test-network/conf/25-ipv4-proxy-arp-disabled.network +++ b/test/test-network/conf/25-ipv4-proxy-arp-disabled.network @@ -4,7 +4,7 @@ Name=dummy98 [Network] IPv4ProxyARP=no -IPv4ProxyARPAddress=192.0.2.1 -IPv4ProxyARPAddress=192.0.2.2 +ProxyNeighbor=192.0.2.1 +ProxyNeighbor=192.0.2.2 IPv6AcceptRA=no Address=10.0.0.1/24 diff --git a/test/test-network/conf/25-ipv4-proxy-arp.network b/test/test-network/conf/25-ipv4-proxy-arp.network index 9327559ed5..f1b2543eb3 100644 --- a/test/test-network/conf/25-ipv4-proxy-arp.network +++ b/test/test-network/conf/25-ipv4-proxy-arp.network @@ -3,10 +3,10 @@ Name=dummy98 [Network] -IPv4ProxyARPAddress=192.0.2.1 -IPv4ProxyARPAddress=192.0.2.2 -IPv4ProxyARPAddress=192.0.2.3 -IPv4ProxyARPAddress=192.0.2.4 -IPv4ProxyARPAddress=192.0.2.5 +ProxyNeighbor=192.0.2.1 +ProxyNeighbor=192.0.2.2 +ProxyNeighbor=192.0.2.3 +ProxyNeighbor=192.0.2.4 +ProxyNeighbor=192.0.2.5 IPv6AcceptRA=no Address=10.0.0.1/24 diff --git a/test/test-network/conf/25-ipv6-proxy-ndp.network b/test/test-network/conf/25-ipv6-proxy-ndp.network index d01a633952..bbbe7f998e 100644 --- a/test/test-network/conf/25-ipv6-proxy-ndp.network +++ b/test/test-network/conf/25-ipv6-proxy-ndp.network @@ -3,11 +3,11 @@ Name=dummy98 [Network] -IPv6ProxyNDPAddress=2607:5300:203:5215:5::1 -IPv6ProxyNDPAddress=2607:5300:203:5215:4::1 -IPv6ProxyNDPAddress=2607:5300:203:5215:3::1 -IPv6ProxyNDPAddress=2607:5300:203:5215:2::1 -IPv6ProxyNDPAddress=2607:5300:203:5215:1::1 +ProxyNeighbor=2607:5300:203:5215:5::1 +ProxyNeighbor=2607:5300:203:5215:4::1 +ProxyNeighbor=2607:5300:203:5215:3::1 +ProxyNeighbor=2607:5300:203:5215:2::1 +ProxyNeighbor=2607:5300:203:5215:1::1 IPv6AcceptRA=no Address=66.70.129.136/32 Address=66.70.129.142/32 diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index e55a509367..5fc3fa04e9 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -5384,10 +5384,10 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities): for i in range(1, 6): self.assertRegex(output, f'192.0.2.{i} *proxy') - # IPv4ProxyARPAddress= implies IPv4ProxyARP=yes, mirroring IPv6ProxyNDPAddress=. + # When a proxy ARP address is specified, IPv4ProxyARP=yes is implied. self.check_ipv4_sysctl_attr('dummy98', 'proxy_arp', '1') - # Explicit IPv4ProxyARP=no must suppress all IPv4ProxyARPAddress= entries and + # Explicit IPv4ProxyARP=no must suppress all IPv4 proxy ARP address entries and # must not force proxy_arp=1. The module is add-only (no reconcile/remove pass), # so phase 2 starts from a clean interface: stop networkd, delete the dummy to # flush the kernel neighbor-proxy table, swap the .network file, and restart. From 78ca255140a4c47577d43d66166bd270c2af4165 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 17 Aug 2026 05:45:47 +0900 Subject: [PATCH 2/6] network: do not touch proxy_ndp sysctl when nothing requested Even though the document says "When unset, the kernel's default will be used." for IPv6ProxyNDP=, previously, when both IPv6ProxyNDP= and IPv6ProxyNDPAddress= are unspecified, proxy_ndp sysctl was disabled. This was also inconsistent to other sysctl settings. --- src/network/networkd-sysctl.c | 4 +++- test/test-network/systemd-networkd-tests.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/network/networkd-sysctl.c b/src/network/networkd-sysctl.c index ca58a1815e..d627ade828 100644 --- a/src/network/networkd-sysctl.c +++ b/src/network/networkd-sysctl.c @@ -522,8 +522,10 @@ static int link_set_ipv6_proxy_ndp(Link *link) { if (link->network->ipv6_proxy_ndp >= 0) v = link->network->ipv6_proxy_ndp; + else if (network_has_neighbor_proxy_address(link->network, AF_INET6)) + v = true; else - v = network_has_neighbor_proxy_address(link->network, AF_INET6); + return 0; return sysctl_write_ip_property_boolean(AF_INET6, link->ifname, "proxy_ndp", v, manager_get_sysctl_shadow(link->manager)); } diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 5fc3fa04e9..77b55e84bb 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -11143,21 +11143,21 @@ class NetworkdSysctlTest(unittest.TestCase, Utilities): call('sysctl -w net.ipv6.conf.dummy98.accept_ra=1') call('sysctl -w net.ipv6.conf.dummy98.mtu=1360') call('sysctl -w net.ipv4.conf.dummy98.promote_secondaries=0') - call('sysctl -w net.ipv6.conf.dummy98.proxy_ndp=1') # And unmanaged ones call('sysctl -w net.ipv6.conf.dummy98.hop_limit=4') call('sysctl -w net.ipv6.conf.dummy98.max_addresses=10') + call('sysctl -w net.ipv6.conf.dummy98.proxy_ndp=1') log = read_networkd_log() # fmt: off self.assertRegex(log, r"Foreign process 'sysctl\[\d+\]' changed sysctl '/proc/sys/net/ipv6/conf/dummy98/accept_ra' from '0' to '1', conflicting with our setting to '0'") self.assertRegex(log, r"Foreign process 'sysctl\[\d+\]' changed sysctl '/proc/sys/net/ipv6/conf/dummy98/mtu' from '1550' to '1360', conflicting with our setting to '1550'") self.assertRegex(log, r"Foreign process 'sysctl\[\d+\]' changed sysctl '/proc/sys/net/ipv4/conf/dummy98/promote_secondaries' from '1' to '0', conflicting with our setting to '1'") - self.assertRegex(log, r"Foreign process 'sysctl\[\d+\]' changed sysctl '/proc/sys/net/ipv6/conf/dummy98/proxy_ndp' from '0' to '1', conflicting with our setting to '0'") # fmt: on self.assertNotIn("changed sysctl '/proc/sys/net/ipv6/conf/dummy98/hop_limit'", log) self.assertNotIn("changed sysctl '/proc/sys/net/ipv6/conf/dummy98/max_addresses'", log) + self.assertNotIn("changed sysctl '/proc/sys/net/ipv6/conf/dummy98/proxy_ndp'", log) self.assertNotIn('Sysctl monitor BPF returned error', log) From a9e74be13c79103e7166100a9cdcab34e6565190 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 17 Aug 2026 08:04:58 +0900 Subject: [PATCH 3/6] network: adjust sysctl setting in network_adjust_neighbor_proxy() This also makes IPv4 proxy neighbor entries independent of proxy_arp sysctl. We can configure IPv4 proxy neighbor entries even if proxy_arp is disabled. --- src/network/networkd-neighbor-proxy.c | 74 +++++++------------ src/network/networkd-neighbor-proxy.h | 2 - src/network/networkd-sysctl.c | 28 ++----- .../conf/25-ipv4-proxy-arp-disabled.network | 4 +- test/test-network/systemd-networkd-tests.py | 14 ++-- 5 files changed, 42 insertions(+), 80 deletions(-) diff --git a/src/network/networkd-neighbor-proxy.c b/src/network/networkd-neighbor-proxy.c index a2b050f34b..49899af6a4 100644 --- a/src/network/networkd-neighbor-proxy.c +++ b/src/network/networkd-neighbor-proxy.c @@ -15,61 +15,37 @@ #include "socket-util.h" #include "string-util.h" -bool network_has_neighbor_proxy_address(const Network *network, int family) { - struct in_addr_data *a; - - assert(network); - assert(IN_SET(family, AF_INET, AF_INET6)); - - SET_FOREACH(a, network->neighbor_proxy_addresses) - if (a->family == family) - return true; - - return false; -} - -static void network_drop_neighbor_proxy_addresses(Network *network, int family) { - struct in_addr_data *a; - - assert(network); - assert(IN_SET(family, AF_INET, AF_INET6)); - - SET_FOREACH(a, network->neighbor_proxy_addresses) - if (a->family == family) - free(set_remove(network->neighbor_proxy_addresses, a)); - - if (set_isempty(network->neighbor_proxy_addresses)) - network->neighbor_proxy_addresses = set_free(network->neighbor_proxy_addresses); -} - void network_adjust_neighbor_proxy(Network *network) { assert(network); - if (set_isempty(network->neighbor_proxy_addresses)) - return; + struct in_addr_data *a; + SET_FOREACH(a, network->neighbor_proxy_addresses) { + switch (a->family) { + case AF_INET6: + if (!socket_ipv6_is_supported()) { + log_warning("%s: Specified IPv6 proxy NDP address %s, but IPv6 is not supported by the kernel, ignoring.", + network->filename, IN_ADDR_TO_STRING(a->family, &a->address)); + free(set_remove(network->neighbor_proxy_addresses, a)); + continue; + } - /* If IPv6 is not supported by the kernel, drop any IPv6 entries up front. */ - if (!socket_ipv6_is_supported() && - network_has_neighbor_proxy_address(network, AF_INET6)) { - log_once(LOG_WARNING, - "%s: IPv6 proxy NDP addresses are set, but IPv6 is not supported by kernel, " - "ignoring IPv6 proxy NDP addresses.", network->filename); - network_drop_neighbor_proxy_addresses(network, AF_INET6); - } + if (network->ipv6_proxy_ndp == 0) { + log_warning("%s: Specified IPv6 proxy NDP address %s, but IPv6ProxyNDP= is disabled, ignoring.", + network->filename, IN_ADDR_TO_STRING(a->family, &a->address)); + free(set_remove(network->neighbor_proxy_addresses, a)); + continue; + } - /* Drop per-family entries when the corresponding proxy sysctl was explicitly disabled. - * For IPv6 the proxy_ndp sysctl is required for manual entries to take effect; for IPv4 we - * apply the same rule for consistency so that an explicit IPv4ProxyARP=no is respected. */ - int family; - FOREACH_ARGUMENT(family, AF_INET, AF_INET6) { - int tristate = family == AF_INET ? network->proxy_arp : network->ipv6_proxy_ndp; + /* IPv6 proxy NDP entry requires that proxy_ndp sysctl is enabled. */ + network->ipv6_proxy_ndp = true; + break; - if (tristate == 0 && network_has_neighbor_proxy_address(network, family)) { - log_warning("%s: %s is disabled. Ignoring %s.", - network->filename, - family == AF_INET ? "IPv4ProxyARP=" : "IPv6ProxyNDP=", - family == AF_INET ? "IPv4ProxyARPAddress=" : "IPv6ProxyNDPAddress="); - network_drop_neighbor_proxy_addresses(network, family); + case AF_INET: + /* IPv4 proxy ARP entry does NOT require that proxy_arp sysctl is enabled. */ + break; + + default: + assert_not_reached(); } } } diff --git a/src/network/networkd-neighbor-proxy.h b/src/network/networkd-neighbor-proxy.h index b7e4bb0f6a..49ee2008ae 100644 --- a/src/network/networkd-neighbor-proxy.h +++ b/src/network/networkd-neighbor-proxy.h @@ -3,8 +3,6 @@ #include "networkd-forward.h" -bool network_has_neighbor_proxy_address(const Network *network, int family); - void network_adjust_neighbor_proxy(Network *network); int link_request_static_neighbor_proxy_addresses(Link *link); diff --git a/src/network/networkd-sysctl.c b/src/network/networkd-sysctl.c index d627ade828..3c0b8d31b9 100644 --- a/src/network/networkd-sysctl.c +++ b/src/network/networkd-sysctl.c @@ -18,7 +18,6 @@ #include "networkd-lldp-tx.h" #include "networkd-manager.h" #include "networkd-ndisc.h" -#include "networkd-neighbor-proxy.h" #include "networkd-network.h" #include "networkd-sysctl.h" #include "path-util.h" @@ -271,25 +270,18 @@ static int link_update_ipv6_sysctl(Link *link) { } static int link_set_proxy_arp(Link *link) { - bool v; - assert(link); assert(link->manager); if (!link_is_configured_for_family(link, AF_INET)) return 0; - if (link->network->proxy_arp >= 0) - v = link->network->proxy_arp; - else if (network_has_neighbor_proxy_address(link->network, AF_INET)) - /* If IPv4ProxyARP= is not explicitly set, but per-address IPv4ProxyARPAddress= - * entries are configured, implicitly enable the proxy_arp sysctl. This matches - * the behavior of IPv6ProxyNDPAddress= which implies IPv6ProxyNDP=yes. */ - v = true; - else + if (link->network->proxy_arp < 0) return 0; - return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "proxy_arp", v, manager_get_sysctl_shadow(link->manager)); + return sysctl_write_ip_property_boolean( + AF_INET, link->ifname, "proxy_arp", link->network->proxy_arp, + manager_get_sysctl_shadow(link->manager)); } static int link_set_proxy_arp_pvlan(Link *link) { @@ -512,22 +504,18 @@ static int link_set_ipv6_retransmission_time(Link *link) { } static int link_set_ipv6_proxy_ndp(Link *link) { - bool v; - assert(link); assert(link->manager); if (!link_is_configured_for_family(link, AF_INET6)) return 0; - if (link->network->ipv6_proxy_ndp >= 0) - v = link->network->ipv6_proxy_ndp; - else if (network_has_neighbor_proxy_address(link->network, AF_INET6)) - v = true; - else + if (link->network->ipv6_proxy_ndp < 0) return 0; - return sysctl_write_ip_property_boolean(AF_INET6, link->ifname, "proxy_ndp", v, manager_get_sysctl_shadow(link->manager)); + return sysctl_write_ip_property_boolean( + AF_INET6, link->ifname, "proxy_ndp", link->network->ipv6_proxy_ndp, + manager_get_sysctl_shadow(link->manager)); } int link_set_ipv6_mtu(Link *link, int log_level) { diff --git a/test/test-network/conf/25-ipv4-proxy-arp-disabled.network b/test/test-network/conf/25-ipv4-proxy-arp-disabled.network index 08e4e8ccc7..e0ca8c4e50 100644 --- a/test/test-network/conf/25-ipv4-proxy-arp-disabled.network +++ b/test/test-network/conf/25-ipv4-proxy-arp-disabled.network @@ -4,7 +4,7 @@ Name=dummy98 [Network] IPv4ProxyARP=no -ProxyNeighbor=192.0.2.1 -ProxyNeighbor=192.0.2.2 +ProxyNeighbor=192.0.3.1 +ProxyNeighbor=192.0.3.2 IPv6AcceptRA=no Address=10.0.0.1/24 diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 77b55e84bb..961a3f49a7 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -5384,13 +5384,9 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities): for i in range(1, 6): self.assertRegex(output, f'192.0.2.{i} *proxy') - # When a proxy ARP address is specified, IPv4ProxyARP=yes is implied. - self.check_ipv4_sysctl_attr('dummy98', 'proxy_arp', '1') + # Even if a proxy ARP address is specified, IPv4ProxyARP=yes is NOT implied. + self.check_ipv4_sysctl_attr('dummy98', 'proxy_arp', '0') - # Explicit IPv4ProxyARP=no must suppress all IPv4 proxy ARP address entries and - # must not force proxy_arp=1. The module is add-only (no reconcile/remove pass), - # so phase 2 starts from a clean interface: stop networkd, delete the dummy to - # flush the kernel neighbor-proxy table, swap the .network file, and restart. stop_networkd() remove_link('dummy98') remove_network_unit('25-ipv4-proxy-arp.network') @@ -5398,10 +5394,14 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities): start_networkd() self.wait_online('dummy98:routable') + # Even if IPv4ProxyARP= is disabled, proxy ARP addresses can be configured. output = check_output('ip -4 neighbor show proxy dev dummy98') print(output) for i in range(1, 6): - self.assertNotIn(f'192.0.2.{i}', output) + self.assertNotRegex(output, f'192.0.2.{i} *proxy') + for i in range(1, 3): + self.assertRegex(output, f'192.0.3.{i} *proxy') + self.check_ipv4_sysctl_attr('dummy98', 'proxy_arp', '0') def test_ipv6_neigh_retrans_time(self): From f3ada8a0fb9a96b4bac130e4f26f9e301a9cc9bb Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 17 Aug 2026 17:05:24 +0900 Subject: [PATCH 4/6] network: introduce and use sysctl_write_ip_property_tristate() helper function This also break some long lines. --- src/basic/sysctl-util.h | 7 ++++ src/network/networkd-sysctl.c | 70 ++++++++++++++++------------------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/basic/sysctl-util.h b/src/basic/sysctl-util.h index 1e706c839c..4d00115a24 100644 --- a/src/basic/sysctl-util.h +++ b/src/basic/sysctl-util.h @@ -20,6 +20,13 @@ int sysctl_read_ip_property_uint32(int af, const char *ifname, const char *prope int sysctl_write_ip_property(int af, const char *ifname, const char *property, const char *value, Hashmap **shadow); int sysctl_write_ip_property_boolean(int af, const char *ifname, const char *property, bool value, Hashmap **shadow); +static inline int sysctl_write_ip_property_tristate(int af, const char *ifname, const char *property, int value, Hashmap **shadow) { + if (value < 0) + return 0; + + return sysctl_write_ip_property_boolean(af, ifname, property, value, shadow); +} + int sysctl_write_ip_neighbor_property(int af, const char *ifname, const char *property, const char *value, Hashmap **shadow); int sysctl_write_ip_neighbor_property_uint32(int af, const char *ifname, const char *property, uint32_t value, Hashmap **shadow); diff --git a/src/network/networkd-sysctl.c b/src/network/networkd-sysctl.c index 3c0b8d31b9..5787e91b37 100644 --- a/src/network/networkd-sysctl.c +++ b/src/network/networkd-sysctl.c @@ -211,17 +211,19 @@ static void manager_set_ip_forwarding(Manager *manager, int family) { return; t = manager->ip_forwarding[family == AF_INET6]; - if (t < 0) - return; /* keep */ /* First, set the default value. */ - r = sysctl_write_ip_property_boolean(family, "default", "forwarding", t, manager_get_sysctl_shadow(manager)); + r = sysctl_write_ip_property_tristate( + family, "default", "forwarding", t, + manager_get_sysctl_shadow(manager)); if (r < 0) log_warning_errno(r, "Failed to %s the default %s forwarding: %m", enable_disable(t), af_to_ipv4_ipv6(family)); /* Then, set the value to all interfaces. */ - r = sysctl_write_ip_property_boolean(family, "all", "forwarding", t, manager_get_sysctl_shadow(manager)); + r = sysctl_write_ip_property_tristate( + family, "all", "forwarding", t, + manager_get_sysctl_shadow(manager)); if (r < 0) log_warning_errno(r, "Failed to %s %s forwarding for all interfaces: %m", enable_disable(t), af_to_ipv4_ipv6(family)); @@ -266,7 +268,9 @@ static int link_update_ipv6_sysctl(Link *link) { if (!link_ipv6_enabled(link)) return 0; - return sysctl_write_ip_property_boolean(AF_INET6, link->ifname, "disable_ipv6", false, manager_get_sysctl_shadow(link->manager)); + return sysctl_write_ip_property_boolean( + AF_INET6, link->ifname, "disable_ipv6", false, + manager_get_sysctl_shadow(link->manager)); } static int link_set_proxy_arp(Link *link) { @@ -276,10 +280,7 @@ static int link_set_proxy_arp(Link *link) { if (!link_is_configured_for_family(link, AF_INET)) return 0; - if (link->network->proxy_arp < 0) - return 0; - - return sysctl_write_ip_property_boolean( + return sysctl_write_ip_property_tristate( AF_INET, link->ifname, "proxy_arp", link->network->proxy_arp, manager_get_sysctl_shadow(link->manager)); } @@ -291,10 +292,9 @@ static int link_set_proxy_arp_pvlan(Link *link) { if (!link_is_configured_for_family(link, AF_INET)) return 0; - if (link->network->proxy_arp_pvlan < 0) - return 0; - - return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "proxy_arp_pvlan", link->network->proxy_arp_pvlan > 0, manager_get_sysctl_shadow(link->manager)); + return sysctl_write_ip_property_tristate( + AF_INET, link->ifname, "proxy_arp_pvlan", link->network->proxy_arp_pvlan, + manager_get_sysctl_shadow(link->manager)); } int link_get_ip_forwarding(Link *link, int family) { @@ -331,10 +331,9 @@ static int link_set_ip_forwarding_impl(Link *link, int family) { return 0; t = link_get_ip_forwarding(link, family); - if (t < 0) - return 0; /* keep */ - - r = sysctl_write_ip_property_boolean(family, link->ifname, "forwarding", t, manager_get_sysctl_shadow(link->manager)); + r = sysctl_write_ip_property_tristate( + family, link->ifname, "forwarding", t, + manager_get_sysctl_shadow(link->manager)); if (r < 0) return log_link_warning_errno(link, r, "Failed to %s %s forwarding, ignoring: %m", enable_disable(t), af_to_ipv4_ipv6(family)); @@ -510,10 +509,7 @@ static int link_set_ipv6_proxy_ndp(Link *link) { if (!link_is_configured_for_family(link, AF_INET6)) return 0; - if (link->network->ipv6_proxy_ndp < 0) - return 0; - - return sysctl_write_ip_property_boolean( + return sysctl_write_ip_property_tristate( AF_INET6, link->ifname, "proxy_ndp", link->network->ipv6_proxy_ndp, manager_get_sysctl_shadow(link->manager)); } @@ -643,10 +639,9 @@ static int link_set_ipv4_accept_local(Link *link) { if (!link_is_configured_for_family(link, AF_INET)) return 0; - if (link->network->ipv4_accept_local < 0) - return 0; - - return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "accept_local", link->network->ipv4_accept_local > 0, manager_get_sysctl_shadow(link->manager)); + return sysctl_write_ip_property_tristate( + AF_INET, link->ifname, "accept_local", link->network->ipv4_accept_local, + manager_get_sysctl_shadow(link->manager)); } static int link_set_ipv4_route_localnet(Link *link) { @@ -656,10 +651,9 @@ static int link_set_ipv4_route_localnet(Link *link) { if (!link_is_configured_for_family(link, AF_INET)) return 0; - if (link->network->ipv4_route_localnet < 0) - return 0; - - return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "route_localnet", link->network->ipv4_route_localnet > 0, manager_get_sysctl_shadow(link->manager)); + return sysctl_write_ip_property_tristate( + AF_INET, link->ifname, "route_localnet", link->network->ipv4_route_localnet, + manager_get_sysctl_shadow(link->manager)); } static int link_set_ipv4_src_valid_mark(Link *link) { @@ -670,10 +664,9 @@ static int link_set_ipv4_src_valid_mark(Link *link) { if (!link_is_configured_for_family(link, AF_INET)) return 0; - if (link->network->ipv4_src_valid_mark < 0) - return 0; - - return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "src_valid_mark", link->network->ipv4_src_valid_mark > 0, manager_get_sysctl_shadow(link->manager)); + return sysctl_write_ip_property_tristate( + AF_INET, link->ifname, "src_valid_mark", link->network->ipv4_src_valid_mark, + manager_get_sysctl_shadow(link->manager)); } static int link_set_ipv4_promote_secondaries(Link *link) { @@ -688,7 +681,9 @@ static int link_set_ipv4_promote_secondaries(Link *link) { * otherwise. The way systemd-networkd works is that the new IP of a lease is added as a * secondary IP and when the primary one expires it relies on the kernel to promote the * secondary IP. See also https://github.com/systemd/systemd/issues/7163 */ - return sysctl_write_ip_property_boolean(AF_INET, link->ifname, "promote_secondaries", true, manager_get_sysctl_shadow(link->manager)); + return sysctl_write_ip_property_boolean( + AF_INET, link->ifname, "promote_secondaries", true, + manager_get_sysctl_shadow(link->manager)); } static int link_set_mpls_input(Link *link) { @@ -698,10 +693,9 @@ static int link_set_mpls_input(Link *link) { if (!link_is_configured_for_family(link, AF_MPLS)) return 0; - if (link->network->mpls_input < 0) - return 0; - - return sysctl_write_ip_property_boolean(AF_MPLS, link->ifname, "input", link->network->mpls_input > 0, manager_get_sysctl_shadow(link->manager)); + return sysctl_write_ip_property_tristate( + AF_MPLS, link->ifname, "input", link->network->mpls_input, + manager_get_sysctl_shadow(link->manager)); } int link_set_sysctl(Link *link) { From 30c177a93731a1451bbd30f7d8e2cde15b85f081 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 17 Aug 2026 08:06:23 +0900 Subject: [PATCH 5/6] test-network: add test cases for IPv6ProxyNDP=no In that case, IPv6 proxy neighbor entries are ignored. This also checks IPv6ProxyNDP=yes is implied if an IPv6 proxy neighbor address is specified. --- test/test-network/systemd-networkd-tests.py | 23 ++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 961a3f49a7..7ca0567bab 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -5370,9 +5370,30 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities): output = check_output('ip neighbor show proxy dev dummy98') print(output) - for i in range(1, 5): + for i in range(1, 6): self.assertRegex(output, f'2607:5300:203:5215:{i}::1 *proxy') + # When a proxy NDP address is specified, IPv6ProxyNDP=yes is implied. + self.check_ipv6_sysctl_attr('dummy98', 'proxy_ndp', '1') + + with open( + os.path.join(network_unit_dir, '25-ipv6-proxy-ndp.network'), mode='a', encoding='utf-8' + ) as f: + f.write('[Network]\nIPv6ProxyNDP=no\n') + + stop_networkd() + remove_link('dummy98') + start_networkd() + self.wait_online('dummy98:routable') + + # If IPv6ProxyNDP= is disabled, proxy NDP addresses are ignored. + output = check_output('ip neighbor show proxy dev dummy98') + print(output) + for i in range(1, 6): + self.assertNotRegex(output, f'2607:5300:203:5215:{i}::1 *proxy') + + self.check_ipv6_sysctl_attr('dummy98', 'proxy_ndp', '0') + def test_ipv4_proxy_arp(self): copy_network_unit('25-ipv4-proxy-arp.network', '12-dummy.netdev') start_networkd() From ab269ec8b2e01ee22adb7806f2d92fe87335bdea Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 17 Aug 2026 04:05:19 +0900 Subject: [PATCH 6/6] doc: mention ProxyNeighbor= in man and NEWS --- NEWS | 9 +++------ man/systemd.network.xml | 37 ++++++++++--------------------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/NEWS b/NEWS index c5686b929f..486bba2afc 100644 --- a/NEWS +++ b/NEWS @@ -79,12 +79,9 @@ CHANGES WITH 262: Changes in systemd-networkd: - * A new IPv4ProxyARPAddress= setting has been added to the [Network] - section of .network files. It is the IPv4 counterpart to - IPv6ProxyNDPAddress= and allows manual entries to be added to the - kernel's IPv4 neighbor proxy table. The setting implies - IPv4ProxyARP=yes but has no effect if IPv4ProxyARP= has been set to - false. This mirrors the behavior of IPv6ProxyNDPAddress=. + * The IPv6ProxyNDPAddress= setting in the [Network] section of .network + files has been renamed to ProxyNeighbor=. The setting can now also be + used to add IPv4 addresses to the kernel's IPv4 neighbor proxy table. Changes in various tools: diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 1893c60404..e2c51fab90 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -1150,25 +1150,6 @@ DuplicateAddressDetection=none - - IPv4ProxyARPAddress= - - An IPv4 address, for which ARP requests will be proxied. This option may be specified - more than once. systemd-networkd will add the - IPv4ProxyARPAddress= entries to the kernel's IPv4 neighbor proxy table. - When IPv4ProxyARP= is unset, this setting implies - IPv4ProxyARP=yes: the per-interface proxy_arp sysctl - is enabled automatically, which also activates automatic proxy ARP for every address the - interface has a route toward on connected subnets, in addition to answering the listed - entries. If IPv4ProxyARP= has been set to false, the - IPv4ProxyARPAddress= entries listed here are discarded and not - programmed into the kernel. This mirrors the behaviour of - IPv6ProxyNDPAddress=/IPv6ProxyNDP=. - - - - - IPv6ProxyNDP= @@ -1188,16 +1169,18 @@ DuplicateAddressDetection=none - IPv6ProxyNDPAddress= + ProxyNeighbor= - An IPv6 address, for which Neighbour Advertisement messages will be proxied. This - option may be specified more than once. systemd-networkd will add the - IPv6ProxyNDPAddress= entries to the kernel's IPv6 neighbor proxy table. - This setting implies IPv6ProxyNDP=yes but has no effect if - IPv6ProxyNDP= has been set to false. When unset, the kernel's default will - be used. + Takes an IPv4 or IPv6 address for which ARP requests or Neighbour Advertisement messages will + be proxied. This option may be specified more than once. systemd-networkd will + add the specified address to the kernel's neighbour proxy table. For an IPv4 address, a proxy ARP + entry will be added. For an IPv6 address, a proxy NDP entry will be added. When an IPv6 address is + specified, IPv6ProxyNDP=yes is implied. If IPv6ProxyNDP= is + explicitly disabled, IPv6 addresses specified with ProxyNeighbor= are ignored. + Note, unlike IPv6, specifying an IPv4 address does not imply + IPv4ProxyARP=. - +