本文介绍

  • 内网穿透原理
  • nps 搭建 cs 模式的 ssh 中转隧道
  • nps 搭建 p2p 模式的 ssh 隧道

我所使用的为:

  • 一台带公网的云服务器,作为 server
  • 一台 Ubuntu 虚拟机,作为被控端 client1
  • 一台 windows10 主机,作为访问端 client2

(但是,由于 NAT 类型不支持,p2p 的穿透失败了,但是过程是对的,后续会补上 p2p 的成功教程)

内网穿透原理

先介绍一下 nps

nps 是一款轻量级、高性能、功能强大的内网穿透代理服务器。目前支持 tcp、udp 流量转发,可支持任何 tcp、udp 上层协议(访问内网网站、本地支付接口调试、ssh 访问、远程桌面,内网 dns 解析等等……),此外还支持内网 http 代理、内网 socks5 代理、p2p 等,并带有功能强大的 web 管理端。
官方网站

内网穿透原理

内网穿透是一种将内部网络中的服务暴露到公网上的技术,其原理是通过一个中间代理服务器,将公网请求转发到内网中的指定服务上,从而实现内网服务的对外访问。

具体来说,内网穿透的原理如下:

  • 在内网中部署一个客户端程序,该程序会与内网中的服务建立连接,并将服务的请求转发到代理服务器上。
  • 在公网中部署一个服务器程序,该程序会接收来自代理服务器的请求,并将请求转发到内网中的客户端程序上。
  • 当公网用户访问内网服务时,请求会先发送到公网服务器,然后由公网服务器将请求转发到内网中的客户端程序上,最终客户端程序将请求转发到内网服务上,并将服务的响应返回给公网用户。

因此,内网穿透分为 CS 模式和 P2P 模式两种
CS 模式所有流量通过公网服务器代理,比较稳定,但是带宽受服务器的限制
P2P 模式公网服务器仅做连接的媒介,不稳定,但是带宽仅受客户端限制,很快

但是,p2p 模式是否成功会跟 NAT 的模式有很大关系

在 STUN 标准中,NAT 模式有 4 种:

图片来自 Info-Finder
img
  • Full Cone NAT(完全锥型 NAT)
    所有从同一个私网 IP 地址和端口(IP1:Port1)发送过来的请求都会被映射成同一个公网 IP 地址和端口(IP:Port)。并且,任何外部主机通过向映射的公网 IP 地址和端口发送报文,都可以实现和内部主机进行通信。
    这是一种比较宽松的策略,只要建立了私网 IP 地址和端口与公网 IP 地址和端口的映射关系,所有的 Internet 上的主机都可以访问该 NAT 之后的主机。
  • Restricted Cone NAT(限制锥型 NAT)
    所有从同一个私网 IP 地址和端口(IP1:Port1)发送过来的请求都会被映射成同一个公网 IP 和端口号(IP:Port)。与完全锥型 NAT 不同的是,当且仅当内部主机之前已经向公网主机发送过报文,此时公网主机才能向私网主机发送报文。
  • Port Restricted Cone NAT(端口限制锥型 NAT)
    与限制锥型 NAT 很相似,只不过它包括端口号。也就是说,一台公网主机(IP2:Port2)想给私网主机发送报文,必须是这台私网主机先前已经给这个 IP 地址和端口发送过报文。
  • Symmetric NAT(对称 NAT)
    所有从同一个私网 IP 地址和端口发送到一个特定的目的 IP 地址和端口的请求,都会被映射到同一个 IP 地址和端口。如果同一台主机使用相同的源地址和端口号发送报文,但是发往不同的目的地,NAT 将会使用不同的映射。此外,只有收到数据的公网主机才可以反过来向私网主机发送报文。
    这和端口限制锥型 NAT 不同,端口限制锥型 NAT 是所有请求映射到相同的公网 IP 地址和端口,而对称 NAT 是不同的请求有不同的映射。

也就是说,要实现 p2p 模式,两个客户端不能都是对称型 NAT, 否则不能成功
然而我所在的校园网都是对称性 NAT, p2p 实验没法成功 (开专!输!)
检测 NAT 的方法可以看文章末尾

下载并启动 nps

通过上面的原理可知,我们需要一台有公网的服务器 (安装 nps), 和若干个客户端 (npc)

下文我们称服务器为 server, 被控客户端为 client1, 访问端为 client2.
即我在 client2 上通过 server 建立 p2p 连接,访问到 client1

注意: nps 和 npc 分别是在服务端 server 和客户端 client 上安装,使用命令时要注意拼写

