the last assignment i set for the operating systems course i was helping with was to write a driver for a virtual network interface that implemented the client side of a protocol i made up.
the protocol was largely inspired by vxlan, but requires some negotiation for a client to get a working link with the concentrator (server). the spec is up at http://www.uq.id.au/dlg/comp3301/assignment3.pdf. i wrote a dodgy server for the students to run their code against, which is up at https://source.eait.uq.edu.au/viewvc/comp3301-pracs/2015/. my implementation of the driver (so i could test the server) is below. Index: conf/GENERIC =================================================================== RCS file: /cvs/src/sys/conf/GENERIC,v retrieving revision 1.220 diff -u -p -r1.220 GENERIC --- conf/GENERIC 10 Aug 2015 20:35:36 -0000 1.220 +++ conf/GENERIC 29 Oct 2015 23:14:30 -0000 @@ -105,6 +105,7 @@ pseudo-device tun # network tunneling o pseudo-device vether # Virtual ethernet pseudo-device vxlan # Virtual extensible LAN pseudo-device vlan # IEEE 802.1Q VLAN +pseudo-device eou pseudo-device bio 1 # ioctl multiplexing device Index: conf/files =================================================================== RCS file: /cvs/src/sys/conf/files,v retrieving revision 1.603 diff -u -p -r1.603 files --- conf/files 28 Sep 2015 08:32:04 -0000 1.603 +++ conf/files 29 Oct 2015 23:14:30 -0000 @@ -541,6 +541,7 @@ pseudo-device trunk: ifnet, ether, ifmed pseudo-device mpe: ifnet, ether pseudo-device mpw: ifnet, ether pseudo-device vether: ifnet, ether +pseudo-device eou: ifnet, ether pseudo-device pppx: ifnet pseudo-device vxlan: ifnet, ether, ifmedia @@ -786,6 +787,7 @@ file net/trunklacp.c trunk file net/if_mpe.c mpe needs-count file net/if_mpw.c mpw & bridge needs-count file net/if_vether.c vether needs-count +file net/if_eou.c eou needs-count file net/if_pppx.c pppx needs-count file net/if_vxlan.c vxlan needs-count file net80211/ieee80211.c wlan Index: net/if_eou.c =================================================================== RCS file: net/if_eou.c diff -N net/if_eou.c --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ net/if_eou.c 29 Oct 2015 23:14:32 -0000 @@ -0,0 +1,781 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2009 Theo de Raadt + * Copyright (c) 2015 David Gwynne <d...@uq.edu.au> + * + * 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/socket.h> +#include <sys/sockio.h> +#include <sys/ioctl.h> +#include <sys/protosw.h> +#include <sys/socketvar.h> +#include <sys/task.h> +#include <sys/timeout.h> + +#include <net/if.h> +#include <net/if_media.h> + +#include <netinet/in.h> +#include <netinet/if_ether.h> + +#include <crypto/siphash.h> + +#include "bpfilter.h" +#if NBPFILTER > 0 +#include <net/bpf.h> +#endif + +/* + * protocol + */ +struct eou_header { + uint32_t eou_network; + uint16_t eou_type; +} __packed; + +#define EOU_T_DATA 0x0000 +#define EOU_T_PING 0x8000 +#define EOU_T_PONG 0x8001 + +struct eou_pingpong { + struct eou_header hdr; + uint16_t _pad; + uint64_t utime; + uint8_t random[32]; + uint8_t mac[8]; +} __packed; + +#define EOU_PORT 3301 + +/* + * driver + */ +struct eou_softc; +TAILQ_HEAD(eou_softcs, eou_softc); + +struct eou_socket { + TAILQ_ENTRY(eou_socket) eso_entry; + struct eou_softcs eso_softcs; + + struct socket *eso_so; + struct sockaddr_storage eso_src; + struct sockaddr_storage eso_dst; +}; + +struct eou_softc { + struct arpcom sc_ac; + struct ifmedia sc_media; + + uint32_t sc_vnetid; /* network byte order */ + + struct eou_socket *sc_eso; + TAILQ_ENTRY(eou_softc) sc_entry; + + struct task sc_send; + struct timeout sc_tick; + struct task sc_ping; + struct timeout sc_dead; +}; + +TAILQ_HEAD(eou_sockets, eou_socket) eou_sockets; +union { + uint8_t bytes[SIPHASH_KEY_LENGTH]; + SIPHASH_KEY key; +} _eou_key = { + .bytes = { + 'c', 'o', 'm', 'p', '3', '3', '0', '1', + 'c', 'o', 'm', 'p', '7', '3', '0', '8' + } +}; +#define eou_key _eou_key.key + +void eouattach(int); + +int eou_create(struct if_clone *, int); +int eou_destroy(struct ifnet *); + +int eou_ioctl(struct ifnet *, u_long, caddr_t); +void eou_start(struct ifnet *); +int eou_media_change(struct ifnet *); +void eou_media_status(struct ifnet *, struct ifmediareq *); + +int eou_can_run(struct eou_softc *); +void eou_run(struct eou_softc *); +void eou_stop(struct eou_softc *); +void eou_bounce(struct eou_softc *); + +int eou_set_tunnel(struct eou_softc *, struct if_laddrreq *); +int eou_get_tunnel(struct eou_softc *, struct if_laddrreq *); +int eou_del_tunnel(struct eou_softc *, struct if_laddrreq *); + +int eou_vnetid_used(struct eou_socket *, uint32_t); +int eou_socreate(struct eou_softc *, const struct sockaddr *, + const struct sockaddr *, struct eou_socket **); +void eou_soclose(struct eou_softc *, struct eou_socket *); + +void eou_upcall(struct socket *, caddr_t, int); +void eou_input(struct eou_socket *, struct mbuf *); + +void eou_send(void *); +void eou_tick(void *); +void eou_ping(void *); +void eou_dead(void *); + +void eou_pong(struct eou_softc *, struct mbuf *); + +struct if_clone eou_cloner = + IF_CLONE_INITIALIZER("eou", eou_create, eou_destroy); + +void +eouattach(int n) +{ + TAILQ_INIT(&eou_sockets); + if_clone_attach(&eou_cloner); +} + +int +eou_create(struct if_clone *ifc, int unit) +{ + struct eou_softc *sc; + struct ifnet *ifp; + + sc = malloc(sizeof(*sc), M_DEVBUF, M_WAITOK|M_ZERO); + + task_set(&sc->sc_send, eou_send, sc); + timeout_set(&sc->sc_tick, eou_tick, sc); + task_set(&sc->sc_ping, eou_ping, sc); + timeout_set(&sc->sc_dead, eou_dead, sc); + + ifp = &sc->sc_ac.ac_if; + snprintf(ifp->if_xname, sizeof(ifp->if_xname), "%s%d", + ifc->ifc_name, unit); + ifp->if_flags = IFF_BROADCAST | IFF_SIMPLEX | IFF_MULTICAST; + ether_fakeaddr(ifp); + + ifp->if_softc = sc; + ifp->if_ioctl = eou_ioctl; + ifp->if_start = eou_start; + IFQ_SET_MAXLEN(&ifp->if_snd, 1); + IFQ_SET_READY(&ifp->if_snd); + + ifp->if_capabilities = IFCAP_VLAN_MTU; + ifp->if_link_state = LINK_STATE_DOWN; + + ifmedia_init(&sc->sc_media, 0, eou_media_change, eou_media_status); + ifmedia_add(&sc->sc_media, IFM_ETHER | IFM_AUTO, 0, NULL); + ifmedia_set(&sc->sc_media, IFM_ETHER | IFM_AUTO); + + if_attach(ifp); + ether_ifattach(ifp); + return (0); +} + +int +eou_destroy(struct ifnet *ifp) +{ + struct eou_softc *sc = ifp->if_softc; + + timeout_del(&sc->sc_tick); + timeout_del(&sc->sc_dead); + + if (sc->sc_eso != NULL) + eou_soclose(sc, sc->sc_eso); + + ifmedia_delete_instance(&sc->sc_media, IFM_INST_ANY); + ether_ifdetach(ifp); + if_detach(ifp); + free(sc, M_DEVBUF, sizeof(*sc)); + return (0); +} + +void +eou_upcall(struct socket *so, caddr_t arg, int waitflag) +{ + struct eou_socket *eso = (struct eou_socket *)arg; + struct mbuf *m; + struct uio uio; + int flags; + int error; + + KASSERT(so == eso->eso_so); + + do { + uio.uio_resid = 1000000000; + flags = MSG_DONTWAIT; + error = soreceive(so, NULL, &uio, &m, NULL, &flags, 0); + if (m != NULL) + eou_input(eso, m); + + if (error != EWOULDBLOCK && + ISSET(so->so_proto->pr_flags, PR_CONNREQUIRED)) { + /* link down? */ + break; + } + } while (m != NULL); +} + +void +eou_input(struct eou_socket *eso, struct mbuf *m) +{ + struct eou_softc *sc; + struct ifnet *ifp; + struct mbuf *mp; + struct eou_header *eouh; + int offp; + struct mbuf_list ml; + + mp = m_pulldown(m, 0, sizeof(*eouh), &offp); + if (mp == NULL) + return; + + eouh = (struct eou_header *)(mp->m_data + offp); + TAILQ_FOREACH(sc, &eso->eso_softcs, sc_entry) { + if (eouh->eou_network == sc->sc_vnetid) + break; + } + if (sc == NULL) + goto done; + + switch (bemtoh16(&eouh->eou_type)) { + case EOU_T_DATA: + ifp = &sc->sc_ac.ac_if; + if (!LINK_STATE_IS_UP(ifp->if_link_state)) + goto done; + + m_adj(m, sizeof(*eouh)); + ml_init(&ml); + ml_enqueue(&ml, m); + if_input(ifp, &ml); + break; + + case EOU_T_PONG: + eou_pong(sc, m); + break; + default: + m_freem(m); + break; + } + + return; +done: + m_freem(m); +} + +/* ARGSUSED */ +int +eou_ioctl(struct ifnet *ifp, u_long cmd, caddr_t data) +{ + struct eou_softc *sc = (struct eou_softc *)ifp->if_softc; + struct ifaddr *ifa = (struct ifaddr *)data; + struct ifreq *ifr = (struct ifreq *)data; + uint32_t vnetid; + int error = 0; + + switch (cmd) { + case SIOCSIFADDR: + ifp->if_flags |= IFF_UP; + if (ifa->ifa_addr->sa_family == AF_INET) + arp_ifinit(&sc->sc_ac, ifa); + /* FALLTHROUGH */ + + case SIOCSIFFLAGS: + if (eou_can_run(sc)) + eou_run(sc); + else + eou_stop(sc); + break; + + case SIOCADDMULTI: + case SIOCDELMULTI: + break; + + case SIOCGIFMEDIA: + case SIOCSIFMEDIA: + error = ifmedia_ioctl(ifp, ifr, &sc->sc_media, cmd); + break; + + case SIOCSLIFPHYADDR: + error = eou_set_tunnel(sc, (struct if_laddrreq *)data); + break; + case SIOCGLIFPHYADDR: + error = eou_get_tunnel(sc, (struct if_laddrreq *)data); + break; + case SIOCDIFPHYADDR: + error = eou_del_tunnel(sc, (struct if_laddrreq *)data); + break; + + case SIOCSVNETID: + if ((error = suser(curproc, 0)) != 0) + break; + + vnetid = htobe32(ifr->ifr_vnetid); + if (sc->sc_vnetid == vnetid) + break; + + if (sc->sc_eso != NULL && + eou_vnetid_used(sc->sc_eso, vnetid)) { + error = EINVAL; + break; + } + + sc->sc_vnetid = vnetid; + if (ISSET(ifp->if_flags, IFF_RUNNING)) + eou_bounce(sc); + break; + + case SIOCGVNETID: + ifr->ifr_vnetid = betoh32(sc->sc_vnetid); + break; + + default: + error = ether_ioctl(ifp, &sc->sc_ac, cmd, data); + } + + return (error); +} + +int +eou_can_run(struct eou_softc *sc) +{ + struct ifnet *ifp = &sc->sc_ac.ac_if; + + return (ISSET(ifp->if_flags, IFF_UP) && sc->sc_eso != NULL); +} + +void +eou_bounce(struct eou_softc *sc) +{ + eou_stop(sc); + eou_run(sc); +} + +void +eou_run(struct eou_softc *sc) +{ + struct ifnet *ifp = &sc->sc_ac.ac_if; + + if (ISSET(ifp->if_flags, IFF_RUNNING)) + return; + + SET(ifp->if_flags, IFF_RUNNING); + + eou_tick(sc); +} + +void +eou_stop(struct eou_softc *sc) +{ + struct ifnet *ifp = &sc->sc_ac.ac_if; + + if (!ISSET(ifp->if_flags, IFF_RUNNING)) + return; + + if (LINK_STATE_IS_UP(ifp->if_link_state)) { + ifp->if_link_state = LINK_STATE_DOWN; + if_link_state_change(ifp); + } + + timeout_del(&sc->sc_tick); + timeout_del(&sc->sc_dead); + + CLR(ifp->if_flags, IFF_RUNNING); +} + +int +eou_set_tunnel(struct eou_softc *sc, struct if_laddrreq *req) +{ + struct eou_socket *eso, *oeso; + struct sockaddr *src = (struct sockaddr *)&req->addr; + struct sockaddr *dst = (struct sockaddr *)&req->dstaddr; + struct sockaddr_in *sin; + struct sockaddr_in6 *sin6; + int error; + + /* sa_family and sa_len must be equal */ + if (src->sa_family != dst->sa_family || src->sa_len != dst->sa_len) + return (EINVAL); + + /* validate */ + switch (dst->sa_family) { + case AF_INET: + if (dst->sa_len != sizeof(*sin)) + return (EINVAL); + + sin = (struct sockaddr_in *)src; + if (in_nullhost(sin->sin_addr)) + return (EINVAL); + + sin = (struct sockaddr_in *)dst; + if (in_nullhost(sin->sin_addr)) + return (EINVAL); + if (sin->sin_port == htons(0)) + sin->sin_port = htons(EOU_PORT); + break; +#ifdef INET6 + case AF_INET6: + if (dst->sa_len != sizeof(*sin6)) + return (EINVAL); + + sin6 = (struct sockaddr_in6 *)src; + if (IN6_IS_ADDR_UNSPECIFIED(&sin6->sin6_addr)) + return (EINVAL); + + sin6 = (struct sockaddr_in6 *)dst; + if (IN6_IS_ADDR_UNSPECIFIED(&sin6->sin6_addr)) + return (EINVAL); + if (sin6->sin6_port == htons(0)) + sin6->sin6_port = htons(EOU_PORT); + break; +#endif + default: + return (EAFNOSUPPORT); + } + + oeso = sc->sc_eso; + if (oeso != NULL) { + if (memcmp(&req->addr, &oeso->eso_src, + oeso->eso_src.ss_len) == 0 && + memcmp(&req->dstaddr, &oeso->eso_dst, + oeso->eso_dst.ss_len) == 0) + return (0); + + eou_soclose(sc, oeso); + } + + /* let's go */ + error = eou_socreate(sc, src, dst, &eso); + if (error != 0) + return (error); + + /* commit */ + sc->sc_eso = eso; + + if (ISSET(sc->sc_ac.ac_if.if_flags, IFF_RUNNING)) + eou_bounce(sc); + + return (0); +} + +int +eou_vnetid_used(struct eou_socket *eso, uint32_t vnetid) +{ + struct eou_softc *sc; + + TAILQ_FOREACH(sc, &eso->eso_softcs, sc_entry) { + if (sc->sc_vnetid == vnetid) + return (1); + } + + return (0); +} + +int +eou_socreate(struct eou_softc *sc, const struct sockaddr *src, + const struct sockaddr *dst, struct eou_socket **esop) +{ + struct eou_socket *eso; + struct socket *so; + struct sockaddr *nam; + struct mbuf *m; + int error; + int s; + + TAILQ_FOREACH(eso, &eou_sockets, eso_entry) { + if (memcmp(src, &eso->eso_src, eso->eso_src.ss_len) == 0 && + memcmp(dst, &eso->eso_dst, eso->eso_dst.ss_len) == 0) { + if (eou_vnetid_used(eso, sc->sc_vnetid)) + return (EINVAL); + + goto done; + } + } + + eso = malloc(sizeof(*eso), M_IFADDR, M_WAITOK|M_ZERO); + + error = socreate(dst->sa_family, &so, SOCK_DGRAM, 0); + if (error != 0) + goto free; + + MGET(m, M_WAIT, MT_SONAME); + m->m_len = src->sa_len; + nam = mtod(m, struct sockaddr *); + + memcpy(nam, src, src->sa_len); + error = sobind(so, m, curproc); + if (error) + goto close; + + memcpy(nam, dst, dst->sa_len); + + s = splsoftnet(); + error = soconnect(so, m); + if (error != 0) + goto splx; + + while (ISSET(so->so_state, SS_ISCONNECTING) && so->so_error == 0) { + error = tsleep(&so->so_timeo, PSOCK | PCATCH, "eoucon", 0); + if (error != 0) + goto splx; + } + splx(s); + + error = so->so_error; + if (error != 0) + goto close; + + m_freem(m); + + so->so_upcall = eou_upcall; + so->so_upcallarg = (caddr_t)eso; + + eso->eso_so = so; + memcpy(&eso->eso_src, src, src->sa_len); + memcpy(&eso->eso_dst, dst, dst->sa_len); + TAILQ_INIT(&eso->eso_softcs); + + TAILQ_INSERT_TAIL(&eou_sockets, eso, eso_entry); + +done: + TAILQ_INSERT_TAIL(&eso->eso_softcs, sc, sc_entry); + *esop = eso; + return (0); + +splx: + splx(s); +close: + m_freem(m); + soclose(so); +free: + free(eso, M_IFADDR, sizeof(*eso)); + return (error); +} + +static inline int +eou_sosend(struct eou_softc *sc, struct mbuf *m) +{ + KASSERT(sc->sc_eso); + + return (sosend(sc->sc_eso->eso_so, NULL, NULL, m, NULL, MSG_DONTWAIT)); +} + +void +eou_soclose(struct eou_softc *sc, struct eou_socket *eso) +{ + TAILQ_REMOVE(&eso->eso_softcs, sc, sc_entry); + if (!TAILQ_EMPTY(&eso->eso_softcs)) + return; + + soclose(eso->eso_so); + TAILQ_REMOVE(&eou_sockets, eso, eso_entry); + free(eso, M_IFADDR, sizeof(*eso)); +} + +int +eou_get_tunnel(struct eou_softc *sc, struct if_laddrreq *req) +{ + struct eou_socket *eso; + + eso = sc->sc_eso; + if (eso == NULL) + return (EADDRNOTAVAIL); + + KASSERT(sizeof(req->addr) == sizeof(eso->eso_src)); + KASSERT(sizeof(req->dstaddr) == sizeof(eso->eso_dst)); + + memcpy(&req->addr, &eso->eso_src, sizeof(req->addr)); + memcpy(&req->dstaddr, &eso->eso_dst, sizeof(req->dstaddr)); + + return (0); +} + +int +eou_del_tunnel(struct eou_softc *sc, struct if_laddrreq *req) +{ + struct ifnet *ifp = &sc->sc_ac.ac_if; + struct eou_socket *eso; + + eso = sc->sc_eso; + if (eso == NULL) + return (EADDRNOTAVAIL); + + eou_soclose(sc, eso); + sc->sc_eso = NULL; + + if (ISSET(ifp->if_flags, IFF_UP)) { + CLR(ifp->if_flags, IFF_RUNNING); + ifp->if_link_state = LINK_STATE_DOWN; + if_link_state_change(ifp); + } + + return (0); +} + +int +eou_media_change(struct ifnet *ifp) +{ + return (0); +} + +void +eou_media_status(struct ifnet *ifp, struct ifmediareq *imr) +{ + if (LINK_STATE_IS_UP(ifp->if_link_state)) { + imr->ifm_status = IFM_AVALID | IFM_ACTIVE; + imr->ifm_active = IFM_ETHER | IFM_AUTO | IFM_FDX; + } else { + imr->ifm_status = IFM_AVALID; + imr->ifm_active = IFM_ETHER | IFM_AUTO; + } +} + +void +eou_mac(const SIPHASH_KEY *key, const struct eou_pingpong *p, void *digest) +{ + SIPHASH_CTX ctx; + + SipHash24_Init(&ctx, key); + SipHash24_Update(&ctx, &p->hdr.eou_network, sizeof(p->hdr.eou_network)); + SipHash24_Update(&ctx, &p->utime, sizeof(p->utime)); + SipHash24_Update(&ctx, &p->random, sizeof(p->random)); + SipHash24_Final(digest, &ctx); +} + +void +eou_tick(void *x) +{ + struct eou_softc *sc = x; + task_add(systq, &sc->sc_ping); + timeout_add_sec(&sc->sc_tick, 30); +} + +void +eou_start(struct ifnet *ifp) +{ + struct eou_softc *sc = ifp->if_softc; + + task_add(systq, &sc->sc_send); +} + +void +eou_send(void *scp) +{ + struct eou_softc *sc = scp; + struct ifnet *ifp = &sc->sc_ac.ac_if; + struct eou_header eouh; + struct mbuf *m; + + eouh.eou_network = sc->sc_vnetid; + eouh.eou_type = htobe16(EOU_T_DATA); + + for (;;) { + IFQ_DEQUEUE(&ifp->if_snd, m); + if (m == NULL) + return; + +#if NBPFILTER > 0 + if (ifp->if_bpf) + bpf_mtap(ifp->if_bpf, m, BPF_DIRECTION_OUT); +#endif /* NBPFILTER > 0 */ + + m = m_prepend(m, sizeof(eouh), M_WAITOK); + if (m == NULL) { + ifp->if_oerrors++; + continue; + } + + memcpy(mtod(m, void *), &eouh, sizeof(eouh)); + + if (eou_sosend(sc, m) != 0) { + ifp->if_oerrors++; + continue; + } + + ifp->if_opackets++; + } +} + +void +eou_ping(void *x) +{ + struct eou_softc *sc = x; + struct mbuf *m; + struct eou_pingpong *ping; + + MGETHDR(m, M_WAIT, MT_DATA); + if (max_linkhdr + max_protohdr + sizeof(*ping) > MHLEN) + MCLGET(m, M_WAIT); + + m->m_data += max_linkhdr + max_protohdr; + m->m_len = m->m_pkthdr.len = sizeof(*ping); + + ping = mtod(m, struct eou_pingpong *); + ping->hdr.eou_network = sc->sc_vnetid; + htobem16(&ping->hdr.eou_type, EOU_T_PING); + ping->_pad = 0; + htobem64(&ping->utime, time_second); + arc4random_buf(ping->random, sizeof(ping->random)); + eou_mac(&eou_key, ping, ping->mac); + + eou_sosend(sc, m); +} + +void +eou_pong(struct eou_softc *sc, struct mbuf *m) +{ + struct ifnet *ifp = &sc->sc_ac.ac_if; + struct mbuf *mp; + struct eou_pingpong *pong; + int offp; + uint8_t mac[8]; + uint64_t utime; + + mp = m_pulldown(m, 0, sizeof(*pong), &offp); + if (mp == NULL) + return; + + pong = (struct eou_pingpong *)(mp->m_data + offp); + eou_mac(&eou_key, pong, mac); + if (memcmp(mac, pong->mac, sizeof(mac)) != 0) + goto done; + + utime = bemtoh64(&pong->utime); + if (utime > time_second + 30 || utime < time_second - 30) + goto done; + + /* validation succeeded */ + if (!LINK_STATE_IS_UP(ifp->if_link_state)) { + ifp->if_link_state = LINK_STATE_UP; + if_link_state_change(ifp); + } + + timeout_add_sec(&sc->sc_dead, 100); + +done: + m_freem(m); +} + +void +eou_dead(void *x) +{ + struct eou_softc *sc = x; + struct ifnet *ifp = &sc->sc_ac.ac_if; + + ifp->if_link_state = LINK_STATE_DOWN; + if_link_state_change(ifp); +}