주소·서브넷·게이트웨이 · 헤어핀 NAT · 실습
헤어핀을 재현하고 두 가지 방법으로 푼다
목표
집 안 노트북·공유기·서버 세 자리를 한 파드 안에 만들고, 목적지만 바꿨을 때
응답이 돌아오지 못하는 것을 실제로 재현합니다. 그다음 출발지까지 바꿔
통하게 만들고, 같은 문제를 이름 해석 쪽에서 푸는 방법과 비교합니다.
왜 중요한가
"밖에서는 되는데 집 안에서만 안 된다" 는 신고는 원인을 짐작하기가 어렵습니다.
연결이 안 되는 것도 아니고 이름이 안 풀리는 것도 아니며, 응답이 오기는 오는데
받는 쪽이 버리기 때문입니다. 버려지는 일은 로그에 아무것도 남기지 않습니다.
이 실습은 그 버려지는 순간을 눈으로 보게 만듭니다. connect() 한 UDP 소켓은
연결한 상대에서 온 것만 받고 나머지는 커널이 조용히 버리는데, 그것이 노트북이
공유기 주소에서 올 응답을 기다리다가 서버의 사설 주소에서 온 응답을 버리는
상황과 정확히 같습니다.
이 파드에서 되는 것과 안 되는 것
실습 파드에는 커널 권한이 없어 ip netns 도 nft 도 tcpdump 도 동작하지
않습니다. 대신 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.html 에 hairpin-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.internal 에 home.example.test 의 내부 답을 적고, curl --resolve 로 내부 답과 바깥 답을 각각 흉내 내 /root/hairpin/07-split.txt 에 담으세요.
8. 두 해법을 /root/hairpin/08-compare.md 에 비교하세요. ## 헤어핀, ## 스플릿, ## 무엇을 고르나 세 절이 필요합니다.
참고
- 서버는
python3 -m http.server 9000 --bind 127.0.0.20 --directory /root/hairpin/site로 띄웁니다. 배경으로 돌릴 때는setsid nohup … >/tmp/서버로그 2>&1 </dev/null &로 표준 출력을 끊으세요. 물려 주면 채점기가 그 프로세스가 끝날 때까지 기다립니다. - 접속이 실패한 것도 증거입니다.
curl -sS -m 3 … 2>&1로 오류 메시지까지 파일에 담으세요. - 4단계에서 소켓 셋을 쓰는 이유는 이렇습니다. 노트북 소켓은 공유기 주소로
connect()하고, 공유기 소켓은127.0.0.99:9500에, 서버 소켓은127.0.0.20:9500에 묶습니다. 소켓으로는 출발지를 위조할 수 없으니, DNAT 만 걸린 상황에서는 원래 출발지를 본문에 실어 서버에 알려 주고 서버가 그 주소로 직접 답하게 만듭니다. socket.timeout을 잡아result=dropped를 출력하세요. 시간이 지나 실패한 것이지 오류가 난 것이 아닙니다.- 7단계의
curl --resolve 이름:포트:주소는 그 이름을 그 주소로 풀린 것처럼 취급합니다. DNS 를 세우지 않고 스플릿 호라이즌을 흉내 낼 수 있습니다. - 채점기는 여러분의
nat.py를 두 모드로 직접 돌려 봅니다. 결과를 박아 두면 한 모드에서 반드시 떨어집니다.
단계 8개
- 세 자리의 지도를 그린다
- 사설 주소로는 되고 바깥 주소로는 안 된다
- 네 개의 튜플을 추적한다
- 목적지만 바꾸면 응답이 버려진다
- 출발지까지 바꾸면 통한다
- 공유기에 걸 규칙을 쓴다
- 같은 문제를 이름 쪽에서 푼다
- 두 해법을 비교한다