bridge: allow member interface vlan to be configured

Add two new bridge(4) interface options, 'vlanfilter' and 'untagged':

	# ifconfig bridge0 vlanfilter ix0
	# ifconfig bridge0 -vlanfilter ix0
	# ifconfig bridge0 untagged ix0 20
	# ifconfig bridge0 -untagged ix0

Setting 'vlanfilter' causes the bridge to filter ingress and egress
traffic on that interface based on the frame's VLAN, rather than simply
passing all frames.  By default, an interface is not permitted on any
VLANs, so all frames will be dropped.

Setting 'untagged' allows the interface to send and receive untagged
traffic in the given VLAN, allowing two (or more) interfaces in the
same VLAN to communicate with each other, but not with any other
interface.

Setting 'untagged' on an interface automatically enables 'vlanfilter'
as well.  The untagged VLAN may be removed using the '-untagged'
option, but this does not disable VLAN filtering automatically.

Tagged frames may not be sent or received on a port with VLAN filtering
enabled.

Update bridge.4 to document this change, and also add an overview of the
existing vlan/.1q support in if_bridge.

Basic tests for the new functionality are included.

Bump __FreeBSD_version for struct ibfreq ABI change.

Reviewed by:	kevans, kp
Approved by:	kevans (mentor)
Differential Revision:	https://reviews.freebsd.org/D49993
This commit is contained in:
Lexi Winter 2025-07-05 05:27:25 +01:00
parent 533c0d345b
commit 65ed1a035c
7 changed files with 362 additions and 17 deletions

View file