下载

首先服务端 server 下载并解压 nps

bash
1
2
3
4
5
wget https://github.com/ehang-io/nps/releases/download/v0.26.10/linux_amd64_server.tar.gz

tar -xzvf linux_amd64_server.tar.gz

sudo ./nps install

客户端 client 下载 npc

推荐使用 docker 的方式

bash
1
docker pull ffdfgdfg/npc

当然,也可以自己手动下载,如下

Linux 平台

bash
1
2
3
wget https://github.com/ehang-io/nps/releases/download/v0.26.10/linux_amd64_client.tar.gz

tar -xzvf linux_amd64_client.tar.gz

windows 平台自己下载解压,点击这里下载

启动 nps

首先进入 nps 目录编辑配置

bash
1
sudo vim /etc/nps/conf/nps.conf

示例如下
其中,需要修改的地方如下,我都留成 TODO 了

  • http_proxy_port (我的是 7997, 防止冲突)
  • https_proxy_port (我的是 7998)
  • bridge_port (我开放的是 8000)
  • public_vkey
  • p2p_ip (如果要用 p2p 服务的话)
  • p2p_port
  • web_username
  • web_password
  • web_port (我开的是 7999)
toml
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
appname = nps
#Boot mode(dev|pro)
runmode = dev

#HTTP(S) proxy port, no startup if empty
http_proxy_ip=0.0.0.0
http_proxy_port=TODO
https_proxy_port=TODO
https_just_proxy=true
#default https certificate setting
https_default_cert_file=conf/server.pem
https_default_key_file=conf/server.key

##bridge
bridge_type=tcp
bridge_port=TODO
bridge_ip=0.0.0.0

# Public password, which clients can use to connect to the server
# After the connection, the server will be able to open relevant ports and parse related domain names according to its own configuration file.
public_vkey=TODO

#Traffic data persistence interval(minute)
#Ignorance means no persistence
#flow_store_interval=1

# log level LevelEmergency->0 LevelAlert->1 LevelCritical->2 LevelError->3 LevelWarning->4 LevelNotice->5 LevelInformational->6 LevelDebug->7
log_level=7
#log_path=nps.log

#Whether to restrict IP access, true or false or ignore
#ip_limit=true

#p2p
p2p_ip=TODO # 这里设置为服务器server的公网IP
p2p_port=TODO # 随便设置一个UDP端口, 记得开放即可

#web
web_host=a.o.com
web_username=TODO # web登录面板的用户名和密码和端口,自定义
web_password=TODO
web_port = TODO
web_ip=0.0.0.0
web_base_url=
web_open_ssl=false
web_cert_file=conf/server.pem
web_key_file=conf/server.key
# if web under proxy use sub path. like http://host/nps need this.
#web_base_url=/nps

#Web API unauthenticated IP address(the len of auth_crypt_key must be 16)
#Remove comments if needed
#auth_key=test
auth_crypt_key =1234567812345678

#allow_ports=9001-9009,10001,11000-12000

#Web management multi-user login
allow_user_login=false
allow_user_register=false
allow_user_change_username=false


#extension
allow_flow_limit=false
allow_rate_limit=false
allow_tunnel_num_limit=false
allow_local_proxy=false
allow_connection_num_limit=false
allow_multi_ip=false
system_info_display=false

#cache
http_cache=false
http_cache_length=100

#get origin ip
http_add_origin_header=false

#pprof debug options
#pprof_ip=0.0.0.0
#pprof_port=9999

#client disconnect timeout
disconnect_timeout=60

PS: 记得开端口,既要在服务器防火墙上设置开启,又要在服务器的供应商那里开放安全组

服务端直接执行如下启动

bash
1
sudo nps start

然后执行

bash
1
sudo cat /var/log/nps.log

可以看到如下的这种就成功了

img

搭建隧道

CS 模式

CS 模式时,client 通过 server 当桥梁,流量都经过 server 中转.
也就是,在访问端 client2 通过访问 <服务器IP>:<端口> 即可定向到被控端 client1

我们使用 docker 容器的方式安装,毕竟相对方便

首先在 client1, 即被控端,找个地方建立一个文件夹 conf, 比如我是在 ~/test/conf

bash
1
2
mkdir conf
vim npc.conf

在客户端 client1 的配置文件 npc.conf 修改如下

其中注意

  • server_addr 是自己服务器 server 的访问 IP 和端口,比如我的是 8000
  • vkey 要修改成一个独立不重复的,这是客户端的 key
  • target 为客户端 client1 上你要访问的目的端口,一般而言就是 localhost:22 表示 ssh 连接
  • server_port 为我们访问服务器的端口,会定向到 client1 去
