diff --git a/.github/workflows/wolfguard.yml b/.github/workflows/wolfguard.yml new file mode 100644 index 0000000..41f4e80 --- /dev/null +++ b/.github/workflows/wolfguard.yml @@ -0,0 +1,96 @@ +name: wolfguard tests (non-FIPS) + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +jobs: + wolfguard: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential autoconf libtool \ + linux-headers-$(uname -r) \ + iproute2 check \ + gawk kmod + + - name: Clone wolfSSL and wolfguard sources + run: | + mkdir -p wolf-sources + # nightly-snapshot is the branch required by wolfguard for its latest + # kernel-module API. Pin to a tagged release once wolfguard stabilises. + git clone --depth=1 https://github.com/wolfssl/wolfssl \ + --branch nightly-snapshot wolf-sources/wolfssl + git clone --depth=1 https://github.com/wolfssl/wolfguard \ + wolf-sources/wolfguard + + - name: Build and install wolfssl user library + working-directory: wolf-sources/wolfssl + run: | + ./autogen.sh + ./configure --quiet --enable-wolfguard --enable-all-asm + make -j$(nproc) + sudo make install + sudo ldconfig + + - name: Build and install wg-fips user tool + run: | + make -j$(nproc) -C wolf-sources/wolfguard/user-src + sudo make -C wolf-sources/wolfguard/user-src install + + - name: Build and install libwolfssl.ko (non-FIPS) + working-directory: wolf-sources/wolfssl + run: | + make distclean + ./configure --quiet --enable-wolfguard --enable-cryptonly \ + --enable-intelasm --enable-linuxkm \ + --with-linux-source=/lib/modules/$(uname -r)/build \ + --prefix=$(pwd)/linuxkm/build + make -j$(nproc) module + sudo make install + + - name: Build wolfguard.ko + working-directory: wolf-sources/wolfguard/kernel-src + run: | + make -j$(nproc) \ + KERNELDIR=/lib/modules/$(uname -r)/build \ + KERNELRELEASE=$(uname -r) \ + EXTRA_CFLAGS="-I$(realpath ../../wolfssl)" + + - name: Load kernel modules + run: | + sudo insmod /lib/modules/$(uname -r)/wolfssl/libwolfssl.ko + sudo insmod wolf-sources/wolfguard/kernel-src/wolfguard.ko + + - name: Build wolfguard unit tests + run: make unit-wolfguard + + - name: Run wolfguard unit tests + timeout-minutes: 5 + run: | + set -euo pipefail + timeout --preserve-status 5m ./build/test/unit-wolfguard + + - name: Build wolfguard functional test + run: make build/test-wolfguard + + - name: Run wolfguard functional test + timeout-minutes: 5 + run: | + set -euo pipefail + timeout --preserve-status 5m sudo ./build/test-wolfguard + + - name: Unload kernel modules + if: always() + run: | + sudo rmmod wolfguard || true + sudo rmmod libwolfssl || true diff --git a/.gitignore b/.gitignore index e4878b3..f63230f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ tags cppcheck_results.xml src/port/stm32h563/app.elf src/port/va416xx/app.elf +wolf-sources/ diff --git a/Makefile b/Makefile index 23a345a..c76fe54 100644 --- a/Makefile +++ b/Makefile @@ -199,6 +199,12 @@ leaksan:LDFLAGS+=-fsanitize=leak ESP_CFLAGS = \ -DWOLFIP_ESP -DWOLFSSL_WOLFIP +WOLFGUARD_CFLAGS = -DWOLFIP_WOLFGUARD + +WG_OBJ = build/wolfguard/wolfip.o \ + build/wolfguard/wolfip_wolfguard.o \ + $(TAP_OBJ) + # Test ifeq ($(CHECK_PKG_LIBS),) @@ -255,6 +261,53 @@ build/ipfilter/wolfip.o: src/wolfip.c build/test/ipfilter_logger.o: CFLAGS+=-DCONFIG_IPFILTER=1 +# wolfguard +build/wolfguard/wolfip.o: src/wolfip.c + @mkdir -p `dirname $@` || true + @echo "[CC] $< (wolfguard)" + @$(CC) $(CFLAGS) $(WOLFGUARD_CFLAGS) -c $< -o $@ + +build/wolfguard/wolfip_wolfguard.o: src/wolfip_wolfguard.c + @mkdir -p `dirname $@` || true + @echo "[CC] $< (wolfguard)" + @$(CC) $(CFLAGS) $(WOLFGUARD_CFLAGS) -c $< -o $@ + +build/test/wolfguard/test_wolfguard.o: src/test/wolfguard/test_wolfguard.c + @mkdir -p `dirname $@` || true + @echo "[CC] $@" + @$(CC) $(CFLAGS) $(WOLFGUARD_CFLAGS) -c $< -o $@ + +build/test-wolfguard: $(WG_OBJ) build/test/wolfguard/test_wolfguard.o + @echo "[LD] $@" + @$(CC) $(CFLAGS) $(WOLFGUARD_CFLAGS) $(LDFLAGS) -o $@ \ + $(BEGIN_GROUP) $(^) $(END_GROUP) -lwolfssl + +unit-wolfguard: build/test/unit-wolfguard + +build/test/unit-wolfguard: src/test/unit/unit_wolfguard.c + @mkdir -p build/test/ + @echo "[CC] unit_wolfguard.c" + @$(CC) $(CFLAGS) $(WOLFGUARD_CFLAGS) $(UNIT_CFLAGS) \ + -c src/test/unit/unit_wolfguard.c -o build/test/unit_wolfguard.o + @echo "[LD] $@" + @$(CC) build/test/unit_wolfguard.o -o $@ \ + $(UNIT_LDFLAGS) $(LDFLAGS) $(UNIT_LIBS) + +clean-unit-wolfguard: + @rm -f build/test/unit-wolfguard build/test/unit_wolfguard.o + +unit-wolfguard-asan: CFLAGS+=-fsanitize=address +unit-wolfguard-asan: LDFLAGS+=-fsanitize=address $(UNIT_LIBS) +unit-wolfguard-asan: clean-unit-wolfguard build/test/unit-wolfguard + +unit-wolfguard-ubsan: CFLAGS+=-fsanitize=undefined -fno-sanitize-recover=all +unit-wolfguard-ubsan: LDFLAGS+=-fsanitize=undefined $(UNIT_LIBS) +unit-wolfguard-ubsan: clean-unit-wolfguard build/test/unit-wolfguard + +unit-wolfguard-leaksan: CFLAGS+=-fsanitize=leak +unit-wolfguard-leaksan: LDFLAGS+=-fsanitize=leak $(UNIT_LIBS) +unit-wolfguard-leaksan: clean-unit-wolfguard build/test/unit-wolfguard + # ipsec esp build/esp/wolfip.o: src/wolfip.c @mkdir -p `dirname $@` || true @@ -438,7 +491,8 @@ install: ldconfig .PHONY: clean all static cppcheck cov autocov unit-asan unit-ubsan unit-leaksan clean-unit \ - unit-esp-asan unit-esp-ubsan unit-esp-leaksan clean-unit-esp + unit-esp-asan unit-esp-ubsan unit-esp-leaksan clean-unit-esp \ + unit-wolfguard unit-wolfguard-asan unit-wolfguard-ubsan unit-wolfguard-leaksan clean-unit-wolfguard cppcheck: $(CPPCHECK) $(CPPCHECK_FLAGS) src/ 2>cppcheck_results.xml diff --git a/README.md b/README.md index ab1b1e9..6d8625e 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ A single network interface can be associated with the device. | **Network** | IPv4 Forwarding | Multi-interface routing (optional) | [RFC 1812](https://datatracker.ietf.org/doc/html/rfc1812) | | **Network** | ICMP | Echo request/reply, TTL exceeded | [RFC 792](https://datatracker.ietf.org/doc/html/rfc792) | | **Network** | IPsec | ESP Transport mode | [RFC 4303](https://datatracker.ietf.org/doc/html/rfc4303) | +| **Network** | WolfGuard | VPN tunnel via wolfguard kernel module (SECP256R1, AES-256-GCM, SHA-256) | See `wolf-sources/wolfssl/wolfguard/README.md` | | **Transport** | UDP | Unicast datagrams, checksum | [RFC 768](https://datatracker.ietf.org/doc/html/rfc768) | | **Transport** | TCP | Connection management, reliable delivery | [RFC 793](https://datatracker.ietf.org/doc/html/rfc793), [RFC 9293](https://datatracker.ietf.org/doc/html/rfc9293) | | **Transport** | TCP | Maximum Segment Size negotiation | [RFC 793](https://datatracker.ietf.org/doc/html/rfc793) | @@ -69,6 +70,92 @@ The `-I wtcp0` flag pins the test to the injected interface and `-c5` generates five echo requests. Successful replies confirm the ICMP datagram socket support end-to-end through the tap device. +## WolfGuard support + +wolfIP can use [WolfGuard](wolf-sources/wolfssl/wolfguard/README.md) as its +link-layer driver, giving every socket opened on the stack transparent +WireGuard-compatible encryption without any changes to application code. + +### How it works + +WolfGuard is a kernel module (`wolfguard.ko`) that registers a standard Linux +network interface (`ARPHRD_NONE`, identical in structure to the upstream +WireGuard driver) and performs the handshake and encryption inside the kernel +using wolfSSL's FIPS-ready primitives (SECP256R1, AES-256-GCM, SHA-256). + +`wolfip_wolfguard.c` is a wolfIP ll_dev driver that bridges the two: + +1. Creates the wolfguard interface via Netlink (`RTM_NEWLINK type=wolfguard`). +2. Configures keys and peers via the wolfguard Generic Netlink family + (`WG_CMD_SET_DEVICE`). +3. Connects to the interface with an `AF_PACKET/SOCK_DGRAM` socket, injecting + and receiving raw IP packets that the kernel module encrypts/decrypts + transparently. +4. Provides a synthetic ARP proxy so wolfIP's Ethernet layer can resolve peer + IPs without kernel ARP involvement. + +### Prerequisites + +Build and load the wolfguard kernel module and its wolfSSL dependency by +following the instructions in +[wolf-sources/wolfssl/wolfguard/README.md](wolf-sources/wolfssl/wolfguard/README.md). +Then load the modules before running any wolfguard-enabled binary: + +```sh +insmod /lib/modules/$(uname -r)/wolfssl/libwolfssl.ko +insmod /path/to/wolfguard.ko +modprobe udp_tunnel +modprobe ip6_udp_tunnel +``` + +### Enabling wolfguard support + +Add `-DWOLFIP_WOLFGUARD` to your `CFLAGS` and link `wolfip_wolfguard.c` into +your build. In the wolfIP Makefile the dedicated targets handle this +automatically: + +```sh +make unit-wolfguard # driver unit tests (no kernel module required) +make build/test-wolfguard # functional test binary +``` + +To integrate into your own application, call `wolfIP_wg_init()` in place of +`tap_init()`: + +```c +#include "wolfip_wolfguard.h" + +struct wolfIP_wg_config cfg = { ... }; /* keys, peers, listen port */ +struct wolfIP *stack; + +wolfIP_init_static(&stack); +wolfIP_wg_init(&cfg, wolfIP_getdev(stack)); +wolfIP_ipconfig_set(stack, atoip4("10.8.0.1"), atoip4("255.255.255.0"), + atoip4("10.8.0.1")); +/* use wolfIP sockets normally — all traffic is encrypted by wolfguard */ +``` + +### Running the tests + +**Unit tests** exercise the driver logic (ARP proxy, L2/L3 bridging) entirely +in userspace with a mock pipe — no kernel module required: + +```sh +make unit-wolfguard +./build/test/unit-wolfguard +``` + +**Functional test** performs a full loopback: wolfIP sends a UDP packet, +wolfguard encrypts it, a kernel-side peer decrypts and echoes it back, and +wolfIP verifies the payload. Requires the kernel modules to be loaded, +`wg-fips` in `PATH`, and root (or `NET_ADMIN` capability) to create network +interfaces: + +```sh +make build/test-wolfguard +sudo ./build/test-wolfguard +``` + ## FreeRTOS Port wolfIP now includes a dedicated FreeRTOS wrapper port at: diff --git a/src/test/unit/unit_esp.c b/src/test/unit/unit_esp.c index 05f21db..f2cf393 100644 --- a/src/test/unit/unit_esp.c +++ b/src/test/unit/unit_esp.c @@ -674,6 +674,7 @@ START_TEST(test_unwrap_pad_too_big) } END_TEST + /* * full enc/dec round-trips * */ @@ -771,6 +772,7 @@ START_TEST(test_roundtrip_aes128_cbc_sha1) } END_TEST +#ifndef HAVE_FIPS START_TEST(test_roundtrip_aes128_cbc_md5) { do_roundtrip_cbc_hmac(k_aes128, sizeof(k_aes128), @@ -779,6 +781,7 @@ START_TEST(test_roundtrip_aes128_cbc_md5) ESP_ICVLEN_HMAC_96); } END_TEST +#endif START_TEST(test_roundtrip_aes256_cbc_sha256_128) { @@ -1140,7 +1143,10 @@ static Suite *esp_suite(void) tcase_add_test(tc, test_roundtrip_aes128_cbc_sha256_128); tcase_add_test(tc, test_roundtrip_aes128_cbc_sha256_96); tcase_add_test(tc, test_roundtrip_aes128_cbc_sha1); +/* run this test only if the build is not in FIPS mode, since md5 is not approved. */ +#ifndef HAVE_FIPS tcase_add_test(tc, test_roundtrip_aes128_cbc_md5); +#endif tcase_add_test(tc, test_roundtrip_aes256_cbc_sha256_128); #ifndef NO_DES3 tcase_add_test(tc, test_roundtrip_des3_sha256); diff --git a/src/test/unit/unit_wolfguard.c b/src/test/unit/unit_wolfguard.c new file mode 100644 index 0000000..3fc6bbf --- /dev/null +++ b/src/test/unit/unit_wolfguard.c @@ -0,0 +1,465 @@ +/* unit_wolfguard.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Unit tests for wolfip_wolfguard.c, driver logic (no kernel required). + * + * The tests replace the real TUN file descriptor with the write-end of a + * pipe so that wg_send/wg_poll can be exercised in a fully userspace + * environment. Kernel operations (interface creation, netlink config, + * ioctl bring-up) are bypassed by calling the internal helpers directly + * rather than going through wolfIP_wg_init(). + */ + +#ifndef WOLFIP_WOLFGUARD +# define WOLFIP_WOLFGUARD +#endif + +#include "check.h" + +#include +#include +#include +#include +#include + +/* Pull in the implementation directly (same approach as unit_esp.c) */ +#include "../../wolfip_wolfguard.c" + + +/* Pipe fds: write_fd is used as the mock TUN fd in wg_state.tun_fd. + * Tests write IP packets to read_fd to simulate incoming kernel traffic, + * and read from read_fd to inspect what was sent toward the kernel. */ +static int pipe_rd; /* read end, inspect wg_send output */ +static int pipe_wr; /* write end, inject wg_poll input */ + +/* A minimal wolfIP_ll_dev used as the argument to wg_poll/wg_send. */ +static struct wolfIP_ll_dev test_ll; + +/* + * Ethernet frame constants reused in tests. + * (These mirror the private defines inside wolfip_wolfguard.c but are + * redefined here so the test file does not depend on internal naming.) + */ +#define T_ETH_HDR_LEN 14 +#define T_ARP_PKT_LEN 42 +#define T_ETH_IP 0x0800 +#define T_ETH_ARP 0x0806 +#define T_ARP_REQ 1 +#define T_ARP_REP 2 + +/* Build a minimal 14-byte Ethernet header at @buf. + * @dst/@src are 6-byte MAC arrays; @etype is the ethertype (host byte order). */ +static void make_eth_hdr(uint8_t *buf, + const uint8_t *dst, const uint8_t *src, + uint16_t etype) +{ + memcpy(buf + 0, dst, 6); + memcpy(buf + 6, src, 6); + buf[12] = (uint8_t)(etype >> 8); + buf[13] = (uint8_t)(etype & 0xFF); +} + +/* + * Build a 42-byte ARP request frame at @buf. + * + * @sha: sender hardware (MAC) address + * @sip: sender IP in network byte order + * @tha: target hardware address (zeros for a request) + * @tip: target IP in network byte order + */ +static void make_arp_request(uint8_t *buf, + const uint8_t *sha, uint32_t sip, + const uint8_t *tha, uint32_t tip) +{ + static const uint8_t bcast[6] = {0xff,0xff,0xff,0xff,0xff,0xff}; + make_eth_hdr(buf, bcast, sha, T_ETH_ARP); + buf[14] = 0x00; buf[15] = 0x01; /* htype = Ethernet */ + buf[16] = 0x08; buf[17] = 0x00; /* ptype = IPv4 */ + buf[18] = 6; /* hlen */ + buf[19] = 4; /* plen */ + buf[20] = 0x00; buf[21] = T_ARP_REQ; /* opcode */ + memcpy(buf + 22, sha, 6); + memcpy(buf + 28, &sip, 4); + memcpy(buf + 32, tha, 6); + memcpy(buf + 38, &tip, 4); +} + +static void setup(void) +{ + int fds[2]; + + memset(&wg_state, 0, sizeof(wg_state)); + memset(&test_ll, 0, sizeof(test_ll)); + + /* Create a pipe that acts as the mock TUN fd */ + ck_assert_int_eq(pipe(fds), 0); + pipe_rd = fds[0]; + pipe_wr = fds[1]; + + /* Make both ends non-blocking */ + fcntl(pipe_rd, F_SETFL, O_NONBLOCK); + fcntl(pipe_wr, F_SETFL, O_NONBLOCK); + + /* + * wg_send() writes to wg_state.tun_fd; we read back from pipe_rd. + * wg_poll() reads from wg_state.tun_fd; we write test data to pipe_wr. + * + * For wg_send tests: tun_fd = pipe_wr (write end → read via pipe_rd) + * For wg_poll tests: tun_fd = pipe_rd (read end ← written via pipe_wr) + * + * We set tun_fd per-test. + */ + + memcpy(test_ll.mac, wg_local_mac, 6); + test_ll.poll = wg_poll; + test_ll.send = wg_send; +} + +static void teardown(void) +{ + close(pipe_rd); + close(pipe_wr); + memset(&wg_state, 0, sizeof(wg_state)); + wg_state.tun_fd = -1; +} + +/* + * wg_send() with an IPv4 frame must strip the 14-byte Ethernet header and + * write the remaining IP packet to the TUN fd. + */ +START_TEST(test_wg_send_ipv4) +{ + uint8_t frame[64]; + uint8_t payload[] = {0x45, 0x00, 0x00, 0x14}; /* fake IP header start */ + uint8_t read_buf[64]; + int ret, n; + + static const uint8_t src[6] = {0xAA,0xBB,0xCC,0xDD,0xEE,0xFF}; + static const uint8_t dst[6] = {0x11,0x22,0x33,0x44,0x55,0x66}; + + memset(frame, 0, sizeof(frame)); + make_eth_hdr(frame, dst, src, T_ETH_IP); + memcpy(frame + T_ETH_HDR_LEN, payload, sizeof(payload)); + + /* Use the write end of the pipe as the TUN fd */ + wg_state.tun_fd = pipe_wr; + + ret = wg_send(&test_ll, frame, (uint32_t)(T_ETH_HDR_LEN + sizeof(payload))); + ck_assert_int_eq(ret, 0); + + /* Verify what was written to the pipe (i.e. what would go to the kernel) */ + n = (int)read(pipe_rd, read_buf, sizeof(read_buf)); + ck_assert_int_eq(n, (int)sizeof(payload)); + ck_assert_mem_eq(read_buf, payload, sizeof(payload)); +} +END_TEST + +/* + * wg_send() with a frame shorter than the Ethernet header must return -1. + */ +START_TEST(test_wg_send_short_frame) +{ + uint8_t frame[4] = {0x00, 0x01, 0x02, 0x03}; + int ret; + + wg_state.tun_fd = pipe_wr; + ret = wg_send(&test_ll, frame, sizeof(frame)); + ck_assert_int_eq(ret, -1); +} +END_TEST + +/* + * wg_send() with an unknown ethertype must silently drop the frame (return 0) + * without writing anything to the TUN fd. + */ +START_TEST(test_wg_send_unknown_etype_drop) +{ + uint8_t frame[64]; + uint8_t read_buf[64]; + int n; + static const uint8_t mac[6] = {0}; + + memset(frame, 0, sizeof(frame)); + make_eth_hdr(frame, mac, mac, 0x86DD); /* IPv6 — not handled */ + + wg_state.tun_fd = pipe_wr; + ck_assert_int_eq(wg_send(&test_ll, frame, sizeof(frame)), 0); + + /* Nothing should have been written to the pipe */ + n = (int)read(pipe_rd, read_buf, sizeof(read_buf)); + ck_assert_int_lt(n, 1); /* EAGAIN → -1, or 0 if the pipe is closed */ +} +END_TEST + +/* + * After wg_send() receives an ARP request, arp_reply_pending must be set. + */ +START_TEST(test_wg_arp_proxy_pending) +{ + uint8_t frame[T_ARP_PKT_LEN]; + static const uint8_t sha[6] = {0x02,0x00,0x57,0x47,0x00,0x01}; + static const uint8_t tha[6] = {0}; + uint32_t sip = htonl(0x0A000001); /* 10.0.0.1 */ + uint32_t tip = htonl(0x0A000002); /* 10.0.0.2 */ + + wg_state.tun_fd = pipe_wr; + wg_state.arp_reply_pending = 0; + + make_arp_request(frame, sha, sip, tha, tip); + ck_assert_int_eq(wg_send(&test_ll, frame, sizeof(frame)), 0); + ck_assert_int_eq(wg_state.arp_reply_pending, 1); +} +END_TEST + +/* + * After an ARP request is processed by wg_send(), wg_poll() must return the + * synthetic reply and clear arp_reply_pending. + */ +START_TEST(test_wg_arp_proxy_poll_returns_reply) +{ + uint8_t req[T_ARP_PKT_LEN]; + uint8_t poll_buf[128]; + static const uint8_t sha[6] = {0xAA,0xBB,0xCC,0xDD,0xEE,0xFF}; + static const uint8_t tha[6] = {0}; + uint32_t sip = htonl(0x0A000001); + uint32_t tip = htonl(0x0A000002); + int n; + + wg_state.tun_fd = pipe_wr; /* poll reads from tun_fd; won't be used here */ + + make_arp_request(req, sha, sip, tha, tip); + ck_assert_int_eq(wg_send(&test_ll, req, sizeof(req)), 0); + ck_assert_int_eq(wg_state.arp_reply_pending, 1); + + /* Now call wg_poll() with the read end as tun_fd (no data there) */ + wg_state.tun_fd = pipe_rd; + n = wg_poll(&test_ll, poll_buf, sizeof(poll_buf)); + + ck_assert_int_eq(n, T_ARP_PKT_LEN); + ck_assert_int_eq(wg_state.arp_reply_pending, 0); + + /* Reply ethertype must be ARP */ + ck_assert_uint_eq(poll_buf[12], 0x08); + ck_assert_uint_eq(poll_buf[13], 0x06); + /* Reply opcode must be 2 (ARP_REPLY) */ + ck_assert_uint_eq(poll_buf[20], 0x00); + ck_assert_uint_eq(poll_buf[21], T_ARP_REP); +} +END_TEST + +/* + * The synthetic ARP reply must have: + * - Correct sender IP (= target IP from request) + * - Correct target IP (= sender IP from request) + * - Sender MAC = wg_peer_mac + * - Target MAC = sender MAC from request + */ +START_TEST(test_wg_arp_reply_fields) +{ + uint8_t req[T_ARP_PKT_LEN]; + uint8_t poll_buf[128]; + static const uint8_t sha[6] = {0x11,0x22,0x33,0x44,0x55,0x66}; + static const uint8_t tha[6] = {0}; + uint32_t sip = htonl(0x0A000001); /* 10.0.0.1 */ + uint32_t tip = htonl(0x0A000002); /* 10.0.0.2 */ + int n; + + wg_state.tun_fd = pipe_wr; + make_arp_request(req, sha, sip, tha, tip); + ck_assert_int_eq(wg_send(&test_ll, req, sizeof(req)), 0); + + wg_state.tun_fd = pipe_rd; + n = wg_poll(&test_ll, poll_buf, sizeof(poll_buf)); + ck_assert_int_eq(n, T_ARP_PKT_LEN); + + /* Sender MAC in reply == wg_peer_mac */ + ck_assert_mem_eq(poll_buf + 22, wg_peer_mac, 6); + /* Sender IP in reply == tip from request (10.0.0.2) */ + ck_assert_mem_eq(poll_buf + 28, &tip, 4); + /* Target MAC in reply == sha from request */ + ck_assert_mem_eq(poll_buf + 32, sha, 6); + /* Target IP in reply == sip from request (10.0.0.1) */ + ck_assert_mem_eq(poll_buf + 38, &sip, 4); +} +END_TEST + +/* + * An ARP request with opcode != 1 must NOT set arp_reply_pending. + */ +START_TEST(test_wg_arp_non_request_ignored) +{ + uint8_t frame[T_ARP_PKT_LEN]; + static const uint8_t mac[6] = {0}; + uint32_t ip = htonl(0x0A000001); + + wg_state.tun_fd = pipe_wr; + wg_state.arp_reply_pending = 0; + + make_arp_request(frame, mac, ip, mac, ip); + /* Overwrite the opcode to 2 (ARP reply) */ + frame[20] = 0x00; + frame[21] = T_ARP_REP; + + ck_assert_int_eq(wg_send(&test_ll, frame, sizeof(frame)), 0); + ck_assert_int_eq(wg_state.arp_reply_pending, 0); +} +END_TEST + +/* + * wg_poll() must prepend a 14-byte Ethernet header with ethertype 0x0800 + * to raw IP data read from the TUN fd. + */ +START_TEST(test_wg_poll_prepends_eth_header) +{ + uint8_t ip_pkt[] = {0x45, 0x00, 0x00, 0x1C, + 0x00, 0x01, 0x40, 0x00, + 0x40, 0x11, 0x00, 0x00, + 0x0A, 0x00, 0x00, 0x01, + 0x0A, 0x00, 0x00, 0x02}; /* minimal IP header */ + uint8_t poll_buf[128]; + int n; + + /* Inject raw IP into the read end of the pipe */ + wg_state.tun_fd = pipe_rd; + ck_assert_int_gt((int)write(pipe_wr, ip_pkt, sizeof(ip_pkt)), 0); + + n = wg_poll(&test_ll, poll_buf, sizeof(poll_buf)); + ck_assert_int_eq(n, (int)(T_ETH_HDR_LEN + sizeof(ip_pkt))); + + /* dst MAC == wg_local_mac */ + ck_assert_mem_eq(poll_buf + 0, wg_local_mac, 6); + /* src MAC == wg_peer_mac */ + ck_assert_mem_eq(poll_buf + 6, wg_peer_mac, 6); + /* ethertype == 0x0800 */ + ck_assert_uint_eq(poll_buf[12], 0x08); + ck_assert_uint_eq(poll_buf[13], 0x00); + /* IP payload is intact */ + ck_assert_mem_eq(poll_buf + T_ETH_HDR_LEN, ip_pkt, sizeof(ip_pkt)); +} +END_TEST + +/* + * wg_poll() must return -1 when an ARP reply is pending but the caller's + * buffer is too small to hold WG_ARP_PKT_LEN bytes. + */ +START_TEST(test_wg_poll_arp_reply_buf_too_small) +{ + uint8_t req[T_ARP_PKT_LEN]; + uint8_t small_buf[T_ARP_PKT_LEN - 1]; + static const uint8_t sha[6] = {0xAA,0xBB,0xCC,0xDD,0xEE,0xFF}; + static const uint8_t tha[6] = {0}; + uint32_t sip = htonl(0x0A000001); + uint32_t tip = htonl(0x0A000002); + + wg_state.tun_fd = pipe_wr; + make_arp_request(req, sha, sip, tha, tip); + ck_assert_int_eq(wg_send(&test_ll, req, sizeof(req)), 0); + ck_assert_int_eq(wg_state.arp_reply_pending, 1); + + wg_state.tun_fd = pipe_rd; + ck_assert_int_eq(wg_poll(&test_ll, small_buf, sizeof(small_buf)), -1); +} +END_TEST + +/* + * wg_send() with an ARP frame shorter than WG_ARP_PKT_LEN must return -1 + * and must NOT set arp_reply_pending. + */ +START_TEST(test_wg_send_arp_buf_too_small) +{ + uint8_t frame[T_ARP_PKT_LEN - 1]; + static const uint8_t mac[6] = {0}; + + memset(frame, 0, sizeof(frame)); + /* Build an ARP request header in what space we have */ + make_eth_hdr(frame, mac, mac, T_ETH_ARP); + frame[20] = 0x00; + frame[21] = T_ARP_REQ; + + wg_state.tun_fd = pipe_wr; + wg_state.arp_reply_pending = 0; + + ck_assert_int_eq(wg_send(&test_ll, frame, sizeof(frame)), -1); + ck_assert_int_eq(wg_state.arp_reply_pending, 0); +} +END_TEST + +/* + * wg_poll() must return a non-positive value (timeout or error) when no data + * is available on the TUN fd and no ARP reply is pending. + */ +START_TEST(test_wg_poll_timeout_no_data) +{ + uint8_t buf[128]; + int n; + + /* Use the read end of the pipe (nothing written to write end) */ + wg_state.tun_fd = pipe_rd; + wg_state.arp_reply_pending = 0; + + n = wg_poll(&test_ll, buf, sizeof(buf)); + ck_assert_int_le(n, 0); /* 0 = timeout, -1 = error/would-block */ +} +END_TEST + +static Suite *wolfguard_suite(void) +{ + Suite *s = suite_create("wolfguard"); + TCase *tc; + + tc = tcase_create("wg_send"); + tcase_add_checked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_wg_send_ipv4); + tcase_add_test(tc, test_wg_send_short_frame); + tcase_add_test(tc, test_wg_send_unknown_etype_drop); + suite_add_tcase(s, tc); + + tc = tcase_create("arp_proxy"); + tcase_add_checked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_wg_arp_proxy_pending); + tcase_add_test(tc, test_wg_arp_proxy_poll_returns_reply); + tcase_add_test(tc, test_wg_arp_reply_fields); + tcase_add_test(tc, test_wg_arp_non_request_ignored); + tcase_add_test(tc, test_wg_send_arp_buf_too_small); + suite_add_tcase(s, tc); + + tc = tcase_create("wg_poll"); + tcase_add_checked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_wg_poll_prepends_eth_header); + tcase_add_test(tc, test_wg_poll_timeout_no_data); + tcase_add_test(tc, test_wg_poll_arp_reply_buf_too_small); + suite_add_tcase(s, tc); + + return s; +} + +int main(void) +{ + int failed; + Suite *s = wolfguard_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + failed = srunner_ntests_failed(sr); + srunner_free(sr); + return (failed == 0) ? 0 : 1; +} diff --git a/src/test/wolfguard/test_wolfguard.c b/src/test/wolfguard/test_wolfguard.c new file mode 100644 index 0000000..88ecffc --- /dev/null +++ b/src/test/wolfguard/test_wolfguard.c @@ -0,0 +1,467 @@ +/* test_wolfguard.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Functional test for wolfip + wolfguard integration (self-contained loopback). + * + * wolfip side (userspace) kernel peer side + * ───────────────────────── ───────────────────────── + * interface : wg-wip interface : wg-peer + * IP : 10.9.0.1/24 IP : 10.9.0.2/24 + * listen : 127.0.0.1:51830 listen : 127.0.0.1:51831 + * peer : wg-peer pubkey peer : wg-wip pubkey + * endpoint : 127.0.0.1:51831 endpoint : 127.0.0.1:51830 + * + * wolfip sends a UDP probe to 10.9.0.2:5555. + * The kernel decrypts it on wg-peer and delivers it to a host UDP socket + * bound to 0.0.0.0:5555 which echoes it back. + * The kernel re-encrypts via wg-peer toward 127.0.0.1:51830. + * wolfip receives and verifies the echo. + * + * PREREQUISITES + * ------------- + * - wolfguard.ko and libwolfssl.ko loaded + * - wg-fips in PATH + * - NET_ADMIN capability (run as root or with the capability) + * - /dev/net/tun accessible + * + * BUILD + * make build/test-wolfguard + * + * RUN + * ./build/test-wolfguard + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "wolfip.h" +#include "wolfip_wolfguard.h" +#include +#ifdef HAVE_FIPS +#include +#endif + +#define WG_LOCAL_IFNAME "wg-wip" +#define WG_PEER_IFNAME "wg-peer" +#define WG_LOCAL_PORT 51830 +#define WG_PEER_PORT 51831 +#define LOCAL_IP "10.9.0.1" +#define PEER_IP "10.9.0.2" +#define SUBNET_MASK "255.255.255.0" +#define ECHO_PORT 5555 +#define TEST_PAYLOAD "wolfip+wolfguard loopback" +#define POLL_TIMEOUT_MS 10000 + +/* Temp files for key material (base64 text) */ +#define TMP_LOCAL_PRIV "/tmp/.wg_local_priv.b64" +#define TMP_LOCAL_PUB "/tmp/.wg_local_pub.b64" +#define TMP_PEER_PRIV "/tmp/.wg_peer_priv.b64" +#define TMP_PEER_PUB "/tmp/.wg_peer_pub.b64" + +/* + * Generate a SECP256R1 keypair with wg-fips and save both as base64 text files. + */ +static int gen_keypair_files(const char *priv_file, const char *pub_file) +{ + char cmd[512]; + snprintf(cmd, sizeof(cmd), + "wg-fips genkey 2>/dev/null | tee '%s' | wg-fips pubkey > '%s' 2>/dev/null", + priv_file, pub_file); + if (system(cmd) != 0) { + fprintf(stderr, "gen_keypair_files: failed (is wg-fips in PATH?)\n"); + return -1; + } + return 0; +} + +/* + * Decode a base64 key file to raw bytes. + * Uses `base64 -d` via a temp file to avoid linking a base64 library. + */ +static int read_key_raw(const char *b64_file, uint8_t *out, size_t want) +{ + char tmp[256], cmd[512]; + FILE *f; + size_t n; + + snprintf(tmp, sizeof(tmp), "%s.bin", b64_file); + snprintf(cmd, sizeof(cmd), + "base64 -d '%s' > '%s' 2>/dev/null", b64_file, tmp); + if (system(cmd) != 0) { + fprintf(stderr, "read_key_raw: base64 -d failed for %s\n", b64_file); + return -EINVAL; + } + + f = fopen(tmp, "rb"); + if (!f) { unlink(tmp); return -errno; } + n = fread(out, 1, want, f); + fclose(f); + unlink(tmp); + + if (n != want) { + fprintf(stderr, "read_key_raw: got %zu bytes, expected %zu from %s\n", + n, want, b64_file); + return -EINVAL; + } + return 0; +} + +/* + * Read a base64 key file as a NUL-terminated string (newline stripped). + * Used when passing keys to wg-fips via shell commands. + */ +static int read_key_b64_str(const char *file, char *out, size_t outlen) +{ + FILE *f; + size_t n; + + f = fopen(file, "r"); + if (!f) return -errno; + n = fread(out, 1, outlen - 1, f); + fclose(f); + if (n == 0) return -EINVAL; + while (n > 0 && (out[n - 1] == '\n' || out[n - 1] == '\r')) + n--; + out[n] = '\0'; + return 0; +} + +/* + * Create and configure the kernel-side wolfguard interface (wg-peer). + * Requires that TMP_LOCAL_PUB already exists (local public key in base64). + */ +static int setup_kernel_peer(void) +{ + char cmd[512], local_pub_b64[256]; + int ret; + + /* Generate peer keypair */ + ret = gen_keypair_files(TMP_PEER_PRIV, TMP_PEER_PUB); + if (ret < 0) return ret; + + /* Read the wolfip side's public key (base64) */ + ret = read_key_b64_str(TMP_LOCAL_PUB, local_pub_b64, sizeof(local_pub_b64)); + if (ret < 0) { + fprintf(stderr, "setup_kernel_peer: cannot read %s\n", TMP_LOCAL_PUB); + return ret; + } + + /* Create the kernel wolfguard interface (EEXIST is acceptable) */ + snprintf(cmd, sizeof(cmd), + "ip link add '%s' type wolfguard 2>/dev/null", WG_PEER_IFNAME); + ret = system(cmd); + /* Verify the interface actually exists regardless of return code */ + snprintf(cmd, sizeof(cmd), "ip link show '%s' > /dev/null 2>&1", WG_PEER_IFNAME); + if (system(cmd) != 0) { + fprintf(stderr, "setup_kernel_peer: failed to create '%s' " + "(is wolfguard.ko loaded?)\n", WG_PEER_IFNAME); + return -1; + } + (void)ret; + + /* Configure: private key, listen port, and the wolfip peer */ + snprintf(cmd, sizeof(cmd), + "wg-fips set '%s'" + " listen-port %d" + " private-key '%s'" + " peer %s" + " endpoint 127.0.0.1:%d" + " persistent-keepalive 5" + " allowed-ips %s/32", + WG_PEER_IFNAME, WG_PEER_PORT, + TMP_PEER_PRIV, + local_pub_b64, + WG_LOCAL_PORT, + LOCAL_IP); + if (system(cmd) != 0) { + fprintf(stderr, "setup_kernel_peer: wg-fips set failed\n"); + return -1; + } + + /* Assign IP and bring up */ + snprintf(cmd, sizeof(cmd), + "ip addr add %s/24 dev '%s' 2>/dev/null; true", + PEER_IP, WG_PEER_IFNAME); + system(cmd); + snprintf(cmd, sizeof(cmd), "ip link set '%s' up", WG_PEER_IFNAME); + if (system(cmd) != 0) { + fprintf(stderr, "setup_kernel_peer: ip link set up failed\n"); + return -1; + } + + return 0; +} + +static void teardown_kernel_peer(void) +{ + char cmd[256]; + snprintf(cmd, sizeof(cmd), + "ip link del '%s' 2>/dev/null; true", WG_PEER_IFNAME); + system(cmd); + unlink(TMP_LOCAL_PRIV); + unlink(TMP_LOCAL_PUB); + unlink(TMP_PEER_PRIV); + unlink(TMP_PEER_PUB); +} + +static uint64_t ms_now(void) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + return (uint64_t)tv.tv_sec * 1000u + (uint64_t)tv.tv_usec / 1000u; +} + +/* + * UDP loopback test + * + * Opens a host (kernel) UDP echo socket at 0.0.0.0:ECHO_PORT and a wolfip + * UDP socket, then drives both in the same poll loop. No threads needed. + * */ +static int test_udp_ping(struct wolfIP *stack) +{ + struct wolfIP_sockaddr_in peer_sa, local_sa; + struct sockaddr_in echo_local, echo_from; + socklen_t fromlen; + int wolfip_sock, echo_fd, flags; + int sent = 0, n, ret; + uint64_t deadline, last_send_at; + uint8_t echo_buf[512], recv_buf[512]; + const char *payload = TEST_PAYLOAD; + size_t payload_len = strlen(payload); + + /* + * Host-side non-blocking echo socket (kernel peer side of the tunnel). + * The kernel decrypts wolfguard traffic on wg-peer and delivers it + * here; this socket echoes it straight back. + * */ + echo_fd = socket(AF_INET, SOCK_DGRAM, 0); + if (echo_fd < 0) { + fprintf(stderr, "test_udp_ping: socket() failed: %s\n", strerror(errno)); + return -1; + } + setsockopt(echo_fd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)); + memset(&echo_local, 0, sizeof(echo_local)); + echo_local.sin_family = AF_INET; + echo_local.sin_port = htons(ECHO_PORT); + echo_local.sin_addr.s_addr = INADDR_ANY; + if (bind(echo_fd, (struct sockaddr *)&echo_local, sizeof(echo_local)) < 0) { + fprintf(stderr, "test_udp_ping: bind echo socket: %s\n", strerror(errno)); + close(echo_fd); + return -1; + } + flags = fcntl(echo_fd, F_GETFL, 0); + fcntl(echo_fd, F_SETFL, flags | O_NONBLOCK); + + /* + * wolfip UDP socket (wolfip side of the tunnel). + * */ + wolfip_sock = wolfIP_sock_socket(stack, AF_INET, IPSTACK_SOCK_DGRAM, 0); + if (wolfip_sock < 0) { + fprintf(stderr, "test_udp_ping: wolfIP_sock_socket failed: %d\n", wolfip_sock); + close(echo_fd); + return -1; + } + + /* Bind to a fixed port so the echo reply can find us */ + memset(&local_sa, 0, sizeof(local_sa)); + local_sa.sin_family = AF_INET; + local_sa.sin_port = ee16(4444); + local_sa.sin_addr.s_addr = 0; + wolfIP_sock_bind(stack, wolfip_sock, + (struct wolfIP_sockaddr *)&local_sa, sizeof(local_sa)); + + /* Peer address: kernel echo socket reachable via wg-peer */ + memset(&peer_sa, 0, sizeof(peer_sa)); + peer_sa.sin_family = AF_INET; + peer_sa.sin_port = ee16(ECHO_PORT); + peer_sa.sin_addr.s_addr = inet_addr(PEER_IP); + + deadline = ms_now() + POLL_TIMEOUT_MS; + last_send_at = 0; + ret = -1; + + printf("[test] polling (timeout %d ms)...\n", POLL_TIMEOUT_MS); + + while (ms_now() < deadline) { + uint64_t t = ms_now(); + + /* Drive the wolfip stack */ + wolfIP_poll(stack, t); + + /* Attempt to send every 200 ms until confirmed sent */ + if (!sent && (t - last_send_at) >= 200u) { + int r = wolfIP_sock_sendto(stack, wolfip_sock, + payload, payload_len, 0, + (struct wolfIP_sockaddr *)&peer_sa, + sizeof(peer_sa)); + last_send_at = t; + if (r >= 0) { + printf("[test] UDP probe sent (%d bytes) to %s:%d\n", + r, PEER_IP, ECHO_PORT); + sent = 1; + } + /* -EAGAIN is normal while ARP is resolving; other errors abort */ + if (r < 0 && r != -WOLFIP_EAGAIN) { + fprintf(stderr, "[test] sendto error: %d\n", r); + break; + } + } + + /* Service the host echo socket */ + fromlen = sizeof(echo_from); + n = recvfrom(echo_fd, echo_buf, sizeof(echo_buf), 0, + (struct sockaddr *)&echo_from, &fromlen); + if (n > 0) { + printf("[echo] %d bytes from %s:%d — echoing\n", + n, inet_ntoa(echo_from.sin_addr), ntohs(echo_from.sin_port)); + sendto(echo_fd, echo_buf, (size_t)n, 0, + (struct sockaddr *)&echo_from, fromlen); + } + + /* Check if the wolfip stack received the echo */ + if (wolfIP_sock_can_read(stack, wolfip_sock)) { + n = wolfIP_sock_recvfrom(stack, wolfip_sock, + recv_buf, sizeof(recv_buf) - 1, + 0, NULL, NULL); + if (n > 0) { + recv_buf[n] = '\0'; + printf("[test] echo received: \"%s\" (%d bytes)\n", + (char *)recv_buf, n); + if ((size_t)n == payload_len && + memcmp(recv_buf, payload, payload_len) == 0) { + printf("[test] PASS: payload matches\n"); + ret = 0; + } else { + fprintf(stderr, "[test] FAIL: payload mismatch\n"); + } + break; + } + } + + usleep(2000); /* 2 ms */ + } + + if (ret != 0 && ms_now() >= deadline) + fprintf(stderr, "[test] FAIL: timed out after %d ms\n", POLL_TIMEOUT_MS); + + wolfIP_sock_close(stack, wolfip_sock); + close(echo_fd); + return ret; +} + +int main(void) +{ + struct wolfIP_wg_config cfg; + struct wolfIP_ll_dev *ll; + struct wolfIP *stack; + int ret; + + printf("=== wolfip + wolfguard functional test ===\n"); + + /* generate local (wolfip-side) keypair */ + printf("[setup] generating local keypair...\n"); + if (gen_keypair_files(TMP_LOCAL_PRIV, TMP_LOCAL_PUB) < 0) + return 1; + + /* create and configure kernel peer interface */ + printf("[setup] configuring kernel peer (%s)...\n", WG_PEER_IFNAME); + if (setup_kernel_peer() < 0) { + unlink(TMP_LOCAL_PRIV); + unlink(TMP_LOCAL_PUB); + return 1; + } + + /* build wolfIP_wg_config */ + memset(&cfg, 0, sizeof(cfg)); + strncpy(cfg.ifname, WG_LOCAL_IFNAME, sizeof(cfg.ifname) - 1); + cfg.listen_port = WG_LOCAL_PORT; + cfg.num_peers = 1; + + ret = read_key_raw(TMP_LOCAL_PRIV, cfg.private_key, WG_PRIVATE_KEY_LEN); + if (ret < 0) { + fprintf(stderr, "main: failed to read local private key: %d\n", ret); + goto fail; + } + + ret = read_key_raw(TMP_PEER_PUB, cfg.peers[0].public_key, WG_PUBLIC_KEY_LEN); + if (ret < 0) { + fprintf(stderr, "main: failed to read peer public key: %d\n", ret); + goto fail; + } + + { + struct sockaddr_in *sin = (struct sockaddr_in *)&cfg.peers[0].endpoint; + sin->sin_family = AF_INET; + sin->sin_port = htons(WG_PEER_PORT); + sin->sin_addr.s_addr = inet_addr("127.0.0.1"); + } + + cfg.peers[0].allowed_ip = atoip4(PEER_IP); /* host byte order */ + cfg.peers[0].allowed_cidr = 32; + cfg.peers[0].keepalive_interval = 5; + + /* initialize the wolfip stack and attach the wolfguard driver */ + wolfIP_init_static(&stack); + ll = wolfIP_getdev(stack); + + printf("[setup] initializing wolfip + wolfguard (%s)...\n", WG_LOCAL_IFNAME); + ret = wolfIP_wg_init(&cfg, ll); + if (ret < 0) { + fprintf(stderr, "main: wolfIP_wg_init failed: %d\n", ret); + goto fail; + } + + wolfIP_ipconfig_set(stack, + atoip4(LOCAL_IP), + atoip4(SUBNET_MASK), + atoip4(LOCAL_IP)); + + printf("[setup] interfaces up — running UDP loopback test...\n"); + ret = test_udp_ping(stack); + + wolfIP_wg_teardown(WG_LOCAL_IFNAME); + teardown_kernel_peer(); +#ifdef HAVE_FIPS + { + int fips_status = wolfCrypt_GetStatus_fips(); + printf("[fips] status: %s (code %d)\n", + fips_status == 0 ? "FIPS mode active" : "NOT in FIPS mode", + fips_status); + } +#endif + printf("=== %s ===\n", ret == 0 ? "PASS" : "FAIL"); + return ret == 0 ? 0 : 1; + +fail: + teardown_kernel_peer(); + return 1; +} diff --git a/src/wolfesp.c b/src/wolfesp.c index 61069a3..ec602b0 100644 --- a/src/wolfesp.c +++ b/src/wolfesp.c @@ -46,6 +46,11 @@ int wolfIP_esp_init(void) wolfIP_esp_sa_del_all(); +/* this callback gets called only if the wolfssl is built in FIPS mode. */ +#ifdef WC_RNG_SEED_CB + wc_SetSeed_Cb(wc_GenerateSeed); +#endif + if (rng_inited == 0) { err = wc_InitRng_ex(&wc_rng, NULL, INVALID_DEVID); if (err) { diff --git a/src/wolfip_wolfguard.c b/src/wolfip_wolfguard.c new file mode 100644 index 0000000..9ec7f6e --- /dev/null +++ b/src/wolfip_wolfguard.c @@ -0,0 +1,836 @@ +/* wolfip_wolfguard.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#ifdef WOLFIP_WOLFGUARD + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define WOLF_POSIX +#include "config.h" +#include "wolfip.h" +#undef WOLF_POSIX + +#include "wolfip_wolfguard.h" + +/* + * WolfGuard Generic Netlink constants (from wolfguard uapi header) + * */ +#define WG_CMD_GET_DEVICE 0 +#define WG_CMD_SET_DEVICE 1 + +/* wgdevice_attribute */ +#define WGDEVICE_A_IFINDEX 1 +#define WGDEVICE_A_IFNAME 2 +#define WGDEVICE_A_PRIVATE_KEY 3 +#define WGDEVICE_A_PUBLIC_KEY 4 +#define WGDEVICE_A_FLAGS 5 +#define WGDEVICE_A_LISTEN_PORT 6 +#define WGDEVICE_A_FWMARK 7 +#define WGDEVICE_A_PEERS 8 + +/* wgpeer_flag */ +#define WGPEER_F_REPLACE_ALLOWEDIPS (1U << 1) + +/* wgpeer_attribute */ +#define WGPEER_A_PUBLIC_KEY 1 +#define WGPEER_A_PRESHARED_KEY 2 +#define WGPEER_A_FLAGS 3 +#define WGPEER_A_ENDPOINT 4 +#define WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL 5 +#define WGPEER_A_ALLOWEDIPS 9 + +/* wgallowedip_attribute */ +#define WGALLOWEDIP_A_FAMILY 1 +#define WGALLOWEDIP_A_IPADDR 2 +#define WGALLOWEDIP_A_CIDR_MASK 3 + +/* ethernet / arp constants */ +#define WG_ETH_HDR_LEN 14 +/* 14-byte eth hdr + 28-byte ARP payload */ +#define WG_ARP_PKT_LEN 42 + +#define WG_ETH_TYPE_IP 0x0800 +#define WG_ETH_TYPE_ARP 0x0806 + +#define WG_ARP_REQUEST 1 +#define WG_ARP_REPLY 2 + +/* + * Fixed dummy MAC addresses used to satisfy wolfip's Ethernet layer. + * The wolfguard TUN interface is Layer-3; these MACs have no meaning on + * the wire, they are technically only seen by the wolfip stack. + * + * local_mac: assigned to ll->mac (wolfip's own MAC on this interface). + * peer_mac: returned in synthetic ARP replies as the "peer's" MAC. + */ +static const uint8_t wg_local_mac[6] = {0x02, 0x00, 0x57, 0x47, 0x00, 0x01}; +static const uint8_t wg_peer_mac[6] = {0x02, 0x00, 0x57, 0x47, 0x00, 0x02}; + +/* + * Driver state + * */ +struct wg_dev { + int tun_fd; /* AF_PACKET/SOCK_DGRAM socket (or pipe fd in unit tests) */ + int ifindex; /* interface index — 0 only in unit tests */ + char ifname[16]; + /* Pending synthetic ARP reply to return on next wg_poll() call */ + uint8_t arp_reply[WG_ARP_PKT_LEN]; + int arp_reply_pending; +}; + +/* One global instance, mirrors tap_linux.c's pattern of a single static fd */ +static struct wg_dev wg_state; + +/* + * Minimal netlink attribute helpers + */ + +#ifndef NLA_ALIGNTO +# define NLA_ALIGNTO 4 +#endif +#ifndef NLA_ALIGN +# define NLA_ALIGN(len) (((len) + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1)) +#endif +#ifndef NLA_HDRLEN +# define NLA_HDRLEN ((int)NLA_ALIGN(sizeof(struct nlattr))) +#endif + +static void nla_put_raw(uint8_t *buf, size_t *off, uint16_t type, + const void *data, uint16_t dlen) +{ + struct nlattr *nla = (struct nlattr *)(buf + *off); + nla->nla_type = type; + nla->nla_len = (uint16_t)(NLA_HDRLEN + dlen); + if (data && dlen) + memcpy((uint8_t *)nla + NLA_HDRLEN, data, dlen); + *off += (size_t)NLA_ALIGN(nla->nla_len); +} + +static void nla_put_u8(uint8_t *buf, size_t *off, uint16_t type, uint8_t v) +{ + nla_put_raw(buf, off, type, &v, sizeof(v)); +} + +static void nla_put_u16(uint8_t *buf, size_t *off, uint16_t type, uint16_t v) +{ + nla_put_raw(buf, off, type, &v, sizeof(v)); +} + +static void nla_put_u32(uint8_t *buf, size_t *off, uint16_t type, uint32_t v) +{ + nla_put_raw(buf, off, type, &v, sizeof(v)); +} + +static void nla_put_str(uint8_t *buf, size_t *off, uint16_t type, + const char *str) +{ + nla_put_raw(buf, off, type, str, (uint16_t)(strlen(str) + 1)); +} + +/* Begin a nested attribute; returns the offset of the attribute header so + * nla_nested_end() can fill in the final length. */ +static size_t nla_nested_start(uint8_t *buf, size_t *off, uint16_t type) +{ + size_t start = *off; + struct nlattr *nla = (struct nlattr *)(buf + start); + nla->nla_type = (uint16_t)(type | NLA_F_NESTED); + *off += (size_t)NLA_HDRLEN; + return start; +} + +static void nla_nested_end(uint8_t *buf, size_t start, size_t *off) +{ + struct nlattr *nla = (struct nlattr *)(buf + start); + nla->nla_len = (uint16_t)(*off - start); + /* Pad to 4-byte boundary */ + if (*off % NLA_ALIGNTO) + *off += NLA_ALIGNTO - (*off % NLA_ALIGNTO); +} + +/* + * Generic Netlink: resolve family ID for "wolfguard" + * */ + +/* These are standard kernel genetlink controller definitions. They are part + * of the stable kernel ABI and safe to define here if not provided by the + * system headers. */ +#ifndef GENL_ID_CTRL +# define GENL_ID_CTRL 0x10 +#endif +#ifndef CTRL_CMD_GETFAMILY +# define CTRL_CMD_GETFAMILY 3 +#endif +#ifndef CTRL_ATTR_FAMILY_NAME +# define CTRL_ATTR_FAMILY_NAME 2 +#endif +#ifndef CTRL_ATTR_FAMILY_ID +# define CTRL_ATTR_FAMILY_ID 1 +#endif + +static int wg_get_genl_family_id(uint16_t *family_id) +{ + uint8_t buf[512]; + struct nlmsghdr *nlh; + struct genlmsghdr *genl; + struct sockaddr_nl sa; + struct nlattr *nla; + int sock, ret, rem; + size_t off = 0; + + memset(buf, 0, sizeof(buf)); + memset(&sa, 0, sizeof(sa)); + + sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_GENERIC); + if (sock < 0) + return -errno; + + sa.nl_family = AF_NETLINK; + if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + close(sock); + return -errno; + } + + /* Build CTRL_CMD_GETFAMILY request */ + nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_type = GENL_ID_CTRL; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nlh->nlmsg_seq = 1; + off += NLMSG_HDRLEN; + + genl = (struct genlmsghdr *)(buf + off); + genl->cmd = CTRL_CMD_GETFAMILY; + genl->version = 1; + off += NLMSG_ALIGN(sizeof(*genl)); + + nla_put_str(buf, &off, CTRL_ATTR_FAMILY_NAME, WG_GENL_NAME); + nlh->nlmsg_len = (uint32_t)off; + + ret = (int)send(sock, buf, off, 0); + if (ret < 0) { + close(sock); + return -errno; + } + + ret = (int)recv(sock, buf, sizeof(buf), 0); + close(sock); + if (ret < 0) + return -errno; + + nlh = (struct nlmsghdr *)buf; + if (nlh->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(nlh); + return err->error; + } + + /* Walk attributes to find CTRL_ATTR_FAMILY_ID */ + nla = (struct nlattr *)(buf + NLMSG_HDRLEN + NLMSG_ALIGN(sizeof(*genl))); + rem = (int)nlh->nlmsg_len - NLMSG_HDRLEN - (int)NLMSG_ALIGN(sizeof(*genl)); + + while (rem >= NLA_HDRLEN) { + if (nla->nla_type == CTRL_ATTR_FAMILY_ID) { + memcpy(family_id, (uint8_t *)nla + NLA_HDRLEN, sizeof(*family_id)); + return 0; + } + rem -= NLA_ALIGN(nla->nla_len); + nla = (struct nlattr *)((uint8_t *)nla + NLA_ALIGN(nla->nla_len)); + } + + return -ENOENT; +} + +/* + * Interface creation / deletion via NETLINK_ROUTE + * */ + +static int wg_iface_create(const char *ifname) +{ + uint8_t buf[512]; + struct nlmsghdr *nlh; + struct ifinfomsg *ifi; + struct sockaddr_nl sa; + int sock, ret; + size_t off = 0, li_start; + + memset(buf, 0, sizeof(buf)); + memset(&sa, 0, sizeof(sa)); + + sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + if (sock < 0) + return -errno; + + sa.nl_family = AF_NETLINK; + if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + close(sock); + return -errno; + } + + /* Build RTM_NEWLINK */ + nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_type = RTM_NEWLINK; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL; + nlh->nlmsg_seq = 1; + off += NLMSG_HDRLEN; + + ifi = (struct ifinfomsg *)(buf + off); + memset(ifi, 0, sizeof(*ifi)); + ifi->ifi_family = AF_UNSPEC; + off += NLMSG_ALIGN(sizeof(*ifi)); + + nla_put_str(buf, &off, IFLA_IFNAME, ifname); + + /* IFLA_LINKINFO -> IFLA_INFO_KIND = "wolfguard" */ + li_start = nla_nested_start(buf, &off, IFLA_LINKINFO); + nla_put_str(buf, &off, IFLA_INFO_KIND, WG_GENL_NAME); + nla_nested_end(buf, li_start, &off); + + nlh->nlmsg_len = (uint32_t)off; + + ret = (int)send(sock, buf, off, 0); + if (ret < 0) { + close(sock); + return -errno; + } + + ret = (int)recv(sock, buf, sizeof(buf), 0); + close(sock); + if (ret < 0) + return -errno; + + nlh = (struct nlmsghdr *)buf; + if (nlh->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(nlh); + if (err->error == 0) + return 0; /* ACK */ + if (err->error == -EEXIST) + return 0; /* Interface already exists — reuse it */ + return err->error; + } + return 0; +} + +static void wg_iface_delete(const char *ifname) +{ + uint8_t buf[256]; + struct nlmsghdr *nlh; + struct ifinfomsg *ifi; + struct sockaddr_nl sa; + int sock; + size_t off = 0; + + memset(buf, 0, sizeof(buf)); + memset(&sa, 0, sizeof(sa)); + + sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + if (sock < 0) + return; + + sa.nl_family = AF_NETLINK; + if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + close(sock); + return; + } + + nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_type = RTM_DELLINK; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nlh->nlmsg_seq = 1; + off += NLMSG_HDRLEN; + + ifi = (struct ifinfomsg *)(buf + off); + memset(ifi, 0, sizeof(*ifi)); + ifi->ifi_family = AF_UNSPEC; + off += NLMSG_ALIGN(sizeof(*ifi)); + + nla_put_str(buf, &off, IFLA_IFNAME, ifname); + nlh->nlmsg_len = (uint32_t)off; + + (void)send(sock, buf, off, 0); + (void)recv(sock, buf, sizeof(buf), 0); + close(sock); +} + +/* + * wolfguard device configuration via Generic Netlink (WG_CMD_SET_DEVICE) + * */ + +static int wg_configure(const struct wolfIP_wg_config *cfg, uint16_t family_id) +{ + uint8_t buf[4096]; + struct nlmsghdr *nlh; + struct genlmsghdr *genl; + struct sockaddr_nl sa; + int sock, ret, i; + size_t off = 0, peers_start, peer_start, aips_start, aip_start; + + memset(buf, 0, sizeof(buf)); + memset(&sa, 0, sizeof(sa)); + + sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_GENERIC); + if (sock < 0) + return -errno; + + sa.nl_family = AF_NETLINK; + if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + close(sock); + return -errno; + } + + nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_type = family_id; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + nlh->nlmsg_seq = 2; + off += NLMSG_HDRLEN; + + genl = (struct genlmsghdr *)(buf + off); + genl->cmd = WG_CMD_SET_DEVICE; + genl->version = WG_GENL_VERSION; + off += NLMSG_ALIGN(sizeof(*genl)); + + /* Device-level attributes */ + nla_put_str(buf, &off, WGDEVICE_A_IFNAME, cfg->ifname); + nla_put_raw(buf, &off, WGDEVICE_A_PRIVATE_KEY, + cfg->private_key, WG_PRIVATE_KEY_LEN); + nla_put_u16(buf, &off, WGDEVICE_A_LISTEN_PORT, cfg->listen_port); + + /* Peers */ + if (cfg->num_peers > 0) { + peers_start = nla_nested_start(buf, &off, WGDEVICE_A_PEERS); + + for (i = 0; i < cfg->num_peers; i++) { + const struct wolfIP_wg_peer *p = &cfg->peers[i]; + uint32_t pflags; + int ep_len; + struct in_addr addr4; + + peer_start = nla_nested_start(buf, &off, 0); + + nla_put_raw(buf, &off, WGPEER_A_PUBLIC_KEY, + p->public_key, WG_PUBLIC_KEY_LEN); + + /* Replace any existing allowed IPs for this peer */ + pflags = WGPEER_F_REPLACE_ALLOWEDIPS; + nla_put_u32(buf, &off, WGPEER_A_FLAGS, pflags); + + /* Endpoint (sockaddr_in or sockaddr_in6 in network byte order) */ + ep_len = (p->endpoint.ss_family == AF_INET6) + ? (int)sizeof(struct sockaddr_in6) + : (int)sizeof(struct sockaddr_in); + nla_put_raw(buf, &off, WGPEER_A_ENDPOINT, &p->endpoint, + (uint16_t)ep_len); + + /* Optional persistent keep-alive */ + if (p->keepalive_interval > 0) + nla_put_u16(buf, &off, WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL, + p->keepalive_interval); + + /* Allowed IPs (one IPv4 entry per peer for now) */ + aips_start = nla_nested_start(buf, &off, WGPEER_A_ALLOWEDIPS); + aip_start = nla_nested_start(buf, &off, 0); + + nla_put_u16(buf, &off, WGALLOWEDIP_A_FAMILY, + (uint16_t)AF_INET); + /* ip4 is host byte order; convert to network byte order */ + addr4.s_addr = htonl(p->allowed_ip); + nla_put_raw(buf, &off, WGALLOWEDIP_A_IPADDR, + &addr4, (uint16_t)sizeof(addr4)); + nla_put_u8(buf, &off, WGALLOWEDIP_A_CIDR_MASK, + p->allowed_cidr); + + nla_nested_end(buf, aip_start, &off); + nla_nested_end(buf, aips_start, &off); + nla_nested_end(buf, peer_start, &off); + } + + nla_nested_end(buf, peers_start, &off); + } + + nlh->nlmsg_len = (uint32_t)off; + + ret = (int)send(sock, buf, off, 0); + if (ret < 0) { + close(sock); + return -errno; + } + + ret = (int)recv(sock, buf, sizeof(buf), 0); + close(sock); + if (ret < 0) + return -errno; + + nlh = (struct nlmsghdr *)buf; + if (nlh->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(nlh); + return err->error; + } + return 0; +} + +/* + * Interface bring-up (ioctl) + * */ + +static int wg_iface_up(const char *ifname) +{ + struct ifreq ifr; + int sock; + + sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (sock < 0) + return -errno; + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); + + if (ioctl(sock, SIOCGIFFLAGS, &ifr) < 0) { + close(sock); + return -errno; + } + ifr.ifr_flags |= (IFF_UP | IFF_RUNNING); + if (ioctl(sock, SIOCSIFFLAGS, &ifr) < 0) { + close(sock); + return -errno; + } + close(sock); + return 0; +} + +/* + * AF_PACKET/SOCK_DGRAM socket bound to the wolfguard interface. + * + * wolfguard (like WireGuard) is an ARPHRD_NONE netdev — it is NOT a TUN/TAP + * device and cannot be opened via /dev/net/tun. An AF_PACKET/SOCK_DGRAM + * socket bound to the interface lets us inject and receive raw IP packets + * directly through the kernel interface, which the wolfguard module then + * encrypts/decrypts transparently. + * */ + +static int wg_open_packet_socket(const char *ifname) +{ + struct sockaddr_ll sll; + int sock, ifidx, flags; + + sock = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_IP)); + if (sock < 0) + return -errno; + + ifidx = (int)if_nametoindex(ifname); + if (ifidx == 0) { + close(sock); + return -ENODEV; + } + + memset(&sll, 0, sizeof(sll)); + sll.sll_family = AF_PACKET; + sll.sll_protocol = htons(ETH_P_IP); + sll.sll_ifindex = ifidx; + if (bind(sock, (struct sockaddr *)&sll, sizeof(sll)) < 0) { + close(sock); + return -errno; + } + + flags = fcntl(sock, F_GETFL, 0); + if (flags >= 0) + (void)fcntl(sock, F_SETFL, flags | O_NONBLOCK); + + wg_state.ifindex = ifidx; + return sock; +} + +/* + * ARP proxy helpers + * + * wolfip sends Ethernet frames (L2). The wolfguard TUN interface is L3. + * When wolfip tries to resolve an IP via ARP, wg_send() intercepts the ARP + * request and queues a synthetic ARP reply in wg_state.arp_reply[]. + * The next wg_poll() call returns that reply to wolfip before checking the + * TUN fd. This satisfies wolfip's ARP machinery without kernel involvement. + * */ + +/* + * Build a synthetic ARP reply frame. + * + * @req: The 42-byte ARP request received from wolfip via wg_send(). + * @out: Buffer for the 42-byte ARP reply to be returned via wg_poll(). + * + * The reply tells wolfip that every IP is reachable at wg_peer_mac, + * so wolfip can forward outbound traffic to the wolfguard TUN fd. + */ +static void build_arp_reply(const uint8_t *req, uint8_t *out) +{ + /* + * ARP packet layout (42 bytes): + * [0..5] Ethernet dst MAC + * [6..11] Ethernet src MAC + * [12..13] Ethertype (0x0806) + * [14..15] htype (0x0001 = Ethernet) + * [16..17] ptype (0x0800 = IPv4) + * [18] hlen (6) + * [19] plen (4) + * [20..21] opcode (1=request, 2=reply) + * [22..27] sender MAC + * [28..31] sender IP + * [32..37] target MAC + * [38..41] target IP + */ + memcpy(out, req, WG_ARP_PKT_LEN); + + /* Ethernet header: reply goes back to whoever sent the request */ + memcpy(out + 0, req + 6, 6); /* dst = request's src MAC */ + memcpy(out + 6, wg_peer_mac, 6); /* src = peer's dummy MAC */ + /* ethertype [12..13] stays 0x0806 */ + + /* ARP opcode: reply */ + out[20] = 0x00; + out[21] = WG_ARP_REPLY; + + /* Sender: we claim to be the peer answering for the requested IP */ + memcpy(out + 22, wg_peer_mac, 6); /* sender MAC = peer's dummy MAC */ + memcpy(out + 28, req + 38, 4); /* sender IP = target IP from request */ + + /* Target: original requester */ + memcpy(out + 32, req + 22, 6); /* target MAC = sender MAC from request */ + memcpy(out + 38, req + 28, 4); /* target IP = sender IP from request */ +} + +/* + * wolfIP_ll_dev callbacks + * */ + +/* + * wg_poll() - Receive a packet from the wolfguard interface. + * + * Priority 1: If a synthetic ARP reply is pending, return it immediately. + * Priority 2: Poll the TUN fd for an incoming (decrypted) IP packet from + * a peer and prepend a 14-byte Ethernet header so that wolfip + * can process it normally. + */ +static int wg_poll(struct wolfIP_ll_dev *ll, void *buf, uint32_t len) +{ + struct wg_dev *dev = &wg_state; + struct pollfd pfd; + int ret; + uint8_t *b = (uint8_t *)buf; + (void)ll; + + /* Return any pending ARP reply first */ + if (dev->arp_reply_pending) { + if (len < WG_ARP_PKT_LEN) + return -1; + memcpy(b, dev->arp_reply, WG_ARP_PKT_LEN); + dev->arp_reply_pending = 0; + return WG_ARP_PKT_LEN; + } + + /* Poll the TUN fd with a short timeout */ + pfd.fd = dev->tun_fd; + pfd.events = POLLIN; + ret = poll(&pfd, 1, 2); + if (ret <= 0) + return ret; /* 0 = timeout, <0 = error */ + + if (len < WG_ETH_HDR_LEN) + return -1; + + /* Read the raw IP packet — recvfrom for packet socket, read for unit tests */ + if (dev->ifindex != 0) + ret = (int)recvfrom(dev->tun_fd, b + WG_ETH_HDR_LEN, + len - WG_ETH_HDR_LEN, 0, NULL, NULL); + else + ret = (int)read(dev->tun_fd, b + WG_ETH_HDR_LEN, + len - WG_ETH_HDR_LEN); + if (ret <= 0) + return ret; + + /* Prepend a synthetic Ethernet header (IPv4) */ + memcpy(b + 0, wg_local_mac, 6); /* dst = our own MAC */ + memcpy(b + 6, wg_peer_mac, 6); /* src = peer's MAC */ + b[12] = 0x08; /* ethertype = 0x0800 */ + b[13] = 0x00; + + return ret + WG_ETH_HDR_LEN; +} + +/* + * wg_send() - Transmit a packet from wolfip onto the wolfguard interface. + * + * Ethernet frames from wolfip are handled as follows: + * - IPv4 (0x0800): strip the 14-byte Ethernet header and write the raw + * IP packet to the TUN fd. wolfguard encrypts and sends. + * - ARP (0x0806, op=1): synthesize a reply and queue it for wg_poll(). + * - Other: silently dropped (no ARP6, no RARP, etc. on a VPN TUN link). + */ +static int wg_send(struct wolfIP_ll_dev *ll, void *buf, uint32_t len) +{ + struct wg_dev *dev = &wg_state; + uint8_t *b = (uint8_t *)buf; + uint16_t etype; + (void)ll; + + if (len < WG_ETH_HDR_LEN) + return -1; + + etype = (uint16_t)((b[12] << 8) | b[13]); + + if (etype == WG_ETH_TYPE_IP) { + int written; + if (dev->ifindex != 0) { + /* Production: inject raw IP via AF_PACKET/SOCK_DGRAM sendto */ + struct sockaddr_ll sll; + memset(&sll, 0, sizeof(sll)); + sll.sll_family = AF_PACKET; + sll.sll_ifindex = dev->ifindex; + sll.sll_protocol = htons(WG_ETH_TYPE_IP); + sll.sll_halen = 0; + written = (int)sendto(dev->tun_fd, b + WG_ETH_HDR_LEN, + len - WG_ETH_HDR_LEN, 0, + (struct sockaddr *)&sll, sizeof(sll)); + } else { + /* Unit test: mock fd is a pipe — use plain write */ + written = (int)write(dev->tun_fd, b + WG_ETH_HDR_LEN, + len - WG_ETH_HDR_LEN); + } + return (written < 0) ? -1 : 0; + } + + if (etype == WG_ETH_TYPE_ARP) { + /* Only handle ARP requests (op == 1) */ + if (len < WG_ARP_PKT_LEN) + return -1; + if (b[20] == 0x00 && b[21] == WG_ARP_REQUEST) { + build_arp_reply(b, dev->arp_reply); + dev->arp_reply_pending = 1; + } + return 0; + } + + /* Unknown ethertype — drop */ + return 0; +} + +/* + * Public API + * */ + +int wolfIP_wg_init(struct wolfIP_wg_config *cfg, struct wolfIP_ll_dev *ll) +{ + uint16_t family_id = 0; + int ret; + + if (!cfg || !ll) + return -EINVAL; + + if (cfg->num_peers < 0 || cfg->num_peers > WOLFIP_WG_MAX_PEERS) { + fprintf(stderr, "wolfIP_wg_init: num_peers %d out of range (max %d)\n", + cfg->num_peers, WOLFIP_WG_MAX_PEERS); + return -EINVAL; + } + + memset(&wg_state, 0, sizeof(wg_state)); + wg_state.tun_fd = -1; + strncpy(wg_state.ifname, cfg->ifname, sizeof(wg_state.ifname) - 1); + + /* Create the wolfguard network interface */ + ret = wg_iface_create(cfg->ifname); + if (ret < 0) { + fprintf(stderr, "wolfIP_wg_init: failed to create interface '%s': %d\n", + cfg->ifname, ret); + return ret; + } + + /* Resolve the wolfguard Generic Netlink family ID */ + ret = wg_get_genl_family_id(&family_id); + if (ret < 0) { + fprintf(stderr, "wolfIP_wg_init: wolfguard genl family not found " + "(is wolfguard.ko loaded?): %d\n", ret); + wg_iface_delete(cfg->ifname); + return ret; + } + + /* Configure keys and peers */ + ret = wg_configure(cfg, family_id); + if (ret < 0) { + fprintf(stderr, "wolfIP_wg_init: WG_CMD_SET_DEVICE failed: %d\n", ret); + wg_iface_delete(cfg->ifname); + return ret; + } + + /* Bring the interface UP */ + ret = wg_iface_up(cfg->ifname); + if (ret < 0) { + fprintf(stderr, "wolfIP_wg_init: failed to bring up '%s': %d\n", + cfg->ifname, ret); + wg_iface_delete(cfg->ifname); + return ret; + } + + /* Open AF_PACKET socket bound to the wolfguard interface */ + ret = wg_open_packet_socket(cfg->ifname); + if (ret < 0) { + fprintf(stderr, "wolfIP_wg_init: failed to open packet socket for '%s': %d\n", + cfg->ifname, ret); + wg_iface_delete(cfg->ifname); + return ret; + } + wg_state.tun_fd = ret; + + /* Populate the wolfIP_ll_dev */ + memset(ll, 0, sizeof(*ll)); + memcpy(ll->mac, wg_local_mac, 6); + strncpy(ll->ifname, cfg->ifname, sizeof(ll->ifname) - 1); + ll->poll = wg_poll; + ll->send = wg_send; + + return 0; +} + +void wolfIP_wg_teardown(const char *ifname) +{ + if (wg_state.tun_fd >= 0) { + close(wg_state.tun_fd); + wg_state.tun_fd = -1; + } + if (ifname && *ifname) + wg_iface_delete(ifname); + memset(&wg_state, 0, sizeof(wg_state)); + wg_state.tun_fd = -1; +} + +#endif /* WOLFIP_WOLFGUARD */ diff --git a/wolfip_wolfguard.h b/wolfip_wolfguard.h new file mode 100644 index 0000000..e2002c4 --- /dev/null +++ b/wolfip_wolfguard.h @@ -0,0 +1,110 @@ +/* wolfip_wolfguard.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#ifndef WOLFIP_WOLFGUARD_H +#define WOLFIP_WOLFGUARD_H + +#ifdef WOLFIP_WOLFGUARD + +#include +#include /* struct sockaddr_storage */ +#include "wolfip.h" /* wolfIP_ll_dev, ip4 */ + +/* WolfGuard Generic Netlink family name and version */ +#define WG_GENL_NAME "wolfguard" +#define WG_GENL_VERSION 1 + +/* + * Key sizes (SECP256R1). + * Public key length depends on whether compressed keys are used. + * Default (no WG_USE_PUBLIC_KEY_COMPRESSION): 65-byte uncompressed point. + */ +#define WG_PRIVATE_KEY_LEN 32 +#ifndef WG_USE_PUBLIC_KEY_COMPRESSION +# define WG_PUBLIC_KEY_LEN 65 +#else +# define WG_PUBLIC_KEY_LEN 33 +#endif +#define WG_SYMMETRIC_KEY_LEN 32 /* reserved for future preshared key support */ + +/* Maximum number of peers per wolfguard interface */ +#ifndef WOLFIP_WG_MAX_PEERS +# define WOLFIP_WG_MAX_PEERS 4 +#endif + +/* + * Per-peer configuration. + * + * endpoint: Peer's UDP endpoint (AF_INET or AF_INET6). + * allowed_ip: IPv4 address in host byte order (ip4 convention, e.g. from + * atoip4() or ntohl(inet_addr("..."))). + * allowed_cidr: Prefix length (0-32). + * keepalive_interval: Persistent keep-alive in seconds; 0 = disabled. + */ +struct wolfIP_wg_peer { + uint8_t public_key[WG_PUBLIC_KEY_LEN]; + struct sockaddr_storage endpoint; + ip4 allowed_ip; + uint8_t allowed_cidr; + uint16_t keepalive_interval; +}; + +/* + * Device configuration passed to wolfIP_wg_init(). + * + * ifname: Interface name (e.g. "wg0"). + * private_key: 32-byte SECP256R1 private key scalar. + * listen_port: UDP listen port (host byte order); 0 = kernel picks randomly. + * num_peers: Number of valid entries in peers[]. + */ +struct wolfIP_wg_config { + char ifname[16]; + uint8_t private_key[WG_PRIVATE_KEY_LEN]; + uint16_t listen_port; + int num_peers; + struct wolfIP_wg_peer peers[WOLFIP_WG_MAX_PEERS]; +}; + +/* + * wolfIP_wg_init() - Create, configure, and open a wolfguard interface. + * + * Creates the wolfguard network interface, configures it via the wolfguard + * generic-netlink API, brings it UP, opens an AF_PACKET/SOCK_DGRAM socket + * bound to the interface, and populates @ll with poll/send callbacks and a + * synthetic MAC address. + * + * The caller passes the returned @ll to wolfIP_ipconfig_set_ex() and + * wolfIP_getdev_ex() to attach it as a wolfip network interface. + * + * Returns 0 on success, negative errno on failure. + */ +int wolfIP_wg_init(struct wolfIP_wg_config *cfg, struct wolfIP_ll_dev *ll); + +/* + * wolfIP_wg_teardown() - Bring down and delete the wolfguard interface. + * + * Closes the TUN file descriptor and removes the interface via RTM_DELLINK. + * @ifname must match what was passed to wolfIP_wg_init(). + */ +void wolfIP_wg_teardown(const char *ifname); + +#endif /* WOLFIP_WOLFGUARD */ +#endif /* WOLFIP_WOLFGUARD_H */