OVS를 이용한 SNAT PoC

개요

OVS 2.6에서 NAT 기능을 지원한다. SONA에서 제공하는 NAT기능을 OVS NAT 기능으로 대체할 수 있는 지에 대한 검토 및 PoC를 진행한다.

PoC를 위한 환경 설정

NAT기능은 OVS 2.6에서만 지원하기 때문에 OVS 2.6.0 이상을 설치하여야 하며, 또한 Linux 4.6.0 이상 버젼을 필요로 한다.

1. Linux Kernel 4.6.0 이상으로 커널을 업그레이드 한다. 본 PoC에서는 4.6.0 을 이용하였다.

$wget kernel.ubuntu.com/~kernel-ppa/mainline/v4.6-yakkety/linux-headers-4.6.0-040600_4.6.0-040600.201606100558_all.deb
$wget kernel.ubuntu.com/~kernel-ppa/mainline/v4.6-yakkety/linux-headers-4.6.0-040600-generic_4.6.0-040600.201606100558_amd64.deb
$wget kernel.ubuntu.com/~kernel-ppa/mainline/v4.6-yakkety/linux-image-4.6.0-040600-generic_4.6.0-040600.201606100558_amd64.deb
$sudo dpkg -i linux-headers-4.6.0*.deb linux-image-4.6.0*.deb
$sudo reboot -h now

 

2. OVS 2.6 설치

본 PoC에서는 OVS 2.6.1을 사용하였으며,  Linux Kernel 4.6 업그레이드 후에는 굳이 커널 모듈 컴파일이 필요 없다.

$ wget http://openvswitch.org/releases/openvswitch-2.6.1.tar.gz
$ tar xzvf openvswitch-2.6.1.tar.gz
$ cd openvswitch-2.6.1/
$ ./boot.sh
$ ./configure
$ make
$ sudo make install
$ sudo mkdir -p /var/run/openvswitch/
$ sudo modprobe openvswitch
$ sudo rm -rf /usr/local/etc/openvswitch/conf.db
$ sudo ovsdb-tool create /usr/local/etc/openvswitch/conf.db vswitchd/vswitch.ovsschema
$ sudo ovsdb-server --remote=punix:/usr/local/var/run/openvswitch/db.sock \
                     --remote=db:Open_vSwitch,Open_vSwitch,manager_options \
                     --private-key=db:Open_vSwitch,SSL,private_key \
                     --certificate=db:Open_vSwitch,SSL,certificate \
                     --bootstrap-ca-cert=db:Open_vSwitch,SSL,ca_cert \
                     --pidfile --detach
$ sudo ovs-vsctl --no-wait init
$ sudo ovs-vswitchd --pidfile --detach

위와 같이 커널 컴파일을 시도해서 compile error가 나면 “./configure” 만 수행하여 커널 모듈 컴파일 과정을 스킵한다.

3. 설치 확인

버젼 확인

$ sudo ovs-vsctl --version
ovs-vsctl (Open vSwitch) 2.6.1
DB Schema 7.14.0

커널 모듈 확인

$ lsmod | grep openv
openvswitch           110592  4
nf_nat_ipv6            16384  1 openvswitch
nf_nat_ipv4            16384  2 openvswitch,iptable_nat
nf_defrag_ipv6         36864  2 openvswitch,nf_conntrack_ipv6
nf_nat                 24576  4 openvswitch,nf_nat_ipv4,nf_nat_ipv6,nf_nat_masquerade_ipv4
nf_conntrack          106496  9 openvswitch,nf_nat,nf_nat_ipv4,nf_nat_ipv6,xt_conntrack,nf_nat_masquerade_ipv4,nf_conntrack_netlink,nf_conntrack_ipv4,nf_conntrack_ipv6
libcrc32c              16384  2 xfs,openvswitch

설치 직후에는 openvswitch의 Reference Count (Used) 값이 0 이다. bridge가 생성이 되면 그때 값이 증가한다.

 

bridge 생성 후 (또는 openstack-node-init command로 bridge 생성 후) connection tracking 기능 동작 확인

