/* $OpenBSD$ */ /* * Copyright (c) 2019 David Gwynne * * 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 "bpfilter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if NBPFILTER > 0 #include #endif /* * 802.1AE Media Access Control (MAC) Security - MACsec */ /* Protocol definitions */ struct macsec_shim { uint8_t tci_an; /* TAG Control Information */ #define MACSEC_TCI_V (0x1 << 7) /* Version */ #define MACSEC_TCI_V_0 (0x0 << 7) #define MACSEC_TCI_ES (0x1 << 6) /* End Station */ #define MACSEC_TCI_SC (0x1 << 5) /* Secure Channel */ #define MACSEC_TCI_SCB (0x1 << 4) /* Single Copy Broadcast */ #define MACSEC_TCI_E (0x1 << 3) /* Encryption */ #define MACSEC_TCI_C (0x1 << 2) /* Changed */ /* Association Number */ #define MACSEC_AN (0x3 << 0) uint8_t sl; /* Short Length */ #define MACSEC_SL_MAX 48 #define MACSEC_SL_UNUSED 0xc0 uint8_t pn[4]; /* Packet Number */ } __packed; struct macsec_sci { uint8_t sysid[ETHER_ADDR_LEN]; uint16_t portid; } __packed; #define MACSEC_PORTID_SCB 0x0000 /* Reserved? */ #define MACSEC_PORTID_ES 0x0001 #define MACSEC_PORTID_MIN 0x0001 #define MACSEC_PORTID_MAX 0xffff #define MACSEC_PORTID_DEFAULT MACSEC_PORTID_ES /* * Software definitions */ struct macsec_multiaddr { TAILQ_ENTRY(macsec_multiaddr) m_entry; unsigned int m_refs; uint8_t m_addrlo[ETHER_ADDR_LEN]; uint8_t m_addrhi[ETHER_ADDR_LEN]; struct sockaddr m_addr; }; TAILQ_HEAD(macsec_multiaddrs, macsec_multiaddr); struct macsec_rx_key { struct macsec_softc *rx_sc; struct macsec_sci rx_sci; RBT_ENTRY(macsec_rx_key) rx_rb_entry; }; RBT_HEAD(macsec_rx_tree, macsec_rx_key); struct macsec_softc { struct arpcom sc_ac; #define sc_if sc_ac.ac_if unsigned int sc_promisc; struct macsec_multiaddrs sc_multiaddrs; unsigned int sc_parent; uint16_t sc_portid; /* network byte order */ void *sc_lh_cookie; void *sc_dh_cookie; }; static int macsec_clone_create(struct if_clone *, int); static int macsec_clone_destroy(struct ifnet *); static void macsec_start(struct ifnet *); static int macsec_ioctl(struct ifnet *, u_long, caddr_t); static int macsec_up(struct macsec_softc *); static int macsec_iff(struct macsec_softc *); static int macsec_down(struct macsec_softc *); static int macsec_set_vnetid(struct macsec_softc *, const struct ifreq *); static int macsec_get_vnetid(struct macsec_softc *, struct ifreq *); static int macsec_set_parent(struct macsec_softc *, const struct if_parent *); static int macsec_get_parent(struct macsec_softc *, struct if_parent *); static int macsec_del_parent(struct macsec_softc *); static void macsec_link_hook(void *); static void macsec_link_state(struct macsec_softc *, u_char, uint64_t); static void macsec_detach_hook(void *); static int macsec_multi_add(struct macsec_softc *, struct ifreq *); static int macsec_multi_del(struct macsec_softc *, struct ifreq *); static int macsec_multi(struct ifnet *, const struct macsec_multiaddr *, u_long); static struct if_clone macsec_cloner = IF_CLONE_INITIALIZER("macsec", macsec_clone_create, macsec_clone_destroy); void macsecattach(int count) { if_clone_attach(&macsec_cloner); } static int macsec_clone_create(struct if_clone *ifc, int unit) { struct macsec_softc *sc; struct ifnet *ifp; sc = malloc(sizeof(*sc), M_DEVBUF, M_WAITOK|M_ZERO); ifp = &sc->sc_if; snprintf(ifp->if_xname, sizeof(ifp->if_xname), "%s%d", ifc->ifc_name, unit); TAILQ_INIT(&sc->sc_multiaddrs); sc->sc_portid = htons(MACSEC_PORTID_DEFAULT); sc->sc_parent = 0; ifp->if_softc = sc; ifp->if_hardmtu = ETHER_MAX_HARDMTU_LEN; ifp->if_ioctl = macsec_ioctl; ifp->if_start = macsec_start; ifp->if_flags = IFF_BROADCAST | IFF_MULTICAST; ifp->if_xflags = IFXF_CLONED; IFQ_SET_MAXLEN(&ifp->if_snd, IFQ_MAXLEN); ether_fakeaddr(ifp); if_attach(ifp); ether_ifattach(ifp); if_counters_alloc(ifp); return (0); } static int macsec_clone_destroy(struct ifnet *ifp) { struct macsec_softc *sc = ifp->if_softc; NET_LOCK(); if (ISSET(ifp->if_flags, IFF_RUNNING)) macsec_down(sc); NET_UNLOCK(); ether_ifdetach(ifp); if_detach(ifp); free(sc, M_DEVBUF, sizeof(*sc)); return (0); } static void macsec_start(struct ifnet *ifp) { struct macsec_softc *sc = ifp->if_softc; struct ifnet *ifp0; struct mbuf *m; ifp0 = if_get(sc->sc_parent); if (ifp0 == NULL || !ISSET(ifp0->if_flags, IFF_RUNNING)) { ifq_purge(&ifp->if_snd); goto done; } while ((m = ifq_dequeue(&ifp->if_snd)) != NULL) { struct mbuf *m0; struct ether_header *eh; struct macsec_shim *shim; int hlen = ETHER_ALIGN + sizeof(*eh) + sizeof(*shim); #if NBPFILTER > 0 caddr_t if_bpf = ifp->if_bpf; if (if_bpf) bpf_mtap(if_bpf, m, BPF_DIRECTION_OUT); #endif /* force prepend of a whole mbuf because of alignment */ m0 = m_get(M_DONTWAIT, m->m_type); if (m0 == NULL) { m_freem(m); continue; } m_align(m0, hlen); m_adj(m0, ETHER_ALIGN); hlen -= ETHER_ALIGN; eh = mtod(m0, struct ether_header *); /* copy the addresses from the original mbuf to the shim */ memcpy(eh, mtod(m, struct ether_header *), offsetof(struct ether_header, ether_type)); eh->ether_type = htons(ETHERTYPE_MACSEC); shim = (struct macsec_shim *)(eh + 1); shim->tci_an = MACSEC_TCI_V_0; /* cut the addresses out of the original mbuf */ m_adj(m, offsetof(struct ether_header, ether_type)); /* wire the shim in */ M_MOVE_PKTHDR(m0, m); m0->m_pkthdr.len += m0->m_len; m0->m_next = m; if_enqueue(ifp0, m0); } done: if_put(ifp0); } static int macsec_ioctl(struct ifnet *ifp, u_long cmd, caddr_t data) { struct macsec_softc *sc = ifp->if_softc; struct ifreq *ifr = (struct ifreq *)data; int error = 0; switch (cmd) { case SIOCSIFFLAGS: if (ISSET(ifp->if_flags, IFF_UP)) { if (!ISSET(ifp->if_flags, IFF_RUNNING)) error = macsec_up(sc); else error = ENETRESET; } else { if (ISSET(ifp->if_flags, IFF_RUNNING)) error = macsec_down(sc); } break; case SIOCSVNETID: error = macsec_set_vnetid(sc, ifr); break; case SIOCGVNETID: error = macsec_get_vnetid(sc, ifr); break; case SIOCSIFPARENT: error = macsec_set_parent(sc, (struct if_parent *)data); break; case SIOCGIFPARENT: error = macsec_get_parent(sc, (struct if_parent *)data); break; case SIOCDIFPARENT: error = macsec_del_parent(sc); break; case SIOCADDMULTI: error = macsec_multi_add(sc, ifr); break; case SIOCDELMULTI: error = macsec_multi_del(sc, ifr); break; default: error = ether_ioctl(ifp, &sc->sc_ac, cmd, data); break; } if (error == ENETRESET) error = macsec_iff(sc); return (error); } static int macsec_up(struct macsec_softc *sc) { struct ifnet *ifp = &sc->sc_if; struct ifnet *ifp0; struct macsec_multiaddr *ma; int error = 0; u_int hardmtu; u_int hlen = sizeof(struct macsec_shim) + sizeof(struct macsec_sci) + sizeof(uint16_t); KASSERT(!ISSET(ifp->if_flags, IFF_RUNNING)); NET_ASSERT_LOCKED(); ifp0 = if_get(sc->sc_parent); if (ifp0 == NULL) return (ENXIO); /* check again if macsec will work on top of the parent */ if (ifp0->if_type != IFT_ETHER) { error = EPROTONOSUPPORT; goto put; } hardmtu = ifp0->if_hardmtu; if (hardmtu < hlen) { error = ENOBUFS; goto put; } hardmtu -= hlen; if (ifp->if_mtu > hardmtu) { error = ENOBUFS; goto put; } /* parent is fine, let's prepare the macsec to handle packets */ ifp->if_hardmtu = hardmtu; TAILQ_FOREACH(ma, &sc->sc_multiaddrs, m_entry) { if (macsec_multi(ifp0, ma, SIOCADDMULTI) != 0) { log(LOG_WARNING, "%s: " "unable to add multicast address on %s", ifp->if_xname, ifp0->if_xname); } } /* Register callback for physical link state changes */ sc->sc_lh_cookie = hook_establish(ifp0->if_linkstatehooks, 1, macsec_link_hook, sc); /* Register callback if parent wants to unregister */ sc->sc_dh_cookie = hook_establish(ifp0->if_detachhooks, 0, macsec_detach_hook, sc); /* we're running now */ SET(ifp->if_flags, IFF_RUNNING); if_put(ifp0); macsec_link_hook(sc); return (ENETRESET); /* call macsec_iff */ put: if_put(ifp0); return (error); } static int macsec_iff(struct macsec_softc *sc) { struct ifnet *ifp = &sc->sc_if; struct ifnet *ifp0; int isset; int error = 0; isset = ISSET(ifp->if_flags, IFF_LINK2); if (isset != sc->sc_p2pmac) { sc->sc_p2pmac = isset; } isset = ISSET(ifp->if_flags, IFF_PROMISC); if (isset != sc->sc_promisc) { ifp0 = if_get(sc->sc_parent); if (ifp0 != NULL) ifpromisc(ifp0, promisc); /* XXX check error? */ if_put(ifp0); sc->sc_promisc = isset; } return (error); } static int macsec_down(struct macsec_softc *sc) { struct ifnet *ifp = &sc->sc_if; struct ifnet *ifp0; struct macsec_multiaddr *ma; NET_ASSERT_LOCKED(); CLR(ifp->if_flags, IFF_RUNNING); if (sc->sc_p2pmac) TAILQ_REMOVE(&macsec_p2pmacs, sc, sc_p2pmac_entry); ifp0 = if_get(sc->sc_parent); if (ifp0 != NULL) { if (sc->sc_promisc) ifpromisc(ifp0, 0); hook_disestablish(ifp0->if_detachhooks, sc->sc_dh_cookie); hook_disestablish(ifp0->if_linkstatehooks, sc->sc_lh_cookie); TAILQ_FOREACH(ma, &sc->sc_multiaddrs, m_entry) { if (macsec_multi(ifp0, ma, SIOCDELMULTI) != 0) { log(LOG_WARNING, "%s: " "unable to del multicast address on %s", ifp->if_xname, ifp0->if_xname); } } } if_put(ifp0); sc->sc_promisc = 0; sc->sc_p2pmac = 0; ifp->if_hardmtu = ETHER_MAX_HARDMTU_LEN; return (0); } static int macsec_set_vnetid(struct macsec_softc *sc, const struct ifreq *ifr) { struct ifnet *ifp = &sc->sc_if; uint16_t portid; if (ifr->ifr_vnetid < MACSEC_PORTID_MIN || ifr->ifr_vnetid > MACSEC_PORTID_MAX) return (EINVAL); portid = htons(ifr->ifr_vnetid); if (portid == sc->sc_portid) return (0); if (ISSET(ifp->if_flags, IFF_RUNNING)) return (EBUSY); /* commit */ sc->sc_portid = portid; return (0); } static int macsec_get_vnetid(struct macsec_softc *sc, struct ifreq *ifr) { ifr->ifr_vnetid = ntohs(sc->sc_portid); return (0); } static int macsec_set_parent(struct macsec_softc *sc, const struct if_parent *p) { struct ifnet *ifp = &sc->sc_if; struct ifnet *ifp0; ifp0 = ifunit(p->ifp_parent); /* doesn't need an if_put */ if (ifp0 == NULL) return (ENXIO); if (ifp0->if_type != IFT_ETHER) return (ENXIO); if (ifp0->if_index == sc->sc_parent) return (0); if (ISSET(ifp->if_flags, IFF_RUNNING)) return (EBUSY); /* commit */ sc->sc_parent = ifp0->if_index; return (0); } static int macsec_get_parent(struct macsec_softc *sc, struct if_parent *p) { struct ifnet *ifp0; int error = 0; ifp0 = if_get(sc->sc_parent); if (ifp0 == NULL) error = EADDRNOTAVAIL; else memcpy(p->ifp_parent, ifp0->if_xname, sizeof(p->ifp_parent)); if_put(ifp0); return (error); } static int macsec_del_parent(struct macsec_softc *sc) { struct ifnet *ifp = &sc->sc_if; if (ISSET(ifp->if_flags, IFF_RUNNING)) return (EBUSY); /* commit */ sc->sc_parent = 0; return (0); } static struct macsec_softc * macsec_find(struct ifnet *ifp0, const struct macsec_sci *sci) { return (NULL); } struct mbuf * macsec_input(struct ifnet *ifp0, struct mbuf *m) { struct macsec_softc *sc; struct ifnet *ifp; struct ether_header *eh; struct macsec_shim *shim; struct macsec_sci *sci, key; unsigned int hlen = sizeof(*eh) + sizeof(*shim); uint8_t tci; struct mbuf *n; int off; if (m->m_len < hlen) { m = m_pullup(m, hlen); if (m == NULL) { /* macsec short ++ */ return (NULL); } } eh = mtod(m, struct ether_header *); shim = (struct macsec_shim *)(eh + 1); tci = shim->tci_an; /* * If the Encryption (E) bit is set and the Changed Text * (C) bit is clear, the frame is not processed by the SecY * but is reserved for use by the KaY. * * The KaY operates on the Uncontrolled Port, so hand it back up. */ if (ISSET(tci, MACSEC_TCI_E) && !ISSET(tci, MACSEC_TCI_C)) return (m); /* * PDU validation: c) The V bit in the TCI is clear. */ if (ISSET(tci, MACSEC_TCI_V) != MACSEC_TCI_V_0) goto drop; /* * PDU validation: d) If the ES or the SCB bit in the TCI * is set, then the SC bit is clear. */ if (ISSET(tci, MACSEC_TCI_ES | MACSEC_TCI_SCB) && ISSET(tci, MACSEC_TCI_SC)) goto drop; /* * PDU validation: e) Bits 7 and 8 of octet 4 of the SecTAG * are clear. */ if (ISSET(shim->sl, MACSEC_SL_UNUSED)) goto drop; if (ISSET(tci, MACSEC_TCI_SC)) { hlen += sizeof(*sci); if (m->m_len < hlen) { m = m_pullup(m, hlen); if (m == NULL) { /* macsec short ++ */ return (NULL); } eh = mtod(m, struct ether_header *); shim = (struct macsec_shim *)(eh + 1); } sci = (struct macsec_sci *)(shim + 1); } else { CTASSERT(sizeof(key.sysid) == sizeof(eh->ether_shost)); memcpy(key.sysid, eh->ether_shost, sizeof(key.sysid)); key.portid = htons(MACSEC_PORTID_DEFAULT); sci = &key; } sc = macsec_find(ifp0, sci); if (sc == NULL) { /* no interface found */ goto drop; } hlen -= offsetof(struct ether_header, ether_type); memmove(mtod(m, caddr_t) + hlen, mtod(m, caddr_t), offsetof(struct ether_header, ether_type)); m_adj(m, hlen); n = m_getptr(m, sizeof(*eh), &off); if (n == NULL) { /* no data ++ */ goto drop; } if (!ALIGNED_POINTER(mtod(n, caddr_t) + off, uint32_t)) { /* unaligned ++ */ n = m_dup_pkt(m, ETHER_ALIGN, M_NOWAIT); m_freem(m); if (n == NULL) return (NULL); m = n; } m->m_flags &= ~(M_BCAST|M_MCAST); #if NPF > 0 pf_pkt_addr_changed(m); #endif ifp = &sc->sc_if; if_vinput(&sc->sc_if, m); return (NULL); drop: m_freem(m); return (NULL); } static void macsec_detach_hook(void *arg) { struct macsec_softc *sc = arg; struct ifnet *ifp = &sc->sc_if; /* XXX */ if (ISSET(ifp->if_flags, IFF_RUNNING)) { macsec_down(sc); CLR(ifp->if_flags, IFF_UP); } sc->sc_parent = 0; } static void macsec_link_hook(void *arg) { struct macsec_softc *sc = arg; struct ifnet *ifp0; u_char link = LINK_STATE_DOWN; uint64_t baud = 0; ifp0 = if_get(sc->sc_parent); if (ifp0 != NULL) { if (ISSET(ifp0->if_flags, IFF_RUNNING)) link = ifp0->if_link_state; baud = ifp0->if_baudrate; } if_put(ifp0); macsec_link_state(sc, link, baud); } static void macsec_link_state(struct macsec_softc *sc, u_char link, uint64_t baud) { struct ifnet *ifp = &sc->sc_if; if (ifp->if_link_state == link) return; ifp->if_link_state = link; ifp->if_baudrate = baud; if_link_state_change(ifp); } static int macsec_multi(struct ifnet *ifp0, const struct macsec_multiaddr *ma, u_long cmd) { struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); /* make it convincing */ CTASSERT(sizeof(ifr.ifr_name) == sizeof(ifp0->if_xname)); memcpy(ifr.ifr_name, ifp0->if_xname, sizeof(ifr.ifr_name)); ifr.ifr_addr = ma->m_addr; return ((*ifp0->if_ioctl)(ifp0, cmd, (caddr_t)&ifr)); } static int macsec_multi_eq(const struct macsec_multiaddr *ma, const uint8_t *addrlo, const uint8_t *addrhi) { return (ETHER_IS_EQ(ma->m_addrlo, addrlo) && ETHER_IS_EQ(ma->m_addrhi, addrhi)); } static int macsec_multi_add(struct macsec_softc *sc, struct ifreq *ifr) { struct ifnet *ifp = &sc->sc_if; struct macsec_multiaddr *ma; uint8_t addrlo[ETHER_ADDR_LEN]; uint8_t addrhi[ETHER_ADDR_LEN]; int error; error = ether_multiaddr(&ifr->ifr_addr, addrlo, addrhi); if (error != 0) return (error); TAILQ_FOREACH(ma, &sc->sc_multiaddrs, m_entry) { if (macsec_multi_eq(ma, addrlo, addrhi)) { ma->m_refs++; return (0); } } ma = malloc(sizeof(*ma), M_DEVBUF, M_WAITOK|M_CANFAIL); if (ma == NULL) return (ENOMEM); ma->m_refs = 1; ma->m_addr = ifr->ifr_addr; memcpy(ma->m_addrlo, addrlo, sizeof(ma->m_addrlo)); memcpy(ma->m_addrhi, addrhi, sizeof(ma->m_addrhi)); TAILQ_INSERT_TAIL(&sc->sc_multiaddrs, ma, m_entry); if (ISSET(ifp->if_flags, IFF_RUNNING)) { struct ifnet *ifp0 = if_get(sc->sc_parent); if (ifp0 != NULL) error = macsec_multi(ifp0, ma, SIOCADDMULTI); if_put(ifp0); } return (0); } static int macsec_multi_del(struct macsec_softc *sc, struct ifreq *ifr) { struct ifnet *ifp = &sc->sc_if; struct macsec_multiaddr *ma; uint8_t addrlo[ETHER_ADDR_LEN]; uint8_t addrhi[ETHER_ADDR_LEN]; int error; error = ether_multiaddr(&ifr->ifr_addr, addrlo, addrhi); if (error != 0) return (error); TAILQ_FOREACH(ma, &sc->sc_multiaddrs, m_entry) { if (macsec_multi_eq(ma, addrlo, addrhi)) break; } if (ma == NULL) return (EINVAL); if (--ma->m_refs > 0) return (0); TAILQ_REMOVE(&sc->sc_multiaddrs, ma, m_entry); if (ISSET(ifp->if_flags, IFF_RUNNING)) { struct ifnet *ifp0 = if_get(sc->sc_parent); if (ifp0 != NULL) error = macsec_multi(ifp0, ma, SIOCADDMULTI); if_put(ifp0); } free(ma, M_DEVBUF, sizeof(*ma)); return (error); }