On Sat Jul 4, 2026 at 5:21 AM EDT, Avinash Duduskar wrote: > BPF_FIB_LOOKUP_VLAN resolves a VLAN egress. The reverse is also > useful: an XDP program receiving a VLAN-tagged frame on a physical > device wants the lookup to behave as if the packet had arrived on the > corresponding VLAN subinterface, so iif-based policy routing and VRF > table selection use the right ingress. > > Add BPF_FIB_LOOKUP_VLAN_INPUT. When set, params->h_vlan_proto and > params->h_vlan_TCI are read as an input VLAN tag and the matching VLAN > device of params->ifindex is resolved with __vlan_find_dev_deep_rcu(). > The device must be up and in the same network namespace as > params->ifindex (a VLAN device can be moved to another netns while > registered on its parent; receive would deliver into that other > namespace, which a lookup here cannot represent). If params->ifindex > is itself a VLAN device, its inner (QinQ) subinterface is matched. > For a bond or team, a tag on a port matches no device and returns > NOT_FWDED; pass the master's ifindex. > The lookup then runs with the resolved device as the ingress; > params->ifindex itself is not modified on the input side. When the > resolved device is enslaved to a VRF, both the full lookup (via the > l3mdev rule) and BPF_FIB_LOOKUP_DIRECT (via l3mdev_fib_table_rcu()) > select the VRF's table from the resolved ingress. That follows from > feeding the resolved device to the flow as the ingress > (fl4.flowi4_iif = dev->ifindex), which is what makes l3mdev resolve > the VRF master from the subinterface rather than from > params->ifindex. > > The two failure classes get different treatment on purpose. A > h_vlan_proto other than 802.1Q/802.1ad is API misuse and returns > -EINVAL, since it would otherwise reach the WARN in vlan_proto_idx() > with a program-controlled value. An unmatched VID, a device that is > down, or one in another namespace is a data outcome and returns > BPF_FIB_LKUP_RET_NOT_FWDED, matching the DIRECT path when > fib_get_table() finds no table and mirroring real ingress, where the > receive path drops such frames. A VID of 0 (a priority tag) is looked > up literally and normally fails the same way; receive instead > processes such frames untagged, so callers should not set the flag for > priority tags. Proceeding on the physical device for any of these > would be fail-open for the policy-routing cases above. > > The h_vlan fields share a union with tbid, so the flag cannot be > combined with BPF_FIB_LOOKUP_TBID. It describes ingress, so it also > cannot be combined with BPF_FIB_LOOKUP_OUTPUT. Both combinations > return -EINVAL; restricting now keeps a later relaxation backward > compatible. Combining with BPF_FIB_LOOKUP_VLAN is allowed: the tag is > consumed on the ingress side and the egress tag is written on > success. > > Under !CONFIG_VLAN_8021Q the __vlan_find_dev_deep_rcu() stub returns > NULL, so every lookup with the flag returns NOT_FWDED, which is > correct since no VLAN device can exist. > > Suggested-by: Toke Høiland-Jørgensen <[email protected]> > Reviewed-by: Toke Høiland-Jørgensen <[email protected]> > Signed-off-by: Avinash Duduskar <[email protected]>
Reviewed-by: Emil Tsalapatis <[email protected]> > --- > include/uapi/linux/bpf.h | 21 ++++++++++- > net/core/filter.c | 66 +++++++++++++++++++++++++++++++--- > tools/include/uapi/linux/bpf.h | 21 ++++++++++- > 3 files changed, 101 insertions(+), 7 deletions(-) > > diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h > index e00f0392e728..d4218954c50f 100644 > --- a/include/uapi/linux/bpf.h > +++ b/include/uapi/linux/bpf.h > @@ -3555,6 +3555,22 @@ union bpf_attr { > * This flag is only valid for XDP programs; tc programs > * receive -EINVAL since they can redirect to the VLAN > * device directly. > + * **BPF_FIB_LOOKUP_VLAN_INPUT** > + * Treat *params*->h_vlan_proto and *params*->h_vlan_TCI > + * as an input VLAN tag and run the lookup as if ingress > + * had happened on the VLAN subinterface carrying that tag > + * on *params*->ifindex. The VID is the low 12 bits of > + * *params*->h_vlan_TCI; *params*->h_vlan_proto must be > + * ETH_P_8021Q or ETH_P_8021AD in network byte order, else > + * **-EINVAL**. If *params*->ifindex is itself a VLAN > + * device, its inner (QinQ) subinterface is matched; for a > + * bond or team, pass the master's ifindex. An unmatched > + * tag, a down device, or one in another namespace returns > + * **BPF_FIB_LKUP_RET_NOT_FWDED**, mirroring real ingress. > + * A VID of 0 is looked up literally, so do not set this > + * flag for priority-tagged frames. Cannot be combined with > + * **BPF_FIB_LOOKUP_TBID** or **BPF_FIB_LOOKUP_OUTPUT** > + * (returns **-EINVAL**). > * > * *ctx* is either **struct xdp_md** for XDP programs or > * **struct sk_buff** tc cls_act programs. > @@ -7351,6 +7367,7 @@ enum { > BPF_FIB_LOOKUP_SRC = (1U << 4), > BPF_FIB_LOOKUP_MARK = (1U << 5), > BPF_FIB_LOOKUP_VLAN = (1U << 6), > + BPF_FIB_LOOKUP_VLAN_INPUT = (1U << 7), > }; > > enum { > @@ -7421,7 +7438,9 @@ struct bpf_fib_lookup { > /* > * output with BPF_FIB_LOOKUP_VLAN: set from the > * resolved egress VLAN device (see the flag); zeroed > - * on other successful lookups. > + * on other successful lookups. input with > + * BPF_FIB_LOOKUP_VLAN_INPUT: the VLAN tag to scope > + * the lookup by. > */ > __be16 h_vlan_proto; > __be16 h_vlan_TCI; > diff --git a/net/core/filter.c b/net/core/filter.c > index b5a45485a54b..0ea362fa4287 100644 > --- a/net/core/filter.c > +++ b/net/core/filter.c > @@ -6229,6 +6229,25 @@ static int bpf_fib_set_fwd_params(struct net_device > *dev, > > return 0; > } > + > +static struct net_device *bpf_fib_vlan_input_dev(struct net_device *dev, > + const struct bpf_fib_lookup > *params) > +{ > + __be16 proto = params->h_vlan_proto; > + struct net_device *vlan_dev; > + u16 vid; > + > + if (proto != htons(ETH_P_8021Q) && proto != htons(ETH_P_8021AD)) > + return ERR_PTR(-EINVAL); > + > + vid = ntohs(params->h_vlan_TCI) & VLAN_VID_MASK; > + vlan_dev = __vlan_find_dev_deep_rcu(dev, proto, vid); > + if (!vlan_dev || !(vlan_dev->flags & IFF_UP) || > + !net_eq(dev_net(vlan_dev), dev_net(dev))) > + return NULL; > + > + return vlan_dev; > +} > #endif > > #if IS_ENABLED(CONFIG_INET) > @@ -6249,6 +6268,14 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct > bpf_fib_lookup *params, > if (unlikely(!dev)) > return -ENODEV; > > + if (flags & BPF_FIB_LOOKUP_VLAN_INPUT) { > + dev = bpf_fib_vlan_input_dev(dev, params); > + if (IS_ERR(dev)) > + return PTR_ERR(dev); > + if (!dev) > + return BPF_FIB_LKUP_RET_NOT_FWDED; > + } > + > /* verify forwarding is enabled on this interface */ > in_dev = __in_dev_get_rcu(dev); > if (unlikely(!in_dev || !IN_DEV_FORWARD(in_dev))) > @@ -6258,7 +6285,11 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct > bpf_fib_lookup *params, > fl4.flowi4_iif = 1; > fl4.flowi4_oif = params->ifindex; > } else { > - fl4.flowi4_iif = params->ifindex; > + /* > + * dev->ifindex, not params->ifindex: VLAN_INPUT may have > + * resolved dev to a subinterface above. > + */ > + fl4.flowi4_iif = dev->ifindex; > fl4.flowi4_oif = 0; > } > fl4.flowi4_dscp = inet_dsfield_to_dscp(params->tos); > @@ -6395,6 +6426,14 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct > bpf_fib_lookup *params, > if (unlikely(!dev)) > return -ENODEV; > > + if (flags & BPF_FIB_LOOKUP_VLAN_INPUT) { > + dev = bpf_fib_vlan_input_dev(dev, params); > + if (IS_ERR(dev)) > + return PTR_ERR(dev); > + if (!dev) > + return BPF_FIB_LKUP_RET_NOT_FWDED; > + } > + > idev = __in6_dev_get_safely(dev); > if (unlikely(!idev || !READ_ONCE(idev->cnf.forwarding))) > return BPF_FIB_LKUP_RET_FWD_DISABLED; > @@ -6403,7 +6442,12 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct > bpf_fib_lookup *params, > fl6.flowi6_iif = 1; > oif = fl6.flowi6_oif = params->ifindex; > } else { > - oif = fl6.flowi6_iif = params->ifindex; > + /* > + * dev->ifindex, not params->ifindex: VLAN_INPUT may have > + * resolved dev to a subinterface above. > + */ > + oif = dev->ifindex; > + fl6.flowi6_iif = oif; > fl6.flowi6_oif = 0; > strict = RT6_LOOKUP_F_HAS_SADDR; > } > @@ -6514,7 +6558,19 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct > bpf_fib_lookup *params, > #define BPF_FIB_LOOKUP_MASK (BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_OUTPUT | > \ > BPF_FIB_LOOKUP_SKIP_NEIGH | BPF_FIB_LOOKUP_TBID | \ > BPF_FIB_LOOKUP_SRC | BPF_FIB_LOOKUP_MARK | \ > - BPF_FIB_LOOKUP_VLAN) > + BPF_FIB_LOOKUP_VLAN | BPF_FIB_LOOKUP_VLAN_INPUT) > + > +static bool bpf_fib_lookup_flags_ok(u32 flags) > +{ > + if (flags & ~BPF_FIB_LOOKUP_MASK) > + return false; > + > + if ((flags & BPF_FIB_LOOKUP_VLAN_INPUT) && > + (flags & (BPF_FIB_LOOKUP_TBID | BPF_FIB_LOOKUP_OUTPUT))) > + return false; > + > + return true; > +} > > BPF_CALL_4(bpf_xdp_fib_lookup, struct xdp_buff *, ctx, > struct bpf_fib_lookup *, params, int, plen, u32, flags) > @@ -6522,7 +6578,7 @@ BPF_CALL_4(bpf_xdp_fib_lookup, struct xdp_buff *, ctx, > if (plen < sizeof(*params)) > return -EINVAL; > > - if (flags & ~BPF_FIB_LOOKUP_MASK) > + if (!bpf_fib_lookup_flags_ok(flags)) > return -EINVAL; > > switch (params->family) { > @@ -6560,7 +6616,7 @@ BPF_CALL_4(bpf_skb_fib_lookup, struct sk_buff *, skb, > if (plen < sizeof(*params)) > return -EINVAL; > > - if (flags & ~BPF_FIB_LOOKUP_MASK) > + if (!bpf_fib_lookup_flags_ok(flags)) > return -EINVAL; > > if (flags & BPF_FIB_LOOKUP_VLAN) > diff --git a/tools/include/uapi/linux/bpf.h b/tools/include/uapi/linux/bpf.h > index e00f0392e728..d4218954c50f 100644 > --- a/tools/include/uapi/linux/bpf.h > +++ b/tools/include/uapi/linux/bpf.h > @@ -3555,6 +3555,22 @@ union bpf_attr { > * This flag is only valid for XDP programs; tc programs > * receive -EINVAL since they can redirect to the VLAN > * device directly. > + * **BPF_FIB_LOOKUP_VLAN_INPUT** > + * Treat *params*->h_vlan_proto and *params*->h_vlan_TCI > + * as an input VLAN tag and run the lookup as if ingress > + * had happened on the VLAN subinterface carrying that tag > + * on *params*->ifindex. The VID is the low 12 bits of > + * *params*->h_vlan_TCI; *params*->h_vlan_proto must be > + * ETH_P_8021Q or ETH_P_8021AD in network byte order, else > + * **-EINVAL**. If *params*->ifindex is itself a VLAN > + * device, its inner (QinQ) subinterface is matched; for a > + * bond or team, pass the master's ifindex. An unmatched > + * tag, a down device, or one in another namespace returns > + * **BPF_FIB_LKUP_RET_NOT_FWDED**, mirroring real ingress. > + * A VID of 0 is looked up literally, so do not set this > + * flag for priority-tagged frames. Cannot be combined with > + * **BPF_FIB_LOOKUP_TBID** or **BPF_FIB_LOOKUP_OUTPUT** > + * (returns **-EINVAL**). > * > * *ctx* is either **struct xdp_md** for XDP programs or > * **struct sk_buff** tc cls_act programs. > @@ -7351,6 +7367,7 @@ enum { > BPF_FIB_LOOKUP_SRC = (1U << 4), > BPF_FIB_LOOKUP_MARK = (1U << 5), > BPF_FIB_LOOKUP_VLAN = (1U << 6), > + BPF_FIB_LOOKUP_VLAN_INPUT = (1U << 7), > }; > > enum { > @@ -7421,7 +7438,9 @@ struct bpf_fib_lookup { > /* > * output with BPF_FIB_LOOKUP_VLAN: set from the > * resolved egress VLAN device (see the flag); zeroed > - * on other successful lookups. > + * on other successful lookups. input with > + * BPF_FIB_LOOKUP_VLAN_INPUT: the VLAN tag to scope > + * the lookup by. > */ > __be16 h_vlan_proto; > __be16 h_vlan_TCI;