@ -211,6 +211,8 @@ bridge_status(if_ctx *ctx)
else
printf(" <unknown state %d>", state);
}
if (member->ifbr_untagged != 0)
printf(" untagged %u", (unsigned)member->ifbr_untagged);
printf("\n");
}
@ -576,6 +578,45 @@ setbridge_ifpathcost(if_ctx *ctx, const char *ifn, const char *cost)
err(1, "BRDGSIFCOST %s", cost);
}
static void
setbridge_untagged(if_ctx *ctx, const char *ifn, const char *vlanid)
{
struct ifbreq req;
u_long val;
memset(&req, 0, sizeof(req));
if (get_val(vlanid, &val) < 0)
errx(1, "invalid VLAN identifier: %s", vlanid);
/*
* Reject vlan 0, since it's not a valid vlan identifier and has a
* special meaning in the kernel interface.
*/
if (val == 0)
errx(1, "invalid VLAN identifier: %lu", val);
strlcpy(req.ifbr_ifsname, ifn, sizeof(req.ifbr_ifsname));
req.ifbr_untagged = val;
if (do_cmd(ctx, BRDGSIFUNTAGGED, &req, sizeof(req), 1) < 0)
err(1, "BRDGSIFUNTAGGED %s", vlanid);
}
static void
unsetbridge_untagged(if_ctx *ctx, const char *ifn, int dummy __unused)
{
struct ifbreq req;
memset(&req, 0, sizeof(req));
strlcpy(req.ifbr_ifsname, ifn, sizeof(req.ifbr_ifsname));
req.ifbr_untagged = 0;
if (do_cmd(ctx, BRDGSIFUNTAGGED, &req, sizeof(req), 1) < 0)
err(1, "BRDGSIFUNTAGGED");
}
static void
setbridge_ifmaxaddr(if_ctx *ctx, const char *ifn, const char *arg)
{
@ -612,17 +653,27 @@ setbridge_timeout(if_ctx *ctx, const char *arg, int dummy __unused)
static void
setbridge_private(if_ctx *ctx, const char *val, int dummy __unused)
{
do_bridgeflag(ctx, val, IFBIF_PRIVATE, 1);
}
static void
unsetbridge_private(if_ctx *ctx, const char *val, int dummy __unused)
{
do_bridgeflag(ctx, val, IFBIF_PRIVATE, 0);
}
static void
setbridge_vlanfilter(if_ctx *ctx, const char *val, int dummy __unused)
{
do_bridgeflag(ctx, val, IFBIF_VLANFILTER, 1);
}
static void
unsetbridge_vlanfilter(if_ctx *ctx, const char *val, int dummy __unused)
{
do_bridgeflag(ctx, val, IFBIF_VLANFILTER, 0);
}
static struct cmd bridge_cmds[] = {
DEF_CMD_ARG("addm", setbridge_add),
DEF_CMD_ARG("deletem", setbridge_delete),
@ -659,6 +710,10 @@ static struct cmd bridge_cmds[] = {
DEF_CMD_ARG2("ifpriority", setbridge_ifpriority),
DEF_CMD_ARG2("ifpathcost", setbridge_ifpathcost),
DEF_CMD_ARG2("ifmaxaddr", setbridge_ifmaxaddr),
DEF_CMD_ARG("vlanfilter", setbridge_vlanfilter),
DEF_CMD_ARG("-vlanfilter", unsetbridge_vlanfilter),
DEF_CMD_ARG2("untagged", setbridge_untagged),
DEF_CMD_ARG("-untagged", unsetbridge_untagged),
DEF_CMD_ARG("timeout", setbridge_timeout),
DEF_CMD_ARG("private", setbridge_private),
DEF_CMD_ARG("-private", unsetbridge_private),

View file

@ -28,7 +28,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
.Dd April 24, 2025
.Dd July 5, 2025
.Dt IFCONFIG 8
.Os
.Sh NAME
@ -2696,6 +2696,25 @@ source addresses are dropped until an existing host cache entry expires or is
removed.
Set to 0 to disable.
.El
.Ss Bridge VLAN Filtering Parameters
The behaviour of these options is described in the
.Dq VLAN SUPPORT
section of
.Xr bridge 4 .
.Bl -tag -width indent
.It Cm vlanfilter Ar interface
Enable VLAN filtering on an interface.
.It Cm -vlanfilter Ar interface
Disable VLAN filtering on an interface.
.It Cm untagged Ar interface Ar vlan-id
Set the untagged VLAN identifier for an interface.
.Pp
Setting
.Cm untagged
will automatically enable VLAN filtering on the interface.
.It Cm -untagged Ar interface Ar vlan-id
Clear the untagged VLAN identifier for an interface.
.El
.Ss Link Aggregation and Link Failover Parameters
The following parameters are specific to lagg interfaces:
.Bl -tag -width indent

View file

@ -36,7 +36,7 @@
.\" ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
.\" POSSIBILITY OF SUCH DAMAGE.
.\"
.Dd May 28, 2025
.Dd July 5, 2025
.Dt IF_BRIDGE 4
.Os
.Sh NAME
@ -271,6 +271,35 @@ by setting the
.Va net.link.bridge.log_stp
node using
.Xr sysctl 8 .
.Sh VLAN SUPPORT
The
.Nm
driver has limited support for virtual LANs (VLANs).
The bridge implements independent VLAN learning, i.e. MAC addresses are
learned on a per-VLAN basis, and the same MAC address may be learned on
multiple interfaces on different VLANs.
Incoming frames with an 802.1Q tag will be assigned to the appropriate
VLAN.
.Pp
By default no access control is enabled, so any interface may
participate in any VLAN.
.Pp
VLAN filtering may be enabled on an interface using the
.Xr ifconfig 8
.Cm vlanfilter
option.
When VLAN filtering is enabled, an interface may only send and receive
untagged frames.
The interface's untagged VLAN ID may be configured using the
.Xr ifconfig 8
.Cm untagged
option.
If an untagged VLAN ID is configured, incoming frames will be assigned
to that VLAN, and the interface may receive outgoing untagged frames
in that VLAN.
.Pp
There is no support for adding or removing 802.1Q tags from frames
processed by the bridge.
.Sh PACKET FILTERING
Packet filtering can be used with any firewall package that hooks in via the
.Xr pfil 9

View file

@ -254,6 +254,7 @@ struct bridge_iflist {
uint32_t bif_addrcnt; /* cur. # of addresses */
uint32_t bif_addrexceeded;/* # of address violations */
struct epoch_context bif_epoch_ctx;
ether_vlanid_t bif_untagged; /* untagged vlan id */
};
/*
@ -335,13 +336,12 @@ static int bridge_enqueue(struct bridge_softc *, struct ifnet *,
static void bridge_rtdelete(struct bridge_softc *, struct ifnet *ifp, int);
static void bridge_forward(struct bridge_softc *, struct bridge_iflist *,
struct mbuf *m);
struct mbuf *m, ether_vlanid_t vlan);
static bool bridge_member_ifaddrs(void);
static void bridge_timer(void *);
static void bridge_broadcast(struct bridge_softc *, struct ifnet *,
struct mbuf *, int);
struct mbuf *, int, ether_vlanid_t);
static void bridge_span(struct bridge_softc *, struct mbuf *);
static int bridge_rtupdate(struct bridge_softc *, const uint8_t *,
@ -353,6 +353,8 @@ static void bridge_rtage(struct bridge_softc *);
static void bridge_rtflush(struct bridge_softc *, int);
static int bridge_rtdaddr(struct bridge_softc *, const uint8_t *,
ether_vlanid_t);
static bool bridge_vfilter_out(const struct bridge_iflist *,
const struct mbuf *, ether_vlanid_t);
static void bridge_rtable_init(struct bridge_softc *);
static void bridge_rtable_fini(struct bridge_softc *);
@ -400,6 +402,7 @@ static int bridge_ioctl_sma(struct bridge_softc *, void *);
static int bridge_ioctl_sifprio(struct bridge_softc *, void *);
static int bridge_ioctl_sifcost(struct bridge_softc *, void *);
static int bridge_ioctl_sifmaxaddr(struct bridge_softc *, void *);
static int bridge_ioctl_sifuntagged(struct bridge_softc *, void *);
static int bridge_ioctl_addspan(struct bridge_softc *, void *);
static int bridge_ioctl_delspan(struct bridge_softc *, void *);
static int bridge_ioctl_gbparam(struct bridge_softc *, void *);
@ -618,6 +621,8 @@ static const struct bridge_control bridge_control_table[] = {
{ bridge_ioctl_sifmaxaddr, sizeof(struct ifbreq),
BC_F_COPYIN|BC_F_SUSER },
{ bridge_ioctl_sifuntagged, sizeof(struct ifbreq),
BC_F_COPYIN|BC_F_SUSER },
};
static const int bridge_control_table_size = nitems(bridge_control_table);
@ -1495,6 +1500,7 @@ bridge_ioctl_gifflags(struct bridge_softc *sc, void *arg)
req->ifbr_addrcnt = bif->bif_addrcnt;
req->ifbr_addrmax = bif->bif_addrmax;
req->ifbr_addrexceeded = bif->bif_addrexceeded;
req->ifbr_untagged = bif->bif_untagged;
/* Copy STP state options as flags */
if (bp->bp_operedge)
@ -1872,6 +1878,25 @@ bridge_ioctl_sifmaxaddr(struct bridge_softc *sc, void *arg)
return (0);
}
static int
bridge_ioctl_sifuntagged(struct bridge_softc *sc, void *arg)
{
struct ifbreq *req = arg;
struct bridge_iflist *bif;
bif = bridge_lookup_member(sc, req->ifbr_ifsname);
if (bif == NULL)
return (ENOENT);
if (req->ifbr_untagged > DOT1Q_VID_MAX)
return (EINVAL);
if (req->ifbr_untagged != DOT1Q_VID_NULL)
bif->bif_flags |= IFBIF_VLANFILTER;
bif->bif_untagged = req->ifbr_untagged;
return (0);
}
static int
bridge_ioctl_addspan(struct bridge_softc *sc, void *arg)
{
@ -2376,7 +2401,7 @@ bridge_transmit(struct ifnet *ifp, struct mbuf *m)
NULL) {
error = bridge_enqueue(sc, dst_if, m);
} else
bridge_broadcast(sc, ifp, m, 0);
bridge_broadcast(sc, ifp, m, 0, DOT1Q_VID_NULL);
return (error);
}
@ -2430,12 +2455,11 @@ bridge_qflush(struct ifnet *ifp __unused)
*/
static void
bridge_forward(struct bridge_softc *sc, struct bridge_iflist *sbif,
struct mbuf *m)
struct mbuf *m, ether_vlanid_t vlan)
{
struct bridge_iflist *dbif;
struct ifnet *src_if, *dst_if, *ifp;
struct ether_header *eh;
uint16_t vlan;
uint8_t *dst;
int error;
@ -2446,7 +2470,6 @@ bridge_forward(struct bridge_softc *sc, struct bridge_iflist *sbif,
if_inc_counter(ifp, IFCOUNTER_IPACKETS, 1);
if_inc_counter(ifp, IFCOUNTER_IBYTES, m->m_pkthdr.len);
vlan = VLANTAGOF(m);
if ((sbif->bif_flags & IFBIF_STP) &&
sbif->bif_stp.bp_state == BSTP_IFSTATE_DISCARDING)
@ -2535,7 +2558,7 @@ bridge_forward(struct bridge_softc *sc, struct bridge_iflist *sbif,
}
if (dst_if == NULL) {
bridge_broadcast(sc, src_if, m, 1);
bridge_broadcast(sc, src_if, m, 1, vlan);
return;
}
@ -2555,6 +2578,10 @@ bridge_forward(struct bridge_softc *sc, struct bridge_iflist *sbif,
if (sbif->bif_flags & dbif->bif_flags & IFBIF_PRIVATE)
goto drop;
/* Do VLAN filtering. */
if (!bridge_vfilter_out(dbif, m, vlan))
goto drop;
if ((dbif->bif_flags & IFBIF_STP) &&
dbif->bif_stp.bp_state == BSTP_IFSTATE_DISCARDING)
goto drop;
@ -2636,6 +2663,27 @@ bridge_input(struct ifnet *ifp, struct mbuf *m)
return (NULL);
}
/* Do VLAN filtering. */
if (bif->bif_flags & IFBIF_VLANFILTER) {
/*
* If the frame was received with a tag, drop it, since we only
* support untagged ports which shouldn't be receiving tagged
* frames.
*
* If the frame was received without a tag, and the port doesn't
* have an untagged vlan configured, drop it.
*/
if (vlan != DOT1Q_VID_NULL ||
bif->bif_untagged == DOT1Q_VID_NULL) {
if_inc_counter(sc->sc_ifp, IFCOUNTER_IERRORS, 1);
m_freem(m);
return (NULL);
}
/* Otherwise, assign the untagged frame to the correct vlan. */
vlan = bif->bif_untagged;
}
bridge_span(sc, m);
if (m->m_flags & (M_BCAST|M_MCAST)) {
@ -2662,7 +2710,7 @@ bridge_input(struct ifnet *ifp, struct mbuf *m)
}
/* Perform the bridge forwarding function with the copy. */
bridge_forward(sc, bif, mc);
bridge_forward(sc, bif, mc, vlan);
#ifdef DEV_NETMAP
/*
@ -2801,7 +2849,7 @@ bridge_input(struct ifnet *ifp, struct mbuf *m)
#undef GRAB_OUR_PACKETS
/* Perform the bridge forwarding function. */
bridge_forward(sc, bif, m);
bridge_forward(sc, bif, m, vlan);
return (NULL);
}
@ -2839,7 +2887,7 @@ bridge_inject(struct ifnet *ifp, struct mbuf *m)
*/
static void
bridge_broadcast(struct bridge_softc *sc, struct ifnet *src_if,
struct mbuf *m, int runfilt)
struct mbuf *m, int runfilt, ether_vlanid_t vlan)
{
struct bridge_iflist *dbif, *sbif;
struct mbuf *mc;
@ -2867,6 +2915,10 @@ bridge_broadcast(struct bridge_softc *sc, struct ifnet *src_if,
if (sbif && (sbif->bif_flags & dbif->bif_flags & IFBIF_PRIVATE))
continue;
/* Do VLAN filtering. */
if (!bridge_vfilter_out(dbif, m, vlan))
continue;
if ((dbif->bif_flags & IFBIF_STP) &&
dbif->bif_stp.bp_state == BSTP_IFSTATE_DISCARDING)
continue;
@ -2950,6 +3002,67 @@ bridge_span(struct bridge_softc *sc, struct mbuf *m)
}
}
/*
* Outgoing VLAN filtering. Given a frame, its vlan, and the member interface
* we intend to send it to, decide whether the port configuration allows it to
* be sent.
*/
static bool
bridge_vfilter_out(const struct bridge_iflist *dbif, const struct mbuf *m,
ether_vlanid_t vlan)
{
struct ether_header *eh;
NET_EPOCH_ASSERT();
/* If VLAN filtering isn't enabled, pass everything. */
if ((dbif->bif_flags & IFBIF_VLANFILTER) == 0)
return (true);
/*
* Always allow untagged 802.1D STP frames, even if they would
* otherwise be dropped. This is required for STP to work on
* a filtering bridge.
*
* Tagged STP (Cisco PVST+) is a non-standard extension, so
* handle those frames via the normal filtering path.
*/
eh = mtod(m, struct ether_header *);
if (vlan == DOT1Q_VID_NULL &&
memcmp(eh->ether_dhost, bstp_etheraddr, ETHER_ADDR_LEN) == 0)
return (true);
/*
* If the frame wasn't assigned to a vlan at ingress, drop it.
* We can't forward these frames to filtering ports because we
* don't know what VLAN they're supposed to be in.
*/
if (vlan == DOT1Q_VID_NULL)
return (false);
/*
* If the frame was received with a vlan tag then drop it,
* since we only support untagged ports.
*
* If the egress port doesn't have an untagged vlan configured,
* it doesn't want untagged frames, so drop it.
*/
if (VLANTAGOF(m) != DOT1Q_VID_NULL ||
dbif->bif_untagged == DOT1Q_VID_NULL)
return (false);
/*
* Make sure the frame's vlan matches the port's untagged vlan.
*/
if (vlan != dbif->bif_untagged)
return (false);
/*
* Everything looks fine, so pass this frame.
*/
return (true);
}
/*
* bridge_rtupdate:
*

View file

@ -122,6 +122,7 @@
#define BRDGSPROTO 28 /* set protocol (ifbrparam) */
#define BRDGSTXHC 29 /* set tx hold count (ifbrparam) */
#define BRDGSIFAMAX 30 /* set max interface addrs (ifbreq) */
#define BRDGSIFUNTAGGED 31 /* set if untagged vlan */
/*
* Generic bridge control request.
@ -139,6 +140,7 @@ struct ifbreq {
uint32_t ifbr_addrcnt; /* member if addr number */
uint32_t ifbr_addrmax; /* member if addr max */
uint32_t ifbr_addrexceeded; /* member if addr violations */
ether_vlanid_t ifbr_untagged; /* member if untagged vlan */
uint8_t pad[32];
};
@ -155,10 +157,11 @@ struct ifbreq {
#define IFBIF_BSTP_ADMEDGE 0x0200 /* member stp admin edge enabled */
#define IFBIF_BSTP_ADMCOST 0x0400 /* member stp admin path cost */
#define IFBIF_PRIVATE 0x0800 /* if is a private segment */
#define IFBIF_VLANFILTER 0x1000 /* if does vlan filtering */
#define IFBIFBITS "\020\001LEARNING\002DISCOVER\003STP\004SPAN" \
"\005STICKY\014PRIVATE\006EDGE\007AUTOEDGE\010PTP" \
"\011AUTOPTP"
"\011AUTOPTP\015VLANFILTER"
#define IFBIFMASK ~(IFBIF_BSTP_EDGE|IFBIF_BSTP_AUTOEDGE|IFBIF_BSTP_PTP| \
IFBIF_BSTP_AUTOPTP|IFBIF_BSTP_ADMEDGE| \
IFBIF_BSTP_ADMCOST) /* not saved */

