Skip to main content

if_ovpn 또는 OpenVPN

원본 : provost.pdf

By Kristof Provost

오늘1은 OpenVPN의 DCO에 대해 알아보겠습니다2.

제임스 요난이 처음 개발한 OpenVPN은 2001년 5월 13일에 처음 출시되었습니다. 이 서비스는 많은 일반적인 플랫폼(예: FreeBSD, OpenBSD, Dragonfly, AIX, ...)과 덜 일반적인 플랫폼(macOS, Linux, Windows)도 지원합니다. 사전 공유 키, 인증서 또는 사용자 이름/비밀번호 기반 인증으로 피어 투 피어 및 클라이언트 서버 모델을 지원합니다.

20년 이상 된 프로젝트에서 기대할 수 있듯이, 다양한 사용 사례를 위해 많은 기능이 추가되었습니다.

문제점

OpenVPN은 매우 훌륭하지만 분명히 문제가 있습니다. 문제가 없다면 이 글은 별로 흥미롭지 않을 것입니다3. 실제로 문제가 하나 있는데, 바로 OpenVPN이 단일 스레드의 사용자 공간 프로세스로 구현된다는 점입니다.

이 프로세스는 if_tun을 사용하여 네트워크 스택에 패킷을 주입합니다. 그 결과 성능이 현재의 연결 속도를 따라가지 못합니다. 또한 최신 멀티코어 하드웨어나 암호화 오프로드 하드웨어를 활용하기 어렵습니다.

provost_fig1.webp

OpenVPN 성능의 주요 문제는 사용자 공간 특성입니다. 들어오는 트래픽은 일반적으로 커널 메모리로 패킷을 DMA하는 NIC에 의해 자연스럽게 수신됩니다. 그런 다음 네트워크 스택에서 패킷이 속한 소켓을 파악하여 사용자 공간으로 전달할 때까지 추가 처리를 거칩니다. 이 소켓은 UDP 또는 TCP일 수 있습니다.

사용자 공간으로 패킷을 전달하려면 패킷을 복사해야 하며, 이때 사용자 공간 OpenVPN 프로세스는 패킷을 확인하고 복호화하여 if_tun을 사용하여 네트워크 스택에 다시 삽입합니다. 이는 추가 처리를 위해 일반 텍스트 패킷을 커널로 다시 복사하는 것을 의미합니다.

이러한 모든 컨텍스트 전환과 복사는 필연적으로 성능에 상당한 영향을 미칩니다.

현재 아키텍처에서는 성능을 크게 개선하기가 매우 어렵습니다.

DCO란?

이제 문제가 무엇인지 확인했으니 해결책을 생각해 볼 수 있습니다4.

컨텍스트가 사용자 공간으로 전환되는 것이 문제라면 작업을 커널 내부에 유지하는 것이 그럴듯한 해결책 중 하나이며, 이것이 바로 DCO(데이터 채널 오프로드)가 하는 일입니다.

provost_fig2.webp

DCO는 데이터 채널, 즉 암호화 작업과 트래픽 터널링을 커널로 이동합니다. 이 작업은 새로운 가상 장치 드라이버인 if_ovpn을 통해 수행됩니다. OpenVPN 사용자 공간 프로세스는 여전히 연결 설정(인증 및 옵션 협상 포함)을 담당하며, 새로운 ioctl 인터페이스를 통해 if_ovpn 드라이버와 조정합니다.

OpenVPN 프로젝트는 DCO의 도입이 일부 레거시 기능을 제거하고 전반적인 정리를 할 수 있는 좋은 기회라고 판단했습니다. 그 일환으로 암호화 알고리즘 선택에 헨리 포드 접근 방식을 채택했습니다. AES-GCM 또는 ChaCha20/Poly1305 중 원하는 알고리즘을 사용할 수 있습니다. 검은색으로. 또한 DCO는 압축, 레이어 2 트래픽, 비서브넷 토폴로지 또는 트래픽 쉐이핑5을 지원하지 않습니다.

