/* $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 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef INET6 #include #endif #if NBPFILTER > 0 #include #endif #if NPF > 0 #include #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 = <db->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); }