From c95df5879eeb2cec8bc8eec2cfa7e741e1d9469f Mon Sep 17 00:00:00 2001 From: Yegor Alexeyev Date: Mon, 15 Mar 2021 21:19:52 +0100 Subject: [PATCH] relay role implementation --- man/systemd.network.xml | 11 ++ src/libsystemd-network/dhcp-server-internal.h | 1 + src/libsystemd-network/sd-dhcp-server.c | 110 ++++++++++++++---- src/network/networkd-dhcp-server-bus.c | 3 + src/network/networkd-dhcp-server.c | 30 +++++ src/network/networkd-dhcp-server.h | 1 + src/network/networkd-network-gperf.gperf | 1 + src/network/networkd-network.h | 1 + src/systemd/sd-dhcp-server.h | 2 + .../fuzz-network-parser/directives.network | 1 + .../conf/agent-client-peer.network | 9 ++ test/test-network/conf/agent-client.network | 5 + .../conf/agent-server-peer.network | 5 + test/test-network/conf/agent-server.network | 10 ++ .../conf/agent-veth-client.netdev | 8 ++ .../conf/agent-veth-server.netdev | 8 ++ test/test-network/systemd-networkd-tests.py | 37 ++++++ 17 files changed, 222 insertions(+), 21 deletions(-) create mode 100644 test/test-network/conf/agent-client-peer.network create mode 100644 test/test-network/conf/agent-client.network create mode 100644 test/test-network/conf/agent-server-peer.network create mode 100644 test/test-network/conf/agent-server.network create mode 100644 test/test-network/conf/agent-veth-client.netdev create mode 100644 test/test-network/conf/agent-veth-server.netdev diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 5d2db39e4a6..554d1f663ce 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -2294,6 +2294,17 @@ IPv6Token=prefixstable:2002:da8:1:: + + RelayTarget= + + Takes an IPv4 address, which must be in the format + described in + inet_pton3. + Turns this DHCP server into a DHCP relay agent. See RFC 1542. + The address is the address of DHCP server or another relay agent to forward DHCP messages to and from. + Check also BindToInterface= option. Turning it off is required for relaying messages outside. + + PoolOffset= PoolSize= diff --git a/src/libsystemd-network/dhcp-server-internal.h b/src/libsystemd-network/dhcp-server-internal.h index b905791f3c2..3628223ae9a 100644 --- a/src/libsystemd-network/dhcp-server-internal.h +++ b/src/libsystemd-network/dhcp-server-internal.h @@ -39,6 +39,7 @@ typedef struct DHCPLease { } DHCPLease; struct sd_dhcp_server { + struct in_addr relay_target; unsigned n_ref; sd_event *event; diff --git a/src/libsystemd-network/sd-dhcp-server.c b/src/libsystemd-network/sd-dhcp-server.c index be61474758d..e2ad99ebcbd 100644 --- a/src/libsystemd-network/sd-dhcp-server.c +++ b/src/libsystemd-network/sd-dhcp-server.c @@ -114,6 +114,12 @@ int sd_dhcp_server_is_running(sd_dhcp_server *server) { return !!server->receive_message; } +int sd_dhcp_server_is_in_relay_mode(sd_dhcp_server *server) { + assert_return(server, -EINVAL); + + return in4_addr_is_set(&server->relay_target); +} + void client_id_hash_func(const DHCPClientId *id, struct siphash *state) { assert(id); assert(id->length); @@ -343,10 +349,27 @@ static int dhcp_server_send_udp(sd_dhcp_server *server, be32_t destination, return 0; } -static bool requested_broadcast(DHCPRequest *req) { - assert(req); +static bool requested_broadcast(DHCPMessage *message) { + assert(message); + return message->flags & htobe16(0x8000); +} - return req->message->flags & htobe16(0x8000); +static int dhcp_server_send(sd_dhcp_server *server, be32_t destination, uint16_t destination_port, + DHCPPacket *packet, size_t optoffset, bool l2_broadcast) { + if (destination != INADDR_ANY) + return dhcp_server_send_udp(server, destination, + destination_port, &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + else if (l2_broadcast) + return dhcp_server_send_udp(server, INADDR_BROADCAST, + destination_port, &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + else + /* we cannot send UDP packet to specific MAC address when the + address is not yet configured, so must fall back to raw + packets */ + return dhcp_server_send_unicast_raw(server, packet, + sizeof(DHCPPacket) + optoffset); } int dhcp_server_send_packet(sd_dhcp_server *server, @@ -404,20 +427,8 @@ int dhcp_server_send_packet(sd_dhcp_server *server, } else if (req->message->ciaddr && type != DHCP_NAK) destination = req->message->ciaddr; - if (destination != INADDR_ANY) - return dhcp_server_send_udp(server, destination, - destination_port, &packet->dhcp, - sizeof(DHCPMessage) + optoffset); - else if (requested_broadcast(req) || type == DHCP_NAK) - return dhcp_server_send_udp(server, INADDR_BROADCAST, - destination_port, &packet->dhcp, - sizeof(DHCPMessage) + optoffset); - else - /* we cannot send UDP packet to specific MAC address when the - address is not yet configured, so must fall back to raw - packets */ - return dhcp_server_send_unicast_raw(server, packet, - sizeof(DHCPPacket) + optoffset); + bool l2_broadcast = requested_broadcast(req->message) || type == DHCP_NAK; + return dhcp_server_send(server, destination, destination_port, packet, optoffset, l2_broadcast); } static int server_message_init(sd_dhcp_server *server, DHCPPacket **ret, @@ -701,6 +712,47 @@ static int get_pool_offset(sd_dhcp_server *server, be32_t requested_ip) { return be32toh(requested_ip & ~server->netmask) - server->pool_offset; } +static int dhcp_server_relay_message(sd_dhcp_server *server, DHCPMessage *message, size_t opt_length) { + _cleanup_free_ DHCPPacket *packet = NULL; + + assert(server); + assert(message); + assert(sd_dhcp_server_is_in_relay_mode(server)); + + if (message->op == BOOTREQUEST) { + log_dhcp_server(server, "(relay agent) BOOTREQUEST (0x%x)", be32toh(message->xid)); + if (message->hops >= 16) + return -ETIME; + message->hops++; + + /* https://tools.ietf.org/html/rfc1542#section-4.1.1 */ + if (message->giaddr == 0) + message->giaddr = server->address; + + return dhcp_server_send_udp(server, server->relay_target.s_addr, DHCP_PORT_SERVER, message, sizeof(DHCPMessage) + opt_length); + } else if (message->op == BOOTREPLY) { + log_dhcp_server(server, "(relay agent) BOOTREPLY (0x%x)", be32toh(message->xid)); + if (message->giaddr != server->address) { + return log_dhcp_server_errno(server, SYNTHETIC_ERRNO(EBADMSG), + "(relay agent) BOOTREPLY giaddr mismatch, discarding"); + } + + int message_type = dhcp_option_parse(message, sizeof(DHCPMessage) + opt_length, NULL, NULL, NULL); + if (message_type < 0) + return message_type; + + packet = malloc0(sizeof(DHCPPacket) + opt_length); + if (!packet) + return -ENOMEM; + memcpy(&packet->dhcp, message, sizeof(DHCPMessage) + opt_length); + + bool l2_broadcast = requested_broadcast(message) || message_type == DHCP_NAK; + const be32_t destination = message_type == DHCP_NAK ? INADDR_ANY : message->ciaddr; + return dhcp_server_send(server, destination, DHCP_PORT_CLIENT, packet, opt_length, l2_broadcast); + } + return -EBADMSG; +} + #define HASH_KEY SD_ID128_MAKE(0d,1d,fe,bd,f1,24,bd,b3,47,f1,dd,6e,73,21,93,30) int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, @@ -999,10 +1051,15 @@ static int server_receive_message(sd_event_source *s, int fd, } } - r = dhcp_server_handle_message(server, message, (size_t) len); - if (r < 0) - log_dhcp_server_errno(server, r, "Couldn't process incoming message: %m"); - + if (sd_dhcp_server_is_in_relay_mode(server)) { + r = dhcp_server_relay_message(server, message, len - sizeof(DHCPMessage)); + if (r < 0) + log_dhcp_server_errno(server, r, "Couldn't relay message: %m"); + } else { + r = dhcp_server_handle_message(server, message, (size_t) len); + if (r < 0) + log_dhcp_server_errno(server, r, "Couldn't process incoming message: %m"); + } return 0; } @@ -1238,3 +1295,14 @@ int sd_dhcp_server_set_callback(sd_dhcp_server *server, sd_dhcp_server_callback_ return 0; } + +int sd_dhcp_server_set_relay_target(sd_dhcp_server *server, const struct in_addr* address) { + assert_return(server, -EINVAL); + assert_return(!sd_dhcp_server_is_running(server), -EBUSY); + + if (memcmp(address, &server->relay_target, sizeof(struct in_addr)) == 0) + return 0; + + server->relay_target = *address; + return 1; +} diff --git a/src/network/networkd-dhcp-server-bus.c b/src/network/networkd-dhcp-server-bus.c index 32f4baed78c..91a10dc33a0 100644 --- a/src/network/networkd-dhcp-server-bus.c +++ b/src/network/networkd-dhcp-server-bus.c @@ -31,6 +31,9 @@ static int property_get_leases( if (!s) return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Link %s has no DHCP server.", l->ifname); + if (sd_dhcp_server_is_in_relay_mode(s)) + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Link %s has DHCP relay agent active.", l->ifname); + r = sd_bus_message_open_container(reply, 'a', "(uayayayayt)"); if (r < 0) return r; diff --git a/src/network/networkd-dhcp-server.c b/src/network/networkd-dhcp-server.c index 64bb23f3870..bd368672da7 100644 --- a/src/network/networkd-dhcp-server.c +++ b/src/network/networkd-dhcp-server.c @@ -352,6 +352,10 @@ int dhcp4_server_configure(Link *link) { if (r < 0) return log_link_error_errno(link, r, "Failed to set router emission for DHCP server: %m"); + r = sd_dhcp_server_set_relay_target(link->dhcp_server, &link->network->dhcp_server_relay_target); + if (r < 0) + return log_link_error_errno(link, r, "Failed to set relay target for DHCP server: %m"); + if (link->network->dhcp_server_emit_timezone) { _cleanup_free_ char *buffer = NULL; const char *tz; @@ -398,6 +402,32 @@ int dhcp4_server_configure(Link *link) { return 0; } +int config_parse_dhcp_server_relay_target( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = userdata; + union in_addr_union a; + int r; + + r = in_addr_from_string(AF_INET, rvalue, &a); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s= address '%s', ignoring: %m", lvalue, rvalue); + return 0; + } + network->dhcp_server_relay_target = a.in; + return r; +} + int config_parse_dhcp_server_emit( const char *unit, const char *filename, diff --git a/src/network/networkd-dhcp-server.h b/src/network/networkd-dhcp-server.h index 4bd5120ea0b..9e5d24fbe81 100644 --- a/src/network/networkd-dhcp-server.h +++ b/src/network/networkd-dhcp-server.h @@ -9,4 +9,5 @@ typedef struct Link Link; int dhcp4_server_configure(Link *link); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_relay_target); CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_emit); diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 6c37e324531..4fc368547c2 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -256,6 +256,7 @@ IPv6AcceptRA.PrefixAllowList, config_parse_ndisc_address_filter, IPv6AcceptRA.PrefixDenyList, config_parse_ndisc_address_filter, 0, offsetof(Network, ndisc_deny_listed_prefix) IPv6AcceptRA.RouteAllowList, config_parse_ndisc_address_filter, 0, offsetof(Network, ndisc_allow_listed_route_prefix) IPv6AcceptRA.RouteDenyList, config_parse_ndisc_address_filter, 0, offsetof(Network, ndisc_deny_listed_route_prefix) +DHCPServer.RelayTarget, config_parse_dhcp_server_relay_target, 0, 0 DHCPServer.MaxLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_max_lease_time_usec) DHCPServer.DefaultLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_default_lease_time_usec) DHCPServer.EmitDNS, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_DNS].emit) diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index df77b426192..16a445982c6 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -186,6 +186,7 @@ struct Network { /* DHCP Server Support */ bool dhcp_server; bool dhcp_server_bind_to_interface; + struct in_addr dhcp_server_relay_target; NetworkDHCPServerEmitAddress dhcp_server_emit[_SD_DHCP_LEASE_SERVER_TYPE_MAX]; bool dhcp_server_emit_router; bool dhcp_server_emit_timezone; diff --git a/src/systemd/sd-dhcp-server.h b/src/systemd/sd-dhcp-server.h index 511c8daf407..3cee2c2b062 100644 --- a/src/systemd/sd-dhcp-server.h +++ b/src/systemd/sd-dhcp-server.h @@ -83,6 +83,8 @@ int sd_dhcp_server_set_default_lease_time(sd_dhcp_server *server, uint32_t t); int sd_dhcp_server_forcerenew(sd_dhcp_server *server); +int sd_dhcp_server_is_in_relay_mode(sd_dhcp_server *server); +int sd_dhcp_server_set_relay_target(sd_dhcp_server *server, const struct in_addr* address); _SD_DEFINE_POINTER_CLEANUP_FUNC(sd_dhcp_server, sd_dhcp_server_unref); _SD_END_DECLARATIONS; diff --git a/test/fuzz/fuzz-network-parser/directives.network b/test/fuzz/fuzz-network-parser/directives.network index 016be501ed8..158f0ffad11 100644 --- a/test/fuzz/fuzz-network-parser/directives.network +++ b/test/fuzz/fuzz-network-parser/directives.network @@ -352,6 +352,7 @@ DNS= SendOption= SendVendorOption= BindToInterface= +RelayTarget= [NextHop] Id= Gateway= diff --git a/test/test-network/conf/agent-client-peer.network b/test/test-network/conf/agent-client-peer.network new file mode 100644 index 00000000000..0db83dc9d23 --- /dev/null +++ b/test/test-network/conf/agent-client-peer.network @@ -0,0 +1,9 @@ +[Match] +Name=client-peer +[Network] +Address=192.168.6.2/24 +DHCPServer=yes +IPForward=ipv4 +[DHCPServer] +RelayTarget=192.168.5.1 +BindToInterface=no diff --git a/test/test-network/conf/agent-client.network b/test/test-network/conf/agent-client.network new file mode 100644 index 00000000000..773dc8a4fba --- /dev/null +++ b/test/test-network/conf/agent-client.network @@ -0,0 +1,5 @@ +[Match] +Name=client +[Network] +DHCP=yes +IPForward=ipv4 diff --git a/test/test-network/conf/agent-server-peer.network b/test/test-network/conf/agent-server-peer.network new file mode 100644 index 00000000000..fdb235e1ffc --- /dev/null +++ b/test/test-network/conf/agent-server-peer.network @@ -0,0 +1,5 @@ +[Match] +Name=server-peer +[Network] +Address=192.168.5.2/24 +IPForward=ipv4 diff --git a/test/test-network/conf/agent-server.network b/test/test-network/conf/agent-server.network new file mode 100644 index 00000000000..d58abdf271b --- /dev/null +++ b/test/test-network/conf/agent-server.network @@ -0,0 +1,10 @@ +[Match] +Name=server +[Network] +Address=192.168.5.1/24 +IPForward=ipv4 +DHCPServer=yes +[DHCPServer] +BindToInterface=no +PoolOffset=150 +PoolSize=1 diff --git a/test/test-network/conf/agent-veth-client.netdev b/test/test-network/conf/agent-veth-client.netdev new file mode 100644 index 00000000000..ace785f5cab --- /dev/null +++ b/test/test-network/conf/agent-veth-client.netdev @@ -0,0 +1,8 @@ +[NetDev] +Name=client +Kind=veth +MACAddress=12:34:56:78:9a:bc + +[Peer] +Name=client-peer +MACAddress=12:34:56:78:9a:bd diff --git a/test/test-network/conf/agent-veth-server.netdev b/test/test-network/conf/agent-veth-server.netdev new file mode 100644 index 00000000000..3a3d3931ba6 --- /dev/null +++ b/test/test-network/conf/agent-veth-server.netdev @@ -0,0 +1,8 @@ +[NetDev] +Name=server +Kind=veth +MACAddress=12:34:56:78:9b:bc + +[Peer] +Name=server-peer +MACAddress=12:34:56:78:9b:bd diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 28ffe3d8ba2..9d868186105 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -3689,6 +3689,43 @@ class NetworkdDHCPServerTests(unittest.TestCase, Utilities): self.assertRegex(output, '192.168.5.*') self.assertRegex(output, 'Europe/Berlin') +class NetworkdDHCPServerRelayAgentTests(unittest.TestCase, Utilities): + links = [ + 'client', + 'server', + 'client-peer', + 'server-peer', + ] + + units = [ + 'agent-veth-client.netdev', + 'agent-veth-server.netdev', + 'agent-client.network', + 'agent-server.network', + 'agent-client-peer.network', + 'agent-server-peer.network', + ] + + def setUp(self): + remove_links(self.links) + stop_networkd(show_logs=False) + + def tearDown(self): + remove_links(self.links) + remove_unit_from_networkd_path(self.units) + stop_networkd(show_logs=True) + + def test_relay_agent(self): + copy_unit_to_networkd_unit_path(*self.units) + start_networkd() + + #Test is disabled until BindToInterface DHCP server configuration option is supported + self.wait_online(['client:routable']) + + output = check_output(*networkctl_cmd, '-n', '0', 'status', 'client', env=env) + print(output) + self.assertRegex(output, 'Address: 192.168.5.150 \(DHCP4 via 192.168.5.1\)') + class NetworkdDHCPClientTests(unittest.TestCase, Utilities): links = [ 'veth99', -- 2.39.5