1. 實驗背景
如下是網絡拓撲圖:
我們使用GRETAP Tunnel 互聯 A&B;
GRE 是encapsulate為 IP packets;
GRETAP encapsulate為 Ethernet frames,請留意 Linux GRETAP可與Cisco EoGRE建立互聯。
2. 使用iproute2創建GRETAP
# 請先確認已安裝iproute,若未安裝,可先安裝iproute:
yum install -y iproute
# 創建、啟用、查看gretap interface
routerA# ip link add gretap1 type gretap local 192.168.200.1 remote 172.16.0.1 dev eth1 routerA# ip link set gretap1 up routerA# ip link show gretap1
6: gretap@eth1: <BROADCAST,MULTICAST> mtu 1462 qdisc noop state DOWN mode DEFAULT qlen 1000 link/ether 62:24:67:45:44:ad brd ff:ff:ff:ff:ff:ff
目的是:創建gretap interface到tunnel 兩側的bridge,可以讓siteA與siteB 是橋接在一起。然后使用IPsec去加密siteA&B之間的GRE traffic 。
3. MTU問題
如上述創建的GRETAP,MTU為1462。計算方式:
1500 (物理接口MTU) - 20 (GRE新增的外部IP包頭) - 4 (GRE header) - 14 (封裝的ethernet header) = 1462.
對於IP packet的傳輸,當傳輸packet大於MTU時,經過的路由設備允許切片重傳就保障數據繼續傳輸(當然重傳會會成倍增加網絡傳輸延遲)。對於Ethernet Network 且 gretap 屬於橋接接口,MTU必須一致才能通信,否則會被丟棄。下面,我們來驗證一下:
# 在routerA 添加gretap interface到bridge br0:
routerA# ip link add br0 type bridge routerA# ip link set eth0 down routerA# ip addr del 10.32.x.x/24 dev eth0 # 移除eth0的IP。此處留意如果是AWS EC2,務必小心;如果是阿里或騰訊Cloud Server,錯誤也可用控制台管理。建議在本地環境試驗。 routerA# ip link set eth0 master br0 routerA# ip link set eth0 up routerA# ip link set br0 up routerA# ip addr add 10.0.0.254/24 dev br0 routerA# ip link set gretap1 up routerA# ip link set gretap1 master br0
# 在routerB 添加gretap interface到bridge br0:routerB
routerB# ip link add br0 type bridge routerB# ip link set eth0 down routerB# ip addr del 10.32.x.x/24 dev eth0 # 移除eth0的IP。此處留意如果是AWS EC2,務必小心;如果是阿里或騰訊Cloud Server,錯誤也可用控制台管理。建議在本地環境試驗。 routerB# ip link set eth0 master br0 routerB# ip link set eth0 up routerB# ip link set br0 up routerB# ip addr add 10.0.0.253/24 dev br0 routerB# ip link set gretap1 up routerB# ip link set gretap1 master br0
# 結果:
hostA# ping -s 172 10.32.0.111 PING 10.32.0.111 (10.32.0.111) 1472(1500) bytes of data. ^C --- 10.32.0.111 ping statistics --- 3 packets transmitted, 0 received, 100% packet loss, time 2015ms
我們-s發起max size frame,從hostA 無法ping hostB
通過抓包分析,我們能看到 小型的ARP request/reply frames 是能正常通過tunnel。然而,ICMP request packets是在routerA的br0就被靜默丟棄,因為routerA 的br0 MTU依然是1462。
# 如果我們將br0強制調整為1500,讓HostA能夠通過1500的包到HostB:
routerA&B的gretap1都需要執行,此處僅展示routerA側br0:
routerA# ip link show br0 4: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1462 qdisc noqueue state UP mode DEFAULT link/ether 00:16:3e:c3:8c:ef brd ff:ff:ff:ff:ff:ff routerA# ip link set gretap1 mtu 1500 routerA# ip link show br0 4: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT link/ether 00:16:3e:c3:8c:ef brd ff:ff:ff:ff:ff:ff
如果強制將gretap1的MTU從1462增加38到1500,則封裝之后,routerA&B的eth1會相應增加到1538. 需要與網絡運營商確認siteA和siteB之間的線路連接,如果是雲服務器,需要調整雲服務器MTU。
3. IP fragmentation
理想情況下,Frame是不可分割時(DF[Don't Fragment]為0),Ethernet frames也能通信的。.
iproute2文檔說,tunnel interface有一個選項nopmtudisc (用途是:disable Path MTU Discovery on this tunnel.)
在此,我們重新創建tunnel interface(同時在routerA&B執行):
routerA# ip link del gretap1 routerA# ip link add gretap1 type gretap local 192.168.200.1 remote 172.16.0.1 dev eth1 nopmtudisc routerA# ip link set gretap1 mtu 1500 routerA# ip link set gretap1 up routerA# ip link set gretap1 master br0
可是如果我們現在從hostA ping (-s 1472) hostB,卻無法ping通,為什么?
抓包顯示,當hostA 發出的包,DF已設置為1,經過GRE時包的DF也為1(即使我們設置了nopmtud),這樣就出現了一個不可拆分的包在被靜默丟棄。
所以,我們在HostA發起ping時,如果顯式的明確disable DF(可以拆分包),則才可以ping通。
ping -Mdont -s 1472 10.32.0.111 PING 10.32.0.111 (10.32.0.111) 1472(1500) bytes of data. 1480 bytes from 10.32.0.111: icmp_req=1 ttl=64 time=1.26 ms 1480 bytes from 10.32.0.111: icmp_req=2 ttl=64 time=0.932 ms 1480 bytes from 10.32.0.111: icmp_req=3 ttl=64 time=1.01 ms ^C --- 10.32.0.111 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2002ms rtt min/avg/max/mdev = 0.932/1.071/1.264/0.143 ms
此時,在routerA&B抓包都顯示包被拆分。(拆包通不是最好的體驗,但總比不能ping通要好)。因為Linux操作系統默認都是執行PMUTD的,所以這里我們人為地去拆分包,僅能在實驗環境求真,不能滿足所有應用自由通信的結果。如果需要Linux默認不支持PMUTD,可以設置kernel:
hostA# echo "net.ipv4.ip_no_pmtu_disc = 1 " >> /etc/sysctl.conf hostA# sysctl -p
# 這里分享gretap encapsulation 圖
首先有一個original ethernet frame最終能達到1514 bytes (1500 IP + 14 ethernet header):
當這個fram被GRE封裝后,得到:
4. NFQUEUE: Clearing DF
我們希望嘗試標准的工具來解決這個問題,但是是有些難度的:
a. 創建iptables 規則,去match 那些 准備用NFQUEUE處理的 包
b. 然后一個user-space程序,就可以接收、處理這些包,並裁決verdict這些包:[1] 發回iptables(按規則修改這些包) [2]丟棄這些包。 這種通訊原理為 nfnetlink , 這個庫主要使用C,但也有使用Perl和Python。 netlink之netfilter Queue詳情Click Here.
# 首先,我們需要iptables rule匹配tunneled packet
我們可以通過 iptables allows tracing 方法,來記錄packet 經過了哪些tables和chains。有這些信息,才能方便地制定相應的iptables rule。
對於outgoing packet ,TRACE結果:
1 TRACE: raw:PREROUTING:policy:2 IN=br0 OUT= PHYSIN=eth0 MAC=00:16:3e:52:ba:6c:00:16:3e:93:08:ca:08:00 SRC=10.32.0.50 DST=10.32.0.111 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=819 SEQ=1 2 TRACE: filter:FORWARD:policy:1 IN=br0 OUT=br0 PHYSIN=eth0 PHYSOUT=gretap MAC=00:16:3e:52:ba:6c:00:16:3e:93:08:ca:08:00 SRC=10.32.0.50 DST=10.32.0.111 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ICMP TYPE=8 CODE=0 ID=819 SEQ=1 3 TRACE: raw:OUTPUT:rule:1 IN= OUT=eth1 SRC=192.168.200.1 DST=172.16.0.1 LEN=122 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=47 4 TRACE: raw:OUTPUT:policy:2 IN= OUT=eth1 SRC=192.168.200.1 DST=172.16.0.1 LEN=122 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=47 5 TRACE: filter:OUTPUT:policy:2 IN= OUT=eth1 SRC=192.168.200.1 DST=172.16.0.1 LEN=122 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=47
對於reply return packet,TRACE結果:
6 TRACE: raw:PREROUTING:policy:2 IN=eth1 OUT= MAC=00:16:3e:c3:8c:12:00:16:3e:11:b8:8e:08:00 SRC=172.16.0.1 DST=192.168.200.1 LEN=122 TOS=0x00 PREC=0x00 TTL=63 ID=40301 PROTO=47 7 TRACE: filter:INPUT:policy:1 IN=eth1 OUT= MAC=00:16:3e:c3:8c:12:00:16:3e:11:b8:8e:08:00 SRC=172.16.0.1 DST=192.168.200.1 LEN=122 TOS=0x00 PREC=0x00 TTL=63 ID=40301 PROTO=47 8 TRACE: raw:PREROUTING:rule:1 IN=br0 OUT= PHYSIN=gretap MAC=00:16:3e:93:08:ca:00:16:3e:52:ba:6c:08:00 SRC=10.32.0.111 DST=10.32.0.50 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18065 PROTO=ICMP TYPE=0 CODE=0 ID=819 SEQ=1 9 TRACE: raw:PREROUTING:policy:2 IN=br0 OUT= PHYSIN=gretap MAC=00:16:3e:93:08:ca:00:16:3e:52:ba:6c:08:00 SRC=10.32.0.111 DST=10.32.0.50 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18065 PROTO=ICMP TYPE=0 CODE=0 ID=819 SEQ=1 10 TRACE: filter:FORWARD:policy:1 IN=br0 OUT=br0 PHYSIN=gretap PHYSOUT=eth0 MAC=00:16:3e:93:08:ca:00:16:3e:52:ba:6c:08:00 SRC=10.32.0.111 DST=10.32.0.50 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18065 PROTO=ICMP TYPE=0 CODE=0 ID=819 SEQ=1
請留意:由於涉及bridge,上面的TRACE實際結果需要依靠是否 /proc/sys/net/bridge/bridge-nf-call-iptables 設置為0或1。很多系統默認bridge-nf-call-iptables為1,則會呈現上述TRACE結果;如果為0,則包經過bridge的TRACE記錄將不會被顯示,也就是上述的步驟1/2/8/9/10步驟不會存在。
那么,我們得到TRACE記錄后,我們究竟tap into哪個flow去獲得packets?顯然,我們想看到GRE包,我們只關注從local LAN 到tunnel 的traffic(也就是上述TRACE的步驟 3、4、5)。所以,我們這里選擇OUTPUT chain,至於raw或filter table,都是OK的。這里,我們選擇filter table在routerA&B 操作:
routerA# iptables -A OUTPUT -s 192.168.200.1 -d 172.16.0.1 -p gre -j NFQUEUE --queue-bypass routerB# iptables -A OUTPUT -s 172.16.0.1 -d 192.168.200.1 -p gre -j NFQUEUE --queue-bypass
這樣就能匹配所有發送traffic 去到NFQUEUE (默認序號queue number為0)。同時,如果程序監聽該隊列(--queue-bypass),則移動到next policy或rule,所以至少可以通過small packet。
# 其次,Clearing DF
我們需要編寫代碼去:
a. 接收來自 queue number 0 的packet
b. 執行clearing DF
c. 發送處理后的包回到iptables
幸運的,有一些 案例C代碼 可提供我們一些參考。現在,我們可以編寫如下代碼:

/*************************************************************************************** * clear_df.c: clear, uh, DF bit from IPv4 packets. Heavily borrowed from * * http://netfilter.org/projects/libnetfilter_queue/doxygen/nfqnl__test_8c_source.html * ***************************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <netinet/in.h> #include <linux/types.h> #include <linux/netfilter.h> /* for NF_ACCEPT */ #include <arpa/inet.h> #include <time.h> #include <libnetfilter_queue/libnetfilter_queue.h> /* Standard IPv4 header checksum calculation, as per RFC 791 */ u_int16_t ipv4_header_checksum(char *hdr, size_t hdrlen) { unsigned long sum = 0; const u_int16_t *bbp; int count = 0; bbp = (u_int16_t *)hdr; while (hdrlen > 1) { /* the checksum field itself should be considered to be 0 (ie, excluded) when calculating the checksum */ if (count != 10) { sum += *bbp; } bbp++; hdrlen -= 2; count += 2; } /* in case hdrlen was an odd number, there will be one byte left to sum */ if (hdrlen > 0) { sum += *(unsigned char *)bbp; } while (sum >> 16) { sum = (sum & 0xffff) + (sum >> 16); } return (~sum); } /* callback function; this is called for every matched packet. */ static int cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct nfq_data *nfa, void *data) { u_int32_t queue_id; struct nfqnl_msg_packet_hdr *ph; int pkt_len; char *buf; size_t hdr_len; /* determine the id of the packet in the queue */ ph = nfq_get_msg_packet_hdr(nfa); if (ph) { queue_id = ntohl(ph->packet_id); } else { return -1; } /* try to get at the actual packet */ pkt_len = nfq_get_payload(nfa, &buf); if (pkt_len >= 0) { hdr_len = ((buf[0] & 0x0f) * 4); /* clear DF bit */ buf[6] &= 0xbf; /* set new packet ID */ *((u_int16_t *)(buf + 4)) = htons((rand() % 65535) + 1); /* recalculate checksum */ *((u_int16_t *)(buf + 10)) = ipv4_header_checksum(buf, hdr_len); } /* "accept" the mangled packet */ return nfq_set_verdict(qh, queue_id, NF_ACCEPT, pkt_len, buf); } int main(int argc, char **argv) { struct nfq_handle *h; struct nfq_q_handle *qh; int fd; int rv; char buf[4096] __attribute__ ((aligned)); /* printf("opening library handle\n"); */ h = nfq_open(); if (!h) { fprintf(stderr, "error during nfq_open()\n"); exit(1); } /* printf("unbinding existing nf_queue handler for AF_INET (if any)\n"); */ if (nfq_unbind_pf(h, AF_INET) < 0) { fprintf(stderr, "error during nfq_unbind_pf()\n"); exit(1); } /* printf("binding nfnetlink_queue as nf_queue handler for AF_INET\n"); */ if (nfq_bind_pf(h, AF_INET) < 0) { fprintf(stderr, "error during nfq_bind_pf()\n"); exit(1); } /* printf("binding this socket to queue '0'\n"); */ qh = nfq_create_queue(h, 0, &cb, NULL); if (!qh) { fprintf(stderr, "error during nfq_create_queue()\n"); exit(1); } /* printf("setting copy_packet mode\n"); */ if (nfq_set_mode(qh, NFQNL_COPY_PACKET, 0xffff) < 0) { fprintf(stderr, "can't set packet_copy mode\n"); exit(1); } fd = nfq_fd(h); /* initialize random number generator */ srand(time(NULL)); while ((rv = recv(fd, buf, sizeof(buf), 0)) && rv >= 0) { nfq_handle_packet(h, buf, rv); } /* printf("unbinding from queue 0\n"); */ nfq_destroy_queue(qh); /* printf("closing library handle\n"); */ nfq_close(h); exit(0); }
在說明前,我們可以回顧一下 IPv4 Header Format 圖:
請留意,如果我們clear DF,則我們需要填充"Indentification" field。當DF設置為1時,Indentification field一般是0,因為這種情況(DF=1)下,包不會被分割,所以不需要Indentification 去標識fragmentation碎片包。而當我們設置DF=0或者說clear DF時,則需要設置Indentification 。此處,我們可以生成一個1-65535的隨機數,去填入Indentification field。
編譯需要提供header files:
gcc -o clear_df -lnfnetlink -lnetfilter_queue clear_df.c
# 最后,我們驗證clear df效果
routerA# clear_df routerB# clear_df hostA# ping -s 1472 -Mdo 10.32.0.111 ##此處我們強制DF,看看效果 PING 10.32.0.111 (10.32.0.111) 1472(1500) bytes of data. 1480 bytes from 10.32.0.111: icmp_req=1 ttl=64 time=1.48 ms 1480 bytes from 10.32.0.111: icmp_req=2 ttl=64 time=0.969 ms 1480 bytes from 10.32.0.111: icmp_req=3 ttl=64 time=1.11 ms 1480 bytes from 10.32.0.111: icmp_req=4 ttl=64 time=0.946 ms 1480 bytes from 10.32.0.111: icmp_req=5 ttl=64 time=0.944 ms ^C --- 10.32.0.111 ping statistics --- 5 packets transmitted, 5 received, 0% packet loss, time 4005ms rtt min/avg/max/mdev = 0.944/1.091/1.481/0.208 ms
5. IPsec
回到正軌,IPsec是我們的最終目的。為了使實驗更精彩,對於IPsec的實現,我們讓routerA使用 " ipsec-tools + racoon",routerB使用 openswan 實現。
此時,在第一個IP header之后,IPsec transport mode將插入一個新的ESP header,以至於Protocol field會由47(GRE)更改為50(ESP),在ESP應用后(當然是,這是是說在未DF前的結構),我們得到如下:
我們再次追蹤新結構的packet到user-space,提前做一下TRACE:
1 TRACE: raw:OUTPUT:policy:2 IN= OUT=eth1 SRC=192.168.200.1 DST=172.16.0.1 LEN=122 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=47 2 TRACE: filter:OUTPUT:rule:2 IN= OUT=eth1 SRC=192.168.200.1 DST=172.16.0.1 LEN=122 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=47 3 TRACE: raw:OUTPUT:rule:1 IN= OUT=eth1 SRC=192.168.200.1 DST=172.16.0.1 LEN=152 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ESP SPI=0xdb28966a 4 TRACE: raw:OUTPUT:policy:2 IN= OUT=eth1 SRC=192.168.200.1 DST=172.16.0.1 LEN=152 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ESP SPI=0xdb28966a 5 TRACE: filter:OUTPUT:policy:3 IN= OUT=eth1 SRC=192.168.200.1 DST=172.16.0.1 LEN=152 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=ESP SPI=0xdb28966a 6 TRACE: raw:PREROUTING:policy:2 IN=eth1 OUT= MAC=00:16:3e:c3:8c:12:00:16:3e:11:b8:8e:08:00 SRC=172.16.0.1 DST=192.168.200.1 LEN=152 TOS=0x00 PREC=0x00 TTL=63 ID=21840 PROTO=ESP SPI=0x4802e17 7 TRACE: filter:INPUT:policy:1 IN=eth1 OUT= MAC=00:16:3e:c3:8c:12:00:16:3e:11:b8:8e:08:00 SRC=172.16.0.1 DST=192.168.200.1 LEN=152 TOS=0x00 PREC=0x00 TTL=63 ID=21840 PROTO=ESP SPI=0x4802e17 8 TRACE: raw:PREROUTING:rule:1 IN=eth1 OUT= MAC=00:16:3e:c3:8c:12:00:16:3e:11:b8:8e:08:00 SRC=172.16.0.1 DST=192.168.200.1 LEN=122 TOS=0x00 PREC=0x00 TTL=63 ID=21840 PROTO=47 9 TRACE: raw:PREROUTING:policy:2 IN=eth1 OUT= MAC=00:16:3e:c3:8c:12:00:16:3e:11:b8:8e:08:00 SRC=172.16.0.1 DST=192.168.200.1 LEN=122 TOS=0x00 PREC=0x00 TTL=63 ID=21840 PROTO=47 10 TRACE: filter:INPUT:policy:1 IN=eth1 OUT= MAC=00:16:3e:c3:8c:12:00:16:3e:11:b8:8e:08:00 SRC=172.16.0.1 DST=192.168.200.1 LEN=122 TOS=0x00 PREC=0x00 TTL=63 ID=21840 PROTO=47
Linux 沒有真正的 IPsec virtual interface,但我們看到IPsec 穿過2次chain:
第1次: before encryption (with PROTO=47, lines 1-2)
第2次: after encryption (with PROTO=ESP, that is, protocol 50, lines 3-5)
這意味着我們有2種處理方式:
第1種:保留現有的iptables rule (但我們的代碼將收到未加密的packet)
第2種:改變匹配協議為ESP (iptables rule 中使用 -p 50 or -p esp都行)
在此,我們推薦使用第2種處理方式。因為有一種情況存在:如果使用AH (protocol 51),我們將不能更改ID field。 由於NFQUEUE會在kernel space和 user space之間來回copy packet,且由於沒有加密的packet 更小,能夠使匹配 Protocol 47更加高效,綜合上述原因,我們不對iptables rule做更改。
這里分享在routerA上的ipsec-tools.conf 配置:
#!/usr/sbin/setkey -f ## Flush the SAD and SPD # flush; spdflush; spdadd 192.168.200.1/32 172.16.0.1/32 gre -P out ipsec esp/transport//require; spdadd 172.16.0.1/32 192.168.200.1/32 gre -P in ipsec esp/transport//require; racoon.conf on routerA: log notify; path pre_shared_key "/etc/racoon/psk.txt"; path certificate "/etc/racoon/certs"; remote 172.16.0.1 { exchange_mode main; proposal { encryption_algorithm 3des; hash_algorithm md5; authentication_method pre_shared_key; dh_group modp1024; } } sainfo address 192.168.200.1/32 gre address 172.16.0.1/32 gre { pfs_group modp1024; encryption_algorithm 3des; authentication_algorithm hmac_md5; compression_algorithm deflate; } ipsec.conf on routerB: version 2.0 # conforms to second version of ipsec.conf specification # basic configuration config setup nhelpers=0 interfaces="%none" protostack=netkey klipsdebug="" plutodebug="" conn to-routerA type=transport left=172.16.0.1 leftsubnet=172.16.0.1/32 right=192.168.200.1 rightsubnet=192.168.200.1/32 authby=secret phase2alg=3des-md5;modp1024 keyexchange=ike ike=3des-md5;modp1024 auto=start leftprotoport=gre rightprotoport=gre
請留意:上述配置僅加密了GRE流量;身份驗證使用了PSK方式。
參考海外英文原稿:Click