Page MenuHomeFreeBSD

D59350.id185786.diff
No OneTemporary

D59350.id185786.diff

diff --git a/share/man/man4/ng_bridge.4 b/share/man/man4/ng_bridge.4
--- a/share/man/man4/ng_bridge.4
+++ b/share/man/man4/ng_bridge.4
@@ -32,7 +32,7 @@
.\"
.\" Author: Archie Cobbs <archie@FreeBSD.org>
.\"
-.Dd August 31, 2026
+.Dd September 3, 2026
.Dt NG_BRIDGE 4
.Os
.Sh NAME
@@ -69,6 +69,14 @@
.Pp
A looped back link will be temporarily muted, i.e., all traffic
received on that link is ignored.
+.Pp
+If a packet is received on a hook that this node recently used to
+transmit that source address, it is treated as a reflected transmit
+and is dropped without moving the host and without muting the hook.
+Promiscuous receive on
+.Xr ng_ether 4
+.Ar lower
+is enough to produce that reflection.
.Sh IPFW PROCESSING
Processing of IP packets via the
.Xr ipfirewall 4
diff --git a/sys/netgraph/ng_bridge.c b/sys/netgraph/ng_bridge.c
--- a/sys/netgraph/ng_bridge.c
+++ b/sys/netgraph/ng_bridge.c
@@ -41,7 +41,10 @@
* each of its connected hooks, or links. A simple loop detection
* algorithm is included which disables a link for priv->conf.loopTimeout
* seconds when a host is seen to have jumped from one link to
- * another within priv->conf.minStableAge seconds.
+ * another within priv->conf.minStableAge seconds. A packet received
+ * on a hook this node recently used to transmit that source is
+ * treated as a reflected transmit, not a move, and is dropped
+ * without muting the hook.
*
* We keep a hashtable that maps Ethernet addresses to host info,
* which is contained in struct ng_bridge_host's. These structures
@@ -134,8 +137,11 @@
struct ng_bridge_host {
u_char addr[6]; /* ethernet address */
link_p link; /* link where addr can be found */
+ link_cp xmitLink; /* last hook we sent this source out */
u_int16_t age; /* seconds ago entry was created */
u_int16_t staleness; /* seconds ago host last heard from */
+ u_int16_t xmitAge; /* seconds since that send */
+ u_int16_t floodAge; /* seconds since flooded this source */
SLIST_ENTRY(ng_bridge_host) next; /* next entry in bucket */
};
@@ -715,9 +721,19 @@
*/
static inline int
ng_bridge_send_data(link_cp dst, int manycast, struct mbuf *m, item_p item) {
+ const priv_p priv = NG_NODE_PRIVATE(NG_HOOK_NODE(dst->hook));
+ struct ng_bridge_host *host;
+ u_char shost[ETHER_ADDR_LEN];
+ int have_shost = 0;
int error = 0;
size_t len = m->m_pkthdr.len;
+ if (m->m_len >= ETHER_HDR_LEN) {
+ bcopy(mtod(m, struct ether_header *)->ether_shost,
+ shost, ETHER_ADDR_LEN);
+ have_shost = 1;
+ }
+
if(item != NULL)
NG_FWD_NEW_DATA(error, item, dst->hook, m);
else
@@ -734,6 +750,19 @@
return (error);
}
+ /*
+ * Record the hook this source was last sent out. Unlocked
+ * during shared access; a lost store costs at most one
+ * missed or spurious reflected-transmit drop.
+ */
+ if (have_shost) {
+ host = ng_bridge_get(priv, shost);
+ if (host != NULL) {
+ host->xmitLink = dst;
+ host->xmitAge = 0;
+ }
+ }
+
counter_u64_add(dst->stats.xmitPackets, 1);
counter_u64_add(dst->stats.xmitOctets, len);
switch (manycast) {
@@ -852,12 +881,27 @@
/* Look up packet's source Ethernet address in hashtable */
if ((host = ng_bridge_get(priv, eh->ether_shost)) != NULL)
/* Update time since last heard from this host.
- * This is safe without locking, because it's
- * the only operation during shared access.
+ * This is safe without locking, like the other
+ * shared-access stores (xmitLink, xmitAge, floodAge).
*/
if (__predict_false(host->staleness > 0))
host->staleness = 0;
+ /*
+ * Our own transmit coming back: the source was recently sent
+ * out this hook or flooded out every hook. Drop it without
+ * moving the host or muting the hook.
+ */
+ if (host != NULL && host->link != ctx.incoming &&
+ ((host->xmitLink == ctx.incoming &&
+ host->xmitAge <= priv->conf.minStableAge) ||
+ host->floodAge <= priv->conf.minStableAge)) {
+ counter_u64_add(ctx.incoming->stats.loopDrops, 1);
+ NG_FREE_ITEM(item);
+ NG_FREE_M(ctx.m);
+ return (0);
+ }
+
/*
* learnMac is 0 on uplink: neither insert a new host nor
* move an existing one from packets received there.
@@ -920,6 +964,10 @@
counter_u64_add(ctx.incoming->stats.recvUnknown, 1);
}
+ /* A flood can reflect back on any hook */
+ if ((host = ng_bridge_get(priv, eh->ether_shost)) != NULL)
+ host->floodAge = 0;
+
/* Distribute unknown, multicast, broadcast pkts to all other links */
NG_NODE_FOREACH_HOOK(node, ng_bridge_send_ctx, &ctx);
@@ -1069,8 +1117,11 @@
return (ENOMEM);
bcopy(addr, host->addr, ETHER_ADDR_LEN);
host->link = link;
+ host->xmitLink = NULL;
host->staleness = 0;
host->age = 0;
+ host->xmitAge = 0xffff;
+ host->floodAge = 0xffff;
/* Add new element to hash bucket */
SLIST_INSERT_HEAD(&priv->tab[bucket], host, next);
@@ -1176,8 +1227,14 @@
*hptr = SLIST_NEXT(host, next);
free(host, M_NETGRAPH_BRIDGE);
priv->numHosts--;
- } else
+ } else {
+ /* forget the freed link */
+ if (host->xmitLink == link) {
+ host->xmitLink = NULL;
+ host->xmitAge = 0xffff;
+ }
hptr = &SLIST_NEXT(host, next);
+ }
}
}
}
@@ -1230,6 +1287,10 @@
} else {
if (host->age < 0xffff)
host->age++;
+ if (host->xmitAge < 0xffff)
+ host->xmitAge++;
+ if (host->floodAge < 0xffff)
+ host->floodAge++;
hptr = &SLIST_NEXT(host, next);
counter++;
}
diff --git a/tests/sys/netgraph/bridge.c b/tests/sys/netgraph/bridge.c
--- a/tests/sys/netgraph/bridge.c
+++ b/tests/sys/netgraph/bridge.c
@@ -34,6 +34,8 @@
#include <atf-c.h>
#include <errno.h>
#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
#include <net/ethernet.h>
#include <netinet/in.h>
@@ -50,6 +52,20 @@
int cnt;
};
+static void get_stats(char const *source, struct ng_mesg *msg, void *ctx);
+struct getstats
+{
+ u_int32_t tok;
+ struct ng_bridge_link_stats st;
+};
+
+static void get_hostlink(char const *source, struct ng_mesg *msg, void *ctx);
+struct gethost
+{
+ u_int32_t tok;
+ char hook[NG_HOOKSIZ];
+};
+
struct frame4
{
struct ether_header eh;
@@ -192,6 +208,7 @@
ATF_TC_BODY(loop, dummy)
{
ng_counter_t r;
+ struct getstats rs;
int i;
ng_init();
@@ -223,6 +240,12 @@
ng_handle_events(50, &r);
ATF_CHECK(r[0] == 0 && r[1] == 1);
+ /* host is learned now: flood again so both bridges record it */
+ ng_counter_clear(r);
+ ng_send_data("a", &msg4, sizeof(msg4));
+ ng_handle_events(50, &r);
+ ATF_CHECK(r[0] == 0 && r[1] == 1);
+
/*-
* Closed loop, DANGEROUS!
*
@@ -235,18 +258,98 @@
ng_counter_clear(r);
msg4.eh.ether_shost[5] = 1;
ng_errors(PASS);
+ errno = 0;
ng_send_data("a", &msg4, sizeof(msg4));
- ATF_CHECK_ERRNO(ELOOP, errno != 0); /* loop might be detected */
+ ATF_CHECK(errno == 0); /* reflected transmit, not a loop */
ng_errors(FAIL);
for (i = 0; i < 10; i++) /* don't run forever */
if (!ng_handle_event(50, &r))
break;
ATF_CHECK(r[0] == 0 && r[1] == 1);
+ /* dropped on the returning hooks, nothing muted */
+ ng_register_msg(get_stats);
+ rs.tok = ng_send_msg("bridge1:", "getstats 12");
+ ng_handle_events(50, &rs);
+ ATF_CHECK(rs.st.loopDrops == 1 && rs.st.loopDetects == 0);
+ rs.tok = ng_send_msg("bridge2:", "getstats 12");
+ ng_handle_events(50, &rs);
+ ATF_CHECK(rs.st.loopDrops == 1 && rs.st.loopDetects == 0);
+
ng_shutdown("bridge1:");
ng_shutdown("bridge2:");
}
+ATF_TC(reflect);
+ATF_TC_HEAD(reflect, conf)
+{
+ atf_tc_set_md_var(conf, "require.user", "root");
+}
+
+ATF_TC_BODY(reflect, dummy)
+{
+ ng_counter_t r;
+ struct getstats rs;
+ struct gethost rh;
+
+ ng_init();
+ ng_errors(PASS);
+ ng_shutdown("bridge:");
+ ng_errors(FAIL);
+
+ ng_mkpeer(".", "a", "bridge", "link0");
+ ng_name("a", "bridge");
+ ng_connect(".", "b", "bridge:", "link1");
+ ng_connect(".", "c", "bridge:", "link2");
+
+ ng_register_data("a", get_data0);
+ ng_register_data("b", get_data1);
+ ng_register_data("c", get_data2);
+
+ /* learn host 1 on link0, then flood again so it is recorded */
+ msg4.eh.ether_shost[5] = 1;
+ msg4.eh.ether_dhost[5] = 0;
+ ng_counter_clear(r);
+ ng_send_data("a", &msg4, sizeof(msg4));
+ ng_handle_events(50, &r);
+ ATF_CHECK(r[0] == 0 && r[1] == 1 && r[2] == 1);
+ ng_counter_clear(r);
+ ng_send_data("a", &msg4, sizeof(msg4));
+ ng_handle_events(50, &r);
+ ATF_CHECK(r[0] == 0 && r[1] == 1 && r[2] == 1);
+
+ /* reflect it back on link1: dropped, no move, no mute */
+ ng_counter_clear(r);
+ errno = 0;
+ ng_send_data("b", &msg4, sizeof(msg4));
+ ATF_CHECK(errno == 0);
+ ng_handle_events(50, &r);
+ ATF_CHECK(r[0] == 0 && r[1] == 0 && r[2] == 0);
+
+ ng_register_msg(get_stats);
+ rs.tok = ng_send_msg("bridge:", "getstats 1");
+ ng_handle_events(50, &rs);
+ ATF_CHECK(rs.st.loopDrops == 1 && rs.st.loopDetects == 0);
+
+ ng_register_msg(get_hostlink);
+ rh.tok = ng_send_msg("bridge:", "gettable");
+ ng_handle_events(50, &rh);
+ ATF_CHECK(strcmp(rh.hook, "link0") == 0);
+
+ /* past minStableAge the same frame is a genuine move */
+ sleep(3);
+ ng_counter_clear(r);
+ ng_send_data("b", &msg4, sizeof(msg4));
+ ng_handle_events(50, &r);
+ ATF_CHECK(r[0] == 1 && r[1] == 0 && r[2] == 1);
+
+ rh.tok = ng_send_msg("bridge:", "gettable");
+ ng_handle_events(50, &rh);
+ ATF_CHECK(strcmp(rh.hook, "link1") == 0);
+
+ ng_shutdown("bridge:");
+}
+
ATF_TC(many_unicasts);
ATF_TC_HEAD(many_unicasts, conf)
{
@@ -611,6 +714,7 @@
{
ATF_TP_ADD_TC(bridge, basic);
ATF_TP_ADD_TC(bridge, loop);
+ ATF_TP_ADD_TC(bridge, reflect);
ATF_TP_ADD_TC(bridge, persistence);
ATF_TP_ADD_TC(bridge, many_unicasts);
ATF_TP_ADD_TC(bridge, many_broadcasts);
@@ -630,3 +734,31 @@
if (rm->tok == msg->header.token)
rm->cnt = gt->numHosts;
}
+
+static void
+get_stats(char const *source, struct ng_mesg *msg, void *ctx)
+{
+ struct getstats *rs = ctx;
+
+ fprintf(stderr, "Response from %s to query %d\n", source, msg->header.token);
+ if (rs->tok == msg->header.token)
+ memcpy(&rs->st, msg->data, sizeof(rs->st));
+}
+
+/* hook of the host msg4 is sent from, or "" if unknown */
+static void
+get_hostlink(char const *source, struct ng_mesg *msg, void *ctx)
+{
+ struct gethost *rh = ctx;
+ struct ng_bridge_host_ary *gt = (void *)msg->data;
+ u_int32_t i;
+
+ fprintf(stderr, "Response from %s to query %d\n", source, msg->header.token);
+ if (rh->tok != msg->header.token)
+ return;
+ rh->hook[0] = '\0';
+ for (i = 0; i < gt->numHosts; i++)
+ if (memcmp(gt->hosts[i].addr, msg4.eh.ether_shost,
+ ETHER_ADDR_LEN) == 0)
+ strlcpy(rh->hook, gt->hosts[i].hook, sizeof(rh->hook));
+}

File Metadata

Mime Type
text/plain
Expires
Mon, Sep 7, 7:40 PM (2 h, 31 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38351741
Default Alt Text
D59350.id185786.diff (10 KB)

Event Timeline