$ sudo ovs-appctl dpctl/dump-conntrack
tcp,orig=(src=10.0.0.163,dst=10.0.0.162,sport=60074,dport=6633),reply=(src=10.0.0.162,dst=10.0.0.163,sport=6633,dport=60074),protoinfo=(state=TIME_WAIT)
tcp,orig=(src=10.0.0.163,dst=10.0.0.162,sport=56470,dport=9876),reply=(src=10.0.0.162,dst=10.0.0.163,sport=9876,dport=56470),protoinfo=(state=ESTABLISHED)
tcp,orig=(src=10.0.0.163,dst=10.0.0.162,sport=60008,dport=6633),reply=(src=10.0.0.162,dst=10.0.0.163,sport=6633,dport=60008),protoinfo=(state=TIME_WAIT)
tcp,orig=(src=10.0.0.163,dst=10.0.0.162,sport=60082,dport=6633),reply=(src=10.0.0.162,dst=10.0.0.163,sport=6633,dport=60082),protoinfo=(state=TIME_WAIT)
tcp,orig=(src=10.0.0.163,dst=10.0.0.162,sport=60092,dport=6633),reply=(src=10.0.0.162,dst=10.0.0.163,sport=6633,dport=60092),protoinfo=(state=CLOSING)

혹시 dpctl/dump-conntrack 과정에서 에러가 나면 리부팅 해서 테스트 해본다. 그러나 가끔 리부팅 후에 ovs 자체가 안 뜨는 경우도 있다. ^^;;

bridge를 생성하기 전에 위 명령어를 실행하면 아래와 같은 에러가 발생한다.

$ sudo ovs-appctl dpctl/dump-conntrack

ovs-vswitchd: no datapaths exist

ovs-appctl: ovs-vswitchd: server returned an error

 

OVS NAT 기능 테스트

1. Bridge 및 포트 생성

$ sudo ovs-vsctl add-port br-int port1 -- set interface port1 type=internal
$ sudo ip link set port1 up
$ sudo route add -net 192.168.10.0/24 dev port1

credit: 위 스크립트는 문현선 매니저님이 손수 테스트하여 제공하였습니다. 문현선 매니저님, 감사합니다. ^^

output port를 위해 포트 하나를 더 생성한다.

$ sudo ovs-vsctl add-port br-int port2 -- set interface port2 type=internal
$ sudo ip link set port2 up

 

2. flow 생성을 위한 포트 번호 확인

가장 확실한 방법은 controller를 붙여서 확인하는 방법이다. 아래는 ONOS에 연결하여 ONOS “ports” command 결과 화면임.

id=of:00004e7963a26a4d, available=false, local-status=disconnected just now, role=MASTER, type=SWITCH, mfr=Nicira, Inc., hw=Open vSwitch, sw=2.6.1, serial=None, driver=ovs, channelId=10.0.0.163:60622, managementAddress=10.0.0.163, protocol=OF_13
  port=LOCAL, state=disabled, type=copper, speed=0 , portName=br-int, portMac=4e:79:63:a2:6a:4d
  port=5, state=disabled, type=copper, speed=0 , portName=port1, portMac=8e:35:19:93:9e:8d
  port=6, state=disabled, type=copper, speed=0 , portName=port2, portMac=92:82:2b:6a:d3:cb

아래와 같이 ovs-ofctl 명령어를 사용해도 포트 번호가 나오네요.. (문매니저님 발견!! ^^)

$ sudo ovs-ofctl show br-int
OFPT_FEATURES_REPLY (xid=0x2): dpid:00004e7963a26a4d
n_tables:254, n_buffers:256
capabilities: FLOW_STATS TABLE_STATS PORT_STATS QUEUE_STATS ARP_MATCH_IP
actions: output enqueue set_vlan_vid set_vlan_pcp strip_vlan mod_dl_src mod_dl_dst mod_nw_src mod_nw_dst mod_nw_tos mod_tp_src mod_tp_dst
 10(ea5b4caee2ed4_l): addr:da:aa:02:65:ba:57
     config:     0
     state:      0
     current:    10GB-FD COPPER
     speed: 10000 Mbps now, 0 Mbps max
 LOCAL(br-int): addr:4e:79:63:a2:6a:4d
     config:     0
     state:      0
     speed: 0 Mbps now, 0 Mbps max
OFPT_GET_CONFIG_REPLY (xid=0x4): frags=normal miss_send_len=0
ovs-appctl 명령어를 이용하면 아래와 같이 포트 번호가 다르게 나오므로 주의!!

$ sudo ovs-appctl dpctl/show
system@ovs-system:
 lookups: hit:9759 missed:574 lost:0
 flows: 0
 masks: hit:9779 total:0 hit/pkt:0.95
 port 0: ovs-system (internal)
 port 1: br-int (internal)
 port 2: port1 (internal)
 port 3: port2 (internal)

 

