git: 31cf66d7554c - main - dummynet: add simple gilbert-elliott channel model

From: Richard Scheffenegger <rscheff_at_FreeBSD.org>
Date: Sun, 17 Dec 2023 21:55:51 UTC
The branch main has been updated by rscheff:

URL: https://cgit.FreeBSD.org/src/commit/?id=31cf66d7554c2fa6a5aea77f4cd54712e611cdd0

commit 31cf66d7554c2fa6a5aea77f4cd54712e611cdd0
Author:     Richard Scheffenegger <rscheff@FreeBSD.org>
AuthorDate: 2023-12-17 12:19:52 +0000
Commit:     Richard Scheffenegger <rscheff@FreeBSD.org>
CommitDate: 2023-12-17 12:20:45 +0000

    dummynet: add simple gilbert-elliott channel model
    
    Have a simple Gilbert-Elliott channel model in
    dummynet to mimick correlated loss behavior of
    realistic environments. This allows simpler testing
    of burst-loss environments.
    
    Reviewed By:           tuexen, kp, pauamma_gundo.com, #manpages
    Sponsored by:          NetApp, Inc.
    Differential Revision: https://reviews.freebsd.org/D42980
---
 sbin/ipfw/dummynet.c                 |  44 +++++++++++----
 sbin/ipfw/ipfw.8                     |  39 +++++++++++++-
 sys/netinet/ip_dummynet.h            |   3 +-
 sys/netpfil/ipfw/ip_dn_glue.c        |  51 ++++++++++--------
 sys/netpfil/ipfw/ip_dn_io.c          |  24 ++++++++-
 sys/netpfil/ipfw/ip_dn_private.h     |   9 ++++
 tests/sys/netpfil/common/dummynet.sh | 102 +++++++++++++++++++++++++++++++++++
 7 files changed, 235 insertions(+), 37 deletions(-)

diff --git a/sbin/ipfw/dummynet.c b/sbin/ipfw/dummynet.c
index 26d535428ec3..9663e983b31a 100644
--- a/sbin/ipfw/dummynet.c
+++ b/sbin/ipfw/dummynet.c
@@ -471,7 +471,7 @@ print_flowset_parms(struct dn_fs *fs, char *prefix)
 {
 	int l;
 	char qs[30];
-	char plr[30];
+	char plr[40];
 	char red[200];	/* Display RED parameters */
 
 	l = fs->qsize;
@@ -482,9 +482,17 @@ print_flowset_parms(struct dn_fs *fs, char *prefix)
 			sprintf(qs, "%d B", l);
 	} else
 		sprintf(qs, "%3d sl.", l);
