https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=298932

            Bug ID: 298932
           Summary: pf: `(ifname:0)` is empty on a second point-to-point
                    interface with the same peer address, NAT fails
                    silently with map-failed
           Product: Base System
           Version: 15.1-RELEASE
          Hardware: Any
                OS: Any
            Status: New
          Severity: Affects Some People
          Priority: ---
         Component: kern
          Assignee: [email protected]
          Reporter: [email protected]

Environment
===========

- FreeBSD 15.1-RELEASE-p3, as shipped in OPNsense 26.7.4_1
(stable/26.7-n283949-083dc7025377). The relevant code is identical in
releng/15.1 and in OPNsense stable/26.7.
- Two PPPoE sessions (mpd5, netgraph ng_iface, renamed to pppoe0 and pppoe1) to
the  same ISP. The ISP's BNG presents the same IPCP peer address on both
sessions.
- IP addresses replaced with ones reserved for documentation

pppoe0: flags=10088d1<UP,POINTOPOINT,RUNNING,NOARP,SIMPLEX,MULTICAST,LOWER_UP>
mtu 1492
        inet 203.0.113.10 --> 198.51.100.1 netmask 0xffffffff
pppoe1: flags=10088d1<UP,POINTOPOINT,RUNNING,NOARP,SIMPLEX,MULTICAST,LOWER_UP>
mtu 1492
        inet 203.0.113.20 --> 198.51.100.1 netmask 0xffffffff

[For transparence: analysis and writeup done by Claude Code, Model Opus 5.5 (1
Mio)]

Description
===========

Outbound NAT on whichever of the two interfaces came up second does not work.
Forwarded IPv4 packets that should leave through it are dropped without a log
entry. IPv6 over the same interface works, and so does traffic the firewall
originates itself.

Typical ruleset (generated by OPNsense):

nat on pppoe1 inet from (SEGMENT_0:network) to any -> (pppoe1:0) port
1024:65535
pass in quick on SEGMENT_0 route-to (pppoe1 198.51.100.1) inet from 10.x.y.52
to any keep state

Observed for a test client, six TCP connection attempts through pppoe1:

- the inbound state is created (2:0 pkts), no outbound state appears;
- pfctl -si: map-failed goes from 144601 to 144613 (+12, one per SYN including
retransmits);
- netstat -s -p ip: packets not forwardable +14;
- tcpdump -i pppoe1 shows nothing; no ICMP error is sent back to the client;
- nothing in pflog.

The same happens without route-to, with a plain host route route add -host
<dst> -interface pppoe1.

Analysis
========

1. ia_getrtprefix() (sys/netinet/in.c, line 988) uses the destination address
as a /32 prefix for point-to-point interfaces. in_addprefix() (line 1092) calls
in_hasrtprefix() (line 1056), finds that the address on pppoe0 already owns the
route to 198.51.100.1 in the same FIB, and returns without installing a route
or setting IFA_ROUTE on the pppoe1 address. This part looks intended.
net.route.multipath=1 does not change it; we tried.

2. pfi_instance_add() (sys/netpfil/pf/pf_if.c, line 731) then skips that
address for (pppoe1:0):

   /*
    * XXX: For point-to-point interfaces, (ifname:0) and IPv4,
    *      jump over addresses without a proper route to work
    *      around a problem with ppp not fully removing the
    *      address used during IPCP.
    */
   if ((ifp->if_flags & IFF_POINTOPOINT) &&
       !(ia->ifa_flags & IFA_ROUTE) &&
       (flags & PFI_AFLAG_NOALIAS) && (af == AF_INET))
           continue;

   pppoe1 has exactly one IPv4 address, and it is the correct one. The dynamic
address list for (pppoe1:0) ends up empty.

3. pf_map_addr() (sys/netpfil/pf/pf_lb.c, line 597) finds pfid_acnt4 < 1 for
the NAT pool and returns PFRES_MAPFAILED. The packet is dropped in
pf_test(PF_OUT), ip_output() returns EACCES, and ip_forward() counts it as not
forwardable without sending ICMP.

Whichever interface was up first keeps the route and works. When it goes away,
in_scrubprefix() moves the route and IFA_ROUTE to the other address, so
failover works. When the first interface comes back, it becomes the one without
IFA_ROUTE, and its NAT breaks. For multi-WAN with gateway groups this means
failback fails reliably.


Expected behaviour
==================

(ifname:0) should resolve to the interface's IPv4 address even if the kernel
did not install the peer route for it because another interface already has
one.

Possible fix
============

The workaround's intent is to skip a stale IPCP address when a proper one
exists on the same interface. Narrowing it to that case would keep the ppp
workaround and fix this setup: skip an address without IFA_ROUTE only if the
same interface has another IPv4address that does have IFA_ROUTE. Sketch, not
compiled:

static bool
pfi_ptp_has_routed_addr4(struct ifnet *ifp)
{
        struct ifaddr *ia;

        CK_STAILQ_FOREACH(ia, &ifp->if_addrhead, ifa_link)
                if (ia->ifa_addr != NULL &&
                    ia->ifa_addr->sa_family == AF_INET &&
                    (ia->ifa_flags & IFA_ROUTE))
                        return (true);
        return (false);
}
...
        if ((ifp->if_flags & IFF_POINTOPOINT) &&
            !(ia->ifa_flags & IFA_ROUTE) &&
            (flags & PFI_AFLAG_NOALIAS) && (af == AF_INET) &&
            pfi_ptp_has_routed_addr4(ifp))
                continue;

A caveat: the dynamic address is recomputed on address events. When IFA_ROUTE
moves between interfaces in in_scrubprefix(), (ifname:0) would have to be
refreshed as well.

We have not checked whether that already happens.

How to reproduce without an ISP (untested)
==========================================

ifconfig gif0 create inet 192.0.2.1 198.51.100.1 up
ifconfig gif1 create inet 192.0.2.2 198.51.100.1 up
netstat -rn -f inet | grep 198.51.100.1     # one route, via gif0
# pf.conf:
#   nat on gif1 inet from 10.0.0.0/24 to any -> (gif1:0)
#   pass out on gif1 from (gif1:0)
pfctl -vvsr | grep gif1                      # (gif1:0) resolves to no address

-- 
You are receiving this mail because:
You are the assignee for the bug.

Reply via email to