커스텀 헤더 없이 패킷 출처 표시하기 — DSCP 필드 재활용 사례

쿠버네티스(Kubernetes)에서 서비스를 여러 개 굴리려면 외부에 노출할 IP도 그만큼 있어야 한다. 정석은 간단하다. IP를 사서 통신사와 트랜짓(transit) 계약을 맺으면 된다. 문제는 비용이다. 원저자는 IP 구매와 트랜짓 계약에 드는 연간 비용이 수천만원대라고 설명한다. 개인이나 소규모 팀이 감당하기엔 부담스러운 액수다.
그래서 나온 대안이 통신사가 나눠주는 가정용 모뎀의 DHCP였다. 모뎀은 최대 8개 안팎의 유동 IPv4 주소를 할당해 주는데, 서버 전원이 꺼지지 않는 한 이 주소는 드물게만 바뀐다. 사실상 고정 IP처럼 쓸 수 있다는 뜻이다. 남은 문제는 하나였다. 이 DHCP로 받은 IP를 쿠버네티스 LoadBalancer의 External IP로 인식시키는 것.
IP가 여러 개 필요한데 헤더에는 자리가 없다
기존 선택지부터 훑어봐야 이 문제의 결이 보인다. MetalLB는 온프레미스 쿠버네티스에서 가장 널리 쓰이는 LoadBalancer 구현체지만, DHCP로 받은 유동 IP를 다루는 기능은 없다. dhcp-cni-plugin 같은 대안도 있지만, 이건 쿠버네티스 노드가 아닌 장비에서 IP 하나를 받아 쓰는 용도에 가깝다. 저사양 엣지 라우터 한 대가 여러 개의 유동 IP를 동시에 받아 클러스터로 흘려보내야 하는 이번 상황과는 맞지 않았다.
기존 방법이 맞지 않아 직접 만들게 된 것이 dumbarp라는 오픈소스 프로젝트다. 저사양 엣지 라우터에서 동작하는 LoadBalancer로, 원저자가 자신의 블로그에 설계 과정과 마주친 문제를 공개했다. 이 글은 그 공개 사례를 분석해, 재사용 가능한 패턴 하나를 뽑아내는 데 집중한다.
그 패턴이 왜 필요했는지 이해하려면 먼저 IPv4 헤더의 한계를 짚어야 한다. IPv4 패킷 헤더에는 소스 주소, 목적지 주소, 프로토콜 번호, 체크섬처럼 통신에 필수인 필드만 정해진 자리를 차지한다. "이 패킷이 어느 엣지 라우터를 거쳐 들어왔는지" 같은 애플리케이션 고유 정보를 실어 보낼 여유 공간은 사실상 없다. 원저자가 택한 방법은 기존 DSCP 필드를 빌려 쓰는 것이었다. DSCP는 미사용 영역이 아니라 패킷의 홉별 전달 동작을 지정하는 표준 필드이며, 이 사례에서는 QoS에 쓰지 않던 6비트를 내부 식별자로 임시 재활용했다.
1차 시도와 그 부작용 — 비대칭 라우팅
DHCP IP를 클러스터로 연결한 첫 결과는 절반만 성공이었다. 엣지 라우터 바로 옆에서 접속하면 멀쩡히 열렸다. 그런데 다른 건물이나 해외에서 접속하면 예외 없이 Connection Timed Out이 떴다.
원인은 DSCP 활용 여부와 별개로 이미 라우팅 구조에 있었다. 요청 패킷은 파이프라인을 거쳐 클러스터까지 무사히 도달한다. 문제는 응답이다. 클러스터 입장에서 그 응답의 목적지는 그냥 "기본 경로(default route)"로 나가는 평범한 목적지 IP일 뿐이다. 이 패킷이 원래 어느 엣지 라우터를 거쳐 들어왔는지는 클러스터가 알 방법이 없다. 그래서 응답은 들어온 길을 되짚어 엣지 라우터로 돌아가지 않고, 곧장 인터넷으로 나가버린다.
같은 건물 안에서는 이 문제가 드러나지 않았다. 같은 통신사 스위치, 즉 같은 L2(2계층) 구간을 거치기 때문에 반환 패킷의 실제 출처가 엣지 라우터와 다르더라도 별문제 없이 통과했다. 하지만 외부에서 들어온 요청의 반환 패킷이 엣지 라우터가 아닌 경로로 나가면, 통신사 방화벽이 그 패킷을 그대로 드롭했다. 이런 식으로 나가는 트래픽과 들어오는 트래픽의 경로가 서로 어긋나는 상태를 비대칭 라우팅(asymmetric routing)이라 부른다. 비대칭 라우팅 자체가 항상 비정상인 것은 아니지만, 이 구성에서는 반환 경로가 엣지 라우터를 벗어나면 통신사 방화벽에 막혔다.
1차 해법은 dumbarp-gateway였다. 특정 IP를 소스로 하는 모든 패킷을 담당 엣지 라우터로 보내는 정책 기반 라우팅(policy routing) 테이블을 만들어, 그 IP발 트래픽은 무조건 해당 엣지 라우터를 거치도록 강제했다. 외부 접속은 이걸로 정상화됐다.
그런데 이번엔 반대쪽에서 문제가 터졌다. 서버실 내부에서 접속이 안 됐다. 서버실 코어 네트워크는 클러스터와 직접 피어링(peering)돼 있어서 평소엔 엣지 라우터를 거칠 이유가 전혀 없다. 그런데 gateway는 그 IP를 소스로 하는 패킷이면 출처를 따지지 않고 무조건 엣지 라우터로 떠넘겼다. 서버실에서 나가는 응답까지 엣지 라우터를 억지로 거치게 되면서, 이번엔 내부 접속이 막혔다.
여기서 문제의 진짜 모양이 드러났다. 필요한 건 "이 IP발 패킷은 무조건 엣지 라우터로"가 아니라, "엣지 라우터를 거쳐 들어온 패킷의 응답만 그 엣지 라우터로 돌려보내고, 나머지는 자유롭게 라우팅되게" 하는 조건부 규칙이었다. IP 하나만으로는 이 조건을 표현할 수 없다. 패킷이 어느 경로로 들어왔는지, 그 이력을 어딘가에 남겨야 했다.
쓰지 않던 DSCP 필드 재활용 — 태깅 설계
패킷의 이력을 남길 별도 자리가 없는 제약에서 원저자는 이미 정의된 필드 가운데 이 네트워크에서 사용하지 않던 값을 골랐다. 그 자리는 우선순위 표시 필드(DSCP, Differentiated Services Code Point)였다. RFC 2474는 기존 IPv4 ToS 옥텟의 상위 6비트를 DSCP로 재정의해 패킷의 PHB(per-hop behavior, 홉별 전달 동작)를 지정하도록 했다. 당시 미사용으로 둔 곳은 인접한 하위 2비트 CU(Currently Unused) 영역이었다. 따라서 이 사례가 재활용한 대상은 미사용 비트가 아니라, 이 네트워크에서 QoS에 쓰지 않던 표준 DSCP 6비트다.
공개된 사례에서는 이 6비트를 엣지 라우터의 식별자로 재활용했다. 흐름은 네 단계다.
- 진입 시 덮어쓰기. 패킷이 고객 접속을 처음 받는 엣지 라우터의
dumbarpd에 들어오면, 커널 내 프로그램(eBPF, extended Berkeley Packet Filter)이 DSCP 필드 값을 해당 엣지 라우터의 등록 ID로 덮어쓴다. - 연결 표식 남기기. 서버실의 클러스터 측 라우터들에서 돌아가는
dumbarp-routerd가 eBPF로 커넥션을 추적하고, DSCP 필드의 엣지 라우터 ID를 읽어 패킷이 어느 엣지 라우터에서 왔는지 식별한 뒤 연결 표식(connmark, connection mark)을 찍는다. connmark는 리눅스 커널이 하나의 연결(connection) 전체에 붙여 두는 표식이다. - 정책 라우팅 등록.
dumbarp-routerd는 connmark가 찍힌 패킷이 나갈 때 그 표식에 맞는 엣지 라우터로 빠져나가도록 정책 기반 라우팅 규칙을 등록한다. - 경계에서 원복. 여기가 이 패턴에서 가장 중요한 단계다. 내부용으로 쓴 DSCP 값이 통신사 바깥으로 그대로 새어나가면 안 되므로, 엣지 라우터의
dumbarpd는 패킷이 처음 들어왔을 때의 원래 DSCP 값을 따로 기억해 뒀다가 나가는 패킷에 그대로 복원한다.
이 네 단계를 통과하면 처음 서버실에서 막혔던 문제도 함께 풀린다. 서버실 내부에서 클러스터로 직접 들어온 패킷은 애초에 엣지 라우터를 거치지 않으므로 DSCP에 엣지 라우터 ID가 찍히지 않고, connmark도 남지 않는다. 정책 라우팅 규칙이 적용될 조건 자체가 성립하지 않으니 그 패킷은 평소처럼 자유롭게 라우팅된다. 반대로 특정 엣지 라우터를 거쳐 들어온 패킷은 DSCP에 그 흔적이 남아 있어서, 응답이 나갈 때 정확히 왔던 길로 돌아간다. IP 하나로는 표현할 수 없었던 "경로 이력에 따른 조건부 라우팅"이, 패킷 안에 새로 만든 6비트짜리 꼬리표 덕분에 가능해졌다.
이 글이 승격하려는 것은 eBPF 훅이나 connmark, 정책 라우팅 테이블 같은 구현 세부가 아니라 그 앞에 있는 아이디어 자체다. 위 구현 단계 설명은 원저자의 글과 공개된 dumbarp 저장소를 따른다.
dumbarpd가 DSCP에 자기 ID를 쓰고, 서버실 라우터의 dumbarp-routerd가 그 ID를 읽어 connmark를 남긴다. 이 표식으로 응답 경로를 정한 뒤 통신사 방향으로 나갈 때는 원래 DSCP 값을 복원한다.| 단계 | 외부 요청의 응답 | 서버실 내부 요청의 응답 | 실패 지점 |
|---|---|---|---|
| 정책 라우팅 도입 전 | 기본 경로로 나가 통신사 방화벽에 drop됨 | 정상 (엣지 라우터를 거치지 않고 클러스터와 직접 피어링) | 외부 접속 전체 실패 |
1차 해법 — dumbarp-gateway(IP 기반 무조건 라우팅) |
정상 (엣지 라우터로 복귀) | 엣지 라우터로 잘못 우회돼 실패 | 서버실 내부 접속 실패 |
| 최종 해법 — DSCP 태깅 + connmark(경로 이력 기반 조건부 라우팅) | 정상 (엣지 라우터로 복귀) | 정상 (조건 미충족, 자유 라우팅) | 없음 |
언제 쓸 수 있고 언제 못 쓰는가
이 패턴을 아무 데나 옮겨 쓸 수는 없다. 가장 먼저 확인해야 할 건 DSCP가 해당 네트워크에서 QoS 용도로 실제 사용되는지다. QoS가 실제로 필요한 네트워크에서는 DSCP를 이런 용도로 빼앗아 쓸 수 없다. 관리하는 네트워크 경계 안에서만 그 필드를 임시로 쓰고, 경계를 벗어나는 순간부터는 원래 값으로 돌려줘야 한다.
이 조건을 좀 더 일반화하면 재사용 가능한 판단 기준이 나온다.
- L3 헤더(IP 계층)에 새 필드를 추가할 수 없는 제약된 환경인가 — 커스텀 헤더를 끼워 넣을 수 없어야 이 패턴을 고려할 이유가 생긴다.
- 후보로 삼은 표준 필드가 관리하는 네트워크 경계 안에서는 원래 용도로 전혀 쓰이지 않는가 — 조금이라도 쓰인다면 충돌부터 해결해야 한다.
- 그 필드 값을 경계 밖으로 내보내기 전에 원래 값으로 되돌리는 지점을 반드시 만들 수 있는가 — 이 복원 지점이 없으면 애초에 시도해선 안 된다.
- 해결하려는 문제가 "패킷이 어디서 왔는지 하위 노드가 알아야 하는" 유형인가 — 그렇지 않다면 굳이 이 정도 복잡도를 들일 이유가 없다.
남은 숙제
이 패턴이 깔끔하게 문제를 풀어낸 건 맞지만, 그 대가로 떠안는 제약도 분명하다. DSCP 6비트는 정확히 64개의 값만 표현할 수 있다. 엣지 라우터가 수십, 수백 대로 늘어나는 대규모 환경이라면 이 식별자 공간 자체가 부족해진다.
또 하나는 이 글이 다루지 않은 영역이다. eBPF 훅이 실제로 얼마나 많은 패킷을 처리할 수 있는지, connmark 추적이 커넥션 수가 늘어날 때 어떻게 버티는지, 실제 성능 수치는 원문에도 공개돼 있지 않고 이 글도 검증하지 않았다. 이 글이 다룬 건 딱 하나, "QoS에 쓰지 않던 표준 DSCP 필드를 내부 식별 채널로 재활용하고 경계에서 반드시 원복한다"는 아이디어의 모양이다. 구현이 그 모양을 실제로 얼마나 견고하게 뒷받침하는지는 저장소 코드를 직접 읽고 판단할 몫으로 남겨둔다. 비슷한 제약 환경에서 인프라를 다루는 글은 이 블로그의 프로그래밍 카테고리에도 몇 편 더 있다.
자주 묻는 질문
IPv4 헤더에 새 필드를 그냥 추가하면 안 되나요?
IPv4 헤더에는 소스·목적지 주소, 프로토콜 번호, 체크섬 같은 필수 필드만 정해진 자리를 차지하고 있어서, 애플리케이션 고유 정보를 실어 보낼 여유 공간이 사실상 없다. 원저자는 이 네트워크에서 QoS에 쓰지 않던 표준 DSCP 필드를 내부 식별자로 재활용하는 방식을 택했다.
QoS가 필요한 네트워크에서도 DSCP를 이런 용도로 써도 되나요?
안 된다. 이 패턴은 DSCP를 QoS 용도로 전혀 쓰지 않는 네트워크 경계 안에서만, 그리고 경계 밖으로 나가기 전에 반드시 원래 값을 복원한다는 조건 아래에서만 성립한다.
DSCP 태깅과 connmark, 정책 라우팅은 각각 무슨 역할을 하나요?
패킷이 엣지 라우터의 dumbarpd에 진입할 때 eBPF가 DSCP 필드를 해당 라우터 ID로 덮어쓰고, 서버실 라우터의 dumbarp-routerd가 그 값을 보고 connmark(연결 표식)를 커넥션에 남긴다. 이후 그 connmark가 찍힌 패킷이 나갈 때는 정책 기반 라우팅 규칙에 따라 원래 들어왔던 엣지 라우터로 되돌아간다. 이 세 요소가 함께 있어야 IP 하나로는 표현할 수 없는 경로 이력 기반 라우팅이 가능해진다.
아직 댓글이 없습니다.