3. Flow rule 생성 및 추가

아래 Flow 룰을 vi 등을 이용하여 텍트트 파일로 생성한다. 포트 번호는 실제 포트 번호로 변경하길…

table=0,ip,in_port=5 actions=ct(commit,zone=1,nat(src=192.168.10.103-192.168.10.115,random)),output:6
table=0,in_port=6,ct_state=-trk,ip,action=ct(table=0,zone=1,nat)
table=0,in_port=6,ct_state=+trk,ip,action=5

아래와 같이 룰을 추가한다. nat-test.flow는 파일 명이다.

$sudo ovs-ofctl add-flows br-int nat-test.flow -O openflow13
$sudo ovs-ofctl dump-flows br-int -O openflow13
OFPST_FLOW reply (OF1.3) (xid=0x2):
 cookie=0x0, duration=3027.688s, table=0, n_packets=0, n_bytes=0, ip,in_port=5 actions=ct(commit,zone=1,nat(src=192.168.10.103-192.168.10.115,random)),output:6
 cookie=0x0, duration=3027.686s, table=0, n_packets=0, n_bytes=0, ct_state=-trk,ip,in_port=6 actions=ct(table=0,zone=1,nat)
 cookie=0x0, duration=3027.686s, table=0, n_packets=0, n_bytes=0, ct_state=+trk,ip,in_port=6 actions=output:5

port 5로 들어온 IP 패킷은 zone 1의 connection table을 거치게 되고 source IP 주소가 설정된 주소 중 하나로 랜덤하게 변경이 되어 port 6 번으로 보내어진다.

port 6 으로 들어온 IP 패킷 중 connection tracking 이 되지 않은 flow는 zone 1의 nat 설정을 통해 IP 주소가 변경이 되고, 다시 table 0 으로 보내어진다.

이 패킷은 이미 tracking이 되고 있기 때문에 2 번째 룰은 통과를 하고, 세번 째 룰을 통해 port 5로 보내어진다. 물론 이 때 주소는 nat 룰에 의해 원래 source IP로 원복이 된 상태이다.

zone에 대한 추가는 옵셔널이라고 되어 있지만, 실제로 zone을 설정하지 않으면 nat가 되지 않는다.

 

4. Test (Forwarding Test Only)

$sudo arp -s 192.168.10.2 00:0c:29:c0:94:bf

IP 패킷 생성을 위해  미리  ARP 정보를 넣어 둔다. 그렇지 않으면 ARP 만 열심히 하다 끝난다.. ^^;;

$ping 192.168.10.2

물론 가상 주소이기때문에 응답은 없다. 그러나…

$ sudo ovs-ofctl dump-flows br-int -O openflow13
OFPST_FLOW reply (OF1.3) (xid=0x2):
 cookie=0x0, duration=3027.688s, table=0, n_packets=13, n_bytes=1274, ip,in_port=5 actions=ct(commit,zone=1,nat(src=192.168.10.103-192.168.10.115,random)),output:6
 cookie=0x0, duration=3027.686s, table=0, n_packets=0, n_bytes=0, ct_state=-trk,ip,in_port=6 actions=ct(table=0,zone=1,nat)
 cookie=0x0, duration=3027.686s, table=0, n_packets=0, n_bytes=0, ct_state=+trk,ip,in_port=6 actions=output:5

이렇게 Flow rule에는 걸린다. 자 그럼 nat가 되어서 보내어지는 지 확인해보자

$ sudo ovs-appctl dpctl/dump-conntrack  | grep 192
tcp,orig=(src=10.0.0.163,dst=10.0.0.162,sport=56192,dport=9876),reply=(src=10.0.0.162,dst=10.0.0.163,sport=9876,dport=56192),protoinfo=(state=ESTABLISHED)
icmp,orig=(src=10.0.0.163,dst=192.168.10.2,id=2637,type=8,code=0),reply=(src=192.168.10.2,dst=192.168.10.113,id=0,type=0,code=0),zone=1
icmp,orig=(src=10.0.0.163,dst=192.168.10.2,id=2637,type=8,code=0),reply=(src=192.168.10.2,dst=10.0.0.163,id=2637,type=0,code=0)

위 두번째 줄 정보 처러 nat 정보가 들어 있는 것을 확인할 수 있다. 이 경우는 113번이 선택이 되었다. 실제로 랜덤이지만.. 항상 113이 선택이 된다. ^^;;;

