一、为什么需要加强安全性

内网穿透实际上是将内网机器的某些端口暴露到公网。普通的端口暴露出来并没有太大危害,比如网站或者某些后台服务的端口。而如果把ssh、nfs、samba或者数据库的一些端口暴露出来就会比较危险了。在公网上,有非常多的攻击,最简单的就是端口扫描后进行暴力破解。这些端口一旦公开在公网上,理论上被成功暴力破解攻破只是时间问题。

通常,我们可以通过封锁IP的方式来解决这种暴力攻击。但是,如果使用了内网穿透。便无法封锁IP,因为在可以识别攻击行为的地方,并不知道真实的攻击者IP。

内网穿透的访问链路如下:

graph TB user1("用户1") user2("攻击者") user3("用户3") subgraph 公网 frps("内网穿透服务端") end subgraph 内网 frpc("内网穿透客户器") server1("内网电脑") server2("内网电脑") end user1-->frps user2-->frps user3-->frps frps-.->frpc frpc-->server1 frpc-->server2

从链路图可以看出,内网电脑可以识别出暴力攻击行为。但是,它只知道是内网穿透客户端访问过来的,并不清楚攻击者的真实IP地址。实际上,连内网穿透客户端也不知道,只有内网穿透服务端才知道。而无论是内网穿透客户端还是内网穿透服务端都不清楚当前用户是否是攻击者。

二、通过白名单加强内网穿透的安全性

首先,我们要搞清楚,哪些端口是开放给所有人访问的,哪些端口是单独给特定的少数人使用的。通常而言,ssh、nfs、samba或者数据库的端口,这些高危端口正好是给特定少数人使用的。所以,我们可以针对这些端口设计一个白名单。只有在白名单中的IP才可以访问这些端口。于是,这些端口也就安全了。链路图变为:

graph TB user1("用户1") user2("攻击者") user3("用户3") reject((拒绝访问)) subgraph 公网 frps("内网穿透服务端") q_wl{"是否在白名单?"} end subgraph 内网 frpc("内网穿透客户器") server1("内网主机A") server2("内网主机B") end user1-->frps user2-->frps user3-->frps frps-->q_wl q_wl--否-->reject q_wl--是-->frpc frpc-->server1 frpc-->server2

接下来,有一个问题,那就是怎么维护白名单。如果手动维护白名单,成本会比较高。尤其是,用户的IP随时可能发生变动。当无法访问的时候再被动的将用户的IP加入到白名单,肯定会非常麻烦不说,还影响用户的正常使用,效率底下。所以,我们需要一个免维护的白名单。要想做到免维护,最好的办法就是授权用户自己去维护白名单。比如,在访问高危端口前,必须先访问某个低危端口提供的接口服务。在这个接口服务中,将用户的IP地址自动加入到白名单。加入白名单以后,只会在较短时间内有效,一旦超时就会从白名单中删除。

首先,由于低危端口的网址是不公开的(将IP加入白名单这个动作),而且与访问高危端口并没有直接的关联关系。所以,攻击者无法进行无脑暴力攻击。

其次,即使有人专门尝试攻击我们的服务器。比如,由于某种原因,我们需要公开低危端口的网址。那么,只需要对低危端口的访问进行安全加固就可以了。

sequenceDiagram participant 用户 participant 内网穿透服务端 participant 白名单服务 participant 内网穿透客户端 participant 内网主机 用户->>白名单服务: 访问某个网址 白名单服务-->>白名单服务: 将用户IP加入白名单 用户->>内网穿透服务端: 请求访问高危端口 内网穿透服务端->>白名单服务: 查询用户IP是否在白名单中 alt 白名单服务-->>内网穿透服务端: 不在白名单中 内网穿透服务端--x用户: 拒绝访问 else 白名单服务->>内网穿透服务端: 在白名单中 内网穿透服务端->>内网穿透客户端: 发送访问请求 内网穿透客户端->>内网主机: 发送访问请求 内网主机-)内网穿透客户端: 响应请求 内网穿透客户端-)内网穿透服务端: 响应请求 内网穿透服务端-)用户: 响应请求 end

注意:

  1. 白名单仅在建立连接的阶段有效。连接被建立后,就不需要白名单了。用户的IP也就可以从白名单中移除了。所以,超时时间可以设置的比较短。
  2. 用户IP加入白名单这个行为可以是自动发生,完全不需要人工参与。即使是用户,也可以做到完全不知道有加白名单这个过程。

三、实现方法

3.1 白名单服务程序

可以使用python来实现白名单服务程序,比较简单,不到50行就可以实现所有的逻辑。

from flask import Flask, request,abort, make_response
import json
import datetime

app = Flask("white_list_demo")

white_list = {}

@app.route("/white_list/<int:id>/<string:key>", methods=['PUT'])
def add_to_white_list(id, key):
    # TODO: check id and key
    now = datetime.datetime.now()
    white_list[request.remote_addr] = now
    for ip in white_list.keys():
        if (now - white_list[ip]).seconds > 60:
            del white_list[ip]
    return 'sucess'

@app.route("/white_list", methods=['POST'])
def is_in_white_list():
    op = request.args.get('op')
    data = {
        "reject":   False,
        "unchange": True,
    }
    while True:
        if op != "NewUserConn":
            break
        req = request.json()
        ip = req["content"]["remote_addr"].split(":")[0]
        if ip not in white_list:
            data["reject"] = True
            data["reject_reason"] = "not found"
        break
    response = make_response(json.dumps(data))
    response.mimetype = 'application/json'
    return response

if __name__=='__main__':
    app.run(host="127.0.0.1", port=5000)

3.2 内网穿透服务器配置

以frp为例,下面是frps的配置文件:

bindPort = 7900

[[httpPlugins]]
name = "white-list"
addr = "http://127.0.0.1:5000"
path = "/white_list"
ops = ["NewUserConn"]