N2N虚拟局域网

手把手教你构建异地局域网

AIO网络

背景

在构建完AIO家庭服务器后,如何在异地访问家庭服务器就成了接下来需要解决的问题,这时候N2N就进入了我的视野。

N2N虚拟局域网不但能够像DDNS一样暴露局域网中的服务(原理不太一样),还能够连接异地不同局域网组成一个超级局域网,这很满足我的需求。

但是,组建N2N虚拟局域网需要一个中心服务器,刚好手上有个ECS服务器,这对我来说完全不是问题。

说干就干,Let’s Do It。

软件安装

我最初接触N2N时,还是1.x向2.0转换的过程中,现在N2N已经发展到了3.1,4.0也在规划中了,有兴趣的可以上N2N的github看看。

我们将采用N2N 3.0,但某些操作系统(如Ubuntu)仓库中的N2N版本太老,所以我们需要从github下载最新版本,如果没有对应的二进制文件,就需要从源码编译(如macOS)。

Ubuntu

直接从github下载二进制文件。目前稳定版本是3.0.0,直接从Release页面下载即可。ECS服务器可通过如下命令下载和安装

1
2
3
wget https://github.com/ntop/n2n/releases/download/3.0/n2n_3.0.0-1038_amd64.deb 

sudo dpkg -i n2n_3.0.0-1038_amd64.deb

OpenWRT

由于我使用的OpenWRT版本较新,仓库内存版本已经更新到3.0.0,可以直接使用。如果没有,可能就需要想其他办法了。

1
2
3
4
opkg install n2n luci-app-n2n

# 可能有用的调试工具
opkg install kmod-ipt-nat n2n luci-app-n2n dmesg iptables-nft net-tools-route bind-dig diffutils

macOS

目前官方并没有放出macOS的二进制文件,只能从源码编译了。由于N2N使用tun/tap来构建虚拟网卡,所以需要先安装一些依赖。

1
2
3
# 请先安装homebrew 
brew install --cask tunnelblick
brew install make gcc
1
2
3
4
# 下载源码
git clone https://github.com/ntop/n2n.git 
cd n2n
./configure && make && make install 

没有条件的可以直接下载我已经编译好的文件:edge_macOS.zip

驱动程序:tap_tun_kext.zip 或者 tunnelblick_tap_tun_kext.zip

选择其中一个,解压后将文件拖上/Library/Extensions,中间可能提示授权,允许即可。

1
2
3
4
5
6
7
# 安装
sudo kextload /Library/Extensions/tap.kext
sudo kextload /Library/Extensions/tun.kext

# 卸载
sudo kextunload /Library/Extensions/tap.kext
sudo kextunload /Library/Extensions/tun.kext

中心节点配置

我们的ECS节点有公网IP,这个很重要,这能够保证我们在任何地方访问ECS服务器,在加上N2N的辅助,这样就可以访问家庭服务器。

在服务端,我们不但要开启N2N server,同时也需要开启一个N2N edge node作为网关使用,提供网络加速服务。

N2N服务器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sudo vim /etc/n2n/supernode.conf 

# 侦听端口,同时在ECS控制台打开该端口的入口流量/UDP
-p=7777
# 关闭MAC地址欺骗保护。
# 至少调试阶段需要关闭这个选项,不然你刚断开再连接就会失败。
-M

sudo systemctl enable supernode.service
sudo systemctl start supernode.service

N2N网关

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
sudo vim /etc/n2n/edge.conf

# 网卡名称
-d=n2n0
# 网络名称与密码
-c=test
-k=123456
# 使用静态IP&MAC更安全,也更方便
-m=02:xx:xx:xx:xx:xx
-a=static:10.20.30.1/24
# 服务器地址与端口
-l=xxxxxxxx:xxxx
# 定义MTU,这个很重要
-M=1390
# 网关数据转发
-r
# 添加路由表
-n=10.10.10.0/24:10.20.30.254

