본 콘텐츠는 사용자의 편의를 고려해 자동 기계 번역 서비스를 사용하였습니다. 영어 원문과 다른 오류, 누락 또는 해석상의 미묘한 차이가 포함될 수 있습니다. 필요하시다면 영어 원문을 참조하시기를 바랍니다.
리눅스 네트워킹 스택이 정확히 무엇을 하는지, 왜 그렇게 했는지 알아내는 사람은 누구든 즉시 사라지고, 더 기이하고 설명할 수 없는 일로 대체될것이라는 이론이 있습니다.
Git이 이러한 일이 이미 몇 번이나 반복되었는지 추적하기 위해 만들어졌다는 또 다른 이론이 있습니다.
성능 개선, 효율성 향상 또는 데이터 센터 간에 IP 서브넷을 공유하는 Cloudflare의 방법인 소프트 unicast와 같은 새로운 기능을 제공하기 위한 네트워크 하드웨어 및 소프트웨어의 한계에 도전하지 않고는 Cloudflare의 많은 제품을 만들 수 없습니다. 다행히 대부분의 사람은 운영 체제에서 네트워크와 인터넷 액세스를 일반적으로 처리하는 방법의 복잡한 내용을 알 필요가 없습니다. 대부분의 Cloudflare 직원도 마찬가지입니다.
하지만 때로는 Linux 네트워킹 스택의 설계 의도를 훨씬 넘어서도록 구현하려고 시도하기도 합니다. 다음은 그러한 시도 중 하나에 대한 이야기입니다.
제가 이전에 리눅스 네트워킹 스택을 다룬 블로그 게시물에서 소프트-unicast라는 이상적인 모델과 IP 패킷 포워딩 규칙이라는 기본적인 현실이 부합하지 않는다는 문제를 제기했습니다. 소프트-unicast는 우리가 컴퓨터 간에 IP 주소를 공유하는 방법에 주어진 이름입니다. 이 시스템으로 수행하는 모든 멋진 일을 보실 수 있습니다. 하지만 단일 시스템의 경우에는 IP 주소와 소스 포트 범위의 조합이 수십 개에서 수백 개에 달하며, 발신 연결을 통해 이 중 무엇이든 선택할 수 있습니다.
iptables의 SNAT 대상은 NAT 중에 선택된 포트를 제한하기 위해 source-portrange 옵션을 지원합니다. 이론적으로는 이러한 목적으로 iptables를 계속 사용할 수 있으며, 여러 IP/포트 조합을 지원하려면 별도의 패킷 표시 또는 여러 TUN 장치를 사용할 수 있습니다. 실제 배포 환경에서는 다수의 iptable 규칙 및 가능한 경우 네트워크 장치 관리, 패킷 마크의 다른 사용에 대한 간섭, 기존 IP 범위의 배포 및 재할당 등의 문제를 극복해야 합니다.
우리는 방화벽에 가해지는 워크로드를 늘리는 대신 소프트 unicast 주소 공간에서 IP 패킷을 송신하는 전용 단일 용도 서비스를 마련했습니다. 시간의 흐름 속에서 길을 잃었다는 의미에서 이름을 SLATFATF, 즉 줄여서 "물어"라고 지었습니다. 소프트 unicast 주소 공간을 사용하여 IP 패킷을 프록시하고 해당 주소의 임대를 관리하는 것이 이 서비스의 전적인 책임입니다.
Cloudflare 네트워크에서 소프트 unicast IP 공간을 사용하는 사용자는 WARP만이 아닙니다. 많은 Cloudflare 제품 및 서비스에서 소프트 unicast 기능을 사용하며, 많은 경우 HTTP 연결 및 기타 TCP 기반 프로토콜을 프록시하거나 전달하기 위해 TCP 소켓을 생성하는 시나리오에서 사용합니다. 따라서 Fisher는 오픈 소켓에서 사용하지 않는 주소를 임대해야 하고, Fisher가 임대한 주소로는 소켓을 열 수 없도록 해야 합니다.
첫 번째 시도는 피싱에서 고유한 클라이언트별 주소를 사용하고 Netfilter/conntrack이 SNAT 규칙을 계속 적용하도록 하는 것이었습니다. 하지만 Linux의 소켓 하위 시스템과 Netfilter conntrack 모듈 간의 상호작용이 패킷 다시 쓰기를 통해 스스로를 극명하게 드러낸다는 문제를 저희가 발견했습니다.
소프트-unicast 주소 조각 198.51.100.10:9000-9009가 있다고 가정해 보겠습니다. 그렇다면 198.51.100.10:9000에 있는 TCP 소켓을 203.0.113.1:443에 연결하려는 두 개의 개별 프로세스가 있다고 가정해 보겠습니다. 첫 번째 프로세스는 이 작업을 성공적으로 수행할 수 있지만, 요청된 5-투플과 일치하는 소켓이 이미 있으므로 두 번째 프로세스가 연결을 시도하면 오류가 발생합니다.
소켓을 만드는 대신, 목적지 IP는 동일하지만 고유한 소스 IP를 가진 TUN 장치에서 패킷을 방출하고 소스 NAT를 사용하여 이 범위 내의 주소에 패킷을 다시 쓰면 어떻게 될까요?
소스 주소를 198.51.100.10:9000-9009에 다시 쓰는 nftables "snat" 규칙을 추가하면, Netfilter는 피쉬tun에 표시되는 각각의 새로운 연결에 대해 conntrack 테이블에 항목을 생성하고, 새로운 소스 주소를 원래의 주소에 매핑합니다. 해당 TUN 장치의 더 많은 연결을 동일한 대상 IP로 전달하려고 하면 사용 가능한 10개의 포트가 모두 할당될 때까지 요청된 범위에서 새 소스 포트가 선택됩니다. 이렇게 되면, 기존의 연결이 만료되어 conntrack 테이블의 항목이 해제될 때까지 새로운 연결이 끊어지게 됩니다.
소켓을 바인딩할 때와 달리 Netfilter는 conntrack 테이블에서 첫 번째 여유 공간을 선택하기만 합니다. 그러나 테이블의 가능한 모든 항목을 사용하면 IP 패킷을 작성할 때 EPERM 오류가 발생합니다. 어느 쪽이든, 커널 소켓을 바인딩하든 conntrack으로 패킷을 다시 작성할 때 요구 사항에 일치하는 무료 항목이 없으면 오류가 발생합니다.
이제 두 가지 접근 방식을 결합한다고 가정해 보겠습니다. 첫 번째 프로세스는 TUN 장치에서 IP 패킷을 방출하여 소프트 unicast 포트 범위의 패킷으로 다시 쓰는 것입니다. 그런 다음 두 번째 프로세스에서 해당 IP 패킷과 동일한 주소를 가진 TCP 소켓을 바인딩하고 연결합니다.
첫 번째 문제는 connect() 호출이 이루어진 시점에 198.51.100.10:9000에서 203.0.113.1:443 사이에 활성 연결이 있다는 것을 두 번째 프로세스가 알 수 있는 방법이 없다는 것입니다. 두 번째 문제는 해당 두 번째 프로세스의 관점에서 연결이 성공한다는 것입니다.
두 연결이 동일한 5-투플을 공유할 수 없어야 합니다. 하지만 실제로는 그렇게 하지 않습니다. 대신, TCP 소켓의 소스 주소는 다음 사용 가능한 포트로 자동으로 다시 작성됩니다.
이 동작은 SNAT 또는 MASQUERade 규칙 없이 conntrack을 사용하는 경우에도 존재합니다. 일반적으로 conntrack 항목의 수명이 관련된 소켓의 수명과 일치하지만, 보장되는 것은 아니며, 생성된 IP 패킷의 소스 주소와 일치하는 소켓의 소스 주소를 신뢰할 수 없습니다.
이는 소프트-unicast에게 매우 중요한 상황에서 conntrack이 포트 조각 외부의 소스 포트를 갖고 컴퓨터에 할당된 연결을 다시 쓸 수 있음을 의미합니다. 이렇게 하면 연결이 조용히 끊기므로 불필요한 지연이 발생하고 연결 제한 시간 초과 보고가 허위로 보고됩니다. 다른 솔루션이 필요합니다.
WARP의 경우 우리가 선택한 솔루션은 IP 패킷 다시 쓰기 및 전달을 중지하는 대신 서버 내의 모든 TCP 연결을 종료하고 올바른 소프트 unicast 주소를 사용하여 로컬로 생성된 TCP 소켓으로 프록시 설정하는 것이었습니다. 이것은 간편하고 실행 가능한 솔루션이었으며, CDN으로 향하는 연결이나 Zero Trust 보안 웹 Gateway의 일부로 가로채는 연결 등 일부 연결에 이미 적용해 놓은 솔루션이었습니다. 하지만, 현재 상태에 비해 리소스 사용량이 추가되고 대기 시간이 늘어날 가능성이 있습니다. 우리는 또 다른 전진 방법을 찾고 싶었습니다.
패킷 다시 쓰기와 바인딩된 소켓을 모두 사용하려면 단일 소스를 결정해야 합니다. Netfilter에서는 소켓 하위 시스템을 인식하지 못하지만, 소켓을 사용하고 소프트 unicast도 인식하는 대부분의 코드는 Cloudflare에서 작성하고 제어하는 코드입니다. 따라서 약간 젊은 사람은 Netfilter의 설계에 맞도록 올바르게 작동하도록 코드를 변경하는 것이 합리적이라고 생각했습니다.
첫 번째 시도는 소켓을 만들기 전에 연결 추적 테이블을 검사하고 조작하기 위해 conntrack 모듈에 대한 Netlink 인터페이스를 사용하는 것이었습니다. Netlink는 다양한 Linux 하위 시스템에 대한 확장 가능한 인터페이스 이며 ip 와 같은 많은 명령줄 도구(우리의 경우에는 conntrack-tools)에서 사용됩니다. 바인딩하려는 소켓에 대한 conntrack 항목을 생성함으로써 conntrack이 유효하지 않은 포트 번호에 연결을 다시 쓰지 않도록 하고 매번 성공을 보장합니다. 마찬가지로, 항목 생성에 실패하면 다른 유효한 주소를 사용할 수 있습니다. 이 접근 방식은 소켓을 바인딩하든 IP 패킷을 전달하든 관계없이 작동합니다.
여기에는 한 가지 문제가 있습니다. 그다지 효율적이지 않다는 것입니다. Netlink는 바인드/연결 소켓 댄스에 비해 느리며, conntrack 항목을 생성할 때 흐름에 대한 제한 시간 초과를 지정하고 연결 시도가 실패할 경우 항목을 삭제해야 연결 테이블이 지정된 5-튜플에 대해 너무 빨리 채워지지 않습니다. 즉, 제한된 리소스로 트래픽이 높은 대상을 지원하려면 tcp_tw_reuse 옵션을 수동으로 다시 구현해야 합니다. 또한 표유 RST 패킷으로 인해 연결 추적 항목이 지워질 수 있습니다. 우리 규모로는 일어날 수 있는 이와 같은 일은 모두 일어날 것입니다. 취약한 솔루션을 끼울 곳이 아닙니다.
conntrack 항목을 생성하는 대신 자신의 이익을 위해 커널 기능을 남용할 수 있습니다. 얼마 전 Linux에 TCP_REPAIR 소켓 옵션이 추가되었는데, 이는 표면적으로는 VM을 재배치하는 경우와 같이 서버 간의 연결 마이그레이션을 지원하기 위한 것입니다. 이 기능을 사용하면 새 TCP 소켓을 만들고 전체 연결 상태를 직접 지정할 수 있습니다.
이 기능의 또 다른 용도는 해당 연결을 설정하는 데 필요한 TCP 3방향 핸드셰이크를 수행하지 않는 "연결된" 소켓을 만드는 것입니다. 적어도, 커널은 그렇게 하지 않았습니다. TCP SYN이 포함된 IP 패킷을 전달하는 경우라면, 앞으로 세상에서 예상되는 상태에 대해 더 확실히 알 수 있습니다.
그러나 TCP Fast Open의 도입으로 이를 수행하는 더욱 간단한 방법이 제공됩니다. SYN 패킷이 초기 페이로드와 함께 전송될 때 연결을 즉시 설정하기 위한 유효한 쿠키가 포함되어 있다는 가정 하에 기존의 3방향 핸드셰이크를 수행하지 않는 "연결된" 소켓을 만들 수 있습니다. 하지만, 소켓에 쓸 때까지 아무것도 전송되지 않기 때문에 우리의 필요를 완벽하게 충족합니다.
직접 시도해 볼 수 있습니다.
TCP_FASTOPEN_CONNECT = 30
TCP_FASTOPEN_NO_COOKIE = 34
s = socket(AF_INET, SOCK_STREAM)
s.setsockopt(SOL_TCP, TCP_FASTOPEN_CONNECT, 1)
s.setsockopt(SOL_TCP, TCP_FASTOPEN_NO_COOKIE, 1)
s.bind(('198.51.100.10', 9000))
s.connect(('1.1.1.1', 53))
실제 소켓과 일치하지 않는 "연결된" 소켓을 바인딩하는 데는 한 가지 중요한 기능이 있습니다. 다른 프로세스가 소켓과 동일한 주소에 바인딩하려고 해도 바인딩하지 못한다는 것입니다. 이는 패킷 포워딩과 소켓 사용을 공존하게 하려는 초기에 겪었던 문제를 해결합니다.
이렇게 한 가지 문제가 해결되지만, 다른 문제가 발생합니다. 기본적으로 로컬에서 시작된 패킷과 전달된 패킷 모두에 IP 주소를 사용할 수는 없습니다.
예를 들어 IP 주소 198.51.100.10을 TUN 장치에 할당했습니다. 이렇게 하면 모든 프로그램에서 주소 198.51.100.10:9000을 사용하여 TCP 소켓을 만들 수 있습니다. 또한 주소가 198.51.100.10:9001인 해당 TUN 장치에 패킷을 쓸 수 있으며, Linux가 해당 패킷을 TCP 소켓과 동일한 경로를 따라 게이트웨이로 전달하도록 구성할 수 있습니다. 지금까지는 잘했습니다.
인바운드 경로에서 198.51.100.10:9000으로 주소가 지정된 TCP 패킷이 허용되며 데이터는 TCP 소켓에 삽입됩니다. 198.51.100.10:9001로 주소가 지정된 TCP 패킷, 삭제됩니다. TUN 장치로 전혀 전달되지 않습니다.
왜 이런 일이 발생할까요? 로컬 라우팅은 특별합니다. 패킷이 로컬 주소로 수신된 경우 적용해야 한다고 생각하는 라우팅과 관계없이 "입력"으로 처리되며 전달되지 않습니다. 기본 라우팅 규칙을 살펴보세요.
cbranch@linux:~$ ip rule
cbranch@linux:~$ ip rule
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
우선 순위 규칙이 음이 아닌 정수인 경우 가장 작은 우선 순위 값이 먼저 평가됩니다. 이렇게 하려면 표시된 패킷을 패킷 전달 서비스의 TUN 장치로 리디렉션하는 조회 규칙을 처음에 '삽입'하기 위해 규칙을 약간 조작해야 합니다. 기존 규칙을 삭제한 다음 올바른 순서로 새 규칙을 만들어야 합니다. 그러나 이러한 라우팅 규칙을 조작하는 동안 패킷이 손실된 경우를 대비하여 경로가 없는 라우팅 규칙을 "로컬" 테이블에 대한 라우팅 규칙으로 두고 싶지는 않습니다. 최종 결과는 다음과 같습니다.
ip rule add fwmark 42 table 100 priority 10
ip rule add lookup local priority 11
ip rule del priority 0
ip route add 0.0.0.0/0 proto static dev fishtun table 100
WARP와 마찬가지로, "피시 툰" 인터페이스에서 들어오는 패킷에 표시를 할당하여 연결 관리를 간소화하며, 이를 통해 다시 그곳으로 라우팅할 수 있습니다. 로컬에서 발생한 TCP 소켓에 이와 동일한 표시가 적용되어 있는 것을 방지하기 위해, 저희는 피쉬툰 대신 루프백 인터페이스에 IP를 할당하고, 따라서 피쉬툰에는 주소가 할당되지 않게 합니다. 하지만 이제는 명시적인 라우팅 규칙이 있으므로 필요하지 않습니다.
이 마지막 수정 사항을 테스트하는 동안 안 좋은 문제가 발생했습니다. 우리의 프로덕션 환경에서는 작동하지 않았습니다.
Linux의 네트워킹 스택을 통해 패킷의 경로를 디버깅하는 것은 간단하지 않습니다. nftables에 nftrace를 설정하거나 iptables에 LOG/TRACE 대상을 적용하는 등, 주어진 패킷에 어떤 규칙과 테이블이 적용되는지 이해하는 데 도움이 되는 몇 가지 도구를 사용할 수 있습니다.
Linux 네트워킹 및 *테이블을 통한 패킷 흐름 경로에 대한 개략도Jan Engelhardt 작성
패킷이 사전 라우팅 후크를 통과할 것으로 예상하고 TUN 장치로 패킷을 전송하기 위한 라우팅 결정이 내려지면 패킷은 포워드 테이블을 통과합니다. 테스트 호스트의 IP에서 발생한 패킷을 추적해 본 결과, 패킷이 사전 라우팅 단계에 들어가지만 '라우팅 결정' 블록 후에는 사라지는 것을 확인할 수 있었습니다.
다이어그램에는 "소켓 조회"를 위한 블록이 있지만, 이는 입력 테이블이 처리된 후에 발생합니다. 패킷은 입력 테이블에 절대 입력되지 않습니다. 유일한 변경 사항은 로컬 소켓을 만드는 것입니다. 소켓 생성을 중단하면 패킷은 이전과 같이 포워드 테이블로 전달됩니다.
'라우팅 결정'의 부분에는 프로토콜별 처리가 포함되는 것으로 나타났습니다. IP 패킷의 경우, 라우팅 결정이 캐시될 수 있으며, 몇 가지 기본 주소 유효성 검사가 수행됩니다. 2012년에는Early demux라는 기능이 추가되었습니다. 그 이유는 패킷 처리의 이 시점에서 이미 무언가를 찾고 있고, 수신된 대부분의 패킷이 알 수 없는 패킷이나 어딘가로 전달되어야 하는 패킷이 아니라 로컬 소켓에 대한 패킷일 것으로 예상되기 때문입니다. 이 경우 여기에서 직접 소켓을 조회하고 추가 경로 조회를 저장하지 않겠습니까?
안타깝게도 Cloudflare에서는 소켓을 생성했을 뿐이며 패킷 수신을 원하지 않았습니다. 라우팅 테이블에 대한 조정은 무시됩니다. 소켓을 찾으면 해당 라우팅 조회를 완전히 건너뛰기 때문입니다. 원시 소켓은 라우팅 결정에 관계없이 모든 패킷을 수신하여 이를 방지하지만, 패킷 전송률이 너무 높아 효율적이지 않습니다. 이 문제를 해결하는 유일한 방법은 초기 demux 기능을 비활성화하는 것입니다. 그러나 패치의 주장에 따르면 이 기능은 성능을 향상시킵니다. 이 기능을 비활성화하면 기존 워크로드에서 성능이 얼마나 저하될까요?
간단한 실험이 필요합니다. net.ipv4.tcp_early_demux를 설정하세요. 데이터 센터의 일부 컴퓨터에서 0으로 설정하여 잠시 실행한 다음 기본 설정과 테스트 중인 컴퓨터와 동일한 하드웨어 구성을 사용하는 컴퓨터와 CPU 사용량을 비교합니다.
주요 지표는 /doc/stat의 CPU 사용량입니다. 성능이 저하될 경우 사용자 공간(상단)이나 커널 시간(하단)에는 거의 변화가 없으며 Linux 네트워크 처리가 이루어지는 컨텍스트인 "softirq"에 더 많은 CPU 사용량이 할당될 것으로 예상됩니다. 이러한 차이는 미미하며, 주로 사용량이 적은 시간대에 효율성을 감소시키는 것으로 보입니다.
IP 패킷 포워딩에 대한 다양한 솔루션을 테스트하는 동안에도 네트워크의 TCP 연결은 계속 종료되었습니다. Cloudflare의 초기 우려에도 불구하고, 성능에 미치는 영향은 작았으며, 향상된 원본 연결 가능성에 대한 가시성, 네트워크 내의 빠른 내부 라우팅, 사용된 소프트 unicast 주소를 더 간단하게 관찰할 수 있는 덕분에 사용 여부를 입증하는 부담이 사라졌습니다. IP 전달 및 서로 다른 두 송신 계층을 지원할까요?
지금까지의 대답은 '아니요'입니다. 현재 우리 네트워크에서 피싱을 실행하고 있지만, ICMP 패킷을 처리하는 책임은 훨씬 덜했습니다. 하지만 모든 IP 패킷을 터널링하기로 결정한 경우에는 그 방법을 정확히 알 수 있습니다.
Cloudflare에서 일반적인 엔지니어링 역할은 이상하고 어려운 문제를 대규모로 해결하는 것입니다. 귀하가 최소한의 문서만으로도 새로운 접근 방식을 시도하고 Linux 커널의 기능을 탐색할 목표 지향적 엔지니어라면 채용 중인 직무를 살펴보세요.Cloudflare에서는 귀사의 의견을 듣고 싶습니다!