Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

Network address translation

컴퓨터 네트워크에서 NAT는 IP 패킷 헤더의 IP 주소를 변경하는 일련의 프로세스다.

인터넷은 공개망(public network)로 모든 정보가 공개되는 것을 원칙으로 한다.. "모든게 공개된다!!" 멋진 말이긴 하지만 외부로 공개하기 껄끄러운 정보도 있기 마련이다. 예컨데, 기업에서 만들어지는 정보의 상당수는 외부로의 공개를 금지한다.

private망은 private망 구성용으로 남겨둔 ip 주소영역을 사용합니다. 각 클래스 별로 private 망 구성용 주소를 남겨두고 있다. Private 망을 위한 IP는 RFC1918에 정의돼 있다.

RFC1918 이름 IP 주소 범위 주소 갯수 CIDR (subnet mask) host id 크기
24-bit 블럭 10.0.0.0 ~ 10.255.255.255 16,777,216 10.0.0,0/8 (255.0.0.0) 24bits
20-bit 블럭 172.16.0.0 ~ 172.31.255.255 1,048,576 172.16.0.0/12 (255.240.0.0) 20bits
16-bit 블럭 192.168.0.0 ~ 192.168.255.255 65,536 192.168.0.0/16 (255.250.0.0) 16bits
회사 개발팀이 사용할 private망을 192.168.100.0/24로 구축을 하면 인터넷에서 격리할 수 있을 거다.

개발망의 IP는 외부에서 식별 할 수 없으므로 안전하게 격리 운용할 수 있다. 하지만 문제가 있다. 외부에서 격리될 뿐만 아니라 내부에서 외부로 나갈 수 없기 때문이다. 인터넷을 사용 할 수 없게 되는 거다. 이 문제를 해결해 보자.

개발망이 인터넷으로 부터 완전히 격리되지만, 인터넷으로의 접점인 게이트웨이(gateway)는 Public IP를 가지고 있다. 그렇다면, 인터넷으로 나가는 패킷의 Source IP를 게이트웨이의 Public IP 바꿔준다면, 이 패킷은 인터넷에서 수신/송신이 가능 할거다. 패킷을 받은 쪽은 Source IP를 Destination IP로 해서 응답을 보낼 건데, 이 IP는 게이트웨이의 IP이니 게이트웨이 까지 무사히 도착할 것이다. 물론 게이트웨이에서는 IP를 변환 정보를 추적하고 있다가, 이 패킷의 destination IP를 내부 아이피로 변환하는 작업을 거쳐야 할거다.

이렇게 주소를 변환해서 네트워크간 통신을 가능하게 하는 기술을 NAT(Network address translation)이라고 한다.

  1. 게이트웨이(이 경우 switch)에 패킷이 도착하면 source ip주소를 201.12.23.44로 바꿔서 인터넷으로 보낸다.
  2. 이 주소는 인터넷에 알려진 주소이므로 인터넷 데이터 통신이 가능하다.
  3. switch는 NAT를 적용한 패킷의 정보를 유지한다. 그래서 NAT된 패킷이 들어오면, 이를 확인해서 수신 패킷의 destination address를 192.168.100.5로 바꾼 다음 내부망으로 보낸다.

SNAT 과 DNAT

위 예에서는 Source IP 주소를 변경했다. 이것을 SNAT(Source NAT) 혹은 IP 매스커레이딩이라고 한다.

이제 회사에 있는 개발자들은 SNAT를 이용해서 자유롭게 인터넷에서 자료를 찾을 수 있게 됐다. 그런데, 해결 해야 하는 또 다른 문제가 생겼다. 외부에서 활동하는 영업맨들을 위해서 내부에 있는 파일 서버를 외부에서 열람할 수 있도록 구성을 하라는 명령이 떨어졌다. 외부에서 내부로 들어오려면 DNAT를 사용해야 한다. DNAT은 아래에서 자세히 다루도록 하겠다.

리눅스의 iptables를 이용해서 SNAT를 구성할 수 있다. SNAT를 위해서 virtualbox를 이용해서 테스트 환경을 구축했다.

  • Linux Box : virtualbox를 실행할 리눅스 박스다. iptables를 조작해서 SNAT를 수행한다.
  • Test VM : 테스트를 위해서 사용하는 VM이다.
Linux Box의 routing 테이블이다.. 가상 인터페이스인 vboxnet0을 확인할 수 있다.. 이 인터페이스를 이용해서 test vm과 통신을 한다.
# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
172.30.1.0      0.0.0.0         255.255.255.0   U     2      0        0 wlan0
192.168.56.0    0.0.0.0         255.255.255.0   U     0      0        0 vboxnet0
0.0.0.0         172.30.1.254    0.0.0.0         UG    0      0        0 wlan0
인터넷으로 나가는 인터페이스는 wlan0이다.