여기서 한 가지 중요한 점은 DCO는 OpenVPN 프로토콜을 변경하지 않는다는 것입니다. 클라이언트가 이를 지원하지 않는 서버와 함께 사용하거나 그 반대의 경우도 가능합니다. 물론 양쪽 모두 사용할 때 가장 큰 이점을 얻을 수 있지만, 반드시 그럴 필요는 없습니다.

고려 사항

이 부분은 제가 이 모든 것이 얼마나 힘들었는지 이야기하는 부분이기 때문에 여러분 모두 제가 실제로 이 작업을 해냈다는 사실에 감탄할 것입니다. 제가 지금 하고 있는 일을 말씀드려도 여전히 효과가 있을까요? 한번 알아봅시다!

어쨌든 특별한 주의가 필요했던 몇 가지 사항이 있습니다:

Multiplexing

첫 번째 문제는 OpenVPN이 터널링된 데이터와 제어 데이터를 모두 전송하기 위해 단일 연결을 사용한다는 것입니다. 터널링된 데이터는 커널에서 처리해야 하고 제어 데이터는 OpenVPN 사용자 공간 프로세스에서 처리해야 합니다.

문제를 확인할 수 있습니다. 소켓은 처음에 열리고 OpenVPN 자체에 의해 완전히 소유됩니다. 이 소켓은 터널을 설정하고 인증을 처리합니다. 이 작업이 완료되면 커널 측(즉, if_ovpn)에 부분적으로 제어권을 넘깁니다.

즉, 커널이 커널 내 구조체 소켓을 조회하는 데 사용하는 파일 설명자를 if_ovpn에 알려주어 해당 파일에 대한 참조를 보유할 수 있도록 합니다. 이렇게 하면 커널이 소켓을 사용하는 동안 소켓이 사라지지 않습니다. OpenVPN 프로세스가 종료되었기 때문일 수도 있고, 나쁜 하루를 보내서 우리를 망치기로 결정했기 때문일 수도 있습니다. 사용자 공간은 미친 짓을 합니다.

커널 코드를 따라가고 싶은 분들을 위해, ovpn_new_peer()6 함수를 찾아보세요.

소켓을 찾았으면 이제 udp_set_kernel_tunneling()을 통해 필터링 기능을 설치할 수도 있습니다. 필터인 ovpn_udp_input()은 지정된 소켓으로 들어오는 모든 패킷을 살펴보고 처리해야 하는 페이로드 패킷인지 아니면 사용자 공간의 OpenVPN이 처리해야 하는 제어 패킷인지 결정합니다.

이 터널링 기능은 나머지 네트워크 스택에서 유일하게 변경해야 했던 부분이기도 합니다. 특정 패킷은 커널에서 처리하고 다른 패킷은 여전히 사용자 공간으로 전달할 수 있도록 학습시켜야 했습니다. 이 작업은 https://cgit.freebsd.org/src/commit/?id=742e7210d00b359d81b9c778ab520003704e9b6c 에서 수행되었습니다.

ovpn_udp_input() 함수는 수신 경로의 주요 진입점입니다. 네트워크 스택은 설치된 소켓에 도착하는 모든 UDP 패킷에 대해 이 함수에 패킷을 넘깁니다.

이 함수는 먼저 패킷이 커널 드라이버에서 처리될 수 있는지 확인합니다. 즉, 패킷이 데이터 패킷이고 알려진 피어 ID로 향하는 패킷인지 확인합니다. 그렇지 않은 경우 필터 함수는 필터 기능이 없는 것처럼 패킷을 정상적인 흐름으로 통과시키도록 UDP 코드에 지시합니다. 즉, 패킷이 소켓에 도착하여 OpenVPN의 사용자 공간 프로세스에 의해 처리됩니다.