toml
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
[common]
server_addr=<IP>:<PORT>
conn_type=tcp
vkey=testubt
auto_reconnection=true
max_conn=1000
flow_limit=1000
rate_limit=1000
crypt=true
compress=true
#pprof_addr=0.0.0.0:9999
disconnect_timeout=60

[health_check_test1]
health_check_timeout=1
health_check_max_failed=3
health_check_interval=1
health_http_url=/
health_check_type=http
health_check_target=127.0.0.1:8083,127.0.0.1:8082

[health_check_test2]
health_check_timeout=1
health_check_max_failed=3
health_check_interval=1
health_check_type=tcp
health_check_target=127.0.0.1:8083,127.0.0.1:8082


[tcp]
mode=tcp
target_addr=127.0.0.1:22
server_port=5500

访问端,也就是 client2, 无需配置

接着在 nps 的 web 控制面板添加一个客户端
验证密钥要和 client1 的一样,下面的加密和压缩都填是,如下

img

然后在 client1 上,通过 docker 容器启动 npc (记得修改本机的 conf 目录为刚刚创建的配置目录)

bash
1
docker run -d --name npc --net=host -v <本机conf目录>:/conf ffdfgdfg/npc -config=/conf/npc.conf

比如我在 user 用户目录下的 test 挂载,就是如下

bash
1
docker run  -d --name npc --net=host -v /home/user/test/conf:/conf ffdfgdfg/npc -config=/conf/npc.conf

此时在 server 面板上可以看到如下已连接

img

然后在访问端 client2 执行如下

bash
1
ssh -p <PORT> username@IP

比如我的 port 是 5500, IP 填服务器公网 IP (也可以是域名)

如下,访问成功

img

p2p 模式

p2p 的原理是客户端 client1 和 server 保持长期连接,我们通过另一个客户端 client2 去访问 server 对应端口,并使用独特的密码标识,就可以通过 server 打通两个客户端的隧道,就可以访问到 client1

我们还是使用 docker 容器的方式安装,毕竟相对方便

首先在 client1, 即被控端,找个地方建立一个文件夹 conf, 比如我是在 ~/test/conf

bash
1
2
mkdir conf
vim npc.conf

在客户端 client1 的配置文件 npc.conf 修改如下

其中注意

  • server_addr 是自己服务器 server 的访问 IP 和端口,比如我的是 8000
  • vkey 要修改成一个独立不重复的,这是客户端的 key
  • p2p 的 password 也要修改成独立不重复的,自定义
  • target 为客户端 client1 上你要访问的目的端口,一般而言就是 localhost:22 表示 ssh 连接
toml
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
[common]
server_addr=<IP>:<Port>
conn_type=tcp
vkey=testubt
auto_reconnection=true
max_conn=1000
flow_limit=1000
rate_limit=1000
crypt=true
compress=true
#pprof_addr=0.0.0.0:9999
disconnect_timeout=60

[health_check_test1]
health_check_timeout=1
health_check_max_failed=3
health_check_interval=1
health_http_url=/
health_check_type=http
health_check_target=127.0.0.1:8083,127.0.0.1:8082

[health_check_test2]
health_check_timeout=1
health_check_max_failed=3
health_check_interval=1
health_check_type=tcp
health_check_target=127.0.0.1:8083,127.0.0.1:8082

[ssh_p2pmode]
mode=p2p
password=pwdforssh
target_addr=localhost:22

在访问端,也就是 client2, 配置文件如下

要注意的也和上面一样,此外多了一个 local_port, 这个就是通过 client2 访问时,client2 上的端口,自定义即可 (不指定也行,默认为 2000)

toml
1
2
3
4
5
6
7
8
9
[common]
server_addr=<IP>:<Port>
conn_type=tcp
vkey=testubt

[p2p_ssh]
local_port=2999
password=pwdforssh
target_addr=22

接着在 nps 的 web 控制面板添加一个客户端
验证密钥要和 client1 的一样,下面的加密和压缩都填是,如下

img

然后在 client1 上,通过 docker 容器启动 (记得修改本机的 conf 目录为刚刚创建的配置目录)

bash
1
docker run -d --name npc --net=host -v <本机conf目录>:/conf ffdfgdfg/npc -config=/conf/npc.conf

比如我在 user 用户目录下的 test 挂载,就是如下

bash
1
docker run  -d --name npc --net=host -v /home/user/test/conf:/conf ffdfgdfg/npc -config=/conf/npc.conf