Test VM의 routing 테이블 이다.
# route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
192.168.56.0    *               255.255.255.0   U     0      0        0 eth0
default         192.168.56.1    0.0.0.0         UG    100    0        0 eth0

마스커레이딩을 위해서는 FORWARD 를 Accept로 해야 한다. 아래 룰을 추가하자.
# iptables -A FORWARD -i wlan0 -j ACCEPT
# iptables -A FORWARD -o wlan0 -j ACCEPT
현재 Test VM에서 192.168.100.1로의 통신은 문제가 없다. 하지만 인터넷 통신이 불가능하다. 인터넷 통신이 가능하도록 Linux Box에 SNAT 설정을 했다.
# iptables -t nat -A POSTROUTING -s 192.168.56.0/24 -o wlan0 -j SNAT --to 172.30.1.3
출발지 주소가 192.168.56.0/24인 패킷에 대해서 nat룰을 걸었다. 192.168.56.0/24에서 출발한 패킷은 리눅스 박스의 IP인 172.30.1.3으로 변경한다. 이제 이 패킷은 wlan0을 타고 바깥으로 나가게 될 거다.

nat룰을 적용한 후 리눅스 커널의 ip_forward를 1로 변경해 줘야 한다. 그래야지만 패킷을 외부로 내보낸다.
# echo 1 > /proc/sys/net/ipv4/ip_forward
sysctl을 이용해서 값을 변경할 수 있습니다.
# sysctl -w net.ipv4.ip_forward=1
이 정보는 휘발성으로 부팅시 날아가 버린다. sysctl.conf에 추가해서 설정 값을 유지한다.
# cat /etc/sysctl.conf
....
net.ipv4.ip_forward=1
....

nat룰이 잘 적용 됐는지 확인해 보자.
# iptables -t nat -L
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         
SNAT       all  --  192.168.56.0/24      anywhere            to:172.30.1.3 

test vm (192.168.56.101)에서 인터넷이 잘 되는지 확인했다.
# ping 8.8.8.8
64 bytes from 8.8.8.8: icmp_req=1 ttl=48 time=206 ms
64 bytes from 8.8.8.8: icmp_req=2 ttl=48 time=204 ms

Linux Box에서 tcpdump로 icmp 패킷을 확인해봤다.
# tcpdump icmp -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on wlan0, link-type EN10MB (Ethernet), capture size 65535 bytes
23:30:35.869225 IP 172.30.1.3 > 8.8.8.8: ICMP echo request, id 1562, seq 1, length 64
23:30:36.072678 IP 8.8.8.8 > 172.30.1.3: ICMP echo reply, id 1562, seq 1, length 64
23:30:36.870535 IP 172.30.1.3 > 8.8.8.8: ICMP echo request, id 1562, seq 2, length 64
23:30:37.074904 IP 8.8.8.8 > 172.30.1.3: ICMP echo reply, id 1562, seq 2, length 64
출발지와 목적지의 주소가 192.168.56.101이 아닌 172.30.1.3으로 변경된걸 확인할 수 있다. test vm(192.168.56.101)에서 icmp 패킷 확인했다.
# tcpdump icmp -n
23:36:16.883660 IP 192.168.56.101 > 8.8.8.8: ICMP echo request, id 1746, seq 1, length 64
23:36:17.086226 IP 8.8.8.8 > 192.168.56.101: ICMP echo reply, id 1746, seq 1, length 64
주소가 다시 192.168.56.101로 바뀐 것을 확인할 수 있다.

Masquerade

기본적으로 masquerade와 snat는 같은일을 한다. 유일한 차이점은 snat는 변경할 source ip를 직접 명시하는데, masquerade는 명시하지 않는다는 점이다. masquerade룰을 설정할 경우 알아서 NIC의 인터넷 주소를 할당한다.
# iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

rp_filter

rp_filter는 reverse path filter 설정을 위한 커널 옵션이다. 기본 값은 1인데, 이 경우 운영체제는 패킷의 출발지 주소가 라우팅 테이블에 등록되있는지를 검사한다. 만약 등록되있지 않다면 패킷을 드롭해 버립니다. NAT 장비의 경우 네트워크 구성에 따라서 private NIC의 rp_filter 옵션을 꺼야 한다.
# echo 0 > /proc/sys/net/ipv4/conf/eth*/rp_filter

DNAT

DNAT는 SNAT의 반대로, 목적지 IP 주소를 변경해서 내부로 접근할 수 있도록 패킷을 수정한다.

DNAT를 이용한 Load balancing

DNAT의 가장 대표적인 사용용도는 서비스 Load balancing이다. 사설 네트워크인 192.168.0.2, 192.168.0.3 두 개에 웹 서비스를 구축을 했다고 가정해 보자. 우리가 원하는 것은 두 개의 내부 웹 서버로 부하를 분산하는 거다.

