diff --git a/sbin/ifconfig/ifbridge.c b/sbin/ifconfig/ifbridge.c --- a/sbin/ifconfig/ifbridge.c +++ b/sbin/ifconfig/ifbridge.c @@ -211,6 +211,8 @@ else printf(" ", state); } + if (member->ifbr_untagged != 0) + printf(" untagged %u", (unsigned)member->ifbr_untagged); printf("\n"); } @@ -576,6 +578,45 @@ 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 @@ 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 @@ 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), diff --git a/sbin/ifconfig/ifconfig.8 b/sbin/ifconfig/ifconfig.8 --- a/sbin/ifconfig/ifconfig.8 +++ b/sbin/ifconfig/ifconfig.8 @@ -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 @@ 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 diff --git a/share/man/man4/bridge.4 b/share/man/man4/bridge.4 --- a/share/man/man4/bridge.4 +++ b/share/man/man4/bridge.4 @@ -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 @@ .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 diff --git a/sys/net/if_bridge.c b/sys/net/if_bridge.c --- a/sys/net/if_bridge.c +++ b/sys/net/if_bridge.c @@ -254,6 +254,7 @@ 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 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_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_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 @@ { 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 @@ 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 @@ 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 @@ 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 @@ */ 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 @@ 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 @@ } if (dst_if == NULL) { - bridge_broadcast(sc, src_if, m, 1); + bridge_broadcast(sc, src_if, m, 1, vlan); return; } @@ -2555,6 +2578,10 @@ 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 @@ 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 @@ } /* 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 @@ #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 @@ */ 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 @@ 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 @@ } } +/* + * 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: * diff --git a/sys/net/if_bridgevar.h b/sys/net/if_bridgevar.h --- a/sys/net/if_bridgevar.h +++ b/sys/net/if_bridgevar.h @@ -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 @@ 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 @@ #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 */ diff --git a/sys/sys/param.h b/sys/sys/param.h --- a/sys/sys/param.h +++ b/sys/sys/param.h @@ -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, diff --git a/tests/sys/net/if_bridge_test.sh b/tests/sys/net/if_bridge_test.sh --- a/tests/sys/net/if_bridge_test.sh +++ b/tests/sys/net/if_bridge_test.sh @@ -829,6 +829,129 @@ 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_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" }