将内网服务暴露到外网?我实在想不出其他的好名字了。

在接入支付宝和微信支付时,由于需要一个公网的HTTPS回调地址,一开始很多支付只能在线上环境测试,涉及到金额的部分又没办法直接修改,于是想到把内网服务暴露到外网的方式,让外网请求可以进入到内网。这样在外网也可以访问到测试环境,同时也可以进行一些未上线功能的演示,或者一些需要特殊地理位置的功能测试。

假定我们需要配置的HTTPS回调地址为 https://api.mikumaycry.com,先做好域名解析,再多解析一个子域名ike.mikumaycry.com到主机上,用于配制和连接VPN。然后可以开始动手了,VPS与本地主机都为Ubuntu 16.04 64位系统。

1.IKEv2隧道配置

如果本地测试环境是在公司或者家中的Linux主机上,可以考虑使用VPN,如果是在自己的主机上开发,推荐使用frp,因为使用系统自带VPN工具的话,会接管本地的所有流量。

这里使用IKEv2协议建立连接,认证方式为EAP-MSCHAPv2,关于StrongSwan的安装,strongswan.conf,防火墙配置,可以参考我之前的配置:IPSecAndIKEv2VPNWithStrongswan

虽然重复配置VPN有点啰唆,不过既然需要使用HTTPS,肯定要用到Let’s Encrypt,这次使用Let’s Encrypt的证书来认证服务端。

1.服务端与客户端的StrongSwan编译安装(这次只使用eap-mschapv2模块来认证,openVZ主机需要附加kernel-libipsec模块)

1
2
3
4
5
6
apt-get install build-essential libgmp3-dev libgmp-dev openssl libssl-dev -y
wget https://download.strongswan.org/strongswan-5.5.1.tar.gz
tar zxvf strongswan-5.5.1.tar.gz
cd strongswan-5.5.1
./configure --sysconfdir=/etc --enable-eap-mschapv2 --enable-eap-identity --enable-md4
make && make install

2.证书及账号密码配置(这一小节主要讲证书怎么存放)

在服务端

/etc/ipsec.d/certs目录下存放fullchain.pem:软链接,指向最新的服务端证书/etc/letsencrypt/live/mikumaycry.com/fullchain.pem,包含api和ike两个子域名;

/etc/ipsec.d/private目录下存放privkey.pem:软链接,指向最新的服务端证书私钥/etc/letsencrypt/live/mikumaycry.com/private.pem

在客户端

/etc/ipsec.d/cacerts目录下存放两个证书

chain.pem:Let‘s Encrypt中间证书,由DST Root CA X3颁发,从Let’s Encrypt生成证书时都会附带,如果没有的话,可以在这里下载:chain.pem

dst.pem:DST Root CA X3根证书,虽然系统已经内置了,但不放在该目录的话,还是会出现验证失败问题,通常可以在/etc/ssl/certs/DST_Root_CA_X3.pem位置找到,如果没有的话,可以从这里下载:DST_Root_CA_X3.pem

虽然我们使用的服务器证书fullchian.pem中包含了我们的域名证书和chain.pem,但StrongSwan似乎只能识别出第一条链,客户端必须在证书目录下存放chain.pemdst.pem才能完成完整的证书链认证。

完成后,在客户端下运行命令

1
ipsec listcacerts

