LabHub
배우기 러닝패스 코스

Addresses, Subnets and Gateways

Reproduce the Hairpin and Solve It Two Ways

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

집 안 노트북·공유기·서버 세 자리를 한 파드 안에 만들고, 목적지만 바꿨을 때 응답이 돌아오지 못하는 것을 실제로 재현합니다. 그다음 출발지까지 바꿔 통하게 만들고, 같은 문제를 이름 해석 쪽에서 푸는 방법과 비교합니다.

왜 중요한가

"밖에서는 되는데 집 안에서만 안 된다" 는 신고는 원인을 짐작하기가 어렵습니다. 연결이 안 되는 것도 아니고 이름이 안 풀리는 것도 아니며, 응답이 오기는 오는데 받는 쪽이 버리기 때문입니다. 버려지는 일은 로그에 아무것도 남기지 않습니다.

이 실습은 그 버려지는 순간을 눈으로 보게 만듭니다. connect() 한 UDP 소켓은 연결한 상대에서 온 것만 받고 나머지는 커널이 조용히 버리는데, 그것이 노트북이 공유기 주소에서 올 응답을 기다리다가 서버의 사설 주소에서 온 응답을 버리는 상황과 정확히 같습니다.

이 파드에서 되는 것과 안 되는 것

실습 파드에는 커널 권한이 없어 ip netnsnfttcpdump 도 동작하지 않습니다. 대신 127.0.0.0/8대역 전체가 자기 자신이라, 권한 없이도 127.0.0.10·127.0.0.20·127.0.0.99 를 한 대에서 함께 쓸 수 있습니다. 세 자리를 그 세 주소로 나눠 씁니다. 6단계의 nftables 규칙은 글로만 쓰고 적용하지 않습니다.

단계

  1. 세 자리의 주소와 하는 일을 /root/hairpin/01-map.txt 에 적으세요. laptop 127.0.0.10, router 127.0.0.99, server 127.0.0.20 이고, 주소 셋을 한 대에서 쓸 수 있는 이유도 한 줄 적습니다.
  2. /root/hairpin/site/index.htmlhairpin-lab-server 한 줄을 넣고, 그 디렉터리를 127.0.0.20:9000 으로 내보내세요. 사설 주소로 붙은 결과와 127.0.0.99:9000 으로 붙은 결과를 /root/hairpin/02-direct.txt 에 함께 담습니다.
  3. 목적지만 바꿨을 때(DNAT 만) 패킷의 출발지와 목적지가 어떻게 변하는지를 /root/hairpin/03-tuple.txt 에 다섯 줄로 적으세요. 줄머리는 각각 request-out, after-dnat, reply, expected, mismatch= 입니다.
  4. /root/hairpin/nat.py 를 만드세요. python3 nat.py dnat-only 가 세 소켓으로 그 상황을 재현하고 마지막 줄에 result=dropped 를 출력해야 합니다. 돌린 결과를 /root/hairpin/04-dnat-only.txt 에 저장하세요.
  5. 같은 도구의 python3 nat.py dnat-snat 이 출발지까지 바꿔 result=delivered 를 내게 하고, 결과를 /root/hairpin/05-dnat-snat.txt 에 저장하세요.
  6. 진짜 공유기에 걸 규칙을 /root/hairpin/06-rules.nft 에 쓰세요. 서버는 192.168.0.20, 집 안 대역은 192.168.0.0/24, 포트는 80 입니다. prerouting 과 postrouting 두 체인이 필요하고, 출발지를 바꾸는 규칙은 집 안에서 온 것만 걸려야 합니다.
  7. 같은 문제를 이름 쪽에서 푸세요. /root/hairpin/hosts.internalhome.example.test 의 내부 답을 적고, curl --resolve 로 내부 답과 바깥 답을 각각 흉내 내 /root/hairpin/07-split.txt 에 담으세요.
  8. 두 해법을 /root/hairpin/08-compare.md 에 비교하세요. ## 헤어핀, ## 스플릿, ## 무엇을 고르나 세 절이 필요합니다.

참고

세 자리의 지도를 그린다

127.0.0.0/8 은 첫 옥텟이 127 이면 전부 자기 자신입니다. 그래서 127.0.0.20 에 서버를 묶어도 되고 127.0.0.99 를 공유기 자리로 써도 됩니다. 커널 권한이 필요 없는 것이 이 실습이 파드에서 도는 이유입니다.

한 줄에 이름·주소·하는 일을 적고, 마지막 줄에 주소 셋을 한 대에서 함께 쓸 수 있는 이유를 적으세요.

사설 주소로는 되고 바깥 주소로는 안 된다

먼저 서버가 내보낼 파일을 만듭니다. 채점기가 응답을 알아보려면 hairpin-lab-server 라는 글자가 들어 있어야 합니다.

python3 -m http.server 9000 --bind 127.0.0.20 --directory /root/hairpin/site

