2024년 12월 3일 화요일

Zephyr RTOS Programming

지난 시간에 이어 Zephyr RTOS programming 얘기(3번째 시간)를 계속 이어가 보도록 하겠다. 이번 시간의 주제는 Zephyr TCP/IP NetworkWireGuard Protocol(Zephyr에 porting하는 방법)에 관한 것이다.  😎


목차

1. STM32 Nucleo-F207ZG 보드
2. Zephyr TCP/IP Stack
3. 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.1] STM32 Nucleo F207ZG 보드 - top view [출처 - 참고문헌 2]

[그림 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를 기반으로 작업 방법이 바뀔 것을 기대해 본다. 🙏

[그림 1.3] STM32 Nucleo F207ZG 보드 Block Diagram [출처 - 참고문헌 2]
📌 솔직히, 기존의 익숙한 방법을 버리고 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.4] STM32 Nucleo F207ZG 보드 Schematic에서 발췌 [출처 - 참고문헌 3]


[그림 1.5] STM32 Nucleo F207ZG 보드 - ethernet pins [출처 - 참고문헌 2]

[그림 1.6] STM32 Nucleo F207ZG 보드 device tree - mac 부분 발췌


[그림 1.7] RMII Signals 설명 [출처 - https://en.wikipedia.org/wiki/Media-independent_interface#RMII]

이 보드와 관련하여 보다 자세한 사항은 아래 site의 내용을 참조해 주기 바란다.



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 까지 한 걸음에 확인해 보도록 하자. 💪


[그림 2.1] Zephyr network tx data flow [출처 - 참고문헌 4]

📌 패킷 송신은 연속되는 함수 call의 과정이다.

[그림 2.2] Zephyr network rx data flow [출처 - 참고문헌 4]
📌 대개 패킷 수신은 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
합계 1116
drwxrwxr-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.3] Zephyr network rx flow(패킷 수신 흐름도)

2.2 TCP/UDP echo server

자, 그럼 TCP/UDP echo server(4242 port 사용) 예제를 하나 돌려 보도록 하자.

[그림 2.4] STM32 Nucleo F207ZG 보드 (ethernet 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로 처리하고 있다는 점이다(사실 특이한 점도 아니다). 😋 

[그림 2.7] TCP/UDP echo-server code의 대략적인 흐름


위의 내용 중, 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)


[그림 2.8] WireGuard Packet Input Flow

지금까지 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( )

[그림 3.2] WireGuard Packet Output Flow


