From 57c15109d1a4d45dedc06897d72722693aeef5b9 Mon Sep 17 00:00:00 2001
From: Ryan Goodfellow <ryan.goodfellow@oxide.computer>
Date: Mon, 30 Jan 2023 14:58:34 -0800
Subject: [PATCH] 17340 Want IPv6 Fast Path Portions contributed by: Patrick
 Mooney <pmooney@pfmooney.com> Portions contributed by: Kyle Simpson
 <kyle@oxide.computer> Portions contributed by: Ryan Zezeski
 <ryan@zinascii.com> Reviewed by: Ryan Zezeski <ryan@zinascii.com>

Change-Id: I561ab018a95b8540ebaf99355c747fa039e9f481
---
 usr/src/pkg/manifests/system-test-ostest.p5m  |   5 +
 usr/src/test/os-tests/tests/mac/Makefile      |  17 +-
 .../tests/mac/data/ipv4_tcp_lso_in.snoop      | Bin 0 -> 35900 bytes
 .../tests/mac/data/ipv4_tcp_lso_out.snoop     | Bin 0 -> 36176 bytes
 .../tests/mac/data/ipv6_tcp_lso_in.snoop      | Bin 0 -> 62624 bytes
 .../tests/mac/data/ipv6_tcp_lso_out.snoop     | Bin 0 -> 63296 bytes
 usr/src/test/os-tests/tests/mac/mac_cksum.c   | 209 +------
 usr/src/test/os-tests/tests/mac/mac_ktest.ksh |  47 +-
 .../os-tests/tests/mac/mac_ktest_common.c     | 198 +++++++
 .../os-tests/tests/mac/mac_ktest_common.h     |  76 +++
 usr/src/test/os-tests/tests/mac/mac_lso.c     | 214 ++++++++
 usr/src/uts/common/inet/ip/ip_if.c            |  18 +-
 usr/src/uts/common/inet/ip_impl.h             |  20 -
 usr/src/uts/common/io/cxgbe/t4nex/t4_sge.c    |   7 +-
 usr/src/uts/common/io/dld/dld_proto.c         |  12 +-
 usr/src/uts/common/io/mac/mac_client.c        |  35 +-
 .../uts/common/io/mac/mac_datapath_setup.c    | 124 ++++-
 usr/src/uts/common/io/mac/mac_ktest.c         | 296 +++++++---
 usr/src/uts/common/io/mac/mac_sched.c         | 508 ++++++++++--------
 usr/src/uts/common/io/mac/mac_soft_ring.c     |   7 +
 usr/src/uts/common/io/mac/mac_stat.c          |  13 +-
 usr/src/uts/common/io/mac/mac_util.c          | 273 +++++-----
 usr/src/uts/common/os/ip_cksum.c              |   4 +-
 usr/src/uts/common/sys/mac_client_impl.h      |   4 +-
 usr/src/uts/common/sys/mac_client_priv.h      |   3 +-
 usr/src/uts/common/sys/mac_impl.h             |   8 +
 usr/src/uts/common/sys/mac_soft_ring.h        |  16 +-
 usr/src/uts/intel/io/viona/viona_rx.c         |  16 +-
 28 files changed, 1395 insertions(+), 735 deletions(-)
 create mode 100644 usr/src/test/os-tests/tests/mac/data/ipv4_tcp_lso_in.snoop
 create mode 100644 usr/src/test/os-tests/tests/mac/data/ipv4_tcp_lso_out.snoop
 create mode 100644 usr/src/test/os-tests/tests/mac/data/ipv6_tcp_lso_in.snoop
 create mode 100644 usr/src/test/os-tests/tests/mac/data/ipv6_tcp_lso_out.snoop
 create mode 100644 usr/src/test/os-tests/tests/mac/mac_ktest_common.c
 create mode 100644 usr/src/test/os-tests/tests/mac/mac_ktest_common.h
 create mode 100644 usr/src/test/os-tests/tests/mac/mac_lso.c

diff --git a/usr/src/pkg/manifests/system-test-ostest.p5m b/usr/src/pkg/manifests/system-test-ostest.p5m
index 16978c8d27..29cda51a3f 100644
--- a/usr/src/pkg/manifests/system-test-ostest.p5m
+++ b/usr/src/pkg/manifests/system-test-ostest.p5m
@@ -145,14 +145,19 @@ file path=opt/os-tests/tests/mac/data/ipv4_bad_proto.snoop mode=0444
 file path=opt/os-tests/tests/mac/data/ipv4_icmp.snoop mode=0444
 file path=opt/os-tests/tests/mac/data/ipv4_sctp.snoop mode=0444
 file path=opt/os-tests/tests/mac/data/ipv4_tcp.snoop mode=0444
+file path=opt/os-tests/tests/mac/data/ipv4_tcp_lso_in.snoop mode=0444
+file path=opt/os-tests/tests/mac/data/ipv4_tcp_lso_out.snoop mode=0444
 file path=opt/os-tests/tests/mac/data/ipv4_udp.snoop mode=0444
 file path=opt/os-tests/tests/mac/data/ipv6_eh_udp.snoop mode=0444
 file path=opt/os-tests/tests/mac/data/ipv6_icmp.snoop mode=0444
 file path=opt/os-tests/tests/mac/data/ipv6_sctp.snoop mode=0444
 file path=opt/os-tests/tests/mac/data/ipv6_tcp.snoop mode=0444
+file path=opt/os-tests/tests/mac/data/ipv6_tcp_lso_in.snoop mode=0444
+file path=opt/os-tests/tests/mac/data/ipv6_tcp_lso_out.snoop mode=0444
 file path=opt/os-tests/tests/mac/data/ipv6_udp.snoop mode=0444
 file path=opt/os-tests/tests/mac/mac_cksum mode=0555
 file path=opt/os-tests/tests/mac/mac_ktest mode=0555
+file path=opt/os-tests/tests/mac/mac_lso mode=0555
 file path=opt/os-tests/tests/mac/mac_parsing mode=0555
 dir  path=opt/os-tests/tests/minttl
 file path=opt/os-tests/tests/minttl/minttl mode=0555
diff --git a/usr/src/test/os-tests/tests/mac/Makefile b/usr/src/test/os-tests/tests/mac/Makefile
index 9a59fbca36..1f761464a9 100644
--- a/usr/src/test/os-tests/tests/mac/Makefile
+++ b/usr/src/test/os-tests/tests/mac/Makefile
@@ -13,7 +13,7 @@
 # Copyright 2025 Oxide Computer Company
 #
 
-include $(SRC)/Makefile.master
+include $(SRC)/Makefile.master.64
 
 ROOTOPTPKG = $(ROOT)/opt/os-tests
 TESTDIR = $(ROOTOPTPKG)/tests/mac
@@ -21,8 +21,11 @@ DATADIR = $(TESTDIR)/data
 
 PROGS = \
 	mac_cksum \
+	mac_lso \
 	mac_parsing
 
+COMMON_OBJS = mac_ktest_common.o
+
 SCRIPTS = mac_ktest
 
 DATAFILES = \
@@ -30,10 +33,14 @@ DATAFILES = \
 	ipv4_icmp.snoop \
 	ipv4_sctp.snoop \
 	ipv4_tcp.snoop \
+	ipv4_tcp_lso_in.snoop \
+	ipv4_tcp_lso_out.snoop \
 	ipv4_udp.snoop \
 	ipv6_icmp.snoop \
 	ipv6_sctp.snoop \
 	ipv6_tcp.snoop \
+	ipv6_tcp_lso_in.snoop \
+	ipv6_tcp_lso_out.snoop \
 	ipv6_udp.snoop \
 	ipv6_eh_udp.snoop
 
@@ -65,6 +72,14 @@ clobber: clean
 
 clean:
 
+mac_lso: mac_lso.o $(COMMON_OBJS)
+	$(LINK.c) $^ -o $@ $(LDLIBS)
+	$(POST_PROCESS)
+
+mac_cksum: mac_cksum.o $(COMMON_OBJS)
+	$(LINK.c) $^ -o $@ $(LDLIBS)
+	$(POST_PROCESS)
+
 $(CMDS): $(TESTDIR) $(PROGS)
 
 $(TESTDIR):
diff --git a/usr/src/test/os-tests/tests/mac/data/ipv4_tcp_lso_in.snoop b/usr/src/test/os-tests/tests/mac/data/ipv4_tcp_lso_in.snoop
new file mode 100644
index 0000000000000000000000000000000000000000..b7aae1db178d66387b4b46413e4aab678dfdfa5b
GIT binary patch
diff --git a/usr/src/test/os-tests/tests/mac/data/ipv4_tcp_lso_out.snoop b/usr/src/test/os-tests/tests/mac/data/ipv4_tcp_lso_out.snoop
new file mode 100644
index 0000000000000000000000000000000000000000..621bd302dce7ea5f07fd94f9796a98207bbac5f4
GIT binary patch
diff --git a/usr/src/test/os-tests/tests/mac/data/ipv6_tcp_lso_in.snoop b/usr/src/test/os-tests/tests/mac/data/ipv6_tcp_lso_in.snoop
new file mode 100644
index 0000000000000000000000000000000000000000..7b606095666e0e0f376cbf5ab9fa80aa1821218d
GIT binary patch
diff --git a/usr/src/test/os-tests/tests/mac/data/ipv6_tcp_lso_out.snoop b/usr/src/test/os-tests/tests/mac/data/ipv6_tcp_lso_out.snoop
new file mode 100644
index 0000000000000000000000000000000000000000..eed2c201c70e275643589af7678f75efe1335773
GIT binary patch
diff --git a/usr/src/test/os-tests/tests/mac/mac_cksum.c b/usr/src/test/os-tests/tests/mac/mac_cksum.c
index 3fe91445ba..86746b44b6 100644
--- a/usr/src/test/os-tests/tests/mac/mac_cksum.c
+++ b/usr/src/test/os-tests/tests/mac/mac_cksum.c
@@ -28,182 +28,21 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <fcntl.h>
-#include <unistd.h>
 #include <err.h>
 #include <errno.h>
-#include <strings.h>
-#include <netinet/in.h>
-#include <sys/mman.h>
-#include <sys/dlpi.h>
-#include <sys/types.h>
-#include <sys/types32.h>
-#include <sys/stat.h>
-#include <sys/sysmacros.h>
-#include <sys/ethernet.h>
+#include <libgen.h>
 
-#include <libnvpair.h>
 #include <libktest.h>
 
-typedef struct snoop_pkt_hdr {
-	uint_t			sph_origlen;
-	uint_t			sph_msglen;
-	uint_t			sph_totlen;
-	uint_t			sph_drops;
-#if defined(_LP64)
-	struct timeval32	sph_timestamp;
-#else
-#error	"ktest is expected to be 64-bit for now"
-#endif
-} snoop_pkt_hdr_t;
-
-typedef struct snoop_file_hdr {
-	char		sfh_magic[8];
-	uint32_t	sfh_vers;
-	uint32_t	sfh_mac_type;
-} snoop_file_hdr_t;
-
-static const char snoop_magic[8] = "snoop\0\0\0";
-static const uint_t snoop_acceptable_vers = 2;
-
-typedef struct pkt_cap_iter {
-	int		pci_fd;
-	const char	*pci_base;
-	size_t		pci_map_sz;
-	size_t		pci_sz;
-	size_t		pci_offset;
-} pkt_cap_iter_t;
-
-static pkt_cap_iter_t *
-pkt_cap_open(int fd)
-{
-	struct stat info;
-	if (fstat(fd, &info) != 0) {
-		return (NULL);
-	}
-	if (info.st_size < sizeof (snoop_file_hdr_t)) {
-		errno = EINVAL;
-		return (NULL);
-	}
-
-	const size_t page_sz = (size_t)sysconf(_SC_PAGESIZE);
-	const size_t map_sz = P2ROUNDUP(info.st_size, page_sz);
-	void *map = mmap(NULL, map_sz, PROT_READ, MAP_PRIVATE, fd, 0);
-	if (map == NULL) {
-		return (NULL);
-	}
-
-	const snoop_file_hdr_t *hdr = (const snoop_file_hdr_t *)map;
-	if (bcmp(&hdr->sfh_magic, snoop_magic, sizeof (hdr->sfh_magic)) != 0 ||
-	    ntohl(hdr->sfh_vers) != snoop_acceptable_vers ||
-	    ntohl(hdr->sfh_mac_type) != DL_ETHER) {
-		(void) munmap(map, map_sz);
-		errno = EINVAL;
-		return (NULL);
-	}
-
-	struct pkt_cap_iter *iter = malloc(sizeof (struct pkt_cap_iter));
-	if (iter == NULL) {
-		(void) munmap(map, map_sz);
-		errno = ENOMEM;
-		return (NULL);
-	}
-
-	iter->pci_fd = fd;
-	iter->pci_base = (const char *)map;
-	iter->pci_map_sz = map_sz;
-	iter->pci_sz = info.st_size;
-	iter->pci_offset = sizeof (*hdr);
-
-	return (iter);
-}
-
-static void
-pkt_cap_close(pkt_cap_iter_t *iter)
-{
-	(void) munmap((void *)iter->pci_base, iter->pci_map_sz);
-	(void) close(iter->pci_fd);
-	free(iter);
-}
-
-static bool
-pkt_cap_next(pkt_cap_iter_t *iter, const void **pkt_buf, uint_t *sizep)
-{
-	size_t remain = iter->pci_sz - iter->pci_offset;
-
-	if (remain < sizeof (snoop_pkt_hdr_t)) {
-		return (false);
-	}
-
-	const snoop_pkt_hdr_t *hdr =
-	    (const snoop_pkt_hdr_t *)&iter->pci_base[iter->pci_offset];
-
-	const uint_t msg_sz = ntohl(hdr->sph_msglen);
-	const uint_t total_sz = ntohl(hdr->sph_totlen);
-	if (remain < total_sz || remain < msg_sz) {
-		return (false);
-	}
-
-	*pkt_buf = (const void *)&hdr[1];
-	*sizep = msg_sz;
-	iter->pci_offset += total_sz;
-	return (true);
-}
+#include "mac_ktest_common.h"
 
 static ktest_hdl_t *kthdl = NULL;
 const char *mac_cksum_cmd = "";
 