초기 버전의 DCO 드라이버에는 제어 메시지를 읽고 쓰기 위한 별도의 ioctl 명령이 있었지만, Linux 및 FreeBSD 드라이버는 모두 소켓을 대신 사용하도록 조정되었습니다. 이렇게 하면 제어 패킷과 새 클라이언트의 처리가 모두 간소화됩니다.

반면에 패킷이 알려진 피어에 대한 데이터 패킷인 경우에는 해독하고 서명의 유효성을 검사한 다음 추가 처리를 위해 네트워크 스택으로 전달합니다.

자세한 내용은 https://cgit.freebsd.org/src/tree/sys/net/if_ovpn.c?id=da69782bf06645f38852a8b23af#n1483 에서 확인할 수 있습니다.

UDP

OpenVPN은 UDP와 TCP 모두에서 실행할 수 있습니다. UDP는 레이어 3 VPN 프로토콜을 위한 당연한 선택이지만, 일부 사용자는 방화벽을 통과하기 위해 TCP를 통해 실행해야 합니다.

FreeBSD 커널은 UDP 소켓을 위한 편리한 필터 기능을 제공하지만, TCP에 해당하는 기능이 없기 때문에 FreeBSD if_ovpn은 현재 UDP만 지원하고 TCP는 지원하지 않습니다.

리눅스 DCO 드라이버 개발자는 용기를 내어 TCP 지원도 구현하기로 결정했습니다. 이 개발자는 예상과 달리 실제로 이 경험에서 살아남았고 지금은 훨씬 더 현명해졌습니다.

Hardware Cryptography Offload

if_ovpn은 암호화 작업을 위해 커널 내 OpenCrypto 프레임워크에 의존합니다. 즉, 시스템에 존재하는 모든 암호화 오프로드 하드웨어를 활용할 수도 있습니다. 이를 통해 성능을 더욱 향상시킬 수 있습니다.

이미 인텔의 퀵어시스트 기술(QAT), SafeXcel EIP-97 암호화 가속기 및 AES-NI로 테스트되었습니다.

Locking Design

잠금에 대해 이야기할 필요 없이 커널 코드에 대해 논의할 수 있을 거라고 생각했다면 무슨 말을 해야 할지 모르겠네요. 그건 순진하게 낙관적인 생각이었죠.

거의 모든 최신 CPU에는 여러 개의 코어가 있으며, 그 중 하나 이상의 코어를 사용할 수 있으면 좋을 것입니다. 즉, 하나의 코어가 작동하는 동안 다른 코어를 잠글 수는 없습니다. 무례하죠. 성능도 좋지 않습니다.

다행히도 이 작업은 상당히 쉬운 것으로 밝혀졌습니다. 전체 접근 방식은 if_ovpn의 내부 데이터 구조에 대한 읽기 및 쓰기 액세스를 구분하는 것을 기반으로 합니다. 즉, 여러 코어가 동시에 무언가를 조회할 수 있도록 허용하지만 오직 한 코어만 변경할 수 있도록 허용합니다(변경이 진행되는 동안에는 어떤 읽기 권한도 허용하지 않음). 이는 대부분의 경우 변경할 필요가 없기 때문에 충분히 잘 작동하는 것으로 밝혀졌습니다.

일반적인 경우 패킷을 받거나 보낼 때 키, 대상 주소, 포트 및 기타 관련 정보를 조회하기만 하면 됩니다.

구성 변경이나 키 재설정 등 무언가를 수정해야 할 때만 쓰기 잠금을 취하고 데이터 채널을 일시 중지합니다. 이는 인간의 작은 두뇌가 알아차리지 못할 정도로 짧기 때문에 모두가 만족할 수 있습니다.