此时在 server 面板上可以看到如下已连接

img

然后在访问端 client2 的 npc 的目录,执行如下命令即可

bash
1
./npc.exe -server=<IP>:<Port> -vkey=testubt -type=tcp -password=pwdforssh -target=localhost:22

不出意外,此时应该会成功启动,显示内容参考如下

2023/04/30 20:38:59.274 [I] [npc.go:231]  the version of client is 0.26.10, the core version of client is 0.26.0
2023/04/30 20:38:59.317 [I] [control.go:97]  Loading configuration file C:\Users\LENOVO\Desktop\windows_amd64_client\conf\npc.conf successfully
2023/04/30 20:38:59.391 [N] [control.go:174]  web access login username:user password:testubt
2023/04/30 20:38:59.394 [I] [local.go:115]  successful start-up of local tcp monitoring, port 2999
2023/04/30 20:38:59.444 [I] [client.go:72]  Successful connection with server 117.50.182.150:8000
2023/04/30 20:39:00.392 [N] [local.go:142]  try to connect to the server 1
2023/04/30 20:39:28.829 [D] [local.go:117]  new p2p connection

此时,再在访问端 client2 执行如下命令

bash
1
ssh -p 2999 root@127.0.0.1

即可成功连接 client1 (我的因为 NAT 不支持,没成功)

debug 过程

port or host may have been occupied

p2p 模式启动时,访问端 client2 显示如下

[control.go:158]  The server returned an error, which port or host may have been occupied or not allowed to open.  ssh_p2pmode

原因是端口没开

此处注意要开放 2 个地方,在系统防火墙里开端口,还要去服务器供应商那里开安全组的端口

connection refused

客户端无法连接到 nps, 连接被拒

我们通过执行如下命令
sudo cat /var/log/nps.log
查看 nps 的日志文件

img

可以看到,端口已被使用,需要换一下端口,改一下 nps 配置文件即可

read session unpack from connection

客户端出现如下错误

mux: read session unpack from connection err read tcp 198.18.0.1:50290-><ip>:8000: use of closed network connection

原因是客户端的配置文件出错
查看服务器日志可以发现如下,这里是 key 没有唯一,所以认证出错,网络被关闭.
改一下认证 key 即可

img

invalid syntax

客户端启动报错如下

2023/04/27 03:09:38.334 [E] [control.go:290]  strconv.Atoi: parsing "": invalid syntax
2023/04/27 03:09:38.334 [E] [control.go:310]  strconv.Atoi: parsing "": invalid syntax
2023/04/27 03:09:38.334 [E] [local.go:206]  strconv.Atoi: parsing "": invalid syntax
2023/04/27 03:09:38.335 [N] [local.go:142]  try to connect to the server 2
2023/04/27 03:09:38.334 [E] [control.go:290]  strconv.Atoi: parsing "": invalid syntax
2023/04/27 03:09:38.336 [E] [control.go:310]  strconv.Atoi: parsing "": invalid syntax
2023/04/27 03:09:38.336 [E] [client.go:126]  strconv.Atoi: parsing "": invalid syntax
2023/04/27 03:09:38.388 [E] [control.go:290]  strconv.Atoi: parsing "": invalid syntax

原因是配置文件错误,多余了或者少了,删除无用条目即可
(官方文件这里真的不人性化,给的配置过多了,不用得删)

NAT 类型不支持

如下,在解决好一切艰难险阻之后,仍然出问题了,访问端 client2 显示如下

img

maybe the nat type is not support p2p
说明我们的 NAT 类型不支持

Linux 查看 NAT 类型可以使用如下命令

bash
1
2
sudo apt-get install stun
stun stun.l.google.com:19302

我的显示如下,0x000012 类型就是 NAT4, 即 symmetric NAT, 很垃圾,不能 p2p 穿透

STUN client version 0.97
Primary: Independent Mapping, Independent Filter, random port, no hairpin	
Return value is 0x000012

为什么 NAT4 不支持 p2p?
因为 NAT4 每次建立新的连接时 IP 和端口都会变化,而 p2p 建立连接之前是通过 server 通信,server 会告诉客户端另一个客户端的地址.
而建立 p2p 连接时,此时就又算另一个连接了,如果是 NAT4, 新建立 p2p 连接时地址和端口就对不上之前 server 告诉的地址和端口,于是失败


参考文章
https://ehang-io.github.io/nps/
https://hub.docker.com/r/ffdfgdfg/npc
https://info.support.huawei.com/info-finder/encyclopedia/zh/NAT.html