-struct payload_opts {
-	uint_t		po_padding;
-	boolean_t	po_cksum_partial;
-	boolean_t	po_cksum_full;
-	boolean_t	po_cksum_ipv4;
-	boolean_t	po_split_ether;
-	uint_t		po_split_manual;
-};
-
-static char *
-build_payload(const void *pkt_buf, uint_t pkt_sz,
-    const struct payload_opts *popts, size_t *payload_sz)
-{
-	nvlist_t *payload = fnvlist_alloc();
-	fnvlist_add_byte_array(payload, "pkt_bytes",
-	    (uchar_t *)pkt_buf, pkt_sz);
-	if (popts->po_padding) {
-		fnvlist_add_uint32(payload, "padding", popts->po_padding);
-	}
-	if (popts->po_cksum_partial) {
-		fnvlist_add_boolean(payload, "cksum_partial");
-	}
-	if (popts->po_cksum_full) {
-		fnvlist_add_boolean(payload, "cksum_full");
-	}
-	if (popts->po_cksum_ipv4) {
-		fnvlist_add_boolean(payload, "cksum_ipv4");
-	}
-
-	uint_t nsplit = 0;
-	uint32_t splits[2];
-	if (popts->po_split_ether) {
-		splits[nsplit++] = sizeof (struct ether_header);
-	}
-	if (popts->po_split_manual != 0) {
-		splits[nsplit++] = popts->po_split_manual;
-	}
-	if (nsplit > 0) {
-		fnvlist_add_uint32_array(payload, "cksum_splits", splits,
-		    nsplit);
-	}
-
-	char *packed = fnvlist_pack(payload, payload_sz);
-	nvlist_free(payload);
-
-	return (packed);
-}
-
-static void
+static void __NORETURN
 mac_cksum_usage(void)
 {
-	(void) fprintf(stderr, "usage: %s [flags] [opts] <cap_file>\n\n"
+	(void) fprintf(stderr, "Usage: %s [flags] [opts] <cap_file>\n\n"
 	    "Flags:\n"
 	    "\t-4\temulate HCK_IPV4_HDRCKSUM\n"
 	    "\t-f\temulate HCK_FULLCKSUM\t(cannot be used with -p)\n"
@@ -226,10 +65,11 @@ int
 main(int argc, char *argv[])
 {
 	/* Peel off command name for usage */
-	mac_cksum_cmd = argv[0];
+	mac_cksum_cmd = basename(argv[0]);
 	argc--;
 	argv++;
 	optind = 0;
+	const char *errstr = NULL;
 
 	struct payload_opts popts = { 0 };
 	int c;
@@ -245,27 +85,29 @@ main(int argc, char *argv[])
 			popts.po_cksum_ipv4 = B_TRUE;
 			break;
 		case 'b':
-			errno = 0;
-			popts.po_padding = strtoul(optarg, NULL, 0);
-			if (errno != 0) {
-				err(EXIT_FAILURE,
-				    "invalid padding value %s", optarg);
+			popts.po_padding =
+			    strtonumx(optarg, 0, UINT16_MAX, &errstr, 0);
+			if (errstr != NULL) {
+				errx(EXIT_FAILURE,
+				    "invalid padding value %s: %s",
+				    optarg, errstr);
 			}
 			break;
 		case 'e':
 			popts.po_split_ether = B_TRUE;
 			break;
 		case 's':
-			errno = 0;
-			popts.po_split_manual = strtoul(optarg, NULL, 0);
-			if (errno != 0) {
-				err(EXIT_FAILURE,
-				    "invalid split value %s", optarg);
+			popts.po_split_manual =
+			    strtonumx(optarg, 0, UINT16_MAX, &errstr, 0);
+			if (errstr != NULL) {
+				errx(EXIT_FAILURE,
+				    "invalid split value %s: %s",
+				    optarg, errstr);
 			}
 			break;
 
 		case '?':
-			warnx("unknown run option: -%c", optopt);
+			warnx("unknown option: -%c", optopt);
 			mac_cksum_usage();
 		}
 	}
@@ -273,10 +115,11 @@ main(int argc, char *argv[])
 	argv += optind;
 
 	if (argc != 1) {
+		(void) fprintf(stderr, "cap_file is a required argument\n");
 		mac_cksum_usage();
 	}
 
-	int fd = open(argv[0], O_RDONLY, 0);
+	int fd = open(argv[0], O_RDONLY);
 	if (fd < 0) {
 		err(EXIT_FAILURE, "could not open cap file %s", argv[0]);
 	}
@@ -286,12 +129,12 @@ main(int argc, char *argv[])
 		err(EXIT_FAILURE, "unrecognized cap file %s", argv[0]);
 	}
 
-	if (!ktest_mod_load("mac")) {
-		err(EXIT_FAILURE, "could not load mac ktest module");
-	}
 	if ((kthdl = ktest_init()) == NULL) {
 		err(EXIT_FAILURE, "could not initialize libktest");
 	}
+	if (!ktest_mod_load("mac")) {
+		err(EXIT_FAILURE, "could not load mac ktest module");
+	}
 
 	const void *pkt_buf;
 	uint_t pkt_sz;
@@ -303,8 +146,8 @@ main(int argc, char *argv[])
 			.krq_test = "mac_sw_cksum_test",
 		};
 		size_t payload_sz;
-		char *payload =
-		    build_payload(pkt_buf, pkt_sz, &popts, &payload_sz);
+		char *payload = build_payload(pkt_buf, pkt_sz, NULL, 0,
+		    &popts, &payload_sz);
 		req.krq_input = (uchar_t *)payload;
 		req.krq_input_len = (uint_t)payload_sz;
 
diff --git a/usr/src/test/os-tests/tests/mac/mac_ktest.ksh b/usr/src/test/os-tests/tests/mac/mac_ktest.ksh
index 278ec355ef..cb00771623 100644
--- a/usr/src/test/os-tests/tests/mac/mac_ktest.ksh
+++ b/usr/src/test/os-tests/tests/mac/mac_ktest.ksh
@@ -19,16 +19,23 @@ unalias -a
 mac_test_dir="$(dirname $0)"
 mac_data_dir="$mac_test_dir/data"
 mac_cksum="$mac_test_dir/mac_cksum"
+mac_lso="$mac_test_dir/mac_lso"
 mac_exit=0
 
 run_one()
 {
-	typeset input="$mac_data_dir/$1"
-	shift
+	typeset prog="$1"
+	typeset input="$mac_data_dir/$2"
+	shift 2
 
-	echo "$mac_cksum $* $input"
-	$mac_cksum $* $input
-	if (( $? != 0 )); then
+	typeset output=""
+	if [[ $prog == $mac_lso ]]; then
+		output="$mac_data_dir/$1"
+		shift
+	fi
+
+	echo "$prog $* $input $output"
+	if ! $prog $* $input $output; then
 		mac_exit=1
 	fi
 }
@@ -45,24 +52,40 @@ run_test()
 	run_one $* -e -s 8
 }
 
+run_cso()
+{
+	run_test $mac_cksum $*
+}
+
+run_lso()
+{
+	run_test $mac_lso $*
+}
+
 # The bad-L4-proto case should only try getting a IPv4 checksum.
 # It would fail to get an L4 checksum
-run_test ipv4_bad_proto.snoop -4
+run_cso ipv4_bad_proto.snoop -4
 
 ipv4_cases="ipv4_icmp.snoop ipv4_tcp.snoop ipv4_udp.snoop"
 for c in $ipv4_cases; do
-	run_test $c -4 -p
-	run_test $c -4 -f
+	run_cso $c -4 -p
+	run_cso $c -4 -f
 done
 
 ipv6_cases="ipv6_icmp.snoop ipv6_tcp.snoop ipv6_udp.snoop ipv6_eh_udp.snoop"
 for c in $ipv6_cases; do
-	run_test $c -p
-	run_test $c -f
+	run_cso $c -p
+	run_cso $c -f
 done
 
 # Only full checksums are supported for SCTP
-run_test "ipv4_sctp.snoop" -4 -f
-run_test "ipv6_sctp.snoop" -f
+run_cso "ipv4_sctp.snoop" -4 -f
+run_cso "ipv6_sctp.snoop" -f
+
+# Only TCP is supported for LSO.
+run_lso "ipv4_tcp_lso_in.snoop" "ipv4_tcp_lso_out.snoop" -4 -f -m 8948
+run_lso "ipv4_tcp_lso_in.snoop" "ipv4_tcp_lso_out.snoop" -4 -p -m 8948
+run_lso "ipv6_tcp_lso_in.snoop" "ipv6_tcp_lso_out.snoop" -f -m 8928
+run_lso "ipv6_tcp_lso_in.snoop" "ipv6_tcp_lso_out.snoop" -p -m 8928
 
 exit $mac_exit
diff --git a/usr/src/test/os-tests/tests/mac/mac_ktest_common.c b/usr/src/test/os-tests/tests/mac/mac_ktest_common.c
new file mode 100644
index 0000000000..331270f174
--- /dev/null
+++ b/usr/src/test/os-tests/tests/mac/mac_ktest_common.c
@@ -0,0 +1,198 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source.  A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright 2025 Oxide Computer Compnay
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <err.h>
+#include <errno.h>
+#include <strings.h>
+#include <netinet/in.h>
+#include <sys/mman.h>
+#include <sys/dlpi.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <sys/ethernet.h>
+
+#include <libnvpair.h>
+
+#include "mac_ktest_common.h"
+
+static const char snoop_magic[8] = "snoop\0\0\0";
+static const uint_t snoop_acceptable_vers = 2;
+
+pkt_cap_iter_t *
+pkt_cap_open(int fd)
+{
+	struct stat info;
+	if (fstat(fd, &info) != 0) {
+		return (NULL);
+	}
+	if (info.st_size < sizeof (snoop_file_hdr_t)) {
+		errno = EINVAL;
+		return (NULL);
+	}
+
+	const size_t page_sz = (size_t)sysconf(_SC_PAGESIZE);
+	const size_t map_sz = P2ROUNDUP(info.st_size, page_sz);
+	void *map = mmap(NULL, map_sz, PROT_READ, MAP_PRIVATE, fd, 0);
+	if (map == NULL) {
+		return (NULL);
+	}
+
+	const snoop_file_hdr_t *hdr = (const snoop_file_hdr_t *)map;
+	if (bcmp(&hdr->sfh_magic, snoop_magic, sizeof (hdr->sfh_magic)) != 0 ||
+	    ntohl(hdr->sfh_vers) != snoop_acceptable_vers ||
+	    ntohl(hdr->sfh_mac_type) != DL_ETHER) {
+		(void) munmap(map, map_sz);
+		errno = EINVAL;
+		return (NULL);
+	}
+
+	struct pkt_cap_iter *iter = malloc(sizeof (struct pkt_cap_iter));
+	if (iter == NULL) {
+		(void) munmap(map, map_sz);
+		errno = ENOMEM;
+		return (NULL);
+	}
+
+	iter->pci_fd = fd;
+	iter->pci_base = (const char *)map;
+	iter->pci_map_sz = map_sz;
+	iter->pci_sz = info.st_size;
+	iter->pci_offset = sizeof (*hdr);
+
+	return (iter);
+}
+
+void
+pkt_cap_close(pkt_cap_iter_t *iter)
+{
+	(void) munmap((void *)iter->pci_base, iter->pci_map_sz);
+	(void) close(iter->pci_fd);
+	free(iter);
+}
+
+void
+pkt_cap_reset(pkt_cap_iter_t *iter)
+{
+	iter->pci_offset = sizeof (snoop_file_hdr_t);
+}
+
+bool
+pkt_cap_next(pkt_cap_iter_t *iter, const void **pkt_buf, uint_t *sizep)
+{
+	size_t remain = iter->pci_sz - iter->pci_offset;
+
+	if (remain < sizeof (snoop_pkt_hdr_t)) {
+		return (false);
+	}
+
+	const snoop_pkt_hdr_t *hdr =
+	    (const snoop_pkt_hdr_t *)&iter->pci_base[iter->pci_offset];
+
+	const uint_t msg_sz = ntohl(hdr->sph_msglen);
+	const uint_t total_sz = ntohl(hdr->sph_totlen);
+	if (remain < total_sz || remain < msg_sz) {
+		return (false);
+	}
+
+	*pkt_buf = (const void *)&hdr[1];
+	*sizep = msg_sz;
+	iter->pci_offset += total_sz;
+	return (true);
+}
+
+char *
+serialize_pkt_chain(pkt_cap_iter_t *iter, uint_t *sizep)
+{
+	/*
+	 * First, figure out how many bytes are needed. We're serializing
+	 * down to `uint32_t` (len) + <bytes> for each packet.
+	 */
+	const void *pkt_buf = NULL;
+	*sizep = 0;
+	uint_t pkt_sz;
+	while (pkt_cap_next(iter, &pkt_buf, &pkt_sz)) {
+		*sizep += sizeof (uint32_t) + pkt_sz;
+	}
+
+	/*
+	 * Rewalk, and copy all the bytes out.
+	 */
+	char *out = malloc(*sizep);
+	pkt_cap_reset(iter);
+
+	if (out == NULL)
+		return (out);
+	char *cur = out;
+	while (pkt_cap_next(iter, &pkt_buf, &pkt_sz)) {
+		uint32_t ps = pkt_sz;
+		bcopy(&ps, cur, sizeof (ps));
+		cur += sizeof (ps);
+		bcopy(pkt_buf, cur, pkt_sz);
+		cur += pkt_sz;
+	}
+
+	return (out);
+}
+
+char *
+build_payload(const void *pkt_buf, uint_t pkt_sz,
+    const void *out_pkt_buf, uint_t out_pkt_sz,
+    const struct payload_opts *popts, size_t *payload_sz)
+{
+	nvlist_t *payload = fnvlist_alloc();
+	fnvlist_add_byte_array(payload, "pkt_bytes",
+	    (uchar_t *)pkt_buf, pkt_sz);
+	if (out_pkt_buf != NULL) {
+		fnvlist_add_byte_array(payload, "out_pkt_bytes",
+		    (uchar_t *)out_pkt_buf, out_pkt_sz);
+	}
+	if (popts->po_mss) {
+		fnvlist_add_uint32(payload, "mss", popts->po_mss);
+	}
+	if (popts->po_padding) {
+		fnvlist_add_uint32(payload, "padding", popts->po_padding);
+	}
+	if (popts->po_cksum_partial) {
+		fnvlist_add_boolean(payload, "cksum_partial");
+	}
+	if (popts->po_cksum_full) {
+		fnvlist_add_boolean(payload, "cksum_full");
+	}
+	if (popts->po_cksum_ipv4) {
+		fnvlist_add_boolean(payload, "cksum_ipv4");
+	}
+
+	uint_t nsplit = 0;
+	uint32_t splits[2];
+	if (popts->po_split_ether) {
+		splits[nsplit++] = sizeof (struct ether_header);
+	}
+	if (popts->po_split_manual != 0) {
+		splits[nsplit++] = popts->po_split_manual;
+	}
+	if (nsplit > 0) {
+		fnvlist_add_uint32_array(payload, "cksum_splits", splits,
+		    nsplit);
+	}
+
+	char *packed = fnvlist_pack(payload, payload_sz);
+	nvlist_free(payload);
+
+	return (packed);
+}
diff --git a/usr/src/test/os-tests/tests/mac/mac_ktest_common.h b/usr/src/test/os-tests/tests/mac/mac_ktest_common.h
new file mode 100644
index 0000000000..885bb7bd13
--- /dev/null
+++ b/usr/src/test/os-tests/tests/mac/mac_ktest_common.h
@@ -0,0 +1,76 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source.  A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright 2025 Oxide Computer Compnay
+ */
+
+#ifndef _MAC_KTEST_COMMON_H
+#define	_MAC_KTEST_COMMON_H
+
+#ifdef	__cplusplus
+extern "C" {
+#endif
+
+#include <sys/stdbool.h>
+#include <sys/types.h>
+#include <sys/types32.h>
+
+typedef struct snoop_pkt_hdr {
+	uint32_t		sph_origlen;
+	uint32_t		sph_msglen;
+	uint32_t		sph_totlen;
+	uint32_t		sph_drops;
+#if defined(_LP64)
+	struct timeval32	sph_timestamp;
+#else
+#error	"ktest is expected to be 64-bit for now"
+#endif
+} snoop_pkt_hdr_t;
+
+typedef struct snoop_file_hdr {
+	char		sfh_magic[8];
+	uint32_t	sfh_vers;
+	uint32_t	sfh_mac_type;
+} snoop_file_hdr_t;
+
+typedef struct pkt_cap_iter {
+	int		pci_fd;
+	const char	*pci_base;
+	size_t		pci_map_sz;
+	size_t		pci_sz;
+	size_t		pci_offset;
+} pkt_cap_iter_t;
+
+extern pkt_cap_iter_t *pkt_cap_open(int);
+extern void pkt_cap_close(pkt_cap_iter_t *);
+extern void pkt_cap_reset(pkt_cap_iter_t *);
+extern bool pkt_cap_next(pkt_cap_iter_t *, const void **, uint_t *);
+
+struct payload_opts {
+	uint_t		po_padding;
+	boolean_t	po_cksum_partial;
+	boolean_t	po_cksum_full;
+	boolean_t	po_cksum_ipv4;
+	boolean_t	po_split_ether;
+	uint_t		po_split_manual;
+	uint_t		po_mss;
+};
+
+extern char *build_payload(const void *, uint_t, const void *, uint_t,
+    const struct payload_opts *, size_t *);
+extern char *serialize_pkt_chain(pkt_cap_iter_t *iter, uint_t *sizep);
+
+#ifdef	__cplusplus
+}
+#endif
+
+#endif /* _MAC_KTEST_COMMON_H */
diff --git a/usr/src/test/os-tests/tests/mac/mac_lso.c b/usr/src/test/os-tests/tests/mac/mac_lso.c
new file mode 100644
index 0000000000..e84e9e82a8
--- /dev/null
+++ b/usr/src/test/os-tests/tests/mac/mac_lso.c
@@ -0,0 +1,214 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source.  A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright 2025 Oxide Computer Compnay
+ */
+
+/*
+ * Executor for mac_sw_lso() ktests.
+ *
+ * This program builds up the packed nvlist payloads expected by the ktest for
+ * mac_sw_lso().  The caller provides a snoop(1) with one packet, which may not
+ * have valid checksums. This operates the runner similarly to mac_cksum:
+ * required checksum types are specified via option flags, and the output mblk
+ * chain is compared byte-for-byte with equality against a separate output
+ * snoop file.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <err.h>
+#include <errno.h>
+#include <libgen.h>
+
+#include <libktest.h>
+
+#include "mac_ktest_common.h"
+
+static ktest_hdl_t *kthdl = NULL;
+const char *mac_lso_cmd = "";
+
+static void __NORETURN
+mac_lso_usage(void)
+{
+	(void) fprintf(stderr, "Usage: %s [flags] [opts] <cap_file_in> "
+	    "<cap_file_out>\n\n"
+	    "Flags:\n"
+	    "\t-4\temulate HCK_IPV4_HDRCKSUM\n"
+	    "\t-f\temulate HCK_FULLCKSUM\t(cannot be used with -p)\n"
+	    "\t-p\temulate HCK_PARTIALCKSUM\t(cannot be used with -f)\n"
+	    "\t\t\tOne of -f/-p *must* be provided.\n"
+	    "\t-e\tsplit mblk after Ethernet header\n"
+	    "Options:\n"
+	    "\t-b <len>\tpad mblk with <len> bytes (must be even)\n"
+	    "\t-s <len>\tsplit mblk after len bytes (must be even)\n"
+	    "\t\t\tif -e is specified, will be applied after that split\n"
+	    "\t-m <len>\tmaximum segment size for LSO (default 1448)\n"
+	    "Arguments:\n"
+	    "\t<cap_file_in> is a snoop capture containing one test packet.\n"
+	    "\t<cap_file_out> is a snoop capture of expected output packets.\n"
+	    "\tInput packets may or may not have filled L3/L4 checksums, as\n"
+	    "\tclients will have different expectations about which\n",
+	    "\tchecksum offloads are available.\n",
+	    mac_lso_cmd);
+	exit(EXIT_FAILURE);
+}
+
+int
+main(int argc, char *argv[])
+{
+	/* Peel off command name for usage */
+	mac_lso_cmd = basename(argv[0]);
+	argc--;
+	argv++;
+	optind = 0;
+	const char *errstr = NULL;
+
+	struct payload_opts popts = {
+		.po_mss = 1448
+	};
+	int c;
+	while ((c = getopt(argc, argv, "4fpeb:s:m:")) != -1) {
+		switch (c) {
+		case 'p':
+			popts.po_cksum_partial = B_TRUE;
+			break;
+		case 'f':
+			popts.po_cksum_full = B_TRUE;
+			break;
+		case '4':
+			popts.po_cksum_ipv4 = B_TRUE;
+			break;
+		case 'b':
+			popts.po_padding =
+			    strtonumx(optarg, 0, UINT16_MAX, &errstr, 0);
+			if (errstr != NULL) {
+				errx(EXIT_FAILURE,
+				    "invalid padding value %s: %s",
+				    optarg, errstr);
+			}
+			break;
+		case 'e':
+			popts.po_split_ether = B_TRUE;
+			break;
+		case 's':
+			popts.po_split_manual =
+			    strtonumx(optarg, 0, UINT16_MAX, &errstr, 0);
+			if (errstr != NULL) {
+				errx(EXIT_FAILURE,
+				    "invalid split value %s: %s",
+				    optarg, errstr);
+			}
+			break;
+		case 'm':
+			popts.po_mss =
+			    strtonumx(optarg, 0, UINT32_MAX, &errstr, 0);
+			if (errstr != NULL) {
+				errx(EXIT_FAILURE,
+				    "invalid MSS value %s: %s",
+				    optarg, errstr);
+			}
+			break;
+
+		case '?':
+			warnx("unknown option: -%c", optopt);
+			mac_lso_usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 2) {
+		(void) fprintf(stderr,
+		    "cap_file_in and cap_file_out are required arguments\n");
+		mac_lso_usage();
+	}
+
+	if (popts.po_cksum_full == popts.po_cksum_partial) {
+		errx(EXIT_FAILURE, "specify exactly one of -f/-p");
+	}
+
+	int in_fd = open(argv[0], O_RDONLY);
+	if (in_fd < 0) {
+		err(EXIT_FAILURE, "could not open input cap file %s", argv[0]);
+	}
+
+	int out_fd = open(argv[1], O_RDONLY);
+	if (out_fd < 0) {
+		err(EXIT_FAILURE, "could not open output cap file %s", argv[0]);
+	}
+
+	pkt_cap_iter_t *in_iter = pkt_cap_open(in_fd);
+	if (in_iter == NULL) {
+		err(EXIT_FAILURE, "unrecognized cap file %s", argv[0]);
+	}
+
+	pkt_cap_iter_t *out_iter = pkt_cap_open(out_fd);
+	if (out_iter == NULL) {
+		err(EXIT_FAILURE, "unrecognized cap file %s", argv[1]);
+	}
+
+	const void *pkt_buf = NULL;
+	uint_t pkt_sz;
+	if (!pkt_cap_next(in_iter, &pkt_buf, &pkt_sz)) {
+		err(EXIT_FAILURE, "no packets in input capture");
+	}
+
+	uint_t out_pkt_sz;
+	char *out_pkt_buf = serialize_pkt_chain(out_iter, &out_pkt_sz);
+	if (out_pkt_buf == NULL) {
+		err(EXIT_FAILURE, "failed to read output packet stream");
+	}
+
+	if ((kthdl = ktest_init()) == NULL) {
+		err(EXIT_FAILURE, "could not initialize libktest");
+	}
+	if (!ktest_mod_load("mac")) {
+		err(EXIT_FAILURE, "could not load mac ktest module");
+	}
+
+	ktest_run_req_t req = {
+		.krq_module = "mac",
+		.krq_suite = "lso",
+		.krq_test = "mac_sw_lso_test",
+	};
+	size_t payload_sz;
+	char *payload =
+	    build_payload(pkt_buf, pkt_sz, out_pkt_buf, out_pkt_sz, &popts,
+	    &payload_sz);
+	req.krq_input = (uchar_t *)payload;
+	req.krq_input_len = (uint_t)payload_sz;
+
+	ktest_run_result_t result = { 0 };
+	if (!ktest_run(kthdl, &req, &result)) {
+		err(EXIT_FAILURE, "failure while attempting ktest run");
+	}
+	free(payload);
+	free(out_pkt_buf);
+
+	const char *code_name = ktest_code_name(result.krr_code);
+	(void) printf("%s\t(len: %u)\n", code_name, pkt_sz);
+	if (result.krr_msg != NULL) {
+		if (result.krr_code != KTEST_CODE_PASS) {
+			(void) printf("MSG: %s\n", result.krr_msg);
+		}
+		free(result.krr_msg);
+	}
+
+	pkt_cap_close(in_iter);
+	pkt_cap_close(out_iter);
+	ktest_fini(kthdl);
+
+	return (result.krr_code == KTEST_CODE_PASS ? EXIT_SUCCESS :
+	    EXIT_FAILURE);
+}
diff --git a/usr/src/uts/common/inet/ip/ip_if.c b/usr/src/uts/common/inet/ip/ip_if.c
index 5b0f3fd565..b3910373a4 100644
--- a/usr/src/uts/common/inet/ip/ip_if.c
+++ b/usr/src/uts/common/inet/ip/ip_if.c
@@ -24,7 +24,7 @@
  * Copyright (c) 2013 by Delphix. All rights reserved.
  * Copyright (c) 2016, Joyent, Inc. All rights reserved.
  * Copyright (c) 2014, OmniTI Computer Consulting, Inc. All rights reserved.
- * Copyright 2023 Oxide Computer Company
+ * Copyright 2025 Oxide Computer Company
  */
 
 /*
@@ -2011,10 +2011,14 @@ ill_capability_direct_enable(ill_t *ill)
 	dld_capab_direct_t	direct;
 	int			rc;
 
-	ASSERT(!ill->ill_isv6 && IAM_WRITER_ILL(ill));
+	ASSERT(IAM_WRITER_ILL(ill));
 
 	bzero(&direct, sizeof (direct));
-	direct.di_rx_cf = (uintptr_t)ip_input;
+	if (ill->ill_isv6) {
+		direct.di_rx_cf = (uintptr_t)ip_input_v6;
+	} else {
+		direct.di_rx_cf = (uintptr_t)ip_input;
+	}
 	direct.di_rx_ch = ill;
 
 	rc = idc->idc_capab_df(idc->idc_capab_dh, DLD_CAPAB_DIRECT, &direct,
@@ -2050,7 +2054,7 @@ ill_capability_poll_enable(ill_t *ill)
 	dld_capab_poll_t	poll;
 	int			rc;
 
-	ASSERT(!ill->ill_isv6 && IAM_WRITER_ILL(ill));
+	ASSERT(IAM_WRITER_ILL(ill));
 
 	bzero(&poll, sizeof (poll));
 	poll.poll_ring_add_cf = (uintptr_t)ip_squeue_add_ring;
@@ -2118,10 +2122,8 @@ ill_capability_dld_enable(ill_t *ill)
 	ASSERT(IAM_WRITER_ILL(ill));
 
 	ill_mac_perim_enter(ill, &mph);
-	if (!ill->ill_isv6) {
-		ill_capability_direct_enable(ill);
-		ill_capability_poll_enable(ill);
-	}
+	ill_capability_direct_enable(ill);
+	ill_capability_poll_enable(ill);
 	ill_capability_lso_enable(ill);
 	ill->ill_capabilities |= ILL_CAPAB_DLD;
 	ill_mac_perim_exit(ill, mph);
diff --git a/usr/src/uts/common/inet/ip_impl.h b/usr/src/uts/common/inet/ip_impl.h
index 36d53ef016..b51e2de376 100644
--- a/usr/src/uts/common/inet/ip_impl.h
+++ b/usr/src/uts/common/inet/ip_impl.h
@@ -169,26 +169,6 @@ extern "C" {
 #define	ILL_DIRECT_CAPABLE(ill)						\
 	(((ill)->ill_capabilities & ILL_CAPAB_DLD_DIRECT) != 0)
 
-/*
- * Determine if a mblk needs to take the "slow path", aka OTH
- * softring. There are multiple reasons why a mblk might take the slow
- * path.
- *
- * o The mblk is not a data message.
- *
- * o There is more than one outstanding reference to the mblk.
- *
- * o The IP header is not aligned (we assume alignment in the checksum
- *   routine).
- *
- * o The mblk doesn't contain enough data to populate a simple IP header.
- */
-#define	MBLK_RX_FANOUT_SLOWPATH(mp, ipha)				\
-	(DB_TYPE(mp) != M_DATA ||					\
-	(DB_REF(mp) != 1) ||						\
-	!OK_32PTR(ipha) ||						\
-	(((uchar_t *)ipha + IP_SIMPLE_HDR_LENGTH) >= (mp)->b_wptr))
-
 /*
  * In non-global zone exclusive IP stacks, data structures such as IRE
  * entries pretend that they're in the global zone.  The following
diff --git a/usr/src/uts/common/io/cxgbe/t4nex/t4_sge.c b/usr/src/uts/common/io/cxgbe/t4nex/t4_sge.c
index 7ca2bfe8a5..06d0e3f6d5 100644
--- a/usr/src/uts/common/io/cxgbe/t4nex/t4_sge.c
+++ b/usr/src/uts/common/io/cxgbe/t4nex/t4_sge.c
@@ -2523,8 +2523,8 @@ csum_to_ctrl(const struct txinfo *txinfo, uint32_t chip_version,
 {
 	const mac_ether_offload_info_t *meoi = &txinfo->meoi;
 	const uint32_t tx_flags = txinfo->flags;
-	const boolean_t needs_l3_csum = (tx_flags & HW_LSO) != 0 ||
-	    (tx_flags & HCK_IPV4_HDRCKSUM) != 0;
+	const boolean_t needs_l3_csum = ((tx_flags & HW_LSO) != 0 || (tx_flags &
+	    HCK_IPV4_HDRCKSUM) != 0) && meoi->meoi_l3proto == ETHERTYPE_IP;
 	const boolean_t needs_l4_csum = (tx_flags & HW_LSO) != 0 ||
 	    (tx_flags & (HCK_FULLCKSUM | HCK_PARTIALCKSUM)) != 0;
 
@@ -2541,8 +2541,7 @@ csum_to_ctrl(const struct txinfo *txinfo, uint32_t chip_version,
 
 	if (needs_l3_csum) {
 		/* Only IPv4 checksums are supported (for L3) */
-		if ((meoi->meoi_flags & MEOI_L3INFO_SET) == 0 ||
-		    meoi->meoi_l3proto != ETHERTYPE_IP) {
+		if ((meoi->meoi_flags & MEOI_L3INFO_SET) == 0) {
 			*ctrlp = ctrl;
 			return (COS_FAIL);
 		}
diff --git a/usr/src/uts/common/io/dld/dld_proto.c b/usr/src/uts/common/io/dld/dld_proto.c
index 0d1eb314dc..7ee13ed77a 100644
--- a/usr/src/uts/common/io/dld/dld_proto.c
+++ b/usr/src/uts/common/io/dld/dld_proto.c
@@ -22,6 +22,7 @@
  * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
  * Copyright 2012, Nexenta Systems, Inc. All rights reserved.
  * Copyright (c) 2018, Joyent, Inc.
+ * Copyright 2025 Oxide Computer Company
  */
 
 /*
@@ -1420,7 +1421,8 @@ dld_capab_poll_enable(dld_str_t *dsp, dld_capab_poll_t *poll)
 	 * (IP) to directly poll the soft ring (since DLS processing
 	 * hasn't been done) nor can we allow DLS bypass.
 	 */
-	if (!mac_rx_bypass_set(dsp->ds_mch, dsp->ds_rx, dsp->ds_rx_arg))
+	if (!mac_rx_bypass_set(dsp->ds_mch, dsp->ds_rx, dsp->ds_rx_arg,
+	    dsp->ds_sap == ETHERTYPE_IPV6))
 		return (ENOTSUP);
 
 	/*
@@ -1541,18 +1543,10 @@ dld_capab(dld_str_t *dsp, uint_t type, void *data, uint_t flags)
 
 	switch (type) {
 	case DLD_CAPAB_DIRECT:
-		if (dsp->ds_sap == ETHERTYPE_IPV6) {
-			err = ENOTSUP;
-			break;
-		}
 		err = dld_capab_direct(dsp, data, flags);
 		break;
 
 	case DLD_CAPAB_POLL:
-		if (dsp->ds_sap == ETHERTYPE_IPV6) {
-			err = ENOTSUP;
-			break;
-		}
 		err =  dld_capab_poll(dsp, data, flags);
 		break;
 
diff --git a/usr/src/uts/common/io/mac/mac_client.c b/usr/src/uts/common/io/mac/mac_client.c
index 94ad441a65..2577942ce9 100644
--- a/usr/src/uts/common/io/mac/mac_client.c
+++ b/usr/src/uts/common/io/mac/mac_client.c
@@ -24,6 +24,7 @@
  * Copyright 2019 Joyent, Inc.
  * Copyright 2017 RackTop Systems.
  * Copyright 2022 OmniOS Community Edition (OmniOSce) Association.
+ * Copyright 2025 Oxide Computer Company
  */
 
 /*
@@ -1363,8 +1364,10 @@ mac_client_open(mac_handle_t mh, mac_client_handle_t *mchp, char *name,
 	mcip->mci_rx_p_fn = NULL;
 	mcip->mci_rx_p_arg = NULL;
 	mcip->mci_p_unicast_list = NULL;
-	mcip->mci_direct_rx_fn = NULL;
-	mcip->mci_direct_rx_arg = NULL;
+	mcip->mci_direct_rx.mdrx_v4 = NULL;
+	mcip->mci_direct_rx.mdrx_v6 = NULL;
+	mcip->mci_direct_rx.mdrx_arg_v4 = NULL;
+	mcip->mci_direct_rx.mdrx_arg_v6 = NULL;
 	mcip->mci_vidcache = MCIP_VIDCACHE_INVALID;
 
 	mcip->mci_unicast_list = NULL;
@@ -1549,7 +1552,8 @@ mac_client_close(mac_client_handle_t mch, uint16_t flags)
  * B_FALSE if it's not possible to enable bypass.
  */
 boolean_t
-mac_rx_bypass_set(mac_client_handle_t mch, mac_direct_rx_t rx_fn, void *arg1)
+mac_rx_bypass_set(mac_client_handle_t mch, mac_direct_rx_t rx_fn, void *arg1,
+    boolean_t v6)
 {
 	mac_client_impl_t	*mcip = (mac_client_impl_t *)mch;
 	mac_impl_t		*mip = mcip->mci_mip;
@@ -1568,8 +1572,13 @@ mac_rx_bypass_set(mac_client_handle_t mch, mac_direct_rx_t rx_fn, void *arg1)
 	 * These are not accessed directly in the data path, and hence
 	 * don't need any protection
 	 */
-	mcip->mci_direct_rx_fn = rx_fn;
-	mcip->mci_direct_rx_arg = arg1;
+	if (v6) {
+		mcip->mci_direct_rx.mdrx_v6 = rx_fn;
+		mcip->mci_direct_rx.mdrx_arg_v6 = arg1;
+	} else {
+		mcip->mci_direct_rx.mdrx_v4 = rx_fn;
+		mcip->mci_direct_rx.mdrx_arg_v4 = arg1;
+	}
 	return (B_TRUE);
 }
 
@@ -4331,21 +4340,7 @@ i_mac_capab_get(mac_handle_t mh, mac_capab_t cap, void *cap_data)
 	if (mip->mi_bridge_link != NULL) {
 		return (cap == MAC_CAPAB_NO_ZCOPY);
 	} else if (mip->mi_callbacks->mc_callbacks & MC_GETCAPAB) {
-		boolean_t res;
-
-		res = mip->mi_getcapab(mip->mi_driver, cap, cap_data);
-		/*
-		 * Until we have suppport for TSOv6 emulation in the MAC
-		 * loopback path, do not allow the TSOv6 capability to be
-		 * advertised to consumers.
-		 */
-		if (res && cap == MAC_CAPAB_LSO) {
-			mac_capab_lso_t *cap_lso = cap_data;
-
-			cap_lso->lso_flags &= ~LSO_TX_BASIC_TCP_IPV6;
-			cap_lso->lso_basic_tcp_ipv6.lso_max = 0;
-		}
-		return (res);
+		return (mip->mi_getcapab(mip->mi_driver, cap, cap_data));
 	} else {
 		return (B_FALSE);
 	}
diff --git a/usr/src/uts/common/io/mac/mac_datapath_setup.c b/usr/src/uts/common/io/mac/mac_datapath_setup.c
index 0da404853c..151942451f 100644
--- a/usr/src/uts/common/io/mac/mac_datapath_setup.c
+++ b/usr/src/uts/common/io/mac/mac_datapath_setup.c
@@ -22,6 +22,7 @@
  * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
  * Copyright 2018 Joyent, Inc.
  * Copyright 2020 RackTop Systems.
+ * Copyright 2025 Oxide Computer Company
  */
 
 #include <sys/types.h>
@@ -395,15 +396,29 @@ mac_srs_client_poll_enable(mac_client_impl_t *mcip,
 
 	softring = mac_srs->srs_soft_ring_head;
 	while (softring != NULL) {
-		if (softring->s_ring_type & (ST_RING_TCP | ST_RING_UDP)) {
+		if (mcip->mci_direct_rx.mdrx_v4 != NULL &&
+		    softring->s_ring_type & (ST_RING_TCP | ST_RING_UDP)) {
 			/*
 			 * TCP and UDP support DLS bypass. Squeue polling
 			 * support implies DLS bypass since the squeue poll
 			 * path does not have DLS processing.
 			 */
 			mac_soft_ring_dls_bypass(softring,
-			    mcip->mci_direct_rx_fn, mcip->mci_direct_rx_arg);
+			    mcip->mci_direct_rx.mdrx_v4,
+			    mcip->mci_direct_rx.mdrx_arg_v4);
 		}
+		if (mcip->mci_direct_rx.mdrx_v6 != NULL &&
+		    softring->s_ring_type & (ST_RING_TCP6 | ST_RING_UDP6)) {
+			/*
+			 * TCP and UDP support DLS bypass. Squeue polling
+			 * support implies DLS bypass since the squeue poll
+			 * path does not have DLS processing.
+			 */
+			mac_soft_ring_dls_bypass(softring,
+			    mcip->mci_direct_rx.mdrx_v6,
+			    mcip->mci_direct_rx.mdrx_arg_v6);
+		}
+
 		/*
 		 * Non-TCP protocols don't support squeues. Hence we don't
 		 * make any ring addition callbacks for non-TCP rings
@@ -1207,9 +1222,15 @@ mac_srs_fanout_list_alloc(mac_soft_ring_set_t *mac_srs)
 		mac_srs->srs_tcp_soft_rings = (mac_soft_ring_t **)
 		    kmem_zalloc(sizeof (mac_soft_ring_t *) * MAX_SR_FANOUT,
 		    KM_SLEEP);
+		mac_srs->srs_tcp6_soft_rings = (mac_soft_ring_t **)
+		    kmem_zalloc(sizeof (mac_soft_ring_t *) * MAX_SR_FANOUT,
+		    KM_SLEEP);
 		mac_srs->srs_udp_soft_rings = (mac_soft_ring_t **)
 		    kmem_zalloc(sizeof (mac_soft_ring_t *) * MAX_SR_FANOUT,
 		    KM_SLEEP);
+		mac_srs->srs_udp6_soft_rings = (mac_soft_ring_t **)
+		    kmem_zalloc(sizeof (mac_soft_ring_t *) * MAX_SR_FANOUT,
+		    KM_SLEEP);
 		mac_srs->srs_oth_soft_rings = (mac_soft_ring_t **)
 		    kmem_zalloc(sizeof (mac_soft_ring_t *) * MAX_SR_FANOUT,
 		    KM_SLEEP);
@@ -1668,7 +1689,9 @@ mac_client_update_classifier(mac_client_impl_t *mcip, boolean_t enable)
 static void
 mac_srs_update_fanout_list(mac_soft_ring_set_t *mac_srs)
 {
-	int tcp_count = 0, udp_count = 0, oth_count = 0, tx_count = 0;
+	int tcp_count = 0, tcp6_count = 0, udp_count = 0, udp6_count = 0,
+	    oth_count = 0, tx_count = 0;
+
 	mac_soft_ring_t *softring;
 
 	softring = mac_srs->srs_soft_ring_head;
@@ -1676,6 +1699,8 @@ mac_srs_update_fanout_list(mac_soft_ring_set_t *mac_srs)
 		ASSERT(mac_srs->srs_soft_ring_count == 0);
 		mac_srs->srs_tcp_ring_count = 0;
 		mac_srs->srs_udp_ring_count = 0;
+		mac_srs->srs_tcp6_ring_count = 0;
+		mac_srs->srs_udp6_ring_count = 0;
 		mac_srs->srs_oth_ring_count = 0;
 		mac_srs->srs_tx_ring_count = 0;
 		return;
@@ -1684,8 +1709,12 @@ mac_srs_update_fanout_list(mac_soft_ring_set_t *mac_srs)
 	while (softring != NULL) {
 		if (softring->s_ring_type & ST_RING_TCP) {
 			mac_srs->srs_tcp_soft_rings[tcp_count++] = softring;
+		} else if (softring->s_ring_type & ST_RING_TCP6) {
+			mac_srs->srs_tcp6_soft_rings[tcp6_count++] = softring;
 		} else if (softring->s_ring_type & ST_RING_UDP) {
 			mac_srs->srs_udp_soft_rings[udp_count++] = softring;
+		} else if (softring->s_ring_type & ST_RING_UDP6) {
+			mac_srs->srs_udp6_soft_rings[udp6_count++] = softring;
 		} else if (softring->s_ring_type & ST_RING_OTH) {
 			mac_srs->srs_oth_soft_rings[oth_count++] = softring;
 		} else {
@@ -1695,10 +1724,12 @@ mac_srs_update_fanout_list(mac_soft_ring_set_t *mac_srs)
 		softring = softring->s_ring_next;
 	}
 
-	ASSERT(mac_srs->srs_soft_ring_count ==
-	    (tcp_count + udp_count + oth_count + tx_count));
+	ASSERT(mac_srs->srs_soft_ring_count == (tcp_count + tcp6_count +
+	    udp_count + udp6_count + oth_count + tx_count));
 	mac_srs->srs_tcp_ring_count = tcp_count;
+	mac_srs->srs_tcp6_ring_count = tcp6_count;
 	mac_srs->srs_udp_ring_count = udp_count;
+	mac_srs->srs_udp6_ring_count = udp6_count;
 	mac_srs->srs_oth_ring_count = oth_count;
 	mac_srs->srs_tx_ring_count = tx_count;
 }
@@ -1728,10 +1759,11 @@ mac_srs_create_proto_softrings(int id, uint16_t type, pri_t pri,
 	 * TCP and UDP support DLS bypass. In addition TCP
 	 * squeue can also poll their corresponding soft rings.
 	 */
-	if (set_bypass && (mcip->mci_resource_arg != NULL)) {
+	if (set_bypass && mcip->mci_direct_rx.mdrx_v4 != NULL &&
+	    (mcip->mci_resource_arg != NULL)) {
 		mac_soft_ring_dls_bypass(softring,
-		    mcip->mci_direct_rx_fn,
-		    mcip->mci_direct_rx_arg);
+		    mcip->mci_direct_rx.mdrx_v4,
+		    mcip->mci_direct_rx.mdrx_arg_v4);
 
 		mrf.mrf_rx_arg = softring;
 		mrf.mrf_intr_handle = (mac_intr_handle_t)softring;
@@ -1759,13 +1791,47 @@ mac_srs_create_proto_softrings(int id, uint16_t type, pri_t pri,
 	    cpuid, rx_func, x_arg1, x_arg2);
 	softring->s_ring_rx_arg2 = NULL;
 
-	if (set_bypass && (mcip->mci_resource_arg != NULL)) {
+	if (set_bypass && mcip->mci_direct_rx.mdrx_v4 != NULL &&
+	    (mcip->mci_resource_arg != NULL)) {
+		mac_soft_ring_dls_bypass(softring,
+		    mcip->mci_direct_rx.mdrx_v4,
+		    mcip->mci_direct_rx.mdrx_arg_v4);
+	}
+
+	/* TCP for IPv6. */
+	softring = mac_soft_ring_create(id, mac_soft_ring_worker_wait,
+	    (type|ST_RING_TCP6), pri, mcip, mac_srs,
+	    cpuid, rx_func, x_arg1, x_arg2);
+	softring->s_ring_rx_arg2 = NULL;
+
+	if (set_bypass && mcip->mci_direct_rx.mdrx_v6 != NULL &&
+	    (mcip->mci_resource_arg != NULL)) {
+		mac_soft_ring_dls_bypass(softring,
+		    mcip->mci_direct_rx.mdrx_v6,
+		    mcip->mci_direct_rx.mdrx_arg_v6);
+
+		mrf.mrf_rx_arg = softring;
+		mrf.mrf_intr_handle = (mac_intr_handle_t)softring;
+
+		softring->s_ring_rx_arg2 =
+		    mcip->mci_resource_add((void *)mcip->mci_resource_arg,
+		    (mac_resource_t *)&mrf);
+	}
+
+	/* UDP for IPv6. */
+	softring = mac_soft_ring_create(id, mac_soft_ring_worker_wait,
+	    (type|ST_RING_UDP6), pri, mcip, mac_srs,
+	    cpuid, rx_func, x_arg1, x_arg2);
+	softring->s_ring_rx_arg2 = NULL;
+
+	if (set_bypass && mcip->mci_direct_rx.mdrx_v6 != NULL &&
+	    (mcip->mci_resource_arg != NULL)) {
 		mac_soft_ring_dls_bypass(softring,
-		    mcip->mci_direct_rx_fn,
-		    mcip->mci_direct_rx_arg);
+		    mcip->mci_direct_rx.mdrx_v6,
+		    mcip->mci_direct_rx.mdrx_arg_v6);
 	}
 
-	/* Create the Oth softrings which has to go through the DLS */
+	/* Create the Oth softrings which has to go through the DLS. */
 	softring = mac_soft_ring_create(id, mac_soft_ring_worker_wait,
 	    (type|ST_RING_OTH), pri, mcip, mac_srs,
 	    cpuid, rx_func, x_arg1, x_arg2);
@@ -1840,10 +1906,20 @@ mac_srs_fanout_modify(mac_client_impl_t *mcip, mac_direct_rx_t rx_func,
 				    (void *)mcip->mci_resource_arg,
 				    softring->s_ring_rx_arg2);
 			}
+			softring = mac_rx_srs->srs_tcp6_soft_rings[i];
+			if (softring->s_ring_rx_arg2 != NULL) {
+				mcip->mci_resource_remove(
+				    (void *)mcip->mci_resource_arg,
+				    softring->s_ring_rx_arg2);
+			}
 			mac_soft_ring_remove(mac_rx_srs,
 			    mac_rx_srs->srs_tcp_soft_rings[i]);
+			mac_soft_ring_remove(mac_rx_srs,
+			    mac_rx_srs->srs_tcp6_soft_rings[i]);
 			mac_soft_ring_remove(mac_rx_srs,
 			    mac_rx_srs->srs_udp_soft_rings[i]);
+			mac_soft_ring_remove(mac_rx_srs,
+			    mac_rx_srs->srs_udp6_soft_rings[i]);
 			mac_soft_ring_remove(mac_rx_srs,
 			    mac_rx_srs->srs_oth_soft_rings[i]);
 		}
@@ -1856,15 +1932,24 @@ mac_srs_fanout_modify(mac_client_impl_t *mcip, mac_direct_rx_t rx_func,
 		cpuid = srs_cpu->mc_rx_fanout_cpus[i];
 		(void) mac_soft_ring_bind(mac_rx_srs->srs_udp_soft_rings[i],
 		    cpuid);
+		(void) mac_soft_ring_bind(mac_rx_srs->srs_udp6_soft_rings[i],
+		    cpuid);
 		(void) mac_soft_ring_bind(mac_rx_srs->srs_oth_soft_rings[i],
 		    cpuid);
 		(void) mac_soft_ring_bind(mac_rx_srs->srs_tcp_soft_rings[i],
 		    cpuid);
+		(void) mac_soft_ring_bind(mac_rx_srs->srs_tcp6_soft_rings[i],
+		    cpuid);
 		softring = mac_rx_srs->srs_tcp_soft_rings[i];
 		if (softring->s_ring_rx_arg2 != NULL) {
 			mcip->mci_resource_bind((void *)mcip->mci_resource_arg,
 			    softring->s_ring_rx_arg2, cpuid);
 		}
+		softring = mac_rx_srs->srs_tcp6_soft_rings[i];
+		if (softring->s_ring_rx_arg2 != NULL) {
+			mcip->mci_resource_bind((void *)mcip->mci_resource_arg,
+			    softring->s_ring_rx_arg2, cpuid);
+		}
 	}
 
 	mac_srs_worker_bind(mac_rx_srs, srs_cpu->mc_rx_workerid);
@@ -3420,6 +3505,8 @@ mac_srs_fanout_list_free(mac_soft_ring_set_t *mac_srs)
 
 		ASSERT(mac_srs->srs_tcp_soft_rings == NULL);
 		ASSERT(mac_srs->srs_udp_soft_rings == NULL);
+		ASSERT(mac_srs->srs_tcp6_soft_rings == NULL);
+		ASSERT(mac_srs->srs_udp6_soft_rings == NULL);
 		ASSERT(mac_srs->srs_oth_soft_rings == NULL);
 		ASSERT(mac_srs->srs_tx_soft_rings != NULL);
 		kmem_free(mac_srs->srs_tx_soft_rings,
@@ -3432,14 +3519,27 @@ mac_srs_fanout_list_free(mac_soft_ring_set_t *mac_srs)
 		}
 	} else {
 		ASSERT(mac_srs->srs_tx_soft_rings == NULL);
+
 		ASSERT(mac_srs->srs_tcp_soft_rings != NULL);
 		kmem_free(mac_srs->srs_tcp_soft_rings,
 		    sizeof (mac_soft_ring_t *) * MAX_SR_FANOUT);
 		mac_srs->srs_tcp_soft_rings = NULL;
+
 		ASSERT(mac_srs->srs_udp_soft_rings != NULL);
 		kmem_free(mac_srs->srs_udp_soft_rings,
 		    sizeof (mac_soft_ring_t *) * MAX_SR_FANOUT);
 		mac_srs->srs_udp_soft_rings = NULL;
+
+		ASSERT(mac_srs->srs_tcp6_soft_rings != NULL);
+		kmem_free(mac_srs->srs_tcp6_soft_rings,
+		    sizeof (mac_soft_ring_t *) * MAX_SR_FANOUT);
+		mac_srs->srs_tcp6_soft_rings = NULL;
+
+		ASSERT(mac_srs->srs_udp6_soft_rings != NULL);
+		kmem_free(mac_srs->srs_udp6_soft_rings,
+		    sizeof (mac_soft_ring_t *) * MAX_SR_FANOUT);
+		mac_srs->srs_udp6_soft_rings = NULL;
+
 		ASSERT(mac_srs->srs_oth_soft_rings != NULL);
 		kmem_free(mac_srs->srs_oth_soft_rings,
 		    sizeof (mac_soft_ring_t *) * MAX_SR_FANOUT);
diff --git a/usr/src/uts/common/io/mac/mac_ktest.c b/usr/src/uts/common/io/mac/mac_ktest.c
index 7cc54d35d8..0ee4820c30 100644
--- a/usr/src/uts/common/io/mac/mac_ktest.c
+++ b/usr/src/uts/common/io/mac/mac_ktest.c
@@ -22,38 +22,56 @@
 #include <sys/dlpi.h>
 #include <sys/ethernet.h>
 #include <sys/ktest.h>
+#include <sys/mac_client.h>
 #include <sys/mac_provider.h>
 #include <sys/pattr.h>
+#include <sys/strsubr.h>
 #include <sys/strsun.h>
 
-typedef mblk_t *(*mac_sw_cksum_t)(mblk_t *, mac_emul_t);
-
 /* Arbitrary limits for cksum tests */
 #define	PADDING_MAX	32
 #define	SPLITS_MAX	8
 
-typedef struct cksum_test_params {
-	mblk_t		*ctp_mp;
-	uchar_t		*ctp_raw;
-	uint_t		ctp_raw_sz;
-	boolean_t	ctp_do_partial;
-	boolean_t	ctp_do_full;
-	boolean_t	ctp_do_ipv4;
-	uint_t		ctp_splits[SPLITS_MAX];
-} cksum_test_params_t;
+typedef struct emul_test_params {
+	mblk_t		*etp_mp;
+	uchar_t		*etp_raw;
+	uint_t		etp_raw_sz;
+	uchar_t		*etp_outputs;
+	uint_t		etp_outputs_sz;
+	boolean_t	etp_do_partial;
+	boolean_t	etp_do_full;
+	boolean_t	etp_do_ipv4;
+	boolean_t	etp_do_lso;
+	uint_t		etp_mss;
+	uint_t		etp_splits[SPLITS_MAX];
+} emul_test_params_t;
+
+static void
+etp_free(const emul_test_params_t *etp)
+{
+	if (etp->etp_mp != NULL) {
+		freemsgchain(etp->etp_mp);
+	}
+	if (etp->etp_raw != NULL) {
+		kmem_free(etp->etp_raw, etp->etp_raw_sz);
+	}
+	if (etp->etp_outputs != NULL) {
+		kmem_free(etp->etp_outputs, etp->etp_outputs_sz);
+	}
+}
 
 static mblk_t *
-cksum_alloc_pkt(const cksum_test_params_t *ctp, uint32_t padding)
+cksum_alloc_pkt(const emul_test_params_t *etp, uint32_t padding)
 {
-	uint32_t remain = ctp->ctp_raw_sz;
+	uint32_t remain = etp->etp_raw_sz;
 	uint_t split_idx = 0;
-	const uint8_t *pkt_bytes = ctp->ctp_raw;
+	const uint8_t *pkt_bytes = etp->etp_raw;
 
 	mblk_t *head = NULL, *tail = NULL;
 	while (remain > 0) {
-		const boolean_t has_split = ctp->ctp_splits[split_idx] != 0;
+		const boolean_t has_split = etp->etp_splits[split_idx] != 0;
 		const uint32_t to_copy = has_split ?
-		    MIN(remain, ctp->ctp_splits[split_idx]) : remain;
+		    MIN(remain, etp->etp_splits[split_idx]) : remain;
 		const uint32_t to_alloc = padding + to_copy;
 
 		mblk_t *mp = allocb(to_alloc, 0);
@@ -89,13 +107,13 @@ cksum_alloc_pkt(const cksum_test_params_t *ctp, uint32_t padding)
 }
 
 static boolean_t
-cksum_test_parse_input(ktest_ctx_hdl_t *ctx, cksum_test_params_t *ctp)
+emul_test_parse_input(ktest_ctx_hdl_t *ctx, emul_test_params_t *etp)
 {
 	uchar_t *bytes;
 	size_t num_bytes = 0;
 
 	ktest_get_input(ctx, &bytes, &num_bytes);
-	bzero(ctp, sizeof (*ctp));
+	bzero(etp, sizeof (*etp));
 
 	nvlist_t *params = NULL;
 	if (nvlist_unpack((char *)bytes, num_bytes, &params, KM_SLEEP) != 0) {
@@ -103,8 +121,8 @@ cksum_test_parse_input(ktest_ctx_hdl_t *ctx, cksum_test_params_t *ctp)
 		return (B_FALSE);
 	}
 
-	uchar_t *pkt_bytes;
-	uint_t pkt_sz;
+	uchar_t *pkt_bytes, *out_pkt_bytes;
+	uint_t pkt_sz, out_pkt_sz;
 
 	if (nvlist_lookup_byte_array(params, "pkt_bytes", &pkt_bytes,
 	    &pkt_sz) != 0) {
@@ -116,6 +134,19 @@ cksum_test_parse_input(ktest_ctx_hdl_t *ctx, cksum_test_params_t *ctp)
 		goto bail;
 	}
 
+	if (nvlist_lookup_byte_array(params, "out_pkt_bytes", &out_pkt_bytes,
+	    &out_pkt_sz) == 0) {
+		if (out_pkt_sz < sizeof (uint32_t)) {
+			KT_ERROR(ctx, "Serialized packets need a u32 length");
+			goto bail;
+		}
+		etp->etp_outputs = kmem_alloc(out_pkt_sz, KM_SLEEP);
+		bcopy(out_pkt_bytes, etp->etp_outputs, out_pkt_sz);
+		etp->etp_outputs_sz = out_pkt_sz;
+	}
+
+	(void) nvlist_lookup_uint32(params, "mss", &etp->etp_mss);
+
 	uint32_t padding = 0;
 	(void) nvlist_lookup_uint32(params, "padding", &padding);
 	if (padding & 1) {
@@ -126,9 +157,9 @@ cksum_test_parse_input(ktest_ctx_hdl_t *ctx, cksum_test_params_t *ctp)
 		goto bail;
 	}
 
-	ctp->ctp_do_ipv4 = fnvlist_lookup_boolean(params, "cksum_ipv4");
-	ctp->ctp_do_partial = fnvlist_lookup_boolean(params, "cksum_partial");
-	ctp->ctp_do_full = fnvlist_lookup_boolean(params, "cksum_full");
+	etp->etp_do_ipv4 = fnvlist_lookup_boolean(params, "cksum_ipv4");
+	etp->etp_do_partial = fnvlist_lookup_boolean(params, "cksum_partial");
+	etp->etp_do_full = fnvlist_lookup_boolean(params, "cksum_full");
 
 	uint32_t *splits;
 	uint_t nsplits;
@@ -146,21 +177,21 @@ cksum_test_parse_input(ktest_ctx_hdl_t *ctx, cksum_test_params_t *ctp)
 				KT_ERROR(ctx, "Splits must be 2-byte aligned");
 				goto bail;
 			}
-			ctp->ctp_splits[i] = splits[i];
+			etp->etp_splits[i] = splits[i];
 		}
 	}
 
-	if (ctp->ctp_do_partial && ctp->ctp_do_full) {
+	if (etp->etp_do_partial && etp->etp_do_full) {
 		KT_ERROR(ctx, "Cannot request full and partial cksum");
 		goto bail;
 	}
 
-	ctp->ctp_raw = kmem_alloc(pkt_sz, KM_SLEEP);
-	bcopy(pkt_bytes, ctp->ctp_raw, pkt_sz);
-	ctp->ctp_raw_sz = pkt_sz;
+	etp->etp_raw = kmem_alloc(pkt_sz, KM_SLEEP);
+	bcopy(pkt_bytes, etp->etp_raw, pkt_sz);
+	etp->etp_raw_sz = pkt_sz;
 
-	ctp->ctp_mp = cksum_alloc_pkt(ctp, padding);
-	if (ctp->ctp_mp == NULL) {
+	etp->etp_mp = cksum_alloc_pkt(etp, padding);
+	if (etp->etp_mp == NULL) {
 		KT_ERROR(ctx, "Could not allocate mblk");
 		goto bail;
 	}
@@ -169,9 +200,8 @@ cksum_test_parse_input(ktest_ctx_hdl_t *ctx, cksum_test_params_t *ctp)
 	return (B_TRUE);
 
 bail:
-	if (ctp->ctp_raw != NULL) {
-		kmem_free(ctp->ctp_raw, ctp->ctp_raw_sz);
-	}
+	etp_free(etp);
+
 	if (params != NULL) {
 		nvlist_free(params);
 	}
@@ -181,7 +211,7 @@ bail:
 /* Calculate pseudo-header checksum for a packet */
 static uint16_t
 cksum_calc_pseudo(ktest_ctx_hdl_t *ctx, const uint8_t *pkt_data,
-    const mac_ether_offload_info_t *meoi)
+    const mac_ether_offload_info_t *meoi, boolean_t exclude_len)
 {
 	if ((meoi->meoi_flags & MEOI_L4INFO_SET) == 0) {
 		KT_ERROR(ctx, "MEOI lacks L4 info");
@@ -234,7 +264,10 @@ cksum_calc_pseudo(ktest_ctx_hdl_t *ctx, const uint8_t *pkt_data,
 		ulp_len = ntohs(ipha->ipha_length) - meoi->meoi_l3hlen;
 	}
 
-	cksum += htons(ulp_len);
+	/* LSO packets omit ULP length from cksum since it may be changing */
+	if (!exclude_len) {
+		cksum += htons(ulp_len);
+	}
 
 	cksum = (cksum >> 16) + (cksum & 0xffff);
 	cksum = (cksum >> 16) + (cksum & 0xffff);
@@ -266,26 +299,26 @@ mblk_write16(mblk_t *mp, uint_t off, uint16_t val)
 	*datap = val;
 }
 
-/* Compare resulting mblk with known good value in test parameters.  */
+/* Compare an individual mblk with known good value in test parameters.  */
 static boolean_t
-cksum_result_compare(ktest_ctx_hdl_t *ctx, const cksum_test_params_t *ctp,
+pkt_compare(ktest_ctx_hdl_t *ctx, const uchar_t *buf, const uint_t len,
     mblk_t *mp)
 {
-	if (msgdsize(mp) != ctp->ctp_raw_sz) {
-		KT_FAIL(ctx, "mp size %u != %u", msgdsize(mp), ctp->ctp_raw_sz);
+	if (msgdsize(mp) != len) {
+		KT_FAIL(ctx, "mp size %u != %u", msgdsize(mp), len);
 		return (B_FALSE);
 	}
 
 	uint32_t fail_val = 0, good_val = 0;
 	uint_t mp_off = 0, fail_len = 0, i;
-	for (i = 0; i < ctp->ctp_raw_sz; i++) {
+	for (i = 0; i < len; i++) {
 		/*
 		 * If we encounter a mismatch, collect up to 4 bytes of context
 		 * to print with the failure.
 		 */
-		if (mp->b_rptr[mp_off] != ctp->ctp_raw[i] || fail_len != 0) {
+		if (mp->b_rptr[mp_off] != buf[i] || fail_len != 0) {
 			fail_val |= mp->b_rptr[mp_off] << (fail_len * 8);
-			good_val |= ctp->ctp_raw[i] << (fail_len * 8);
+			good_val |= buf[i] << (fail_len * 8);
 
 			fail_len++;
 			if (fail_len == 4) {
@@ -309,35 +342,59 @@ cksum_result_compare(ktest_ctx_hdl_t *ctx, const cksum_test_params_t *ctp,
 	return (B_TRUE);
 }
 
-/*
- * Verify mac_sw_cksum() emulation against an arbitrary input packet.  If the
- * packet is of a support protocol, any L3 and L4 checksums are cleared, and
- * then mac_sw_cksum() is called to perform the offload emulation.  Afterwards,
- * the packet is compared to see if it equasl the input, which is assumed to
- * have correct checksums.
- *
- * This can request either emulation via HCK_PARTIALCKSUM or HCK_FULLCKSUM.
- */
-static void
-mac_sw_cksum_test(ktest_ctx_hdl_t *ctx)
+/* Compare resulting mblk chain with known good values in test parameters. */
+static boolean_t
+pkt_result_compare_chain(ktest_ctx_hdl_t *ctx, const emul_test_params_t *etp,
+    mblk_t *mp)
 {
-	ddi_modhandle_t hdl = NULL;
-	mac_sw_cksum_t mac_sw_cksum = NULL;
+	uint_t remaining = etp->etp_outputs_sz;
+	const uchar_t *raw_cur = etp->etp_outputs;
+
+	uint_t idx = 0;
+	while (remaining != 0 && mp != NULL) {
+		uint32_t inner_pkt_len;
+		if (remaining < sizeof (inner_pkt_len)) {
+			KT_ERROR(ctx, "insufficient bytes to read packet len");
+			return (B_FALSE);
+		}
+		bcopy(raw_cur, &inner_pkt_len, sizeof (inner_pkt_len));
+		remaining -= sizeof (inner_pkt_len);
+		raw_cur += sizeof (inner_pkt_len);
 
-	if (ktest_hold_mod("mac", &hdl) != 0) {
-		KT_ERROR(ctx, "failed to hold 'mac' module");
-		return;
+		if (remaining < inner_pkt_len) {
+			KT_ERROR(ctx, "wanted %u bytes to read packet, had %u",
+			    inner_pkt_len, remaining);
+			return (B_FALSE);
+		}
+
+		if (!pkt_compare(ctx, raw_cur, inner_pkt_len, mp)) {
+			ktest_msg_prepend(ctx, "packet %u: ", idx);
+			return (B_FALSE);
+		}
+
+		remaining -= inner_pkt_len;
+		raw_cur += inner_pkt_len;
+		idx++;
+		mp = mp->b_next;
 	}
-	if (ktest_get_fn(hdl, "mac_sw_cksum", (void **)&mac_sw_cksum) != 0) {
-		KT_ERROR(ctx, "failed to resolve symbol mac`mac_sw_cksum");
-		goto cleanup;
+
+	if (remaining != 0) {
+		KT_FAIL(ctx, "fewer packets returned than expected");
+		return (B_FALSE);
 	}
 
-	cksum_test_params_t ctp;
-	if (!cksum_test_parse_input(ctx, &ctp)) {
-		goto cleanup;
+	if (mp != NULL) {
+		KT_FAIL(ctx, "more packets returned than expected");
+		return (B_FALSE);
 	}
-	mblk_t *mp = ctp.ctp_mp;
+
+	return (B_TRUE);
+}
+
+static void
+mac_hw_emul_test(ktest_ctx_hdl_t *ctx, emul_test_params_t *etp)
+{
+	mblk_t *mp = etp->etp_mp;
 
 	mac_ether_offload_info_t meoi;
 	mac_ether_offload_info(mp, &meoi);
@@ -346,19 +403,29 @@ mac_sw_cksum_test(ktest_ctx_hdl_t *ctx)
 	    (meoi.meoi_l3proto != ETHERTYPE_IP &&
 	    meoi.meoi_l3proto != ETHERTYPE_IPV6)) {
 		KT_SKIP(ctx, "l3 protocol not recognized/supported");
-		goto cleanup;
+		return;
 	}
 
 	mac_emul_t emul_flags = 0;
 	uint_t hck_flags = 0, hck_start = 0, hck_stuff = 0, hck_end = 0;
-	if (meoi.meoi_l3proto == ETHERTYPE_IP && ctp.ctp_do_ipv4) {
+
+	if (etp->etp_do_lso) {
+		emul_flags |= MAC_LSO_EMUL;
+		hck_flags |= HW_LSO;
+		if (etp->etp_mss == 0) {
+			KT_ERROR(ctx, "invalid MSS for LSO");
+			return;
+		}
+	}
+
+	if (meoi.meoi_l3proto == ETHERTYPE_IP && etp->etp_do_ipv4) {
 		mblk_write16(mp,
 		    meoi.meoi_l2hlen + offsetof(ipha_t, ipha_hdr_checksum), 0);
 		emul_flags |= MAC_IPCKSUM_EMUL;
 		hck_flags |= HCK_IPV4_HDRCKSUM;
 	}
 
-	const boolean_t do_l4 = ctp.ctp_do_partial || ctp.ctp_do_full;
+	const boolean_t do_l4 = etp->etp_do_partial || etp->etp_do_full;
 	if ((meoi.meoi_flags & MEOI_L4INFO_SET) != 0 && do_l4) {
 		boolean_t skip_pseudo = B_FALSE;
 		hck_start = meoi.meoi_l2hlen + meoi.meoi_l3hlen;
@@ -391,27 +458,28 @@ mac_sw_cksum_test(ktest_ctx_hdl_t *ctx)
 			 * account for its increased width.
 			 */
 			hck_stuff += SCTP_CHECKSUM_OFFSET;
-			if (ctp.ctp_do_full) {
+			if (etp->etp_do_full) {
 				mblk_write16(mp, hck_stuff, 0);
 				mblk_write16(mp, hck_stuff + 2, 0);
 			} else {
 				KT_SKIP(ctx,
 				    "Partial L4 cksum not supported for SCTP");
+				return;
 			}
 			break;
 		default:
 			KT_SKIP(ctx,
 			    "Partial L4 cksum not supported for proto");
-			goto cleanup;
+			return;
 		}
 
 		emul_flags |= MAC_HWCKSUM_EMUL;
-		if (ctp.ctp_do_partial) {
+		if (etp->etp_do_partial) {
 			hck_flags |= HCK_PARTIALCKSUM;
 			if (!skip_pseudo) {
 				/* Populate L4 pseudo-header cksum */
-				const uint16_t pcksum =
-				    cksum_calc_pseudo(ctx, ctp.ctp_raw, &meoi);
+				const uint16_t pcksum = cksum_calc_pseudo(ctx,
+				    etp->etp_raw, &meoi, etp->etp_do_lso);
 				mblk_write16(mp, hck_stuff, pcksum);
 			} else {
 				mblk_write16(mp, hck_stuff, 0);
@@ -424,7 +492,7 @@ mac_sw_cksum_test(ktest_ctx_hdl_t *ctx)
 	}
 	if (do_l4 && (hck_flags & (HCK_FULLCKSUM|HCK_PARTIALCKSUM)) == 0) {
 		KT_SKIP(ctx, "L4 checksum not supported for packet");
-		goto cleanup;
+		return;
 	}
 
 	if (emul_flags != 0) {
@@ -443,31 +511,80 @@ mac_sw_cksum_test(ktest_ctx_hdl_t *ctx)
 		/* Set hcksum information on all mblks in chain */
 		for (mblk_t *cmp = mp; cmp != NULL; cmp = cmp->b_cont) {
 			mac_hcksum_set(cmp, hck_start, hck_stuff, hck_end, 0,
-			    hck_flags);
+			    hck_flags & HCK_FLAGS);
+			lso_info_set(cmp, etp->etp_mss,
+			    hck_flags & HW_LSO_FLAGS);
 		}
-		ctp.ctp_mp = mp = mac_sw_cksum(mp, emul_flags);
 
-		KT_ASSERT3UG(mp, !=, NULL, ctx, cleanup);
-		if (!cksum_result_compare(ctx, &ctp, mp)) {
-			goto cleanup;
+		mac_hw_emul(&mp, NULL, NULL, emul_flags);
+		KT_ASSERT3P(mp, !=, NULL, ctx);
+		etp->etp_mp = mp;
+
+		boolean_t success = (etp->etp_outputs == NULL) ?
+		    pkt_compare(ctx, etp->etp_raw, etp->etp_raw_sz, mp) :
+		    pkt_result_compare_chain(ctx, etp, mp);
+		if (!success) {
+			return;
 		}
 	} else {
-		KT_SKIP(ctx, "no checksums supported for packet");
-		goto cleanup;
+		KT_SKIP(ctx, "offloads unsupported for packet");
+		return;
 	}
 
 	KT_PASS(ctx);
+}
+
+/*
+ * Verify checksum emulation against an arbitrary chain of packets.  If the
+ * packet is of a supported protocol, any L3 and L4 checksums are cleared, and
+ * then mac_hw_emul() is called to perform the offload emulation.  Afterwards,
+ * the packet is compared to see if it equals the input, which is assumed to
+ * have correct checksums.
+ */
+static void
+mac_sw_cksum_test(ktest_ctx_hdl_t *ctx)
+{
+	emul_test_params_t etp;
+	if (!emul_test_parse_input(ctx, &etp)) {
+		goto cleanup;
+	}
+
+	mac_hw_emul_test(ctx, &etp);
 
 cleanup:
-	if (hdl != NULL) {
-		ktest_release_mod(hdl);
+	etp_free(&etp);
+}
+
+/*
+ * Verify mac_sw_lso() (and checksum) emulation against an arbitrary input
+ * packet.  This test functions like mac_sw_cksum_test insofar as checksums can
+ * be customised, but also sets HW_LSO on any input packet, and compares the
+ * outputs against a mandatory chain of packets provided by the caller.
+ */
+static void
+mac_sw_lso_test(ktest_ctx_hdl_t *ctx)
+{
+	emul_test_params_t etp;
+	if (!emul_test_parse_input(ctx, &etp)) {
+		goto cleanup;
 	}
-	if (ctp.ctp_mp != NULL) {
-		freemsg(ctp.ctp_mp);
+
+	if (etp.etp_mss == 0) {
+		KT_ERROR(ctx, "invalid MSS for LSO");
+		goto cleanup;
 	}
-	if (ctp.ctp_raw != NULL) {
-		kmem_free(ctp.ctp_raw, ctp.ctp_raw_sz);
+
+	if (etp.etp_outputs == NULL) {
+		KT_ERROR(ctx, "LSO tests require explicit packet list");
+		goto cleanup;
 	}
+
+	etp.etp_do_lso = B_TRUE;
+
+	mac_hw_emul_test(ctx, &etp);
+
+cleanup:
+	etp_free(&etp);
 }
 
 typedef struct meoi_test_params {
@@ -790,6 +907,11 @@ _init()
 	VERIFY0(ktest_add_test(ks, "mac_sw_cksum_test",
 	    mac_sw_cksum_test, KTEST_FLAG_INPUT));
 
+	ks = NULL;
+	VERIFY0(ktest_add_suite(km, "lso", &ks));
+	VERIFY0(ktest_add_test(ks, "mac_sw_lso_test",
+	    mac_sw_lso_test, KTEST_FLAG_INPUT));
+
 	ks = NULL;
 	VERIFY0(ktest_add_suite(km, "parsing", &ks));
 	VERIFY0(ktest_add_test(ks, "mac_ether_offload_info_test",
diff --git a/usr/src/uts/common/io/mac/mac_sched.c b/usr/src/uts/common/io/mac/mac_sched.c
index ea7d6dddb0..a64f56d62d 100644
--- a/usr/src/uts/common/io/mac/mac_sched.c
+++ b/usr/src/uts/common/io/mac/mac_sched.c
@@ -23,7 +23,7 @@
  * Use is subject to license terms.
  * Copyright 2018 Joyent, Inc.
  * Copyright 2013 Nexenta Systems, Inc. All rights reserved.
- * Copyright 2024 Oxide Computer Company
+ * Copyright 2025 Oxide Computer Company
  */
 
 /*
@@ -300,16 +300,16 @@
  * the device has frames delivered directly to the MAC client.
  *
  * Otherwise, all fanout is performed by software. MAC divides incoming frames
- * into one of three buckets -- IPv4 TCP traffic, IPv4 UDP traffic, and
- * everything else. Regardless of the type of fanout, these three categories
- * or buckets are always used.
+ * into one of five buckets -- IPv4 TCP traffic, IPv4 UDP traffic, IPv6 TCP
+ * traffic, IPv6 UDP traffic, and everything else. Regardless of the type of
+ * fanout, these five categories of buckets are always used.
  *
  * The difference between protocol level fanout and full software ring protocol
  * fanout is the number of software rings that end up getting created. The
  * system always uses the same number of software rings per protocol bucket. So
  * in the first case when we're just doing protocol level fanout, we just create
- * one software ring each for IPv4 TCP traffic, IPv4 UDP traffic, and everything
- * else.
+ * one software ring each for IPv4 TCP traffic, IPv4 UDP traffic, IPv6 TCP
+ * traffic, IPv6 UDP traffic, and everything else.
  *
  * In the case where we do full software ring protocol fanout, we generally use
  * mac_compute_soft_ring_count() to determine the number of rings. There are
@@ -336,7 +336,7 @@
  * used, which are useful for higher performance and allow it to use direct
  * function calls to DLS instead of using STREAMS.
  *
- * When we have IPv4 TCP or UDP software rings, then traffic on those rings is
+ * When we have TCP or UDP software rings, then traffic on those rings is
  * eligible for what we call the dls bypass. In those cases, rather than going
  * out mac_rx_deliver() to DLS, DLS instead registers them to go directly via
  * the direct callback registered with DLS, generally ip_input().
@@ -1434,6 +1434,17 @@ mac_srs_fire(void *arg)
 	(ntohl((src) + (dst)) ^ ((ports) >> 24) ^ ((ports) >> 16) ^	\
 	((ports) >> 8) ^ (ports))
 
+/*
+ * Uniform distribution hash for IPv6 4-tuple.
+ */
+#define	HASH_ADDR6(src, dst, ports)					\
+	((src.s6_addr32[0] ^ src.s6_addr32[1] ^                         \
+	src.s6_addr32[2] ^ src.s6_addr32[3]) ^				\
+	(dst.s6_addr32[0] ^ dst.s6_addr32[1] ^                          \
+	dst.s6_addr32[2] ^ dst.s6_addr32[3]) ^				\
+	((ports) >> 24) ^ ((ports) >> 16) ^	                        \
+	((ports) >> 8) ^ (ports))
+
 #define	COMPUTE_INDEX(key, sz)	(key % sz)
 
 #define	FANOUT_ENQUEUE_MP(head, tail, cnt, bw_ctl, sz, sz0, mp) {	\
@@ -1454,14 +1465,16 @@ mac_srs_fire(void *arg)
 #define	MAC_FANOUT_RND_ROBIN	1
 int mac_fanout_type = MAC_FANOUT_DEFAULT;
 
-#define	MAX_SR_TYPES	3
+#define	MAX_SR_TYPES	5
 /* fanout types for port based hashing */
-enum pkt_type {
+typedef enum pkt_type {
 	V4_TCP = 0,
 	V4_UDP,
+	V6_TCP,
+	V6_UDP,
 	OTH,
 	UNDEF
-};
+} pkt_type_t;
 
 /*
  * Pair of local and remote ports in the transport header
@@ -1482,28 +1495,15 @@ enum pkt_type {
 static void
 mac_rx_srs_proto_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 {
-	struct ether_header		*ehp;
-	struct ether_vlan_header	*evhp;
-	uint32_t			sap;
-	ipha_t				*ipha;
-	uint8_t				*dstaddr;
-	size_t				hdrsize;
-	mblk_t				*mp;
-	mblk_t				*headmp[MAX_SR_TYPES];
-	mblk_t				*tailmp[MAX_SR_TYPES];
-	int				cnt[MAX_SR_TYPES];
-	size_t				sz[MAX_SR_TYPES];
-	size_t				sz1;
-	boolean_t			bw_ctl;
-	boolean_t			hw_classified;
-	boolean_t			dls_bypass;
-	boolean_t			is_ether;
-	boolean_t			is_unicast;
-	enum pkt_type			type;
-	mac_client_impl_t		*mcip = mac_srs->srs_mcip;
+	mblk_t			*headmp[MAX_SR_TYPES] = { 0 };
+	mblk_t			*tailmp[MAX_SR_TYPES] = { 0 };
+	int			cnt[MAX_SR_TYPES] = { 0 };
+	size_t			sz[MAX_SR_TYPES] = { 0 };
+	mac_client_impl_t	*mcip = mac_srs->srs_mcip;
 
-	is_ether = (mcip->mci_mip->mi_info.mi_nativemedia == DL_ETHER);
-	bw_ctl = ((mac_srs->srs_type & SRST_BW_CONTROL) != 0);
+	const boolean_t is_ether =
+	    (mcip->mci_mip->mi_info.mi_nativemedia == DL_ETHER);
+	const boolean_t bw_ctl = ((mac_srs->srs_type & SRST_BW_CONTROL) != 0);
 
 	/*
 	 * If we don't have a Rx ring, S/W classification would have done
@@ -1512,7 +1512,7 @@ mac_rx_srs_proto_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 	 * then we need to make sure that the mac address really belongs
 	 * to us.
 	 */
-	hw_classified = mac_srs->srs_ring != NULL &&
+	const boolean_t hw_classified = mac_srs->srs_ring != NULL &&
 	    mac_srs->srs_ring->mr_classify_type == MAC_HW_CLASSIFIER;
 
 	/*
@@ -1521,14 +1521,10 @@ mac_rx_srs_proto_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 	 * DLS bypass may also be disabled via the
 	 * MCIS_RX_BYPASS_DISABLE flag.
 	 */
-	dls_bypass = ((mac_srs->srs_type & SRST_DLS_BYPASS) != 0) &&
+	const boolean_t dls_bypass =
+	    ((mac_srs->srs_type & SRST_DLS_BYPASS) != 0) &&
 	    ((mcip->mci_state_flags & MCIS_RX_BYPASS_DISABLE) == 0);
 
-	bzero(headmp, MAX_SR_TYPES * sizeof (mblk_t *));
-	bzero(tailmp, MAX_SR_TYPES * sizeof (mblk_t *));
-	bzero(cnt, MAX_SR_TYPES * sizeof (int));
-	bzero(sz, MAX_SR_TYPES * sizeof (size_t));
-
 	/*
 	 * We have a chain from SRS that we need to split across the
 	 * soft rings. The squeues for the TCP and IPv4 SAPs use their
@@ -1537,72 +1533,79 @@ mac_rx_srs_proto_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 	 * be polled.
 	 */
 	while (head != NULL) {
-		mp = head;
+		mac_ether_offload_info_t meoi = { 0 };
+		uint8_t ether_addr[ETHERADDRL];
+		const uint8_t *dstaddr = ether_addr;
+		mac_header_info_t non_ether_mhi;
+		boolean_t is_unicast = B_FALSE;
+
+		mblk_t *mp = head;
 		head = head->b_next;
 		mp->b_next = NULL;
-
-		type = OTH;
-		sz1 = (mp->b_cont == NULL) ? MBLKL(mp) : msgdsize(mp);
+		const size_t sz1 =
+		    (mp->b_cont == NULL) ? MBLKL(mp) : msgdsize(mp);
 
 		if (is_ether) {
-			/*
-			 * At this point we can be sure the packet at least
-			 * has an ether header.
-			 */
-			if (sz1 < sizeof (struct ether_header)) {
+			uint32_t vlan_tci;
+
+			mac_ether_offload_info(mp, &meoi);
+			if ((meoi.meoi_flags & MEOI_L2INFO_SET) == 0 ||
+			    !mac_ether_l2_info(mp, ether_addr, &vlan_tci)) {
 				mac_rx_drop_pkt(mac_srs, mp);
 				continue;
 			}
-			ehp = (struct ether_header *)mp->b_rptr;
 
 			/*
-			 * Determine if this is a VLAN or non-VLAN packet.
+			 * Check if the VID of the packet, if any, belongs to
+			 * this client.  Technically, if this packet came up via
+			 * a HW classified ring then we don't need to perform
+			 * this check.  Perhaps a future optimization.
 			 */
-			if ((sap = ntohs(ehp->ether_type)) == VLAN_TPID) {
-				evhp = (struct ether_vlan_header *)mp->b_rptr;
-				sap = ntohs(evhp->ether_type);
-				hdrsize = sizeof (struct ether_vlan_header);
+			if ((meoi.meoi_flags & MEOI_VLAN_TAGGED) != 0) {
+				ASSERT3U(meoi.meoi_l2hlen, ==,
+				    sizeof (struct ether_vlan_header));
+				ASSERT3U(vlan_tci, <=, UINT16_MAX);
 
-				/*
-				 * Check if the VID of the packet, if
-				 * any, belongs to this client.
-				 * Technically, if this packet came up
-				 * via a HW classified ring then we
-				 * don't need to perform this check.
-				 * Perhaps a future optimization.
-				 */
 				if (!mac_client_check_flow_vid(mcip,
-				    VLAN_ID(ntohs(evhp->ether_tci)))) {
+				    VLAN_ID(vlan_tci))) {
 					mac_rx_drop_pkt(mac_srs, mp);
 					continue;
 				}
-			} else {
-				hdrsize = sizeof (struct ether_header);
 			}
-			is_unicast =
-			    ((((uint8_t *)&ehp->ether_dhost)[0] & 0x01) == 0);
-			dstaddr = (uint8_t *)&ehp->ether_dhost;
-		} else {
-			mac_header_info_t		mhi;
 
+			is_unicast = ((ether_addr[0] & 0x01) == 0);
+		} else {
 			if (mac_header_info((mac_handle_t)mcip->mci_mip,
-			    mp, &mhi) != 0) {
+			    mp, &non_ether_mhi) != 0) {
 				mac_rx_drop_pkt(mac_srs, mp);
 				continue;
 			}
-			hdrsize = mhi.mhi_hdrsize;
-			sap = mhi.mhi_bindsap;
-			is_unicast = (mhi.mhi_dsttype == MAC_ADDRTYPE_UNICAST);
-			dstaddr = (uint8_t *)mhi.mhi_daddr;
+
+			meoi.meoi_l2hlen = non_ether_mhi.mhi_hdrsize;
+			meoi.meoi_l3proto = non_ether_mhi.mhi_bindsap;
+			meoi.meoi_flags = MEOI_L2INFO_SET;
+			(void) mac_partial_offload_info(mp, 0, &meoi);
+
+			is_unicast =
+			    (non_ether_mhi.mhi_dsttype == MAC_ADDRTYPE_UNICAST);
+			dstaddr = non_ether_mhi.mhi_daddr;
 		}
 
 		if (!dls_bypass) {
-			FANOUT_ENQUEUE_MP(headmp[type], tailmp[type],
-			    cnt[type], bw_ctl, sz[type], sz1, mp);
+			DTRACE_PROBE4(rx__fanout, mblk_t *, mp,
+			    mac_ether_offload_info_t *, &meoi,
+			    mac_soft_ring_set_t *, mac_srs, pkt_type_t, OTH);
+			FANOUT_ENQUEUE_MP(headmp[OTH], tailmp[OTH],
+			    cnt[OTH], bw_ctl, sz[OTH], sz1, mp);
 			continue;
 		}
 
-		if (sap == ETHERTYPE_IP) {
+		ASSERT((meoi.meoi_flags & MEOI_L2INFO_SET) != 0);
+
+		boolean_t is_fastpath = B_FALSE;
+
+		if (meoi.meoi_l3proto == ETHERTYPE_IP ||
+		    meoi.meoi_l3proto == ETHERTYPE_IPV6) {
 			/*
 			 * If we are H/W classified, but we have promisc
 			 * on, then we need to check for the unicast address.
@@ -1614,10 +1617,10 @@ mac_rx_srs_proto_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 				map = mcip->mci_unicast;
 				if (bcmp(dstaddr, map->ma_addr,
 				    map->ma_len) == 0)
-					type = UNDEF;
+					is_fastpath = B_TRUE;
 				rw_exit(&mcip->mci_rw_lock);
 			} else if (is_unicast) {
-				type = UNDEF;
+				is_fastpath = B_TRUE;
 			}
 		}
 
@@ -1634,44 +1637,63 @@ mac_rx_srs_proto_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 		 * performance and may bypass DLS. All other cases go through
 		 * the 'OTH' type path without DLS bypass.
 		 */
-		ipha = (ipha_t *)(mp->b_rptr + hdrsize);
-		if ((type != OTH) && MBLK_RX_FANOUT_SLOWPATH(mp, ipha))
-			type = OTH;
+		if (is_fastpath) {
+			if ((meoi.meoi_flags & MEOI_L3INFO_SET) == 0 ||
+			    (meoi.meoi_flags & MEOI_L4INFO_SET) == 0) {
+				is_fastpath = B_FALSE;
+			}
+			if (DB_TYPE(mp) != M_DATA || DB_REF(mp) != 1) {
+				is_fastpath = B_FALSE;
+			}
 
-		if (type == OTH) {
-			FANOUT_ENQUEUE_MP(headmp[type], tailmp[type],
-			    cnt[type], bw_ctl, sz[type], sz1, mp);
-			continue;
+			const size_t total_hdr_len = meoi.meoi_l2hlen
+			    + meoi.meoi_l3hlen + meoi.meoi_l4hlen;
+
+			if (!OK_32PTR(mp->b_rptr + meoi.meoi_l2hlen) ||
+			    total_hdr_len > MBLKL(mp)) {
+				is_fastpath = B_FALSE;
+			}
 		}
 
-		ASSERT(type == UNDEF);
+		if (!is_fastpath) {
+			DTRACE_PROBE4(rx__fanout, mblk_t *, mp,
+			    mac_ether_offload_info_t *, &meoi,
+			    mac_soft_ring_set_t *, mac_srs, pkt_type_t, OTH);
+			FANOUT_ENQUEUE_MP(headmp[OTH], tailmp[OTH],
+			    cnt[OTH], bw_ctl, sz[OTH], sz1, mp);
+			continue;
+		}
 
 		/*
-		 * Determine the type from the IP protocol value. If
-		 * classified as TCP or UDP, then update the read
-		 * pointer to the beginning of the IP header.
-		 * Otherwise leave the message as is for further
-		 * processing by DLS.
+		 * Determine the type from the IP protocol value. If classified
+		 * as TCP or UDP, then update the read pointer to the beginning
+		 * of the IP header.  Otherwise leave the message as is for
+		 * further processing by DLS.
 		 */
-		switch (ipha->ipha_protocol) {
+		pkt_type_t type = OTH;
+		switch (meoi.meoi_l4proto) {
 		case IPPROTO_TCP:
-			type = V4_TCP;
-			mp->b_rptr += hdrsize;
+			type = (meoi.meoi_l3proto == ETHERTYPE_IPV6) ?
+			    V6_TCP : V4_TCP;
+			mp->b_rptr += meoi.meoi_l2hlen;
 			break;
 		case IPPROTO_UDP:
-			type = V4_UDP;
-			mp->b_rptr += hdrsize;
+			type = (meoi.meoi_l3proto == ETHERTYPE_IPV6) ?
+			    V6_UDP : V4_UDP;
+			mp->b_rptr += meoi.meoi_l2hlen;
 			break;
 		default:
-			type = OTH;
 			break;
 		}
 
+		DTRACE_PROBE4(rx__fanout, mblk_t *, mp,
+		    mac_ether_offload_info_t *, &meoi, mac_soft_ring_set_t *,
+		    mac_srs, pkt_type_t, type);
 		FANOUT_ENQUEUE_MP(headmp[type], tailmp[type], cnt[type],
 		    bw_ctl, sz[type], sz1, mp);
 	}
 
-	for (type = V4_TCP; type < UNDEF; type++) {
+	for (pkt_type_t type = V4_TCP; type < UNDEF; type++) {
 		if (headmp[type] != NULL) {
 			mac_soft_ring_t			*softring;
 
@@ -1680,9 +1702,15 @@ mac_rx_srs_proto_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 			case V4_TCP:
 				softring = mac_srs->srs_tcp_soft_rings[0];
 				break;
+			case V6_TCP:
+				softring = mac_srs->srs_tcp6_soft_rings[0];
+				break;
 			case V4_UDP:
 				softring = mac_srs->srs_udp_soft_rings[0];
 				break;
+			case V6_UDP:
+				softring = mac_srs->srs_udp6_soft_rings[0];
+				break;
 			case OTH:
 				softring = mac_srs->srs_oth_soft_rings[0];
 			}
@@ -1702,7 +1730,7 @@ int	fanout_unaligned = 0;
  */
 static int
 mac_rx_srs_long_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *mp,
-    uint32_t sap, size_t hdrsize, enum pkt_type *type, uint_t *indx)
+    uint32_t sap, size_t hdrsize, pkt_type_t *type, uint_t *indx)
 {
 	ip6_t		*ip6h;
 	ipha_t		*ipha;
@@ -1885,42 +1913,19 @@ src_dst_based_fanout:
  * along with these members) and create an array of this uber struct so we
  * don't have to do kmem_alloc.
  */
-int	fanout_oth1 = 0;
-int	fanout_oth2 = 0;
-int	fanout_oth3 = 0;
-int	fanout_oth4 = 0;
-int	fanout_oth5 = 0;
 
 static void
 mac_rx_srs_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 {
-	struct ether_header		*ehp;
-	struct ether_vlan_header	*evhp;
-	uint32_t			sap;
-	ipha_t				*ipha;
-	uint8_t				*dstaddr;
-	uint_t				indx;
-	size_t				ports_offset;
-	size_t				ipha_len;
-	size_t				hdrsize;
-	uint_t				hash;
-	mblk_t				*mp;
 	mblk_t				*headmp[MAX_SR_TYPES][MAX_SR_FANOUT];
 	mblk_t				*tailmp[MAX_SR_TYPES][MAX_SR_FANOUT];
 	int				cnt[MAX_SR_TYPES][MAX_SR_FANOUT];
 	size_t				sz[MAX_SR_TYPES][MAX_SR_FANOUT];
-	size_t				sz1;
-	boolean_t			bw_ctl;
-	boolean_t			hw_classified;
-	boolean_t			dls_bypass;
-	boolean_t			is_ether;
-	boolean_t			is_unicast;
-	int				fanout_cnt;
-	enum pkt_type			type;
 	mac_client_impl_t		*mcip = mac_srs->srs_mcip;
 
-	is_ether = (mcip->mci_mip->mi_info.mi_nativemedia == DL_ETHER);
-	bw_ctl = ((mac_srs->srs_type & SRST_BW_CONTROL) != 0);
+	const boolean_t is_ether =
+	    (mcip->mci_mip->mi_info.mi_nativemedia == DL_ETHER);
+	const boolean_t bw_ctl = ((mac_srs->srs_type & SRST_BW_CONTROL) != 0);
 
 	/*
 	 * If we don't have a Rx ring, S/W classification would have done
@@ -1929,7 +1934,7 @@ mac_rx_srs_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 	 * then we need to make sure that the mac address really belongs
 	 * to us.
 	 */
-	hw_classified = mac_srs->srs_ring != NULL &&
+	const boolean_t hw_classified = mac_srs->srs_ring != NULL &&
 	    mac_srs->srs_ring->mr_classify_type == MAC_HW_CLASSIFIER;
 
 	/*
@@ -1939,7 +1944,8 @@ mac_rx_srs_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 	 * MCIS_RX_BYPASS_DISABLE flag, but this is only consumed by
 	 * sun4v vsw currently.
 	 */
-	dls_bypass = ((mac_srs->srs_type & SRST_DLS_BYPASS) != 0) &&
+	const boolean_t dls_bypass =
+	    ((mac_srs->srs_type & SRST_DLS_BYPASS) != 0) &&
 	    ((mcip->mci_state_flags & MCIS_RX_BYPASS_DISABLE) == 0);
 
 	/*
@@ -1950,7 +1956,7 @@ mac_rx_srs_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 	 * of reduction in fanout, we will need to ensure that happens
 	 * behind the SRS_PROC.
 	 */
-	fanout_cnt = mac_srs->srs_tcp_ring_count;
+	const int fanout_cnt = mac_srs->srs_tcp_ring_count;
 
 	bzero(headmp, MAX_SR_TYPES * MAX_SR_FANOUT * sizeof (mblk_t *));
 	bzero(tailmp, MAX_SR_TYPES * MAX_SR_FANOUT * sizeof (mblk_t *));
@@ -1964,84 +1970,110 @@ mac_rx_srs_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 	 * and the rest goes in other.
 	 */
 	while (head != NULL) {
-		mp = head;
+		mac_ether_offload_info_t meoi = { 0 };
+		uint8_t ether_addr[ETHERADDRL];
+		const uint8_t *dstaddr = ether_addr;
+		mac_header_info_t non_ether_mhi;
+		pkt_type_t type;
+		uint_t indx;
+		boolean_t is_unicast = B_FALSE;
+
+		mblk_t *mp = head;
 		head = head->b_next;
 		mp->b_next = NULL;
-
-		type = OTH;
-		sz1 = (mp->b_cont == NULL) ? MBLKL(mp) : msgdsize(mp);
+		const size_t sz1 =
+		    (mp->b_cont == NULL) ? MBLKL(mp) : msgdsize(mp);
 
 		if (is_ether) {
+			uint32_t vlan_tci;
+
 			/*
 			 * At this point we can be sure the packet at least
 			 * has an ether header.
 			 */
-			if (sz1 < sizeof (struct ether_header)) {
+			mac_ether_offload_info(mp, &meoi);
+			if ((meoi.meoi_flags & MEOI_L2INFO_SET) == 0 ||
+			    !mac_ether_l2_info(mp, ether_addr, &vlan_tci)) {
 				mac_rx_drop_pkt(mac_srs, mp);
 				continue;
 			}
-			ehp = (struct ether_header *)mp->b_rptr;
 
 			/*
-			 * Determine if this is a VLAN or non-VLAN packet.
+			 * Check if the VID of the packet, if any, belongs to
+			 * this client.  Technically, if this packet came up via
+			 * a HW classified ring then we don't need to perform
+			 * this check.  Perhaps a future optimization.
 			 */
-			if ((sap = ntohs(ehp->ether_type)) == VLAN_TPID) {
-				evhp = (struct ether_vlan_header *)mp->b_rptr;
-				sap = ntohs(evhp->ether_type);
-				hdrsize = sizeof (struct ether_vlan_header);
+			if ((meoi.meoi_flags & MEOI_VLAN_TAGGED) != 0) {
+				ASSERT3U(meoi.meoi_l2hlen, ==,
+				    sizeof (struct ether_vlan_header));
+				ASSERT3U(vlan_tci, <=, UINT16_MAX);
 
-				/*
-				 * Check if the VID of the packet, if
-				 * any, belongs to this client.
-				 * Technically, if this packet came up
-				 * via a HW classified ring then we
-				 * don't need to perform this check.
-				 * Perhaps a future optimization.
-				 */
 				if (!mac_client_check_flow_vid(mcip,
-				    VLAN_ID(ntohs(evhp->ether_tci)))) {
+				    VLAN_ID(vlan_tci))) {
 					mac_rx_drop_pkt(mac_srs, mp);
 					continue;
 				}
-			} else {
-				hdrsize = sizeof (struct ether_header);
 			}
-			is_unicast =
-			    ((((uint8_t *)&ehp->ether_dhost)[0] & 0x01) == 0);
-			dstaddr = (uint8_t *)&ehp->ether_dhost;
-		} else {
-			mac_header_info_t		mhi;
 
+			is_unicast = (ether_addr[0] & 0x01) == 0;
+		} else {
 			if (mac_header_info((mac_handle_t)mcip->mci_mip,
-			    mp, &mhi) != 0) {
+			    mp, &non_ether_mhi) != 0) {
 				mac_rx_drop_pkt(mac_srs, mp);
 				continue;
 			}
-			hdrsize = mhi.mhi_hdrsize;
-			sap = mhi.mhi_bindsap;
-			is_unicast = (mhi.mhi_dsttype == MAC_ADDRTYPE_UNICAST);
-			dstaddr = (uint8_t *)mhi.mhi_daddr;
+
+			meoi.meoi_l2hlen = non_ether_mhi.mhi_hdrsize;
+			meoi.meoi_l3proto = non_ether_mhi.mhi_bindsap;
+			meoi.meoi_flags = MEOI_L2INFO_SET;
+			(void) mac_partial_offload_info(mp, 0, &meoi);
+
+			is_unicast =
+			    (non_ether_mhi.mhi_dsttype == MAC_ADDRTYPE_UNICAST);
+			dstaddr = non_ether_mhi.mhi_daddr;
 		}
 
 		if (!dls_bypass) {
-			if (mac_rx_srs_long_fanout(mac_srs, mp, sap,
-			    hdrsize, &type, &indx) == -1) {
+			if (mac_rx_srs_long_fanout(mac_srs, mp,
+			    meoi.meoi_l3proto, meoi.meoi_l2hlen,
+			    &type, &indx) == -1) {
 				mac_rx_drop_pkt(mac_srs, mp);
 				continue;
 			}
 
+			DTRACE_PROBE4(rx__fanout, mblk_t *, mp,
+			    mac_ether_offload_info_t *, &meoi,
+			    mac_soft_ring_set_t *, mac_srs, pkt_type_t, type);
 			FANOUT_ENQUEUE_MP(headmp[type][indx],
-			    tailmp[type][indx], cnt[type][indx], bw_ctl,
+			    tailmp[type][indx],
+			    cnt[type][indx], bw_ctl,
 			    sz[type][indx], sz1, mp);
 			continue;
 		}
 
+		/*
+		 * While MEOI is unable to parse ESP headers, for the purposes
+		 * of classification here, we treat such packets like UDP, so we
+		 * can grant it a reprieve here.  This is acceptable since we
+		 * will not go rooting around in the ESP headers.
+		 */
+		if ((meoi.meoi_flags & MEOI_L3INFO_SET) != 0 &&
+		    (meoi.meoi_flags & MEOI_L4INFO_SET) == 0 &&
+		    meoi.meoi_l4proto == IPPROTO_ESP) {
+			/* ESP header should consist of at least 8 octets */
+			meoi.meoi_l4hlen = 8;
+			meoi.meoi_flags |= MEOI_L4INFO_SET;
+		}
+
 		/*
 		 * If we are using the default Rx ring where H/W or S/W
 		 * classification has not happened, we need to verify if
 		 * this unicast packet really belongs to us.
 		 */
-		if (sap == ETHERTYPE_IP) {
+		boolean_t is_fastpath = B_FALSE;
+		if (meoi.meoi_l3proto == ETHERTYPE_IP ||
+		    meoi.meoi_l3proto == ETHERTYPE_IPV6) {
 			/*
 			 * If we are H/W classified, but we have promisc
 			 * on, then we need to check for the unicast address.
@@ -2053,69 +2085,83 @@ mac_rx_srs_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 				map = mcip->mci_unicast;
 				if (bcmp(dstaddr, map->ma_addr,
 				    map->ma_len) == 0)
-					type = UNDEF;
+					is_fastpath = B_TRUE;
 				rw_exit(&mcip->mci_rw_lock);
 			} else if (is_unicast) {
-				type = UNDEF;
+				is_fastpath = B_TRUE;
 			}
 		}
 
 		/*
-		 * This needs to become a contract with the driver for
-		 * the fast path.
+		 * Verify that the requirements for taking the fast path are all
+		 * still met.  This needs to become a contract with the driver.
 		 */
+		if (is_fastpath) {
+			if ((meoi.meoi_flags & MEOI_L3INFO_SET) == 0 ||
+			    (meoi.meoi_flags & MEOI_L4INFO_SET) == 0) {
+				is_fastpath = B_FALSE;
+			}
+			if (DB_TYPE(mp) != M_DATA || DB_REF(mp) != 1) {
+				is_fastpath = B_FALSE;
+			}
 
-		ipha = (ipha_t *)(mp->b_rptr + hdrsize);
-		if ((type != OTH) && MBLK_RX_FANOUT_SLOWPATH(mp, ipha)) {
-			type = OTH;
-			fanout_oth1++;
-		}
+			const size_t total_hdr_len = meoi.meoi_l2hlen
+			    + meoi.meoi_l3hlen + meoi.meoi_l4hlen;
 
-		if (type != OTH) {
-			uint16_t	frag_offset_flags;
-
-			switch (ipha->ipha_protocol) {
-			case IPPROTO_TCP:
-			case IPPROTO_UDP:
-			case IPPROTO_SCTP:
-			case IPPROTO_ESP:
-				ipha_len = IPH_HDR_LENGTH(ipha);
-				if ((uchar_t *)ipha + ipha_len + PORTS_SIZE >
-				    mp->b_wptr) {
-					type = OTH;
-					break;
-				}
-				frag_offset_flags =
-				    ntohs(ipha->ipha_fragment_offset_and_flags);
-				if ((frag_offset_flags &
-				    (IPH_MF | IPH_OFFSET)) != 0) {
-					type = OTH;
-					fanout_oth3++;
-					break;
-				}
-				ports_offset = hdrsize + ipha_len;
-				break;
-			default:
-				type = OTH;
-				fanout_oth4++;
-				break;
+			if (!OK_32PTR(mp->b_rptr + meoi.meoi_l2hlen) ||
+			    total_hdr_len > MBLKL(mp)) {
+				is_fastpath = B_FALSE;
+			}
+
+			if ((meoi.meoi_flags &
+			    (MEOI_L3_FRAG_MORE | MEOI_L3_FRAG_OFFSET)) != 0) {
+				is_fastpath = B_FALSE;
 			}
 		}
+		switch (meoi.meoi_l4proto) {
+		case IPPROTO_TCP:
+		case IPPROTO_UDP:
+		case IPPROTO_SCTP:
+		case IPPROTO_ESP:
+			if (is_fastpath) {
+				/*
+				 * Since the above checks ensure that the first
+				 * mblk covers the L2-L4 headers, we can be
+				 * confident that the "ports" portion of the
+				 * hashing payload is covered too.
+				 */
+				ASSERT3U(meoi.meoi_l4hlen, >=, PORTS_SIZE);
+			}
+			break;
+		default:
+			break;
+		}
 
-		if (type == OTH) {
-			if (mac_rx_srs_long_fanout(mac_srs, mp, sap,
-			    hdrsize, &type, &indx) == -1) {
+		if (!is_fastpath) {
+			if (mac_rx_srs_long_fanout(mac_srs, mp,
+			    meoi.meoi_l3proto, meoi.meoi_l2hlen,
+			    &type, &indx) == -1) {
 				mac_rx_drop_pkt(mac_srs, mp);
 				continue;
 			}
 
+			DTRACE_PROBE4(rx__fanout, mblk_t *, mp,
+			    mac_ether_offload_info_t *, &meoi,
+			    mac_soft_ring_set_t *, mac_srs, pkt_type_t, type);
 			FANOUT_ENQUEUE_MP(headmp[type][indx],
 			    tailmp[type][indx], cnt[type][indx], bw_ctl,
 			    sz[type][indx], sz1, mp);
 			continue;
 		}
 
-		ASSERT(type == UNDEF);
+		/*
+		 * By now, the fastpath requirements ensure that direct access
+		 * to the L3/L4 headers will fall safely within the mblk.
+		 */
+		const ipha_t *ipha = (ipha_t *)(mp->b_rptr + meoi.meoi_l2hlen);
+		const ip6_t *ip6 = (ip6_t *)(mp->b_rptr + meoi.meoi_l2hlen);
+		const uint32_t *ports = (uint32_t *)
+		    (mp->b_rptr + meoi.meoi_l2hlen + meoi.meoi_l3hlen);
 
 		/*
 		 * XXX-Sunay: We should hold srs_lock since ring_count
@@ -2125,25 +2171,39 @@ mac_rx_srs_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 		 * to change fanout type or ring count, the calling
 		 * thread needs to be behind SRS_PROC.
 		 */
-		switch (ipha->ipha_protocol) {
+		uint_t hash;
+		switch (meoi.meoi_l4proto) {
 		case IPPROTO_TCP:
 			/*
 			 * Note that for ESP, we fanout on SPI and it is at the
 			 * same offset as the 2x16-bit ports. So it is clumped
 			 * along with TCP, UDP and SCTP.
 			 */
-			hash = HASH_ADDR(ipha->ipha_src, ipha->ipha_dst,
-			    *(uint32_t *)(mp->b_rptr + ports_offset));
+			if (meoi.meoi_l3proto == ETHERTYPE_IP) {
+				hash = HASH_ADDR(ipha->ipha_src, ipha->ipha_dst,
+				    *ports);
+				type = V4_TCP;
+			}
+			if (meoi.meoi_l3proto == ETHERTYPE_IPV6) {
+				hash = HASH_ADDR6(ip6->ip6_src, ip6->ip6_dst,
+				    *ports);
+				type = V6_TCP;
+			}
 			indx = COMPUTE_INDEX(hash, mac_srs->srs_tcp_ring_count);
-			type = V4_TCP;
-			mp->b_rptr += hdrsize;
+			mp->b_rptr += meoi.meoi_l2hlen;
 			break;
 		case IPPROTO_UDP:
 		case IPPROTO_SCTP:
 		case IPPROTO_ESP:
 			if (mac_fanout_type == MAC_FANOUT_DEFAULT) {
-				hash = HASH_ADDR(ipha->ipha_src, ipha->ipha_dst,
-				    *(uint32_t *)(mp->b_rptr + ports_offset));
+				if (meoi.meoi_l3proto == ETHERTYPE_IP) {
+					hash = HASH_ADDR(ipha->ipha_src,
+					    ipha->ipha_dst, *ports);
+				}
+				if (meoi.meoi_l3proto == ETHERTYPE_IPV6) {
+					hash = HASH_ADDR6(ip6->ip6_src,
+					    ip6->ip6_dst, *ports);
+				}
 				indx = COMPUTE_INDEX(hash,
 				    mac_srs->srs_udp_ring_count);
 			} else {
@@ -2151,22 +2211,24 @@ mac_rx_srs_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 				    mac_srs->srs_udp_ring_count;
 				mac_srs->srs_ind++;
 			}
-			type = V4_UDP;
-			mp->b_rptr += hdrsize;
+			type = (meoi.meoi_l3proto == ETHERTYPE_IPV6) ?
+			    V6_UDP : V4_UDP;
+			mp->b_rptr += meoi.meoi_l2hlen;
 			break;
 		default:
 			indx = 0;
 			type = OTH;
 		}
 
+		DTRACE_PROBE4(rx__fanout, mblk_t *, mp,
+		    mac_ether_offload_info_t *, &meoi, mac_soft_ring_set_t *,
+		    mac_srs, pkt_type_t, type);
 		FANOUT_ENQUEUE_MP(headmp[type][indx], tailmp[type][indx],
 		    cnt[type][indx], bw_ctl, sz[type][indx], sz1, mp);
 	}
 
-	for (type = V4_TCP; type < UNDEF; type++) {
-		int	i;
-
-		for (i = 0; i < fanout_cnt; i++) {
+	for (pkt_type_t type = V4_TCP; type < UNDEF; type++) {
+		for (int i = 0; i < fanout_cnt; i++) {
 			if (headmp[type][i] != NULL) {
 				mac_soft_ring_t	*softring;
 
@@ -2176,10 +2238,18 @@ mac_rx_srs_fanout(mac_soft_ring_set_t *mac_srs, mblk_t *head)
 					softring =
 					    mac_srs->srs_tcp_soft_rings[i];
 					break;
+				case V6_TCP:
+					softring =
+					    mac_srs->srs_tcp6_soft_rings[i];
+					break;
 				case V4_UDP:
 					softring =
 					    mac_srs->srs_udp_soft_rings[i];
 					break;
+				case V6_UDP:
+					softring =
+					    mac_srs->srs_udp6_soft_rings[i];
+					break;
 				case OTH:
 					softring =
 					    mac_srs->srs_oth_soft_rings[i];
diff --git a/usr/src/uts/common/io/mac/mac_soft_ring.c b/usr/src/uts/common/io/mac/mac_soft_ring.c
index c8a16e6fd3..100b46d9aa 100644
--- a/usr/src/uts/common/io/mac/mac_soft_ring.c
+++ b/usr/src/uts/common/io/mac/mac_soft_ring.c
@@ -22,6 +22,7 @@
  * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
  * Use is subject to license terms.
  * Copyright 2018 Joyent, Inc.
+ * Copyright 2025 Oxide Computer Company
  */
 
 /*
@@ -161,9 +162,15 @@ mac_soft_ring_create(int id, clock_t wait, uint16_t type,
 	if (type & ST_RING_TCP) {
 		(void) snprintf(name, sizeof (name),
 		    "mac_tcp_soft_ring_%d_%p", id, (void *)mac_srs);
+	} else if (type & ST_RING_TCP6) {
+		(void) snprintf(name, sizeof (name),
+		    "mac_tcp6_soft_ring_%d_%p", id, (void *)mac_srs);
 	} else if (type & ST_RING_UDP) {
 		(void) snprintf(name, sizeof (name),
 		    "mac_udp_soft_ring_%d_%p", id, (void *)mac_srs);
+	} else if (type & ST_RING_UDP6) {
+		(void) snprintf(name, sizeof (name),
+		    "mac_udp6_soft_ring_%d_%p", id, (void *)mac_srs);
 	} else if (type & ST_RING_OTH) {
 		(void) snprintf(name, sizeof (name),
 		    "mac_oth_soft_ring_%d_%p", id, (void *)mac_srs);
diff --git a/usr/src/uts/common/io/mac/mac_stat.c b/usr/src/uts/common/io/mac/mac_stat.c
index dbb5c0a914..7fc73b8abf 100644
--- a/usr/src/uts/common/io/mac/mac_stat.c
+++ b/usr/src/uts/common/io/mac/mac_stat.c
@@ -22,6 +22,7 @@
  * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
  * Use is subject to license terms.
  * Copyright 2018 Joyent, Inc.
+ * Copyright 2025 Oxide Computer Company
  */
 
 /*
@@ -872,7 +873,8 @@ static uint64_t
 i_mac_rx_fanout_stat_get(void *handle, uint_t stat)
 {
 	mac_soft_ring_t		*tcp_ringp = (mac_soft_ring_t *)handle;
-	mac_soft_ring_t		*udp_ringp = NULL, *oth_ringp = NULL;
+	mac_soft_ring_t		*tcp6_ringp = NULL, *udp_ringp = NULL;
+	mac_soft_ring_t		*udp6_ringp = NULL, *oth_ringp = NULL;
 	mac_soft_ring_set_t	*mac_srs = tcp_ringp->s_ring_set;
 	int			index;
 	uint64_t		val;
@@ -881,24 +883,31 @@ i_mac_rx_fanout_stat_get(void *handle, uint_t stat)
 	/* Extract corresponding udp and oth ring pointers */
 	for (index = 0; mac_srs->srs_tcp_soft_rings[index] != NULL; index++) {
 		if (mac_srs->srs_tcp_soft_rings[index] == tcp_ringp) {
+			tcp6_ringp = mac_srs->srs_tcp6_soft_rings[index];
 			udp_ringp = mac_srs->srs_udp_soft_rings[index];
+			udp6_ringp = mac_srs->srs_udp6_soft_rings[index];
 			oth_ringp = mac_srs->srs_oth_soft_rings[index];
 			break;
 		}
 	}
 
-	ASSERT((udp_ringp != NULL) && (oth_ringp != NULL));
+	ASSERT((tcp6_ringp != NULL) && (udp_ringp != NULL) &&
+	    (udp6_ringp != NULL) && (oth_ringp != NULL));
 
 	switch (stat) {
 	case MAC_STAT_RBYTES:
 		val = (tcp_ringp->s_ring_total_rbytes) +
+		    (tcp6_ringp->s_ring_total_rbytes) +
 		    (udp_ringp->s_ring_total_rbytes) +
+		    (udp6_ringp->s_ring_total_rbytes) +
 		    (oth_ringp->s_ring_total_rbytes);
 		break;
 
 	case MAC_STAT_IPACKETS:
 		val = (tcp_ringp->s_ring_total_inpkt) +
+		    (tcp6_ringp->s_ring_total_inpkt) +
 		    (udp_ringp->s_ring_total_inpkt) +
+		    (udp6_ringp->s_ring_total_inpkt) +
 		    (oth_ringp->s_ring_total_inpkt);
 		break;
 
diff --git a/usr/src/uts/common/io/mac/mac_util.c b/usr/src/uts/common/io/mac/mac_util.c
index b8125655b1..80000550f9 100644
--- a/usr/src/uts/common/io/mac/mac_util.c
+++ b/usr/src/uts/common/io/mac/mac_util.c
@@ -700,24 +700,28 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 {
 	uint32_t ocsum_flags, ocsum_start, ocsum_stuff;
 	uint32_t mss;
-	uint32_t oehlen, oiphlen, otcphlen, ohdrslen, opktlen, odatalen;
-	uint32_t oleft;
+	uint32_t oehlen, oiphlen, otcphlen, ohdrslen, opktlen;
+	uint32_t odatalen, oleft;
 	uint_t nsegs, seg;
 	int len;
 
-	struct ether_vlan_header *oevh;
-	const ipha_t *oiph;
+	const void *oiph;
 	const tcph_t *otcph;
 	ipha_t *niph;
 	tcph_t *ntcph;
 	uint16_t ip_id;
 	uint32_t tcp_seq, tcp_sum, otcp_sum;
 
-	uint32_t offset;
+	boolean_t is_v6 = B_FALSE;
+	ip6_t *niph6;
+
+	uint32_t offset = 0;
 	mblk_t *odatamp;
 	mblk_t *seg_chain, *prev_nhdrmp, *next_nhdrmp, *nhdrmp, *ndatamp;
 	mblk_t *tmptail;
 
+	mac_ether_offload_info_t meoi = { 0 };
+
 	ASSERT3P(head, !=, NULL);
 	ASSERT3P(tail, !=, NULL);
 	ASSERT3P(count, !=, NULL);
@@ -726,38 +730,50 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 	/* Assume we are dealing with a single LSO message. */
 	ASSERT3P(omp->b_next, ==, NULL);
 
+	mac_ether_offload_info(omp, &meoi);
+	opktlen = meoi.meoi_len;
+	oehlen = meoi.meoi_l2hlen;
+	oiphlen = meoi.meoi_l3hlen;
+	otcphlen = meoi.meoi_l4hlen;
+	ohdrslen = oehlen + oiphlen + otcphlen;
+
+	/* Performing LSO requires that we successfully read fully up to L4 */
+	if ((MEOI_L4INFO_SET & meoi.meoi_flags) == 0) {
+		mac_drop_pkt(omp, "unable to fully parse packet to L4");
+		goto fail;
+	}
+
+	if (meoi.meoi_l3proto != ETHERTYPE_IP &&
+	    meoi.meoi_l3proto != ETHERTYPE_IPV6) {
+		mac_drop_pkt(omp, "LSO'd packet has non-IP L3 header: %x",
+		    meoi.meoi_l3proto);
+		goto fail;
+	}
+
+	if (meoi.meoi_l4proto != IPPROTO_TCP) {
+		mac_drop_pkt(omp, "LSO unsupported protocol: %x",
+		    meoi.meoi_l4proto);
+		goto fail;
+	}
+
+	is_v6 = meoi.meoi_l3proto == ETHERTYPE_IPV6;
+
+	mss = DB_LSOMSS(omp);
+	if (mss == 0) {
+		mac_drop_pkt(omp, "packet misconfigured for LSO (MSS == 0)");
+		goto fail;
+	}
+	ASSERT3U(opktlen, <=, IP_MAXPACKET + oehlen);
+
 	/*
-	 * XXX: This is a hack to deal with mac_add_vlan_tag().
-	 *
-	 * When VLANs are in play, mac_add_vlan_tag() creates a new
-	 * mblk with just the ether_vlan_header and tacks it onto the
-	 * front of 'omp'. This breaks the assumptions made below;
-	 * namely that the TCP/IP headers are in the first mblk. In
-	 * this case, since we already have to pay the cost of LSO
-	 * emulation, we simply pull up everything. While this might
-	 * seem irksome, keep in mind this will only apply in a couple
-	 * of scenarios: a) an LSO-capable VLAN client sending to a
-	 * non-LSO-capable client over the "MAC/bridge loopback"
-	 * datapath or b) an LSO-capable VLAN client is sending to a
-	 * client that, for whatever reason, doesn't have DLS-bypass
-	 * enabled. Finally, we have to check for both a tagged and
-	 * untagged sized mblk depending on if the mblk came via
-	 * mac_promisc_dispatch() or mac_rx_deliver().
-	 *
-	 * In the future, two things should be done:
-	 *
-	 * 1. This function should make use of some yet to be
-	 *    implemented "mblk helpers". These helper functions would
-	 *    perform all the b_cont walking for us and guarantee safe
-	 *    access to the mblk data.
-	 *
-	 * 2. We should add some slop to the mblks so that
-	 *    mac_add_vlan_tag() can just edit the first mblk instead
-	 *    of allocating on the hot path.
+	 * Ensure the headers are contiguous. The IP header is used only for the
+	 * benefit of DTrace SDTs, whereas the TCP header is actively read.
+	 * This small pullup should only practically happen when
+	 * mac_add_vlan_tag is in play, which prepends a new mblk in front
+	 * containing the amended Ethernet header.
 	 */
-	if (MBLKL(omp) == sizeof (struct ether_vlan_header) ||
-	    MBLKL(omp) == sizeof (struct ether_header)) {
-		mblk_t *tmp = msgpullup(omp, -1);
+	if (MBLKL(omp) < ohdrslen) {
+		mblk_t *tmp = msgpullup(omp, ohdrslen);
 
 		if (tmp == NULL) {
 			mac_drop_pkt(omp, "failed to pull up");
@@ -769,69 +785,15 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 		omp = tmp;
 	}
 
-	mss = DB_LSOMSS(omp);
-	ASSERT3U(msgsize(omp), <=, IP_MAXPACKET +
-	    sizeof (struct ether_vlan_header));
-	opktlen = msgsize(omp);
-
-	/*
-	 * First, get references to the IP and TCP headers and
-	 * determine the total TCP length (header + data).
-	 *
-	 * Thanks to mac_hw_emul() we know that the first mblk must
-	 * contain (at minimum) the full L2 header. However, this
-	 * function assumes more than that. It assumes the L2/L3/L4
-	 * headers are all contained in the first mblk of a message
-	 * (i.e., no b_cont walking for headers). While this is a
-	 * current reality (our native TCP stack and viona both
-	 * enforce this) things may become more nuanced in the future
-	 * (e.g. when introducing encap support or adding new
-	 * clients). For now we guard against this case by dropping
-	 * the packet.
-	 */
-	oevh = (struct ether_vlan_header *)omp->b_rptr;
-	if (oevh->ether_tpid == htons(ETHERTYPE_VLAN))
-		oehlen = sizeof (struct ether_vlan_header);
-	else
-		oehlen = sizeof (struct ether_header);
-
-	ASSERT3U(MBLKL(omp), >=, (oehlen + sizeof (ipha_t) + sizeof (tcph_t)));
-	if (MBLKL(omp) < (oehlen + sizeof (ipha_t) + sizeof (tcph_t))) {
-		mac_drop_pkt(omp, "mblk doesn't contain TCP/IP headers");
-		goto fail;
-	}
-
-	oiph = (ipha_t *)(omp->b_rptr + oehlen);
-	oiphlen = IPH_HDR_LENGTH(oiph);
+	oiph = (void *)(omp->b_rptr + oehlen);
 	otcph = (tcph_t *)(omp->b_rptr + oehlen + oiphlen);
-	otcphlen = TCP_HDR_LENGTH(otcph);
-
-	/*
-	 * Currently we only support LSO for TCP/IPv4.
-	 */
-	if (IPH_HDR_VERSION(oiph) != IPV4_VERSION) {
-		mac_drop_pkt(omp, "LSO unsupported IP version: %uhh",
-		    IPH_HDR_VERSION(oiph));
-		goto fail;
-	}
-
-	if (oiph->ipha_protocol != IPPROTO_TCP) {
-		mac_drop_pkt(omp, "LSO unsupported protocol: %uhh",
-		    oiph->ipha_protocol);
-		goto fail;
-	}
 
 	if (otcph->th_flags[0] & (TH_SYN | TH_RST | TH_URG)) {
 		mac_drop_pkt(omp, "LSO packet has SYN|RST|URG set");
 		goto fail;
 	}
 
-	ohdrslen = oehlen + oiphlen + otcphlen;
-	if ((len = MBLKL(omp)) < ohdrslen) {
-		mac_drop_pkt(omp, "LSO packet too short: %d < %u", len,
-		    ohdrslen);
-		goto fail;
-	}
+	len = MBLKL(omp);
 
 	/*
 	 * Either we have data in the first mblk or it's just the
@@ -848,10 +810,11 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 	}
 
 	/* Make sure we still have enough data. */
-	ASSERT3U(msgsize(odatamp), >=, opktlen - ohdrslen);
+	odatalen = opktlen - ohdrslen;
+	ASSERT3U(msgsize(odatamp), >=, odatalen);
 
 	/*
-	 * If a MAC negotiated LSO then it must negotioate both
+	 * If a MAC negotiated LSO then it must negotiate both
 	 * HCKSUM_IPHDRCKSUM and either HCKSUM_INET_FULL_V4 or
 	 * HCKSUM_INET_PARTIAL; because both the IP and TCP headers
 	 * change during LSO segmentation (only the 3 fields of the
@@ -871,7 +834,6 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 	 */
 	ASSERT3S(DB_TYPE(omp), ==, M_DATA);
 	ocsum_flags = DB_CKSUMFLAGS(omp);
-	ASSERT3U(ocsum_flags & HCK_IPV4_HDRCKSUM, !=, 0);
 	ASSERT3U(ocsum_flags & (HCK_PARTIALCKSUM | HCK_FULLCKSUM), !=, 0);
 
 	/*
@@ -901,8 +863,6 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 		ocsum_stuff = (uint32_t)DB_CKSUMSTUFF(omp);
 	}
 
-	odatalen = opktlen - ohdrslen;
-
 	/*
 	 * Subtract one to account for the case where the data length
 	 * is evenly divisble by the MSS. Add one to account for the
@@ -916,8 +876,8 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 	}
 
 	DTRACE_PROBE6(sw__lso__start, mblk_t *, omp, void_ip_t *, oiph,
-	    __dtrace_tcp_tcph_t *, otcph, uint_t, odatalen, uint_t, mss, uint_t,
-	    nsegs);
+	    __dtrace_tcp_tcph_t *, otcph, uint_t, odatalen, uint_t, mss,
+	    uint_t, nsegs);
 
 	seg_chain = NULL;
 	tmptail = seg_chain;
@@ -939,6 +899,10 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 		}
 		ASSERT3P(nhdrmp->b_cont, ==, NULL);
 
+		/* Copy over the header stack. */
+		bcopy(omp->b_rptr, nhdrmp->b_rptr, ohdrslen);
+		nhdrmp->b_wptr += ohdrslen;
+
 		if (seg_chain == NULL) {
 			seg_chain = nhdrmp;
 		} else {
@@ -949,7 +913,7 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 		tmptail = nhdrmp;
 
 		/*
-		 * Calculate this segment's lengh. It's either the MSS
+		 * Calculate this segment's length. It's either the MSS
 		 * or whatever remains for the last segment.
 		 */
 		seg_len = last_seg ? oleft : mss;
@@ -967,6 +931,13 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 		DB_CKSUMFLAGS(ndatamp) &= ~HW_LSO;
 		ASSERT3U(seg_len, <=, oleft);
 		oleft -= seg_len;
+
+		/* Setup partial checksum offsets. */
+		if (ocsum_flags & HCK_PARTIALCKSUM) {
+			DB_CKSUMSTART(nhdrmp) = ocsum_start;
+			DB_CKSUMEND(nhdrmp) = oiphlen + otcphlen + seg_len;
+			DB_CKSUMSTUFF(nhdrmp) = ocsum_stuff;
+		}
 	}
 
 	/* We should have consumed entire LSO msg. */
@@ -983,13 +954,30 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 	 * Set headers and checksum for first segment.
 	 */
 	nhdrmp = seg_chain;
-	bcopy(omp->b_rptr, nhdrmp->b_rptr, ohdrslen);
-	nhdrmp->b_wptr = nhdrmp->b_rptr + ohdrslen;
-	niph = (ipha_t *)(nhdrmp->b_rptr + oehlen);
 	ASSERT3U(msgsize(nhdrmp->b_cont), ==, mss);
-	niph->ipha_length = htons(oiphlen + otcphlen + mss);
-	niph->ipha_hdr_checksum = 0;
-	ip_id = ntohs(niph->ipha_ident);
+
+	if (is_v6) {
+		niph6 = (ip6_t *)(nhdrmp->b_rptr + oehlen);
+		niph6->ip6_plen = htons(
+		    (oiphlen - IPV6_HDR_LEN) + otcphlen + mss);
+	} else {
+		niph = (ipha_t *)(nhdrmp->b_rptr + oehlen);
+		niph->ipha_length = htons(oiphlen + otcphlen + mss);
+		/*
+		 * If the v4 checksum was filled, we won't have a v4 offload
+		 * flag. We can't write zero checksums without inserting said
+		 * flag, but our output frames won't necessarily be rechecked by
+		 * the caller! As a compromise, we need to force emulation to
+		 * uphold the same contracts the packet already agreed to.
+		 */
+		if (niph->ipha_hdr_checksum != 0) {
+			emul |= MAC_IPCKSUM_EMUL;
+			ocsum_flags |= HCK_IPV4_HDRCKSUM;
+		}
+		niph->ipha_hdr_checksum = 0;
+		ip_id = ntohs(niph->ipha_ident);
+	}
+
 	ntcph = (tcph_t *)(nhdrmp->b_rptr + oehlen + oiphlen);
 	tcp_seq = BE32_TO_U32(ntcph->th_seq);
 	tcp_seq += mss;
@@ -1011,9 +999,6 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 	 * we need to calculate it here.
 	 */
 	if (ocsum_flags & HCK_PARTIALCKSUM) {
-		DB_CKSUMSTART(nhdrmp) = ocsum_start;
-		DB_CKSUMEND(nhdrmp) = ntohs(niph->ipha_length);
-		DB_CKSUMSTUFF(nhdrmp) = ocsum_stuff;
 		tcp_sum = BE16_TO_U16(ntcph->th_sum);
 		otcp_sum = tcp_sum;
 		tcp_sum += mss + otcphlen;
@@ -1021,8 +1006,7 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 		U16_TO_BE16(tcp_sum, ntcph->th_sum);
 	}
 
-	if ((ocsum_flags & (HCK_PARTIALCKSUM | HCK_FULLCKSUM)) &&
-	    (emul & MAC_HWCKSUM_EMULS)) {
+	if ((ocsum_flags & HCK_TX_FLAGS) && (emul & MAC_HWCKSUM_EMULS)) {
 		next_nhdrmp = nhdrmp->b_next;
 		nhdrmp->b_next = NULL;
 		nhdrmp = mac_sw_cksum(nhdrmp, emul);
@@ -1051,9 +1035,8 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 
 	seg = 1;
 	DTRACE_PROBE5(sw__lso__seg, mblk_t *, nhdrmp, void_ip_t *,
-	    (ipha_t *)(nhdrmp->b_rptr + oehlen), __dtrace_tcp_tcph_t *,
-	    (tcph_t *)(nhdrmp->b_rptr + oehlen + oiphlen), uint_t, mss,
-	    uint_t, seg);
+	    (is_v6 ? (void *)niph6 : (void *)niph),
+	    __dtrace_tcp_tcph_t *, ntcph, uint_t, mss, int_t, seg);
 	seg++;
 
 	/* There better be at least 2 segs. */
@@ -1082,13 +1065,17 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 		 * their original to make sure we produce the correct
 		 * value.
 		 */
-		bcopy(seg_chain->b_rptr, nhdrmp->b_rptr, ohdrslen);
-		nhdrmp->b_wptr = nhdrmp->b_rptr + ohdrslen;
-		niph = (ipha_t *)(nhdrmp->b_rptr + oehlen);
-		niph->ipha_ident = htons(++ip_id);
 		ASSERT3P(msgsize(nhdrmp->b_cont), ==, mss);
-		niph->ipha_length = htons(oiphlen + otcphlen + mss);
-		niph->ipha_hdr_checksum = 0;
+		if (is_v6) {
+			niph6 = (ip6_t *)(nhdrmp->b_rptr + oehlen);
+			niph6->ip6_plen = htons(
+			    (oiphlen - IPV6_HDR_LEN) + otcphlen + mss);
+		} else {
+			niph = (ipha_t *)(nhdrmp->b_rptr + oehlen);
+			niph->ipha_ident = htons(++ip_id);
+			niph->ipha_length = htons(oiphlen + otcphlen + mss);
+			niph->ipha_hdr_checksum = 0;
+		}
 		ntcph = (tcph_t *)(nhdrmp->b_rptr + oehlen + oiphlen);
 		U32_TO_BE32(tcp_seq, ntcph->th_seq);
 		tcp_seq += mss;
@@ -1099,18 +1086,14 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 		ntcph->th_flags[0] &= ~(TH_FIN | TH_PUSH);
 		DB_CKSUMFLAGS(nhdrmp) = (uint16_t)(ocsum_flags & ~HW_LSO);
 
-		if (ocsum_flags & HCK_PARTIALCKSUM) {
-			/*
-			 * First and middle segs have same
-			 * pseudo-header checksum.
-			 */
+		/*
+		 * First and middle segs have same
+		 * pseudo-header checksum.
+		 */
+		if (ocsum_flags & HCK_PARTIALCKSUM)
 			U16_TO_BE16(tcp_sum, ntcph->th_sum);
-			DB_CKSUMSTART(nhdrmp) = ocsum_start;
-			DB_CKSUMEND(nhdrmp) = ntohs(niph->ipha_length);
-			DB_CKSUMSTUFF(nhdrmp) = ocsum_stuff;
-		}
 
-		if ((ocsum_flags & (HCK_PARTIALCKSUM | HCK_FULLCKSUM)) &&
+		if ((ocsum_flags & HCK_TX_FLAGS) &&
 		    (emul & MAC_HWCKSUM_EMULS)) {
 			next_nhdrmp = nhdrmp->b_next;
 			nhdrmp->b_next = NULL;
@@ -1139,9 +1122,8 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 		}
 
 		DTRACE_PROBE5(sw__lso__seg, mblk_t *, nhdrmp, void_ip_t *,
-		    (ipha_t *)(nhdrmp->b_rptr + oehlen), __dtrace_tcp_tcph_t *,
-		    (tcph_t *)(nhdrmp->b_rptr + oehlen + oiphlen),
-		    uint_t, mss, uint_t, seg);
+		    (is_v6 ? (void *)niph6 : (void *)niph),
+		    __dtrace_tcp_tcph_t *, ntcph, uint_t, mss, uint_t, seg);
 
 		ASSERT3P(nhdrmp->b_next, !=, NULL);
 		prev_nhdrmp = nhdrmp;
@@ -1156,30 +1138,30 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 	 * Now we set the last segment header. The difference being
 	 * that FIN/PSH/RST flags are allowed.
 	 */
-	bcopy(seg_chain->b_rptr, nhdrmp->b_rptr, ohdrslen);
-	nhdrmp->b_wptr = nhdrmp->b_rptr + ohdrslen;
-	niph = (ipha_t *)(nhdrmp->b_rptr + oehlen);
-	niph->ipha_ident = htons(++ip_id);
 	len = msgsize(nhdrmp->b_cont);
 	ASSERT3S(len, >, 0);
-	niph->ipha_length = htons(oiphlen + otcphlen + len);
-	niph->ipha_hdr_checksum = 0;
+	if (is_v6) {
+		niph6 = (ip6_t *)(nhdrmp->b_rptr + oehlen);
+		niph6->ip6_plen = htons(
+		    (oiphlen - IPV6_HDR_LEN) + otcphlen + len);
+	} else {
+		niph = (ipha_t *)(nhdrmp->b_rptr + oehlen);
+		niph->ipha_ident = htons(++ip_id);
+		niph->ipha_length = htons(oiphlen + otcphlen + len);
+		niph->ipha_hdr_checksum = 0;
+	}
 	ntcph = (tcph_t *)(nhdrmp->b_rptr + oehlen + oiphlen);
 	U32_TO_BE32(tcp_seq, ntcph->th_seq);
 
 	DB_CKSUMFLAGS(nhdrmp) = (uint16_t)(ocsum_flags & ~HW_LSO);
 	if (ocsum_flags & HCK_PARTIALCKSUM) {
-		DB_CKSUMSTART(nhdrmp) = ocsum_start;
-		DB_CKSUMEND(nhdrmp) = ntohs(niph->ipha_length);
-		DB_CKSUMSTUFF(nhdrmp) = ocsum_stuff;
 		tcp_sum = otcp_sum;
 		tcp_sum += len + otcphlen;
 		tcp_sum = (tcp_sum >> 16) + (tcp_sum & 0xFFFF);
 		U16_TO_BE16(tcp_sum, ntcph->th_sum);
 	}
 
-	if ((ocsum_flags & (HCK_PARTIALCKSUM | HCK_FULLCKSUM)) &&
-	    (emul & MAC_HWCKSUM_EMULS)) {
+	if ((ocsum_flags & HCK_TX_FLAGS) && (emul & MAC_HWCKSUM_EMULS)) {
 		/* This should be the last mblk. */
 		ASSERT3P(nhdrmp->b_next, ==, NULL);
 		nhdrmp = mac_sw_cksum(nhdrmp, emul);
@@ -1193,9 +1175,8 @@ mac_sw_lso(mblk_t *omp, mac_emul_t emul, mblk_t **head, mblk_t **tail,
 	}
 
 	DTRACE_PROBE5(sw__lso__seg, mblk_t *, nhdrmp, void_ip_t *,
-	    (ipha_t *)(nhdrmp->b_rptr + oehlen), __dtrace_tcp_tcph_t *,
-	    (tcph_t *)(nhdrmp->b_rptr + oehlen + oiphlen), uint_t, len,
-	    uint_t, seg);
+	    (is_v6 ? (void *)niph6 : (void *)niph),
+	    __dtrace_tcp_tcph_t *, ntcph, uint_t, len, uint_t, seg);
 
 	/*
 	 * Free the reference to the original LSO message as it is
diff --git a/usr/src/uts/common/os/ip_cksum.c b/usr/src/uts/common/os/ip_cksum.c
index 862b33cd11..b51d552385 100644
--- a/usr/src/uts/common/os/ip_cksum.c
+++ b/usr/src/uts/common/os/ip_cksum.c
@@ -23,6 +23,7 @@
  * Use is subject to license terms.
  * Copyright 2021 Joyent, Inc.
  * Copyright 2022 Garrett D'Amore
+ * Copyright 2025 Oxide Computer Company
  */
 /* Copyright (c) 1990 Mentat Inc. */
 
@@ -179,6 +180,7 @@ ip_cksum(mblk_t *mp, int offset, uint_t sum)
 	if (dp->db_struioflag & STRUIO_IP)
 		psum = *(ushort_t *)dp->db_struioun.data;
 slow:
+	DTRACE_PROBE(ip_cksum_slow);
 	pmp = 0;
 slow1:
 	mlen = 0;
@@ -436,7 +438,7 @@ ip_csum_hdr(ipha_t *ipha)
  *             transport header. This argument may be set to NULL if
  *             only the length is desired.
  *
- * return: Whether or not the header was malformed.
+ * return: Whether or not the header is well formed.
  *
  * This function assumes the IPv6 header along with all extensions are
  * contained solely in this mblk: i.e., there is no b_cont walking.
diff --git a/usr/src/uts/common/sys/mac_client_impl.h b/usr/src/uts/common/sys/mac_client_impl.h
index 0e3a6306e0..17879d48c8 100644
--- a/usr/src/uts/common/sys/mac_client_impl.h
+++ b/usr/src/uts/common/sys/mac_client_impl.h
@@ -22,6 +22,7 @@
  * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
  * Use is subject to license terms.
  * Copyright (c) 2012, Joyent, Inc.  All rights reserved.
+ * Copyright 2025 Oxide Computer Company
  */
 /*
  * Copyright 2018 Joyent, Inc.
@@ -119,8 +120,7 @@ struct mac_client_impl_s {			/* Protected by */
 	uint32_t		mci_state_flags;	/* WO */
 	mac_rx_t		mci_rx_fn;		/* Rx Quiescence */
 	void			*mci_rx_arg;		/* Rx Quiescence */
-	mac_direct_rx_t		mci_direct_rx_fn;	/* SL */
-	void			*mci_direct_rx_arg;	/* SL */
+	mac_direct_rxs_t	mci_direct_rx;		/* SL */
 	mac_rx_t		mci_rx_p_fn;		/* Rx Quiescence */
 	void			*mci_rx_p_arg;		/* Rx Quiescence */
 	void			*mci_p_unicast_list;
diff --git a/usr/src/uts/common/sys/mac_client_priv.h b/usr/src/uts/common/sys/mac_client_priv.h
index 01cb27644c..701fb719b6 100644
--- a/usr/src/uts/common/sys/mac_client_priv.h
+++ b/usr/src/uts/common/sys/mac_client_priv.h
@@ -23,6 +23,7 @@
  * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
  * Use is subject to license terms.
  * Copyright 2018 Joyent, Inc.
+ * Copyright 2025 Oxide Computer Company
  */
 
 /*
@@ -50,7 +51,7 @@ extern "C" {
 #endif
 
 extern boolean_t mac_rx_bypass_set(mac_client_handle_t, mac_direct_rx_t,
-    void *);
+    void *, boolean_t);
 extern void mac_rx_bypass_enable(mac_client_handle_t);
 extern void mac_rx_bypass_disable(mac_client_handle_t);
 
diff --git a/usr/src/uts/common/sys/mac_impl.h b/usr/src/uts/common/sys/mac_impl.h
index 21f2c10a8e..d99c016d6c 100644
--- a/usr/src/uts/common/sys/mac_impl.h
+++ b/usr/src/uts/common/sys/mac_impl.h
@@ -21,6 +21,7 @@
 /*
  * Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved.
  * Copyright 2019 Joyent, Inc.
+ * Copyright 2025 Oxide Computer Company
  */
 
 #ifndef	_SYS_MAC_IMPL_H
@@ -907,6 +908,13 @@ extern void mac_led_init(mac_impl_t *);
 extern int mac_led_get(mac_handle_t, mac_led_mode_t *, mac_led_mode_t *);
 extern int mac_led_set(mac_handle_t, mac_led_mode_t);
 
+typedef struct mac_direct_rxs_s {
+	mac_direct_rx_t	mdrx_v4;
+	mac_direct_rx_t	mdrx_v6;
+	void		*mdrx_arg_v4;
+	void		*mdrx_arg_v6;
+} mac_direct_rxs_t;
+
 #ifdef	__cplusplus
 }
 #endif
diff --git a/usr/src/uts/common/sys/mac_soft_ring.h b/usr/src/uts/common/sys/mac_soft_ring.h
index 5a41899e60..1b2d7687a5 100644
--- a/usr/src/uts/common/sys/mac_soft_ring.h
+++ b/usr/src/uts/common/sys/mac_soft_ring.h
@@ -23,6 +23,7 @@
  * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
  * Use is subject to license terms.
  * Copyright 2017 Joyent, Inc.
+ * Copyright 2025 Oxide Computer Company
  */
 
 #ifndef	_SYS_MAC_SOFT_RING_H
@@ -74,9 +75,9 @@ struct mac_soft_ring_s {
 	mblk_t		*s_ring_first;	/* first mblk chain or NULL */
 	mblk_t		*s_ring_last;	/* last mblk chain or NULL */
 
-	mac_direct_rx_t	s_ring_rx_func;
-	void		*s_ring_rx_arg1;
-	mac_resource_handle_t  s_ring_rx_arg2;
+	mac_direct_rx_t		s_ring_rx_func;
+	void			*s_ring_rx_arg1;
+	mac_resource_handle_t	s_ring_rx_arg2;
 
 	/*
 	 * Threshold after which packets get dropped.
@@ -326,6 +327,10 @@ struct mac_soft_ring_set_s {
 	int		srs_tcp_ring_count;
 	mac_soft_ring_t	**srs_udp_soft_rings;
 	int		srs_udp_ring_count;
+	mac_soft_ring_t	**srs_tcp6_soft_rings;
+	int		srs_tcp6_ring_count;
+	mac_soft_ring_t	**srs_udp6_soft_rings;
+	int		srs_udp6_ring_count;
 	mac_soft_ring_t	**srs_oth_soft_rings;
 	int		srs_oth_ring_count;
 	/*
@@ -395,6 +400,9 @@ struct mac_soft_ring_set_s {
 #define	ST_RING_BW_CTL		0x0020
 #define	ST_RING_TX		0x0040
 
+#define	ST_RING_TCP6		0x0080
+#define	ST_RING_UDP6		0x0100
+
 /*
  * State flags.
  */
@@ -684,7 +692,7 @@ extern mac_tx_cookie_t mac_tx_srs_no_desc(mac_soft_ring_set_t *, mblk_t *,
 
 /* Subflow specific stuff */
 extern int mac_srs_flow_create(struct mac_client_impl_s *, flow_entry_t *,
-    mac_resource_props_t *, int, int, mac_direct_rx_t);
+    mac_resource_props_t *, int, int, mac_direct_rxs_t *);
 extern void mac_srs_update_bwlimit(flow_entry_t *, mac_resource_props_t *);
 extern void mac_srs_adjust_subflow_bwlimit(struct mac_client_impl_s *);
 extern void mac_srs_update_drv(struct mac_client_impl_s *);
diff --git a/usr/src/uts/intel/io/viona/viona_rx.c b/usr/src/uts/intel/io/viona/viona_rx.c
index 40514731e6..2e4e89def5 100644
--- a/usr/src/uts/intel/io/viona/viona_rx.c
+++ b/usr/src/uts/intel/io/viona/viona_rx.c
@@ -554,15 +554,23 @@ viona_rx_common(viona_vring_t *ring, mblk_t *mp, boolean_t is_loopback)
 
 				/*
 				 * Emulation of LSO requires that cksum offload
-				 * be enabled on the mblk.  Since only IPv4 is
-				 * supported by the LSO emulation, the v4 cksum
-				 * is enabled unconditionally.
+				 * be enabled on the mblk.
 				 */
 				if ((DB_CKSUMFLAGS(mp) &
 				    (HCK_FULLCKSUM | HCK_PARTIALCKSUM)) == 0) {
 					DB_CKSUMFLAGS(mp) |= HCK_FULLCKSUM;
 				}
-				DB_CKSUMFLAGS(mp) |= HCK_IPV4_HDRCKSUM;
+
+				/*
+				 * IPv4 packets should have the offload enabled
+				 * for the IPv4 header checksum.
+				 */
+				mac_ether_offload_info_t meoi;
+				mac_ether_offload_info(mp, &meoi);
+				if ((meoi.meoi_flags & MEOI_L2INFO_SET) != 0 &&
+				    meoi.meoi_l3proto == ETHERTYPE_IP) {
+					DB_CKSUMFLAGS(mp) |= HCK_IPV4_HDRCKSUM;
+				}
 
 				mac_hw_emul(&mp, &tail, &n_pkts, MAC_ALL_EMULS);
 				if (mp == NULL) {
-- 
2.49.0

