> On Jan 31, 2026, at 3:44 PM, Martin Mayer <[email protected]>
> wrote:
>
> Let me explain my intend from a higher level and forget about the code
> details for a moment.
>
> If a packet ingresses an interface where
> ( dst MAC != interface MAC ) &&
> ( dst MAC != multicast joined groups ) &&
> ( dst MAC != broadcast )
> I'd expect
> - the packet to be directly discarded if !( IFF_PROMISC || IFF_PPROMISC )
> - the packet to be accepted but not passed to upper layers if ( IFF_PROMISC
> || IFF_PPROMISC )
>
> A special case is CARP where IFF_PROMISC is set and should be passed to upper
> layers if
> ( dst MAC == CARP instance MAC && CARP enabled )
>
> I think we have a consensus until here. This is the current (normal) behavior.
>
> This behavior changes as soon as netgraph is involved.
> If ng_ether hooks are connected, the host skips these checks and passes the
> packet to the upper layers.
>
> The practical issue to my question is:
> https://github.com/opnsense/src/issues/279
>
> I want to understand the root cause of this behavior and want to identify if
> this is intentionally or if it's a bug.
> As per my understanding, this happens in if_ethersubr.c because the hook for
> the packet to be claimed by netgraph is BEFORE the destination addresses are
> checked.
>
> [...]
> /* Allow ng_ether(4) to claim this frame. */
> if (ifp->if_l2com != NULL) {
> KASSERT(ng_ether_input_p != NULL,
> ("%s: ng_ether_input_p is NULL", __func__));
> m->m_flags &= ~M_PROMISC;
> (*ng_ether_input_p)(ifp, &m);
> if (m == NULL) {
> CURVNET_RESTORE();
> return;
> }
> eh = mtod(m, struct ether_header *);
> }
> [...]
> if (ifp->if_carp && (*carp_forus_p)(ifp, eh->ether_dhost)) {
> m->m_flags &= ~M_PROMISC;
> } else {
> /*
> * If the frame received was not for our MAC address, set the
> * M_PROMISC flag on the mbuf chain. The frame may need to
> * be seen by the rest of the Ethernet input path in case of
> * re-entry (e.g. bridge, vlan, netgraph) but should not be
> * seen by upper protocol layers.
> */
> if (!ETHER_IS_MULTICAST(eh->ether_dhost) &&
> memcmp(IF_LLADDR(ifp), eh->ether_dhost, ETHER_ADDR_LEN) !=
> 0)
> m->m_flags |= M_PROMISC;
> }
>
> ether_demux(ifp, m);
> [...]
>
> ether_demux() (which is the re-entry point from ng_ether) won't be able to
> correctly evaluate (m->m_flags & M_PROMISC).
>
> Thanks, Martin
>
>
The logic from ng_ether(4),
```
/*
* Handle an mbuf received on the "upper" hook.
*/
static int
ng_ether_rcv_upper(hook_p hook, item_p item)
{
....
m->m_pkthdr.rcvif = ifp;
/* Pass the packet to the bridge, it may come back to us */
if (ifp->if_bridge) {
BRIDGE_INPUT(ifp, m);
if (m == NULL)
return (0);
}
/* Route packet back in */
ether_demux(ifp, m);
return (0);
}
```
it lacks checking of the dest MAC of the inbound packets, and unconditionally
inject packets ( no matter
whether they're for us or not ) via ether_demux() to the net stack.
Probably we should
1. check the dest MAC address prior to passing the packets to ng_ether(4), or
2. teach ng_ether(4) to check the dest MAC address, or
3. Move the checking to ether_demux().
The 3rd one has the minimal impact. I'm CCing Gleb who is more familiar with
netgraph.
Best regards,
Zhenlei