Add dummymbuf module for testing purposes

Reviewed by:	kp
Differential Revision:	https://reviews.freebsd.org/D45928
This commit is contained in:
Igor Ostapenko 2024-08-14 14:45:38 +02:00 committed by Kristof Provost
parent bd4f2023bb
commit 8aaffd78c0
6 changed files with 657 additions and 0 deletions

View file

@ -135,6 +135,7 @@ MAN= aac.4 \
ds1307.4 \
ds3231.4 \
${_dtrace_provs} \
dummymbuf.4 \
dummynet.4 \
edsc.4 \
ehci.4 \

209
share/man/man4/dummymbuf.4 Normal file
View file

@ -0,0 +1,209 @@
.\"
.\" SPDX-License-Identifier: BSD-2-Clause
.\"
.\" Copyright (c) 2024 Igor Ostapenko <pm@igoro.pro>
.\"
.\" 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} <ifname> <opname>[ <opargs>];
.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 <number-of-bytes>
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: <filter match>: rule parsing failed: <the rule in question>"
If everything looks fine then extra spaces removal may help due to the parser
is kept very simple.
.It "dummymbuf: <filter match>: mbuf operation failed: <the rule in question>"
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 .

View file

@ -4151,6 +4151,7 @@ net/bpf_jitter.c optional bpf_jitter
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

View file

@ -100,6 +100,7 @@ SUBDIR= \
${_dpdk_lpm4} \
${_dpdk_lpm6} \
${_dpms} \
dummymbuf \
dummynet \
${_dwwdt} \
${_e6000sw} \

View file

@ -0,0 +1,9 @@
.PATH: ${SRCTOP}/sys/net
KMOD= dummymbuf
SRCS= dummymbuf.c
SRCS+= opt_inet.h opt_inet6.h
EXPORT_SYMS= YES
.include <bsd.kmod.mk>

436
sys/net/dummymbuf.c Normal file
View file

@ -0,0 +1,436 @@
/*-
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2024 Igor Ostapenko <pm@igoro.pro>
*
* 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 <sys/param.h>
#include <sys/kernel.h>
#include <sys/mbuf.h>
#include <sys/module.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <net/if.h>
#include <net/if_var.h>
#include <net/vnet.h>
#include <net/pfil.h>
/*
* 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 = &empty;
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} <ifname> <opname>[ <opargs>];"
" ...;");
/*
* 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} <ifname> <opname>[ <opargs>];
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);