diff --git a/share/man/man4/Makefile b/share/man/man4/Makefile --- a/share/man/man4/Makefile +++ b/share/man/man4/Makefile @@ -136,6 +136,7 @@ ds1307.4 \ ds3231.4 \ ${_dtrace_provs} \ + dummymbuf.4 \ dummynet.4 \ edsc.4 \ ehci.4 \ diff --git a/share/man/man4/dummymbuf.4 b/share/man/man4/dummymbuf.4 new file mode 100644 --- /dev/null +++ b/share/man/man4/dummymbuf.4 @@ -0,0 +1,209 @@ +.\" +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.\" Copyright (c) 2024 Igor Ostapenko +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.\" Note: The date here should be updated whenever a non-trivial +.\" change is made to the manual page. +.Dd August 2, 2024 +.Dt DUMMYMBUF 4 +.Os +.Sh NAME +.Nm dummymbuf +.Nd "mbuf alteration pfil hooks" +.Sh SYNOPSIS +To compile the driver into the kernel, +place the following line in the +kernel configuration file: +.Bd -ragged -offset indent +.Cd "device dummymbuf" +.Ed +.Pp +Alternatively, to load the driver as a +module at boot time, place the following line in +.Xr loader.conf 5 : +.Bd -literal -offset indent +dummymbuf_load="YES" +.Ed +.Sh DESCRIPTION +This module is intended to test networking code in the face of unusual mbuf +layouts. +The special +.Xr pfil 9 +hooks are provided for mbuf alteration and can be listed with +.Xr pfilctl 8 +as follows: +.Bd -literal -offset indent + Hook Type + dummymbuf:ethernet Ethernet + dummymbuf:inet6 IPv6 + dummymbuf:inet IPv4 +.Ed +.Pp +To activate a hook it must be linked to the respective +.Xr pfil 9 +head. +.Xr pfilctl 8 +can be used for the linking. +.Pp +Each time a hook is invoked it reads a single shared set of +.Sx RULES +from +.Va net.dummymbuf.rules +sysctl. +The rules are evaluated sequentially and each matching rule performs the +specified operation over the mbuf. +.Pp +After every successfully applied operation the +.Va net.dummymbuf.hits +sysctl counter is increased. +.Pp +A hook returns an altered mbuf for further processing, but it drops a packet +if rules parsing or an operation fails. +Also, the first mbuf of the original chain may be changed. +.Pp +The module is +.Xr VNET 9 +based, hence every +.Xr jail 2 +provides its own set of hooks and sysctl variables. +.Sh RULES +The set of rules is a semicolon separated list. +An empty string is treated as a parsing failure. +A rule conceptually has two parts, filter and operation, with the following +syntax: +.Bd -literal -offset indent +{inet | inet6 | ethernet} {in | out} [ ]; +.Ed +.Ss Filter +The first word of a rule matches +.Xr pfil 9 +type. +The second matches packet's direction, and the third matches the network +interface a packet is coming from. +.Ss Operation +An operation may have arguments separated from its name by space. +The available operations are: +.Bl -tag -width indent +.It pull-head +Unconditionally creates a brand new cluster-based mbuf and links it to be the +first mbuf of the original mbuf chain, with respective packet header moving. +After, the given number of bytes are pulled from the original mbuf chain. +.Pp +If it is asked to pull 0 bytes then the first mbuf of the resulting chain will +be left empty. +Asking to pull more than +.Dv MCLBYTES +is treated as an operation failure. +If a mbuf chain has less data than asked then the entire packet is pulled with +tail mbuf(s) left empty. +.Pp +As a result, only the layout of a mbuf chain is altered, its content logically +is left intact. +.El +.Sh SYSCTL VARIABLES +The following variables are available: +.Bl -tag -width indent +.It Va net.dummymbuf.rules +A string representing a single set of +.Sx RULES +shared among all +.Nm +hooks. +.It Va net.dummymbuf.hits +Number of times a rule has been applied. +It is reset to zero upon writing. +.El +.Sh EXAMPLES +As it was intended, +.Nm +can be found useful for firewall testing. +A mbuf chain could be altered before it hits a firewall to test that the latter +can handle a case respectively. +Thus, it is important to have correct sequence of hooks. +A test case should prepare and enable a firewall first to get its hooks linked. +After, the +.Xr pfilctl 8 +should be used to link +.Nm +hook(s) to put them in front of a firewall. +.Pp +The following links +.Va dummymbuf:inet6 +hook for inbound and puts it in front of other hooks: +.Bd -literal -offset indent +pfilctl link -i dummymbuf:inet6 inet6 +.Ed +.Pp +And this does the same for outbound: +.Bd -literal -offset indent +pfilctl link -o -a dummymbuf:inet6 inet6 +.Ed +.Pp +For instance, we want to test a scenario in which the very first mbuf in a +chain has zero m_len, to verify that a firewall can correctly read the +packet data despite that. +The following set of rules does it for inbound and outbound: +.Bd -literal -offset indent +sysctl net.dummymbuf.rules="inet6 in em0 pull-head 0; inet6 out em0 pull-head 0;" +.Ed +.Pp +It is encouraged to verify +.Va net.dummymbuf.hits +sysctl counter along with other test assertions to make sure that +.Nm +really does its work and there is no false positive due to misconfiguration. +It is a good idea to reset it before the action: +.Bd -literal -offset indent +sysctl net.dummymbuf.hits=0 +.Ed +.Pp +It is equally important to cleanup the things after the test case: +.Bd -literal -offset indent +pfilctl unlink -i dummymbuf:inet6 inet6 +pfilctl unlink -o dummymbuf:inet6 inet6 +sysctl net.dummymbuf.rules="" +.Ed +.Pp +If a test case operates within a temporary vnet then explicit cleanup can be +omitted, the +.Nm +facilities will vanish along with its vnet instance. +.Sh DIAGNOSTICS +.Bl -diag +.It "dummymbuf: : rule parsing failed: " +If everything looks fine then extra spaces removal may help due to the parser +is kept very simple. +.It "dummymbuf: : mbuf operation failed: " +Incorrect operation argument has been found, mbuf allocation has failed, etc. +.El +.Sh SEE ALSO +.Xr jail 2 , +.Xr pfilctl 8 , +.Xr mbuf 9 , +.Xr pfil 9 , +.Xr VNET 9 +.Sh AUTHORS +The module and this manual page were written by +.An Igor Ostapenko Aq Mt pm@igoro.pro . diff --git a/sys/conf/files b/sys/conf/files --- a/sys/conf/files +++ b/sys/conf/files @@ -4151,6 +4151,7 @@ net/bpf_filter.c optional bpf | netgraph_bpf net/bpf_zerocopy.c optional bpf net/bridgestp.c optional bridge | if_bridge +net/dummymbuf.c optional dummymbuf net/ieee8023ad_lacp.c optional lagg net/if.c standard net/ifq.c standard diff --git a/sys/modules/Makefile b/sys/modules/Makefile --- a/sys/modules/Makefile +++ b/sys/modules/Makefile @@ -101,6 +101,7 @@ ${_dpdk_lpm4} \ ${_dpdk_lpm6} \ ${_dpms} \ + dummymbuf \ dummynet \ ${_dwwdt} \ ${_e6000sw} \ diff --git a/sys/modules/dummymbuf/Makefile b/sys/modules/dummymbuf/Makefile new file mode 100644 --- /dev/null +++ b/sys/modules/dummymbuf/Makefile @@ -0,0 +1,9 @@ +.PATH: ${SRCTOP}/sys/net + +KMOD= dummymbuf +SRCS= dummymbuf.c +SRCS+= opt_inet.h opt_inet6.h + +EXPORT_SYMS= YES + +.include diff --git a/sys/net/dummymbuf.c b/sys/net/dummymbuf.c new file mode 100644 --- /dev/null +++ b/sys/net/dummymbuf.c @@ -0,0 +1,436 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024 Igor Ostapenko + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "opt_inet.h" +#include "opt_inet6.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/* + * Separate sysctl sub-tree + */ + +SYSCTL_NODE(_net, OID_AUTO, dummymbuf, 0, NULL, + "Dummy mbuf sysctl"); + +/* + * Rules + */ + +static MALLOC_DEFINE(M_DUMMYMBUF_RULES, "dummymbuf_rules", + "dummymbuf rules string buffer"); + +#define RULES_MAXLEN 1024 +VNET_DEFINE_STATIC(char *, dmb_rules) = NULL; +#define V_dmb_rules VNET(dmb_rules) + +VNET_DEFINE_STATIC(struct sx, dmb_rules_lock); +#define V_dmb_rules_lock VNET(dmb_rules_lock) + +#define DMB_RULES_SLOCK() sx_slock(&V_dmb_rules_lock) +#define DMB_RULES_SUNLOCK() sx_sunlock(&V_dmb_rules_lock) +#define DMB_RULES_XLOCK() sx_xlock(&V_dmb_rules_lock) +#define DMB_RULES_XUNLOCK() sx_xunlock(&V_dmb_rules_lock) + +static int +dmb_sysctl_handle_rules(SYSCTL_HANDLER_ARGS) +{ + int error = 0; + char empty = '\0'; + char **rulesp = (char **)arg1; + + if (req->newptr == NULL) { + // read only + DMB_RULES_SLOCK(); + arg1 = *rulesp; + if (arg1 == NULL) { + arg1 = ∅ + arg2 = 0; + } + error = sysctl_handle_string(oidp, arg1, arg2, req); + DMB_RULES_SUNLOCK(); + } else { + // read and write + DMB_RULES_XLOCK(); + if (*rulesp == NULL) + *rulesp = malloc(arg2, M_DUMMYMBUF_RULES, M_WAITOK); + arg1 = *rulesp; + error = sysctl_handle_string(oidp, arg1, arg2, req); + DMB_RULES_XUNLOCK(); + } + + return (error); +} + +SYSCTL_PROC(_net_dummymbuf, OID_AUTO, rules, + CTLTYPE_STRING | CTLFLAG_MPSAFE | CTLFLAG_RW | CTLFLAG_VNET, + &VNET_NAME(dmb_rules), RULES_MAXLEN, dmb_sysctl_handle_rules, "A", + "{inet | inet6 | ethernet} {in | out} [ ];" + " ...;"); + +/* + * Statistics + */ + +VNET_DEFINE_STATIC(counter_u64_t, dmb_hits); +#define V_dmb_hits VNET(dmb_hits) +SYSCTL_PROC(_net_dummymbuf, OID_AUTO, hits, + CTLTYPE_U64 | CTLFLAG_MPSAFE | CTLFLAG_STATS | CTLFLAG_RW | CTLFLAG_VNET, + &VNET_NAME(dmb_hits), 0, sysctl_handle_counter_u64, + "QU", "Number of times a rule has been applied"); + +/* + * pfil(9) context + */ + +VNET_DEFINE_STATIC(pfil_hook_t, dmb_pfil_inet_hook); +#define V_dmb_pfil_inet_hook VNET(dmb_pfil_inet_hook) + +VNET_DEFINE_STATIC(pfil_hook_t, dmb_pfil_inet6_hook); +#define V_dmb_pfil_inet6_hook VNET(dmb_pfil_inet6_hook) + +VNET_DEFINE_STATIC(pfil_hook_t, dmb_pfil_ethernet_hook); +#define V_dmb_pfil_ethernet_hook VNET(dmb_pfil_ethernet_hook) + +/* + * Logging + */ + +#define FEEDBACK(pfil_type, pfil_flags, ifp, rule, msg) \ + printf("dummymbuf: %s %b %s: %s: %.*s\n", \ + (pfil_type == PFIL_TYPE_IP4 ? "PFIL_TYPE_IP4" : \ + pfil_type == PFIL_TYPE_IP6 ? "PFIL_TYPE_IP6" : \ + pfil_type == PFIL_TYPE_ETHERNET ? "PFIL_TYPE_ETHERNET" : \ + "PFIL_TYPE_UNKNOWN"), \ + (pfil_flags), "\20\21PFIL_IN\22PFIL_OUT", \ + (ifp)->if_xname, \ + (msg), \ + (rule).syntax_len, (rule).syntax_begin \ + ) + +/* + * Internals + */ + +struct rule; +typedef struct mbuf * (*op_t)(struct mbuf *, struct rule *); +struct rule { + const char *syntax_begin; + int syntax_len; + int pfil_type; + int pfil_dir; + char ifname[IFNAMSIZ]; + op_t op; + const char *opargs; +}; + +static struct mbuf * +dmb_m_pull_head(struct mbuf *m, struct rule *rule) +{ + struct mbuf *n; + int count; + + count = (int)strtol(rule->opargs, NULL, 10); + if (count < 0 || count > MCLBYTES) + goto bad; + + if (!(m->m_flags & M_PKTHDR)) + goto bad; + if (m->m_pkthdr.len <= 0) + return (m); + if (count > m->m_pkthdr.len) + count = m->m_pkthdr.len; + + if ((n = m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR)) == NULL) + goto bad; + + m_move_pkthdr(n, m); + m_copydata(m, 0, count, n->m_ext.ext_buf); + n->m_len = count; + + m_adj(m, count); + n->m_next = m; + + return (n); + +bad: + m_freem(m); + return (NULL); +} + +static bool +read_rule(const char **cur, struct rule *rule) +{ + // {inet | inet6 | ethernet} {in | out} [ ]; + + rule->syntax_begin = NULL; + rule->syntax_len = 0; + + if (*cur == NULL) + return (false); + + // syntax_begin + while (**cur == ' ') + (*cur)++; + rule->syntax_begin = *cur; + + // syntax_len + char *delim = strchr(*cur, ';'); + if (delim == NULL) + return (false); + rule->syntax_len = (int)(delim - *cur + 1); + + // pfil_type + if (strstr(*cur, "inet6") == *cur) { + rule->pfil_type = PFIL_TYPE_IP6; + *cur += strlen("inet6"); + } else if (strstr(*cur, "inet") == *cur) { + rule->pfil_type = PFIL_TYPE_IP4; + *cur += strlen("inet"); + } else if (strstr(*cur, "ethernet")) { + rule->pfil_type = PFIL_TYPE_ETHERNET; + *cur += strlen("ethernet"); + } else { + return (false); + } + while (**cur == ' ') + (*cur)++; + + // pfil_dir + if (strstr(*cur, "in") == *cur) { + rule->pfil_dir = PFIL_IN; + *cur += strlen("in"); + } else if (strstr(*cur, "out") == *cur) { + rule->pfil_dir = PFIL_OUT; + *cur += strlen("out"); + } else { + return (false); + } + while (**cur == ' ') + (*cur)++; + + // ifname + char *sp = strchr(*cur, ' '); + if (sp == NULL || sp > delim) + return (false); + size_t len = sp - *cur; + if (len >= sizeof(rule->ifname)) + return (false); + strncpy(rule->ifname, *cur, len); + rule->ifname[len] = 0; + *cur = sp; + while (**cur == ' ') + (*cur)++; + + // opname + if (strstr(*cur, "pull-head") == *cur) { + rule->op = dmb_m_pull_head; + *cur += strlen("pull-head"); + } else { + return (false); + } + while (**cur == ' ') + (*cur)++; + + // opargs + if (*cur > delim) + return (false); + rule->opargs = *cur; + + *cur = delim + 1; + + return (true); +} + +static pfil_return_t +dmb_pfil_mbuf_chk(int pfil_type, struct mbuf **mp, struct ifnet *ifp, + int flags, void *ruleset, void *unused) +{ + struct mbuf *m = *mp; + const char *cursor; + bool parsed; + struct rule rule; + + DMB_RULES_SLOCK(); + cursor = V_dmb_rules; + while ((parsed = read_rule(&cursor, &rule))) { + if (rule.pfil_type == pfil_type && + rule.pfil_dir == (flags & rule.pfil_dir) && + strcmp(rule.ifname, ifp->if_xname) == 0) { + m = rule.op(m, &rule); + if (m == NULL) { + FEEDBACK(pfil_type, flags, ifp, rule, + "mbuf operation failed"); + break; + } + counter_u64_add(V_dmb_hits, 1); + } + if (strlen(cursor) == 0) + break; + } + if (!parsed) { + FEEDBACK(pfil_type, flags, ifp, rule, "rule parsing failed"); + m_freem(m); + m = NULL; + } + DMB_RULES_SUNLOCK(); + + if (m == NULL) { + *mp = NULL; + return (PFIL_DROPPED); + } + if (m != *mp) { + *mp = m; + return (PFIL_REALLOCED); + } + + return (PFIL_PASS); +} + +static pfil_return_t +dmb_pfil_inet_mbuf_chk(struct mbuf **mp, struct ifnet *ifp, int flags, + void *ruleset, struct inpcb *inp) +{ + return (dmb_pfil_mbuf_chk(PFIL_TYPE_IP4, mp, ifp, flags, + ruleset, inp)); +} + +static pfil_return_t +dmb_pfil_inet6_mbuf_chk(struct mbuf **mp, struct ifnet *ifp, int flags, + void *ruleset, struct inpcb *inp) +{ + return (dmb_pfil_mbuf_chk(PFIL_TYPE_IP6, mp, ifp, flags, + ruleset, inp)); +} + +static pfil_return_t +dmb_pfil_ethernet_mbuf_chk(struct mbuf **mp, struct ifnet *ifp, int flags, + void *ruleset, struct inpcb *inp) +{ + return (dmb_pfil_mbuf_chk(PFIL_TYPE_ETHERNET, mp, ifp, flags, + ruleset, inp)); +} + +static void +dmb_pfil_init(void) +{ + struct pfil_hook_args pha = { + .pa_version = PFIL_VERSION, + .pa_modname = "dummymbuf", + .pa_flags = PFIL_IN | PFIL_OUT, + }; + +#ifdef INET + pha.pa_type = PFIL_TYPE_IP4; + pha.pa_mbuf_chk = dmb_pfil_inet_mbuf_chk; + pha.pa_rulname = "inet"; + V_dmb_pfil_inet_hook = pfil_add_hook(&pha); +#endif + +#ifdef INET6 + pha.pa_type = PFIL_TYPE_IP6; + pha.pa_mbuf_chk = dmb_pfil_inet6_mbuf_chk; + pha.pa_rulname = "inet6"; + V_dmb_pfil_inet6_hook = pfil_add_hook(&pha); +#endif + + pha.pa_type = PFIL_TYPE_ETHERNET; + pha.pa_mbuf_chk = dmb_pfil_ethernet_mbuf_chk; + pha.pa_rulname = "ethernet"; + V_dmb_pfil_ethernet_hook = pfil_add_hook(&pha); +} + +static void +dmb_pfil_uninit(void) +{ +#ifdef INET + pfil_remove_hook(V_dmb_pfil_inet_hook); +#endif + +#ifdef INET6 + pfil_remove_hook(V_dmb_pfil_inet6_hook); +#endif + + pfil_remove_hook(V_dmb_pfil_ethernet_hook); +} + +static void +vnet_dmb_init(void *unused __unused) +{ + sx_init(&V_dmb_rules_lock, "dummymbuf rules"); + V_dmb_hits = counter_u64_alloc(M_WAITOK); + dmb_pfil_init(); +} +VNET_SYSINIT(vnet_dmb_init, SI_SUB_PROTO_PFIL, SI_ORDER_ANY, + vnet_dmb_init, NULL); + +static void +vnet_dmb_uninit(void *unused __unused) +{ + dmb_pfil_uninit(); + counter_u64_free(V_dmb_hits); + sx_destroy(&V_dmb_rules_lock); + free(V_dmb_rules, M_DUMMYMBUF_RULES); +} +VNET_SYSUNINIT(vnet_dmb_uninit, SI_SUB_PROTO_PFIL, SI_ORDER_ANY, + vnet_dmb_uninit, NULL); + +static int +dmb_modevent(module_t mod __unused, int event, void *arg __unused) +{ + int error = 0; + + switch (event) { + case MOD_LOAD: + case MOD_UNLOAD: + break; + default: + error = EOPNOTSUPP; + break; + } + + return (error); +} + +static moduledata_t dmb_mod = { + "dummymbuf", + dmb_modevent, + NULL +}; + +DECLARE_MODULE(dummymbuf, dmb_mod, SI_SUB_PROTO_PFIL, SI_ORDER_ANY); +MODULE_VERSION(dummymbuf, 1);