From 371c7c69bc5d0758dfa00af256ea0c1ed07149ee Mon Sep 17 00:00:00 2001 From: spoljak-ent Date: Thu, 7 Nov 2024 14:15:49 +0100 Subject: [PATCH] DHCPv6: Add support for sending Option 17 (VSIO) (#383) * DHCP: Add support for sending DHCP option 125 and DHCPv6 Option 17 (VSIO) Note wireshark doesn't decode option 125 correctly when the it needs to be split into more options if it exceeds 255 bytes. --------- Signed-off-by: Stipe Poljak (EXT) Co-authored-by: Roy Marples --- src/dhcp.c | 114 +++++++++++++++++++++++++++++ src/dhcp6.c | 90 +++++++++++++++++++++-- src/dhcpcd.conf.5.in | 38 +++++++++- src/if-options.c | 167 +++++++++++++++++++++++++++++++++++++++++++ src/if-options.h | 23 ++++++ 5 files changed, 426 insertions(+), 6 deletions(-) diff --git a/src/dhcp.c b/src/dhcp.c index 2dbdfe5f..11c69bc1 100644 --- a/src/dhcp.c +++ b/src/dhcp.c @@ -723,6 +723,76 @@ dhcp_message_add_addr(struct bootp *bootp, return 0; } +#ifndef SMALL +struct rfc3396_ctx { + uint8_t code; + uint8_t *len; + uint8_t **buf; + size_t buflen; +}; + +/* Encode data as a DHCP Long Option, RFC 3396. */ +/* NOTE: Wireshark does not decode this correctly + * when the option overflows the boundary and another option + * is created to hold the resta of the data. + * Tested against Wireshark-4.4.1 */ +#define RFC3396_BOUNDARY UINT8_MAX +static ssize_t +rfc3396_write(struct rfc3396_ctx *ctx, void *data, size_t len) +{ + uint8_t *datap = data; + size_t wlen, left, r = 0; + + while (len != 0) { + if (ctx->len == NULL || *ctx->len == RFC3396_BOUNDARY) { + if (ctx->buflen < 2) { + errno = ENOMEM; + return -1; + } + *(*ctx->buf)++ = ctx->code; + ctx->len = (*ctx->buf)++; + *ctx->len = 0; + r += 2; + } + + wlen = len < RFC3396_BOUNDARY ? len : RFC3396_BOUNDARY; + left = RFC3396_BOUNDARY - *ctx->len; + if (left < wlen) + wlen = left; + if (ctx->buflen < wlen) { + errno = ENOMEM; + return -1; + } + + memcpy(*ctx->buf, datap, wlen); + datap += wlen; + *ctx->buf += wlen; + ctx->buflen -= wlen; + *ctx->len = (uint8_t)(*ctx->len + wlen); + len -= wlen; + r += wlen; + } + + return (ssize_t)r; +} + +static ssize_t +rfc3396_write_byte(struct rfc3396_ctx *ctx, uint8_t byte) +{ + + return rfc3396_write(ctx, &byte, sizeof(byte)); +} + +static uint8_t * +rfc3396_zero(struct rfc3396_ctx *ctx) { + uint8_t *zerop = *ctx->buf, zero = 0; + + if (rfc3396_write(ctx, &zero, sizeof(zero)) == -1) + return NULL; + return zerop; +} +#endif + static ssize_t make_message(struct bootp **bootpm, const struct interface *ifp, uint8_t type) { @@ -1095,6 +1165,50 @@ make_message(struct bootp **bootpm, const struct interface *ifp, uint8_t type) } } +#ifndef SMALL + if (ifo->vsio_len && + !has_option_mask(ifo->nomask, DHO_VIVSO)) + { + struct vsio *vso = ifo->vsio; + size_t vlen = ifo->vsio_len; + struct vsio_so *so; + size_t slen; + struct rfc3396_ctx rctx = { + .code = DHO_VIVSO, + .buf = &p, + .buflen = AREA_LEFT, + }; + + for (; vlen > 0; vso++, vlen--) { + if (vso->so_len == 0) + continue; + + so = vso->so; + slen = vso->so_len; + + ul = htonl(vso->en); + if (rfc3396_write(&rctx, &ul, sizeof(ul)) == -1) + goto toobig; + lp = rfc3396_zero(&rctx); + if (lp == NULL) + goto toobig; + + for (; slen > 0; so++, slen--) { + if (rfc3396_write_byte(&rctx, + (uint8_t)so->opt) == -1) + goto toobig; + if (rfc3396_write_byte(&rctx, + (uint8_t)so->len) == -1) + goto toobig; + if (rfc3396_write(&rctx, + so->data, so->len) == -1) + goto toobig; + *lp = (uint8_t)(*lp + so->len + 2); + } + } + } +#endif + #ifdef AUTH if ((ifo->auth.options & DHCPCD_AUTH_SENDREQUIRE) != DHCPCD_AUTH_SENDREQUIRE && diff --git a/src/dhcp6.c b/src/dhcp6.c index 3fcb4b71..ca076e4e 100644 --- a/src/dhcp6.c +++ b/src/dhcp6.c @@ -204,6 +204,11 @@ static int dhcp6_hasprefixdelegation(struct interface *); !((ia)->flags & IPV6_AF_STALE) && \ (ia)->prefix_vltime != 0) + +/* Gets a pointer to the length part of the option to fill it + * in later. */ +#define NEXTLEN(p) ((p) + offsetof(struct dhcp6_option, len)) + void dhcp6_printoptions(const struct dhcpcd_ctx *ctx, const struct dhcp_opt *opts, size_t opts_len) @@ -337,6 +342,74 @@ dhcp6_makevendor(void *data, const struct interface *ifp) return sizeof(o) + len; } +#ifndef SMALL +/* DHCPv6 Option 17 (Vendor-Specific Information Option) */ +static size_t +dhcp6_makevendoropts(void *data, const struct interface *ifp) +{ + uint8_t *p = data, *olenp; + const struct if_options *ifo = ifp->options; + size_t len = 0, olen; + const struct vsio *vsio, *vsio_endp = ifo->vsio6 + ifo->vsio6_len; + const struct vsio_so *so, *so_endp; + struct dhcp6_option o; + uint32_t en; + uint16_t opt, slen; + + for (vsio = ifo->vsio6; vsio != vsio_endp; ++vsio) { + if (vsio->so_len == 0) + continue; + + if (p != NULL) { + olenp = NEXTLEN(p); + o.code = htons(D6_OPTION_VENDOR_OPTS); + o.len = 0; + memcpy(p, &o, sizeof(o)); + p += sizeof(o); + + en = htonl(vsio->en); + memcpy(p, &en, sizeof(en)); + p += sizeof(en); + } else + olenp = NULL; + + olen = sizeof(en); + + so_endp = vsio->so + vsio->so_len; + for (so = vsio->so; so != so_endp; so++) { + if (olen + sizeof(opt) + sizeof(slen) + + so->len > UINT16_MAX) + { + logerrx("%s: option too big", __func__); + break; + } + + if (p != NULL) { + opt = htons(so->opt); + memcpy(p, &opt, sizeof(opt)); + p += sizeof(opt); + slen = htons(so->len); + memcpy(p, &slen, sizeof(slen)); + p += sizeof(slen); + memcpy(p, so->data, so->len); + p += so->len; + } + + olen += sizeof(opt) + sizeof(slen) + so->len; + } + + if (olenp != NULL) { + slen = htons((uint16_t)olen); + memcpy(olenp, &slen, sizeof(slen)); + } + + len += sizeof(o) + olen; + } + + return len; +} +#endif + static void * dhcp6_findoption(void *data, size_t data_len, uint16_t code, uint16_t *len) { @@ -805,6 +878,11 @@ dhcp6_makemessage(struct interface *ifp) if (!has_option_mask(ifo->nomask6, D6_OPTION_VENDOR_CLASS)) len += dhcp6_makevendor(NULL, ifp); +#ifndef SMALL + if (!has_option_mask(ifo->nomask6, D6_OPTION_VENDOR_OPTS)) + len += dhcp6_makevendoropts(NULL, ifp); +#endif + /* IA */ m = NULL; ml = 0; @@ -950,7 +1028,6 @@ dhcp6_makemessage(struct interface *ifp) p += (_len); \ } \ } while (0 /* CONSTCOND */) -#define NEXTLEN (p + offsetof(struct dhcp6_option, len)) /* Options are listed in numerical order as per RFC 7844 Section 4.1 * XXX: They should be randomised. */ @@ -968,7 +1045,7 @@ dhcp6_makemessage(struct interface *ifp) for (l = 0; IA && l < ifo->ia_len; l++) { ifia = &ifo->ia[l]; - o_lenp = NEXTLEN; + o_lenp = NEXTLEN(p); /* TA structure is the same as the others, * it just lacks the T1 and T2 timers. * These happen to be at the end of the struct, @@ -1064,7 +1141,7 @@ dhcp6_makemessage(struct interface *ifp) state->send->type != DHCP6_DECLINE && n_options) { - o_lenp = NEXTLEN; + o_lenp = NEXTLEN(p); o.len = 0; COPYIN1(D6_OPTION_ORO, 0); for (l = 0, opt = ifp->ctx->dhcp6_opts; @@ -1125,11 +1202,16 @@ dhcp6_makemessage(struct interface *ifp) if (!has_option_mask(ifo->nomask6, D6_OPTION_VENDOR_CLASS)) p += dhcp6_makevendor(p, ifp); +#ifndef SMALL + if (!has_option_mask(ifo->nomask6, D6_OPTION_VENDOR_OPTS)) + p += dhcp6_makevendoropts(p, ifp); +#endif + if (state->send->type != DHCP6_RELEASE && state->send->type != DHCP6_DECLINE) { if (fqdn != FQDN_DISABLE) { - o_lenp = NEXTLEN; + o_lenp = NEXTLEN(p); COPYIN1(D6_OPTION_FQDN, 0); if (hl == 0) *p = D6_FQDN_NONE; diff --git a/src/dhcpcd.conf.5.in b/src/dhcpcd.conf.5.in index dacdc015..a67c68f7 100644 --- a/src/dhcpcd.conf.5.in +++ b/src/dhcpcd.conf.5.in @@ -24,7 +24,7 @@ .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF .\" SUCH DAMAGE. .\" -.Dd October 11, 2024 +.Dd November 1, 2024 .Dt DHCPCD.CONF 5 .Os .Sh NAME @@ -766,7 +766,7 @@ It should only be used for Microsoft DHCP servers and the should be set to "MSFT 98" or "MSFT 5.0". This option is not RFC compliant. .It Ic vendor Ar code , Ns Ar value -Add an encapsulated vendor option. +Add an encapsulated vendor-specific information option (DHCP Option 43). .Ar code should be between 1 and 254 inclusive. To add a raw vendor string, omit @@ -782,6 +782,40 @@ Set the vendor option 03 with an IP address as a string. .D1 vendor 03,\e"192.168.0.2\e" Set un-encapsulated vendor option to hello world. .D1 vendor ,"hello world" +.It Ic vsio Ar en Ar code, Ns Ar value +Add an encapsulated vendor-specific information option (DHCP Option 125) with +IANA assigned Enterprise Number +.Ar en +proceeding with the +.Ar code +which should be between 1 and 255 inclusive, and the +.Ar value +after the comma. +Examples: +.Pp +Set the vsio for enterprise number 155 option 01 with an IPv4 address. +.D1 vsio 155 01,192.168.1.1 +Set the vsio for enterprise number 155 option 02 with a string. +.D1 vsio 155 02,"hello world" +Set the vsio for enterprise number 255 option 01 with a hex code. +.D1 vsio 255 01,01:02:03:04:05 +.It Ic vsio6 Ar en Ar code, Ns Ar value +Add an encapsulated vendor-specific information option (DHCPv6 Option 17) with +IANA assigned Enterprise Number +.Ar en +proceeding with the +.Ar code +which should be between 1 and 65535 inclusive, and the +.Ar value +after the comma. +Examples: +.Pp +Set the vsio for enterprise number 155 option 01 with an IPv6 address. +.D1 vsio6 155 01,2001:0db8:85a3:0000:0000:8a2e:0370:7334 +Set the vsio for enterprise number 155 option 02 with a string. +.D1 vsio6 155 02,"hello world" +Set the vsio for enterprise number 255 option 01 with a hex code. +.D1 vsio6 255 01,01:02:03:04:05 .It Ic vendorclassid Ar string Set the DHCP Vendor Class. DHCPv6 has its own option as shown below. diff --git a/src/if-options.c b/src/if-options.c index 85de5330..c50f65a1 100644 --- a/src/if-options.c +++ b/src/if-options.c @@ -87,6 +87,8 @@ const struct option cf_options[] = { #ifndef SMALL {"msuserclass", required_argument, NULL, O_MSUSERCLASS}, #endif + {"vsio", required_argument, NULL, O_VSIO}, + {"vsio6", required_argument, NULL, O_VSIO6}, {"vendor", required_argument, NULL, 'v'}, {"waitip", optional_argument, NULL, 'w'}, {"exit", no_argument, NULL, 'x'}, @@ -666,7 +668,11 @@ parse_option(struct dhcpcd_ctx *ctx, const char *ifname, struct if_options *ifo, struct if_ia *ia; uint8_t iaid[4]; #ifndef SMALL + struct in6_addr in6addr; struct if_sla *sla, *slap; + struct vsio **vsiop = NULL, *vsio; + size_t *vsio_lenp = NULL, opt_max, opt_header; + struct vsio_so *vsio_so; #endif #endif @@ -897,6 +903,139 @@ parse_option(struct dhcpcd_ctx *ctx, const char *ifname, struct if_options *ifo, ifo->userclass[0] = (uint8_t)s; break; #endif + + case O_VSIO: +#ifndef SMALL + vsiop = &ifo->vsio; + vsio_lenp = &ifo->vsio_len; + opt_max = UINT8_MAX; + opt_header = sizeof(uint8_t) + sizeof(uint8_t); +#endif + /* FALLTHROUGH */ + case O_VSIO6: +#ifndef SMALL + if (vsiop == NULL) { + vsiop = &ifo->vsio6; + vsio_lenp = &ifo->vsio6_len; + opt_max = UINT16_MAX; + opt_header = sizeof(uint16_t) + sizeof(uint16_t); + } +#endif + ARG_REQUIRED; +#ifdef SMALL + logwarnx("%s: vendor options not compiled in", ifname); + return -1; +#else + fp = strwhite(arg); + if (fp) + *fp++ = '\0'; + u = (uint32_t)strtou(arg, NULL, 0, 0, UINT32_MAX, &e); + if (e) { + logerrx("invalid code: %s", arg); + return -1; + } + + fp = strskipwhite(fp); + p = strchr(fp, ','); + if (!p || !p[1]) { + logerrx("invalid vendor format: %s", arg); + return -1; + } + + /* Strip and preserve the comma */ + *p = '\0'; + i = (int)strtoi(fp, NULL, 0, 1, (intmax_t)opt_max, &e); + *p = ','; + if (e) { + logerrx("vendor option should be between" + " 1 and %zu inclusive", opt_max); + return -1; + } + + fp = p + 1; + + if (fp) { + if (inet_pton(AF_INET, fp, &addr) == 1) { + s = sizeof(addr.s_addr); + dl = (size_t)s; + np = malloc(dl); + if (np == NULL) { + logerr(__func__); + return -1; + } + memcpy(np, &addr.s_addr, dl); + } else if (inet_pton(AF_INET6, fp, &in6addr) == 1) { + s = sizeof(in6addr.s6_addr); + dl = (size_t)s; + np = malloc(dl); + if (np == NULL) { + logerr(__func__); + return -1; + } + memcpy(np, &in6addr.s6_addr, dl); + } else { + s = parse_string(NULL, 0, fp); + if (s == -1) { + logerr(__func__); + return -1; + } + dl = (size_t)s; + np = malloc(dl); + if (np == NULL) { + logerr(__func__); + return -1; + } + parse_string(np, dl, fp); + } + } else { + dl = 0; + np = NULL; + } + + for (sl = 0, vsio = *vsiop; sl < *vsio_lenp; sl++, vsio++) { + if (vsio->en == (uint32_t)u) + break; + } + if (sl == *vsio_lenp) { + vsio = reallocarray(*vsiop, *vsio_lenp + 1, + sizeof(**vsiop)); + if (vsio == NULL) { + logerr("%s: reallocarray vsio", __func__); + free(np); + return -1; + } + *vsiop = vsio; + vsio = &(*vsiop)[(*vsio_lenp)++]; + vsio->en = (uint32_t)u; + vsio->so = NULL; + vsio->so_len = 0; + } + + for (sl = 0, vsio_so = vsio->so; + sl < vsio->so_len; + sl++, vsio_so++) + opt_max -= opt_header + vsio_so->len; + if (opt_header + dl > opt_max) { + logerrx("vsio is too big: %s", fp); + free(np); + return -1; + } + + vsio_so = reallocarray(vsio->so, vsio->so_len + 1, + sizeof(*vsio_so)); + if (vsio_so == NULL) { + logerr("%s: reallocarray vsio_so", __func__); + free(np); + return -1; + } + + vsio->so = vsio_so; + vsio_so = &vsio->so[vsio->so_len++]; + vsio_so->opt = (uint16_t)i; + vsio_so->len = (uint16_t)dl; + vsio_so->data = np; + break; +#endif case 'v': ARG_REQUIRED; p = strchr(arg, ','); @@ -2801,6 +2940,10 @@ free_options(struct dhcpcd_ctx *ctx, struct if_options *ifo) #ifdef AUTH struct token *token; #endif +#ifndef SMALL + struct vsio *vsio; + struct vsio_so *vsio_so; +#endif if (ifo == NULL) return; @@ -2856,6 +2999,30 @@ free_options(struct dhcpcd_ctx *ctx, struct if_options *ifo) vo++, ifo->vivco_len--) free(vo->data); free(ifo->vivco); +#ifndef SMALL + for (vsio = ifo->vsio; + ifo->vsio_len > 0; + vsio++, ifo->vsio_len--) + { + for (vsio_so = vsio->so; + vsio->so_len > 0; + vsio_so++, vsio->so_len--) + free(vsio_so->data); + free(vsio->so); + } + free(ifo->vsio); + for (vsio = ifo->vsio6; + ifo->vsio6_len > 0; + vsio++, ifo->vsio6_len--) + { + for (vsio_so = vsio->so; + vsio->so_len > 0; + vsio_so++, vsio->so_len--) + free(vsio_so->data); + free(vsio->so); + } + free(ifo->vsio6); +#endif for (opt = ifo->vivso_override; ifo->vivso_override_len > 0; opt++, ifo->vivso_override_len--) diff --git a/src/if-options.h b/src/if-options.h index b77fc302..f3b9c17a 100644 --- a/src/if-options.h +++ b/src/if-options.h @@ -61,6 +61,7 @@ #define USERCLASS_MAX_LEN 255 #define VENDOR_MAX_LEN 255 #define MUDURL_MAX_LEN 255 +#define ENTERPRISE_NUMS_MAX_LEN 255 #define DHCPCD_ARP (1ULL << 0) #define DHCPCD_RELEASE (1ULL << 1) @@ -191,6 +192,8 @@ #define O_REQUEST_TIME O_BASE + 54 #define O_FALLBACK_TIME O_BASE + 55 #define O_IPV4LL_TIME O_BASE + 56 +#define O_VSIO O_BASE + 57 +#define O_VSIO6 O_BASE + 58 extern const struct option cf_options[]; @@ -222,6 +225,19 @@ struct vivco { uint8_t *data; }; +#ifndef SMALL +struct vsio_so { + uint16_t opt; + uint16_t len; + void *data; +}; +struct vsio { + uint32_t en; + size_t so_len; + struct vsio_so *so; +}; +#endif + struct if_options { time_t mtime; uint8_t iaid[4]; @@ -293,6 +309,13 @@ struct if_options { struct dhcp_opt *vivso_override; size_t vivso_override_len; +#ifndef SMALL + size_t vsio_len; + struct vsio *vsio; + size_t vsio6_len; + struct vsio *vsio6; +#endif + struct auth auth; };