"프로세스 데이터를 변경하지 않는다"는 규칙에 한 가지 예외가 있는데, 바로 패킷 카운터입니다. 모든 패킷은 짝수 두 번(패킷 수에 한 번, 바이트 수에 한 번) 카운트되며, 이 작업은 동시에 수행되어야 합니다. 여기서도 운이 좋게도 커널의 counter(9) 프레임워크가 이 상황을 위해 정확히 설계되어 있습니다. 한 코어가 다른 코어에 영향을 미치거나 속도를 늦추지 않도록 CPU 코어당 총계를 유지합니다. 카운터가 실제로 읽혀질 때만 각 코어에 총계를 물어보고 합산합니다.

Control Interface

각 OpenVPN DCO 플랫폼은 사용자 공간 OpenVPN과 커널 모듈 간의 고유한 통신 방식을 가지고 있습니다.

Linux에서는 netlink를 통해 이 작업을 수행하지만, if_ovpn 작업은 FreeBSD의 netlink 구현이 준비되기 전에 완료되었습니다. 지난번 인과관계 위반으로 아직 집행 유예 중이기 때문에 대신 다른 것을 사용하기로 결정했습니다.

if_ovpn 드라이버는 기존 인터페이스 ioctl 경로를 통해 구성됩니다. 구체적으로는 SIOCSDRVSPEC/SIOCGDRVSPEC 호출입니다.

이러한 호출은 ifdrv 구조체를 커널에 전달합니다. ifd_cmd 필드는 명령을 전달하는 데 사용되며, ifd_data 및 ifd_len 필드는 커널과 사용자 공간 간에 장치별 구조체를 전달하는 데 사용됩니다.

if_ovpn은 구조체가 아닌 직렬화된 nvlist를 전송한다는 점에서 기존 접근 방식에서 다소 벗어납니다. 따라서 인터페이스를 더 쉽게 확장할 수 있습니다. 즉, 기존 사용자 공간 소비자를 손상시키지 않고 인터페이스를 확장할 수 있다는 뜻입니다. 구조체에 새 필드를 추가하면 레이아웃이 변경되어 기존 코드가 크기 불일치로 인해 이를 받아들이지 않거나7 필드가 더 이상 예전의 의미가 아니기 때문에 매우 혼란스러워집니다.

직렬화된 nvlist를 사용하면 다른 쪽을 혼동하지 않고 필드를 추가할 수 있습니다. 알 수 없는 필드는 그냥 무시됩니다. 따라서 새로운 기능을 훨씬 쉽게 추가할 수 있습니다.

Routing Lookups

if_ovpn은 라우팅 결정에 대해 걱정할 필요가 없다고 생각할 수 있습니다. 패킷이 네트워크 드라이버에 도착할 때쯤이면 커널의 네트워크 스택이 이미 라우팅 결정을 내렸을 테니까요. 틀린 생각입니다. 저도 이 사실을 알아내는 데 시간이 좀 걸렸습니다.

문제는 주어진 if_ovpn 인터페이스에 잠재적으로 여러 개의 피어가 있을 수 있다는 것입니다(예: 서버 역할을 하고 있고 여러 클라이언트가 있는 경우). 커널은 문제의 패킷이 이들 중 한 곳으로 가야 한다는 것을 알아냈지만, 커널은 이러한 모든 클라이언트가 단일 브로드캐스트 도메인에 있다는 가정 하에 작동합니다. 즉, 인터페이스에서 전송된 패킷은 모든 클라이언트가 볼 수 있습니다. 여기서는 그렇지 않으므로 if_ovpn은 패킷이 어느 클라이언트로 가야 하는지 알아내야 합니다.

이 작업은 ovpn_route_peer()가 처리합니다. 이 함수는 먼저 피어 목록을 살펴보고 피어의 VPN 주소가 목적지 주소와 일치하는지 확인합니다. (주소 패밀리에 따라 ovpn_find_peer_by_ip() 또는 ovpn_find_peer_by_ip6()에 의해 수행됩니다). 일치하는 피어를 찾으면 이 피어에게 패킷이 전송됩니다. 그렇지 않은 경우 ovpn_route_peer()는 경로 조회를 수행하고 결과 게이트웨이 주소로 피어 조회를 반복합니다.