먼저, 예제 코드(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

[그림 3.3] Serial console 실행 모습

*** 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:DA
MTU       : 1500
Flags     : AUTO_START,IPv4,IPv6
Device    : ethernet@40028000 (0x8021d54)
Ethernet capabilities supported:
        10 Mbits
        100 Mbits
IPv6 unicast addresses (max 3):
        2001:db8::1 manual preferred infinite
        fe80::280:e1ff:fe4f:acda autoconf preferred infinite
IPv6 multicast addresses (max 4):
        ff02::1
        ff02::1:ff00:1
        ff02::1:ff4f:acda
IPv6 prefixes (max 2):
        <none>
IPv6 hop limit           : 64
IPv6 base reachable time : 30000
IPv6 reachable time      : 43513
IPv6 retransmit timer    : 0
IPv4 unicast addresses (max 1):
        192.0.2.1/255.255.255.0 manual preferred infinite
IPv4 multicast addresses (max 2):
        224.0.0.1
IPv4 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 2
Interface 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:DA
MTU       : 1500
Flags     : AUTO_START,IPv4,IPv6
Device    : ethernet@40028000 (0x8021d54)
Ethernet capabilities supported:
        10 Mbits
        100 Mbits
IPv6 unicast addresses (max 3):
        2001:db8::1 manual preferred infinite
        fe80::280:e1ff:fe4f:acda autoconf preferred infinite
IPv6 multicast addresses (max 4):
        ff02::1
        ff02::1:ff00:1
        ff02::1:ff4f:acda
IPv6 prefixes (max 2):
        <none>
IPv6 hop limit           : 64
IPv6 base reachable time : 30000
IPv6 reachable time      : 44989
IPv6 retransmit timer    : 0
IPv4 unicast addresses (max 1):
        192.0.2.1/255.255.255.0 manual preferred infinite
IPv4 multicast addresses (max 2):
        224.0.0.1
IPv4 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:E0
MTU       : 576
Flags     : POINTOPOINT,NO_AUTO_START,IPv4,IPv6
Device    : IP_TUNNEL0 (0x8021d40)
IPv6 not enabled for this interface.
IPv4 unicast addresses (max 1):
        <none>
IPv4 multicast addresses (max 2):
        224.0.0.1
IPv4 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 5737
CONFIG_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
합계 44
drwxrwxr-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.rst
drwxrwxr-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.yaml
drwxrwxr-x  2 chyi chyi 4096 11월 28 18:26 src
chyi@earth:~/zephyrproject/zephyr/samples/net/sockets/wireguard$ cd src
chyi@earth:~/zephyrproject/zephyr/samples/net/sockets/wireguard/src$ ls -la
합계 28
drwxrwxr-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 송신 과정>

  1. 먼저 tun0 interface에 VPN ip 주소 10.1.1.100/24을 할당(설정)해 준다.

  2. App은 peer VPN(예: 10.1.1.200)을 향하여 data를 내보낸다. 10.1.1.200은 10.1.1.0/24 network에 속하므로, 해당 패킷은 tun0 interface로 전달된다.

  3. tun0 interface로 전달된 패킷은 외부로 나가지 않고, (tun device driver) read buffer에 쌓인다.

  4. 위의 그림에서 Process로 표시된 VPN client는 tun0 interface로 부터 패킷을 수신(read 함수 호출)한 후, 암호(encryption) 및 터널(capsulation) 처리를 한 후, 실제 물리 interface인 eth0로 vpn 패킷을 내보낸다(sendto 함수 호출).


[그림 4.7] userspace wireguard encryption(encapsulation) 개요도

<tun device 기반 vpn packet 수신 과정>

  1. Peer VPN은 특정 vpn 포트로 VPN packet을 전송한다.

  2. 패킷을 수신한 Process 즉, VPN Client는 decapsulation & decryption 과정을 통해  추출한 패킷을 tun0 interface로 내보낸다(write). 왜냐하면, decapsulation & decryption 과정을 통해 추출한 패킷의 src/dst ip가 tun0 interface의 ip 대역에 포함되기 때문이다(라우팅의 동작 원리에 입각하여 처리된다는 뜻이다).

  3. 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
 -> 실제로 작업한 내용은 다음과 같다.
합계 200
drwxrwxr-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 Makefile
drwxrwxr-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.h
drwxrwxr-x 2 chyi chyi  4096 12월  2 16:32 lib
drwxrwxr-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.o
gcc -W -Wall -D_GNU_SOURCE -g -c wg_comm.c -o wg_comm.o
gcc -W -Wall -D_GNU_SOURCE -g -c wg_config.c -o wg_config.o
gcc -W -Wall -D_GNU_SOURCE -g -c wg_tun_device_common.c -o wg_tun_device_common.o
gcc -W -Wall -D_GNU_SOURCE -g -c wg_tun_device_linux.c -o wg_tun_device_linux.o
gcc -W -Wall -D_GNU_SOURCE -g -c wireguard_vpn.c -o wireguard_vpn.o
gcc -W -Wall -D_GNU_SOURCE -g -c wireguardif.c -o wireguardif.o
gcc -W -Wall -D_GNU_SOURCE -g -c wireguard.c -o wireguard.o
gcc -W -Wall -D_GNU_SOURCE -g -c wireguard-platform.c -o wireguard-platform.o
gcc -W -Wall -D_GNU_SOURCE -g -c wg_timer.c -o wg_timer.o
gcc -W -Wall -D_GNU_SOURCE -g -c crypto.c -o crypto.o
gcc -W -Wall -D_GNU_SOURCE -g -c crypto/blake2s.c -o crypto/blake2s.o
gcc -W -Wall -D_GNU_SOURCE -g -c crypto/chacha20.c -o crypto/chacha20.o
gcc -W -Wall -D_GNU_SOURCE -g -c crypto/chacha20poly1305.c -o crypto/chacha20poly1305.o
gcc -W -Wall -D_GNU_SOURCE -g -c crypto/poly1305-donna.c -o crypto/poly1305-donna.o
gcc -W -Wall -D_GNU_SOURCE -g -c crypto/x25519.c -o crypto/x25519.o
gcc -W -Wall -D_GNU_SOURCE -g -c lib/log.c -o lib/log.o
gcc -W -Wall -D_GNU_SOURCE -g -c lib/strlib.c -o lib/strlib.o
gcc -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 flag
debug=1

#Local information ============================================
#Local vpn ipv4 address & subnet mask
my_vpn_ip_address=10.1.1.100
my_vpn_netmask=255.255.255.0
my_vpn_netmask_CIDR=24

#local wireguard port
local_wg_port=51820

#local private key
local_wg_private_key="iHsZNqK/OW7ExUccUkLvAv6ihz787ZjQFXR9l0EbJkU="

#Peer information ============================================
#Peer vpn ipv4 address
peer_vpn_ip_address=10.1.1.200

#Endpoint address
endpoint_ip_address=192.168.8.139

#peer wireguard port
peer_wg_port=51820

#peer public key
peer_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

마지막으로 남은 일은 동작을 확인해 보는 것이다.


<시험 환경>

wireguard kernel(Ubuntu 18.04 10.1.1.200/24) <== Switch ==> wireguard-c daemon(Ubuntu 22.04 LTS, 10.1.1.100/24)


[그림 4.9] wireguard daemon 구동 모습


[그림 4.10] wireguard daemon 구동 상황에서 vpn peer로 ping한 모습

OK, 예상대로 잘 동작한다. 😎

끝으로, 지금까지 작업한 내용은 아래 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을 내보내면 될 듯.

[그림 5.1] Zephyr용 WireGuard 개요도

<Porting 시 주의 사항>
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 등에 문제가 없는지 확인한다.


5.1 Zephyr용 wireguard porting 하기

1차적으로 코드를 추가한 모습은 다음과 같다.


[그림 5.2] wireguard app용 CMakeLists.txt 파일 내용

 Compile도 이상이 없다.

chyi@earth:~/zephyrproject/zephyr$ west build -b nucleo_f207zg samples/net/sockets/wireguard --pristine

chyi@earth:~/zephyrproject/zephyr$ west flash


<주의 사항>

아직 정확한 이유는 알 수 없으나, ping 테스트 과정에서 (ICMP 패킷에 대한) Invalid checksum 에러가 발생하여, 아래 코드를 한 줄 막은 상태에서 테스트를 진행하였다.


[그림 5.3] zephyr/subsys/net/ip/icmpv4.c 파일 일부 수정


5.2 Zephyr용 wireguard 동작 시험하기

지금까지 작업한 내용을 아래 환경에서 확인해 보도록 하자.

<시험 환경>

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 정보로 교체하니 정상 동작한다. 😎

[그림 5.4] zephyr용 wireguard code 정상 동작 모습(1)

이 상태에서 아래 명령을 실행해 주면, virtual interface가 up되고, 이후 wireguard handshaking이 시작된다.

$ net iface up 2

[그림 5.5] zephyr용 wireguard code 정상 동작 모습(2)

아래 2개의 화면은 peer machine에서 zephyr wireguard로 ping을 하고, tcpdump로 packet capture를 한 모습니다. 물론 zephyr console에서 net ping 10.1.1.200을 해도 정상적으로 ping이 된다.

[그림 5.6] Peer(Ubuntu 18.04)에서 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

[2] STM32 Nucleo-144 boards(MB1137) - User manual, STMicroelectronics

[3] STM32 Nucleo-144 boards schematic, STMicroelectronics

[4] https://docs.zephyrproject.org/latest/connectivity/networking/net-stack-architecture.html
[5] https://lxd.me/a-simple-vpn-tunnel-with-tun-device-demo-and-some-basic-concepts
[6] https://recolog.blogspot.com/2016/06/tuntap-devices-on-linux.html
[7] https://github.com/smartalock/wireguard-lwip
[8] https://github.com/trombik/esp_wireguard 
[9] And, Google~

Slowboot