wg: Add netmap support

When in netmap (emulated) mode, wireguard interfaces prepend or strip a
dummy ethernet header when interfacing with netmap.  The netmap
application thus sees unencrypted, de-encapsulated frames with a fixed
header.

In this mode, netmap hooks the if_input and if_transmit routines of the
ifnet.  Packets from the host TX ring are handled by wg_if_input(),
which simply hands them to the netisr layer; packets which would
otherwise be tunneled are intercepted in wg_output() and placed in the
host RX ring.

The "physical" TX ring is processed by wg_transmit(), which behaves
identically to wg_output() when netmap is not enabled, and packets
appear in the "physical" RX ring by hooking wg_deliver_in().

Reviewed by:	vmaffione
MFC after:	1 month
Sponsored by:	Klara, Inc.
Sponsored by:	Zenarmor
Differential Revision:	https://reviews.freebsd.org/D43460
This commit is contained in:
Mark Johnston 2024-04-20 12:01:28 -04:00
parent 278d695094
commit bf454ca88b
2 changed files with 163 additions and 6 deletions

View file

@ -121,6 +121,19 @@ as follows:
Although a valid Curve25519 key must have 5 bits set to
specific values, this is done by the interface and so it
will accept any random 32-byte base64 string.
.Sh NETMAP
.Xr netmap 4
applications may open a WireGuard interface in emulated mode.
The netmap application will receive decrypted, unencapsulated packets prepended
by a dummy Ethernet header.
The Ethertype field will be one of
.Dv ETHERTYPE_IP
or
.Dv ETHERTYPE_IPV6
depending on the address family of the packet.
Packets transmitted by the application should similarly begin with a dummy
Ethernet header; this header will be stripped before the packet is encrypted
and tunneled.
.Sh EXAMPLES
Create a
.Nm
@ -183,6 +196,7 @@ is not assigned to the allowed IPs of Peer X.
.Xr ip 4 ,
.Xr ipsec 4 ,
.Xr netintro 4 ,
.Xr netmap 4 ,
.Xr ovpn 4 ,
.Xr ipf 5 ,
.Xr pf.conf 5 ,

View file

