git: 8f4a0d2f7b96 - main - libc: Import OpenBSD's inet_net_{ntop,pton}

From: Lexi Winter <ivy_at_FreeBSD.org>
Date: Sat, 18 Oct 2025 14:46:32 UTC
The branch main has been updated by ivy:

URL: https://cgit.FreeBSD.org/src/commit/?id=8f4a0d2f7b96099001dbc51e06114df1a0e6d291

commit 8f4a0d2f7b96099001dbc51e06114df1a0e6d291
Author:     Lexi Winter <ivy@FreeBSD.org>
AuthorDate: 2025-10-18 14:18:40 +0000
Commit:     Lexi Winter <ivy@FreeBSD.org>
CommitDate: 2025-10-18 14:45:42 +0000

    libc: Import OpenBSD's inet_net_{ntop,pton}
    
    Our versions of these functions (originally taken from BIND) simply
    don't work correctly for AF_INET6.  These were removed from BIND itself
    quite a while ago, but OpenBSD has made several fixes in the mean time,
    so import their code.
    
    Add tests for both functions.
    
    PR:             289198
    Reported by:    Nico Sonack <nsonack@herrhotzenplotz.de>
    MFC after:      1 week
    Reviewed by:    des
    Obtained from:  OpenBSD (lib/libc/net)
    Sponsored by:   https://www.patreon.com/bsdivy
    Differential Revision:  https://reviews.freebsd.org/D52629
---
 lib/libc/inet/inet_net_ntop.c       | 206 ++++++--------------
 lib/libc/inet/inet_net_pton.c       | 363 +++++++++++-------------------------
 lib/libc/tests/net/Makefile         |   2 +
 lib/libc/tests/net/inet_net_test.cc | 333 +++++++++++++++++++++++++++++++++
 4 files changed, 492 insertions(+), 412 deletions(-)

diff --git a/lib/libc/inet/inet_net_ntop.c b/lib/libc/inet/inet_net_ntop.c
index 9d98dbb5ca99..30dd5c0571f2 100644
--- a/lib/libc/inet/inet_net_ntop.c
+++ b/lib/libc/inet/inet_net_ntop.c
@@ -1,20 +1,23 @@
-/*-
- * SPDX-License-Identifier: ISC
+/*	$OpenBSD: inet_net_ntop.c,v 1.9 2019/07/03 03:24:04 deraadt Exp $	*/
+
+/*
+ * Copyright (c) 2012 by Gilles Chehade <gilles@openbsd.org>
+ * Copyright (c) 1996 by Internet Software Consortium.
  *
- * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
- * Copyright (c) 1996,1999 by Internet Software Consortium.
+ * SPDX-License-Identifier: ISC
  *
  * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
- * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
- * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS
+ * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE
+ * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+ * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+ * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+ * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
+ * SOFTWARE.
  */
 
 #include "port_before.h"
@@ -31,18 +34,10 @@
 
 #include "port_after.h"
 
-#ifdef SPRINTF_CHAR
-# define SPRINTF(x) strlen(sprintf/**/x)
-#else
-# define SPRINTF(x) ((size_t)sprintf x)
-#endif
-
-static char *	inet_net_ntop_ipv4(const u_char *src, int bits, char *dst,
-		    size_t size);
-static char *	inet_net_ntop_ipv6(const u_char *src, int bits, char *dst,
-		    size_t size);
+static char *inet_net_ntop_ipv4(const u_char *, int, char *, size_t);
+static char *inet_net_ntop_ipv6(const u_char *, int, char *, size_t);
 