DNAT를 이용하면 패킷을 내부로 보낼 수가 있으므로, 웹 요청이 올 경우 192.168.0.2와 192.168.0.3 양쪽으로 보내도록 제어할 수 있다. DNAT 테스트를 위해서 다음과 같은 테스트 환경을 만들었습니다.

Vituralbox를 이용해서 NAT 환경을 만들고, 2 개의 VM에 Apache 웹 서버를 설치했다. DNAT으로 Load balnacing를 제대로 구현한다면, HTTP 요청이 Web server 1과 2에 적당히 분배돼야 할 겁니다.

아래와 같이 DNAT 설정을 만들었습니다.
# iptables -t nat -A PREROUTING -i wlan0 -p tcp --dport 80 -m state \
--state NEW -m statistic --mode nth --every 2 --packet 0 -j DNAT --to 192.168.56.102
# iptables -t nat -A PREROUTING -i wlan0 -p tcp --dport 80 -m state \
--state NEW -m statistic --mode nth --every 1 --packet 0 -j DNAT --to 192.168.56.101
SNAT와는 달리 PREROUTING룰을 추가했습니다. 패킷의 IP 주소는 라우팅 되기 전에 nat 룰이 걸려야 하기 때문이다. wlan0 즉 인터넷으로 부터 들어오는 패킷 중 목적지 포트가 80인 포트에 대해서 룰을 적용했다.

-m 옵션을 이용해서 모듈을 로딩할 수 있다. 먼저 state 모듈을 로딩했는데, 이 모듈을 이용하면 패킷의 연결 상태에 따라서 조건을 심을 수 있다. ESTABLISHED, 'NEW를 줄 수 있다. NEW는 새로 연결을 맺는 것을 의미한다.

다음 statistic 모듈을 로딩했다. 이 모듈은 통계에 기반해서 패킷에 조건을 주기 위해서 사용한다. random과 nth가 있는데, nth를 사용하기로 했다. 첫번째 연결은 101, 두번째 연결은 102로 보내라 이런 의미다.

테스트 장비에서 172.30.1.3으로 http 요청을 보냈습니다. 패킷이 어떻게 분배됐는지 iptables로 확인했다.
# iptables -t nat -L -v
Chain PREROUTING (policy ACCEPT 293 packets, 52270 bytes)
 pkts bytes target     prot opt in     out   source     destination   
    6   312 DNAT       tcp  --  wlan0  any   anywhere   anywhere     
                    tcp dpt:www state NEW statistic mode nth every 2 packet 1 to:192.168.56.102 
    3   156 DNAT       tcp  --  wlan0  any   anywhere   anywhere      
                    tcp dpt:www state NEW statistic mode nth every 2 to:192.168.56.101 

성능 측정

위 환경에서 iperf를 돌렸다. 성능을 측정하는게 목적이었으나, 개인 노트북에서 테스트한 거라서, 기능이 작동한다 정도만 확인할 수 있었다. 개인적으로 실제 환경에서의 성능을 측정해 보고 싶었다. 그래서 haproxy와 함께 비교 테스트를 해보기로 했다.

haproxy
  • 테스트 대역폭 : 1GBits/sec
  • 로드 밸런싱 웹 서버 : apache 서버 4대
  • haproxy : 약 360 Mbits/sec
  • iptables DNAT : 약 750 Mbits/sec
iptables가 2배 가량 성능이 좋다. CPU 점유율도 haproxy는 90%이상을 소모했는데, iptable는 대략 5%를 소비했다.

문제 해결 및 튜닝

  • /proc/sys/net/ipv4/netfilter/ip_conntrack_count
현재 추적하고 있는 연결의 갯수
  • /proc/net/ip_conntrack
추적중인 연결의 상세 정보를 확인할 수 있습니다.
  • /proc/sys/net/ipv4/netfilter/ip_conntrack_max
자원의 한계가 있개 때문에, 연결을 무한정 추적할 수는 없습니다. 추적가능한 연결의 최대 갯수가 정의돼 있습니다. 만약 이 최대 갯수를 추가해서 연결이 이뤄지면 패킷이 드랍됩니다. 아주 바쁜 웹 서버를 DNAT 기반으로 로드밸런싱 할 경우 ip_conntrack_max를 초과해서 패킷이 드랍될 수 있습니다. 이때는 ip_conntrack_max의 값을 변경하면 됩니다.
# cat /proc/sys/net/ipv4/netfilter/ip_conntrack_max  // 현재 값
65536
# echo 131072 > /proc/sys/net/ipv4/netfilter/ip_conntrack_max
  • /sys/module/nf_conntrack_ipv4/parameters/hashsize
