Rust는 C/C++ 급의 속도와 (C/C++에서는 하지 못하는) 메모리 안정성을 보장한다는 커다란 장점이 있음에도 불구하고, 언어 자체의 난이도가 높은 탓에 정복하기가 쉽지 않은 언어로 인식되고 있다. 이번 시간에는 Rust로 구현한 wireguard code를 분석해 봄으로써, Rust를 좀 더 깊히 이해할 수 있는 시간을 가져 보고자 한다. 😎
Rust WireGuard running on OpenWrt One Router
목차
1. OpenWrt One Router
2. Wireguard-rs 프로젝트 소개
3. Boringtun 프로젝트 소개
4. todo!
5. References
Rust는 Linux kernel(Android 포함), Zephyr RTOS는 물론이고, Windows 등에서 채택되어 사용되고 있다. 뿐만 아니라, C/C++가 독점해온 Embedded 분야에서도 그 입지를 넓혀가고 있다. Rust는 이제 더 이상 선택이 아닌 필수가 되었다. 자, 그럼 지금부터 Rust의 세계로 떠나 보도록 하자. 🚀
1. OpenWrt One Router
Banana Pi 팀과 OpenWrt open source community에서 만든 OpenWrt Router 보드를 하나 입수했다. 이름하여 OpenWrt One~ 이번 장에서는 rust program을 테스트하기 위한 target 장치로, OpenWrt One router를 소개해 보고자 한다.
1.1 OpenWrt One Router H/W Review
[그림 1.1] OpenWrt One Router 외관(1)
📌 라우터 외관은 살짝 촌스럽다.
Key Features
- OpenWrt official board and support
- MediaTek MT7981B (Filogic 820) SoC
- Dual-band WiFI 6 via MediaTek MT7976C (2×2 2.4 GHz + 3×3 5Ghz)
- 1GB DDR4
- 1 x 2.5GbE RJ45 port and |1 x Gigabit Ethernet RJ45 port
- 256 MiB SPI NAND and 16 MiB SPI NOR flash are used to make the board almost unbrickable
- M.2 2242/2230 socket for NVMe SSD (PCIe gen 2 x1)
- RTC support
- PoE support
- MikroBUS socket for expansion modules
뚜겅을 열어 보니, (발열이 심한지) 커다란 heat sink가 눈에 들어온다.
OpenWrt One board의 top/bottom view가 궁금하다면, 아래 2개의 그림이 도움이 될 것이다.
[그림 1.3] OpenWrt One 보드 - Top view [출처 - 참고문헌 14]
1.2 OpenWrt One Router Booting 하기
Serial console을 실행한 상태에서, 부팅하는 모습을 capture 보았다.
$ minicom -D /dev/ttyACM0 -b 115200
(OpenWrt이므로) 부팅이 완료된 후에는 web browser로 LuCI webUI에 접근 가능하다.
http://192.168.1.1
1.3 OpenWrt/LEDE S/W Architecutre 소개
OpenWrt의 전체 build system 및 주요 s/w 구성 요소를 그림으로 확인해 보면 다음과 같다. 때로는 장황한 설명보다 한장의 그림이 더 좋을 때가 있다. 😋
1.4 OpenWrt One Router용 source code build 하기
<Ubuntu 22.04 LTS>
$ git clone https://github.com/openwrt/openwrt$ cd openwrt
$ git pull
$ git checkout openwrt-24.10
$ ./scripts/feeds update -a
$ ./scripts/feeds install -a
$ make menuconfig
build 시 에러가 발생한다면, 아래와 같이 하여 error를 확인할 수 있다.
$ make V=99 -j1
<build 결과>
$ cd bin/targets/mediatek/filogic
$ ls -la
합계 74880
drwxr-xr-x 3 chyi chyi 4096 5월 20 21:26 .
drwxr-xr-x 3 chyi chyi 4096 5월 20 17:59 ..
-rw-r--r-- 1 chyi chyi 204 5월 20 21:14 config.buildinfo
-rw-r--r-- 1 chyi chyi 368 5월 20 21:14 feeds.buildinfo
-rw-r--r-- 1 chyi chyi 210365 5월 20 21:17 mt7981-ram-ddr3-bl2.bin
-rw-r--r-- 1 chyi chyi 210365 5월 20 21:17 mt7981-ram-ddr4-bl2.bin
-rw-r--r-- 1 chyi chyi 189656 5월 20 21:18 mt7986-ram-ddr3-bl2.bin
-rw-r--r-- 1 chyi chyi 189656 5월 20 21:18 mt7986-ram-ddr4-bl2.bin
-rw-r--r-- 1 chyi chyi 239053 5월 20 21:18 mt7988-ram-comb-bl2.bin
-rw-r--r-- 1 chyi chyi 21757952 5월 20 21:26 openwrt-mediatek-filogic-openwrt_one-factory.ubi
-rw-r--r-- 1 chyi chyi 8585216 5월 20 21:26 openwrt-mediatek-filogic-openwrt_one-initramfs.itb
-rw-r--r-- 1 chyi chyi 350552 5월 20 21:26 openwrt-mediatek-filogic-openwrt_one-nor-bl31-uboot.fip
-rw-r--r-- 1 chyi chyi 10158080 5월 20 21:26 openwrt-mediatek-filogic-openwrt_one-nor-factory.bin
-rw-r--r-- 1 chyi chyi 222893 5월 20 21:26 openwrt-mediatek-filogic-openwrt_one-nor-preloader.bin
-rw-r--r-- 1 chyi chyi 961017 5월 20 21:26 openwrt-mediatek-filogic-openwrt_one-snand-bl31-uboot.fip
-rw-r--r-- 1 chyi chyi 22806528 5월 20 21:26 openwrt-mediatek-filogic-openwrt_one-snand-factory.bin
-rw-r--r-- 1 chyi chyi 234341 5월 20 21:26 openwrt-mediatek-filogic-openwrt_one-snand-preloader.bin
-rw-r--r-- 1 chyi chyi 10486565 5월 20 21:26 openwrt-mediatek-filogic-openwrt_one-squashfs-sysupgrade.itb
-rw-r--r-- 1 chyi chyi 4103 5월 20 21:26 openwrt-mediatek-filogic-openwrt_one.manifest
drwxr-xr-x 3 chyi chyi 12288 5월 20 21:26 packages
-rw-r--r-- 1 chyi chyi 3387 5월 20 21:26 profiles.json
-rw-r--r-- 1 chyi chyi 1980 5월 20 21:26 sha256sums
-rw-r--r-- 1 chyi chyi 18 5월 20 21:14 version.buildinfo
<kernel compilation 정리>
linux kernel source code 위치 =>
openwrt/build_dir/target-aarch64_cortex-a53_musl/linux-mediatek_filogic/linux-6.6.65
$ make kernel_menuconfig
=> kernel menuconfig
$ make target/linux/clean
=> kernel clean
$ make target/linux/compile V=s -j1
=> 조용히 순차적으로 kernel compile 수행(kernel build 시 error를 찾기 위해 시도)
or
$ make target/linux/compile V=99 -j$(nproc) 2>&1 | tee build.log
=> kernel build 과정을 자세히 출력하고, log file에 build 과정을 저장하기
_______________________________________________________
Target board가 준비되었으니, 이제 부터는 Rust로 구현한 wireguard project를 본격적으로 분석해 보기로 하자.
2. Wireguard-rs 프로젝트 소개
wireguard-rs project는 wireguard 진영에서 공식으로 인정하는 rust 기반 wireguard project이다. 하지만, 아쉽게도 5년전에 commit한 내용을 끝으로 더 이상은 개발이 진행되지 않고 있는 듯하다. 😂
2.1 wireguard-rs project 돌려 보기
우선, 제대로 동작할 것인지 확인해 보도록 하자.
<How to build on Ubuntu 22.04 LTS>
$ git clone https://github.com/WireGuard/wireguard-rs
$ cd wireguard-rs
$ cargo build --relase
$ cd target/release
$ sudo ./wireguard-rs wg1
-> background로 돌면서 자동으로 wg1 interface가 생성된다.
📌 wireguard-rs binary의 크기는 2656216 bytes 즉, 2.65MB 정도로 작은편이다(Golang version 대비 작은편임).
<How to run wireguard>
$ ps aux|grep wireguard-rs
nobody 15126 0.1 0.0 3588728 2688 ? Sl 12:07 0:00 ./wireguard-rs wg1
$ sudo ip address add dev wg1 10.1.2.100/24
$ sudo ip link set up dev wg1
$ sudo wg set wg1 listen-port 59760 private-key ./privatekey peer fQEC0IgJjbWRotaunnKlOdhJw+kKzL8q1PYN/5DoWGs= allowed-ips 10.1.2.0/24 endpoint 192.168.1.169:51820
$ sudo wg show
interface: wg1
public key: 6+INX3ZlitudBP9j7dgKiWW7xl95sVoUNx6TcRsykT8=
private key: (hidden)
listening port: 59760
peer: fQEC0IgJjbWRotaunnKlOdhJw+kKzL8q1PYN/5DoWGs=
endpoint: 192.168.1.169:51820
allowed ips: 10.1.2.0/24
latest handshake: 1 second ago
transfer: 436 B received, 380 B sent
$ ping 10.1.2.1
PING 10.1.2.1 (10.1.2.1) 56(84) bytes of data.
64 bytes from 10.1.2.1: icmp_seq=1 ttl=128 time=13.3 ms
64 bytes from 10.1.2.1: icmp_seq=2 ttl=128 time=2.52 ms
64 bytes from 10.1.2.1: icmp_seq=3 ttl=128 time=5.06 ms
...
한편, Peer 쪽 설정은 다음과 같으며, 자세한 설정 과정은 생략한다(당연히 동시에 설정해 주어야 한다).
[그림 2.1] Peer 설정 - Wireguard windows client
2.2 wireguard-rs를 openwrt one board에서 돌려 보기
이번에는 wireguard-rs를 aarch64용(openwrt one router)으로 cross compile해 보자.
$ vi ~/.cargo/config.toml
-> 아래의 내용을 담은 파일을 하나 만들자.
[target.aarch64-unknown-linux-musl]
linker = "/mnt/hdd/workspace/mini_devices/bpi/openwrt/staging_dir/toolchain-aarch64_cortex-a53_gcc-14.2.0_musl/bin/aarch64-openwrt-linux-musl-gcc"
...
$ rustup target add aarch64-unknown-linux-musl
$ export PATH=/mnt/hdd/workspace/mini_devices/bpi/openwrt/staging_dir/toolchain-aarch64_cortex-a53_gcc-14.2.0_musl/bin:$PATH
$ export STAGING_DIR=/mnt/hdd/workspace/mini_devices/bpi/openwrt/staging_dir/toolchain-aarch64_cortex-a53_gcc-14.2.0_musl
$ export TARGET_CC=aarch64-openwrt-linux-musl-gcc
$ export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=/mnt/hdd/workspace/mini_devices/bpi/openwrt/staging_dir/toolchain-aarch64_cortex-a53_gcc-14.2.0_musl/bin/aarch64-openwrt-linux-musl-gcc
$ cargo clean
$ cargo build --target=aarch64-unknown-linux-musl
or
$ cargo build --target=aarch64-unknown-linux-musl --release
--> (헐 생각보다 많은) build error 발생한다. 일단 pass! 😂
2.3 wireguard-rs 주요 코드 분석
기본적인 동작 과정은 확인해 보았으니, 이번에는 주요 코드를 분석해 볼 차례이다. wireguard-rs github에는 아래의 그림이 하나 있을 뿐인데, (코드를 들여다 보니) 이 그림이 많은 점을 암시하고 있음을 알 수가 있다. 💬
<wireguard-rs 코드 분석 기준>
[1] main routine
[2] handshake module
[3] router module
[4] timers
(위의 4개의 category 관점에서) 대략적인 코드 분석은 진행해 보았으나, aarch64로 cross compile이 안되는 등 다수의 문제가 보이는 바, 여기에서 따로 정리하지는 않기로 한다. 😓
3. Boringtun 프로젝트 소개
Boringtun(터널 뚫기 정도로 해석하면 좋을 듯)은 (wireguard project랑은 무관하게) Cloudflare 개발진이 자체적으로 만든 open source project로 wireguard daemon(boringtun-cli)은 물론이고, library 형태로 다양한 OS를 지원하는 특징이 있다. 최신 master branch는 개선 중(실제로는 오랜 기간 작업을 안하고 있음)인 듯 보이므로, 여기에서는 latest stable 버젼인 v0.6.0을 기준으로 시험을 진행해 보기로 한다.
📌 Wireguard를 만든 Jason A. Donenfeld는 boringtun을 만든 Cloudflare 개발진이 wireguard-rs project에 합류(leading)해 주기를 원했으나, 결국은 성사되지 않았다. 😂
3.1 boringtun project 돌려 보기
우선, 제대로 동작하는지를 확인해 보도록 하자.
<How to build on Ubuntu 22.04 LTS>
$ tar xvzf boringtun-boringtun-0.6.0.tar.gz
-> Download boringtun-boringtun-0.6.0.tar.gz from https://github.com/cloudflare/boringtun
$ cd boringtun-boringtun-0.6.0
$ cargo build --bin boringtun-cli --release
<How to run wireguard>
$ cd target/release
$ sudo ./boringtun-cli wg1
BoringTun started successfully
$ ps aux|grep boringtun-cli
chyi 11482 0.0 0.0 278736 7380 ? Sl 15:22 0:00 ./boringtun-cli wg1
$ sudo ip address add dev wg1 10.1.2.100/24
$ sudo ip link set up dev wg1
$ sudo wg set wg1 listen-port 59760 private-key ./privatekey peer fQEC0IgJjbWRotaunnKlOdhJw+kKzL8q1PYN/5DoWGs= allowed-ips 10.1.2.0/24 endpoint 192.168.1.169:51820
$ ping 10.1.2.1
PING 10.1.2.1 (10.1.2.1) 56(84) bytes of data.
64 bytes from 10.1.2.1: icmp_seq=1 ttl=128 time=31.1 ms
64 bytes from 10.1.2.1: icmp_seq=2 ttl=128 time=5.47 ms
64 bytes from 10.1.2.1: icmp_seq=3 ttl=128 time=27.4 ms
...
$ sudo wg show
interface: wg1
listening port: 59760
peer: fQEC0IgJjbWRotaunnKlOdhJw+kKzL8q1PYN/5DoWGs=
endpoint: 192.168.1.169:51820
allowed ips: 10.1.2.0/24
latest handshake: 56 years, 1 day, 6 hours, 24 minutes, 53 seconds ago
transfer: 14.25 KiB received, 14.25 KiB sent
Peer(windows client) 설정을 3장과 동일하게 유지한 상태에서 테스트해 보니, 정상 동작한다. 그런데, 희한하게도 boringtun-cli를 돌릴 경우에는 위와 같이 wg show의 interface 설정 정보가 좀 다르게 출력된다.
3.2 aarch64용으로 cross compile 하기
이번에는 boringtun을 1장에서 설명한 OpenWrt One router(aarch64)용으로 cross compile해 보자.
<Ubuntu 22.04 LTS PC>
$ vi ~/.cargo/config.toml
-> 아래의 내용으로 구성된 config.toml 파일을 하나 만들자.
[target.aarch64-unknown-linux-musl]
linker = "/mnt/hdd/workspace/mini_devices/bpi/openwrt/staging_dir/toolchain-aarch64_cortex-a53_gcc-14.2.0_musl/bin/aarch64-openwrt-linux-musl-gcc"
...
$ rustup target add aarch64-unknown-linux-musl
이후, 몇가지 aarch64-openwrt-linux-musl-gcc 관련 환경 설정을 진행하도록 한다.
$ export PATH=/mnt/hdd/workspace/mini_devices/bpi/openwrt/staging_dir/toolchain-aarch64_cortex-a53_gcc-14.2.0_musl/bin:$PATH
$ export STAGING_DIR=/mnt/hdd/workspace/mini_devices/bpi/openwrt/staging_dir/toolchain-aarch64_cortex-a53_gcc-14.2.0_musl
$ export TARGET_CC=aarch64-openwrt-linux-musl-gcc
$ export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=/mnt/hdd/workspace/mini_devices/bpi/openwrt/staging_dir/toolchain-aarch64_cortex-a53_gcc-14.2.0_musl/bin/aarch64-openwrt-linux-musl-gcc
이후, boringtun-cli를 build해 본다.
$ cargo clean
<debug mode로 build 하기>
$ cargo build --bin boringtun-cli --target=aarch64-unknown-linux-musl
<release mode로 build 하기>
$ cargo build --bin boringtun-cli --target=aarch64-unknown-linux-musl --release
$ cd target/aarch64-unknown-linux-musl/debug
$ file boringtun-cli
boringtun-cli: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped
<library 형태로 build 하기>
$ cargo clean
$ cargo build --lib --no-default-features --release --target=aarch64-unknown-linux-musl
$ ls -l target/aarch64-unknown-linux-musl/release/*.a
-rw-rw-r-- 2 chyi chyi 28533114 12월 23 11:23 libboringtun.a
<Target board>
boringtun-cli를 target board로 올려 동작하는지 확인해 보도록 하자.
root@OpenWrt:~/workspace# ./boringtun-cli -h
OK, 실행은 된다. 그런데, 이 상태에서 option으로 interface name을 주어 실행해 보니, 아래와 같이 error가 발생한다. 😂
root@OpenWrt:~/workspace# ./boringtun-cli -f wg0
2025-12-19T08:54:42.865319Z ERROR boringtun_cli: Failed to initialize tunnel, error: Socket(Os { code: 2, kind: )
at boringtun-cli/src/main.rs:160
2025-12-19T08:54:42.865319Z ERROR boringtun_cli: Failed to initialize tunnel, error: Socket(Os { code: 2, kind: )
at boringtun-cli/src/main.rs:160
Target board를 확인해 보니, tun device file(/dev/net/tun)이 안 보인다.
(코드를 살짝 들여다 보니) 이런, 아무래도 OpenWrt One에 tun.ko 모듈이 설치되어 있지 않아 발생한 문제로 보인다.
$ make menuconfig
-> kmon-tun을 Module로 선택한다.
$ make -j8
$ find . -name "tun.ko" -print
./staging_dir/target-aarch64_cortex-a53_musl/root-mediatek/lib/modules/6.6.65/tun.ko
./build_dir/target-aarch64_cortex-a53_musl/linux-mediatek_filogic/packages/.pkgdir/kmod-tun/lib/modules/6.6.65/tun.ko
./build_dir/target-aarch64_cortex-a53_musl/linux-mediatek_filogic/packages/ipkg-aarch64_cortex-a53/kmod-tun/lib/modules/6.6.65/tun.ko
./build_dir/target-aarch64_cortex-a53_musl/linux-mediatek_filogic/linux-6.6.65/drivers/net/tun.ko
tun.ko를 target board로 복사하자.
$ cd build_dir/target-aarch64_cortex-a53_musl/linux-mediatek_filogic/linux-6.6.65/drivers/net
$ scp ./tun.ko root@192.168.1.1:~/workspace
root@192.168.1.1's password:
tun.ko 100% 489KB 9.1MB/s 00:00
tun.ko를 구동(insmod)하면, /dev/net/tun file도 자동 생성된다.
root@OpenWrt:~/workspace# insmod ./tun.ko
[ 1436.907518] tun: Universal TUN/TAP device driver, 1.6
root@OpenWrt:~/workspace# ls -l /dev/net/tun
crw------- 1 root root 10, 200 Dec 19 12:29 /dev/net/tun
이 상태에서 다시, boringtun-cli를 실행시킨다.
root@OpenWrt:~/workspace# ./boringtun-cli -f wg0
2025-12-19T12:29:30.091485Z ERROR boringtun::device: Poll error, error: "Interrupted system call (os error 4)"
at boringtun/src/device/mod.rs:268
2025-12-19T12:29:30.091429Z ERROR boringtun_cli: Failed to drop privileges, error: DropPrivileges("Failed to per)
at boringtun-cli/src/main.rs:168
2025-12-19T12:29:30.091507Z ERROR boringtun::device: Poll error, error: "Interrupted system call (os error 4)"
at boringtun/src/device/mod.rs:268
어라, 이번에는 또 다른 에러가 발생한다. 아무래도 drop-privileges 처리에 문제가 있는 듯하다.
root@OpenWrt:~/workspace# ./boringtun-cli --disable-drop-privileges wg0
BoringTun started successfully
OK, 일단 구동에 성공하였다. 😀
나머지 wireguard 설정을 추가한 후, peer wireguard로 ping test를 해보니, 정상 동작한다.
# ip address add dev wg0 10.1.2.200/24
# ip link set up dev wg0
# /root/workspace/wg set wg0 listen-port 59760 private-key ./privatekey peer fQEC0IgJjbWRotaunnKlOdhJw+kKzL8q1PYN/5DoWGs= allowed-ips 10.1.2.0/24 endpoint 192.168.1.169:51820
# ip link set up dev wg0
# /root/workspace/wg set wg0 listen-port 59760 private-key ./privatekey peer fQEC0IgJjbWRotaunnKlOdhJw+kKzL8q1PYN/5DoWGs= allowed-ips 10.1.2.0/24 endpoint 192.168.1.169:51820
📌 wireguard-tools package도 빠져 있어, wireguard-tools package를 선택 & build 후, wg binary만 복사하여 시험하였다.
[그림 3.5] wireguard-tools package enable 하기
root@OpenWrt:~/workspace# ping 10.1.2.1
PING 10.1.2.1 (10.1.2.1): 56 data bytes
64 bytes from 10.1.2.1: seq=0 ttl=128 time=2.339 ms
64 bytes from 10.1.2.1: seq=1 ttl=128 time=2.795 ms
64 bytes from 10.1.2.1: seq=2 ttl=128 time=4.346 ms
root@OpenWrt:~/workspace# ./wg show
3.3 boringtun 주요 코드 분석
기본적인 동작 과정은 확인해 보았으니, 이제 (주요 코드 흐름을 중심으로) code 분석에 들어가 보도록 하자.
1) boringtun 주요 코드 목록 분석
boringtun-cli/src/*
--> main routine
-rw-rw-r-- 1 chyi chyi 6258 7월 8 2023 main.rs
boringtun/src/*
--> library를 구성하는 routines
drwxrwxr-x 3 chyi chyi 4096 12월 23 12:45 device
--> 주로 tun device, peer, wg 설정(set/get - api.rs) 등에 관한 코드로 구성
allowed_ips.rs
api.rs
dev_lock.rs
drop_privileges.rs
epoll.rs
integration_tests
kqueue.rs
mod.rs //device module에 대한 핵심 코드(struct & implementaion) 위치
peer.rs
tun_darwin.rs
tun_linux.rs //tun device 관련 linux code
drwxrwxr-x 2 chyi chyi 4096 12월 23 13:13 ffi
--> FFI(foreign function interface) 관련 코드
mod.rs
-rw-rw-r-- 1 chyi chyi 7175 7월 8 2023 jni.rs
--> JNI(Java Native Interface) 관련 코드(android에서 사용하기 위함)
-rw-rw-r-- 1 chyi chyi 670 7월 8 2023 lib.rs
--> client에서 library로 사용하도록 하기 위한 코드
drwxrwxr-x 2 chyi chyi 4096 12월 23 13:15 noise
--> noise handshake 관련 코드
errors.rs
handshake.rs //noise handshake code
mod.rs //noise module에 대한 핵심 코드(struct & implementaion) 위치
rate_limiter.rs
session.rs
timers.rs //timer 관련 코드
-rw-rw-r-- 1 chyi chyi 1070 7월 8 2023 serialization.rs
drwxrwxr-x 2 chyi chyi 4096 12월 20 15:23 sleepyinstant
mod.rs
unix.rs
windows.rs
-rw-rw-r-- 1 chyi chyi 3855 7월 8 2023 wireguard_ffi.h
--> FFI 관련 C header file
(*) 아주 크게 보아 main routine(main.rs)과 2개의 module(device, noise - 2개의 mod.rs)로 구성되었다고 말할 수 있다.
2) 주요 structure 정리
코드 흐름을 제대로 이해하려면, 주요 struct의 구성을 확인해 보는게 우선되어야 한다.
2.1) wireguard device 관련 주요 struct 정의
-> boringtun/src/device/mod.rs
pub struct DeviceHandle {
device: Arc<Lock<Device>>, // The interface this handle owns
threads: Vec<JoinHandle<()>>,
}
pub struct DeviceConfig {
pub n_threads: usize,
pub use_connected_socket: bool,
#[cfg(target_os = "linux")]
pub use_multi_queue: bool,
#[cfg(target_os = "linux")]
pub uapi_fd: i32,
}
pub struct Device {
key_pair: Option<(x25519::StaticSecret, x25519::PublicKey)>,
queue: Arc<EventPoll<Handler>>,
listen_port: u16,
fwmark: Option<u32>,
iface: Arc<TunSocket>,
udp4: Option<socket2::Socket>,
udp6: Option<socket2::Socket>,
yield_notice: Option<EventRef>,
exit_notice: Option<EventRef>,
peers: HashMap<x25519::PublicKey, Arc<Mutex<Peer>>>,
peers_by_ip: AllowedIps<Arc<Mutex<Peer>>>,
peers_by_idx: HashMap<u32, Arc<Mutex<Peer>>>,
next_index: IndexLfsr,
config: DeviceConfig,
cleanup_paths: Vec<String>,
mtu: AtomicUsize,
rate_limiter: Option<Arc<RateLimiter>>,
#[cfg(target_os = "linux")]
uapi_fd: i32,
}
2.2) Peer 관련 주요 struct 정의
-> boringtun/src/device/peer.rs
pub struct Endpoint {
pub addr: Option<SocketAddr>,
pub conn: Option<socket2::Socket>,
}
pub struct Peer {
/// The associated tunnel struct
pub(crate) tunnel: Tunn,
/// The index the tunnel uses
index: u32,
endpoint: RwLock<Endpoint>,
allowed_ips: AllowedIps<()>,
preshared_key: Option<[u8; 32]>,
}
pub struct AllowedIP {
pub addr: IpAddr,
pub cidr: u8,
}
2.3) Tunnel & Noise handshake 관련 주요 stuct & enum 정의
pub enum TunnResult<'a> {
Done,
Err(WireGuardError),
WriteToNetwork(&'a mut [u8]),
WriteToTunnelV4(&'a mut [u8], Ipv4Addr),
WriteToTunnelV6(&'a mut [u8], Ipv6Addr),
}
pub struct Tunn {
/// The handshake currently in progress
handshake: handshake::Handshake,
/// The N_SESSIONS most recent sessions, index is session id modulo N_SESSIONS
sessions: [Option<session::Session>; N_SESSIONS],
/// Index of most recently used session
current: usize,
/// Queue to store blocked packets
packet_queue: VecDeque<Vec<u8>>,
/// Keeps tabs on the expiring timers
timers: timers::Timers,
tx_bytes: usize,
rx_bytes: usize,
rate_limiter: Arc<RateLimiter>,
}
pub struct HandshakeInit<'a> {
sender_idx: u32,
unencrypted_ephemeral: &'a [u8; 32],
encrypted_static: &'a [u8],
encrypted_timestamp: &'a [u8],
}
pub struct HandshakeResponse<'a> {
sender_idx: u32,
pub receiver_idx: u32,
unencrypted_ephemeral: &'a [u8; 32],
encrypted_nothing: &'a [u8],
}
pub struct PacketCookieReply<'a> {
pub receiver_idx: u32,
nonce: &'a [u8],
encrypted_cookie: &'a [u8],
}
pub struct PacketData<'a> {
pub receiver_idx: u32,
counter: u64,
encrypted_encapsulated_packet: &'a [u8],
}
pub enum Packet<'a> {
HandshakeInit(HandshakeInit<'a>),
HandshakeResponse(HandshakeResponse<'a>),
PacketCookieReply(PacketCookieReply<'a>),
PacketData(PacketData<'a>),
}
3) 주요 코드 흐름 정리
3.1) main routine
main( ) routine은 매우 simple한데, (요점만 얘기하자면) 명령행 인자를 받아 이에 대한 처리(예: daemon화, log 초기화 등)를 하고난 후, DeviceHandle::new() function을 호출하는게 전부다. 이후 new() function 안에서 다른 new(tun, udp socket) function 등을 계속해서 호출하면서, 전체 구조를 잡아가는 방식으로 이해하면 될 것 같다.
fn main() { //boringtun-cli/src/main.rs
//argument 파싱
let matches = Command::new("boringtun")
.version(env!("CARGO_PKG_VERSION"))
.author("Vlad Krasnov <vlad@cloudflare.com>")
.args(&[
Arg::new(...),
Arg::new(...),
Arg::new(...),
...
])
.get_matches();
//daemon 시작
if background {
let daemonize = Daemonize::new().working_directory(...).exit_action(...);
match daemonize.start() { ... };
}
//DeviceHandle::new() 함수 호출 - 여기가 시작점임.
let mut device_handle: DeviceHandle = match DeviceHandle::new(tun_name, config) { ... };
...
//device_handle 종료 대기
device_handle.wait();
}
3.2) tun device & udp socket 초기화
impl DeviceHandle { //boringtun/src/device/mod.rs
pub fn new(name: &str, config: DeviceConfig) -> Result<DeviceHandle, Error> {
let n_threads = config.n_threads;
//TUN device create 및 이후 처리(몇가지 함수 registration) - boringtun/src/device/mod.rs
let mut wg_interface = Device::new(name, config)?;
// let iface = Arc::new(TunSocket::new(name) ...
// device.register_api_fd()
// device.register_iface_handler() 함수 호출 - tun device로 부터 packet read 후, 해당 peer에 맞게 encapsulation(encryption) 처리 후, peer's endpoint로 패킷 전달(udp send)
// device.register_timers()
//udp socket open 및 이후 처리
wg_interface.open_listen_socket(0)?; // Start listening on a random port
// register_udp_handler() - udp socket으로 부터 packet receive 후, packet parsing. 이후 peer를 찾아 decapsulation(decryption) 처리 후, tun device로 write
let interface_lock = Arc::new(Lock::new(wg_interface));
let mut threads = vec![];
//default 4개의 thread 생성 - event_loop 함수 호출
for i in 0..n_threads {
threads.push({
let dev = Arc::clone(&interface_lock);
thread::spawn(move || DeviceHandle::event_loop(i, &dev))
});
}
//DeviceHandle struct return
Ok(DeviceHandle {
device: interface_lock,
threads,
})
}
....
}
3.3) tun device -> packet encapsulation -> packet send(udp) to peer 흐름
fn register_iface_handler(&self, iface: Arc<TunSocket>) -> Result<(), Error> { //boringtun/src/device/mod.rs
// The iface_handler handles packets received from the WireGuard virtual network
// interface. The flow is as follows:
// * Read a packet
// * Determine peer based on packet destination ip
// * Encapsulate the packet for the given peer
// * Send encapsulated packet to the peer's endpoint
let src = match iface.read(&mut t.src_buf[..mtu])
let mut peer = match peers.find(dst_addr)
match peer.tunnel.encapsulate(src, &mut t.dst_buf[..])
let mut endpoint = peer.endpoint_mut()
udp4.send_to(packet, &addr.into())
}
3.4) udp socket -> packet read -> decapsulation -> packet send to TUN device 흐름
-> udp packet 수신 후, parsing
-> 이후, hanshake packet이면 handshake 처리 routine call
-> transport packet(encrypted packet)이면 decrypt 처리 후, tun device로 write
fn register_udp_handler(&self, udp: socket2::Socket) -> Result<(), Error> { //boringtun/src/device/mod.rs
while let Ok((packet_len, addr)) = udp.recv_from(src_buf) {
let parsed_packet = match rate_limiter.verify_packet()
let peer = match &parsed_packet { ... }
match p.tunnel.handle_verified_packet(parsed_packet, &mut t.dst_buf[..]) {
TunnResult::WriteToTunnelV4(packet, addr) => t.iface.write4(packet)
}
d.register_conn_handler(Arc::clone(peer), sock, ip_addr)
}
}
//handshake or transport packet 수신 시 - 각각의 처리 함수로 분기
pub(crate) fn handle_verified_packet<'a>(...) { //boringtun/src/noise/mod.rs
match packet {
Packet::HandshakeInit(p) => self.handle_handshake_init(p, dst),
//handshake initiation packet 수신 처리
//receive_handshake_initialization() 호출 - boringtun/src/noise/handshake.rs
Packet::HandshakeResponse(p) => self.handle_handshake_response(p, dst),
//handshake response packet 수신 처리
//receive_handshake_response() 호출 - boringtun/src/noise/handshake.rs
Packet::PacketCookieReply(p) => self.handle_cookie_reply(p),
//cookie 수신 처리
//receive_cookie_reply() 호출 - boringtun/src/noise/handshake.rs
Packet::PacketData(p) => self.handle_data(p, dst),
//packet decryption 처리
}
.unwrap_or_else(TunnResult::from)
}
3.5) noise handshake 시작 routine
impl Tunn { //boringtun/src/noise/mod.rs
/// Encapsulate a single packet from the tunnel interface.
pub fn encapsulate<'a>(&mut self, src: &[u8], dst: &'a mut [u8]) -> TunnResult<'a> {
...
// Initiate a new handshake if none is in progress
self.format_handshake_initiation(dst, false)
//boringtun/src/noise/handshake.rs에 정의되어 있음.
}
}
3.6) wireguard config set/get routine
pub fn register_api_fd(&mut self, fd: i32) -> Result<(), Error> {
let io_file = unsafe { UnixStream::from_raw_fd(fd) }
<---- unix domain socket
if reader.read_line(&mut cmd).is_ok() {
let status = match cmd.as_ref() {
// Only two commands are legal according to the protocol, get=1 and set=1.
"get=1" => api_get(&mut writer, d),
//device 설정 내용을 획득
"set=1" => api_set(&mut reader, d),
//wg 설정 내용이 device에게로 전달
};
}
}
wireguard 설정(get/set)과 관련해서는 이전 posting의 2.2절을 참조해 주기 바란다.
__________________________________________________
여기까지 boringtun project에 관하여 간략히 살펴 보았다.
4. todo!
다음 시간에는 Rust 관련하여 아래와 같은 내용을 다뤄 보고자 한다. 😎
1) Rust로 구현하는 Linux kernel programming
2) Embedded board용 rust programming
To be continued...
5. References
<main - Rust wireguard implentations>
[1] https://github.com/WireGuard/wireguard-rs
[2] https://github.com/cloudflare/boringtun
<misc - Rust implementations for wireguard>
[3] https://github.com/rodit-org/altuntun
[4] https://github.com/lz1998/wg-rs
[5] https://github.com/conradludgate/rustyguard
[6] https://github.com/defguard/defguard
[7] https://github.com/luqmana/wireguard-uwp-rs
<Linux kernel>
[8] https://bootlin.com/pub/conferences/2025/cdl/kernel-review.pdf
<Windows rust>
[9] https://github.com/microsoft/windows-rs
[10] https://github.com/nulldotblack/wireguard-nt
<Rust Books>
[11] https://www.manning.com/books/learn-rust-in-a-month-of-lunches
[12] Rust in Action, Systems programming concepts and techniques, Timothy Samuel McNamara, Manning
[13] https://www.oreilly.com/library/view/programming-rust-2nd/9781492052586/
<OpenWrt One>
[14] https://docs.banana-pi.org/en/OpenWRT-One/BananaPi_OpenWRT-One
[15] https://openwrt.org/toh/openwrt/one
-> OpenWrt One WiKi page
[16] https://one.openwrt.org/hardware/
-> OpenWrt One datasheet, schematic 등 포함
[17] BPI_OpenWRT_ONE_V10_SCH_20240618-R.pdf
-> OpenWrt schematic
[18] https://openwrt.org/docs/guide-developer/toolchain/use-buildsystem
[19] https://firmware-selector.openwrt.org/?version=SNAPSHOT&target=mediatek%2Ffilogic&id=openwrt_one
-> OpenWrt One firmware image download
[20] http://events17.linuxfoundation.org/sites/events/files/slides/ELC_OpenWrt_LEDE.pdf
[21] https://blog.dend.ro/building-rust-for-routers/
-> Rust programming on OpenWrt
[22] And, Google~
