그다음 두 번 붙어 봅니다. 127.0.0.20:9000 은 되고 127.0.0.99:9000 은 안 됩니다. 지금은 공유기 자리에 아무것도 없어서 곧바로 거절당하는데, 이것이 '포트포워딩을 안 걸어 둔 공유기' 자리입니다.

실패한 쪽의 메시지도 파일에 담아야 합니다. curl -sS -m 3 http://127.0.0.99:9000/ 2>&1 처럼 표준 오류를 함께 받으세요.

네 개의 튜플을 추적한다

DNAT 은 목적지만 바꿉니다. 그래서 공유기를 지난 뒤에도 출발지는 여전히 노트북이고, 서버는 노트북에게 직접 답합니다.

네 줄을 차례로 적으세요.

마지막 mismatch= 줄에는 replyexpected 가 어느 자리에서 어긋나는지를 적습니다. 포트 번호는 아무 값이나 골라 일관되게 쓰면 됩니다.

목적지만 바꾸면 응답이 버려진다

핵심은 connect() 입니다. UDP 소켓도 connect() 하면 커널에 '누구에게서 올 응답을 기다리는가' 가 등록되고, 다른 주소에서 온 데이터그램은 소켓에 닿지 못한 채 버려집니다.

소켓 셋을 이렇게 놓습니다.

laptop  bind(('127.0.0.10', 0))  connect(('127.0.0.99', 9500))
router  bind(('127.0.0.99', 9500))
server  bind(('127.0.0.20', 9500))

노트북이 보내면 공유기가 받습니다. DNAT 만 걸린 상황이므로 서버가 보는 출발지는 노트북이어야 하는데, 소켓으로는 출발지를 위조할 수 없습니다. 그래서 원래 출발지를 본문에 실어 보내고, 서버가 그 주소로 직접 답하게 만드세요.

마지막에 laptop.recv() 를 2초 제한으로 시도하고, socket.timeout 이 나면 result=dropped 를 출력합니다.

출발지까지 바꾸면 통한다

고칠 것은 하나뿐입니다. 공유기가 요청을 서버로 넘길 때 자기 소켓으로 보내면, 서버가 보는 출발지가 공유기가 되고 응답도 공유기에게 옵니다. 공유기는 그 응답을 노트북에게 되돌려 주면 됩니다.

이때 노트북이 받는 응답의 출발지는 127.0.0.99:9500 이고, 그것이 노트북이 connect() 한 상대와 같으므로 커널이 통과시킵니다.

같은 도구를 모드만 바꿔 돌린다는 것이 중요합니다. 채점기가 두 모드를 모두 돌려 결과가 실제로 갈리는지 봅니다.

공유기에 걸 규칙을 쓴다

이 파드에는 커널 권한이 없어 nft 로 적용할 수 없습니다. 규칙을 정확히 쓰는 것이 목적입니다.

체인 둘이 필요합니다.

chain prerouting { type nat hook prerouting priority dstnat; … dnat to … }
chain postrouting { type nat hook postrouting priority srcnat; … masquerade }

출발지를 바꾸는 규칙에 조건을 빠뜨리지 마세요. 조건 없이 걸면 밖에서 들어온 요청까지 출발지가 공유기로 바뀌어, 서버 로그의 접속 기록이 전부 한 주소로 뭉개집니다. ip saddr 로 집 안 대역에서 온 것만 고르세요.

규칙마다 무엇을 하는 줄인지 # 주석을 답니다.

같은 문제를 이름 쪽에서 푼다

스플릿 호라이즌 DNS 는 같은 이름을 안에서 물으면 사설 주소를, 밖에서 물으면 공인 주소를 답하게 하는 것입니다. 그러면 집 안 기기는 애초에 공유기까지 가지 않습니다.

DNS 서버를 세우지 않고 curl --resolve 로 두 경우를 흉내 냅니다.

curl -sS -m 3 --resolve home.example.test:9000:127.0.0.20 http://home.example.test:9000/
curl -sS -m 3 --resolve home.example.test:9000:127.0.0.99 http://home.example.test:9000/ 2>&1

주소가 아니라 이름으로 접속한다는 것이 핵심입니다. 사설 주소로 직접 붙게 만들면 인증서에 적힌 이름과 접속한 주소가 달라 경고가 뜹니다.

hosts.internal 에는 내부에서 답할 값만 둡니다. 바깥 주소를 함께 적으면 그것은 이미 스플릿이 아닙니다.

두 해법을 비교한다

두 해법은 고치는 자리가 다릅니다. 헤어핀은 공유기 한 대만 손대면 되지만 트래픽이 공유기를 왕복하고, 스플릿 DNS 는 스위치 안에서 끝나지만 관리할 곳이 둘이 됩니다.

비교에 반드시 넣을 것 셋입니다.

절 제목 ## 헤어핀, ## 스플릿, ## 무엇을 고르나 는 채점기가 그대로 찾습니다.