-/*%
+/*
  * char *
  * inet_net_ntop(af, src, bits, dst, size)
  *	convert network number from network to presentation format.
@@ -66,7 +61,7 @@ inet_net_ntop(int af, const void *src, int bits, char *dst, size_t size)
 	}
 }
 
-/*%
+/*
  * static char *
  * inet_net_ntop_ipv4(src, bits, dst, size)
  *	convert IPv4 network number from network to presentation format.
@@ -83,53 +78,63 @@ static char *
 inet_net_ntop_ipv4(const u_char *src, int bits, char *dst, size_t size)
 {
 	char *odst = dst;
-	char *t;
 	u_int m;
 	int b;
+	char *ep;
+	int advance;
+
+	ep = dst + size;
+	if (ep <= dst)
+		goto emsgsize;
 
 	if (bits < 0 || bits > 32) {
 		errno = EINVAL;
 		return (NULL);
 	}
-
 	if (bits == 0) {
-		if (size < sizeof "0")
+		if (ep - dst < sizeof "0")
 			goto emsgsize;
 		*dst++ = '0';
-		size--;
 		*dst = '\0';
 	}
 
 	/* Format whole octets. */
 	for (b = bits / 8; b > 0; b--) {
-		if (size <= sizeof "255.")
+		if (ep - dst < sizeof "255.")
+			goto emsgsize;
+		advance = snprintf(dst, ep - dst, "%u", *src++);
+		if (advance <= 0 || advance >= ep - dst)
 			goto emsgsize;
-		t = dst;
-		dst += SPRINTF((dst, "%u", *src++));
+		dst += advance;
 		if (b > 1) {
+			if (dst + 1 >= ep)
+				goto emsgsize;
 			*dst++ = '.';
 			*dst = '\0';
 		}
-		size -= (size_t)(dst - t);
 	}
 
 	/* Format partial octet. */
 	b = bits % 8;
 	if (b > 0) {
-		if (size <= sizeof ".255")
+		if (ep - dst < sizeof ".255")
 			goto emsgsize;
-		t = dst;
 		if (dst != odst)
 			*dst++ = '.';
 		m = ((1 << b) - 1) << (8 - b);
-		dst += SPRINTF((dst, "%u", *src & m));
-		size -= (size_t)(dst - t);
+		advance = snprintf(dst, ep - dst, "%u", *src & m);
+		if (advance <= 0 || advance >= ep - dst)
+			goto emsgsize;
+		dst += advance;
 	}
 
 	/* Format CIDR /width. */
-	if (size <= sizeof "/32")
+	if (ep - dst < sizeof "/32")
+		goto emsgsize;
+	advance = snprintf(dst, ep - dst, "/%u", bits);
+	if (advance <= 0 || advance >= ep - dst)
 		goto emsgsize;
-	dst += SPRINTF((dst, "/%u", bits));
+	dst += advance;
 	return (odst);
 
  emsgsize:
@@ -137,132 +142,27 @@ inet_net_ntop_ipv4(const u_char *src, int bits, char *dst, size_t size)
 	return (NULL);
 }
 
-/*%
- * static char *
- * inet_net_ntop_ipv6(src, bits, fakebits, dst, size)
- *	convert IPv6 network number from network to presentation format.
- *	generates CIDR style result always. Picks the shortest representation
- *	unless the IP is really IPv4.
- *	always prints specified number of bits (bits).
- * return:
- *	pointer to dst, or NULL if an error occurred (check errno).
- * note:
- *	network byte order assumed.  this means 192.5.5.240/28 has
- *	0b11110000 in its fourth octet.
- * author:
- *	Vadim Kogan (UCB), June 2001
- *  Original version (IPv4) by Paul Vixie (ISC), July 1996
- */
-
 static char *
