src/sys/net/if_sec.c

619 lines
12 KiB
C

/* $OpenBSD: if_sec.c,v 1.11 2024/03/19 03:49:11 dlg Exp $ */
/*
* Copyright (c) 2022 The University of Queensland
*
* 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.
*/
/*
* This code was written by David Gwynne <dlg@uq.edu.au> as part
* of the Information Technology Infrastructure Group (ITIG) in the
* Faculty of Engineering, Architecture and Information Technology
* (EAIT).
*/
#ifndef IPSEC
#error sec enabled without IPSEC defined
#endif
#include "bpfilter.h"
#include "pf.h"
#include <sys/param.h>
#include <sys/mbuf.h>
#include <sys/socket.h>
#include <sys/sockio.h>
#include <sys/systm.h>
#include <sys/errno.h>
#include <sys/smr.h>
#include <sys/refcnt.h>
#include <sys/task.h>
#include <sys/mutex.h>
#include <net/if.h>
#include <net/if_var.h>
#include <net/if_types.h>
#include <net/toeplitz.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_ipsp.h>
#ifdef INET6
#include <netinet/ip6.h>
#endif
#if NBPFILTER > 0
#include <net/bpf.h>
#endif
#if NPF > 0
#include <net/pfvar.h>
#endif
#define SEC_MTU 1280
#define SEC_MTU_MIN 1280
#define SEC_MTU_MAX 32768 /* could get closer to 64k... */
struct sec_softc {
struct ifnet sc_if;
unsigned int sc_dead;
unsigned int sc_up;
struct task sc_send;
int sc_txprio;
unsigned int sc_unit;
SMR_SLIST_ENTRY(sec_softc) sc_entry;
struct refcnt sc_refs;
};
SMR_SLIST_HEAD(sec_bucket, sec_softc);
static int sec_output(struct ifnet *, struct mbuf *, struct sockaddr *,
struct rtentry *);
static int sec_enqueue(struct ifnet *, struct mbuf *);
static void sec_send(void *);
static void sec_start(struct ifqueue *);
static int sec_ioctl(struct ifnet *, u_long, caddr_t);
static int sec_up(struct sec_softc *);
static int sec_down(struct sec_softc *);
static int sec_clone_create(struct if_clone *, int);
static int sec_clone_destroy(struct ifnet *);
static struct tdb *
sec_tdb_get(unsigned int);
static void sec_tdb_gc(void *);
static struct if_clone sec_cloner =
IF_CLONE_INITIALIZER("sec", sec_clone_create, sec_clone_destroy);
static unsigned int sec_mix;
static struct sec_bucket sec_map[256] __aligned(CACHELINESIZE);
static struct tdb *sec_tdbh[256] __aligned(CACHELINESIZE);
static struct tdb *sec_tdb_gc_list;
static struct task sec_tdb_gc_task =
TASK_INITIALIZER(sec_tdb_gc, NULL);
static struct mutex sec_tdb_gc_mtx =
MUTEX_INITIALIZER(IPL_MPFLOOR);
void
secattach(int n)
{
sec_mix = arc4random();
if_clone_attach(&sec_cloner);
}
static int
sec_clone_create(struct if_clone *ifc, int unit)
{
struct sec_softc *sc;
struct ifnet *ifp;
sc = malloc(sizeof(*sc), M_DEVBUF, M_WAITOK|M_ZERO);
sc->sc_unit = unit;
task_set(&sc->sc_send, sec_send, sc);
snprintf(sc->sc_if.if_xname, sizeof sc->sc_if.if_xname, "%s%d",
ifc->ifc_name, unit);
ifp = &sc->sc_if;
ifp->if_softc = sc;
ifp->if_type = IFT_TUNNEL;
ifp->if_mtu = SEC_MTU;
ifp->if_flags = IFF_POINTOPOINT|IFF_MULTICAST;
ifp->if_xflags = IFXF_CLONED | IFXF_MPSAFE;
ifp->if_bpf_mtap = p2p_bpf_mtap;
ifp->if_input = p2p_input;
ifp->if_output = sec_output;
ifp->if_enqueue = sec_enqueue;
ifp->if_qstart = sec_start;
ifp->if_ioctl = sec_ioctl;
ifp->if_rtrequest = p2p_rtrequest;
if_counters_alloc(ifp);
if_attach(ifp);
if_alloc_sadl(ifp);
#if NBPFILTER > 0
bpfattach(&ifp->if_bpf, ifp, DLT_LOOP, sizeof(uint32_t));
#endif
return (0);
}
static int
sec_clone_destroy(struct ifnet *ifp)
{
struct sec_softc *sc = ifp->if_softc;
NET_LOCK();
sc->sc_dead = 1;
if (ISSET(ifp->if_flags, IFF_RUNNING))
sec_down(sc);
NET_UNLOCK();
if_detach(ifp);
free(sc, M_DEVBUF, sizeof(*sc));
return (0);
}
static int
sec_ioctl(struct ifnet *ifp, u_long cmd, caddr_t data)
{
struct sec_softc *sc = ifp->if_softc;
struct ifreq *ifr = (struct ifreq *)data;
int error = 0;
switch (cmd) {
case SIOCSIFADDR:
break;
case SIOCSIFFLAGS:
if (ISSET(ifp->if_flags, IFF_UP)) {
if (!ISSET(ifp->if_flags, IFF_RUNNING))
error = sec_up(sc);
else
error = 0;
} else {
if (ISSET(ifp->if_flags, IFF_RUNNING))
error = sec_down(sc);
}
break;
case SIOCADDMULTI:
case SIOCDELMULTI:
break;
case SIOCSIFMTU:
if (ifr->ifr_mtu < SEC_MTU_MIN ||
ifr->ifr_mtu > SEC_MTU_MAX) {
error = EINVAL;
break;
}
ifp->if_mtu = ifr->ifr_mtu;
break;
default:
error = ENOTTY;
break;
}
return (error);
}
static int
sec_up(struct sec_softc *sc)
{
struct ifnet *ifp = &sc->sc_if;
unsigned int idx = stoeplitz_h32(sc->sc_unit) % nitems(sec_map);
NET_ASSERT_LOCKED();
KASSERT(!ISSET(ifp->if_flags, IFF_RUNNING));
if (sc->sc_dead)
return (ENXIO);
/*
* coordinate with sec_down(). if sc_up is still up and
* we're here then something else is running sec_down.
*/
if (sc->sc_up)
return (EBUSY);
sc->sc_up = 1;
refcnt_init(&sc->sc_refs);
SET(ifp->if_flags, IFF_RUNNING);
SMR_SLIST_INSERT_HEAD_LOCKED(&sec_map[idx], sc, sc_entry);
return (0);
}
static int
sec_down(struct sec_softc *sc)
{
struct ifnet *ifp = &sc->sc_if;
unsigned int idx = stoeplitz_h32(sc->sc_unit) % nitems(sec_map);
NET_ASSERT_LOCKED();
KASSERT(ISSET(ifp->if_flags, IFF_RUNNING));
/*
* taking sec down involves waiting for it to stop running
* in various contexts. this thread cannot hold netlock
* while waiting for a barrier for a task that could be trying
* to take netlock itself. so give up netlock, but don't clear
* sc_up to prevent sec_up from running.
*/
CLR(ifp->if_flags, IFF_RUNNING);
NET_UNLOCK();
smr_barrier();
taskq_del_barrier(systq, &sc->sc_send);
refcnt_finalize(&sc->sc_refs, "secdown");
NET_LOCK();
SMR_SLIST_REMOVE_LOCKED(&sec_map[idx], sc, sec_softc, sc_entry);
sc->sc_up = 0;
return (0);
}
static int
sec_output(struct ifnet *ifp, struct mbuf *m, struct sockaddr *dst,
struct rtentry *rt)
{
struct m_tag *mtag;
int error = 0;
if (!ISSET(ifp->if_flags, IFF_RUNNING)) {
error = ENETDOWN;
goto drop;
}
switch (dst->sa_family) {
case AF_INET:
#ifdef INET6
case AF_INET6:
#endif
#ifdef MPLS
case AF_MPLS:
#endif
break;
default:
error = EAFNOSUPPORT;
goto drop;
}
mtag = NULL;
while ((mtag = m_tag_find(m, PACKET_TAG_GRE, mtag)) != NULL) {
if (ifp->if_index == *(int *)(mtag + 1)) {
error = EIO;
goto drop;
}
}
mtag = m_tag_get(PACKET_TAG_GRE, sizeof(ifp->if_index), M_NOWAIT);
if (mtag == NULL) {
error = ENOBUFS;
goto drop;
}
*(int *)(mtag + 1) = ifp->if_index;
m_tag_prepend(m, mtag);
m->m_pkthdr.ph_family = dst->sa_family;
error = if_enqueue(ifp, m);
if (error != 0)
counters_inc(ifp->if_counters, ifc_oqdrops);
return (error);
drop:
m_freem(m);
return (error);
}
static int
sec_enqueue(struct ifnet *ifp, struct mbuf *m)
{
struct sec_softc *sc = ifp->if_softc;
struct ifqueue *ifq = &ifp->if_snd;
int error;
error = ifq_enqueue(ifq, m);
if (error)
return (error);
task_add(systq, &sc->sc_send);
return (0);
}
static void
sec_send(void *arg)
{
struct sec_softc *sc = arg;
struct ifnet *ifp = &sc->sc_if;
struct ifqueue *ifq = &ifp->if_snd;
struct tdb *tdb;
struct mbuf *m;
int error;
unsigned int flowid;
if (!ISSET(ifp->if_flags, IFF_RUNNING))
return;
tdb = sec_tdb_get(sc->sc_unit);
if (tdb == NULL)
goto purge;
flowid = sc->sc_unit ^ sec_mix;
NET_LOCK();
while ((m = ifq_dequeue(ifq)) != NULL) {
CLR(m->m_flags, M_BCAST|M_MCAST);
#if NPF > 0
pf_pkt_addr_changed(m);
#endif
#if NBPFILTER > 0
if (ifp->if_bpf)
bpf_mtap_af(ifp->if_bpf, m->m_pkthdr.ph_family, m,
BPF_DIRECTION_OUT);
#endif
m->m_pkthdr.pf.prio = sc->sc_txprio;
SET(m->m_pkthdr.csum_flags, M_FLOWID);
m->m_pkthdr.ph_flowid = flowid;
error = ipsp_process_packet(m, tdb,
m->m_pkthdr.ph_family, /* already tunnelled? */ 0);
if (error != 0)
counters_inc(ifp->if_counters, ifc_oerrors);
}
NET_UNLOCK();
tdb_unref(tdb);
return;
purge:
counters_add(ifp->if_counters, ifc_oerrors, ifq_purge(ifq));
}
static void
sec_start(struct ifqueue *ifq)
{
struct ifnet *ifp = ifq->ifq_if;
struct sec_softc *sc = ifp->if_softc;
/* move this back to systq for KERNEL_LOCK */
task_add(systq, &sc->sc_send);
}
/*
* ipsec_input handling
*/
struct sec_softc *
sec_get(unsigned int unit)
{
unsigned int idx = stoeplitz_h32(unit) % nitems(sec_map);
struct sec_bucket *sb = &sec_map[idx];
struct sec_softc *sc;
smr_read_enter();
SMR_SLIST_FOREACH(sc, sb, sc_entry) {
if (sc->sc_unit == unit) {
refcnt_take(&sc->sc_refs);
break;
}
}
smr_read_leave();
return (sc);
}
void
sec_input(struct sec_softc *sc, int af, int proto, struct mbuf *m)
{
struct ip *iph;
int hlen;
switch (af) {
case AF_INET:
iph = mtod(m, struct ip *);
hlen = iph->ip_hl << 2;
break;
#ifdef INET6
case AF_INET6:
hlen = sizeof(struct ip6_hdr);
break;
#endif
default:
unhandled_af(af);
}
m_adj(m, hlen);
switch (proto) {
case IPPROTO_IPV4:
af = AF_INET;
break;
case IPPROTO_IPV6:
af = AF_INET6;
break;
case IPPROTO_MPLS:
af = AF_MPLS;
break;
default:
af = AF_UNSPEC;
break;
}
m->m_pkthdr.ph_family = af;
if_vinput(&sc->sc_if, m);
}
void
sec_put(struct sec_softc *sc)
{
refcnt_rele_wake(&sc->sc_refs);
}
/*
* tdb handling
*/
static int
sec_tdb_valid(struct tdb *tdb)
{
KASSERT(ISSET(tdb->tdb_flags, TDBF_IFACE));
if (!ISSET(tdb->tdb_flags, TDBF_TUNNELING))
return (0);
if (ISSET(tdb->tdb_flags, TDBF_INVALID))
return (0);
if (tdb->tdb_iface_dir != IPSP_DIRECTION_OUT)
return (0);
return (1);
}
/*
* these are called from netinet/ip_ipsp.c with tdb_sadb_mtx held,
* which we rely on to serialise modifications to the sec_tdbh.
*/
void
sec_tdb_insert(struct tdb *tdb)
{
unsigned int idx;
struct tdb **tdbp;
struct tdb *ltdb;
if (!sec_tdb_valid(tdb))
return;
idx = stoeplitz_h32(tdb->tdb_iface) % nitems(sec_tdbh);
tdbp = &sec_tdbh[idx];
tdb_ref(tdb); /* take a ref for the SMR pointer */
/* wire the tdb into the head of the list */
ltdb = SMR_PTR_GET_LOCKED(tdbp);
SMR_PTR_SET_LOCKED(&tdb->tdb_dnext, ltdb);
SMR_PTR_SET_LOCKED(tdbp, tdb);
}
void
sec_tdb_remove(struct tdb *tdb)
{
struct tdb **tdbp;
struct tdb *ltdb;
unsigned int idx;
if (!sec_tdb_valid(tdb))
return;
idx = stoeplitz_h32(tdb->tdb_iface) % nitems(sec_tdbh);
tdbp = &sec_tdbh[idx];
while ((ltdb = SMR_PTR_GET_LOCKED(tdbp)) != NULL) {
if (ltdb == tdb) {
/* take the tdb out of the list */
ltdb = SMR_PTR_GET_LOCKED(&tdb->tdb_dnext);
SMR_PTR_SET_LOCKED(tdbp, ltdb);
/* move the ref to the gc */
mtx_enter(&sec_tdb_gc_mtx);
tdb->tdb_dnext = sec_tdb_gc_list;
sec_tdb_gc_list = tdb;
mtx_leave(&sec_tdb_gc_mtx);
task_add(systq, &sec_tdb_gc_task);
return;
}
tdbp = &ltdb->tdb_dnext;
}
panic("%s: unable to find tdb %p", __func__, tdb);
}
static void
sec_tdb_gc(void *null)
{
struct tdb *tdb, *ntdb;
mtx_enter(&sec_tdb_gc_mtx);
tdb = sec_tdb_gc_list;
sec_tdb_gc_list = NULL;
mtx_leave(&sec_tdb_gc_mtx);
if (tdb == NULL)
return;
smr_barrier();
NET_LOCK();
do {
ntdb = tdb->tdb_dnext;
tdb_unref(tdb);
tdb = ntdb;
} while (tdb != NULL);
NET_UNLOCK();
}
struct tdb *
sec_tdb_get(unsigned int unit)
{
unsigned int idx;
struct tdb **tdbp;
struct tdb *tdb;
idx = stoeplitz_h32(unit) % nitems(sec_map);
tdbp = &sec_tdbh[idx];
smr_read_enter();
while ((tdb = SMR_PTR_GET(tdbp)) != NULL) {
KASSERT(ISSET(tdb->tdb_flags, TDBF_IFACE));
if (!ISSET(tdb->tdb_flags, TDBF_DELETED) &&
tdb->tdb_iface == unit) {
tdb_ref(tdb);
break;
}
tdbp = &tdb->tdb_dnext;
}
smr_read_leave();
return (tdb);
}