/* * PF-on-Linux transmit + return-packet helpers (ISC 2026). * if_output_tso/ml + if_enqueue mirror OpenBSD calls; route-to uses them * with a gateway dst (see pf_route.c, T14). Return errors use Linux * ipstack helpers (exported: icmp_send; P1 exports ip_route_output_key, * ip6_route_output, tcp_v4_send_reset if needed). */ #include #include static struct dst_entry * pfk_route4(__be32 daddr, __be32 saddr, int oif) { struct flowi4 fl4 = { }; struct rtable *rt; fl4.daddr = daddr; fl4.saddr = saddr; if (oif) fl4.flowi4_oif = oif; rt = ip_route_output_key(&init_net, &fl4); if (IS_ERR(rt)) return NULL; return &rt->dst; } /* * Forced nexthop (route-to/dup-to/reply-to). Linux resolves the neighbour * from the *packet's* destination unless the route says it goes through a * gateway (ip_neigh_for_gw() only honours rt_gw4 when rt_gw_family is set), * which would ARP the final host on the gateway's link. Look the gateway up * as its own host route and mark it as the nexthop - the same pattern OVS * uses after its own FIB lookups. */ static struct dst_entry * pfk_route4_nh(__be32 gw, int oif) { struct rtable *rt = (struct rtable *)pfk_route4(gw, 0, oif); if (rt == NULL) return NULL; rt->rt_gw_family = AF_INET; rt->rt_gw4 = gw; return &rt->dst; } #if IS_ENABLED(CONFIG_IPV6) static struct dst_entry * pfk_route6(const struct in6_addr *daddr, const struct in6_addr *saddr, int oif) { struct flowi6 fl6 = { }; struct dst_entry *d; fl6.daddr = *daddr; if (saddr) fl6.saddr = *saddr; if (oif) fl6.flowi6_oif = oif; d = ip6_route_output(&init_net, NULL, &fl6); if (!d || d->error) { if (d) dst_release(d); return NULL; } return d; } #endif /* xmit: BSD semantics โ€” caller hands fully-formed IP packet + dst sock. * rt arg is OpenBSD's cached route (opaque to us). Returns 0/errno. */ int if_output_tso(struct pfk_ifnet *ifp, struct mbuf **mp, void *dst, void *rt, int mtu) { struct mbuf *m = *mp; struct sk_buff *skb = m_to_skb(m); struct dst_entry *d = NULL; struct sockaddr *sa = (struct sockaddr *)dst; int ret = 0; if (!pskb_may_pull(skb, sizeof(struct pf_ip))) { ret = EINVAL; goto drop; } if (skb_ensure_writable(skb, 0)) { ret = ENOBUFS; goto drop; } switch (sa->sa_family) { case AF_INET: { __be32 gw = 0; /* if route was forced (route-to), dst holds gw sockaddr * stored in sin_addr by pf_route; normal case dst==final */ gw = sintosa(dst)->sin_addr.s_addr; /* PFLINUX: look the route up by destination only. Passing the * packet's source (a transit address, not local to us) makes * ip_route_output_key fail with ENETUNREACH; ip_output keeps * the header source untouched. */ d = pfk_route4_nh(gw, ifp->if_index); if (!d) { ret = EHOSTUNREACH; goto drop; } skb_dst_set(skb, d); break; } #if IS_ENABLED(CONFIG_IPV6) case AF_INET6: { const struct in6_addr *gw = &sin6tosa(dst)->sin6_addr; if (!d) /* PFLINUX: destination-only, as for IPv4 */ d = pfk_route6(gw, NULL, ifp->if_index); if (!d) { ret = EHOSTUNREACH; goto drop; } skb_dst_set(skb, d); skb->protocol = htons(ETH_P_IPV6); break; } #endif default: ret = EAFNOSUPPORT; goto drop; } skb->dev = skb_dst(skb)->dev; skb_reset_network_header(skb); if (skb->protocol == 0) skb->protocol = htons(ETH_P_IP); /* push through network layer exactly like OpenBSD's if_output: * dst->output = ip_output/ip6_output (headers already built) */ if (!netif_running(skb->dev)) { ret = ENETDOWN; goto drop_dst_nofree; } /* * The stack's output path always consumes exactly one skb reference. * Take our own so the caller's references (the hook queue ref and the * mbuf wrapper ref) stay balanced - without this the work queue's * cleanup double-frees the skb and corrupts the slab. */ /* * pf_route() has already evaluated the OUT ruleset (it calls * pf_test(PF_OUT) itself, exactly like OpenBSD's ip_forward). Mark the * packet so our POST_ROUTING hook does not run the out ruleset a second * time - that re-match hits the state, re-enters pf_route and recurses * (OpenBSD stops this with PF_TAG_PROCESSED carried in the mbuf). */ skb->mark |= PFK_DONE_OUT; /* * The port stashes its mbuf pointer in skb->cb; the IP stack uses the * same bytes as its own control block (IPCB). Hand the packet to the * stack with a clean cb, otherwise ip_output() reads our pointer as * frag_max_size/flags and the packet is silently lost. */ memset(skb->cb, 0, sizeof(skb->cb)); skb_get(skb); ret = skb_dst(skb)->output(&init_net, NULL, skb); /* output consumed skb (and wrapper); avoid double free */ if (ret == 0) { m->skb = NULL; /* ownership moved */ m_freem(m); *mp = NULL; } return ret; drop_dst_nofree: ret = ENETDOWN; drop: if (m) { m_freem(m); *mp = NULL; } if (ret == ENOBUFS || ret == ENETDOWN || ret == EHOSTUNREACH) pfk_if_congestion(); return ret; } int if_output_ml(struct pfk_ifnet *ifp, struct mbuf_list *ml, void *dst, void *rt) { struct mbuf *m; int err = 0, n; while ((m = ml->ml_first) != NULL) { ml->ml_first = m->m_next; m->m_next = NULL; n = if_output_tso(ifp, &m, dst, rt, ifp->if_mtu); if (n) err = n; } ml->ml_len = 0; return err; } int if_enqueue(struct pfk_ifnet *ifp, struct mbuf *m) { /* PFLINUX M1-M4: no qdisc attached โ†’ direct xmit via same path; * M5 replaces this with the pfq enqueue (spec ยง4.3). */ struct sockaddr sa = { .sa_family = AF_INET }; struct mbuf *mp = m; if (m_to_skb(m)->protocol == htons(ETH_P_IPV6)) sa.sa_family = AF_INET6; if_output_tso(ifp, &mp, &sa, NULL, ifp->if_mtu); if (mp == NULL) return 0; m_freem(mp); return ENOBUFS; } /* default-route egress device helper for pfk_egress_refresh() */ struct net_device * ip_rt_get_default_dev(struct net *net) { struct dst_entry *d = pfk_route4(htonl(0x08080808), 0, 0); struct net_device *dev = NULL; if (d) { dev = d->dev; if (dev) dev_hold(dev); dst_release(d); } return dev; } /* ---------------- return errors ---------------- */ u_int16_t pfk_in_cksum(const void *data, int len) { return csum_fold(csum_partial(data, len, 0)); } void pfk_send_icmp_error(int family, struct sk_buff *skb_in, int type, int code, __be32 info) { if (family == AF_INET) { icmp_send(skb_in, type, code, info); return; } #if IS_ENABLED(CONFIG_IPV6) pr_warn_once("pfk: ICMPv6 error path pending P1 export (type %d)\n", type); #endif } /* NOTE: TCP RST generation is done by PF itself (pf.c builds the segment * and calls if_output_tso), so no kernel tcp_v4_send_reset export is * needed โ€” matches OpenBSD's pf_send_rst() design. */ /* * pfk_if_output(ifp, m, dst, rt): BSD if_output() equivalent used by the * ported PF code (route-to / refragment / divert paths). */ int pfk_if_output(struct pfk_ifnet *ifp, struct mbuf *m, void *dst, void *rt) { struct mbuf *mp = m; int err; err = if_output_tso(ifp, &mp, dst, rt, ifp->if_mtu); if (mp) m_freem(mp); return err; }