可以看到详细的证书信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  subject:  "C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3"
  issuer:   "O=Digital Signature Trust Co., CN=DST Root CA X3"
  validity:  not before Mar 18 00:40:46 2016, ok
             not after  Mar 18 00:40:46 2021, ok (expires in 1489 days)
  serial:    0a:01:41:42:00:00:01:53:85:73:6a:0b:85:ec:a7:08
  flags:     CA CRLSign
  CRL URIs:  http://crl.identrust.com/DSTROOTCAX3CRL.crl
  OCSP URIs: http://isrg.trustid.ocsp.identrust.com
  pathlen:   0
  certificatePolicies:
             2.23.140.1.2.1
             1.3.6.1.4.1.44947.1.1.1
             CPS: http://cps.root-x1.letsencrypt.org
  authkeyId: c4:a7:b1:a4:7b:2c:71:fa:db:e1:4b:90:75:ff:c4:15:60:85:89:10
  subjkeyId: a8:4a:6a:63:04:7d:dd:ba:e6:d1:39:b7:a6:45:65:ef:f3:a8:ec:a1
  pubkey:    RSA 2048 bits
  keyid:     da:9b:52:a8:77:11:69:d3:13:18:a5:67:e1:dc:9b:1f:44:b5:b3:5c
  subjkey:   a8:4a:6a:63:04:7d:dd:ba:e6:d1:39:b7:a6:45:65:ef:f3:a8:ec:a1

  subject:  "O=Digital Signature Trust Co., CN=DST Root CA X3"
  issuer:   "O=Digital Signature Trust Co., CN=DST Root CA X3"
  validity:  not before Oct 01 05:12:19 2000, ok
             not after  Sep 30 22:01:15 2021, ok (expires in 1686 days)
  serial:    44:af:b0:80:d6:a3:27:ba:89:30:39:86:2e:f8:40:6b
  flags:     CA CRLSign self-signed
  subjkeyId: c4:a7:b1:a4:7b:2c:71:fa:db:e1:4b:90:75:ff:c4:15:60:85:89:10
  pubkey:    RSA 2048 bits
  keyid:     a8:e3:02:96:70:a6:8b:57:eb:ec:ef:cc:29:4e:91:74:9a:d4:92:38
  subjkey:   c4:a7:b1:a4:7b:2c:71:fa:db:e1:4b:90:75:ff:c4:15:60:85:89:10

如果是在服务端执行的话,会看到服务端证书信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  subject:  "CN=mikumaycry.com"
  issuer:   "C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3"
  validity:  not before Feb 04 20:51:00 2017, ok
             not after  May 05 20:51:00 2017, ok (expires in 77 days)
  serial:    03:2b:7f:06:23:f2:d9:d1:8d:a4:7d:42:40:28:da:bb:51:3c
  altNames:  mikumaycry.com, api.mikumaycry.com, ike.mikumaycry.com
  flags:     serverAuth clientAuth
  OCSP URIs: http://ocsp.int-x3.letsencrypt.org/
  certificatePolicies:
             2.23.140.1.2.1
             1.3.6.1.4.1.44947.1.1.1
             CPS: http://cps.letsencrypt.org
  authkeyId: a8:4a:6a:63:04:7d:dd:ba:e6:d1:39:b7:a6:45:65:ef:f3:a8:ec:a1
  subjkeyId: b9:1e:ea:66:0f:08:cb:e8:f2:c8:30:9b:73:08:13:8e:ab:96:b9:18
  pubkey:    RSA 2048 bits, has private key
  keyid:     46:46:12:a0:39:32:b0:77:6e:8c:41:23:f8:44:d0:7f:b5:ed:a3:eb
  subjkey:   b9:1e:ea:66:0f:08:cb:e8:f2:c8:30:9b:73:08:13:8e:ab:96:b9:18

还是只能看到第一条链,但以本站点为例(本站点也使用了Let’s Encrypt证书),如果使用openssl命令,模拟握手,可以看到详细的证书链,命令和部分输出如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
openssl s_client -servername wbuntu.com -connect wbuntu.com:443 < /dev/null

CONNECTED(00000003)
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify return:1
depth=0 CN = wbuntu.com
verify return:1
---
Certificate chain
 0 s:/CN=wbuntu.com
   i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
 1 s:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
   i:/O=Digital Signature Trust Co./CN=DST Root CA X3
---

3.配置StrongSwan

配置服务端 ipsec.conf

我们将为客户端分配一个固定内网IP:10.0.0.1,一个固定eap_identity和帐号alice。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
config setup
        uniqueids=no
conn %default
        ikelifetime=60m
        keylife=20m
        rekeymargin=3m
        keyingtries=1
        keyexchange=ike
        compress = yes
        forceencaps=yes
        fragmentation=yes