자 그럼. 실제로 패킷이 바뀌었을까?

$ sudo tcpdump -i any icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
15:18:24.720686 IP onos02 > 192.168.10.2: ICMP echo request, id 2750, seq 2, length 64
15:18:24.720718 IP 192.168.10.113 > 192.168.10.2: ICMP echo request, id 0, seq 2, length 64
15:18:25.728610 IP onos02 > 192.168.10.2: ICMP echo request, id 2750, seq 3, length 64
15:18:25.728623 IP 192.168.10.113 > 192.168.10.2: ICMP echo request, id 0, seq 3, length 64
15:18:26.736648 IP onos02 > 192.168.10.2: ICMP echo request, id 2750, seq 4, length 64
15:18:26.736670 IP 192.168.10.113 > 192.168.10.2: ICMP echo request, id 0, seq 4, length 64
15:18:27.744655 IP onos02 > 192.168.10.2: ICMP echo request, id 2750, seq 5, length 64
15:18:27.744686 IP 192.168.10.113 > 192.168.10.2: ICMP echo request, id 0, seq 5, length 64

tcpdump 를 떠 보면 실제로 source IP가 113으로 변경되어 port 6에서 캡춰되는 것을 확인할 수 있다.

위와 같이 wireshark에서도 확인 가능…

일단 여기 까지 보면 동작은 하는 것 같다.. Reverse Traffic의 동작 확인은 다음에..

 

5. Full Test (Forward & Reverse)

docker 로 2 개의 container를 생성하고 이 컨테이너의 인터페이스를 OVS bridge에 연결하고 Ping Test를 NAT를 거쳐서 테스트한다.

docker container 생성

아래와 같이 container 2개를 2개의 shell을 통해서 각각 생성한다. 이 때 static arp 설정을 위해서 –privileged 옵션을 주어서 띄운다. 추가로 ovs bridge 연결을 위해서 이름을 설정하는 게 좋다.

$docker run -it --name cont1 --net=none --privileged busybox
$docker run -it --name cont2 --net=none --privileged busybox

 

OVS bridge에 container 연결

아래와 같이 ovs-docker command를 이용하여  두 컨테이너를 OVS br-int 에 연결한다. br-int bridge가 생성되어 있지 않다면 생성해야 한다.

$sudo ovs-docker add-port br-int eth0 cont1 --ipaddress=192.168.0.5/16
$sudo ovs-docker add-port br-int eth0 cont2 --ipaddress=192.168.0.7/16

위와 같이 네트워크 설정을 하고 나면 생성한 컨테이너에 eth0이 해당 IP 주소로 생성이 되고 route도 설정이 된다.

/ # ifconfig
eth0      Link encap:Ethernet  HWaddr E6:7D:20:E1:4F:54
          inet addr:192.168.0.5  Bcast:0.0.0.0  Mask:255.255.0.0
          inet6 addr: fe80::e47d:20ff:fee1:4f54/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:95 errors:0 dropped:0 overruns:0 frame:0
          TX packets:100 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:9006 (8.7 KiB)  TX bytes:6976 (6.8 KiB)
 
lo        Link encap:Local Loopback 
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:40 errors:0 dropped:0 overruns:0 frame:0
          TX packets:40 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1
          RX bytes:4480 (4.3 KiB)  TX bytes:4480 (4.3 KiB)
 
/ # ip route
192.168.0.0/16 dev eth0  src 192.168.0.5

포트가 생성이 되면 아래와 같이 ovs-vsctl command를 이용하여도 생성된 포트를 확인할 수 있다.

$ sudo ovs-vsctl show
db540baf-8198-41cb-9476-b01b5b4fe9c3
    Bridge br-int
        Controller "tcp:10.0.0.162:6633"
        Port "24677ba350844_l"
            Interface "24677ba350844_l"
        Port "5f72485051d44_l"
            Interface "5f72485051d44_l"
        Port br-int
            Interface br-int
                type: internal

Flow rule 생성 및 추가

위 PoC에서와 같이 간단한 룰을 생성한다. 포트 번호는 실제 컨테이너와 연결된 포트 번호를 바꾸어야 하고, ARP를 위해서 첫번째 flow rule을 추가하였다.

table=0,arp, actions=flood
table=0,ip,in_port=7 actions=ct(commit,zone=1,nat(src=192.168.0.103-192.168.0.115,random)),output:9
table=0,in_port=9,ct_state=-trk,ip,action=ct(table=0,zone=1,nat)
table=0,in_port=9,ct_state=+trk,ip,action=7