if_ovpn이 패킷을 전송할 피어를 찾은 경우에만 패킷을 암호화하여 전송할 수 있습니다.

Key Rotation

OpenVPN은 때때로 터널을 보호하는 데 사용되는 키를 변경합니다. 이는 if_ovpn이 사용자 공간에 맡기는 어려운 작업 중 하나이므로 OpenVPN과 if_ovpn 간의 약간의 조정이 필요합니다.

OpenVPN은 OVPN_NEW_KEY 명령을 사용하여 새 키를 설치합니다. 각 키에는 ID가 있으며, 모든 패킷에는 암호화에 사용된 키 ID가 포함됩니다. 즉, 키가 교체되는 동안에도 이전 키와 새 키가 모두 커널에 알려져 있고 활성 상태로 유지되므로 모든 패킷을 해독할 수 있습니다.

새 키가 설치되면 OVPN_SWAP_KEYS 명령을 사용하여 활성화할 수 있습니다. 즉, 새 키는 발신 패킷을 암호화하는 데 사용됩니다.

나중에 OVPN_DEL_KEY 명령을 사용하여 이전 키를 삭제할 수 있습니다.

vnet

네, 브이넷에 대해 이야기해야 할 것 같습니다. 이 글을 쓰다 보니 어쩔 수 없는 일이죠.

제가 너무 게을러서 전부 설명하기는 어렵기 때문에 훨씬 더 뛰어난 저자인 올리비에 코샤르-라베가 쓴 "감옥: 사례로 보는 vnet"8이라는 글을 소개해드리겠습니다.

vnet은 감옥을 자체 IP 스택을 갖춘 가상 머신으로 전환하는 것으로 생각하면 됩니다.

pfSense 사용 사례에 반드시 필요한 것은 아니지만 테스트를 훨씬 더 쉽게 해줍니다. 즉, 외부 도구 없이도 단일 머신에서 테스트할 수 있다는 뜻입니다(OpenVPN 자체는 매우 당연한 이유 때문에 제외).

이 방법이 궁금하신 분들께는 FreeBSD 저널의 다른 기사도 도움이 될 수 있습니다: "자동화된 테스트 프레임워크", 작성자... 잠깐만요, 크리스토프 프로보스트라는 사람을 알 것 같습니다.

Performance

이 모든 과정을 거치고 나면 "그래도 정말 도움이 될까?"라는 의문이 드실 겁니다.

다행히도 제게는 그렇습니다.

넷게이트의 동료 중 한 명이 넷게이트 410010 디바이스에 iperf3을 설치하여 테스트한 결과 다음과 같은 결과를 얻었습니다:

if_tun 207.3 Mbit/s
DCO Software 213.1 Mbit/s
DCO AES-NI 751.2 Mbit/s
DCO QAT 1,064.8 Mbit/s

"if_tun"은 DCO가 없는 기존 OpenVPN 방식입니다. 사용자 공간에서 AES-NI 명령어를 사용했고 'DCO 소프트웨어' 설정은 사용하지 않았다는 점에 주목할 필요가 있습니다. 이러한 노골적인 치팅 시도에도 불구하고 DCO는 여전히 약간 더 빨랐습니다. 공평한 경쟁의 장(즉, DCO가 AES-NI 명령어를 사용하는 경우)에서는 경쟁이 없습니다. DCO가 3배 이상 빠릅니다.

인텔에게도 좋은 소식이 있습니다: 인텔의 QuickAssist 오프로드 엔진은 AES-NI보다 훨씬 빨라서 OpenVPN이 이전보다 5배 더 빨라졌습니다.

Future Work