연결 정보는 빠른 검색을 위해서 해쉬 테이블에 저장이 됩니다. 해쉬 테이블의 크기를 조정하는 것으로 성능을 튜닝할 수 있습니다. 바쁜 웹서버라면 해쉬 테이블을 크게 해서 선형 검색에 드는 시간을 줄일 수 있겠습니다. 기본 값은 16384 입니다. 만약 백만개 정도의 연결을 관리한다면, 하나의 해쉬 레코드는 1048576/16384 = 64개의 정보를 가지게 될건데, 선형 검색하기에는 지나치게 큰 수 입니다. 32768로 하면 32가 되니 좀더 효율적으로 작동할 겁니다.
# echo 32768 > /sys/module/nf_conntrack_ipv4/parameters/hashsize
해쉬 테이블을 크게 하면 메모리도 그만큼 더 소비하게 되겠죠. conntrack 하나의 크기는 228 byte이니 해쉬 테이블 크기에 따른 사용 메모리를 계산하실 수 있을 겁니다.

이제 tcp timeout에 대한 시간을 튜닝하면 됩니다. timeout 시간이 길면, 오랫동안 해쉬 테이블에 남아 있으니 성능이 떨어질 수 밖에 없습니다. 해쉬 테이블이 가득 차서 패킷이 드랍될 수도 있습니다. 그러니 가능한 해쉬 테이블을 빠르게 비워주는게 좋겠죠. 물론 그렇다고해서 timeout을 너무 짧게 하면 서비스에 문제가 생길 수 있으니 적당히 조절해 줘야 겠습니다. 관련 파일들은 /proc/sys/net/ipv4/netfilter/ip_conntrack_tcp_timeout_* 입니다.

리눅스의 기본 값은 굉장히 큽니다. 가능한 연결을 오래 보존하겠다는 얘기죠.
net.ipv4.ip_conntrack_max = 65496
net.ipv4.netfilter.ip_conntrack_generic_timeout = 600
net.ipv4.netfilter.ip_conntrack_icmp_timeout = 30
net.ipv4.netfilter.ip_conntrack_udp_timeout_stream = 180
net.ipv4.netfilter.ip_conntrack_udp_timeout = 30
net.ipv4.netfilter.ip_conntrack_tcp_timeout_close = 10
net.ipv4.netfilter.ip_conntrack_tcp_timeout_time_wait = 120
net.ipv4.netfilter.ip_conntrack_tcp_timeout_last_ack = 30
net.ipv4.netfilter.ip_conntrack_tcp_timeout_close_wait = 259200
net.ipv4.netfilter.ip_conntrack_tcp_timeout_fin_wait = 120
net.ipv4.netfilter.ip_conntrack_tcp_timeout_established = 432000
net.ipv4.netfilter.ip_conntrack_tcp_timeout_syn_recv = 60
net.ipv4.netfilter.ip_conntrack_tcp_timeout_syn_sent = 120
net.ipv4.netfilter.ip_conntrack_max = 65496

이걸 웹 서비스에 맞게 조절을 했습니다. 웹은 연결을 유지하는 서비스가 아니니 timeout을 짧게 가져가도 괜찮을 겁니다. 값은 서비스 종류와 접속량등에 따라서 달라질 수 있으니, 적당히 튜닝해서 사용하시면 되겠습니다.
net.ipv4.netfilter.ip_conntrack_tcp_timeout_close = 10
net.ipv4.netfilter.ip_conntrack_tcp_timeout_time_wait = 20
net.ipv4.netfilter.ip_conntrack_tcp_timeout_last_ack = 20
net.ipv4.netfilter.ip_conntrack_tcp_timeout_close_wait = 20
net.ipv4.netfilter.ip_conntrack_tcp_timeout_fin_wait = 20
net.ipv4.netfilter.ip_conntrack_tcp_timeout_established = 30
net.ipv4.netfilter.ip_conntrack_tcp_timeout_syn_recv = 20 
net.ipv4.netfilter.ip_conntrack_tcp_timeout_syn_sent = 20 

NAT 설정 저장

Ubuntu 리눅스에서 NAT 설정을 저장하는 방법이다.
# iptables-save > /etc/iptables.rules 
iptable-save명령을 실행하면, 현재 iptables 설정정보를 표준출력 한다.

# cat /etc/network/interfaces
pre-up iptables-restore < /etc/iptables.rules

stateless NAT

Stateless NAT은 dumb NAT 라고 부르기도 한다. NAT의 가장 간단한 형태로 단지 SNAT은 Source IP 주소만 변환하고, DNAT은 Destination IP반 변환해서 넘긴다. conntrack를 유지 하지 않으므로 stateful NAT 보다 빠르게 작동할 것이라고 생각된다. 반면 1:1 static NAT만 사용할 수 있다.