위에서 설명한 바와 같이 위 내용을 이용하여 file 하나를 생성하고 ovs-ofctl add-flows 명령을 가지고 flow rule을 추가 한다. 기존의 rule이 있다면 삭제하는 게 좋다.

$ sudo ovs-ofctl add-flows br-int nat-test.flow
$ sudo ovs-ofctl dump-flows br-int
NXST_FLOW reply (xid=0x4):
 cookie=0x0, duration=3832.702s, table=0, n_packets=0, n_bytes=0, idle_age=3758, arp actions=FLOOD
 cookie=0x0, duration=3832.700s, table=0, n_packets=0, n_bytes=0, idle_age=3720, ip,in_port=7 actions=ct(commit,zone=1,nat(src=192.168.0.103-192.168.0.115,random)),output:9
 cookie=0x0, duration=3832.700s, table=0, n_packets=0, n_bytes=0, idle_age=3720, ct_state=-trk,ip,in_port=9 actions=ct(table=0,zone=1,nat)
 cookie=0x0, duration=3832.700s, table=0, n_packets=0, n_bytes=0, idle_age=3720, ct_state=+trk,ip,in_port=9 actions=output:7

 

Ping Tet

NAT action으로 연결된 포트에 연결된 Container에서 똔 다른 Container로 Ping을 수행한다. 위 예에서는 port 7에 연결된 container에서 port 9 에 연결된 container로 ping test를 수행한다.

/ # ping 192.168.0.7

이 때 응답은 오지 않는다. 그 이유는 NAT rule을 거쳐서 source IP 주소가 변경이 되어서 destination container에 전달이 되기 때문이다. 이 때 destination container에서 응답을 보내려고 하나 NATing 된 주소를 가진 container는 실제로 존재를 하지 않아 ARP resolution이 되지 않아서 응답이 전송이 되지 안는다.

이를 통해 OVS는 NAT만 수행할 뿐 NAT된 IP 주소에 대한 ARP는 수행하지 않음을 알 수 있다. 뭐.. 당연한 사실 일 수 있지만.. ^^;;

아무튼 Reverse direction 에 대한 PoC를 하기 위해서 아래와 같이 ICMP Request를 받는 컨테이너쪽에서 ARP를 static으로 설정을 해준다. 이 때 NAT IP 주소는 ovs-appctl 명령어를 사용해서 알아내야 하며, MAC 주소는 실제 ICMP Request를 전송한 container의 eth0 에 대한 MAC 주소를 설정해야 한다.

sona@onos02:~$ sudo ovs-appctl dpctl/dump-conntrack | grep 192
icmp,orig=(src=192.168.0.5,dst=192.168.0.7,id=8448,type=8,code=0),reply=(src=192.168.0.7,dst=192.168.0.106,id=0,type=0,code=0),zone=1
/ # arp -s 192.168.0.106 DE:38:1F:6A:57:F7
/ # arp -an
? (192.168.0.106) at de:38:1f:6a:57:f7 [ether] PERM on eth0
? (192.168.0.5) at de:38:1f:6a:57:f7 [ether]  on eth0

위의 ARP의 static 설정을 위해서 container를 privileged mode로 띄울 필요가 있다. 그렇지 않으면 root이지만 “Operation not permitted” 에러가 발생한다.

 

이제 다시 ping을 하면 응답이 오는 것을 확인할 수 있다.

/ # ping 192.168.0.7
PING 192.168.0.7 (192.168.0.7): 56 data bytes
64 bytes from 192.168.0.7: seq=0 ttl=64 time=0.925 ms
64 bytes from 192.168.0.7: seq=1 ttl=64 time=0.083 ms
64 bytes from 192.168.0.7: seq=2 ttl=64 time=0.103 ms

그리고 NAT의 확인을 위해서 Wireshark를 통해 traffic을 확인해보면 아래와 같이 ICMP Request의 source IP가 192.168.0.5 -> 192.168.0.106 으로 바뀌고, ICMP Response의 Destination IP가 192.168.0.106에서 192.168.0.5 로 다시 바뀌는 것을 확인할 수 있다.

References

https://mail.openvswitch.org/pipermail/ovs-dev/2015-December/306707.html

http://openvswitch.org/support/dist-docs/ovs-ofctl.8.html

https://developer.ibm.com/recipes/tutorials/using-ovs-bridge-for-docker-networking/