-inet_net_ntop_ipv6(const u_char *src, int bits, char *dst, size_t size) {
-	u_int	m;
-	int	b;
-	int	p;
-	int	zero_s, zero_l, tmp_zero_s, tmp_zero_l;
-	int	i;
-	int	is_ipv4 = 0;
-	unsigned char inbuf[16];
-	char outbuf[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
-	char	*cp;
-	int	words;
-	u_char	*s;
+inet_net_ntop_ipv6(const u_char *src, int bits, char *dst, size_t size)
+{
+	int	ret;
+	char	buf[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255:255:255:255/128")];
 
 	if (bits < 0 || bits > 128) {
 		errno = EINVAL;
 		return (NULL);
 	}
 
-	cp = outbuf;
-
-	if (bits == 0) {
-		*cp++ = ':';
-		*cp++ = ':';
-		*cp = '\0';
-	} else {
-		/* Copy src to private buffer.  Zero host part. */
-		p = (bits + 7) / 8;
-		memcpy(inbuf, src, p);
-		memset(inbuf + p, 0, 16 - p);
-		b = bits % 8;
-		if (b != 0) {
-			m = ~0 << (8 - b);
-			inbuf[p-1] &= m;
-		}
-
-		s = inbuf;
-
-		/* how many words need to be displayed in output */
-		words = (bits + 15) / 16;
-		if (words == 1)
-			words = 2;
-
-		/* Find the longest substring of zero's */
-		zero_s = zero_l = tmp_zero_s = tmp_zero_l = 0;
-		for (i = 0; i < (words * 2); i += 2) {
-			if ((s[i] | s[i+1]) == 0) {
-				if (tmp_zero_l == 0)
-					tmp_zero_s = i / 2;
-				tmp_zero_l++;
-			} else {
-				if (tmp_zero_l && zero_l < tmp_zero_l) {
-					zero_s = tmp_zero_s;
-					zero_l = tmp_zero_l;
-					tmp_zero_l = 0;
-				}
-			}
-		}
-
-		if (tmp_zero_l && zero_l < tmp_zero_l) {
-			zero_s = tmp_zero_s;
-			zero_l = tmp_zero_l;
-		}
-
-		if (zero_l != words && zero_s == 0 && ((zero_l == 6) ||
-		    ((zero_l == 5 && s[10] == 0xff && s[11] == 0xff) ||
-		    ((zero_l == 7 && s[14] != 0 && s[15] != 1)))))
-			is_ipv4 = 1;
-
-		/* Format whole words. */
-		for (p = 0; p < words; p++) {
-			if (zero_l != 0 && p >= zero_s && p < zero_s + zero_l) {
-				/* Time to skip some zeros */
-				if (p == zero_s)
-					*cp++ = ':';
-				if (p == words - 1)
-					*cp++ = ':';
-				s++;
-				s++;
-				continue;
-			}
+	if (inet_ntop(AF_INET6, src, buf, size) == NULL)
+		return (NULL);
 
-			if (is_ipv4 && p > 5 ) {
-				*cp++ = (p == 6) ? ':' : '.';
-				cp += SPRINTF((cp, "%u", *s++));
-				/* we can potentially drop the last octet */
-				if (p != 7 || bits > 120) {
-					*cp++ = '.';
-					cp += SPRINTF((cp, "%u", *s++));
-				}
-			} else {
-				if (cp != outbuf)
-					*cp++ = ':';
-				cp += SPRINTF((cp, "%x", *s * 256 + s[1]));
-				s += 2;
-			}
-		}
+	ret = snprintf(dst, size, "%s/%d", buf, bits);
+	if (ret < 0 || ret >= size) {
+		errno = EMSGSIZE;
+		return (NULL);
 	}
-	/* Format CIDR /width. */
-	sprintf(cp, "/%u", bits);
-	if (strlen(outbuf) + 1 > size)
-		goto emsgsize;
-	strcpy(dst, outbuf);
 
 	return (dst);
-
-emsgsize:
-	errno = EMSGSIZE;
-	return (NULL);
 }
 
 /*
@@ -271,5 +171,3 @@ emsgsize:
  */
 #undef inet_net_ntop
 __weak_reference(__inet_net_ntop, inet_net_ntop);
-
-/*! \file */
diff --git a/lib/libc/inet/inet_net_pton.c b/lib/libc/inet/inet_net_pton.c
index d566a0e1d3c3..14c88eb72014 100644
--- a/lib/libc/inet/inet_net_pton.c
+++ b/lib/libc/inet/inet_net_pton.c
@@ -1,20 +1,23 @@
-/*-
- * SPDX-License-Identifier: ISC
+/*	$OpenBSD: inet_net_pton.c,v 1.14 2022/12/27 17:10:06 jmc Exp $	*/
+
+/*
+ * Copyright (c) 2012 by Gilles Chehade <gilles@openbsd.org>
+ * Copyright (c) 1996,1999 by Internet Software Consortium.
  *
- * Copyright (C) 2004, 2005, 2008  Internet Systems Consortium, Inc. ("ISC")
- * Copyright (C) 1996, 1998, 1999, 2001, 2003  Internet Software Consortium.
+ * SPDX-License-Identifier: ISC
  *
- * Permission to use, copy, modify, and/or distribute this software for any
+ * Permission to use, copy, modify, and distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
  *
- * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- * AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- * PERFORMANCE OF THIS SOFTWARE.
+ * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS
+ * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE
+ * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+ * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+ * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+ * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
+ * SOFTWARE.
  */
 
 #include "port_before.h"
@@ -22,7 +25,6 @@
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
-#include <arpa/nameser.h>
 #include <arpa/inet.h>
 
 #include <assert.h>
@@ -34,13 +36,37 @@
 
 #include "port_after.h"
 
-#ifdef SPRINTF_CHAR
-# define SPRINTF(x) strlen(sprintf/**/x)
-#else
-# define SPRINTF(x) ((size_t)sprintf x)
-#endif
+static int	inet_net_pton_ipv4(const char *, u_char *, size_t);
+static int	inet_net_pton_ipv6(const char *, u_char *, size_t);
 
-/*%
+/*
+ * static int
+ * inet_net_pton(af, src, dst, size)
+ *	convert network number from presentation to network format.
+ *	accepts hex octets, hex strings, decimal octets, and /CIDR.
+ *	"size" is in bytes and describes "dst".
+ * return:
+ *	number of bits, either imputed classfully or specified with /CIDR,
+ *	or -1 if some failure occurred (check errno).  ENOENT means it was
+ *	not a valid network specification.
+ * author:
+ *	Paul Vixie (ISC), June 1996
+ */
+int
+inet_net_pton(int af, const char *src, void *dst, size_t size)
+{
+	switch (af) {
+	case AF_INET:
+		return (inet_net_pton_ipv4(src, dst, size));
+	case AF_INET6:
+		return (inet_net_pton_ipv6(src, dst, size));
+	default:
+		errno = EAFNOSUPPORT;
+		return (-1);
+	}
+}
+
+/*
  * static int
  * inet_net_pton_ipv4(src, dst, size)
  *	convert IPv4 network number from presentation to network format.
@@ -57,22 +83,24 @@
  *	Paul Vixie (ISC), June 1996
  */
 static int
-inet_net_pton_ipv4(const char *src, u_char *dst, size_t size) {
-	static const char xdigits[] = "0123456789abcdef";
-	static const char digits[] = "0123456789";
-	int n, ch, tmp = 0, dirty, bits;
+inet_net_pton_ipv4(const char *src, u_char *dst, size_t size)
+{
+	static const char
+		xdigits[] = "0123456789abcdef",
+		digits[] = "0123456789";
+	int n, ch, tmp, dirty, bits;
 	const u_char *odst = dst;
 
-	ch = *src++;
+	ch = (unsigned char)*src++;
 	if (ch == '0' && (src[0] == 'x' || src[0] == 'X')
-	    && isascii((unsigned char)(src[1]))
-	    && isxdigit((unsigned char)(src[1]))) {
+	    && isascii((unsigned char)src[1]) && isxdigit((unsigned char)src[1])) {
 		/* Hexadecimal: Eat nybble string. */
-		if (size <= 0U)
+		if (size == 0)
 			goto emsgsize;
-		dirty = 0;
-		src++;	/*%< skip x or X. */
-		while ((ch = *src++) != '\0' && isascii(ch) && isxdigit(ch)) {
+		tmp = 0, dirty = 0;
+		src++;	/* skip x or X. */
+		while ((ch = (unsigned char)*src++) != '\0' &&
+		    isascii(ch) && isxdigit(ch)) {
 			if (isupper(ch))
 				ch = tolower(ch);
 			n = strchr(xdigits, ch) - xdigits;
@@ -82,14 +110,14 @@ inet_net_pton_ipv4(const char *src, u_char *dst, size_t size) {
 			else
 				tmp = (tmp << 4) | n;
 			if (++dirty == 2) {
-				if (size-- <= 0U)
+				if (size-- == 0)
 					goto emsgsize;
 				*dst++ = (u_char) tmp;
 				dirty = 0;
 			}
 		}
-		if (dirty) {  /*%< Odd trailing nybble? */
-			if (size-- <= 0U)
+		if (dirty) {  /* Odd trailing nybble? */
+			if (size-- == 0)
 				goto emsgsize;
 			*dst++ = (u_char) (tmp << 4);
 		}
@@ -104,16 +132,16 @@ inet_net_pton_ipv4(const char *src, u_char *dst, size_t size) {
 				tmp += n;
 				if (tmp > 255)
 					goto enoent;
-			} while ((ch = *src++) != '\0' &&
+			} while ((ch = (unsigned char)*src++) != '\0' &&
 				 isascii(ch) && isdigit(ch));
-			if (size-- <= 0U)
+			if (size-- == 0)
 				goto emsgsize;
 			*dst++ = (u_char) tmp;
 			if (ch == '\0' || ch == '/')
 				break;
 			if (ch != '.')
 				goto enoent;
-			ch = *src++;
+			ch = (unsigned char)*src++;
 			if (!isascii(ch) || !isdigit(ch))
 				goto enoent;
 		}
@@ -121,10 +149,10 @@ inet_net_pton_ipv4(const char *src, u_char *dst, size_t size) {
 		goto enoent;
 
 	bits = -1;
-	if (ch == '/' && isascii((unsigned char)(src[0])) &&
-	    isdigit((unsigned char)(src[0])) && dst > odst) {
+	if (ch == '/' && isascii((unsigned char)src[0]) &&
+	    isdigit((unsigned char)src[0]) && dst > odst) {
 		/* CIDR width specifier.  Nothing can follow it. */
-		ch = *src++;	/*%< Skip over the /. */
+		ch = (unsigned char)*src++;	/* Skip over the /. */
 		bits = 0;
 		do {
 			n = strchr(digits, ch) - digits;
@@ -132,8 +160,9 @@ inet_net_pton_ipv4(const char *src, u_char *dst, size_t size) {
 			bits *= 10;
 			bits += n;
 			if (bits > 32)
-				goto enoent;
-		} while ((ch = *src++) != '\0' && isascii(ch) && isdigit(ch));
+				goto emsgsize;
+		} while ((ch = (unsigned char)*src++) != '\0' &&
+			 isascii(ch) && isdigit(ch));
 		if (ch != '\0')
 			goto enoent;
 	}
@@ -147,29 +176,23 @@ inet_net_pton_ipv4(const char *src, u_char *dst, size_t size) {
 		goto enoent;
 	/* If no CIDR spec was given, infer width from net class. */
 	if (bits == -1) {
-		if (*odst >= 240)	/*%< Class E */
+		if (*odst >= 240)	/* Class E */
 			bits = 32;
-		else if (*odst >= 224)	/*%< Class D */
-			bits = 8;
-		else if (*odst >= 192)	/*%< Class C */
+		else if (*odst >= 224)	/* Class D */
+			bits = 4;
+		else if (*odst >= 192)	/* Class C */
 			bits = 24;
-		else if (*odst >= 128)	/*%< Class B */
+		else if (*odst >= 128)	/* Class B */
 			bits = 16;
-		else			/*%< Class A */
+		else			/* Class A */
 			bits = 8;
 		/* If imputed mask is narrower than specified octets, widen. */
 		if (bits < ((dst - odst) * 8))
 			bits = (dst - odst) * 8;
-		/*
-		 * If there are no additional bits specified for a class D
-		 * address adjust bits to 4.
-		 */
-		if (bits == 8 && *odst == 224)
-			bits = 4;
 	}
 	/* Extend network to cover the actual mask. */
 	while (bits > ((dst - odst) * 8)) {
-		if (size-- <= 0U)
+		if (size-- == 0)
 			goto emsgsize;
 		*dst++ = '\0';
 	}
@@ -184,222 +207,48 @@ inet_net_pton_ipv4(const char *src, u_char *dst, size_t size) {
 	return (-1);
 }
 
-static int
-getbits(const char *src, int *bitsp) {
-	static const char digits[] = "0123456789";
-	int n;
-	int val;
-	char ch;
-
-	val = 0;
-	n = 0;
-	while ((ch = *src++) != '\0') {
-		const char *pch;
-
-		pch = strchr(digits, ch);
-		if (pch != NULL) {
-			if (n++ != 0 && val == 0)	/*%< no leading zeros */
-				return (0);
-			val *= 10;
-			val += (pch - digits);
-			if (val > 128)			/*%< range */
-				return (0);
-			continue;
-		}
-		return (0);
-	}
-	if (n == 0)
-		return (0);
-	*bitsp = val;
-	return (1);
-}
 
 static int
-getv4(const char *src, u_char *dst, int *bitsp) {
-	static const char digits[] = "0123456789";
-	u_char *odst = dst;
-	int n;
-	u_int val;
-	char ch;
-
-	val = 0;
-	n = 0;
-	while ((ch = *src++) != '\0') {
-		const char *pch;
+inet_net_pton_ipv6(const char *src, u_char *dst, size_t size)
+{
+	struct in6_addr	 in6;
+	int		 ret;
+	int		 bits;
+	size_t		 bytes;
+	char		 buf[INET6_ADDRSTRLEN + sizeof("/128")];
+	char		*sep;
+	const char	*errstr;
 
-		pch = strchr(digits, ch);
-		if (pch != NULL) {
-			if (n++ != 0 && val == 0)	/*%< no leading zeros */
-				return (0);
-			val *= 10;
-			val += (pch - digits);
-			if (val > 255)			/*%< range */
-				return (0);
-			continue;
-		}
-		if (ch == '.' || ch == '/') {
-			if (dst - odst > 3)		/*%< too many octets? */
-				return (0);
-			*dst++ = val;
-			if (ch == '/')
-				return (getbits(src, bitsp));
-			val = 0;
-			n = 0;
-			continue;
-		}
-		return (0);
+	if (strlcpy(buf, src, sizeof buf) >= sizeof buf) {
+		errno = EMSGSIZE;
+		return (-1);
 	}
-	if (n == 0)
-		return (0);
-	if (dst - odst > 3)		/*%< too many octets? */
-		return (0);
-	*dst++ = val;
-	return (1);
-}
 
-static int
-inet_net_pton_ipv6(const char *src, u_char *dst, size_t size) {
-	static const char xdigits_l[] = "0123456789abcdef",
-			  xdigits_u[] = "0123456789ABCDEF";
-	u_char tmp[NS_IN6ADDRSZ], *tp, *endp, *colonp;
-	const char *xdigits, *curtok;
-	int ch, saw_xdigit;
-	u_int val;
-	int digits;
-	int bits;
-	size_t bytes;
-	int words;
-	int ipv4;
+	sep = strchr(buf, '/');
+	if (sep != NULL)
+		*sep++ = '\0';
 
-	memset((tp = tmp), '\0', NS_IN6ADDRSZ);
-	endp = tp + NS_IN6ADDRSZ;
-	colonp = NULL;
-	/* Leading :: requires some special handling. */
-	if (*src == ':')
-		if (*++src != ':')
-			goto enoent;
-	curtok = src;
-	saw_xdigit = 0;
-	val = 0;
-	digits = 0;
-	bits = -1;
-	ipv4 = 0;
-	while ((ch = *src++) != '\0') {
-		const char *pch;
+	ret = inet_pton(AF_INET6, buf, &in6);
+	if (ret != 1)
+		return (-1);
 
-		if ((pch = strchr((xdigits = xdigits_l), ch)) == NULL)
-			pch = strchr((xdigits = xdigits_u), ch);
-		if (pch != NULL) {
-			val <<= 4;
-			val |= (pch - xdigits);
-			if (++digits > 4)
-				goto enoent;
-			saw_xdigit = 1;
-			continue;
-		}
-		if (ch == ':') {
-			curtok = src;
-			if (!saw_xdigit) {
-				if (colonp)
-					goto enoent;
-				colonp = tp;
-				continue;
-			} else if (*src == '\0')
-				goto enoent;
-			if (tp + NS_INT16SZ > endp)
-				return (0);
-			*tp++ = (u_char) (val >> 8) & 0xff;
-			*tp++ = (u_char) val & 0xff;
-			saw_xdigit = 0;
-			digits = 0;
-			val = 0;
-			continue;
-		}
-		if (ch == '.' && ((tp + NS_INADDRSZ) <= endp) &&
-		     getv4(curtok, tp, &bits) > 0) {
-			tp += NS_INADDRSZ;
-			saw_xdigit = 0;
-			ipv4 = 1;
-			break;	/*%< '\\0' was seen by inet_pton4(). */
-		}
-		if (ch == '/' && getbits(src, &bits) > 0)
-			break;
-		goto enoent;
-	}
-	if (saw_xdigit) {
-		if (tp + NS_INT16SZ > endp)
-			goto enoent;
-		*tp++ = (u_char) (val >> 8) & 0xff;
-		*tp++ = (u_char) val & 0xff;
-	}
-	if (bits == -1)
+	if (sep == NULL)
 		bits = 128;
-
-	words = (bits + 15) / 16;
-	if (words < 2)
-		words = 2;
-	if (ipv4)
-		words = 8;
-	endp =  tmp + 2 * words;
-
-	if (colonp != NULL) {
-		/*
-		 * Since some memmove()'s erroneously fail to handle
-		 * overlapping regions, we'll do the shift by hand.
-		 */
-		const int n = tp - colonp;
-		int i;
-
-		if (tp == endp)
-			goto enoent;
-		for (i = 1; i <= n; i++) {
-			endp[- i] = colonp[n - i];
-			colonp[n - i] = 0;
+	else {
+		bits = strtonum(sep, 0, 128, &errstr);
+		if (errstr) {
+			errno = EINVAL;
+			return (-1);
 		}
-		tp = endp;
 	}
-	if (tp != endp)
-		goto enoent;
 
 	bytes = (bits + 7) / 8;
-	if (bytes > size)
-		goto emsgsize;
-	memcpy(dst, tmp, bytes);
-	return (bits);
-
- enoent:
-	errno = ENOENT;
-	return (-1);
-
- emsgsize:
-	errno = EMSGSIZE;
-	return (-1);
-}
-
-/*%
- * int
- * inet_net_pton(af, src, dst, size)
- *	convert network number from presentation to network format.
- *	accepts hex octets, hex strings, decimal octets, and /CIDR.
- *	"size" is in bytes and describes "dst".
- * return:
- *	number of bits, either imputed classfully or specified with /CIDR,
- *	or -1 if some failure occurred (check errno).  ENOENT means it was
- *	not a valid network specification.
- * author:
- *	Paul Vixie (ISC), June 1996
- */
-int
-inet_net_pton(int af, const char *src, void *dst, size_t size) {
-	switch (af) {
-	case AF_INET:
-		return (inet_net_pton_ipv4(src, dst, size));
-	case AF_INET6:
-		return (inet_net_pton_ipv6(src, dst, size));
-	default:
-		errno = EAFNOSUPPORT;
+	if (bytes > size) {
+		errno = EMSGSIZE;
 		return (-1);
 	}
+	memcpy(dst, &in6.s6_addr, bytes);
+	return (bits);
 }
 
 /*
@@ -408,5 +257,3 @@ inet_net_pton(int af, const char *src, void *dst, size_t size) {
  */
 #undef inet_net_pton
 __weak_reference(__inet_net_pton, inet_net_pton);
-
-/*! \file */
diff --git a/lib/libc/tests/net/Makefile b/lib/libc/tests/net/Makefile
index 24cff61e8d24..ec0668633508 100644
--- a/lib/libc/tests/net/Makefile
+++ b/lib/libc/tests/net/Makefile
@@ -4,8 +4,10 @@ ATF_TESTS_C+=	ether_test
 ATF_TESTS_C+=	eui64_aton_test
 ATF_TESTS_C+=	eui64_ntoa_test
 ATF_TESTS_CXX+=	link_addr_test
+ATF_TESTS_CXX+=	inet_net_test
 
 CXXSTD.link_addr_test=	c++20
+CXXSTD.inet_net_test=	c++20
 
 CFLAGS+=	-I${.CURDIR}
 
diff --git a/lib/libc/tests/net/inet_net_test.cc b/lib/libc/tests/net/inet_net_test.cc
new file mode 100644
index 000000000000..4ecf5a3de492
--- /dev/null
+++ b/lib/libc/tests/net/inet_net_test.cc
@@ -0,0 +1,333 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2025 Lexi Winter <ivy@FreeBSD.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * Tests for inet_net_pton() and inet_net_ntop().
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <ranges>
+#include <string>
+#include <vector>
+
+#include <atf-c++.hpp>
+
+using namespace std::literals;
+
+/*
+ * inet_net_ntop() and inet_net_pton() for IPv4.
+ */
+ATF_TEST_CASE_WITHOUT_HEAD(inet_net_inet4)
+ATF_TEST_CASE_BODY(inet_net_inet4)
+{
+	/*
+	 * Define a list of addresses we want to check.  Each address is passed
+	 * to inet_net_pton() to convert it to an in_addr, then we convert the
+	 * in_addr back to a string and compare it with the expected value.  We
+	 * want to test over-long prefixes here (such as 10.0.0.1/8), so we also
+	 * specify what the result is expected to be.
+	 */
+
+	struct test_addr {
+		std::string input;
+		unsigned bits;
+		std::string output;
+	};
+
+	auto test_addrs = std::vector<test_addr>{
+		// Simple prefixes that fall on octet boundaries.
+		{ "10.0.0.0/8",		8,	"10/8" },
+		{ "10.1.0.0/16",	16,	"10.1/16" },
+		{ "10.1.2.0/24",	24,	"10.1.2/24" },
+		{ "10.1.2.3/32",	32,	"10.1.2.3/32" },
+
+		// Simple prefixes with the short-form address.
+		{ "10/8",		8,	"10/8" },
+		{ "10.1/16",		16,	"10.1/16" },
+		{ "10.1.2/24",		24,	"10.1.2/24" },
+
+		// A prefix that doesn't fall on an octet boundary.
+		{ "10.1.64/18",		18,	"10.1.64/18" },
+
+		// An overlong prefix with bits that aren't part of the prefix.
+		{ "10.0.0.1/8",		8,	"10/8" },
+	};
+
+	for (auto const &addr: test_addrs) {
+		/*
+		 * Convert the input string to an in_addr + bits, and make
+		 * sure the result produces the number of bits we expected.
+		 */
+
+		auto in = in_addr{};
+		auto bits = inet_net_pton(AF_INET, addr.input.c_str(),
+		    &in, sizeof(in));
+		ATF_REQUIRE(bits != -1);
+		ATF_REQUIRE_EQ(bits, addr.bits);
+
+		/*
+		 * Convert the in_addr back to a string
+		 */
+
+		/*
+		 * XXX: Should there be a constant for the size of the result
+		 * buffer?  For now, use ADDRSTRLEN + 3 ("/32") + 1 (NUL).
+		 *
+		 * Fill the buffer with 'Z', so we can check the result was
+		 * properly terminated.
+		 */
+		auto strbuf = std::vector<char>(INET_ADDRSTRLEN + 3 + 1, 'Z');
+		auto ret = inet_net_ntop(AF_INET, &in, bits,
+		    strbuf.data(), strbuf.size());
+		ATF_REQUIRE(ret != NULL);
+		ATF_REQUIRE_EQ(ret, strbuf.data());
+
+		/* Make sure the result was NUL-terminated and find the NUL */
+		ATF_REQUIRE(strbuf.size() >= 1);
+		auto end = std::ranges::find(strbuf, '\0');
+		ATF_REQUIRE(end != strbuf.end());
+
+		/*
+		 * Check the result matches what we expect.  Use a temporary
+		 * string here instead of std::ranges::equal because this
+		 * means ATF can print the mismatch.
+		 */
+		auto str = std::string(std::ranges::begin(strbuf), end);
+		ATF_REQUIRE_EQ(str, addr.output);
+	}
+}
+
+/*
+ * inet_net_ntop() and inet_net_pton() for IPv6.
+ */
+ATF_TEST_CASE_WITHOUT_HEAD(inet_net_inet6)
+ATF_TEST_CASE_BODY(inet_net_inet6)
+{
+	/*
+	 * Define a list of addresses we want to check.  Each address is
+	 * passed to inet_net_pton() to convert it to an in6_addr, then we
+	 * convert the in6_addr back to a string and compare it with the
+	 * expected value.  We want to test over-long prefixes here (such
+	 * as 2001:db8::1/32), so we also specify what the result is
+	 * expected to be.
+	 */
+
+	struct test_addr {
+		std::string input;
+		unsigned bits;
+		std::string output;
+	};
+
+	auto test_addrs = std::vector<test_addr>{
+		// A prefix with a trailing ::
+		{ "2001:db8::/32",	32,	"2001:db8::/32" },
+
+		// A prefix with a leading ::.  Note that the output is
+		// different from the input because inet_ntop() renders
+		// this prefix with an IPv4 suffix for legacy reasons.
+		{ "::ffff:0:0/96",	96,	"::ffff:0.0.0.0/96" },
+
*** 184 LINES SKIPPED ***