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;


Reply via email to