From fc9f6985abf2591ce42d01dc3dce627f1837eaef Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Fri, 25 Jul 2025 18:47:33 +0100 Subject: [PATCH] Add --dhcp-split-relay option. This makes a DHCPv4 relay which is functional when client and server networks aren't mutually route-able. --- CHANGELOG | 4 + man/dnsmasq.8 | 23 +++++- src/dhcp-common.c | 4 +- src/dhcp-protocol.h | 1 + src/dhcp.c | 122 +-------------------------- src/dnsmasq.h | 3 + src/option.c | 21 ++++- src/rfc2131.c | 196 +++++++++++++++++++++++++++++++++++++++++++- 8 files changed, 247 insertions(+), 127 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8977dc7..8d4b289 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -66,6 +66,10 @@ version 2.92 lease is created _only_ when the --dhcp-authoritative option is set. This matches the behavior of the DHCPv4 server. + Add --dhcp-split-relay option. This makes a DHCPv4 relay which + is functional when client and server networks aren't mutually + route-able. + version 2.91 Fix spurious "resource limit exceeded messages". Thanks to diff --git a/man/dnsmasq.8 b/man/dnsmasq.8 index 8b29556..d900d2a 100644 --- a/man/dnsmasq.8 +++ b/man/dnsmasq.8 @@ -1487,7 +1487,28 @@ DHCPv4 to a DHCPv6 server or vice-versa. The DHCP relay function for IPv6 includes the ability to snoop prefix-delegation from relayed DHCP transactions. See .B --dhcp-script -for details. +for details. +.TP +.B --dhcp-split-relay=,[[#]], +A usefully enchanced version of DHCPv4 relay. IPv4 DHCP normally uses a single address +for two functions; it is used by the DHCP server to determine which network to allocate +an address on, and it is used as the address of the relay to which the server sends packets. + +This version of DHCP relay splits these functions. It uses the address of the server-facing relay +interface as the address that the server talks to. The address of the client-facing interface +(the first item in the config) is used as to determine the client's subnet. The +local address is also used as server-ID override so that the client always sends requests +via the relay. The effect of this is that server doesn't require +a route to the client network and the clients don't require a route to the server. + +The interface parameter is mandatory and a cannot be a wildcard. + +If setting up a network where the client networks have limited routing, be careful +about configuring the DHCP server. Dnsmasq, as DHCP server, will set the default route to the +client-facing relay interface unless explicitly configured: that is a sensible default. +The normal default DNS server (the same address as the DHCP server) +will not be appropriate when there is no route bewteen the +two, so this will have to be explicitly configured. .TP .B \-U, --dhcp-vendorclass=set:,[enterprise:,] Map from a vendor-class string to a tag. Most DHCP clients provide a diff --git a/src/dhcp-common.c b/src/dhcp-common.c index d421e86..d1d3eb0 100644 --- a/src/dhcp-common.c +++ b/src/dhcp-common.c @@ -1067,10 +1067,12 @@ void log_relay(int family, struct dhcp_relay *relay) { if (broadcast) my_syslog(MS_DHCP | LOG_INFO, _("DHCP relay from %s via %s"), daemon->addrbuff, relay->interface); + else if (relay->split_mode) + my_syslog(MS_DHCP | LOG_INFO, _("DHCP split-relay from %s to %s via %s"), daemon->addrbuff, daemon->namebuff, relay->interface); else my_syslog(MS_DHCP | LOG_INFO, _("DHCP relay from %s to %s via %s"), daemon->addrbuff, daemon->namebuff, relay->interface); } - else + else my_syslog(MS_DHCP | LOG_INFO, _("DHCP relay from %s to %s"), daemon->addrbuff, daemon->namebuff); } diff --git a/src/dhcp-protocol.h b/src/dhcp-protocol.h index 78849eb..72c420d 100644 --- a/src/dhcp-protocol.h +++ b/src/dhcp-protocol.h @@ -73,6 +73,7 @@ #define SUBOPT_REMOTE_ID 2 #define SUBOPT_SUBNET_SELECT 5 /* RFC 3527 */ #define SUBOPT_SUBSCR_ID 6 /* RFC 3393 */ +#define SUBOPT_FLAGS 10 /* RFC 5010 */ #define SUBOPT_SERVER_OR 11 /* RFC 5107 */ #define SUBOPT_PXE_BOOT_ITEM 71 /* PXE standard */ diff --git a/src/dhcp.c b/src/dhcp.c index 35541de..5aa9306 100644 --- a/src/dhcp.c +++ b/src/dhcp.c @@ -32,8 +32,6 @@ static int complete_context(struct in_addr local, int if_index, char *label, struct in_addr netmask, struct in_addr broadcast, void *vparam); static int check_listen_addrs(struct in_addr local, int if_index, char *label, struct in_addr netmask, struct in_addr broadcast, void *vparam); -static void relay_upstream4(int iface_index, struct dhcp_packet *mess, size_t sz); -static struct dhcp_relay *relay_reply4(struct dhcp_packet *mess, char *arrival_interface); static int make_fd(int port) { @@ -344,7 +342,7 @@ void dhcp_packet(time_t now, int pxe_fd) if (!iface_enumerate(AF_INET, &parm, (callback_t){.af_inet=complete_context})) return; - relay_upstream4(iface_index, mess, (size_t)sz); + relay_upstream4(iface_index, mess, (size_t)sz, unicast_dest); /* May have configured relay, but not DHCP server */ if (!daemon->dhcp) @@ -1091,122 +1089,4 @@ char *host_from_dns(struct in_addr addr) return NULL; } -static void relay_upstream4(int iface_index, struct dhcp_packet *mess, size_t sz) -{ - struct in_addr giaddr = mess->giaddr; - u8 hops = mess->hops; - struct dhcp_relay *relay; - - if (mess->op != BOOTREQUEST) - return; - - for (relay = daemon->relay4; relay; relay = relay->next) - if (relay->iface_index != 0 && relay->iface_index == iface_index) - break; - - /* No relay config. */ - if (!relay) - return; - - for (; relay; relay = relay->next) - if (relay->iface_index != 0 && relay->iface_index == iface_index) - { - union mysockaddr to; - union all_addr from; - - mess->hops = hops; - mess->giaddr = giaddr; - - if ((mess->hops++) > 20) - continue; - - /* source address == relay address */ - from.addr4 = relay->local.addr4; - - /* already gatewayed ? */ - if (giaddr.s_addr) - { - /* if so check if by us, to stomp on loops. */ - if (giaddr.s_addr == relay->local.addr4.s_addr) - continue; - } - else - { - /* plug in our address */ - mess->giaddr.s_addr = relay->local.addr4.s_addr; - } - - to.sa.sa_family = AF_INET; - to.in.sin_addr = relay->server.addr4; - to.in.sin_port = htons(relay->port); -#ifdef HAVE_SOCKADDR_SA_LEN - to.in.sin_len = sizeof(struct sockaddr_in); -#endif - - /* Broadcasting to server. */ - if (relay->server.addr4.s_addr == 0) - { - struct ifreq ifr; - - if (relay->interface) - safe_strncpy(ifr.ifr_name, relay->interface, IF_NAMESIZE); - - if (!relay->interface || strchr(relay->interface, '*') || - ioctl(daemon->dhcpfd, SIOCGIFBRDADDR, &ifr) == -1) - { - my_syslog(MS_DHCP | LOG_ERR, _("Cannot broadcast DHCP relay via interface %s"), relay->interface); - continue; - } - - to.in.sin_addr = ((struct sockaddr_in *) &ifr.ifr_addr)->sin_addr; - } - -#ifdef HAVE_DUMPFILE - { - union mysockaddr fromsock; - fromsock.in.sin_port = htons(daemon->dhcp_server_port); - fromsock.in.sin_addr = from.addr4; - fromsock.sa.sa_family = AF_INET; - - dump_packet_udp(DUMP_DHCP, (void *)mess, sz, &fromsock, &to, -1); - } -#endif - - send_from(daemon->dhcpfd, 0, (char *)mess, sz, &to, &from, 0); - - if (option_bool(OPT_LOG_OPTS)) - { - inet_ntop(AF_INET, &relay->local, daemon->addrbuff, ADDRSTRLEN); - if (relay->server.addr4.s_addr == 0) - snprintf(daemon->dhcp_buff2, DHCP_BUFF_SZ, _("broadcast via %s"), relay->interface); - else - inet_ntop(AF_INET, &relay->server.addr4, daemon->dhcp_buff2, DHCP_BUFF_SZ); - my_syslog(MS_DHCP | LOG_INFO, _("DHCP relay at %s -> %s"), daemon->addrbuff, daemon->dhcp_buff2); - } - } - - /* restore in case of a local reply. */ - mess->giaddr = giaddr; -} - - -static struct dhcp_relay *relay_reply4(struct dhcp_packet *mess, char *arrival_interface) -{ - struct dhcp_relay *relay; - - if (mess->giaddr.s_addr == 0 || mess->op != BOOTREPLY) - return NULL; - - for (relay = daemon->relay4; relay; relay = relay->next) - { - if (mess->giaddr.s_addr == relay->local.addr4.s_addr) - { - if (!relay->interface || wildcard_match(relay->interface, arrival_interface)) - return relay->iface_index != 0 ? relay : NULL; - } - } - - return NULL; -} - #endif diff --git a/src/dnsmasq.h b/src/dnsmasq.h index d0faf06..df071a9 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -1148,6 +1148,7 @@ struct dhcp_relay { char *interface; /* Allowable interface for replies from server, and dest for IPv6 multicast */ int iface_index; /* working - interface in which requests arrived, for return */ int port; /* Port of relay we forward to. */ + int split_mode; /* Split address allocation and relay address. */ #ifdef HAVE_SCRIPT struct snoop_record { struct in6_addr client, prefix; @@ -1666,6 +1667,8 @@ size_t dhcp_reply(struct dhcp_context *context, char *iface_name, int int_index, time_t recvtime, struct in_addr leasequery_source); unsigned char *extended_hwaddr(int hwtype, int hwlen, unsigned char *hwaddr, int clid_len, unsigned char *clid, int *len_out); +void relay_upstream4(int iface_index, struct dhcp_packet *mess, size_t sz, int unicast); +struct dhcp_relay *relay_reply4(struct dhcp_packet *mess, char *arrival_interface); #endif /* dnsmasq.c */ diff --git a/src/option.c b/src/option.c index 85dd202..48d6880 100644 --- a/src/option.c +++ b/src/option.c @@ -196,6 +196,7 @@ struct myoption { #define LOPT_NO_ENCODE 387 #define LOPT_DO_ENCODE 388 #define LOPT_LEASEQUERY 389 +#define LOPT_SPLIT_RELAY 390 #ifdef HAVE_GETOPT_LONG static const struct option opts[] = @@ -374,6 +375,7 @@ static const struct myoption opts[] = { "dnssec-timestamp", 1, 0, LOPT_DNSSEC_STAMP }, { "dnssec-limits", 1, 0, LOPT_DNSSEC_LIMITS }, { "dhcp-relay", 1, 0, LOPT_RELAY }, + { "dhcp-split-relay", 1, 0, LOPT_SPLIT_RELAY }, { "ra-param", 1, 0, LOPT_RA_PARAM }, { "quiet-dhcp", 0, 0, LOPT_QUIET_DHCP }, { "quiet-dhcp6", 0, 0, LOPT_QUIET_DHCP6 }, @@ -542,6 +544,7 @@ static struct { { LOPT_GEN_NAMES, ARG_DUP, "[=tag:]", gettext_noop("Generate hostnames based on MAC address for nameless clients."), NULL}, { LOPT_PROXY, ARG_DUP, "[=]...", gettext_noop("Use these DHCP relays as full proxies."), NULL }, { LOPT_RELAY, ARG_DUP, ",[,]", gettext_noop("Relay DHCP requests to a remote server"), NULL}, + { LOPT_SPLIT_RELAY, ARG_DUP, ",,", gettext_noop("Relay DHCP requests to a remote server"), NULL}, { LOPT_CNAME, ARG_DUP, ",[,]", gettext_noop("Specify alias name for LOCAL DNS name."), NULL }, { LOPT_PXE_PROMT, ARG_DUP, ",[]", gettext_noop("Prompt to send to PXE clients."), NULL }, { LOPT_PXE_SERV, ARG_DUP, "", gettext_noop("Boot service for PXE menu."), NULL }, @@ -4716,11 +4719,21 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma break; case LOPT_RELAY: /* --dhcp-relay */ + case LOPT_SPLIT_RELAY: /* --dhcp-splt-relay */ { struct dhcp_relay *new = opt_malloc(sizeof(struct dhcp_relay)); char *two = split(arg); char *three = split(two); - + + if (option == LOPT_SPLIT_RELAY) + { + new->split_mode = 1; + + /* split mode must have two addresses and a non-wildcard interface name. */ + if (!three || strchr(three, '*')) + two = NULL; + } + new->iface_index = 0; if (two) @@ -4748,7 +4761,7 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma daemon->relay4 = new; } #ifdef HAVE_DHCP6 - else if (inet_pton(AF_INET6, arg, &new->local)) + else if (inet_pton(AF_INET6, arg, &new->local) && !new->split_mode) { char *hash = split_chr(two, '#'); @@ -4769,7 +4782,9 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma daemon->relay6 = new; } #endif - + else + two = NULL; + new->interface = opt_string_alloc(three); } diff --git a/src/rfc2131.c b/src/rfc2131.c index c736fef..710b173 100644 --- a/src/rfc2131.c +++ b/src/rfc2131.c @@ -204,6 +204,10 @@ size_t dhcp_reply(struct dhcp_context *context, char *iface_name, int int_index, memcpy(agent_id, opt, total); } + /* look for RFC5010 flags sub-option */ + if ((sopt = option_find1(option_ptr(opt, 0), option_ptr(opt, option_len(opt)), SUBOPT_FLAGS, INADDRSZ))) + unicast_dest = !!(option_uint(opt, 0, 1) & 0x80); + /* look for RFC3527 Link selection sub-option */ if ((sopt = option_find1(option_ptr(opt, 0), option_ptr(opt, option_len(opt)), SUBOPT_SUBNET_SELECT, INADDRSZ))) subnet_addr = option_addr(sopt); @@ -289,6 +293,8 @@ size_t dhcp_reply(struct dhcp_context *context, char *iface_name, int int_index, { addr = subnet_addr; force = 1; + if (mess->giaddr.s_addr) + via_relay = 1; } else if (mess->giaddr.s_addr) { @@ -357,7 +363,12 @@ size_t dhcp_reply(struct dhcp_context *context, char *iface_name, int int_index, if (context_tmp->local.s_addr == 0) context_tmp->local = fallback; if (context_tmp->router.s_addr == 0 && !share) - context_tmp->router = mess->giaddr; + { + if (override.s_addr) + context_tmp->router = override; + else + context_tmp->router = mess->giaddr; + } /* fill in missing broadcast addresses for relayed ranges */ if (!(context_tmp->flags & CONTEXT_BRDCAST) && context_tmp->broadcast.s_addr == 0 ) @@ -3044,4 +3055,187 @@ static void apply_delay(u32 xid, time_t recvtime, struct dhcp_netid *netid) } } +void relay_upstream4(int iface_index, struct dhcp_packet *mess, size_t sz, int unicast) +{ + struct in_addr giaddr = mess->giaddr; + u8 hops = mess->hops; + struct dhcp_relay *relay; + size_t orig_sz = sz; + unsigned char *endopt = NULL; + + if (mess->op != BOOTREQUEST) + return; + + for (relay = daemon->relay4; relay; relay = relay->next) + if (relay->iface_index != 0 && relay->iface_index == iface_index) + { + union mysockaddr to; + union all_addr from; + struct ifreq ifr; + + /* restore orig packet */ + mess->hops = hops; + mess->giaddr = giaddr; + if (endopt) + *endopt = OPTION_END; + sz = orig_sz; + + if ((mess->hops++) > 20) + continue; + + if (relay->interface) + { + safe_strncpy(ifr.ifr_name, relay->interface, IF_NAMESIZE); + ifr.ifr_addr.sa_family = AF_INET; + } + + if (!relay->split_mode) + { + /* already gatewayed ? */ + if (giaddr.s_addr) + { + /* if so check if by us, to stomp on loops. */ + if (giaddr.s_addr == relay->local.addr4.s_addr) + continue; + } + + /* plug in our address */ + from.addr4 = mess->giaddr = relay->local.addr4; + } + else + { + /* Split mode. We put our address on the server-facing interface + into giaddr for the server to talk back to us on. + + Our address on client-facing interface goes into agent-id + subnet-selector subopt, so that the server allocates the correct address. */ + + /* get our address on the server-facing interface. */ + if (ioctl(daemon->dhcpfd, SIOCGIFADDR, &ifr) == -1) + continue; + + /* already gatewayed ? */ + if (giaddr.s_addr) + { + /* if so check if by us, to stomp on loops. */ + if (giaddr.s_addr == ((struct sockaddr_in *) &ifr.ifr_addr)->sin_addr.s_addr) + continue; + } + + /* giaddr is our address on the outgoing interface in split mode. */ + from.addr4 = mess->giaddr = ((struct sockaddr_in *) &ifr.ifr_addr)->sin_addr; + + if (!endopt) + { + /* Add an RFC3026 relay agent information option (2 bytes) at the very end of the options. + Said option to contain a RFC 3527 link selection sub option (6 bytes) and + RFC 5017 serverid-override option (6 bytes) and RFC5010 (3 bytes). + New END option is a 18th byte, so we need 18 bytes free. + We only need to do this once, and poke the address into the same place each time. */ + + if (!(endopt = option_find1((&mess->options[0] + sizeof(u32)), ((unsigned char *)mess) + sz, OPTION_END, 0)) || + (endopt + 18 > (unsigned char *)(mess + 1))) + continue; + + endopt[1] = 15; /* length */ + endopt[2] = SUBOPT_SUBNET_SELECT; + endopt[3] = 4; /* length */ + endopt[8] = SUBOPT_SERVER_OR; + endopt[9] = 4; + endopt[14] = SUBOPT_FLAGS; + endopt[15] = 1; /* length */ + endopt[17] = OPTION_END; + sz = (endopt - (unsigned char *)mess) + 18; + } + + /* IP address is already in network byte order */ + memcpy(&endopt[4], &relay->local.addr4.s_addr, INADDRSZ); + memcpy(&endopt[10], &relay->local.addr4.s_addr, INADDRSZ); + endopt[16] = unicast ? 0x80 : 0x00; + endopt[0] = OPTION_AGENT_ID; + } + + to.sa.sa_family = AF_INET; + to.in.sin_addr = relay->server.addr4; + to.in.sin_port = htons(relay->port); +#ifdef HAVE_SOCKADDR_SA_LEN + to.in.sin_len = sizeof(struct sockaddr_in); +#endif + + /* Broadcasting to server. */ + if (relay->server.addr4.s_addr == 0) + { + if (!relay->interface || strchr(relay->interface, '*') || + ioctl(daemon->dhcpfd, SIOCGIFBRDADDR, &ifr) == -1) + { + my_syslog(MS_DHCP | LOG_ERR, _("Cannot broadcast DHCP relay via interface %s"), relay->interface); + continue; + } + + to.in.sin_addr = ((struct sockaddr_in *) &ifr.ifr_addr)->sin_addr; + } + +#ifdef HAVE_DUMPFILE + { + union mysockaddr fromsock; + fromsock.in.sin_port = htons(daemon->dhcp_server_port); + fromsock.in.sin_addr = from.addr4; + fromsock.sa.sa_family = AF_INET; + + dump_packet_udp(DUMP_DHCP, (void *)mess, sz, &fromsock, &to, -1); + } +#endif + + send_from(daemon->dhcpfd, 0, (char *)mess, sz, &to, &from, 0); + + if (option_bool(OPT_LOG_OPTS)) + { + inet_ntop(AF_INET, &relay->local, daemon->addrbuff, ADDRSTRLEN); + if (relay->server.addr4.s_addr == 0) + snprintf(daemon->dhcp_buff2, DHCP_BUFF_SZ, _("broadcast via %s"), relay->interface); + else + inet_ntop(AF_INET, &relay->server.addr4, daemon->dhcp_buff2, DHCP_BUFF_SZ); + my_syslog(MS_DHCP | LOG_INFO, _("DHCP relay at %s -> %s"), daemon->addrbuff, daemon->dhcp_buff2); + } + } + + /* restore in case of a local reply. */ + mess->hops = hops; + mess->giaddr = giaddr; + if (endopt) + *endopt = OPTION_END; +} + +struct dhcp_relay *relay_reply4(struct dhcp_packet *mess, char *arrival_interface) +{ + struct dhcp_relay *relay; + + if (mess->giaddr.s_addr == 0 || mess->op != BOOTREPLY) + return NULL; + + for (relay = daemon->relay4; relay; relay = relay->next) + { + if (relay->split_mode) + { + struct ifreq ifr; + + safe_strncpy(ifr.ifr_name, arrival_interface, IF_NAMESIZE); + ifr.ifr_addr.sa_family = AF_INET; + + /* giaddr is our address on the returning interface in split mode. */ + if (ioctl(daemon->dhcpfd, SIOCGIFADDR, &ifr) == -1 || + mess->giaddr.s_addr != ((struct sockaddr_in *) &ifr.ifr_addr)->sin_addr.s_addr) + continue; + } + else if (mess->giaddr.s_addr != relay->local.addr4.s_addr) + continue; + + if (!relay->interface || wildcard_match(relay->interface, arrival_interface)) + return relay->iface_index != 0 ? relay : NULL; + } + + return NULL; +} + + #endif /* HAVE_DHCP */