View file

@ -74,7 +74,7 @@
* cannot include sys/param.h and should only be updated here.
*/
#undef __FreeBSD_version
#define __FreeBSD_version 1500050
#define __FreeBSD_version 1500051
/*
* __FreeBSD_kernel__ indicates that this system uses the kernel of FreeBSD,

View file

@ -829,6 +829,129 @@ member_ifaddrs_vlan_cleanup()
vnet_cleanup
}
atf_test_case "vlan_pvid" "cleanup"
vlan_pvid_head()
{
atf_set descr 'bridge with two ports with pvid set'
atf_set require.user root
}
vlan_pvid_body()
{
vnet_init
vnet_init_bridge
epone=$(vnet_mkepair)
eptwo=$(vnet_mkepair)
vnet_mkjail one ${epone}b
vnet_mkjail two ${eptwo}b
jexec one ifconfig ${epone}b 192.0.2.1/24 up
jexec two ifconfig ${eptwo}b 192.0.2.2/24 up
bridge=$(vnet_mkbridge)
ifconfig ${bridge} up
ifconfig ${epone}a up
ifconfig ${eptwo}a up
ifconfig ${bridge} addm ${epone}a untagged ${epone}a 20
ifconfig ${bridge} addm ${eptwo}a untagged ${eptwo}a 20
# With VLAN filtering enabled, traffic should be passed.
atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
# Removed the untagged VLAN on one port; traffic should not be passed.
ifconfig ${bridge} -untagged ${epone}a
atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
}
vlan_pvid_cleanup()
{
vnet_cleanup
}
atf_test_case "vlan_pvid_filtered" "cleanup"
vlan_pvid_filtered_head()
{
atf_set descr 'bridge with two ports with different pvids'
atf_set require.user root
}
vlan_pvid_filtered_body()
{
vnet_init
vnet_init_bridge
epone=$(vnet_mkepair)
eptwo=$(vnet_mkepair)
vnet_mkjail one ${epone}b
vnet_mkjail two ${eptwo}b
jexec one ifconfig ${epone}b 192.0.2.1/24 up
jexec two ifconfig ${eptwo}b 192.0.2.2/24 up
bridge=$(vnet_mkbridge)
ifconfig ${bridge} up
ifconfig ${epone}a up
ifconfig ${eptwo}a up
ifconfig ${bridge} addm ${epone}a untagged ${epone}a 20
ifconfig ${bridge} addm ${eptwo}a untagged ${eptwo}a 30
atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
}
vlan_pvid_filtered_cleanup()
{
vnet_cleanup
}
atf_test_case "vlan_pvid_tagged" "cleanup"
vlan_pvid_tagged_head()
{
atf_set descr 'bridge pvid with tagged frames for pvid'
atf_set require.user root
}
vlan_pvid_tagged_body()
{
vnet_init
vnet_init_bridge
epone=$(vnet_mkepair)
eptwo=$(vnet_mkepair)
vnet_mkjail one ${epone}b
vnet_mkjail two ${eptwo}b
# Create two tagged interfaces on the appropriate VLANs
jexec one ifconfig ${epone}b up
jexec one ifconfig ${epone}b.20 create 192.0.2.1/24 up
jexec two ifconfig ${eptwo}b up
jexec two ifconfig ${eptwo}b.20 create 192.0.2.2/24 up
bridge=$(vnet_mkbridge)
ifconfig ${bridge} up
ifconfig ${epone}a up
ifconfig ${eptwo}a up
ifconfig ${bridge} addm ${epone}a untagged ${epone}a 20
ifconfig ${bridge} addm ${eptwo}a untagged ${eptwo}a 20
# Tagged frames should not be passed.
atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
}
vlan_pvid_tagged_cleanup()
{
vnet_cleanup
}
atf_init_test_cases()
{
atf_add_test_case "bridge_transmit_ipv4_unicast"
@ -847,4 +970,7 @@ atf_init_test_cases()
atf_add_test_case "member_ifaddrs_enabled"
atf_add_test_case "member_ifaddrs_disabled"
atf_add_test_case "member_ifaddrs_vlan"
atf_add_test_case "vlan_pvid"
atf_add_test_case "vlan_pvid_filtered"
atf_add_test_case "vlan_pvid_tagged"
}