From: dlg Date: Sun, 21 Feb 2021 03:26:46 +0000 (+0000) Subject: add etherbridge, the guts of a learning bridge that can be reused. X-Git-Url: http://artulab.com/gitweb/?a=commitdiff_plain;h=5a88a7345428e9da5a05349249d21b1afb03f65c;p=openbsd add etherbridge, the guts of a learning bridge that can be reused. this allows for the factoring out of the learning bridge code i wrote in bpe and nvme, and should be reusable for other drivers needing a mac learning bridge. the core data structures are an etherbridge struct to represent the learning bridge, eb_entry structs for each mac address entry that the bridge knows about, and an etherbridge_ops struct that drivers fill in so that they can use this code. eb_entry structs are stored in a hash table made up of SMR_TAILQs to support lookups of entries quickly and concurrently in the forwarding path. they are also stored in a locked red-black tree to help manage the uniqueness of the mac address in the table. the etherbridge_ops handlers mostly deal with comparing and testing the "ports" associated with mac address table entries. the "port" that a mac address entry is associated with is opaque to the etherbridge code, which allows for this code to be used by nvgre and bpe which map mac addresses inside the bridge to addresses in their underlay networks. it also supports traditional bridges where "ports" are actual interfaces. ok patrick@ jmatthew@ --- diff --git a/sys/conf/files b/sys/conf/files index 836b8a61321..efa0609f4de 100644 --- a/sys/conf/files +++ b/sys/conf/files @@ -1,4 +1,4 @@ -# $OpenBSD: files,v 1.694 2021/02/08 11:20:03 stsp Exp $ +# $OpenBSD: files,v 1.695 2021/02/21 03:26:46 dlg Exp $ # $NetBSD: files,v 1.87 1996/05/19 17:17:50 jonathan Exp $ # @(#)files.newconf 7.5 (Berkeley) 5/10/93 @@ -13,6 +13,7 @@ define audio {} define scsi {} define atascsi {} define ifmedia +define etherbridge define mii {[phy = -1]} define midibus {} define radiobus {} @@ -813,6 +814,7 @@ file net/if_tun.c tun needs-count file net/if_bridge.c bridge needs-count file net/bridgectl.c bridge file net/bridgestp.c bridge +file net/if_etherbridge.c etherbridge file net/if_vlan.c vlan needs-count file net/if_switch.c switch needs-count file net/switchctl.c switch @@ -841,7 +843,7 @@ file net/if_wg.c wg file net/wg_noise.c wg file net/wg_cookie.c wg file net/bfd.c bfd -file net/toeplitz.c stoeplitz needs-flag +file net/toeplitz.c stoeplitz | etherbridge needs-flag file net80211/ieee80211.c wlan file net80211/ieee80211_amrr.c wlan file net80211/ieee80211_crypto.c wlan diff --git a/sys/net/if_etherbridge.c b/sys/net/if_etherbridge.c new file mode 100644 index 00000000000..50a4255c610 --- /dev/null +++ b/sys/net/if_etherbridge.c @@ -0,0 +1,586 @@ +/* $OpenBSD: if_etherbridge.c,v 1.1 2021/02/21 03:26:46 dlg Exp $ */ + +/* + * Copyright (c) 2018, 2021 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 +#include + +/* for bridge stuff */ +#include + +#include + +static inline void ebe_take(struct eb_entry *); +static inline void ebe_rele(struct eb_entry *); +static void ebe_free(void *); + +static void etherbridge_age(void *); + +RBT_PROTOTYPE(eb_tree, eb_entry, ebe_tentry, ebt_cmp); + +static struct pool eb_entry_pool; + +static inline int +eb_port_eq(struct etherbridge *eb, void *a, void *b) +{ + return ((*eb->eb_ops->eb_op_port_eq)(eb->eb_cookie, a, b)); +} + +static inline void * +eb_port_take(struct etherbridge *eb, void *port) +{ + return ((*eb->eb_ops->eb_op_port_take)(eb->eb_cookie, port)); +} + +static inline void +eb_port_rele(struct etherbridge *eb, void *port) +{ + return ((*eb->eb_ops->eb_op_port_rele)(eb->eb_cookie, port)); +} + +static inline size_t +eb_port_ifname(struct etherbridge *eb, char *dst, size_t len, void *port) +{ + return ((*eb->eb_ops->eb_op_port_ifname)(eb->eb_cookie, dst, len, + port)); +} + +static inline void +eb_port_sa(struct etherbridge *eb, struct sockaddr_storage *ss, void *port) +{ + (*eb->eb_ops->eb_op_port_sa)(eb->eb_cookie, ss, port); +} + +int +etherbridge_init(struct etherbridge *eb, const char *name, + const struct etherbridge_ops *ops, void *cookie) +{ + size_t i; + + if (eb_entry_pool.pr_size == 0) { + pool_init(&eb_entry_pool, sizeof(struct eb_entry), + 0, IPL_SOFTNET, 0, "ebepl", NULL); + } + + eb->eb_table = mallocarray(ETHERBRIDGE_TABLE_SIZE, + sizeof(*eb->eb_table), M_DEVBUF, M_WAITOK|M_CANFAIL); + if (eb->eb_table == NULL) + return (ENOMEM); + + eb->eb_name = name; + eb->eb_ops = ops; + eb->eb_cookie = cookie; + + mtx_init(&eb->eb_lock, IPL_SOFTNET); + RBT_INIT(eb_tree, &eb->eb_tree); + + eb->eb_num = 0; + eb->eb_max = 100; + eb->eb_max_age = 240; + timeout_set(&eb->eb_tmo_age, etherbridge_age, eb); + + for (i = 0; i < ETHERBRIDGE_TABLE_SIZE; i++) { + struct eb_list *ebl = &eb->eb_table[i]; + SMR_TAILQ_INIT(ebl); + } + + return (0); +} + +int +etherbridge_up(struct etherbridge *eb) +{ + etherbridge_age(eb); + + return (0); +} + +int +etherbridge_down(struct etherbridge *eb) +{ + smr_barrier(); + + return (0); +} + +void +etherbridge_destroy(struct etherbridge *eb) +{ + struct eb_entry *ebe, *nebe; + + /* XXX assume that nothing will calling etherbridge_map now */ + + timeout_del_barrier(&eb->eb_tmo_age); + + free(eb->eb_table, M_DEVBUF, + ETHERBRIDGE_TABLE_SIZE * sizeof(*eb->eb_table)); + + RBT_FOREACH_SAFE(ebe, eb_tree, &eb->eb_tree, nebe) { + RBT_REMOVE(eb_tree, &eb->eb_tree, ebe); + ebe_free(ebe); + } +} + +static struct eb_list * +etherbridge_list(struct etherbridge *eb, const struct ether_addr *ea) +{ + uint16_t hash = stoeplitz_eaddr(ea->ether_addr_octet); + hash &= ETHERBRIDGE_TABLE_MASK; + return (&eb->eb_table[hash]); +} + +static struct eb_entry * +ebl_find(struct eb_list *ebl, const struct ether_addr *ea) +{ + struct eb_entry *ebe; + + SMR_TAILQ_FOREACH(ebe, ebl, ebe_lentry) { + if (ETHER_IS_EQ(ea, &ebe->ebe_addr)) + return (ebe); + } + + return (NULL); +} + +static inline void +ebl_insert(struct eb_list *ebl, struct eb_entry *ebe) +{ + SMR_TAILQ_INSERT_TAIL_LOCKED(ebl, ebe, ebe_lentry); +} + +static inline void +ebl_remove(struct eb_list *ebl, struct eb_entry *ebe) +{ + SMR_TAILQ_REMOVE_LOCKED(ebl, ebe, ebe_lentry); +} + +static inline int +ebt_cmp(const struct eb_entry *aebe, const struct eb_entry *bebe) +{ + return (memcmp(&aebe->ebe_addr, &bebe->ebe_addr, + sizeof(aebe->ebe_addr))); +} + +RBT_GENERATE(eb_tree, eb_entry, ebe_tentry, ebt_cmp); + +static inline struct eb_entry * +ebt_insert(struct etherbridge *eb, struct eb_entry *ebe) +{ + return (RBT_INSERT(eb_tree, &eb->eb_tree, ebe)); +} + +static inline void +ebt_replace(struct etherbridge *eb, struct eb_entry *oebe, + struct eb_entry *nebe) +{ + struct eb_entry *rvebe; + + RBT_REMOVE(eb_tree, &eb->eb_tree, oebe); + rvebe = RBT_INSERT(eb_tree, &eb->eb_tree, nebe); + KASSERTMSG(rvebe == NULL, "ebt_replace eb %p nebe %p rvebe %p", + eb, nebe, rvebe); +} + +static inline void +ebt_remove(struct etherbridge *eb, struct eb_entry *ebe) +{ + RBT_REMOVE(eb_tree, &eb->eb_tree, ebe); +} + +static inline void +ebe_take(struct eb_entry *ebe) +{ + refcnt_take(&ebe->ebe_refs); +} + +static void +ebe_rele(struct eb_entry *ebe) +{ + if (refcnt_rele(&ebe->ebe_refs)) + smr_call(&ebe->ebe_smr_entry, ebe_free, ebe); +} + +static void +ebe_free(void *arg) +{ + struct eb_entry *ebe = arg; + struct etherbridge *eb = ebe->ebe_etherbridge; + + eb_port_rele(eb, ebe->ebe_port); + pool_put(&eb_entry_pool, ebe); +} + +void * +etherbridge_resolve(struct etherbridge *eb, const struct ether_addr *ea) +{ + struct eb_list *ebl = etherbridge_list(eb, ea); + struct eb_entry *ebe; + + SMR_ASSERT_CRITICAL(); + + ebe = ebl_find(ebl, ea); + if (ebe != NULL) { + if (ebe->ebe_type == EBE_DYNAMIC) { + int diff = getuptime() - ebe->ebe_age; + if (diff > eb->eb_max_age) + return (NULL); + } + + return (ebe->ebe_port); + } + + return (NULL); +} + +void +etherbridge_map(struct etherbridge *eb, void *port, + const struct ether_addr *ea) +{ + struct eb_list *ebl; + struct eb_entry *oebe, *nebe; + unsigned int num; + void *nport; + int new = 0; + + if (ETHER_IS_MULTICAST(ea->ether_addr_octet) || + ETHER_IS_EQ(ea->ether_addr_octet, etheranyaddr)) + return; + + ebl = etherbridge_list(eb, ea); + + smr_read_enter(); + oebe = ebl_find(ebl, ea); + if (oebe == NULL) + new = 1; + else { + oebe->ebe_age = getuptime(); + + /* does this entry need to be replaced? */ + if (oebe->ebe_type == EBE_DYNAMIC && + !eb_port_eq(eb, oebe->ebe_port, port)) { + new = 1; + ebe_take(oebe); + } else + oebe = NULL; + } + smr_read_leave(); + + if (!new) + return; + + nport = eb_port_take(eb, port); + if (nport == NULL) { + /* XXX should we remove the old one and flood? */ + return; + } + + nebe = pool_get(&eb_entry_pool, PR_NOWAIT); + if (nebe == NULL) { + /* XXX should we remove the old one and flood? */ + eb_port_rele(eb, nport); + return; + } + + smr_init(&nebe->ebe_smr_entry); + refcnt_init(&nebe->ebe_refs); + nebe->ebe_etherbridge = eb; + + nebe->ebe_addr = *ea; + nebe->ebe_port = nport; + nebe->ebe_type = EBE_DYNAMIC; + nebe->ebe_age = getuptime(); + + mtx_enter(&eb->eb_lock); + num = eb->eb_num + (oebe == NULL); + if (num <= eb->eb_max && ebt_insert(eb, nebe) == oebe) { + /* we won, do the update */ + ebl_insert(ebl, nebe); + + if (oebe != NULL) { + ebl_remove(ebl, oebe); + ebt_replace(eb, oebe, nebe); + + /* take the table reference away */ + if (refcnt_rele(&oebe->ebe_refs)) { + panic("%s: eb %p oebe %p refcnt", + __func__, eb, oebe); + } + } + + nebe = NULL; + eb->eb_num = num; + } + mtx_leave(&eb->eb_lock); + + if (nebe != NULL) { + /* + * the new entry didnt make it into the + * table, so it can be freed directly. + */ + ebe_free(nebe); + } + + if (oebe != NULL) { + /* + * the old entry could be referenced in + * multiple places, including an smr read + * section, so release it properly. + */ + ebe_rele(oebe); + } +} + +static void +etherbridge_age(void *arg) +{ + struct etherbridge *eb = arg; + struct eb_entry *ebe, *nebe; + struct eb_queue ebq = TAILQ_HEAD_INITIALIZER(ebq); + int diff; + unsigned int now = getuptime(); + size_t i; + + timeout_add_sec(&eb->eb_tmo_age, 100); + + for (i = 0; i < ETHERBRIDGE_TABLE_SIZE; i++) { + struct eb_list *ebl = &eb->eb_table[i]; +#if 0 + if (SMR_TAILQ_EMPTY(ebl)); + continue; +#endif + + mtx_enter(&eb->eb_lock); /* don't block map too much */ + SMR_TAILQ_FOREACH_SAFE_LOCKED(ebe, ebl, ebe_lentry, nebe) { + if (ebe->ebe_type != EBE_DYNAMIC) + continue; + + diff = now - ebe->ebe_age; + if (diff < eb->eb_max_age) + continue; + + ebl_remove(ebl, ebe); + ebt_remove(eb, ebe); + eb->eb_num--; + + /* we own the tables ref now */ + + TAILQ_INSERT_TAIL(&ebq, ebe, ebe_qentry); + } + mtx_leave(&eb->eb_lock); + } + + TAILQ_FOREACH_SAFE(ebe, &ebq, ebe_qentry, nebe) { + TAILQ_REMOVE(&ebq, ebe, ebe_qentry); + ebe_rele(ebe); + } +} + +void +etherbridge_detach_port(struct etherbridge *eb, void *port) +{ + struct eb_entry *ebe, *nebe; + struct eb_queue ebq = TAILQ_HEAD_INITIALIZER(ebq); + size_t i; + + for (i = 0; i < ETHERBRIDGE_TABLE_SIZE; i++) { + struct eb_list *ebl = &eb->eb_table[i]; + + mtx_enter(&eb->eb_lock); /* don't block map too much */ + SMR_TAILQ_FOREACH_SAFE_LOCKED(ebe, ebl, ebe_lentry, nebe) { + if (!eb_port_eq(eb, ebe->ebe_port, port)) + continue; + + ebl_remove(ebl, ebe); + ebt_remove(eb, ebe); + eb->eb_num--; + + /* we own the tables ref now */ + + TAILQ_INSERT_TAIL(&ebq, ebe, ebe_qentry); + } + mtx_leave(&eb->eb_lock); + } + + smr_barrier(); /* try and do it once for all the entries */ + + TAILQ_FOREACH_SAFE(ebe, &ebq, ebe_qentry, nebe) { + TAILQ_REMOVE(&ebq, ebe, ebe_qentry); + if (refcnt_rele(&ebe->ebe_refs)) + ebe_free(ebe); + } +} + +void +etherbridge_flush(struct etherbridge *eb, uint32_t flags) +{ + struct eb_entry *ebe, *nebe; + struct eb_queue ebq = TAILQ_HEAD_INITIALIZER(ebq); + size_t i; + + for (i = 0; i < ETHERBRIDGE_TABLE_SIZE; i++) { + struct eb_list *ebl = &eb->eb_table[i]; + + mtx_enter(&eb->eb_lock); /* don't block map too much */ + SMR_TAILQ_FOREACH_SAFE_LOCKED(ebe, ebl, ebe_lentry, nebe) { + if (flags == IFBF_FLUSHDYN && + ebe->ebe_type != EBE_DYNAMIC) + continue; + + ebl_remove(ebl, ebe); + ebt_remove(eb, ebe); + eb->eb_num--; + + /* we own the tables ref now */ + + TAILQ_INSERT_TAIL(&ebq, ebe, ebe_qentry); + } + mtx_leave(&eb->eb_lock); + } + + smr_barrier(); /* try and do it once for all the entries */ + + TAILQ_FOREACH_SAFE(ebe, &ebq, ebe_qentry, nebe) { + TAILQ_REMOVE(&ebq, ebe, ebe_qentry); + if (refcnt_rele(&ebe->ebe_refs)) + ebe_free(ebe); + } +} + +int +etherbridge_rtfind(struct etherbridge *eb, struct ifbaconf *baconf) +{ + struct eb_entry *ebe; + struct ifbareq bareq; + caddr_t buf; + size_t len, nlen; + time_t age, now = getuptime(); + int error; + + if (baconf->ifbac_len == 0) { + /* single read is atomic */ + baconf->ifbac_len = eb->eb_num * sizeof(bareq); + return (0); + } + + buf = malloc(baconf->ifbac_len, M_TEMP, M_WAITOK|M_CANFAIL); + if (buf == NULL) + return (ENOMEM); + len = 0; + + mtx_enter(&eb->eb_lock); + RBT_FOREACH(ebe, eb_tree, &eb->eb_tree) { + nlen = len + sizeof(bareq); + if (nlen > baconf->ifbac_len) + break; + + strlcpy(bareq.ifba_name, eb->eb_name, + sizeof(bareq.ifba_name)); + eb_port_ifname(eb, + bareq.ifba_ifsname, sizeof(bareq.ifba_ifsname), + ebe->ebe_port); + memcpy(&bareq.ifba_dst, &ebe->ebe_addr, + sizeof(bareq.ifba_dst)); + + memset(&bareq.ifba_dstsa, 0, sizeof(bareq.ifba_dstsa)); + eb_port_sa(eb, &bareq.ifba_dstsa, ebe->ebe_port); + + switch (ebe->ebe_type) { + case EBE_DYNAMIC: + age = now - ebe->ebe_age; + bareq.ifba_age = MIN(age, 0xff); + bareq.ifba_flags = IFBAF_DYNAMIC; + break; + case EBE_STATIC: + bareq.ifba_age = 0; + bareq.ifba_flags = IFBAF_STATIC; + break; + } + + memcpy(buf + len, &bareq, sizeof(bareq)); + len = nlen; + } + nlen = baconf->ifbac_len; + baconf->ifbac_len = eb->eb_num * sizeof(bareq); + mtx_leave(&eb->eb_lock); + + error = copyout(buf, baconf->ifbac_buf, len); + free(buf, M_TEMP, nlen); + + return (error); +} + +int +etherbridge_set_max(struct etherbridge *eb, struct ifbrparam *bparam) +{ + if (bparam->ifbrp_csize < 1 || + bparam->ifbrp_csize > 4096) /* XXX */ + return (EINVAL); + + /* commit */ + eb->eb_max = bparam->ifbrp_csize; + + return (0); +} + +int +etherbridge_get_max(struct etherbridge *eb, struct ifbrparam *bparam) +{ + bparam->ifbrp_csize = eb->eb_max; + + return (0); +} + +int +etherbridge_set_tmo(struct etherbridge *eb, struct ifbrparam *bparam) +{ + if (bparam->ifbrp_ctime < 8 || + bparam->ifbrp_ctime > 3600) + return (EINVAL); + + /* commit */ + eb->eb_max_age = bparam->ifbrp_ctime; + + return (0); +} + +int +etherbridge_get_tmo(struct etherbridge *eb, struct ifbrparam *bparam) +{ + bparam->ifbrp_ctime = eb->eb_max_age; + + return (0); +} diff --git a/sys/net/if_etherbridge.h b/sys/net/if_etherbridge.h new file mode 100644 index 00000000000..6c5b5ef9a21 --- /dev/null +++ b/sys/net/if_etherbridge.h @@ -0,0 +1,103 @@ +/* $OpenBSD: if_etherbridge.h,v 1.1 2021/02/21 03:26:46 dlg Exp $ */ + +/* + * Copyright (c) 2018, 2021 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. + */ + +#ifndef _NET_ETHERBRIDGE_H_ +#define _NET_ETHERBRIDGE_H_ + +#define ETHERBRIDGE_TABLE_BITS 8 +#define ETHERBRIDGE_TABLE_SIZE (1U << ETHERBRIDGE_TABLE_BITS) +#define ETHERBRIDGE_TABLE_MASK (ETHERBRIDGE_TABLE_SIZE - 1) + +struct etherbridge_ops { + int (*eb_op_port_eq)(void *, void *, void *); + void *(*eb_op_port_take)(void *, void *); + void (*eb_op_port_rele)(void *, void *); + size_t (*eb_op_port_ifname)(void *, char *, size_t, void *); + void (*eb_op_port_sa)(void *, struct sockaddr_storage *, void *); +}; + +struct etherbridge; + +struct eb_entry { + SMR_TAILQ_ENTRY(eb_entry) ebe_lentry; + union { + RBT_ENTRY(eb_entry) _ebe_tentry; + TAILQ_ENTRY(eb_entry) _ebe_qentry; + } _ebe_entries; +#define ebe_tentry _ebe_entries._ebe_tentry +#define ebe_qentry _ebe_entries._ebe_qentry + + struct ether_addr ebe_addr; + void *ebe_port; + unsigned int ebe_type; +#define EBE_DYNAMIC 0x0 +#define EBE_STATIC 0x1 +#define EBE_DEAD 0xdead + time_t ebe_age; + + struct etherbridge *ebe_etherbridge; + struct refcnt ebe_refs; + struct smr_entry ebe_smr_entry; +}; + +SMR_TAILQ_HEAD(eb_list, eb_entry); +RBT_HEAD(eb_tree, eb_entry); +TAILQ_HEAD(eb_queue, eb_entry); + +struct etherbridge { + const char *eb_name; + const struct etherbridge_ops *eb_ops; + void *eb_cookie; + + struct mutex eb_lock; + unsigned int eb_num; + unsigned int eb_max; + int eb_max_age; /* seconds */ + struct timeout eb_tmo_age; + + struct eb_list *eb_table; + struct eb_tree eb_tree; + +}; + +int etherbridge_init(struct etherbridge *, const char *, + const struct etherbridge_ops *, void *); +int etherbridge_up(struct etherbridge *); +int etherbridge_down(struct etherbridge *); +void etherbridge_destroy(struct etherbridge *); + +void etherbridge_map(struct etherbridge *, void *, + const struct ether_addr *); +void *etherbridge_resolve(struct etherbridge *, const struct ether_addr *); +void etherbridge_detach_port(struct etherbridge *, void *); + +/* ioctl support */ +int etherbridge_set_max(struct etherbridge *, struct ifbrparam *); +int etherbridge_get_max(struct etherbridge *, struct ifbrparam *); +int etherbridge_set_tmo(struct etherbridge *, struct ifbrparam *); +int etherbridge_get_tmo(struct etherbridge *, struct ifbrparam *); +int etherbridge_rtfind(struct etherbridge *, struct ifbaconf *); +void etherbridge_flush(struct etherbridge *, uint32_t); + +static inline unsigned int +etherbridge_num(const struct etherbridge *eb) +{ + return (eb->eb_num); +} + +#endif /* _NET_ETHERBRIDGE_H_ */