src/sys/netinet6/ip6_divert.c

347 lines
8.1 KiB
C

/* $OpenBSD: ip6_divert.c,v 1.95 2024/02/13 12:22:09 bluhm Exp $ */
/*
* Copyright (c) 2009 Michele Marchetto <michele@openbsd.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/mbuf.h>
#include <sys/protosw.h>
#include <sys/socket.h>
#include <sys/socketvar.h>
#include <sys/sysctl.h>
#include <net/if.h>
#include <net/route.h>
#include <net/if_var.h>
#include <net/netisr.h>
#include <netinet/in.h>
#include <netinet6/in6_var.h>
#include <netinet/ip.h>
#include <netinet/ip_var.h>
#include <netinet/ip6.h>
#include <netinet6/ip6_var.h>
#include <netinet/in_pcb.h>
#include <netinet/ip_divert.h>
#include <netinet6/ip6_divert.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <netinet/icmp6.h>
#include <net/pfvar.h>
struct inpcbtable divb6table;
struct cpumem *div6counters;
#ifndef DIVERT_SENDSPACE
#define DIVERT_SENDSPACE (65536 + 100)
#endif
u_int divert6_sendspace = DIVERT_SENDSPACE;
#ifndef DIVERT_RECVSPACE
#define DIVERT_RECVSPACE (65536 + 100)
#endif
u_int divert6_recvspace = DIVERT_RECVSPACE;
#ifndef DIVERTHASHSIZE
#define DIVERTHASHSIZE 128
#endif
const struct sysctl_bounded_args divert6ctl_vars[] = {
{ DIVERT6CTL_RECVSPACE, &divert6_recvspace, 0, INT_MAX },
{ DIVERT6CTL_SENDSPACE, &divert6_sendspace, 0, INT_MAX },
};
const struct pr_usrreqs divert6_usrreqs = {
.pru_attach = divert6_attach,
.pru_detach = divert_detach,
.pru_lock = divert_lock,
.pru_unlock = divert_unlock,
.pru_locked = divert_locked,
.pru_bind = divert_bind,
.pru_shutdown = divert_shutdown,
.pru_send = divert6_send,
.pru_control = in6_control,
.pru_sockaddr = in6_sockaddr,
.pru_peeraddr = in6_peeraddr,
};
int divb6hashsize = DIVERTHASHSIZE;
int divert6_output(struct inpcb *, struct mbuf *, struct mbuf *,
struct mbuf *);
void
divert6_init(void)
{
in_pcbinit(&divb6table, divb6hashsize);
div6counters = counters_alloc(div6s_ncounters);
}
int
divert6_output(struct inpcb *inp, struct mbuf *m, struct mbuf *nam,
struct mbuf *control)
{
struct sockaddr_in6 *sin6;
int error, min_hdrlen, nxt, off, dir;
struct ip6_hdr *ip6;
m_freem(control);
if ((error = in6_nam2sin6(nam, &sin6)))
goto fail;
/* Do basic sanity checks. */
if (m->m_pkthdr.len < sizeof(struct ip6_hdr))
goto fail;
if ((m = m_pullup(m, sizeof(struct ip6_hdr))) == NULL) {
/* m_pullup() has freed the mbuf, so just return. */
div6stat_inc(div6s_errors);
return (ENOBUFS);
}
ip6 = mtod(m, struct ip6_hdr *);
if ((ip6->ip6_vfc & IPV6_VERSION_MASK) != IPV6_VERSION)
goto fail;
if (m->m_pkthdr.len < sizeof(struct ip6_hdr) + ntohs(ip6->ip6_plen))
goto fail;
/*
* Recalculate the protocol checksum since the userspace application
* may have modified the packet prior to reinjection.
*/
off = ip6_lasthdr(m, 0, IPPROTO_IPV6, &nxt);
if (off < sizeof(struct ip6_hdr))
goto fail;
dir = (IN6_IS_ADDR_UNSPECIFIED(&sin6->sin6_addr) ? PF_OUT : PF_IN);
switch (nxt) {
case IPPROTO_TCP:
min_hdrlen = sizeof(struct tcphdr);
m->m_pkthdr.csum_flags |= M_TCP_CSUM_OUT;
break;
case IPPROTO_UDP:
min_hdrlen = sizeof(struct udphdr);
m->m_pkthdr.csum_flags |= M_UDP_CSUM_OUT;
break;
case IPPROTO_ICMPV6:
min_hdrlen = sizeof(struct icmp6_hdr);
m->m_pkthdr.csum_flags |= M_ICMP_CSUM_OUT;
break;
default:
min_hdrlen = 0;
break;
}
if (min_hdrlen && m->m_pkthdr.len < off + min_hdrlen)
goto fail;
m->m_pkthdr.pf.flags |= PF_TAG_DIVERTED_PACKET;
if (dir == PF_IN) {
struct rtentry *rt;
struct ifnet *ifp;
rt = rtalloc(sin6tosa(sin6), 0, inp->inp_rtableid);
if (!rtisvalid(rt) || !ISSET(rt->rt_flags, RTF_LOCAL)) {
rtfree(rt);
error = EADDRNOTAVAIL;
goto fail;
}
m->m_pkthdr.ph_ifidx = rt->rt_ifidx;
rtfree(rt);
/*
* Recalculate the protocol checksum for the inbound packet
* since the userspace application may have modified the packet
* prior to reinjection.
*/
in6_proto_cksum_out(m, NULL);
ifp = if_get(m->m_pkthdr.ph_ifidx);
if (ifp == NULL) {
error = ENETDOWN;
goto fail;
}
ipv6_input(ifp, m);
if_put(ifp);
} else {
m->m_pkthdr.ph_rtableid = inp->inp_rtableid;
error = ip6_output(m, NULL, &inp->inp_route,
IP_ALLOWBROADCAST | IP_RAWOUTPUT, NULL, NULL);
}
div6stat_inc(div6s_opackets);
return (error);
fail:
div6stat_inc(div6s_errors);
m_freem(m);
return (error ? error : EINVAL);
}
void
divert6_packet(struct mbuf *m, int dir, u_int16_t divert_port)
{
struct inpcb *inp = NULL;
struct socket *so;
struct sockaddr_in6 sin6;
div6stat_inc(div6s_ipackets);
if (m->m_len < sizeof(struct ip6_hdr) &&
(m = m_pullup(m, sizeof(struct ip6_hdr))) == NULL) {
div6stat_inc(div6s_errors);
goto bad;
}
mtx_enter(&divb6table.inpt_mtx);
TAILQ_FOREACH(inp, &divb6table.inpt_queue, inp_queue) {
if (inp->inp_lport != divert_port)
continue;
in_pcbref(inp);
break;
}
mtx_leave(&divb6table.inpt_mtx);
if (inp == NULL) {
div6stat_inc(div6s_noport);
goto bad;
}
memset(&sin6, 0, sizeof(sin6));
sin6.sin6_family = AF_INET6;
sin6.sin6_len = sizeof(sin6);
if (dir == PF_IN) {
struct ifaddr *ifa;
struct ifnet *ifp;
ifp = if_get(m->m_pkthdr.ph_ifidx);
if (ifp == NULL) {
div6stat_inc(div6s_errors);
goto bad;
}
TAILQ_FOREACH(ifa, &ifp->if_addrlist, ifa_list) {
if (ifa->ifa_addr->sa_family != AF_INET6)
continue;
sin6.sin6_addr = satosin6(ifa->ifa_addr)->sin6_addr;
break;
}
if_put(ifp);
} else {
/*
* Calculate protocol checksum for outbound packet diverted
* to userland. pf out rule diverts before cksum offload.
*/
in6_proto_cksum_out(m, NULL);
}
so = inp->inp_socket;
mtx_enter(&so->so_rcv.sb_mtx);
if (sbappendaddr(so, &so->so_rcv, sin6tosa(&sin6), m, NULL) == 0) {
mtx_leave(&so->so_rcv.sb_mtx);
div6stat_inc(div6s_fullsock);
goto bad;
}
mtx_leave(&so->so_rcv.sb_mtx);
sorwakeup(so);
in_pcbunref(inp);
return;
bad:
if (inp != NULL)
in_pcbunref(inp);
m_freem(m);
}
int
divert6_attach(struct socket *so, int proto, int wait)
{
int error;
if (so->so_pcb != NULL)
return EINVAL;
if ((so->so_state & SS_PRIV) == 0)
return EACCES;
error = in_pcballoc(so, &divb6table, wait);
if (error)
return (error);
error = soreserve(so, divert6_sendspace, divert6_recvspace);
if (error)
return (error);
return (0);
}
int
divert6_send(struct socket *so, struct mbuf *m, struct mbuf *addr,
struct mbuf *control)
{
struct inpcb *inp = sotoinpcb(so);
soassertlocked(so);
return (divert6_output(inp, m, addr, control));
}
int
divert6_sysctl_div6stat(void *oldp, size_t *oldlenp, void *newp)
{
uint64_t counters[div6s_ncounters];
struct div6stat div6stat;
u_long *words = (u_long *)&div6stat;
int i;
CTASSERT(sizeof(div6stat) == (nitems(counters) * sizeof(u_long)));
counters_read(div6counters, counters, nitems(counters), NULL);
for (i = 0; i < nitems(counters); i++)
words[i] = (u_long)counters[i];
return (sysctl_rdstruct(oldp, oldlenp, newp,
&div6stat, sizeof(div6stat)));
}
/*
* Sysctl for divert variables.
*/
int
divert6_sysctl(int *name, u_int namelen, void *oldp, size_t *oldlenp,
void *newp, size_t newlen)
{
int error;
/* All sysctl names at this level are terminal. */
if (namelen != 1)
return (ENOTDIR);
switch (name[0]) {
case DIVERT6CTL_STATS:
return (divert6_sysctl_div6stat(oldp, oldlenp, newp));
default:
NET_LOCK();
error = sysctl_bounded_arr(divert6ctl_vars,
nitems(divert6ctl_vars), name, namelen, oldp, oldlenp,
newp, newlen);
NET_UNLOCK();
return (error);
}
/* NOTREACHED */
}