개선할 수 없을 정도로 좋은 것은 없지만, 어떤 면에서 이번 개선은 DCO 설계의 성공으로 인한 결과입니다. 유선 OpenVPN 프로토콜은 32비트 초기화 벡터(IV)를 사용하며, 여기서는 설명하지 않겠습니다11, 암호화상의 이유로 동일한 키로 IV를 재사용하는 것은 좋지 않은 생각입니다.

즉, 키를 재협상해야 한다는 뜻입니다. OpenVPN의 기본 재협상 간격은 3600초이며, 안전을 위해 30%의 여유를 두면 2^32 * 0.7 / 3600, 즉 초당 약 835.000개의 패킷이 전송됩니다. 이는 "겨우" 8~9Gbit/s입니다(1300바이트 패킷 가정).

DCO를 사용하면 최신 하드웨어로는 이미 어느 정도 도달할 수 있는 수준입니다.

좋은 문제이긴 하지만, 여전히 문제이기 때문에 OpenVPN 개발자들은 64비트 IV를 사용하는 패킷 포맷을 업데이트하기 위해 노력하고 있습니다.

Thanks

if_ovpn 작업은 Rubicon Communications(Netgate로 거래)의 후원으로 pfSense 제품 라인에 사용되었습니다. 22.05 pfSense plus 릴리스12부터 사용되었습니다. 이 작업은 FreeBSD로 업스트림되었으며 최근 14.0 릴리스의 일부입니다. 사용하려면 OpenVPN 2.6.0 이상이 필요합니다.

또한, 초기 FreeBSD 패치가 나왔을 때 매우 환영해 주셨고 도움이 없었다면 이 프로젝트가 이만큼 잘 진행되지 못했을 OpenVPN 개발자들에게도 감사의 말씀을 전하고 싶습니다.

Footnotes:

  1. 아니면 이 글을 읽을 때마다.

  2. 좋아요 글쓰기. 읽고 이봐요, 현학적으로 얘기할 거면 하루 종일 이 얘기를 할 거예요.

  3. DCO에 관심이 없다면 다음 기사를 읽으면 돼요. 아주 좋은 글일 겁니다.

  4. 제가 "저희"라고 말했지만, 이 솔루션에 대한 공로를 인정받고 싶은 만큼 DCO 아키텍처를 고안하고 Windows와 Linux용으로 구현한 것은 OpenVPN 개발자들입니다. 저는 그들이 한 일만 했을 뿐입니다. 다만 FreeBSD용입니다.

  5. OpenVPN에서는 DCO는 OS의 트래픽 쉐이핑(즉, 더미넷)과 결합할 수 있습니다.

  6. https://cgit.freebsd.org/src/tree/sys/net/if_ovpn.c?id=da69782bf06645f38852a8b23af#n490

  7. 구조가 뚱뚱해졌기 때문이라고 말할 수도 있습니다. 그럴 수도 있습니다만, 전 너무 예의가 없습니다.

  8. https://freebsdfoundation.org/wp-content/uploads/2020/03/Jail-vnet-by-Examples.pdf

  9. https://freebsdfoundation.org/wp-content/uploads/2019/05/The-Automated-Testing-Framework.pdf

  10. https://shop.netgate.com/products/4100-base-pfsense

  11. 대부분 제가 직접 이해하지 못하기 때문입니다.

  12. https://www.netgate.com/blog/pfsense-plus-software-version-22.05-now-available



크리스토프 프로보스트는 네트워크 및 비디오 애플리케이션을 전문으로 하는 프리랜서 임베디드 소프트웨어 엔지니어입니다. 그는 FreeBSD 커미터이자 FreeBSD의 pf 방화벽 유지 관리자입니다. 그는 현재 대부분의 시간을 Netgate의 pfSense 작업에 할애하고 있습니다.

크리스토프는 안타깝게도 uClibc 버그에 걸려 넘어지는 경향이 있고, FTP에 대한 증오심이 강합니다. 그에게 IPv6 조각화에 대해 이야기하지 마세요.