conn ikev2-eap-mschapv2
        keyexchange=ikev2
        leftauth=pubkey
        leftcert=fullchain.pem
        leftid=@ike.mikumaycry.com
        leftsendcert=always
        left=%defaultroute
        leftsubnet=0.0.0.0/0
        rightauth=eap-mschapv2
        right=%any
        rightid=alice
        rightsourceip=10.0.0.1/32
        eap_identity=alice
        auto=add

配置客户端ipsec.conf

注意最后两行,closeaction设置为restart可以让客户端在服务端服务重启或断开连接后尝试重新连接,但如果重连次数过多且不断失败,客户端就会断开连接,不再主动发起连接,而auto设置为start则表示客户端启动ipsec服务时,主动向服务器发起连接。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
config setup
        uniqueids=no
conn %default
        ikelifetime=60m
        keylife=20m
        rekeymargin=3m
        keyingtries=1
        keyexchange=ike
        compress = yes
        forceencaps=yes
        fragmentation=yes
conn ikev2-eap-mschapv2
        keyexchange=ikev2
        left=%defaultroute
        leftid=alice
        leftsourceip=%any
        leftauth=eap-mschapv2
        right=ike.mikumaycry.com
        rightid=ike.mikumaycry.com
        rightauth=pubkey
        eap_identity=alice
        closeaction=restart
        auto=start

配置ipsec.secrets

服务端配置,需要引入服务端私钥 privkey.pem

1
2
: RSA privkey.pem
alice : EAP "WbdoabHKtNomDdp"

客户端配置

1
alice : EAP "WbdoabHKtNomDdp"

推荐使用openssl生成密码

分别在客户端与服务端启动StrongSwan,应该是能够顺利完成连接,客户端被映射为服务端的一个内网主机。

2.HTTPS证书及Nginx配置

虽然之前的一篇文章里已经写了关于如何配置HTTPS证书:配置Let’s Encrypt证书

由于是从StartSSL证书升级过来的,所以并没有多讲其他内容,这次我们的环境中有了Nginx作反向代理,也有Nginx运行在Docker容器中的情况,因此需要考虑到在在反向代理状态下时,不停止Nginx,如何生成,配置,以及更新HTTPS证书。下面是正文。

假定客户端上的api服务端口为2333,则Nginx只需要将所有请求转发到10.0.0.1:2333即可。

首先当然是做好域名解析,其次把certbot项目克隆到VPS,首次执行certbot目录下的cerbot-auto程序,它会检测运行环境,并配置好一切依赖软件

1
2
3
git clone https://github.com/certbot/certbot.git
cd certbot
./certbot-auto

假定已经配置好隧道,并把80端口的请求转发给了内网主机,那么Nginx的conf应该如下所示

