지난 시간에 이어 Zephyr RTOS programming 얘기(3번째 시간)를 계속 이어가 보도록 하겠다. 이번 시간의 주제는 Zephyr TCP/IP Network과 WireGuard Protocol(Zephyr에 porting하는 방법)에 관한 것이다. 😎
목차
1. STM32 Nucleo-F207ZG 보드2. Zephyr TCP/IP Stack3. Zephyr Virtual Network Interface 만들기4. Linux 사용자 영역에서 동작하는 WireGuard 구현(C언어 버젼)하기5. Zephyr 환경에서 동작하는 WireGuard Porting 하기 I - Ethernet 편6. TODO: Zephyr 환경에서 동작하는 WireGuard Porting 하기 II - Wi-Fi 편7. References
Zephyr는 대학에서 학생들에게 가르치면 좋을만한 범용 RTOS이다. Linux도 매우 훌륭한 OS이지만, 이제는 공룡이 되어버린 탓에, 배우기에는 아기 공룡인 Zephyr가 좀 더 수월하지 않을까 싶다. 😗
2024년 11월 27일 (첫눈/폭설이 오던 날)
Task & Memory Management(a.k.a Kernel Services), 각종 Device Driver model... 그럼 그 다음은 무엇을 살펴 보아야 할까 ? 바로 Network이다. Network 관련 코드에는 유선(Ethernet) 및 무선(Wi-Fi, Bluetooth, ZigBee, Thread, LoraWAN 등)을 포함하여 아주 많은 내용(network stack과 socket application)이 담겨 있다.
1. STM32 Nucleo-F207ZG 보드Ethernet port가 나와 있는 STM32 보드를 하나 구매했다. Resource 제한 장치의 경우, ethernet 보다는 Wi-Fi가 현실적인(일반적인) 방안이겠으나, ethernet도 나름 분석의 묘미가 있다. 😙 이번 posting에서는 아래 보드를 이용하여 zephyr의 ethernet & tcp/ip stack의 구조를 파악해 보고자 한다.
[그림 1.2] STM32 Nucleo F207ZG 보드 - top layout [출처 - 참고문헌 2]
<STM32 Nucleo F207ZG 보드 h/w features>- STM32F207ZGT6 in LQFP144 package
- ARM® 32-bit Cortex® -M3 CPU
- 120 MHz max CPU frequency
- VDD from 1.7 V to 3.6 V
- 1 MB Flash
- 128 KB SRAM
- GPIO with external interrupt capability
- 12-bit ADC with 24 channels
- RTC
- 17 General purpose timers
- 2 watchdog timers (independent and window)
- SysTick timer
- USART/UART (6)
- I2C (3)
- SPI (3)
- SDIO
- USB 2.0 OTG FS
- DMA Controller
- 10/100 Ethernet MAC with dedicated DMA
- CRC calculation unit
- True random number generator
STM32 보드는 ST-Link adapter가 장착되어 있어, programming(flash writing) & debugging이 매우 용이하다. 뿐만아니라, (STM32CubeIDE와 같은) 개발환경이 잘 갖추어져 있고, 적절한 사용자 매뉴얼과 풍부한 sample code가 준비되어 있는 장점이 있다. 사실, 이제까지의 방법은 STM32 SDK + FreeRTOS + 3rd party library(예: lwip)가 일반적이었으나, 앞으로는 (쉽지는 않겠지만) Zephyr를 기반으로 작업 방법이 바뀔 것을 기대해 본다. 🙏
📌 솔직히, 기존의 익숙한 방법을 버리고 Zephyr로 갈아타는 것은 업계 현실상 쉬운 일이 아닐 수도 있을 것 같다. 😂
Nucleo F207ZG 보드는 Ethernet 관련하여 RMII interface를 사용한다. 아래 회로도와 pinmap 도표 및 device tree는 ethernet controller(= MAC)와 PHY 간의 RMII 연결 관계를 보여준다. RMII의 특성 상, RX pin과 TX pin이 각각 2개이고, 50Mhz의 Clock이 인가되어 최대 100Mbps(2 x 50)의 data 속도가 나오게 된다.
[그림 1.7] RMII Signals 설명 [출처 - https://en.wikipedia.org/wiki/Media-independent_interface#RMII]
이 보드와 관련하여 보다 자세한 사항은 아래 site의 내용을 참조해 주기 바란다.
- STM32F207ZGT6 in LQFP144 package
- ARM® 32-bit Cortex® -M3 CPU
- 120 MHz max CPU frequency
- VDD from 1.7 V to 3.6 V
- 1 MB Flash
- 128 KB SRAM
- GPIO with external interrupt capability
- 12-bit ADC with 24 channels
- RTC
- 17 General purpose timers
- 2 watchdog timers (independent and window)
- SysTick timer
- USART/UART (6)
- I2C (3)
- SPI (3)
- SDIO
- USB 2.0 OTG FS
- DMA Controller
- 10/100 Ethernet MAC with dedicated DMA
- CRC calculation unit
- True random number generator
2. Zephyr TCP/IP Stack이 장에서는 zephyr의 TCP/IP stack의 구조를 살펴 보고, TCP/UDP echo server 예제를 통해, wireguard porting을 위한 기초 준비를 해 보고자 한다.
2.1 Zephyr Ethernet Driver & TCP/IP Stack
아래 2개의 그림은 (그리 새로울 것도 없는) 일반적인 tcp/ip stack 상에서 UDP packet을 내보내고 받는 과정을 표현하고 있다. Linux 같은 범용 OS 환경에서라면 너무나도 익숙한 부분이라, 특별히 들여다 보고 싶은 생각이 들지 않지만, Zephyr의 경우라면 다르다. 🚀 STM32 ethernet device driver에서 부터 시작하여, tcp/ip stack 및 socket API 까지 한 걸음에 확인해 보도록 하자. 💪
📌 패킷 송신은 연속되는 함수 call의 과정이다.
📌 대개 패킷 수신은 interrupt(polling 방식을 사용하기도 함)와 callback 함수(혹은 thread 함수)를 사용하여 이루어진다.
위의 그림에서 ethernet device driver 관련해서는 실제로 아래 코드가 사용된다.
<stm32 ethernet driver>- zephyr/drivers/ethernet/eth_stm32_hal.c
- zephyr/subsys/net/l2/ethernet/ethernet.c
한편 tcp/ip stack code는 아래 위치(subsys/net/ip 디렉토리)에 있다.
chyi@earth:~/zephyrproject/zephyr/subsys/net/ip$ ls -la합계 1116drwxrwxr-x 2 chyi chyi 4096 11월 24 17:37 .drwxrwxr-x 7 chyi chyi 4096 7월 12 12:40 ..-rw-rw-r-- 1 chyi chyi 40605 7월 12 12:40 6lo.c-rw-rw-r-- 1 chyi chyi 2321 7월 12 12:40 6lo.h-rw-rw-r-- 1 chyi chyi 4625 7월 12 12:40 6lo_private.h-rw-rw-r-- 1 chyi chyi 2573 7월 12 12:40 CMakeLists.txt-rw-rw-r-- 1 chyi chyi 29941 7월 12 12:40 Kconfig-rw-rw-r-- 1 chyi chyi 3121 7월 12 12:40 Kconfig.debug-rw-rw-r-- 1 chyi chyi 5693 7월 12 12:40 Kconfig.ipv4-rw-rw-r-- 1 chyi chyi 9325 7월 12 12:40 Kconfig.ipv6-rw-rw-r-- 1 chyi chyi 4247 7월 12 12:40 Kconfig.mgmt-rw-rw-r-- 1 chyi chyi 833 7월 12 12:40 Kconfig.stack-rw-rw-r-- 1 chyi chyi 3505 7월 12 12:40 Kconfig.stats-rw-rw-r-- 1 chyi chyi 9102 7월 12 12:40 Kconfig.tcp-rw-rw-r-- 1 chyi chyi 659 7월 12 12:40 canbus_socket.c-rw-rw-r-- 1 chyi chyi 765 7월 12 12:40 canbus_socket.h-rw-rw-r-- 1 chyi chyi 25768 7월 12 12:40 connection.c-rw-rw-r-- 1 chyi chyi 6248 7월 12 12:40 connection.h-rw-rw-r-- 1 chyi chyi 13076 7월 12 12:40 icmp.c-rw-rw-r-- 1 chyi chyi 14913 7월 12 12:40 icmpv4.c-rw-rw-r-- 1 chyi chyi 1613 7월 12 12:40 icmpv4.h-rw-rw-r-- 1 chyi chyi 10116 7월 12 12:40 icmpv6.c-rw-rw-r-- 1 chyi chyi 5641 7월 12 12:40 icmpv6.h-rw-rw-r-- 1 chyi chyi 18070 7월 12 12:40 igmp.c-rw-rw-r-- 1 chyi chyi 11073 7월 12 12:40 ipv4.c-rw-rw-r-- 1 chyi chyi 12062 7월 12 12:40 ipv4.h-rw-rw-r-- 1 chyi chyi 10476 7월 12 12:40 ipv4_acd.c-rw-rw-r-- 1 chyi chyi 3823 7월 12 12:40 ipv4_autoconf.c-rw-rw-r-- 1 chyi chyi 16979 7월 12 12:40 ipv4_fragment.c-rw-rw-r-- 1 chyi chyi 21331 7월 12 12:40 ipv6.c-rw-rw-r-- 1 chyi chyi 19461 7월 12 12:40 ipv6.h-rw-rw-r-- 1 chyi chyi 18709 7월 12 12:40 ipv6_fragment.c-rw-rw-r-- 1 chyi chyi 10727 7월 12 12:40 ipv6_mld.c-rw-rw-r-- 1 chyi chyi 67825 7월 12 12:40 ipv6_nbr.c-rw-rw-r-- 1 chyi chyi 19618 7월 12 12:40 ipv6_pe.c-rw-rw-r-- 1 chyi chyi 5038 7월 12 12:40 nbr.c-rw-rw-r-- 1 chyi chyi 5552 7월 12 12:40 nbr.h-rw-rw-r-- 1 chyi chyi 77929 7월 12 12:40 net_context.c-rw-rw-r-- 1 chyi chyi 13999 7월 12 12:40 net_core.c-rw-rw-r-- 1 chyi chyi 127194 7월 12 12:40 net_if.c-rw-rw-r-- 1 chyi chyi 11776 7월 12 12:40 net_mgmt.c-rw-rw-r-- 1 chyi chyi 51640 7월 12 12:40 net_pkt.c-rw-rw-r-- 1 chyi chyi 10351 7월 12 12:40 net_private.h-rw-rw-r-- 1 chyi chyi 10342 7월 12 12:40 net_stats.c-rw-rw-r-- 1 chyi chyi 18856 7월 12 12:40 net_stats.h-rw-rw-r-- 1 chyi chyi 9243 7월 12 12:40 net_tc.c-rw-rw-r-- 1 chyi chyi 5368 7월 12 12:40 net_tc_mapping.h-rw-rw-r-- 1 chyi chyi 3775 7월 12 12:40 net_timeout.c-rw-rw-r-- 1 chyi chyi 1071 7월 12 12:40 packet_socket.c-rw-rw-r-- 1 chyi chyi 904 7월 12 12:40 packet_socket.h-rw-rw-r-- 1 chyi chyi 1617 7월 12 12:40 promiscuous.c-rw-rw-r-- 1 chyi chyi 24999 7월 12 12:40 route.c-rw-rw-r-- 1 chyi chyi 8900 7월 12 12:40 route.h-rw-rw-r-- 1 chyi chyi 113323 7월 12 12:40 tcp.c-rw-rw-r-- 1 chyi chyi 3211 7월 12 12:40 tcp.h-rw-rw-r-- 1 chyi chyi 10672 7월 12 12:40 tcp_internal.h-rw-rw-r-- 1 chyi chyi 9637 7월 12 12:40 tcp_private.h-rw-rw-r-- 1 chyi chyi 12709 7월 12 12:40 tp.c-rw-rw-r-- 1 chyi chyi 4598 7월 12 12:40 tp.h-rw-rw-r-- 1 chyi chyi 1422 7월 12 12:40 tp_priv.h-rw-rw-r-- 1 chyi chyi 4712 7월 12 12:40 udp.c-rw-rw-r-- 1 chyi chyi 3370 7월 12 12:40 udp_internal.h-rw-rw-r-- 1 chyi chyi 20343 7월 12 12:40 utils.c📌 zephyr는 ipv6가 기본이지만, 여기서는 ipv4를 중심으로 살펴 보기로 한다.
코드량이 그리 많지 않으니, 전체적으로 훑어보는데 아무런 문제가 없다. 참고로, 패킷 수신 과정을 따라가 보면 대략 다음과 같다.
2.2 TCP/UDP echo server
- zephyr/drivers/ethernet/eth_stm32_hal.c
- zephyr/subsys/net/l2/ethernet/ethernet.c
자, 그럼 TCP/UDP echo server(4242 port 사용) 예제를 하나 돌려 보도록 하자.
<Echo Server 예제 소개>chyi@earth:~/zephyrproject/zephyr$ west build -b nucleo_f207zg samples/net/sockets/echo_server --pristine...chyi@earth:~/zephyrproject/zephyr$ west flash...
$ sudo minicom -D /dev/ttyACM0
[그림 2.5] Serial console 실행 모습
Network의 경우는 network shell 이라는게 있어서, (serial console 상에서) 아래와 같이 명령어를 직접 입력할 수 있다.
[그림 2.6] Serial console 실행 모습 - network shell📌 아무리 작은 시스템일지라도 있을 건 다 있다. ㅋ
uart:~$ net iface Hostname: zephyr Interface eth0 (0x20002a1c) (Ethernet) [1] =================================== Link addr : 00:80:E1:4F:AC:DA MTU : 1500 Flags : AUTO_START,IPv4,IPv6 Device : ethernet@40028000 (0x8024de4) Ethernet capabilities supported: 10 Mbits 100 Mbits IPv6 unicast addresses (max 3): fe80::280:e1ff:fe4f:acda autoconf preferred infinite 2001:db8::1 manual preferred infinite IPv6 multicast addresses (max 4): ff02::1 ff02::1:ff4f:acda ff02::1:ff00:1 IPv6 prefixes (max 2): <none> IPv6 hop limit : 64 IPv6 base reachable time : 30000 IPv6 reachable time : 36131 IPv6 retransmit timer : 0 IPv4 unicast addresses (max 1): 192.168.8.101/255.255.255.0 manual preferred infinite IPv4 multicast addresses (max 2): 224.0.0.1 IPv4 gateway : 0.0.0.0
uart:~$ net ping 192.168.8.1 PING 192.168.8.1 28 bytes from 192.168.8.1 to 192.168.8.101: icmp_seq=1 ttl=64 time=1 ms 28 bytes from 192.168.8.1 to 192.168.8.101: icmp_seq=2 ttl=64 time=1 ms 28 bytes from 192.168.8.1 to 192.168.8.101: icmp_seq=3 ttl=64 time=0 ms
아래 코드(snippet)는 TCP/UDP echo server의 대략적인 흐름을 정리해 본 것이다. 특이한 점은 tcp, udp 모두 패킷 수신 부분을 thread로 처리하고 있다는 점이다(사실 특이한 점도 아니다). 😋
위의 내용 중, wireguard porting을 위해서는 udp process_udp4 thread와 recvfrom( ) 함수 호출 부분을 눈여겨 볼 필요가 있다. udp recvfrom( )으로 수신한 패킷을 복호화한 후(internal original ip packet을 얻게됨)에는 아래 함수를 사용하여 다시 ip stack으로 패킷을 던져주기만 하면 될 것이다(Check Point#1).
recvfrom( ) -> wireguardif_network_rx() -> wireguardif_process_data_message( ) -> decrypt_wg_packet() -> net_ipv4_input(struct net_pkt *pkt, bool is_loopback)
지금까지 zephyr의 tcp/ip stack의 구조를 간략히 살펴본 후, echo server 예제를 분석해 보았다. 다음 장에서는 가상 network driver를 하나 만들고, 이를 wireguard porting 시 어떻게 활용할 지에 대해서 고민해 보고자 한다.
3. Zephyr Virtual Network Interface 만들기이번 장에서는 zephyr/samples/net/virtual 예제를 기반으로, virtual interface를 만드는 방법을 소개해 보고자 한다.
Virtual interface code를 사용해야 하는 이유는 linux의 tun device 같은 것이 필요하기 때문이다. 즉, virtual interface에 vpn ip가 설정되어 있으므로, peer vpn ip로 패킷을 내보내게 되면, (routing에 의해) 자연스럽게 virtual interface의 send 함수가 호출될 것이고, 이 함수 내에서 wireguard 암호화 처리 후, 이를 실제 물리적인 interface를 통해 패킷을 내보내게 되면, 원하는 처리(암호화 및 터널링)가 이루어지게 되는 것이다(Check Point#2).
📌 주의할 점은 원할한 통신을 위해 ip forwarding이 enable되어 있어야 한다는 것이다.
[그림 3.1] Linux의 tun device를 이용한 wireguard 통신 개념도📌 실제로 Linux의 경우는 tun driver의 send 함수가 호출되는 구조는 아니고, kernel buffer에 packet이 쌓이면, user space application에서 이를 읽어가 사용하는 형태이다.
virtual_wg_interface_send( ) -> wireguardif_output( ) -> wireguardif_output_to_peer( ) -> wireguard_encrypt_packet( ) -> wireguardif_peer_output( ) -> sendto( )
먼저, 예제 코드(samples/net/virtual)를 하나 돌려 보도록 하자.
3.1 Virtual Interface 예제chyi@earth:~/zephyrproject/zephyr$ west build -b nucleo_f207zg samples/net/virtual --pristine
...chyi@earth:~/zephyrproject/zephyr$ west flash...
$ sudo minicom -D /dev/ttyACM0
*** Booting Zephyr OS build v3.7.0-rc2-417-g1e20f58c17c1 ***[00:00:01.551,000] <inf> net_virtual_interface_sample: Start application (dev VIRTUAL_TEST/0x8021d2c)[00:00:01.551,000] <inf> net_virtual_interface_sample: My example tunnel interface 3 (VirtualTest-1 / 0x2000142c)[00:00:01.551,000] <inf> net_virtual_interface_sample: Tunnel interface 2 ( / 0x2000131c)[00:00:01.551,000] <inf> net_virtual_interface_sample: Tunnel interface 4 (VirtualTest-2 / 0x2000153c)[00:00:01.551,000] <inf> net_virtual_interface_sample: IPIP interface -1 (0)[00:00:01.551,000] <inf> net_virtual_interface_sample: Ethernet interface 1 (0x2000120c)[00:00:01.552,000] <inf> net_virtual_interface_sample: This interface 3/0x2000142c attached to 2/0x2000131c
uart:~$ net iface -> 현재 동작 중인 network interface를 출력해 본다.Interface eth0 (0x2000120c) (Ethernet) [1]===================================Virtual interfaces attached to this : 2 Link addr : 00:80:E1:4F:AC:DAMTU : 1500Flags : AUTO_START,IPv4,IPv6Device : ethernet@40028000 (0x8021d54)Ethernet capabilities supported: 10 Mbits 100 MbitsIPv6 unicast addresses (max 3): 2001:db8::1 manual preferred infinite fe80::280:e1ff:fe4f:acda autoconf preferred infiniteIPv6 multicast addresses (max 4): ff02::1 ff02::1:ff00:1 ff02::1:ff4f:acdaIPv6 prefixes (max 2): <none>IPv6 hop limit : 64IPv6 base reachable time : 30000IPv6 reachable time : 43513IPv6 retransmit timer : 0IPv4 unicast addresses (max 1): 192.0.2.1/255.255.255.0 manual preferred infiniteIPv4 multicast addresses (max 2): 224.0.0.1IPv4 gateway : 0.0.0.0
Interface net0 (0x2000131c) (Virtual) [2]==================================Interface is down.
Interface net1 (0x2000142c) (Virtual) [3]==================================Interface is down.
Interface net2 (0x2000153c) (Virtual) [4]==================================Interface is down.
Interface net3 (0x2000164c) (Virtual) [5]==================================Interface is down.
확인 결과, 물리 interface인 eth0를 포함하여, 가상의 interface net0 ~ net3까지 총 5개의 interface가 출력된다. 그리고, 가상의 interface net0 ~ net3은 모두 down되어 있음을 알 수 있다.
uart:~$ net iface up 2Interface 2 is up -> down된 interface는 shell 명령어를 이용해 up 시킬 수가 있다.
uart:~$ net iface
Interface eth0 (0x2000120c) (Ethernet) [1]===================================Virtual interfaces attached to this : 2 Link addr : 00:80:E1:4F:AC:DAMTU : 1500Flags : AUTO_START,IPv4,IPv6Device : ethernet@40028000 (0x8021d54)Ethernet capabilities supported: 10 Mbits 100 MbitsIPv6 unicast addresses (max 3): 2001:db8::1 manual preferred infinite fe80::280:e1ff:fe4f:acda autoconf preferred infiniteIPv6 multicast addresses (max 4): ff02::1 ff02::1:ff00:1 ff02::1:ff4f:acdaIPv6 prefixes (max 2): <none>IPv6 hop limit : 64IPv6 base reachable time : 30000IPv6 reachable time : 44989IPv6 retransmit timer : 0IPv4 unicast addresses (max 1): 192.0.2.1/255.255.255.0 manual preferred infiniteIPv4 multicast addresses (max 2): 224.0.0.1IPv4 gateway : 0.0.0.0
Interface net0 (0x2000131c) (Virtual) [2]==================================Virtual interfaces attached to this : 3 Virtual name : <unknown>Attached : 1 (Ethernet / 0x2000120c)Link addr : BE:87:79:7C:01:E0MTU : 576Flags : POINTOPOINT,NO_AUTO_START,IPv4,IPv6Device : IP_TUNNEL0 (0x8021d40)IPv6 not enabled for this interface.IPv4 unicast addresses (max 1): <none>IPv4 multicast addresses (max 2): 224.0.0.1IPv4 gateway : 0.0.0.0
Interface net1 (0x2000142c) (Virtual) [3]==================================Interface is down.
Interface net2 (0x2000153c) (Virtual) [4]==================================Interface is down.
Interface net3 (0x2000164c) (Virtual) [5]==================================Interface is down.
3.2 Virtual Interface 코드 분석Virtual interface 예제 코드를 분석해 볼 필요가 있겠는데, 대략적인 골짜만 정리해 보면 다음과 같다._____________________________________________________________
struct ud { struct net_if *my_iface; //my interface(가상 interface) struct net_if *ethernet; //physical interface(eth0) struct net_if *ip_tunnel_1; //ip tunnel 1 interface(가상 interface) struct net_if *ip_tunnel_2; //ip tunnel 2 interface(가상 interface) struct net_if *ipip; //ipip tunnel interface(가상 interface)};📌 가상 interface는 4개 중 하나만 필요하다.
static const struct virtual_interface_api virtual_test_iface_api = { .iface_api.init = virtual_test_iface_init, //virtual interface 초기화
.get_capabilities = virtual_test_get_capabilities, .start = virtual_test_interface_start, //virtual interface 시작 .stop = virtual_test_interface_stop, //virtual interface 중지 .send = virtual_test_interface_send, //virtual interface packet send .recv = virtual_test_interface_recv, //virtual interface packet receive .attach = virtual_test_interface_attach, //virtual interface를 다른 interface에 attach};
//가상 interface를 생성해 준다. net_virtual_interface_attach() 함수를 호출하므로써 다른 interface에 연결(binding)되게 된다.NET_VIRTUAL_INTERFACE_INIT(virtual_test1, VIRTUAL_TEST, NULL, NULL, &virtual_test_context_data1, NULL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &virtual_test_iface_api, VIRTUAL_TEST_MTU);
main( ){ net_if_foreach(iface_cb, &ud) //5개의 interface의 iface_cb() callback 호출하여 iface 초기화 net_virtual_interface_attach() //특정 virtual interface를 다른 virtual or physical interface에 attach 시킴 ... setup_iface() //interface setup 즉, ip/netmask, mtu 등 설정 ...}
<prj.conf>//virtual interface의 ip/netmask 값은 prj.conf에 설정되어 있다.CONFIG_NET_SAMPLE_IFACE2_MY_IPV6_ADDR="2001:db8:100::1"# TEST-NET-2 from RFC 5737CONFIG_NET_SAMPLE_IFACE2_MY_IPV4_ADDR="198.51.100.1"CONFIG_NET_SAMPLE_IFACE2_MY_IPV4_NETMASK="255.255.255.0"_____________________________________________________________
_____________________________________________________________static int virtual_test_interface_send(struct net_if *iface, struct net_pkt *pkt){ struct virtual_test_context *ctx = net_if_get_device(iface)->data;
if (ctx->attached_to == NULL) { return -ENOENT; }
#if 0 return net_send_data(pkt);#else //아래의 코드 추가 예정#endif}_____________________________________________________________
우리에게 필요한 가상 interface는 1개면 충분하다. 따라서 1개의 가상 interface를 생성한 후, physical interface에 attach하는 코드만 남기고 나머지 코드는 모두 제거하도록 하자.
<가상 interface> => attach => <물리 interface>
3.3 echo server와 virtual interface 예제 코드 통합이제 부터는 2장과 3장에서 소개한 두개의 program을 (아래와 같이) 하나로 통합하도록 하자.
chyi@earth:~/zephyrproject/zephyr/samples/net/sockets/wireguard$ ls -al합계 44drwxrwxr-x 4 chyi chyi 4096 11월 28 18:26 .drwxrwxr-x 25 chyi chyi 4096 11월 28 18:26 ..-rw-rw-r-- 1 chyi chyi 596 11월 28 17:50 CMakeLists.txt-rw-rw-r-- 1 chyi chyi 2384 7월 12 12:40 Kconfig-rw-rw-r-- 1 chyi chyi 4359 7월 12 12:40 README.rstdrwxrwxr-x 2 chyi chyi 4096 11월 28 13:45 boards-rw-rw-r-- 1 chyi chyi 1618 11월 28 18:10 prj.conf-rw-rw-r-- 1 chyi chyi 4307 7월 12 12:40 sample.yamldrwxrwxr-x 2 chyi chyi 4096 11월 28 18:26 srcchyi@earth:~/zephyrproject/zephyr/samples/net/sockets/wireguard$ cd srcchyi@earth:~/zephyrproject/zephyr/samples/net/sockets/wireguard/src$ ls -la합계 28drwxrwxr-x 2 chyi chyi 4096 11월 28 18:26 .drwxrwxr-x 4 chyi chyi 4096 11월 28 18:26 ..-rw-rw-r-- 1 chyi chyi 1196 11월 28 17:48 common.h //common header-rw-rw-r-- 1 chyi chyi 3911 11월 28 17:53 pack_recv.c //udp packet reception codes-rw-rw-r-- 1 chyi chyi 7668 11월 28 18:23 tunnel.c //virutal interface codes-rw-rw-r-- 1 chyi chyi 3481 11월 28 17:33 wg_main.c //main routine
4. Linux 사용자 영역에서 동작하는 WireGuard 구현(C언어 버젼) 하기익히 알고 있는 바와 같이, linux kernel용 wireguard는 C로 구현되어 있으나, 이를 다른 OS로 porting하기는 쉽지가 않다(이유: linux kernel에 특화되어 있기 때문). 그렇다고 사용자 영역에서 구현된 wireguard-go code를 사용하는 것도 문제가 있다(이유: Go로 compile한 binary 크기가 너무 크다).📌 TinyGo 같은 것을 사용하여 binary를 작게 만드는 방법도 있으나, 몇가지 API를 지원하지 않는 문제가 있어, 이 마져도 쉽지가 않다.
그렇다면, C로 구현되어 있으면서, 사용자 영역에서 동작하는 것은 없을까 ? 이런 고민을 하던 차에, 반가운 open source를 하나 발견했다. 😍
바로, Daniel Hope이 2021년에 작성한 코드로 wireguard를 lwip 기반 위에서 돌아가도록 해 주고 있다. 아래 두 link는 이 코드를 기반으로 약간의 변형(ESP32 등에서 동작)을 가한 example들이다.
4.1 wireguard-lwip code 분석따라서 이번 장에서는 Zephyr RTOS를 위한 WireGuard porting 그 두번째 단계로써, wireguard-lwip code를 Linux userspace용으로 porting하는 내용을 다뤄 보고자 한다. Linux용 porting을 먼저하는 이유는 LwIP code를 걷어내는 과정에서 문제가 없는 지를 미리 검증해 보기 위해서이다.
📌 나중에 보면 알게되겠지만, linux용으로 porting을 먼저하게 되면, zephyr로 porting하는 절차가 매우 쉬워진다(이유: zephyr가 POSIX standard를 따르고 있기 때문).
[그림 4.1] wireguard-lwip source code 목록
위의 코드는 WireGuard 원 저자인 Jason A. Donenfeld가 작성한 것에 비해 한 수준 아래의 코드로 보이지만, 나름 쓸만하다고 판단된다.
📌 오해는 마시길~ Daniel Hope이 작성한 코드도 매우 훌륭하다. 😂
[그림 4.2] wireguard 초기 설정 흐름(1)
[그림 4.3] wireguard 초기 설정 흐름(2)
[그림 4.4] 패킷 수신 코드 주요 흐름
[그림 4.5] 패킷 송신 코드 주요 흐름
여기까지 wireguard-lwip code를 대략적으로 살펴 보았으니, 이제부터 할 일은 linux tun device의 동작 원리를 파악한 후, 이 내용을 코드에 반영하는 것이 될 것이다. 아래 그림은 tun device의 동작 원리를 보여준다.
[그림 4.6] linux tun device 동작 원리 [출처 - 참고문헌 6]이를 기반으로 vpn packet을 송신하거나 수신하는 절차를 보다 구체화 해보면 다음과 같다.
<tun device 기반 vpn packet 송신 과정>
먼저 tun0 interface에 VPN ip 주소 10.1.1.100/24을 할당(설정)해 준다.
App은 peer VPN(예: 10.1.1.200)을 향하여 data를 내보낸다. 10.1.1.200은 10.1.1.0/24 network에 속하므로, 해당 패킷은 tun0 interface로 전달된다.
tun0 interface로 전달된 패킷은 외부로 나가지 않고, (tun device driver) read buffer에 쌓인다.
위의 그림에서 Process로 표시된 VPN client는 tun0 interface로 부터 패킷을 수신(read 함수 호출)한 후, 암호(encryption) 및 터널(capsulation) 처리를 한 후, 실제 물리 interface인 eth0로 vpn 패킷을 내보낸다(sendto 함수 호출).
<tun device 기반 vpn packet 수신 과정>
Peer VPN은 특정 vpn 포트로 VPN packet을 전송한다.
패킷을 수신한 Process 즉, VPN Client는 decapsulation & decryption 과정을 통해 추출한 패킷을 tun0 interface로 내보낸다(write). 왜냐하면, decapsulation & decryption 과정을 통해 추출한 패킷의 src/dst ip가 tun0 interface의 ip 대역에 포함되기 때문이다(라우팅의 동작 원리에 입각하여 처리된다는 뜻이다).
tun0 interface로 전달된 packet의 목적지 주소가 10.1.1.100 즉, 자기 자신인 관계로 해당 패킷은 application(App)으로 전달된다.
[그림 4.8] userspace wireguard decryption(decapsulation) 개요도
4.2 Linux 사용자 영역에서 동작하는 wireguard porting 하기자, 코드 분석과 설계 방향이 정해졌으니, 앞서 설명한 내용을 기반으로 wireguard-lwip를 linux용으로 porting해 보도록 하자.
chyi@earth:/mnt/hdd/workspace/mygithub_prj/wireguard-c/src$ ls -la -> 실제로 작업한 내용은 다음과 같다.합계 200drwxrwxr-x 5 chyi chyi 4096 12월 2 16:32 .drwxrwxr-x 6 chyi chyi 4096 12월 2 15:41 ..-rw-rw-r-- 1 chyi chyi 957 12월 1 16:15 Makefiledrwxrwxr-x 2 chyi chyi 4096 12월 2 16:32 crypto-rw-rw-r-- 1 chyi chyi 2074 11월 29 15:09 crypto.c-rw-rw-r-- 1 chyi chyi 3147 12월 1 17:59 crypto.hdrwxrwxr-x 2 chyi chyi 4096 12월 2 16:32 libdrwxrwxr-x 2 chyi chyi 4096 11월 30 18:11 lwip_h-rw-rw-r-- 1 chyi chyi 6091 12월 2 12:09 wg_comm.c-rw-rw-r-- 1 chyi chyi 845 12월 2 15:27 wg_comm.h-rw-rw-r-- 1 chyi chyi 5153 12월 2 16:20 wg_config.c-rw-rw-r-- 1 chyi chyi 2316 12월 2 16:16 wg_config.h-rw-rw-r-- 1 chyi chyi 6486 12월 2 14:24 wg_main.c-rw-rw-r-- 1 chyi chyi 733 12월 2 15:28 wg_main.h-rw-rw-r-- 1 chyi chyi 1425 12월 2 11:37 wg_timer.c-rw-rw-r-- 1 chyi chyi 383 12월 2 15:28 wg_timer.h-rw-rw-r-- 1 chyi chyi 6172 12월 2 13:30 wg_tun.c-rw-rw-r-- 1 chyi chyi 929 12월 2 15:28 wg_tun.h-rw-rw-r-- 1 chyi chyi 2989 12월 2 15:23 wireguard-platform.c-rw-rw-r-- 1 chyi chyi 3013 4월 26 2024 wireguard-platform.h-rw-rw-r-- 1 chyi chyi 40247 4월 26 2024 wireguard.c-rw-rw-r-- 1 chyi chyi 10883 12월 2 15:29 wireguard.h-rw-rw-r-- 1 chyi chyi 3675 12월 2 16:18 wireguard_vpn.c-rw-rw-r-- 1 chyi chyi 896 12월 2 16:15 wireguard_vpn.h-rw-rw-r-- 1 chyi chyi 33667 12월 2 15:24 wireguardif.c-rw-rw-r-- 1 chyi chyi 4795 12월 2 15:26 wireguardif.h
chyi@earth:/mnt/hdd/workspace/mygithub_prj/wireguard-c/src$ make -> compile도 문제없이 진행된다.gcc -W -Wall -D_GNU_SOURCE -g -c wg_main.c -o wg_main.ogcc -W -Wall -D_GNU_SOURCE -g -c wg_comm.c -o wg_comm.ogcc -W -Wall -D_GNU_SOURCE -g -c wg_config.c -o wg_config.ogcc -W -Wall -D_GNU_SOURCE -g -c wg_tun_device_common.c -o wg_tun_device_common.ogcc -W -Wall -D_GNU_SOURCE -g -c wg_tun_device_linux.c -o wg_tun_device_linux.ogcc -W -Wall -D_GNU_SOURCE -g -c wireguard_vpn.c -o wireguard_vpn.ogcc -W -Wall -D_GNU_SOURCE -g -c wireguardif.c -o wireguardif.ogcc -W -Wall -D_GNU_SOURCE -g -c wireguard.c -o wireguard.ogcc -W -Wall -D_GNU_SOURCE -g -c wireguard-platform.c -o wireguard-platform.ogcc -W -Wall -D_GNU_SOURCE -g -c wg_timer.c -o wg_timer.ogcc -W -Wall -D_GNU_SOURCE -g -c crypto.c -o crypto.ogcc -W -Wall -D_GNU_SOURCE -g -c crypto/blake2s.c -o crypto/blake2s.ogcc -W -Wall -D_GNU_SOURCE -g -c crypto/chacha20.c -o crypto/chacha20.ogcc -W -Wall -D_GNU_SOURCE -g -c crypto/chacha20poly1305.c -o crypto/chacha20poly1305.ogcc -W -Wall -D_GNU_SOURCE -g -c crypto/poly1305-donna.c -o crypto/poly1305-donna.ogcc -W -Wall -D_GNU_SOURCE -g -c crypto/x25519.c -o crypto/x25519.ogcc -W -Wall -D_GNU_SOURCE -g -c lib/log.c -o lib/log.ogcc -W -Wall -D_GNU_SOURCE -g -c lib/strlib.c -o lib/strlib.ogcc -W -Wall -D_GNU_SOURCE -g -o wireguard wg_main.o wg_comm.o wg_config.o wg_tun_device_common.o wg_tun_device_linux.o wireguard_vpn.o wireguardif.o wireguard.o wireguard-platform.o wg_timer.o crypto.o crypto/blake2s.o crypto/chacha20.o crypto/chacha20poly1305.o crypto/poly1305-donna.o crypto/x25519.o lib/log.o lib/strlib.o -lpthread
chyi@earth:/mnt/hdd/workspace/mygithub_prj/wireguard-c/etc$ vi wireguard.conf -> peer와의 vpn 시험을 하기 전에, 이 파일을 열어, 적절히 수정해 준다.
## wireguard-c configuration file#
#debug flagdebug=1
#Local information ============================================#Local vpn ipv4 address & subnet maskmy_vpn_ip_address=10.1.1.100my_vpn_netmask=255.255.255.0my_vpn_netmask_CIDR=24
#local wireguard portlocal_wg_port=51820
#local private keylocal_wg_private_key="iHsZNqK/OW7ExUccUkLvAv6ihz787ZjQFXR9l0EbJkU="
#Peer information ============================================#Peer vpn ipv4 addresspeer_vpn_ip_address=10.1.1.200
#Endpoint addressendpoint_ip_address=192.168.8.139
#peer wireguard portpeer_wg_port=51820
#peer public keypeer_wg_public_key="isbaRdaRiSo5/WtqEdmpH+NrFeT1+QoLvnhVI1oFfhE="~
아래와 같이 실행해 보자(일단 죽지 않고 돌아간다).
chyi@earth:/mnt/hdd/workspace/mygithub_prj/wireguard-c/src$ sudo ./wireguard -d ../etc/wireguard.conf[sudo] chyi 암호: Starting Wireguard VPN
따라서 이번 장에서는 Zephyr RTOS를 위한 WireGuard porting 그 두번째 단계로써, wireguard-lwip code를 Linux userspace용으로 porting하는 내용을 다뤄 보고자 한다. Linux용 porting을 먼저하는 이유는 LwIP code를 걷어내는 과정에서 문제가 없는 지를 미리 검증해 보기 위해서이다.
📌 나중에 보면 알게되겠지만, linux용으로 porting을 먼저하게 되면, zephyr로 porting하는 절차가 매우 쉬워진다(이유: zephyr가 POSIX standard를 따르고 있기 때문).
위의 코드는 WireGuard 원 저자인 Jason A. Donenfeld가 작성한 것에 비해 한 수준 아래의 코드로 보이지만, 나름 쓸만하다고 판단된다.
📌 오해는 마시길~ Daniel Hope이 작성한 코드도 매우 훌륭하다. 😂[그림 4.2] wireguard 초기 설정 흐름(1)
[그림 4.3] wireguard 초기 설정 흐름(2)
[그림 4.4] 패킷 수신 코드 주요 흐름
[그림 4.5] 패킷 송신 코드 주요 흐름
여기까지 wireguard-lwip code를 대략적으로 살펴 보았으니, 이제부터 할 일은 linux tun device의 동작 원리를 파악한 후, 이 내용을 코드에 반영하는 것이 될 것이다. 아래 그림은 tun device의 동작 원리를 보여준다.
<tun device 기반 vpn packet 송신 과정>
먼저 tun0 interface에 VPN ip 주소 10.1.1.100/24을 할당(설정)해 준다.
App은 peer VPN(예: 10.1.1.200)을 향하여 data를 내보낸다. 10.1.1.200은 10.1.1.0/24 network에 속하므로, 해당 패킷은 tun0 interface로 전달된다.
tun0 interface로 전달된 패킷은 외부로 나가지 않고, (tun device driver) read buffer에 쌓인다.
위의 그림에서 Process로 표시된 VPN client는 tun0 interface로 부터 패킷을 수신(read 함수 호출)한 후, 암호(encryption) 및 터널(capsulation) 처리를 한 후, 실제 물리 interface인 eth0로 vpn 패킷을 내보낸다(sendto 함수 호출).
<tun device 기반 vpn packet 수신 과정>
Peer VPN은 특정 vpn 포트로 VPN packet을 전송한다.
패킷을 수신한 Process 즉, VPN Client는 decapsulation & decryption 과정을 통해 추출한 패킷을 tun0 interface로 내보낸다(write). 왜냐하면, decapsulation & decryption 과정을 통해 추출한 패킷의 src/dst ip가 tun0 interface의 ip 대역에 포함되기 때문이다(라우팅의 동작 원리에 입각하여 처리된다는 뜻이다).
tun0 interface로 전달된 packet의 목적지 주소가 10.1.1.100 즉, 자기 자신인 관계로 해당 패킷은 application(App)으로 전달된다.
자, 코드 분석과 설계 방향이 정해졌으니, 앞서 설명한 내용을 기반으로 wireguard-lwip를 linux용으로 porting해 보도록 하자.
끝으로, 지금까지 작업한 내용은 아래 github에서 확인 가능하다.
5. Zephyr 환경에서 동작하는 WireGuard Porting하기 I - Ethernet 편이번 장에서는 2-4장의 내용을 토대로하여, WireGuard protocol을 Zephyr RTOS에 porting하는 방법을 소개해 보고자 한다.
<핵심 Points>1) 새로운 network interface를 어떻게 등록할 것인가 ? virtual interface 예제 활용2) Packet 수신을 어떻게 할 것인가 ? 실제로는 udp 패킷 수신 후, wireguard packet recv 처리 루틴을 타도록 처리하면 될 것임.3) Encrypt 후, 실제 물리 interface를 통해 packet send를 어떻게 할 것인가 ? virtual interface의 send routine에서 암호화 처리 후, ethernet driver로 packet을 내보내면 될 듯.
1) packet send, recv 부분이 제대로 동작하는지 확인해야 한다.2) ip forwarding이 enable되어 있는지 확인한다.3) wireguard protocol은 time 관련 필드가 header에 포함되는 바, 시간을 구하는 함수가 제대로 동작해야 한다(시간이 순차적으로 증가하지 않으면 해당 packet은 drop된다).4) pthread code를 zephyr thread code로 적절히 교체해야 한다.5) timer 함수를 zephyr 함수로 교체해야 한다.6) log 함수를 zephyr code로 교체해야 한다.7) stack size 등에 문제가 없는지 확인한다.
chyi@earth:~/zephyrproject/zephyr$ west build -b nucleo_f207zg samples/net/sockets/wireguard --pristine
chyi@earth:~/zephyrproject/zephyr$ west flash
지금까지 작업한 내용을 아래 환경에서 확인해 보도록 하자.
<시험 환경>
wireguard kernel(Ubuntu 18.04 10.1.1.200/24) <== Switch ==> zephyr wireguard app(10.1.1.50/24)
<시험 결과>1) handshaking은 정상 동작한다.2) send routine은 이상이 없다.3) packet 수신 후, 복호화 과정도 이상이 없다.4) 문제는 복호화 후, 응답을 내보낼 때, virtual interface send 함수를 타지 않고, eth0로 내보낸다. 💣 -> zephyr net shell에서 net ping 10.1.1.200을 하면 virtual interface send 함수를 타고 peer로 나간다.
<문제 해결>pkt = net_pkt_alloc_with_buffer(eth_if, tot_len, AF_INET, IPPROTO_IP, K_NO_WAIT);->pkt = net_pkt_alloc_with_buffer(tun_if, tot_len, AF_INET, IPPROTO_IP, K_NO_WAIT);
OK, net_ipv4_input() 함수 호출 전에 net_pkt을 할당하는 함수의 net_if 부분을 virtual interface 정보로 교체하니 정상 동작한다. 😎
이 상태에서 아래 명령을 실행해 주면, virtual interface가 up되고, 이후 wireguard handshaking이 시작된다.
$ net iface up 2
아래 2개의 화면은 peer machine에서 zephyr wireguard로 ping을 하고, tcpdump로 packet capture를 한 모습니다. 물론 zephyr console에서 net ping 10.1.1.200을 해도 정상적으로 ping이 된다.
[그림 5.7] Peer(Ubuntu 18.04)에서 tcpdump하는 모습
Zephyr용 WireGuard source code는 아래 github에서 확인 가능하다.
지금까지 Zephyr OS 상에서 동작하는 wireguard를 구현하는 과정을 상세히 살펴 보았다. 끝까지 읽어 주신 독자 여러분께 감사의 마음을 전한다. 😎
6. Zephyr 환경에서 동작하는 WireGuard Porting하기 II - Wi-Fi 편
To be continued...
7. References[1] https://www.st.com/en/evaluation-tools/nucleo-f207zg.html
<시험 환경>
wireguard kernel(Ubuntu 18.04 10.1.1.200/24) <== Switch ==> zephyr wireguard app(10.1.1.50/24)