本文介绍

  • 内网穿透原理
  • 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

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的方式

1
docker pull ffdfgdfg/npc

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

Linux平台

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目录编辑配置

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)
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: 记得开端口, 既要在服务器防火墙上设置开启, 又要在服务器的供应商那里开放安全组

服务端直接执行如下启动

1
sudo nps start

然后执行

1
sudo cat /var/log/nps.log

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

img

搭建隧道

CS模式

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

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

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

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去
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目录为刚刚创建的配置目录)

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

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

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

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

img

然后在访问端client2执行如下

1
ssh -p <PORT> username@IP

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

如下, 访问成功

img

p2p模式

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

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

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

1
2
mkdir conf
vim npc.conf

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

其中注意

  • server_addr是自己服务器server的访问IP和端口, 比如我的是8000
  • vkey要修改成一个独立不重复的, 这是客户端的key
  • p2p的password也要修改成独立不重复的, 自定义
  • target为客户端client1上你要访问的目的端口, 一般而言就是localhost:22表示ssh连接
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)

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目录为刚刚创建的配置目录)

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

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

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

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

img

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

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执行如下命令

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类型可以使用如下命令

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