1
2
3
4
5
6
7
server {
  listen 80;
  server_name api.mikumaycry.com;
  location / {
    proxy_pass http://10.0.0.1:2333;
    proxy_set_header X-Forwarded-For $remote_addr;
 }

按照certbot的示例指令来生成证书

1
./certbot-auto certonly --standalone --email admin@mikumaycry.com -d mikumaycry.com -d api.mikumaycry.com -d ike.mikumaycry.com

Nginx未启用状态下,它会暂时占用80端口,在指定目录下生成认证文件,然后向Let’s Encrypt申请生成证书,默认会以首个域名创建文件夹和更新配置文件。执行完成后,可以发现新增了/etc/letsencrypt目录,关于目录信息可以查看之前的博客。我们需要关注的是该目录下的liverenewallive目录中存放着以首个域名为名字的文件夹,在其中存放指向最新证书的软连接,renewal目录下存放着用于更新证书的配置信息。我们在命令中输入的邮箱会在证书即将过期时收到邮件提醒,我上次收到的时间是证书过期前第19天。

Nginx启用状态下,会提示80或443端口已被占用,这个时候需要手动指定网页目录。

1
./certbot-auto certonly --webroot -w /pathToWebHostRoot --email admin@mikumaycry.com -d mikumaycry.com -d api.mikumaycry.com -d ike.mikumaycry.com

但是这个命令还是会提示错误,因为我们配置了nginx,把发送给80端口的所有请求都转发给内网主机了,而certbot需要一个目录来生成认证文件,因此可以修改下conf文件,添加认证路径,默认为格式为 域名/.well-known,映射到本地目录,目录可随意指定,cerbot-auto会在给出的目录下建立.well-known文件夹。修改后conf文件如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
server {
  listen 80;
  server_name api.mikumaycry.com;
  location / {
    proxy_pass http://10.0.0.1:2333;
    proxy_set_header X-Forwarded-For $remote_addr;
 }
  location /.well-known {
    alias /pathToWebHostRoot/.well-known;
 }
}

执行 nginx -s reload 命令,应用修改,然后再执行生成证书的命令即可。

对于docker容器中的nginx,我们可以把本地的一个/pathToWebHost目录挂载到容器中的同名路径。这样在执行cerbot-auto命令时,在宿主机上的修改可以被容器中nginx实时读取,也可以完成认证。

如果后期更新了证书,只需要执行

1
2
./certbot-auto renew
nginx -s reload

cerbot-auto会读取/etc/letsencrypt/renewal目录下的配置文件,更新所有证书即将过期证书,nginx可平滑更新证书,不会影响到已建立的连接。

下面是最终完成后的nginx配置文件,我们获取到证书后,重新修改配置,把所有的http请求重定向到https地址。同样执行nginx -s reload应用修改。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server {
  listen 80;
  server_name api.mikumaycry.com;
  location /.well-known {
    alias /pathToWebHostRoot/.well-known;
 }
  location / {
    proxy_pass http://10.0.0.1:2333;
    proxy_set_header X-Forwarded-For $remote_addr;
 }
}

server {
  listen 443 ssl http2;
  server_name api.mikumaycry.com;

  ssl_certificate /etc/letsencrypt/live/mikumaycry.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/mikumaycry.com/privkey.pem;

  location / {
    proxy_pass http://10.0.0.1:2333;
    proxy_set_header X-Forwarded-For $remote_addr;
  }
}

3.使用FRP

在前一篇中就讲过要尝试一下frp,下面是正文。

由于服务端处理了所有请求的HTTPS握手,在转发给客户端时,内容是明文的HTTP报文,如果内容没有加密就直接传输给客户端的话,我觉得还是不太合适。IKEv2完善的加密和认证体系可以保证客户端与服务端间的通信安全,至于FRP就不太清楚了,但它也带有加密功能。项目链接:https://github.com/fatedier/frp

幸运的是FRP是国人开发的,中文文档十分完善。下载对应release包,解压后内容如下:

1
2
3
4
5
6
7
8
frp_0.9.3_linux_amd64/
frp_0.9.3_linux_amd64/frpc_min.ini
frp_0.9.3_linux_amd64/LICENSE
frp_0.9.3_linux_amd64/frpc.ini
frp_0.9.3_linux_amd64/frps.ini
frp_0.9.3_linux_amd64/frpc
frp_0.9.3_linux_amd64/frps_min.ini
frp_0.9.3_linux_amd64/frps

对于VPS端,我们可以从frps_min.ini文件修改,内容如下:

1
2
3
4
5
6
7
8
9
[common]
bind_addr = 127.0.0.1
bind_port = 7000

[api]
type = tcp
auth_token = 123
bind_addr = 127.0.0.1
listen_port = 6000

对于客户端,我们可以从frpc_min.ini文件修改,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[common]
server_addr = mikumaycry.com
server_port = 7000
auth_token = 123

[api]
type = tcp
local_ip = 127.0.0.1
local_port = 2333
remote_port = 6000

虽然换了一种方式,但套路还是相同的,外网https请求依次经过

nginx:443 –> frps:6000 –> frpc:7000 –>  localservice–>local:2333

然后修改Nginx配置文件中转发目标的端口号为6000,应用修改。分别在服务端与客户端运行frp,外网的请求应该就能穿透进来了。