-	if (fs->plr)
-		sprintf(plr, "plr %f", 1.0 * fs->plr / (double)(0x7fffffff));
-	else
+	if (fs->plr[0] || fs->plr[1]) {
+		if (fs->plr[1] == 0)
+			sprintf(plr, "plr %f",
+				1.0 * fs->plr[0] / (double)(0x7fffffff));
+		else
+			sprintf(plr, "plr %f,%f,%f,%f",
+				1.0 * fs->plr[0] / (double)(0x7fffffff),
+				1.0 * fs->plr[1] / (double)(0x7fffffff),
+				1.0 * fs->plr[2] / (double)(0x7fffffff),
+				1.0 * fs->plr[3] / (double)(0x7fffffff));
+	} else
 		plr[0] = '\0';
 
 	if (fs->flags & DN_IS_RED) {	/* RED parameters */
@@ -1408,13 +1416,27 @@ ipfw_config_pipe(int ac, char **av)
 
 		case TOK_PLR:
 			NEED(fs, "plr is only for pipes");
-			NEED1("plr needs argument 0..1\n");
-			d = strtod(av[0], NULL);
-			if (d > 1)
-				d = 1;
-			else if (d < 0)
-				d = 0;
-			fs->plr = (int)(d*0x7fffffff);
+			NEED1("plr needs one or four arguments 0..1\n");
+			if ((end = strsep(&av[0], ","))) {
+				d = strtod(end, NULL);
+				d = (d < 0) ? 0 : (d <= 1) ? d : 1;
+				fs->plr[0] = (int)(d*0x7fffffff);
+			}
+			if ((end = strsep(&av[0], ","))) {
+				d = strtod(end, NULL);
+				d = (d < 0) ? 0 : (d <= 1) ? d : 1;
+				fs->plr[1] = (int)(d*0x7fffffff);
+			}
+			if ((end = strsep(&av[0], ","))) {
+				d = strtod(end, NULL);
+				d = (d < 0) ? 0 : (d <= 1) ? d : 1;
+				fs->plr[2] = (int)(d*0x7fffffff);
+			}
+			if ((end = strsep(&av[0], ","))) {
+				d = strtod(end, NULL);
+				d = (d < 0) ? 0 : (d <= 1) ? d : 1;
+				fs->plr[3] = (int)(d*0x7fffffff);
+			}
 			ac--; av++;
 			break;
 
diff --git a/sbin/ipfw/ipfw.8 b/sbin/ipfw/ipfw.8
index e62b8d6efc95..715d8580f1ce 100644
--- a/sbin/ipfw/ipfw.8
+++ b/sbin/ipfw/ipfw.8
@@ -1,5 +1,5 @@
 .\"
-.Dd September 28, 2023
+.Dd December 17, 2023
 .Dt IPFW 8
 .Os
 .Sh NAME
@@ -3039,12 +3039,47 @@ needed for some experimental setups where you want to simulate
 loss or congestion at a remote router.
 .Pp
 .It Cm plr Ar packet-loss-rate
+.It Cm plr Ar K,p,H,r
 Packet loss rate.
 Argument
 .Ar packet-loss-rate
 is a floating-point number between 0 and 1, with 0 meaning no
 loss, 1 meaning 100% loss.
-The loss rate is internally represented on 31 bits.
+.Pp
+When invoked with four arguments, the simple Gilbert-Elliott
+channel model with two states (Good and Bad) is used.
+.Bd -literal -offset indent
+                        r
+               .----------------.
+               v                |
+         .------------.   .------------.
+         |     G      |   |     B      |
+         |  drop (K)  |   |  drop (H)  |
+         '------------'   '------------'
+               |                ^
+               '----------------'
+                        p
+
+.Ed
+This has the associated probabilities
+.Po Ar K
+and
+.Ar H Pc
+for the loss probability. This is different from the literature,
+where this model is described with probabilities of successful
+transmission k and h. However, converting from literature is
+easy:
+.Pp
+K = 1 - k ; H = 1 - h
+.Pp
+This is to retain consistency within the interface and allow the
+quick re-use of loss probability when giving only a single argument.
+In addition the state change probabilities
+.Po Ar p
+and
+.Ar r Pc
+are given.
+All of the above probabilities are internally represented on 31 bits.
 .Pp
 .It Cm queue Brq Ar slots | size Ns Cm Kbytes
 Queue size, in
diff --git a/sys/netinet/ip_dummynet.h b/sys/netinet/ip_dummynet.h
index b36b93bbe96b..4e05dcca606f 100644
--- a/sys/netinet/ip_dummynet.h
+++ b/sys/netinet/ip_dummynet.h
@@ -145,7 +145,7 @@ struct dn_fs {
 	uint32_t fs_nr;		/* the flowset number */
 	uint32_t flags;		/* userland flags */
 	int qsize;		/* queue size in slots or bytes */
-	int32_t plr;		/* PLR, pkt loss rate (2^31-1 means 100%) */
+	int32_t pl_state;	/* packet loss state */
 	uint32_t buckets;	/* buckets used for the queue hash table */
 
 	struct ipfw_flow_id flow_mask;
@@ -168,6 +168,7 @@ struct dn_fs {
 	int min_th ;		/* minimum threshold for queue (scaled) */
 	int max_p ;		/* maximum value for p_b (scaled) */
 
+	int32_t plr[4];		/* PLR, pkt loss rate (2^31-1 means 100%) */
 };
 
 /*
diff --git a/sys/netpfil/ipfw/ip_dn_glue.c b/sys/netpfil/ipfw/ip_dn_glue.c
index 204b34091781..0412b730e4df 100644
--- a/sys/netpfil/ipfw/ip_dn_glue.c
+++ b/sys/netpfil/ipfw/ip_dn_glue.c
@@ -77,35 +77,35 @@ struct dn_heap7 {
 
 /* Common to 7.2 and 8 */
 struct dn_flow_set {
-	SLIST_ENTRY(dn_flow_set)    next;   /* linked list in a hash slot */
+	SLIST_ENTRY(dn_flow_set) next;	/* linked list in a hash slot */
 
-	u_short fs_nr ;             /* flow_set number       */
+	u_short fs_nr ;			/* flow_set number       */
 	u_short flags_fs;
 #define DNOLD_HAVE_FLOW_MASK   0x0001
-#define DNOLD_IS_RED       0x0002
+#define DNOLD_IS_RED           0x0002
 #define DNOLD_IS_GENTLE_RED    0x0004
-#define DNOLD_QSIZE_IS_BYTES   0x0008  /* queue size is measured in bytes */
-#define DNOLD_NOERROR      0x0010  /* do not report ENOBUFS on drops  */
-#define DNOLD_HAS_PROFILE      0x0020  /* the pipe has a delay profile. */
-#define DNOLD_IS_PIPE      0x4000
-#define DNOLD_IS_QUEUE     0x8000
+#define DNOLD_QSIZE_IS_BYTES   0x0008	/* queue size is measured in bytes */
+#define DNOLD_NOERROR          0x0010	/* do not report ENOBUFS on drops  */
+#define DNOLD_HAS_PROFILE      0x0020	/* the pipe has a delay profile. */
+#define DNOLD_IS_PIPE          0x4000
+#define DNOLD_IS_QUEUE         0x8000
 
-	struct dn_pipe7 *pipe ;  /* pointer to parent pipe */
-	u_short parent_nr ;     /* parent pipe#, 0 if local to a pipe */
+	struct dn_pipe7 *pipe ;		/* pointer to parent pipe */
+	u_short parent_nr ;		/* parent pipe#, 0 if local to a pipe */
 
-	int weight ;        /* WFQ queue weight */
-	int qsize ;         /* queue size in slots or bytes */
-	int plr ;           /* pkt loss rate (2^31-1 means 100%) */
+	int weight ;			/* WFQ queue weight */
+	int qsize ;			/* queue size in slots or bytes */
+	int plr[4] ;			/* pkt loss rate (2^31-1 means 100%) */
 
 	struct ipfw_flow_id flow_mask ;
 
 	/* hash table of queues onto this flow_set */
-	int rq_size ;       /* number of slots */
-	int rq_elements ;       /* active elements */
-	struct dn_flow_queue7 **rq;  /* array of rq_size entries */
+	int rq_size ;			/* number of slots */
+	int rq_elements ;		/* active elements */
+	struct dn_flow_queue7 **rq ;	/* array of rq_size entries */
 
-	u_int32_t last_expired ;    /* do not expire too frequently */
-	int backlogged ;        /* #active queues for this flowset */
+	u_int32_t last_expired ;	/* do not expire too frequently */
+	int backlogged ;		/* #active queues for this flowset */
 
         /* RED parameters */
 #define SCALE_RED               16
@@ -420,7 +420,10 @@ dn_compat_config_queue(struct dn_fs *fs, void* v)
 	fs->flow_mask = f->flow_mask;
 	fs->buckets = f->rq_size;
 	fs->qsize = f->qsize;
-	fs->plr = f->plr;
+	fs->plr[0] = f->plr[0];
+	fs->plr[1] = f->plr[1];
+	fs->plr[2] = f->plr[2];
+	fs->plr[3] = f->plr[3];
 	fs->par[0] = f->weight;
 	fs->flags = convertflags2new(f->flags_fs);
 	if (fs->flags & DN_IS_GENTLE_RED || fs->flags & DN_IS_RED) {
@@ -645,7 +648,10 @@ dn_c_copy_pipe(struct dn_schk *s, struct copy_args *a, int nq)
 
 	fs->parent_nr = l->link_nr - DN_MAX_ID;
 	fs->qsize = f->fs.qsize;
-	fs->plr = f->fs.plr;
+	fs->plr[0] = f->fs.plr[0];
+	fs->plr[1] = f->fs.plr[1];
+	fs->plr[2] = f->fs.plr[2];
+	fs->plr[3] = f->fs.plr[3];
 	fs->w_q = f->fs.w_q;
 	fs->max_th = f->max_th;
 	fs->min_th = f->min_th;
@@ -698,7 +704,10 @@ dn_c_copy_fs(struct dn_fsk *f, struct copy_args *a, int nq)
 	fs->next.sle_next = (struct dn_flow_set *)DN_IS_QUEUE;
 	fs->fs_nr = f->fs.fs_nr;
 	fs->qsize = f->fs.qsize;
-	fs->plr = f->fs.plr;
+	fs->plr[0] = f->fs.plr[0];
+	fs->plr[1] = f->fs.plr[1];
+	fs->plr[2] = f->fs.plr[2];
+	fs->plr[3] = f->fs.plr[3];
 	fs->w_q = f->fs.w_q;
 	fs->max_th = f->max_th;
 	fs->min_th = f->min_th;
diff --git a/sys/netpfil/ipfw/ip_dn_io.c b/sys/netpfil/ipfw/ip_dn_io.c
index 3e6bd0e229b5..03116cb0641c 100644
--- a/sys/netpfil/ipfw/ip_dn_io.c
+++ b/sys/netpfil/ipfw/ip_dn_io.c
@@ -497,8 +497,28 @@ dn_enqueue(struct dn_queue *q, struct mbuf* m, int drop)
 	ni->tot_pkts++;
 	if (drop)
 		goto drop;
-	if (f->plr && random() < f->plr)
-		goto drop;
+	if (f->plr[0] || f->plr[1]) {
+		if (__predict_true(f->plr[1] == 0)) {
+			if (random() < f->plr[0])
+				goto drop;
+		} else {
+			switch (f->pl_state) {
+			case PLR_STATE_B:
+				if (random() < f->plr[3])
+					f->pl_state = PLR_STATE_G;
+				if (random() < f->plr[2])
+					goto drop;
+				break;
+			case PLR_STATE_G: /* FALLTHROUGH */
+			default:
+				if (random() < f->plr[1])
+					f->pl_state = PLR_STATE_B;
+				if (random() < f->plr[0])
+					goto drop;
+				break;
+			}
+		}
+	}
 	if (m->m_pkthdr.rcvif != NULL)
 		m_rcvif_serialize(m);
 #ifdef NEW_AQM
diff --git a/sys/netpfil/ipfw/ip_dn_private.h b/sys/netpfil/ipfw/ip_dn_private.h
index ea5b809d8d28..756a997b6ec3 100644
--- a/sys/netpfil/ipfw/ip_dn_private.h
+++ b/sys/netpfil/ipfw/ip_dn_private.h
@@ -392,6 +392,15 @@ enum {
 	PROTO_IFB =	0x0c, /* layer2 + ifbridge */
 };
 
+/*
+ * States for the Packet Loss Rate Gilbert-Elliott
+ * channel model
+ */
+enum {
+	PLR_STATE_G = 0,
+	PLR_STATE_B,
+};
+
 //extern struct dn_parms V_dn_cfg;
 VNET_DECLARE(struct dn_parms, dn_cfg);
 #define V_dn_cfg	VNET(dn_cfg)
diff --git a/tests/sys/netpfil/common/dummynet.sh b/tests/sys/netpfil/common/dummynet.sh
index 14d863d001c8..e5ffd3836dfc 100644
--- a/tests/sys/netpfil/common/dummynet.sh
+++ b/tests/sys/netpfil/common/dummynet.sh
@@ -517,6 +517,102 @@ nat_cleanup()
 	firewall_cleanup $1
 }
 
+pls_basic_head()
+{
+	atf_set descr 'Basic dummynet packet loss rate test'
+	atf_set require.user root
+}
+
+pls_basic_body()
+{
+	fw=$1
+	firewall_init $fw
+	dummynet_init $fw
+
+	epair=$(vnet_mkepair)
+	vnet_mkjail alcatraz ${epair}b
+
+	ifconfig ${epair}a 192.0.2.1/24 up
+	jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
+
+	firewall_config alcatraz ${fw} \
+		"ipfw"	\
+			"ipfw add 65432 ip from any to any" \
+		"pf"	\
+			"pass on ${epair}b"
+
+	# Sanity check
+	atf_check -s exit:0 -o match:'100 packets transmitted, 100 packets received' ping -i .1 -c 100 192.0.2.2
+
+	jexec alcatraz dnctl pipe 1 config plr 0.1
+
+	firewall_config alcatraz ${fw} \
+		"ipfw"	\
+			"ipfw add 1000 pipe 1 ip from 192.0.2.1 to 192.0.2.2" \
+		"pf"	\
+			"pass on ${epair}b dnpipe 1"
+
+	# check if the expected number of pings
+	# are dropped (84 - 96 responses).
+	# repeat up to 6 times if the initial
+	# checks fail
+	atf_check -s exit:0 -o match:'100 packets transmitted, (8[4-9]|9[0-6]) packets received' -r 6:10 ping -i 0.010 -c 100 192.0.2.2
+}
+
+pls_basic_cleanup()
+{
+	firewall_cleanup $1
+}
+
+pls_gilbert_head()
+{
+	atf_set descr 'dummynet Gilbert-Elliott packet loss model test'
+	atf_set require.user root
+}
+
+pls_gilbert_body()
+{
+	fw=$1
+	firewall_init $fw
+	dummynet_init $fw
+
+	epair=$(vnet_mkepair)
+	vnet_mkjail alcatraz ${epair}b
+
+	ifconfig ${epair}a 192.0.2.1/24 up
+	jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
+
+	firewall_config alcatraz ${fw} \
+		"ipfw"	\
+			"ipfw add 65432 ip from any to any" \
+		"pf"	\
+			"pass on ${epair}b"
+
+	# Sanity check
+	atf_check -s exit:0 -o match:'100 packets transmitted, 100 packets received' ping -i .1 -c 100 192.0.2.2
+
+	jexec alcatraz dnctl pipe 1 config plr 0.01,0.1,0.8,0.2
+
+	firewall_config alcatraz ${fw} \
+		"ipfw"	\
+			"ipfw add 1000 pipe 1 ip from 192.0.2.1 to 192.0.2.2" \
+		"pf"	\
+			"pass on ${epair}b dnpipe 1"
+
+	# check if the expected number of pings
+	# are dropped (70 - 85 responses).
+	# repeat up to 6 times if the initial
+	# checks fail
+	atf_check -s exit:0 -o match:'100 packets transmitted, (7[0-9]|8[0-5]) packets received' -r 6:10 ping -i 0.010 -c 100 192.0.2.2
+}
+
+pls_gilbert_cleanup()
+{
+	firewall_cleanup $1
+}
+
+
+
 setup_tests		\
 	interface_removal	\
 		ipfw	\
@@ -539,4 +635,10 @@ setup_tests		\
 		ipfw	\
 		pf	\
 	nat		\
+		pf	\
+	pls_basic	\
+		ipfw	\
+		pf	\
+	pls_gilbert	\
+		ipfw	\
 		pf