Rust는 C/C++ 급의 속도와 (C/C++에서는 하지 못하는) 메모리 안정성을 보장한다는 커다란 장점이 있음에도 불구하고, 언어 자체의 난이도가 높은 탓에 정복하기가 쉽지 않은 언어로 인식되고 있다. 이번 시간에는 Rust로 구현한 wireguard code를 분석해 봄으로써, Rust를 좀 더 깊히 이해할 수 있는 시간을 가져 보고자 한다. 😎
Rust WireGuard running on OpenWrt One Router
목차
1. OpenWrt One Router
2. Ready to learn Rust ?
3. Wireguard-rs 프로젝트 소개
4. Boringtun 프로젝트 소개
5. todo!
6. 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. Ready to learn Rust ?
Rust는 현대적인 programming language 답게, (절차지향 programming은 물론이고) 요즘 유행하는 functional programming 적인 요소를 기본으로 탑재하고 있다(따라서 Python이나 Golang에서 보았던 요소들이 많이 눈에 띈다). 뿐만 아니라 Class와 Interface는 없으나 Trait라는 개념을 통해 Object oriented programming도 흉내낼 수가 있다.
Rust는 C/C++처럼 빠른 속도를 제공하면서도, C/C++에서는 제공하지 못하는 메모리 안정성을 보장한다는 커다란 장점이 있다. 하지만, 언어 자체의 난이도가 높은 탓에 정복하기가 쉽지 않은 언어(learning curve가 가파르다고 표현)로 인식되고 있다. 😓
그렇다면, Rust는 왜 그렇게 어렵게 느껴지는 것일까 ?
이는, Rust가 memory 안정성을 보장하기 위해, 여타의 programming language하고는 전혀 다른 접근 방법을 채택하고 있기 때문이다.
<Rust의 난해한 요인 및 주요 특징>
1) Trait + Generic + Struct가 함께 표현되어 있을 경우, 코드가 매우 복잡하고 난해함.
- Class & Interface는 없으나, Trait + Struct가 그 이상을 해 낼 수가 있다.
- self, Self가 엄청 자주 나온다.
2) Rust에서 제공하는 다양한 Utility Trait(Drop, Sized, Clone, Copy, PartialEq, Deref, Default, AsRef, Borrow, BorrowMut, From, Into, ToOwned, Cow 등) 에 대한 의미 및 사용 방식에 대한 이해 어려움.
- 뭐하러 이렇게 만들었나 싶은 생각이 들 정도로, 초반에 이해하기 쉽지가 않다.
- 문제는 위에 나열한 것 말고도 더 있다는 점이다.
3) 소유권(Ownership), Liefetime 개념
- 기존 programming language랑은 좀 다르다. 하지만, 뭐 그런대로 이해할만 하긴 하다.
4) 다양한 Smart Pointer 사용 방법
- 종류도 많고, 표현 기법이 잘 와닫질 않는다. 아래 그림을 보라~
5) Option, Result를 제공하는 enum type
- 별거 아닌듯 한데, 사용하기에 살짝 불편하다(어렵다는 표현이 맞으려나).
- 어떤 함수가 Option이나 Result를 결과로 반환하는지 알아야 programming이 가능하다.
6) 현란한 Functional programming 기법
- a().b().c().d().e() ....
- iterator & closure => 쉬운 듯 어렵다.
- 적절한 function을 연이어 붙이는 것이 쉽지가 않다. 뿐만아니라, unwrap, unwrap_or_else, ?, expect, as_ref, as_mut 등 함께할 때 꽤나 복잡해진다.
7) 여타 programming lanaguage에서 제공하는 concept (그나마 이건 쉬운 편)
- Jeneric, Collections(Vector, HashMap, LinkedList...), Iterator, Closure, Thread(spawn, channel) ...
- if/else, loop, while, for, match 문
8) module(crate라고 함) 개념 제공
- use x::y::z 형태로 module을 import하여 사용한다. C++ 느낌도 살짝 난다.
- 근데, crate랑 trait는 전혀 다른 개념이다.
9) 강력한 concurrency(동시성) programming 기법 제공
- Golang의 Go routine 보다는 안전하단다.
10) 강력한 macro
- C/C++의 macro 보다 강력하다.
11) 다양한 attributes
- 얘도 종류가 많아서 코드 이해를 어렵게 만든다.
12) 다른 language code 사용 가능(가령 C code 호출)
- Unsafe { } 사용
13) Compiler(cargo)가 강력하다.
- compile 단계에서 많은 error를 잡아준다. 뿐만아니라 package 관리까지 해준다.
- 코드량이 많아질 경우, 좀 느릴 수 있다.
14) test code를 작성하기 편리하다.
__________________________________________________________________
[그림 2.1] Rust의 Smart Pointer [출처 - 참고문헌 12]
어떤 면에서 볼 때, Rust는 코드 가독성이 그리 좋은 편은 못되는 것 같다(코드를 보는게 좀 어렵게 느껴진다). 하지만, 전반적인 개념을 파악하고 나면, 아주 흥미로운 language라는 점을 깨닫게 된다(정복하고 싶다는 강력한 욕구가 샘솟는다). 😍
여기에 초심자들이 읽기에 좋은 몇가지 rust 책을 열거해 보았다(필자의 개인적인 의견일 뿐 책 저자나 출판사랑은 전혀 무관하다. 😋).
📌 (한글판의 경우 중/후반부의 해석 상태가 조금은 문제가 있어 보이나) 원본이 워낙 훌륭한 이유로 강추한다. 아래 site도 원본 저자가 운영하는 site로 참조할만하다. 오른쪽이 원서다. 👍
[그림 2.3] Rust 책 추천 #2 - 초급(2)
📌 이 책은 Python과 Rust를 비교하면서 아주 쉽게 Rust를 설명하는게 특징이다. (번역서가 아니므로) 빠르게 읽어 볼만하다.
[그림 2.4] Rust 책 추천 #3 - 초급(3)
📌 앞부분에서는 rust의 전반적인 내용을 소개하고, 후반부에는 systems programming 관점에서 내용을 소개하는 점이 특이하다(재밌는 그림이 마음에 든다).
[그림 2.5] Rust 책 추천 #4 - 중급
📌 이 책은 중급 개발자를 위한 내용을 담고 있다. 처음부터 끝까지 완독하기에는 분량이 상당하니, 필요한 부분을 중심으로 그때 그때 읽어 볼 것을 권한다(한국판은 번역 상태도 좋은 편이다).
정답이 어디 있겠는가 ? 본인이 느끼기에 가장 쉬운 방법(서적 혹은 internet site)을 선택하여, 인내심을 가지고 study해 보는 것이 답일 것이다(위의 책은 어디까지나 필자가 제시한 예시에 불과하다 💣).
Language에서 제공하는 모든 기능을 다 이해하고 있어야 programming이 가능한 것은 아니다. 중요한 것은 전체적인 개념을 파악(rust의 경우는 인내심을 요구한다)한 후, 직접 coding해 보는 것이다. 그리고, 언어를 master하기 위해서는 누구에게나 충분한 시간이 필요한 법이다. ⏳
3. Wireguard-rs 프로젝트 소개
wireguard-rs project는 wireguard 진영에서 공식으로 인정하는 rust 기반 wireguard project이다. 하지만, 아쉽게도 5년전에 commit한 내용을 끝으로 더 이상은 개발이 진행되지 않고 있는 듯하다. 😂
3.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 쪽 설정은 다음과 같으며, 자세한 설정 과정은 생략한다(당연히 동시에 설정해 주어야 한다).
[그림 3.1] Peer 설정 - Wireguard windows client
3.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! 😂
3.3 wireguard-rs 주요 코드 분석
기본적인 동작 과정은 확인해 보았으니, 이번에는 주요 코드를 분석해 볼 차례이다. wireguard-rs github에는 아래의 그림이 하나 있을 뿐인데, (코드를 들여다 보니) 이 그림이 많은 점을 암시하고 있음을 알 수가 있다. 💬
<wireguard-rs 코드 분석 기준>
[1] main routine
[2] handshake module
[3] router module
[4] timers
(위의 4개의 category 관점에서) 대략적인 코드 분석은 진행해 보았으나, aarch64로 cross compile이 안되는 등 다수의 문제가 보이는 바, 여기에서 따로 정리하지는 않기로 한다. 😓
4. 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)해 주기를 원했으나, 결국은 성사되지 않았다. 😂
4.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 설정 정보가 좀 다르게 출력된다.
4.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만 복사하여 시험하였다.
[그림 4.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
4.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에 관하여 간략히 살펴 보았다.
5. todo!
다음 시간에는 Rust 관련하여 아래와 같은 내용을 다뤄 보고자 한다. 😎
1) Rust로 구현하는 Linux kernel programming
2) Embedded board용 rust programming
To be continued...
6. 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~
Slowboot

































댓글 없음:
댓글 쓰기