sudo systemctl enable edge.service
sudo systemctl start edge.service

配置NAT规则

1
2
3
4
5
6
7
8
sudo iptables -A FORWARD -i n2n0 -j ACCEPT
sudo iptables -A FORWARD -o n2n0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# MASQUERADE
sudo iptables -t nat -A POSTROUTING -s 10.20.30.0/24 -j MASQUERADE

# 如果有静态IP,可使用SNAT代替MASQUERADE,性能更好
sudo iptables -t nat -A POSTROUTING -s 10.20.30.0/24 -o -j SNAT --to-source <ip>

更多可参考官方文档

内网网关配置

由于我的家庭服务器使用OpenWRT作为主路由,所以直接将N2N edge安装到OpenWRT上,这样可以省去为每个设备设置N2N网络的麻烦。

2023-05-18-18-07-17@2x

  1. 配置网卡名称
  2. 配置静态IP和MAC地址
  3. 配置中心节点地址
  4. 配置网络名称与密码
  5. 网关一定要启用数据包转发
  6. 启用IP动态伪装(MASQUERADE)方便服务器上管理

防火墙配置

新建interface(接口) N2N,指定n2n0为其端口,同时将N2N添加到firewall的lan。而其他配置,基本上就是什么都不要,只要firewall把其当成lan口处理就行。

不要担心n2n0不会得到配置,由于n2n服务晚于network启动,所以当n2n启动时,n2n0的ip、route都会配置好。

OpenWrt上的n2n提供了一个参数 - IP动态伪装,为n2n0出口数据做了Masquerade转发,对应以下iptables rule:

定义入口流量NAT规则, 网络 - 防火墙 - NAT规则:

外网主机/macOS配置

使用刚刚编译好的软件,其实一条命令就可以连接整个N2N网络:

1
edge -c <name> -k <key> -a static:10.20.30.99/24 -l <服务器> -M 1390

然后设置路由表将需要的流量导入N2N网络即可。但这样多少有些不方便,写个小脚本吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
vim n2n.sh 

#!/bin/bash
ip=${1:-10.20.30.99/24}
net=${2:-10.10.10.0/24}
gw=${3:-10.20.30.254}

exec 2>&1
exec 1>/tmp/edge.log

sudo ps aux | grep edge > /dev/null && sudo pkill edge
sudo route -n delete -net $net $gw > /dev/null 2>&1

sudo edge -c <网络> -k <密码> -a static:$ip -l <服务器> -M 1390

# fix for -n for macOS
sleep 1
sudo route -n add -net $net $gw > /dev/null 2>&1

备注:macOS上edge -n 配置路由表存在bug,不生效!

配置路由表

在所有N2N edge(OpenWRT路由器除外)上配置内网路由表

1
2
3
4
5
# add route with 'ip'
sudo ip route add 10.10.10.0/24 via 10.20.30.1 dev n2n0

# macOS
sudo route -n add -net 10.10.10.0/24 10.20.30.1

测试

  • N2N服务器
1
2
3
4
ping 10.20.30.254   # 测试内网路由器的n2n0接口 - 检查内网路由器的N2N设置
ping 10.10.10.1     # 测试内网路由器的物理接口 - 检查路由器NAT规则
ping 10.10.10.99    # 测试内网主机(未运行N2N) - 检查路由器NAT规则,可尝试在路由器上ping进行对比
ping 10.20.30.99    # 测试外网主机(运行N2N) - (外网不受控,很可能ping不通)
  • OpenWRT路由器
1
2
3
4
5
6
7
8
9
ping 10.20.30.1     # 测试N2N服务器 - 检查本地和服务器上N2N的设置
ping 10.20.30.99    # 测试外网主机 - (外网不受控,很可能ping不通)

# 找个ping得通的公共ip,将其添加到路由表中
ip route add 106.11.253.86/32 via 10.20.30.1 dev n2n0
traceroute 106.11.253.86 # 测试路由是否设置成功

