541 lines
13 KiB
C
541 lines
13 KiB
C
/*
|
|
* PF-on-Linux BSD IPv4/IPv6 layer used by pf.c (ISC 2026).
|
|
*
|
|
* OpenBSD's netinet entry points that PF calls directly for the AFRT
|
|
* (64<->4 translation), route-to, RST/error-return and state-ICMP paths.
|
|
* The staging seds rename ip_output/ip_forward/ip6_output to pfk_* (they
|
|
* clash with Linux declarations); ip_send/ip6_send/icmp_error/ip_fragment
|
|
* have no Linux declaration and keep their BSD names.
|
|
*
|
|
* All of these CONSUME the mbuf (OpenBSD semantics), matching the callers
|
|
* in pf.c which set pd->m = NULL right after.
|
|
*/
|
|
#include <pfk.h>
|
|
|
|
static struct rtable *
|
|
pfk_ip4_route(__be32 daddr, __be32 saddr)
|
|
{
|
|
struct flowi4 fl4 = { };
|
|
struct rtable *rt;
|
|
|
|
fl4.daddr = daddr;
|
|
fl4.saddr = saddr;
|
|
rt = ip_route_output_key(&init_net, &fl4);
|
|
if (IS_ERR(rt))
|
|
return NULL;
|
|
return rt;
|
|
}
|
|
|
|
/* one's complement sum over an mbuf range (shim: linear sk_buff) */
|
|
u_int16_t
|
|
in_cksum(struct mbuf *m, int len)
|
|
{
|
|
struct sk_buff *skb = m_to_skb(m);
|
|
|
|
if (!skb || !pskb_may_pull(skb, len))
|
|
return 0xffff;
|
|
return pfk_in_cksum(skb->data, len);
|
|
}
|
|
|
|
/*
|
|
* in4_cksum(m, nxt, off, len): transport checksum including the IPv4
|
|
* pseudo-header. off/len are relative to the packet start (the IP hdr).
|
|
* Returns the one's complement sum; a valid packet computes to 0, and a
|
|
* value to be STORED is (sum == 0 ? 0xffff : ~sum) style — callers do
|
|
* their own fixups; we return the raw folded sum like OpenBSD.
|
|
*/
|
|
u_int16_t
|
|
in4_cksum(struct mbuf *m, __u8 nxt, u_int32_t off, u_int32_t len)
|
|
{
|
|
struct sk_buff *skb = m_to_skb(m);
|
|
struct pf_ip *ip;
|
|
struct {
|
|
__be32 src, dst;
|
|
__u8 zero, proto;
|
|
__be16 len;
|
|
} ph;
|
|
__wsum base;
|
|
|
|
if (!skb || !pskb_may_pull(skb, off + len) ||
|
|
!pskb_may_pull(skb, sizeof(*ip)))
|
|
return 0xffff;
|
|
ip = (struct pf_ip *)skb->data;
|
|
|
|
ph.src = ip->ip_src.s_addr;
|
|
ph.dst = ip->ip_dst.s_addr;
|
|
ph.zero = 0;
|
|
ph.proto = nxt;
|
|
ph.len = htons(len);
|
|
base = csum_partial(&ph, sizeof(ph), 0);
|
|
return csum_fold(csum_partial(skb->data + off, len, base));
|
|
}
|
|
|
|
u_int16_t
|
|
in6_cksum(struct mbuf *m, __u8 nxt, u_int32_t off, u_int32_t len)
|
|
{
|
|
struct sk_buff *skb = m_to_skb(m);
|
|
struct pf_ip6_hdr *ip6;
|
|
__wsum base;
|
|
|
|
if (!skb || !pskb_may_pull(skb, sizeof(*ip6)) ||
|
|
!pskb_may_pull(skb, off + len))
|
|
return 0xffff;
|
|
ip6 = (struct pf_ip6_hdr *)skb->data;
|
|
|
|
base = csum_partial(ip6->ip6_src.s6_addr, sizeof(ip6->ip6_src), 0);
|
|
base = csum_add(base, csum_partial(ip6->ip6_dst.s6_addr,
|
|
sizeof(ip6->ip6_dst), 0));
|
|
base = csum_add(base, (__force __u32)htonl(len));
|
|
base = csum_add(base, (__force __u32)htonl(nxt));
|
|
return csum_fold(csum_partial(skb->data + off, len, base));
|
|
}
|
|
|
|
/* recompute the IPv4 header checksum in place */
|
|
void
|
|
in_hdr_cksum_out(struct mbuf *m, void *ro)
|
|
{
|
|
struct sk_buff *skb = m_to_skb(m);
|
|
struct pf_ip *ip;
|
|
int hlen;
|
|
|
|
(void)ro;
|
|
if (!skb || !pskb_may_pull(skb, sizeof(*ip)))
|
|
return;
|
|
ip = (struct pf_ip *)skb->data;
|
|
hlen = ip->ip_hl << 2;
|
|
if (hlen < (int)sizeof(*ip) || !pskb_may_pull(skb, hlen))
|
|
return;
|
|
ip->ip_sum = 0;
|
|
ip->ip_sum = csum_fold(csum_partial(skb->data, hlen, 0));
|
|
pfk_mbuf_refresh(m);
|
|
}
|
|
|
|
/* compute a pending transport checksum requested via csum_flags */
|
|
static void
|
|
pfk_proto_cksum_out(struct mbuf *m)
|
|
{
|
|
struct sk_buff *skb = m_to_skb(m);
|
|
struct pf_ip *ip;
|
|
int hlen;
|
|
|
|
if (!skb || !(m->m_pkthdr.csum_flags &
|
|
(M_TCP_CSUM_OUT | M_UDP_CSUM_OUT)))
|
|
return;
|
|
if (!pskb_may_pull(skb, sizeof(*ip)))
|
|
return;
|
|
ip = (struct pf_ip *)skb->data;
|
|
hlen = ip->ip_hl << 2;
|
|
if (ip->ip_p == IPPROTO_TCP) {
|
|
__be16 *sum;
|
|
u_int16_t s;
|
|
|
|
if (!pskb_may_pull(skb, hlen + 20))
|
|
return;
|
|
sum = (__be16 *)(skb->data + hlen + 16); /* th_sum */
|
|
*sum = 0;
|
|
s = in4_cksum(m, IPPROTO_TCP, hlen, ntohs(ip->ip_len) - hlen);
|
|
*sum = s == 0 ? (__force __be16)0xffff : (__force __be16)~s;
|
|
} else if (ip->ip_p == IPPROTO_UDP) {
|
|
__be16 *sum;
|
|
u_int16_t s;
|
|
|
|
if (!pskb_may_pull(skb, hlen + 8))
|
|
return;
|
|
sum = (__be16 *)(skb->data + hlen + 6); /* uh_sum */
|
|
*sum = 0;
|
|
s = in4_cksum(m, IPPROTO_UDP, hlen, ntohs(ip->ip_len) - hlen);
|
|
if (s == 0)
|
|
s = 0xffff; /* RFC 768: zero means "unused" */
|
|
*sum = (__force __be16)s;
|
|
}
|
|
pfk_mbuf_refresh(m);
|
|
}
|
|
|
|
/*
|
|
* pfk_ip_output(m, opt, ro, flags, imcb, so, flowid) — BSD ip_output().
|
|
* PFLINUX: option processing (opt/record-route), IPsec metadata (imcb) and
|
|
* socket accounting (so) are not modelled; flags IP_FORWARDING /
|
|
* IP_ALLOWBROADCAST / IP_REDIRECT / *_IPSEC are accepted. The packet is
|
|
* routed by its own destination header and injected into the stack output
|
|
* path (exactly what OpenBSD's ip_output ends up doing at if_output).
|
|
* Consumes m. Returns 0 or errno.
|
|
*/
|
|
int
|
|
pfk_ip_output(struct mbuf *m, void *opt, void *ro, u_int flags, void *imcb,
|
|
void *so, u_int flowid)
|
|
{
|
|
struct sk_buff *skb = m ? m_to_skb(m) : NULL;
|
|
struct pf_ip *ip;
|
|
struct rtable *rt;
|
|
|
|
(void)opt; (void)ro; (void)flags; (void)imcb; (void)so; (void)flowid;
|
|
if (!skb)
|
|
return EINVAL;
|
|
if (!pskb_may_pull(skb, sizeof(*ip))) {
|
|
m_freem(m);
|
|
return EMSGSIZE;
|
|
}
|
|
ip = (struct pf_ip *)skb->data;
|
|
|
|
pfk_proto_cksum_out(m);
|
|
in_hdr_cksum_out(m, NULL);
|
|
skb_reset_network_header(skb);
|
|
skb->protocol = htons(ETH_P_IP);
|
|
|
|
rt = pfk_ip4_route(ip->ip_dst.s_addr, ip->ip_src.s_addr);
|
|
if (!rt) {
|
|
/* BSD: no route while forwarding → ICMP host unreachable */
|
|
if (flags & IP_FORWARDING) {
|
|
/* re-derive a mutable copy for quoting */
|
|
struct sk_buff *q;
|
|
|
|
q = skb_copy(skb, GFP_ATOMIC);
|
|
if (q) {
|
|
icmp_error(m_dtom(q), ICMP_UNREACH,
|
|
ICMP_UNREACH_HOST, 0, 0);
|
|
}
|
|
} else {
|
|
struct sk_buff *q = skb_copy(skb, GFP_ATOMIC);
|
|
|
|
if (q)
|
|
icmp_error(m_dtom(q), ICMP_UNREACH,
|
|
ICMP_UNREACH_HOST, 0, 0);
|
|
}
|
|
m_freem(m);
|
|
pfk_if_congestion();
|
|
return EHOSTUNREACH;
|
|
}
|
|
skb_dst_set(skb, &rt->dst);
|
|
skb->dev = rt->dst.dev;
|
|
if (!netif_running(skb->dev)) {
|
|
m->skb = NULL;
|
|
m_freem(m);
|
|
kfree_skb(skb);
|
|
return ENETDOWN;
|
|
}
|
|
m->skb = NULL; /* output consumes the skb */
|
|
m_freem(m);
|
|
skb_dst(skb)->output(&init_net, NULL, skb);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* ip_send(m): route + transmit a complete IPv4 packet to its own
|
|
* destination (OpenBSD: used by PF to send RST/error packets).
|
|
* Consumes m.
|
|
*/
|
|
void
|
|
ip_send(struct mbuf *m)
|
|
{
|
|
if (!m)
|
|
return;
|
|
(void)pfk_ip_output(m, NULL, NULL, 0, NULL, NULL, 0);
|
|
}
|
|
|
|
/*
|
|
* pfk_ip_forward(m, ifp, tag, flags) — BSD ip_forward(): ttl-- , recheck,
|
|
* forward toward the packet's destination.
|
|
* PFLINUX: ICMP redirect generation (IP_REDIRECT flag) is NOT implemented;
|
|
* the port lets Linux routing emit redirects. IP_FORWARDING_IPSEC has no
|
|
* meaning in the port (no kernel IPsec coupling) and is ignored.
|
|
* Consumes m.
|
|
*/
|
|
void
|
|
pfk_ip_forward(struct mbuf *m, struct pfk_ifnet *ifp, void *tag, u_int flags)
|
|
{
|
|
struct sk_buff *skb = m ? m_to_skb(m) : NULL;
|
|
struct pf_ip *ip;
|
|
|
|
(void)ifp; (void)tag;
|
|
if (!skb)
|
|
return;
|
|
if (!pskb_may_pull(skb, sizeof(*ip))) {
|
|
m_freem(m);
|
|
return;
|
|
}
|
|
ip = (struct pf_ip *)skb->data;
|
|
if (skb_ensure_writable(skb, sizeof(*ip))) {
|
|
m_freem(m);
|
|
return;
|
|
}
|
|
ip = (struct pf_ip *)skb->data;
|
|
|
|
if (ip->ip_ttl <= IPTTLDEC) {
|
|
struct sk_buff *q = skb_copy(skb, GFP_ATOMIC);
|
|
|
|
if (q)
|
|
icmp_error(m_dtom(q), ICMP_TIMXCEED,
|
|
ICMP_TIMXCEED_INTRANS, 0, 0);
|
|
m_freem(m);
|
|
return;
|
|
}
|
|
ip->ip_ttl -= IPTTLDEC;
|
|
in_hdr_cksum_out(m, NULL);
|
|
|
|
(void)pfk_ip_output(m, NULL, NULL, flags | IP_FORWARDING, NULL, NULL,
|
|
0);
|
|
}
|
|
|
|
/*
|
|
* ip_fragment(m, ml, ifp, mtu): split a too-large IPv4 packet into
|
|
* MTU-sized fragments appended to ml; consumes m (OpenBSD semantics:
|
|
* always consumed). Returns 0 or errno.
|
|
*/
|
|
int
|
|
ip_fragment(struct mbuf *m, struct mbuf_list *ml, struct pfk_ifnet *ifp,
|
|
int mtu)
|
|
{
|
|
struct pf_ip *ip;
|
|
int hlen, len, dlen, flen, off, error = 0;
|
|
__be16 ipid;
|
|
int maxfacilen;
|
|
|
|
if (!m || !ml)
|
|
return EINVAL;
|
|
ip = mtod(m, struct pf_ip *);
|
|
if (m->m_len < (int)sizeof(*ip)) {
|
|
m_freem(m);
|
|
return EMSGSIZE;
|
|
}
|
|
hlen = ip->ip_hl << 2;
|
|
len = ntohs(ip->ip_len);
|
|
if (len <= mtu) {
|
|
/* single piece */
|
|
if (ml->ml_last)
|
|
ml->ml_last->m_next = m;
|
|
else
|
|
ml->ml_first = m;
|
|
ml->ml_last = m;
|
|
ml->ml_len++;
|
|
return 0;
|
|
}
|
|
dlen = len - hlen;
|
|
flen = (mtu - hlen) & ~7;
|
|
if (flen < 8) {
|
|
m_freem(m);
|
|
return EMSGSIZE;
|
|
}
|
|
ipid = ip->ip_id;
|
|
maxfacilen = dlen;
|
|
|
|
off = 0;
|
|
while (off < maxfacilen) {
|
|
struct mbuf *fm;
|
|
struct pf_ip *fi;
|
|
int thislen = maxfacilen - off;
|
|
int totlen;
|
|
|
|
if (thislen > flen)
|
|
thislen = flen;
|
|
fm = m_gethdr(M_NOWAIT, MT_HEADER);
|
|
if (!fm) {
|
|
error = ENOBUFS;
|
|
break;
|
|
}
|
|
totlen = hlen + thislen;
|
|
if (skb_put(fm->skb, totlen) == NULL) {
|
|
m_freem(fm);
|
|
error = ENOBUFS;
|
|
break;
|
|
}
|
|
memcpy(fm->m_data, m->m_data, hlen);
|
|
memcpy(fm->m_data + hlen, m->m_data + hlen + off, thislen);
|
|
fi = mtod(fm, struct pf_ip *);
|
|
fi->ip_len = htons(totlen);
|
|
fi->ip_id = ipid;
|
|
fi->ip_off = htons(off >> 3);
|
|
if (off + thislen < maxfacilen)
|
|
fi->ip_off |= htons(IP_MF);
|
|
fi->ip_sum = 0;
|
|
pfk_mbuf_refresh(fm);
|
|
in_hdr_cksum_out(fm, NULL);
|
|
|
|
if (ml->ml_last)
|
|
ml->ml_last->m_next = fm;
|
|
else
|
|
ml->ml_first = fm;
|
|
ml->ml_last = fm;
|
|
ml->ml_len++;
|
|
off += thislen;
|
|
}
|
|
m_freem(m);
|
|
return error;
|
|
}
|
|
|
|
/*
|
|
* icmp_error(n, type, code, dest, mtu): build and send an ICMP error
|
|
* quoting the offending packet n; consumes n (OpenBSD semantics).
|
|
*/
|
|
void
|
|
icmp_error(struct mbuf *n, u_int type, u_int code, u_int dest, u_int mtu)
|
|
{
|
|
struct sk_buff *skb = n ? m_to_skb(n) : NULL;
|
|
struct pf_ip *oip;
|
|
struct mbuf *m;
|
|
struct pf_ip *ip;
|
|
struct pf_icmp *ic;
|
|
int hlen, qlen, len;
|
|
__be32 rsaddr;
|
|
|
|
if (!skb || !pskb_may_pull(skb, sizeof(*oip))) {
|
|
if (n)
|
|
m_freem(n);
|
|
return;
|
|
}
|
|
oip = (struct pf_ip *)skb->data;
|
|
hlen = oip->ip_hl << 2;
|
|
|
|
/* quote: ip header + at least 8 bytes, fit into RFC 792 minimum */
|
|
qlen = min_t(int, ntohs(oip->ip_len), 576 - 20 - 8);
|
|
if (qlen < hlen + 8)
|
|
qlen = min(hlen + 8, (int)skb->len);
|
|
|
|
len = 20 + 8 + qlen;
|
|
m = m_gethdr(M_DONTWAIT, MT_HEADER);
|
|
if (!m) {
|
|
m_freem(n);
|
|
return;
|
|
}
|
|
if (skb_put(m->skb, len) == NULL) {
|
|
m_freem(m);
|
|
m_freem(n);
|
|
return;
|
|
}
|
|
ip = mtod(m, struct pf_ip *);
|
|
ic = (struct pf_icmp *)(ip + 1);
|
|
memset(ip, 0, 20);
|
|
ip->ip_v = 4;
|
|
ip->ip_hl = 5;
|
|
ip->ip_len = htons(len);
|
|
ip->ip_id = ip_randomid();
|
|
ip->ip_off = htons(atomic_load_int(&ip_mtudisc) ? IP_DF : 0);
|
|
ip->ip_ttl = atomic_load_int(&ip_defttl);
|
|
ip->ip_p = IPPROTO_ICMP;
|
|
ip->ip_dst = oip->ip_src; /* answer to the offender */
|
|
/* source: address of our route egress interface (mirrors BSD,
|
|
* which fills ip_src from the route/interface). Linux 7.3's
|
|
* struct rtable no longer caches rt_src, so consult the PF
|
|
* interface registry for the device's first IPv4 address. */
|
|
{
|
|
struct rtable *rt = pfk_ip4_route(oip->ip_src.s_addr, 0);
|
|
struct pfk_ifnet *ifp;
|
|
|
|
rsaddr = 0;
|
|
if (rt) {
|
|
ifp = rt->dst.dev ?
|
|
pfk_ifp_byindex(rt->dst.dev->ifindex) : NULL;
|
|
if (ifp) {
|
|
struct ifaddr *ifa;
|
|
|
|
TAILQ_FOREACH(ifa, &ifp->if_addrlist, ifa_list) {
|
|
if (ifa->ifa_addr &&
|
|
ifa->ifa_addr->sa_family == AF_INET) {
|
|
rsaddr = sintosa(ifa->ifa_addr)->sin_addr.s_addr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
dst_release(&rt->dst);
|
|
}
|
|
}
|
|
ip->ip_src.s_addr = rsaddr;
|
|
|
|
ic->icmp_type = type;
|
|
ic->icmp_code = code;
|
|
ic->icmp_cksum = 0;
|
|
if (type == ICMP_UNREACH && code == ICMP_UNREACH_NEEDFRAG) {
|
|
ic->icmp_nextmtu = htons(((__u16)mtu));
|
|
} else {
|
|
ic->icmp_void = 0;
|
|
if (dest)
|
|
ic->icmp_gwaddr = (__force __be32)dest;
|
|
}
|
|
memcpy(ic + 1, (char *)oip, qlen);
|
|
{
|
|
__wsum w = csum_partial(ic, 8 + qlen, 0);
|
|
|
|
ic->icmp_cksum = csum_fold(w);
|
|
}
|
|
m->m_pkthdr.len = m->m_len = len;
|
|
pfk_mbuf_refresh(m);
|
|
m->m_pkthdr.pf.flags |= PF_TAG_GENERATED;
|
|
|
|
if (n)
|
|
m_freem(n);
|
|
ip_send(m);
|
|
}
|
|
|
|
/*
|
|
* pfk_ip6_output(m, opt, ro, flags, so, flowid) — BSD ip6_output() for
|
|
* IPv6 packets with a complete header. PFLINUX: same reductions as
|
|
* pfk_ip_output; consumes m; returns 0/errno.
|
|
*/
|
|
int
|
|
pfk_ip6_output(struct mbuf *m, void *opt, void *ro, u_int flags, void *so,
|
|
void *flowid)
|
|
{
|
|
struct sk_buff *skb = m ? m_to_skb(m) : NULL;
|
|
struct pf_ip6_hdr *ip6;
|
|
struct flowi6 fl6 = { };
|
|
struct dst_entry *d;
|
|
|
|
(void)opt; (void)ro; (void)flags; (void)so; (void)flowid;
|
|
if (!skb)
|
|
return EINVAL;
|
|
if (!pskb_may_pull(skb, sizeof(*ip6))) {
|
|
m_freem(m);
|
|
return EMSGSIZE;
|
|
}
|
|
ip6 = (struct pf_ip6_hdr *)skb->data;
|
|
if (m->m_pkthdr.csum_flags & (M_TCP_CSUM_OUT | M_UDP_CSUM_OUT)) {
|
|
int plen = ntohs(ip6->ip6_plen);
|
|
|
|
if (ip6->ip6_nxt == IPPROTO_TCP && plen >= 20) {
|
|
__be16 *sum = (__be16 *)(skb->data + sizeof(*ip6) + 16);
|
|
|
|
*sum = 0;
|
|
*sum = (__force __be16)in6_cksum(m, IPPROTO_TCP,
|
|
sizeof(*ip6), plen);
|
|
} else if (ip6->ip6_nxt == IPPROTO_UDP && plen >= 8) {
|
|
__be16 *sum = (__be16 *)(skb->data + sizeof(*ip6) + 6);
|
|
u_int16_t s = in6_cksum(m, IPPROTO_UDP, sizeof(*ip6),
|
|
plen);
|
|
|
|
*sum = s == 0 ? 0xffff : (__force __be16)s;
|
|
}
|
|
}
|
|
skb_reset_network_header(skb);
|
|
skb->protocol = htons(ETH_P_IPV6);
|
|
fl6.daddr = ip6->ip6_dst;
|
|
fl6.saddr = ip6->ip6_src;
|
|
d = ip6_route_output(&init_net, NULL, &fl6);
|
|
if (!d || d->error) {
|
|
if (d)
|
|
dst_release(d);
|
|
m_freem(m);
|
|
pfk_if_congestion();
|
|
return EHOSTUNREACH;
|
|
}
|
|
skb_dst_set(skb, d);
|
|
skb->dev = d->dev;
|
|
if (!netif_running(skb->dev)) {
|
|
m->skb = NULL;
|
|
m_freem(m);
|
|
kfree_skb(skb);
|
|
return ENETDOWN;
|
|
}
|
|
m->skb = NULL;
|
|
m_freem(m);
|
|
skb_dst(skb)->output(&init_net, NULL, skb);
|
|
return 0;
|
|
}
|
|
|
|
/* ip6_send(m): BSD convenience — output an IPv6 packet to its dst */
|
|
void
|
|
ip6_send(struct mbuf *m)
|
|
{
|
|
if (!m)
|
|
return;
|
|
(void)pfk_ip6_output(m, NULL, NULL, 0, NULL, 0);
|
|
}
|