前言
本教程不适合小白,本人因为水平有限,也无法解决大家遇到的问题,一旦出现问题还得请各位自行摸索。
首要条件:NAT 类型为 NAT1,即 fullcone;光猫桥接或开启 dmz;路由器能刷机或者开启 upnp/dmz
建议:路由器能够控制具体的端口开放与否,而不是只能全开或全关。
本文中的操作基于 OpenWrt 上的 Lucky,在其它设备使用或是使用 NATMAP 进行穿透同样可以类比。建议在阅读了第一期与第二期的基础上阅读本文。
需解决的问题与思路
STUN穿透的端口不固定,并且更换较为频繁,虽然在第一期中,我们使用Cloudflare页面规则,将域名重定向到了公网端口,解决了web应用的访问;而绝大多数需要连接自建后端的应用程序并不支持重定向,必须指定端口,甚至部分应用程序不允许修改默认端口。
我们可以借鉴其它内网穿透软件的思路,通过一个端口来转发其他端口的流量,常见的做法有两种:
- 通过frp的stcp和sudp,将内网端口一对一的绑定到异地设备的本机端口上。
- 通过wireguard或n2n,将内网整个网段转发过去。
因为我的内网服务比较多,不想让连teamspeak的朋友都来欣赏我的“学习资料”,所以我选择使用frp。
此处放两个教程供各位参考,不过他们用的是NATMAP。
使用NATMap在NAT-1私网IP宽带上部署FRP服务
natmap内网穿透让内网实现公网IP,远程访问异地组网,做frps,ssh ,web,wireguard..
如果你不考虑访问者的权限,仅仅想把整个内网网段转发出去,可以考虑使用tailscale,并利用Lucky将搭建的Derp中继服务器穿透出去,操作更为简单,具体方法请参考第四期教程。
第一步 在内网安装frps和frpc,并配置stcp
在内网配置的frps和frpc使用官方版即可,此处简单带过。官方文档
frps
在路由器或nas上下载并安装frps,我是在Unraid里用docker安装:
配置frps.ini :
[common]
bind_port = 7000
token = your_token
只需配置bind_port
和token
就行,也就是frps的访问端口和令牌,默认采用TCP协议。
配置完成后运行frps,接下来参考之前的教程,它创建STUN穿透通道、配置端口转发、开启防火墙,在外网能ping通后为它设置开机自启。
frpc
为何在内网中也要frpc?这跟stcp与sudp的工作方式有关,官网的描述如下:
STCP 和 SUDP 的 (S) 的含义是 Secret。其作用是为 TCP 和 UDP 类型的服务提供一种安全访问的能力,避免让端口直接暴露在公网上导致任何人都能访问到。
这两种代理要求在被访问服务的机器上以及要访问的用户的机器上都部署上 frp 的客户端。被访问的服务所在机器叫做服务端,另一端叫做访问端。
frp 会在访问端监听一个端口和服务端的端口做映射。访问端的用户需要提供相同的密钥才能连接成功,从而保证安全性。
参考官方的使用示例 ,配置时根据需要把~~xtcp~~
改成~~stcp~~
或~~sdup~~
即可。 官方已经不再推荐使用ini配置文件。
下载安装官方版frpc后,编辑frpc.ini。common
中的token
必须与frps相同。
[common]
server_addr = 127.0.0.1
server_port = 7000
token = your_token
# jellyfin
[svc_jellyfin]
type = stcp
sk = B9jNFfUy3hDrmi
local_ip = 127.0.0.1
local_port = 8096
use_compression = true
role = server
# nginx
[svc_nginx]
type = stcp
sk = x5Y6f8VBbXQ5NY
local_ip = 192.168.0.115
local_port = 18443
use_compression = true
# teamspeak
[svc_ts1]
type = sudp
sk = khgtqgyCk3u3JZ
local_ip = 127.0.0.1
local_port = 9987
use_compression = false
[svc_ts2]
type = stcp
sk = khgtqgyCk3u3JZ
local_ip = 127.0.0.1
local_port = 10011
use_compression = true
[svc_ts3]
type = stcp
sk = khgtqgyCk3u3JZ
local_ip = 127.0.0.1
local_port = 30033
use_compression = true
我将frpc和frps部署在了同一台机器上,因此server_addr
填的是127.0.0.1,你要改为frps的内网ip。有网友反映将frps和frpc部署在同一台机器上可能会出错,但是我没遇到。
为你想要在外网访问的所有端口都配置frpc的stcp/sudp服务端(如果不指定role
则默认为server
),local_ip
和local_port
为相应服务的内网地址和端口。我这里的添加了本地地址上的jellyfin、nginx和teamspeak。
如果你需要访问的所有服务都经过反向代理,那也可以只为反向代理配置stcp,并在异地机器上修改HOSTS文件。
第二步 解析frps的公网端口
绕了这么一大圈,还是要面对最头疼的问题:frps的公网访问端口不固定怎么办?
解决思路
ip4p
NATMAP的作者给出了名为ip4p的方案,也就是将ipv4地址和端口转化成ipv6地址,添加为一条AAAA记录。当公网地址或端口改变时,只要修改该记录,即可实现类似DDNS的效果。作者也对frpc进行了修改,为它添加了解析ip4p的功能。支持ip4p的frp
The IP4P address format uses IPv6 special addresses to encode IPv4 addresses and ports for easy distribution through DNS AAAA records.
2001::{port}:{ipv4-hi16}:{ipv4-lo16}
-
ip4p存在的问题
在windows平台和安卓平台上,如果没有外网ipv6环境,那么他程序里使用的LookupIP函数不会获取AAAA记录。虽然调用nslookup可以解析AAAA记录,但是我懒......
也就是说,你连不上外网ipv6,就解析不了ip4p,但是如果你连得上ipv6,为什么不用ipv6直连呢......
TXT记录
我进交流群向带佬求助后,带佬建议我直接把公网ip和端口放进TXT记录里。于是我花了俩小时自学了一下golang,添加了解析TXT记录的功能。我的做法是,假设公网地址是183.6.66.666:6666
,将其用BASE64编码,变成MTgzLjYuNjYuNjY2OjY2NjY=
放进TXT记录里,供frpc解析。如果你觉得不安全,那可以考虑加个AES加密之类的。
创建TXT记录
进入Cloudflare后台,在dns - 记录中创建一条TXT记录,TTL我设为了一分钟,不确定是否生效。
将你刚才创建的TXT记录的域名、Cloudflare的邮箱、Global API Key和域名的区域id填入下面的指令里(忘记的可以看看第一期第四步和第五步),并在命令行里执行。
curl -s -X GET "https://api.cloudflare.com/client/v4/zones/区域id/dns_records?type=TXT&name=ip4p.s***********n.online" -H "X-Auth-Email:******@qq.com" -H "X-Auth-Key:f9***********************1b" -H "Content-Type: application/json"
把结果里"id":
后面的那一串数字+字母复制下来,这就是这条记录的id。
把包括刚才id在内的信息替换到下一行指令里执行,尝试对刚才添加的TXT记录进行修改:
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/区域id/dns_records/记录id" -H "X-Auth-Email:******@qq.com" -H "X-Auth-Key:f9*************************1b" -H "Content-Type: application/json" --data '{"type":"TXT","name":"ip4p.s************.online","content":"changed","ttl":60,"proxied":false}'
再回到Cloudflare后台,应该就能看到这条记录更改了。
部分免费域名无法使用API修改。
配置脚本
在以下脚本中填入Cloudflare的邮箱、Global API key、区域id、txt记录的id。
domain="ip4p.s******n.online"
zone="e7***************************fb"
txt_id="a19***********************8a06"
email="****@qq.com"
key="f9*****************1b"
echo "change txt record"
#base64 encode
addr_64=$(echo -n ${ipAddr} | base64)
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone/dns_records/$txt_id" \
-H "X-Auth-Email: $email" -H "X-Auth-Key: $key" \
-H "Content-Type: application/json" \
--data '{"type":"TXT","name":"'$domain'","content":"'$addr_64'","ttl":60,"proxied":false}'
然后编辑穿透规则,开启自定义脚本触发,把它粘贴到自定义脚本中。
第二步剩余内容已经过时,仅留作记录,请跳至第三步
配置脚本
Lucky从2.5.1起,支持STUN地址改变时调用脚本,本节内容失效。
在第二期创建的webhook服务器的脚本目录下创建frp.sh,并填入Cloudflare的邮箱、Global API key、区域id、txt记录的id。我的ip4p.s********n.online
域名除了txt记录外,还添加了ip4p的aaaa记录;如果你需要,请仿照上一步手动添加一条aaaa记录,并填入这条记录的id,如果不需要那就把对应的部分注释掉。
#!/bin/bash
# param is ip,port,addr
domain="ip4p.s********n.online"
zone="e7***************************************fb"
ip4p_id="96************************************1b93"
txt_id="a19*********************************8a06"
email="******@qq.com"
key="f9*************************1b"
echo "change ip4p record"
array=(${ip//./ })
ip4p=2001::$(printf '%x:%x%x:%x%x\n' $port ${array[0]} ${array[1]} ${array[2]} ${array[3]})
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone/dns_records/$ip4p_id" \
-H "X-Auth-Email:$email" -H "X-Auth-Key:$key" \
-H "Content-Type: application/json" \
--data '{"type":"AAAA","name":"'$domain'","content":"'$ip4p'","ttl":60,"proxied":false}'
echo ""
echo "change txt record"
# base64 encode
addr_64=$(echo -n $addr | base64)
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone/dns_records/$txt_id" \
-H "X-Auth-Email:$email" -H "X-Auth-Key:$key" \
-H "Content-Type: application/json" \
--data '{"type":"TXT","name":"'$domain'","content":"'$addr_64'","ttl":60,"proxied":false}'
在Lucky中设置webhook
Lucky从2.5.1起,支持STUN地址改变时调用脚本,本节内容失效。
打开Lucky后台,编辑stun的穿透规则,打开webhook,将接口地址设置为http://webhookd的ip地址:wenhookd的端口/frp?ip=#{ip}&port=#{port}&addr=#{ipAddr}
(webhook服务器地址、端口和脚本名称根据实际情况来),接口调用成功包含的字符串填"success":true
。
第三步 在异地电脑上配置frpc
异地电脑上的frpc必须使用魔改版,如需解析txt记录,可以使用我改的版本,但我只在Windows上编译过,其它平台请自行编译。
配置frpc.ini
修改frpc.ini
,server_addr
填添加了ip4p或TXT记录的域名,server_port
不用填,建议指定dns_server
。
[common]
server_addr = ip4p.s******n.online
token = your_token
dns_server = 8.8.8.8
# jellyfin
[vst_jellyfin]
type = stcp
role = visitor
server_name = svc_jellyfin
sk = B9jNFfUy3hDrmi
bind_ip = 127.0.0.1
bind_port = 38096
use_compression = true
# nginx
[vst_nginx]
type = stcp
role = visitor
server_name = svc_nginx
sk = x5Y6f8VBbXQ5NY
bind_ip = 127.0.0.1
bind_port = 18443
use_compression = true
# teamspeak
[vst_ts1]
type = sudp
role = visitor
server_name = svc_ts1
sk = khgtqgyCk3u3JZ
bind_ip = 127.0.0.1
bind_port = 9987
use_compression = false
[vst_ts2]
type = stcp
role = visitor
server_name = svc_ts2
sk = khgtqgyCk3u3JZ
bind_ip = 127.0.0.1
bind_port = 10011
use_compression = true
[vst_ts3]
type = stcp
role = visitor
server_name = svc_ts3
sk = khgtqgyCk3u3JZ
bind_ip = 127.0.0.1
bind_port = 30033
use_compression = true
访问端必须手动指定role
为visitor
,必须指定连接的服务端名称server_name
,并且sk
应与服务端相同。
如果bind_ip
填为127.0.0.1
,则仅有本设备能访问。若想让其它设备访问,则填为0.0.0.0
。
在Windows上将frpc注册为服务
这一步面向零基础使用者。(也就是你的小白朋友)
先尝试运行一次
打开frpc.exe所在文件夹,在资源管理器空白处右键
- 打开命令提示符
。
输入.\frpc.exe
并按下回车。如果没有出现错误,可以尝试访问所有的服务。
如果发现有的服务访问不上,打开frpc.ini
,找那个服务到对应的bind_port
,记下这个数字。
创建文件检查端口是否占用.ps1
,代码如下。右键
- 使用PowerShell运行
while(1)
{
$port = Read-Host "请输入需要查询的端口,输入0退出:"
IF ($port -eq 0) {exit}
$res=$(Get-NetTCPConnection | ? {$_.LocalPort -eq $port})
if ($res){
echo $res
Get-Process -Id (Get-NetTCPConnection -LocalPort $port).OwningProcess
}
else {echo "未被占用"}
}
你可以输入刚才记下的数字,查看该端口是否被占用、被什么程序占用,然后选择关闭该程序或者选择另一个没有被占用的端口(1025-65535之间)。
一旦关闭命令提示符窗口,frpc就会关闭。
注册服务
可以使用nssm将程序注册为服务。
先将frpc关闭。在资源管理器中打开nssm.exe
所在目录。32位系统打开win32
目录,64位系统打开win64
目录。
在资源管理器空白处右键
- 打开命令提示符
,输入.\nssm.exe install frpc
。
会弹出NSSM service installer窗口,Application Path
选择frpc.exe
,Startup directory
选择frpc.exe所在目录,Arugments不填,点击Install service。
在任务管理器
- 服务
- 打开服务
弹出窗口中找到frpc。右键frpc,点击属性
,将启动类型改为自动,并点击确定
。再右键frpc,点击启动
。
魔改版frp使用问题总结
如果你的应用强制要求https连接(如BitWarden),可以在frp中添加反向代理的端口,并在异地电脑上修改hosts劫持域名。
我尚未解决的问题记录如下:
- 我尝试让frp使用QUIC协议,访问端可以连接到服务端,可以ping通,但是几乎收不到包;
- 部分常用第三方远程桌面无法正常使用(moonlight可以)
- RustDesk的ID服务器可以连接,但是中继服务器无法连接;
- parsec无法打洞(不过我也不觉得能成功)。
- 安卓手机若无root,开了穿透软件就无法魔法上网。
结语
至此,我们补上了STUN内网穿透的最后一块拼图,基本解决了大内网环境下的所有痛点,近似达到了拥有公网ip的效果。但使用体验仍不能称得上完美,期待之后能有更好的方案出现。