標籤:

Site to Site IPsec VPN

需求是這樣的,一方面是公司的北京和美國辦公室的內網網段要連起來,另一方面如果可能的話要給北京辦公室提供fq服務。公司拉不起專線,於是找到了一個從北京和美國連的速度還很不錯的vps作為跳板,剩下的問題就是怎麼連vpn了。

一開始嘗試用 strongswan 嘗試 ikev2,但是發現有兩個問題,第一個是默認非 ipip 隧道,分路由非常麻煩(想導入 chnroute 幾千條動態的不可能全放配置文件里),第二個問題是重啟跳板上的 strongswan 就會導致全部連接中斷,必須按順序從一端到另一端關閉再啟動...這麼麻煩的維護顯然不是我想要的,雖然一開始沒有搞明白是為什麼。

然後在 yyf 的指引之下看到了這篇東西,稍微試驗了一下發現非常不錯,靜態的 sh 腳本載入,不過這篇東西里套了兩層,即在 l2tpv3 里套了一層 ipsec,感覺沒有太大必要,於是各種嘗試,最終決定直接使用 espinudp-nonike,配靜態密鑰,overhead 大概在每個包 50byte 左右。由於要不可避免的穿透 NAT,所以沒有直接用 esp。配置腳本大概這樣:

function setup_tunnel {IF=$1case $MODE instop*)echo "Removing tunnel $IF"ip xfrm state del src $LEFT_IP dst $RIGHT_IP proto esp spi $IDip xfrm state del src $RIGHT_IP dst $LEFT_IP proto esp spi $IDip xfrm policy del src $LEFT_IP dst $RIGHT_IP dir outip xfrm policy del src $RIGHT_IP dst $LEFT_IP dir inip tunnel del $IF;;start*)echo "Adding tunnel $IF"ip tunnel add $IF mode ipip local $LEFT_IP remote $RIGHT_IP dev $ETHDEVip addr add $LEFT peer $RIGHT dev $IFip xfrm state add src $LEFT_IP dst $RIGHT_IP proto esp spi $ID reqid $ID mode transport encap espinudp-nonike $LEFT_PORT $RIGHT_PORT 0.0.0.0 enc aes $KEYip xfrm state add src $RIGHT_IP dst $LEFT_IP proto esp spi $ID reqid $ID mode transport encap espinudp-nonike $RIGHT_PORT $LEFT_PORT 0.0.0.0 enc aes $KEYip xfrm policy add dir out src $LEFT_IP dst $RIGHT_IP action allow tmpl proto esp spi $ID reqid $ID mode transportip xfrm policy add dir in src $RIGHT_IP dst $LEFT_IP action allow tmpl proto esp spi $ID reqid $ID mode transportip link set $IF up;;esac}

最關鍵的是這兩句

ip xfrm state add src $LEFT_IP dst $RIGHT_IP proto esp spi $ID reqid $ID mode transport encap espinudp-nonike $LEFT_PORT $RIGHT_PORT 0.0.0.0 enc aes $KEYip xfrm state add src $RIGHT_IP dst $LEFT_IP proto esp spi $ID reqid $ID mode transport encap espinudp-nonike $RIGHT_PORT $LEFT_PORT 0.0.0.0 enc aes $KEY

這個描述了如何讓內核把 udp 包解為 ip 包以及如何把 ip 包加成 udp 包。enc 使用靜態的 aes 沒有使用一些動態滾動加密,是因為動態密碼雖然確實可以防止中間人,但服務重啟之後很容易出問題,之前strongswan必須按順序重啟也是同樣的原因。這樣靜態加密保證機器一起來包一旦來了就立刻能到,並且也確實沒有太大的需求去防中間人。

下面坑爹的地方來了。這兩個機器在NAT後面的話,這樣隧道依舊是起不來的,最主要的原因是,源udp埠在出NAT的時候會被轉換成別的埠,於是 $RIGHT_PORT 就變得沒有意義了。坑爹的是,即便在 iptables nat prerouting 的時候做一下 SNAT ,依舊不起作用。在經過了大概半個世紀的 google 之後...

Simple UDP ESP Encapsulation (NAT-T) for AWS EC2 IPSEC Tunnel

於是是這樣的,首先要開一個程序監聽那個埠,並把 xfrm policy assign 到打開的 socket 上,然後讓程序開著就行,這樣內核就能認出這個包了...

以下來自 strongswan 源碼