@ -1674,6 +1674,31 @@ wg_deliver_out(struct wg_peer *peer)
}
}
#ifdef DEV_NETMAP
/*
* Hand a packet to the netmap RX ring, via netmap's
* freebsd_generic_rx_handler().
*/
static void
wg_deliver_netmap(if_t ifp, struct mbuf *m, int af)
{
struct ether_header *eh;
M_PREPEND(m, ETHER_HDR_LEN, M_NOWAIT);
if (__predict_false(m == NULL)) {
if_inc_counter(ifp, IFCOUNTER_IQDROPS, 1);
return;
}
eh = mtod(m, struct ether_header *);
eh->ether_type = af == AF_INET ?
htons(ETHERTYPE_IP) : htons(ETHERTYPE_IPV6);
memcpy(eh->ether_shost, "\x02\x02\x02\x02\x02\x02", ETHER_ADDR_LEN);
memcpy(eh->ether_dhost, "\xff\xff\xff\xff\xff\xff", ETHER_ADDR_LEN);
if_input(ifp, m);
}
#endif
static void
wg_deliver_in(struct wg_peer *peer)
{
@ -1682,6 +1707,7 @@ wg_deliver_in(struct wg_peer *peer)
struct wg_packet *pkt;
struct mbuf *m;
struct epoch_tracker et;
int af;
while ((pkt = wg_queue_dequeue_serial(&peer->p_decrypt_serial)) != NULL) {
if (atomic_load_acq_int(&pkt->p_state) != WG_PACKET_CRYPTED)
@ -1707,19 +1733,25 @@ wg_deliver_in(struct wg_peer *peer)
if (m->m_pkthdr.len == 0)
goto done;
MPASS(pkt->p_af == AF_INET || pkt->p_af == AF_INET6);
af = pkt->p_af;
MPASS(af == AF_INET || af == AF_INET6);
pkt->p_mbuf = NULL;
m->m_pkthdr.rcvif = ifp;
NET_EPOCH_ENTER(et);
BPF_MTAP2_AF(ifp, m, pkt->p_af);
BPF_MTAP2_AF(ifp, m, af);
CURVNET_SET(if_getvnet(ifp));
M_SETFIB(m, if_getfib(ifp));
if (pkt->p_af == AF_INET)
#ifdef DEV_NETMAP
if ((if_getcapenable(ifp) & IFCAP_NETMAP) != 0)
wg_deliver_netmap(ifp, m, af);
else
#endif
if (af == AF_INET)
netisr_dispatch(NETISR_IP, m);
if (pkt->p_af == AF_INET6)
else if (af == AF_INET6)
netisr_dispatch(NETISR_IPV6, m);
CURVNET_RESTORE();
NET_EPOCH_EXIT(et);
@ -2164,13 +2196,36 @@ determine_af_and_pullup(struct mbuf **m, sa_family_t *af)
return (0);
}
#ifdef DEV_NETMAP
static int
determine_ethertype_and_pullup(struct mbuf **m, int *etp)
{
struct ether_header *eh;
*m = m_pullup(*m, sizeof(struct ether_header));
if (__predict_false(*m == NULL))
return (ENOBUFS);
eh = mtod(*m, struct ether_header *);
*etp = ntohs(eh->ether_type);
if (*etp != ETHERTYPE_IP && *etp != ETHERTYPE_IPV6)
return (EAFNOSUPPORT);
return (0);
}
/*
* This should only be invoked by netmap, via nm_os_generic_xmit_frame(), to
* transmit packets from the netmap TX ring.
*/
static int
wg_transmit(if_t ifp, struct mbuf *m)
{
sa_family_t af;
int ret;
int et, ret;
struct mbuf *defragged;
KASSERT((if_getcapenable(ifp) & IFCAP_NETMAP) != 0,
("%s: ifp %p is not in netmap mode", __func__, ifp));
defragged = m_defrag(m, M_NOWAIT);
if (defragged)
m = defragged;
@ -2180,14 +2235,94 @@ wg_transmit(if_t ifp, struct mbuf *m)
return (ENOBUFS);
}
ret = determine_ethertype_and_pullup(&m, &et);
if (ret) {
xmit_err(ifp, m, NULL, AF_UNSPEC);
return (ret);
}
m_adj(m, sizeof(struct ether_header));
ret = determine_af_and_pullup(&m, &af);
if (ret) {
xmit_err(ifp, m, NULL, AF_UNSPEC);
return (ret);
}
return (wg_xmit(ifp, m, af, if_getmtu(ifp)));
/*
* netmap only gets to see transient errors, since it handles errors by
* refusing to advance the transmit ring and retrying later.
*/
ret = wg_xmit(ifp, m, af, if_getmtu(ifp));
if (ret == ENOBUFS)
return (ret);
return (0);
}
/*
* This should only be invoked by netmap, via nm_os_send_up(), to process
* packets from the host TX ring.
*/
static void
wg_if_input(if_t ifp, struct mbuf *m)
{
int et;
KASSERT((if_getcapenable(ifp) & IFCAP_NETMAP) != 0,
("%s: ifp %p is not in netmap mode", __func__, ifp));
if (determine_ethertype_and_pullup(&m, &et) != 0) {
if_inc_counter(ifp, IFCOUNTER_IERRORS, 1);
m_freem(m);
return;
}
CURVNET_SET(if_getvnet(ifp));
switch (et) {
case ETHERTYPE_IP:
m_adj(m, sizeof(struct ether_header));
netisr_dispatch(NETISR_IP, m);
break;
case ETHERTYPE_IPV6:
m_adj(m, sizeof(struct ether_header));
netisr_dispatch(NETISR_IPV6, m);
break;
default:
__assert_unreachable();
}
CURVNET_RESTORE();
}
/*
* Deliver a packet to the host RX ring. Because the interface is in netmap
* mode, the if_transmit() call should pass the packet to netmap_transmit().
*/
static int
wg_xmit_netmap(if_t ifp, struct mbuf *m, int af)
{
struct ether_header *eh;
if (__predict_false(if_tunnel_check_nesting(ifp, m, MTAG_WGLOOP,
MAX_LOOPS))) {
printf("%s:%d\n", __func__, __LINE__);
if_inc_counter(ifp, IFCOUNTER_IERRORS, 1);
m_freem(m);
return (ELOOP);
}
M_PREPEND(m, ETHER_HDR_LEN, M_NOWAIT);
if (__predict_false(m == NULL)) {
if_inc_counter(ifp, IFCOUNTER_IQDROPS, 1);
return (ENOBUFS);
}
eh = mtod(m, struct ether_header *);
eh->ether_type = af == AF_INET ?
htons(ETHERTYPE_IP) : htons(ETHERTYPE_IPV6);
memcpy(eh->ether_shost, "\x06\x06\x06\x06\x06\x06", ETHER_ADDR_LEN);
memcpy(eh->ether_dhost, "\xff\xff\xff\xff\xff\xff", ETHER_ADDR_LEN);
return (if_transmit(ifp, m));
}
#endif /* DEV_NETMAP */
static int
wg_output(if_t ifp, struct mbuf *m, const struct sockaddr *dst, struct route *ro)
{
@ -2206,6 +2341,11 @@ wg_output(if_t ifp, struct mbuf *m, const struct sockaddr *dst, struct route *ro
return (EAFNOSUPPORT);
}
#ifdef DEV_NETMAP
if ((if_getcapenable(ifp) & IFCAP_NETMAP) != 0)
return (wg_xmit_netmap(ifp, m, af));
#endif
defragged = m_defrag(m, M_NOWAIT);
if (defragged)
m = defragged;
@ -2781,7 +2921,10 @@ wg_clone_create(struct if_clone *ifc, char *name, size_t len,
if_setinitfn(ifp, wg_init);
if_setreassignfn(ifp, wg_reassign);
if_setqflushfn(ifp, wg_qflush);
#ifdef DEV_NETMAP
if_settransmitfn(ifp, wg_transmit);
if_setinputfn(ifp, wg_if_input);
#endif
if_setoutputfn(ifp, wg_output);
if_setioctlfn(ifp, wg_ioctl);
if_attach(ifp);