From e5b7cf0c213dd4da2321c98ea4aaf1f7068745ce Mon Sep 17 00:00:00 2001 From: kedimuzafer Date: Sat, 29 Aug 2026 20:04:42 +0300 Subject: [PATCH] perf: add a zero-copy pbuf output path for WireGuard wireguard_udp_output_fn takes a flat byte buffer, so both magicsock output paths mem_malloc() a buffer and pbuf_copy_partial() the encrypted pbuf into it. The integrator then has to build a pbuf again to hand it to udp_sendto, which is a second allocation and a second copy of the same ~1.3 KB. All four happen on the TCPIP thread, once per outgoing packet. The encrypted pbuf is allocated PBUF_TRANSPORT in wireguardif_output_to_peer, so it already has room for the UDP and IP headers and udp_sendto can take it as-is. Adds wireguard_udp_output_pbuf_fn plus wireguardif_set_udp_output_pbuf(), and prefers it in both output paths when set. The byte-buffer callback is untouched and still used when no pbuf callback is registered, so this is additive for existing integrators. ml_wg_mgr registers one, since it already sends through a raw udp_pcb. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016bqLmUG7wJ7p9ya2Vp4A9J --- .../components/wireguard_lwip/src/wireguard.h | 9 ++++++++ .../wireguard_lwip/src/wireguardif.c | 22 +++++++++++++++++++ .../wireguard_lwip/src/wireguardif.h | 4 ++++ components/microlink/src/ml_wg_mgr.c | 18 +++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/components/microlink/components/wireguard_lwip/src/wireguard.h b/components/microlink/components/wireguard_lwip/src/wireguard.h index 9e16379..aec0a73 100644 --- a/components/microlink/components/wireguard_lwip/src/wireguard.h +++ b/components/microlink/components/wireguard_lwip/src/wireguard.h @@ -85,6 +85,13 @@ typedef err_t (*wireguard_derp_output_fn)(const uint8_t *peer_public_key, const // len: length of data // Returns: ERR_OK on success, error code on failure typedef err_t (*wireguard_udp_output_fn)(uint32_t dest_ip, uint16_t dest_port, const uint8_t *data, size_t len, void *ctx); +/* Same thing, but handed the encrypted pbuf itself. The byte-buffer form above + * forces a mem_malloc and a linearising copy here, and typically a second + * pbuf_alloc and copy inside the integrator's sender - two allocations and two + * ~1.3 KB copies per packet, all of it on the TCPIP thread. The encrypted pbuf + * is already PBUF_TRANSPORT with room for the UDP/IP headers, so udp_sendto() + * can take it as-is. */ +typedef err_t (*wireguard_udp_output_pbuf_fn)(uint32_t dest_ip, uint16_t dest_port, struct pbuf *q, void *ctx); struct wireguard_keypair { bool valid; @@ -207,6 +214,8 @@ struct wireguard_device { // UDP output callback for magicsock mode (external unified socket) wireguard_udp_output_fn udp_output_fn; void *udp_output_ctx; + wireguard_udp_output_pbuf_fn udp_output_pbuf_fn; + void *udp_output_pbuf_ctx; // Force all peer output through DERP relay (cellular mode) bool force_derp_output; diff --git a/components/microlink/components/wireguard_lwip/src/wireguardif.c b/components/microlink/components/wireguard_lwip/src/wireguardif.c index 6627fd0..2d792c2 100644 --- a/components/microlink/components/wireguard_lwip/src/wireguardif.c +++ b/components/microlink/components/wireguard_lwip/src/wireguardif.c @@ -157,6 +157,12 @@ static err_t wireguardif_peer_output(struct netif *netif, struct pbuf *q, struct return ERR_RTE; } + // Zero-copy magicsock output: hand the encrypted pbuf over untouched. + if (device->udp_output_pbuf_fn) { + uint32_t dest_ip = ip4_addr_get_u32(ip_2_ip4(&peer->ip)); + return device->udp_output_pbuf_fn(dest_ip, peer->port, q, device->udp_output_pbuf_ctx); + } + // In magicsock mode, use external UDP output callback if (device->udp_output_fn) { WG_DEBUG("[WG_OUT] Using magicsock callback\n"); @@ -191,6 +197,11 @@ static err_t wireguardif_peer_output(struct netif *netif, struct pbuf *q, struct } static err_t wireguardif_device_output(struct wireguard_device *device, struct pbuf *q, const ip_addr_t *ipaddr, u16_t port) { + if (device->udp_output_pbuf_fn) { + uint32_t dest_ip = ip4_addr_get_u32(ip_2_ip4(ipaddr)); + return device->udp_output_pbuf_fn(dest_ip, port, q, device->udp_output_pbuf_ctx); + } + // In magicsock mode, use external UDP output callback if (device->udp_output_fn) { uint8_t *data = (uint8_t *)mem_malloc(q->tot_len); @@ -1376,6 +1387,17 @@ void wireguardif_set_udp_output(struct netif *netif, wireguard_udp_output_fn fn, } } +void wireguardif_set_udp_output_pbuf(struct netif *netif, wireguard_udp_output_pbuf_fn fn, void *ctx) { + LWIP_ASSERT("netif != NULL", (netif != NULL)); + LWIP_ASSERT("state != NULL", (netif->state != NULL)); + struct wireguard_device *device = (struct wireguard_device *)netif->state; + if (device->valid) { + device->udp_output_pbuf_fn = fn; + device->udp_output_pbuf_ctx = ctx; + WG_DEBUG("[WG] zero-copy UDP output callback registered\n"); + } +} + void wireguardif_force_derp_output(struct netif *netif, bool force) { LWIP_ASSERT("netif != NULL", (netif != NULL)); LWIP_ASSERT("state != NULL", (netif->state != NULL)); diff --git a/components/microlink/components/wireguard_lwip/src/wireguardif.h b/components/microlink/components/wireguard_lwip/src/wireguardif.h index 479b6d4..05712f0 100644 --- a/components/microlink/components/wireguard_lwip/src/wireguardif.h +++ b/components/microlink/components/wireguard_lwip/src/wireguardif.h @@ -173,6 +173,10 @@ void wireguardif_disable_socket_bind(void); // via an external unified socket instead of the internal lwIP UDP PCB. void wireguardif_set_udp_output(struct netif *netif, wireguard_udp_output_fn fn, void *ctx); +/* Zero-copy variant. Takes precedence over wireguardif_set_udp_output() when + * set; prefer it whenever the sender can consume a pbuf. */ +void wireguardif_set_udp_output_pbuf(struct netif *netif, wireguard_udp_output_pbuf_fn fn, void *ctx); + // Force all peer output through DERP relay callback (cellular mode). // When enabled, peer_output always uses DERP even if peer has a direct endpoint. void wireguardif_force_derp_output(struct netif *netif, bool force); diff --git a/components/microlink/src/ml_wg_mgr.c b/components/microlink/src/ml_wg_mgr.c index ed40942..7bddacc 100644 --- a/components/microlink/src/ml_wg_mgr.c +++ b/components/microlink/src/ml_wg_mgr.c @@ -181,6 +181,23 @@ static err_t wg_derp_output_cb(const uint8_t *peer_public_key, * on that thread. */ static struct udp_pcb *s_wg_output_pcb = NULL; +/* Zero-copy WireGuard output. The encrypted pbuf is already PBUF_TRANSPORT + * with header room, so it goes straight to udp_sendto - no mem_malloc, no + * linearise, no second pbuf, no second copy. Ownership stays with + * wireguardif: udp_sendto does not consume q, and this callback is + * synchronous, so the caller's pbuf_free still applies. */ +static err_t wg_udp_output_pbuf_cb(uint32_t dest_ip, uint16_t dest_port, + struct pbuf *q, void *ctx) { + (void)ctx; + if (!s_wg_output_pcb) return ERR_CONN; + + ip_addr_t dst; + IP_SET_TYPE_VAL(dst, IPADDR_TYPE_V4); + ip4_addr_set_u32(ip_2_ip4(&dst), dest_ip); /* already network byte order */ + + return udp_sendto(s_wg_output_pcb, q, &dst, dest_port); +} + static err_t wg_udp_output_cb(uint32_t dest_ip, uint16_t dest_port, const uint8_t *data, size_t len, void *ctx) { microlink_t *ml = (microlink_t *)ctx; @@ -291,6 +308,7 @@ static esp_err_t wg_init_interface(microlink_t *ml) { /* Register output callbacks for magicsock mode */ wireguardif_set_derp_output(netif, wg_derp_output_cb, ml); wireguardif_set_udp_output(netif, wg_udp_output_cb, ml); + wireguardif_set_udp_output_pbuf(netif, wg_udp_output_pbuf_cb, ml); /* On cellular AT socket bridge, force all WG output through DERP relay. * AT sockets are TCP-only, so direct UDP is impossible.