飞牛OS会给大家提供一个FN ID,这个ID会根据网络情况自动切换使用内网、外网或者中继转发,我一直好奇是怎么实现的,于是看了看飞牛FN Connect的前端代码。
获取相关信息#
FnOS会通过/api/v1/fn/con请求获取你设备上传的信息,比如:内网ip,公网ip,ddns域名,飞牛提供的域名,设备的端口等等。
返回信息大概长这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
{
"msg": "",
"code": 0,
"data": {
"ddns": null,
"ipv4": [
"192.168.2.xxx"
],
"ipv6": [],
"publicIpv4": [
"39.xx.xx.xx",
],
"publicIpv6": [],
"fn": [
"xxxx.5ddd.com:443"
],
"port": {
"httpsPort": 8001,
"httpPort": 8002
},
"checkSum": "24860",
"ver": "1.6.0"
}
}
|
可以通过前端抓包获取,也可以通过下面这段代码获取
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
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import hashlib
import time
import random
import json
import requests
PREFIX = "NDzZTVxnRKP8Z0jXg1VAMonaG8akvh"
API_KEY = "zIGtkc3dqZnJpd29qZXJqa2w7c"
FN_ID = "xxxx" # 改成你的FN ID
TARGET_URL = "/api/v1/fn/con"
FULL_URL = "https://5ddd.com/api/v1/fn/con"
def get_md5(s):
return hashlib.md5(s.encode('utf-8')).hexdigest()
def get_sha256(s):
return hashlib.sha256(s.encode('utf-8')).hexdigest()
def get_authx_headers(json_payload, fn_id):
nonce = str(random.randint(100000, 999999))
timestamp = str(int(time.time() * 1000))
payload_str = json.dumps(json_payload, separators=(',', ':'))
payload_md5 = get_md5(payload_str)
sign_source = "_".join([PREFIX, TARGET_URL, nonce, timestamp, payload_md5, API_KEY])
sign = get_md5(sign_source)
authx_value = f"nonce={nonce}×tamp={timestamp}&sign={sign}"
fn_sign_source = f"trim_connect`{fn_id}`{timestamp}`anna"
fn_sign_value = get_sha256(fn_sign_source)
return {
'authx': authx_value,
'fn-sign': fn_sign_value,
'Content-Type': 'application/json'
}
data = {
'fnId': FN_ID,
}
custom_headers = get_authx_headers(data, FN_ID)
response = requests.post(FULL_URL, headers=custom_headers, data=json.dumps(data, separators=(',', ':')))
print(response.text)
|
连通性测试#
获取到收集到的IP之后,需要进行连通性测试,才能进行跳转。飞牛以版本1.4.0为分界线,大于等于1.4.0的会使用STUN方法进行检测,小于1.4.0使用Iframe Ping方法进行检测。
STUN#

飞牛会以你的所有内网IP + 系统设置中的httpsPort作为STUN服务器,并行尝试进行连接,通过promiseAny获取第一个成功响应的连接,并且跳转到 IP + httpPort 。(这里应该是考虑内网环境下,不需要加密)

具体来说,就是通过 WebRTC 连接到你的IP+https端口,判断通过 STUN 服务器反射得到的地址的端口是否为发出的端口,如果是则说明是通的。

这时候就好奇了,我的https端口不是监听着web服务吗,怎么能作为STUN服务器呢?
其实飞牛这里将TCP和UDP分开监听了,TCP作为web服务,UDP作为STUN服务器了,并且由一个叫pxy的程序监听。

而对于json中提供的所有公网IP,在测试内网ip不通后,也会进行STUN测试,不过最后并不会直接跳转,而是展示出来,让你选择一个。

Iframe Ping#
这个方法的思路就很简单,通过创建一个iframe去访问nas上的/static/bridge.html?t=${Date.now()},在这个iframe中请求/trimfn获取飞牛ID,将自己的IP和飞牛ID一并通过parent.postMessage发送给外层的页面,来达到判断通信成功。


接口测试#
对于DDNS和官方提供的Relay,会尝试访问/trimcon,判断返回的状态码是否为200或204,如果成功,则说明是通的。

不过这里有一个很奇怪的点,在系统中设置DDNS之后,使用api并没有把DDNS的域名加载出来,始终为null,不知道是我设置错了还是怎么样。
在浏览器中由于跨域的存在,我们需要不在另一端进行配置跨域就检测某个网页是否正常运行,直接请求肯定是不行的。飞牛提供了两个思路,一个需要额外运行STUN服务器程序并开放UDP,一个通过iframe绕过了跨域。但是飞牛为什么不能直接使用一个api,配置仅允许飞牛的域名进行跨域,直接进行检测呢?目前没有想到这两个方案的优势在哪。
同时通过分析我们可以发现,我们如果想要成功使用FNID连接上我们的设备,需要开放HTTP端口的TCP通信,HTTPS端口的TCP和UDP通信,否则就会跳转到飞牛官方的最终兜底策略中继转发。与此同时,飞牛的手机APP使用的策略似乎与网页不同,手机APP的HTTPS端口只需要开放TCP,就可以使用公网进行连接。