ping 106.11.253.86  # 测试N2N服务器网关 - 检查服务器的NAT规则
ip route del 106.11.253.86
  • 内网主机 - 未运行N2N
1
2
3
ping 10.20.30.254   # 测试内网路由器 - 检查路由器NAT规则
ping 10.20.30.1     # 测试N2N服务器 - 检查路由器的路由表
ping 10.20.30.99    # 测试外网主机 - (外网不受控,很可能ping不通)
  • 外网主机 - 运行N2N
1
2
3
4
ping 10.20.30.1     # 测试N2N服务器 - 检查本地N2N设置
ping 10.20.30.254   # 测试内网路由器的n2n0
ping 10.10.10.1     # 测试内网路由器的物理接口
ping 10.10.10.99    # 测试内网主机

如何设置适当的MTU值

由于N2N需要对IP数据包进行封装,所以必然有额外的开销,这导致默认的MTU=1500并不适用于N2N网卡。这里将讲述如何设置MTU。

实际链路MTU一般为1500或1492,为了保证我们的N2N网络正常运行,N2N MTU <= 1500/1492 - N2N报文头大小。

测试物理链路MTU:

1
2
3
4
5
6
7
# 禁用IP分片,并带数据ping服务器
ping -M do -s 1472 <服务器地址>  # 1500 - 28

ping: local error: message too long, mtu=1492
# 这里的MTU值直接检测出来了,但不一定可靠,继续减少数据大小,直到能够ping通服务器

# MTU = ping携带的数据大小 + 28(ping报文头大小)

本地测试ping最多能够发送1464+28字节数据,所以MTU=1492。

测试N2N链路MTU:

1
2
3
4
5
# 前提: 将所有N2N节点MTU设置为1500或更大

# 禁用IP分片,并带数据ping N2N服务器网关
ping -M do -s 1464 10.20.30.1   # 1492 - 28
# 持续减少数据大小,直到能够ping通服务器

本地测试ping最多能够发送1368+28字节数据,所以MTU=1396,N2N报文头大小为96。

哪些MAC地址可以安全的使用

有私有IP,自然也有私有MAC地址,可以放心使用:

1
2
3
4
x2-xx-xx-xx-xx-xx
x6-xx-xx-xx-xx-xx
xA-xx-xx-xx-xx-xx
xE-xx-xx-xx-xx-xx

N2N服务器网关无响应问题

在配置路由表的过程中,我们发现部分网站或部分终端无法通过N2N服务器网关,通过tcpdump发现N2N服务器上出现Destination unreachable (Fragmentation needed)的错误:

bad 通过分析发现:

  1. 上游网络发来巨型帧(2882),这显然无法通过我们的N2N网络,所以需要分片。但服务器没有分片,而是给上游网络发送ICMP告知其下一跳MTU为1390。

  2. 上游网络收到ICMP将数据分片重发,但是重发的数据仍然大于MTU,这样就限入死循环了。

好吧,没办法,找个正常的情况对比一下:

2023-05-18-14-38-05@2x

正常情况下,MSS = MTU - 40。但是不管我们设置MTU为多少,TCP SYN中MSS值始终为1460,也就是MTU被当成了1500,而实际MSS为1390 - 40 = 1350。这样,返回的报文自然无法在服务器上自动完成分片操作,所以连接就无响应。

进一步测试发现,使用不同的软件,该MSS值也将不同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 在N2N服务器网关上打开tcpdump
sudo tcpdump -i n2n0 

# 在本地进行测试
tcping 10.20.30.1

# MSS = 1350 ==> 正确
19:37:20.249736 IP 10.20.30.254.42978 > ECS.http: Flags [S], seq 3264860104, win 64800, options [mss 1350,sackOK,TS val 4094004489 ecr 0,nop,wscale 7], length 0

# 在本地测试
wget https://api.github.com/meta