/* We dont receive packets on the send socket, but we need a INBOUND policy. * Otherwise, UDP decapsulation does not work!!! */ policy.sadb_x_policy_dir = IPSEC_DIR_INBOUND; if (setsockopt(skt, sol, ipsec_policy, &policy, sizeof(policy)) < 0) { DBG1(DBG_NET, "unable to set IPSEC_POLICY on send socket: %s", strerror(errno)); close(skt); return 0; } /* bind the send socket */ if (bind(skt, (struct sockaddr *)&addr, sizeof(addr)) < 0) { DBG1(DBG_NET, "unable to bind send socket: %s", strerror(errno)); close(skt); return 0; } if (family == AF_INET) { /* enable UDP decapsulation globally, only for one socket needed */ if (setsockopt(skt, SOL_UDP, UDP_ENCAP, &type, sizeof(type)) < 0) { DBG1(DBG_NET, "unable to set UDP_ENCAP: %s; NAT-T may fail", strerror(errno)); } }

當時看到這裡的時候真是一口老血......於是試著弄個了 c 程序

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/types.h>#include <sys/socket.h>#include <string.h>#include <errno.h>#include <unistd.h>#include <stdlib.h>#include <fcntl.h>#include <sys/ioctl.h>#include <netinet/in_systm.h>#include <netinet/in.h>#include <netinet/ip.h>#include <netinet/udp.h>#include <net/if.h>#define _LINUX_IN6_H#include <linux/xfrm.h>int set_ipsec_policy(int fd){ struct xfrm_userpolicy_info policy; u_int sol, ipsec_policy; sol = IPPROTO_IP; ipsec_policy = IP_XFRM_POLICY; memset(&policy, 0, sizeof(policy)); policy.action = XFRM_POLICY_ALLOW; policy.sel.family = AF_INET; policy.dir = XFRM_POLICY_OUT; if (setsockopt(fd, sol, ipsec_policy, &policy, sizeof(policy)) < 0) { printf("unable to set IPSEC_POLICY on socket: %s
",strerror(errno)); return -1; } policy.dir = XFRM_POLICY_IN; if (setsockopt(fd, sol, ipsec_policy, &policy, sizeof(policy)) < 0) { printf("unable to set IPSEC_POLICY on socket: %s
",strerror(errno)); return -1; } return 0;}int main(int argc, char *argv[]){ int sockfd = -1; struct sockaddr_in host_addr; if((sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP))<0) { printf("socket() error!
"); exit(1); } memset(&host_addr, 0, sizeof(host_addr)); host_addr.sin_family = AF_INET; host_addr.sin_port = htons(atoi(argv[1])); host_addr.sin_addr.s_addr = htonl(INADDR_ANY); const int on = 1; if(setsockopt(sockfd, IPPROTO_IP, SO_REUSEADDR, &on, sizeof(on))<0) { printf("setsockopt() error!
"); exit(0); } int encap = 1; if(setsockopt(sockfd, IPPROTO_UDP, 100, &encap, sizeof(encap))<0) { printf("setsockopt() udp error!
"); exit(0); } if (bind(sockfd, (struct sockaddr*)&host_addr, sizeof(host_addr)) < 0) { printf("unable to bind socket: %s
", strerror(errno)); close(sockfd); return -1; } set_ipsec_policy(sockfd); printf("bind ..
"); while(1) { sleep(1); } return 0;}

發現這樣 vpn 就通了之後又是一口老血...

總之這樣 vpn 算是通了,後面 chinadns + chnroute 都是小意思了。這樣搞起來的隧道靜態配置,重啟路徑上任何一台機器只要開機啟動上述腳本 vpn 路由就通了。缺點是,首先不知道為什麼不能 traceroute 了,然後建立 vpn 的兩個 ip 之間除了 vpn udp 無法有其它正常通信了,不過我司有三個外網出口所以問題不大,負載均衡上加條規則就行。還有個缺點是,這個解決方案不適用於 mobile client,只能用於 site to site,不過倒是挺穩定的,ipip tunnel 也能搞路由表了。

以上。

------

題外話,VMware vSphere 真特么好用啊,vlan 真特么好用啊,有機會折騰好開心。下一個要搞定的目標是 ldap 了,希望一切順利。


推薦閱讀:

RCNN學習筆記(5):faster rcnn
「茴」字的五種寫法
TCP協議三次握手、揮手實現原理
香格里拉遊記隨想
在公司用免費的Wi-Fi被人監控了如何處理?

TAG:計算機網路 |