# MSS = 1460 ==> 错误
19:38:06.520800 IP 10.20.30.254.32774 > 20.205.243.168.https: Flags [S], seq 3752455363, win 64240, options [mss 1460,sackOK,TS val 3038086888 ecr 0,nop,wscale 7], length 0

好吧,经过3天的努力,问题终于找到了,但更多的问题也来了。这个MSS到底什么情况,怎么还能变化?头都要爆炸了!!!

通过无尽的探索,终于让我们发现了解决方案:

解决方案1: 修改advmss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 获取网卡特性
ethtool -k eth0

# 关闭物理网卡的tso/gso/gro
#  tso - tcp-segmentation-offload
#  gso - generic-segmentation-offload
#  gro - generic-receive-offload
ethtool -k eth0 tso off
...

# 在本地和N2N服务器同时设置advmss
# 服务器
ip route change 10.20.30.0/24 dev n2n0 proto kernel scope link src 10.20.30.1 advmss 1350
# 本地
ip route change 10.20.30.0/24 dev n2n0 proto kernel scope link src 10.20.30.254 advmss 1350

==> 此方法未经验证,修改网卡属性影响实在太大,特别是在服务器上。

==> 不修改网卡属性(关闭tso/gso/gro),advmss是不会生效的。

本质问题:现在市面上多数网卡(包括虚拟网卡)都支持自动分片,但我们N2N使用的虚拟网卡却不支持,所以上游网络发来的巨型帧是无法通过我们的N2N网络,除非在入口网卡或内核中完成IP分片。

解决方案2:强写MSS值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 使用 --clamp-mss-to-pmtu
sudo iptables -I FORWARD -i n2n0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
# 使用 --set-mss
sudo iptables -I FORWARD -i n2n0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1350

# 修改前:
20:01:19.942146 IP ECS.54044 > 20.205.243.168.https: Flags [S], seq 951446441, win 64240, options [mss 1460,sackOK,TS val 3039480309 ecr 0,nop,wscale 7], length 0

# 修改后
20:03:16.621032 IP ECS.60406 > 20.205.243.168.https: Flags [S], seq 2888455217, win 64240, options [mss 1350,sackOK,TS val 3039596987 ecr 0,nop,wscale 7], length 0

OpenWRT/N2N的改进

OpenWRT上N2N网络的启动脚本/etc/init.d/n2n还是有些小问题:

  1. 不支持将host ip添加到路由表
  2. 不支持MAC地址设置
  3. IP转发默认为MASQUARADE,不支持SNAT,太不智能了
  4. 服务器地址未加入路由表中,这导致n2n0无法作为默认网关使用,也容易导致问题。
  5. 启动时间太晚,导致某些服务无法使用N2N网络初始化
  6. 重启n2n会让路由丢失 [update 2023-05-22]

这是修改过的版本:n2n-fixed.zip

Updated 2023-05-22:n2n-fixed.zip

生成随机密码

由于N2N网络一般不会随意改动,所以适合使用非常复杂的密码。

这里推荐一个生成随机密码的简单办法,只要软件支持,你可以生成一个最大长度的密码,让那些想破解的人直接闷逼。

1
2
3
openssl rand -base64 12 # 随机生成一个16字节的字符串

# ‘+’在N2N中是通配符,如果生成的结果包含‘+’,重新生成一个

==> N2N报文头加密支持19字节,而加密算法支持128/192/256 bits,也就是16/24/32字节。所以,我们选择一个16-19字节的字符串作为网络名称与密码最合适

结束语

因为MSS的事情,做这个网络,前前后后花了3-4天的时间,我甚至给阿里云的友人们提了几个工单,这里特别感谢在后台为我提供技术支持的友人们。

参考

  1. https://github.com/ntop/n2n
  2. https://blog.51cto.com/fengjicheng/5177620

小酌怡情
Built with Hugo
主题 StackJimmy 设计
访问量 -    访客数 - 人次