跳转至

networking

生成树协议

本文的内容已经整合到知识库中。

Spanning Tree Protocol

STP(Spanning Tree Protocol)可以在 802.1D-1998 第 8 章中找到。STP 协议工作在交换机上,需要根据交换机连接的拓扑,自动计算出一个生成树,并且把不在生成树上的边禁用,这样即使连接的拓扑有环路,禁用以后就没有环了。有了 STP 以后,连接交换机的时候就可以刻意连成环,从而提供冗余。

软硬件队列接口

背景

在网卡等场景下,经常会需要在软硬件之间传输大量的数据,通常的方法是建立循环队列,例如 H2C(Host to Chip)方向,是 Host 作为 Producer 增加数据到队尾,Chip 作为 Consumer 从队头读取数据。由于每次传输的数据不定长,为了方便,队列的项是一个定长的 Descriptor,Descriptor 指向了数据的地址。但具体的细节,不同的实现还不太一样。下面逐个案例进行分析。

Transport Layer Interface 考古

Transport Layer Interface

现在网络编程主要采用的是 BSD Sockets API,但实际上当年还有另一套 API,就是 TLI(Transport Layer Interface),后来 BSD Sockets 胜出,进入了 POSIX 标准,TLI 后面也标准化为了 XTI,现在可以在部分 Unix 系统中找到。TLI/XTI 的使用方法和 Sockets API 有些类似,但是比较特别的一点在于,Sockets API 第一步是 socket 调用,传的参数就决定了这是 TCP 还是 UDP 还是其他什么协议,而 TLI 是通过打开不同的设备文件来进行区分:

int fd = t_open("/dev/udp", O_RDWR, NULL);

比如 TCP 就是 /dev/tcp,UDP 就是 /dev/udp,同理还有 /dev/icmp 等等。这颇有 Unix 的哲学:everything is a file。而 BSD Sockets API 则是有对应的系统调用,libc 基本不需要做什么事情。

升级 Linksys E8450 的 OpenWRT 系统到 UBI

背景

OpenWRT Linksys E8450 页面 中,如果要用新版的固件,需要转换到 UBI 格式的文件系统。之前用的是 non-UBI 格式的文件系统,直接在官方的分区下,覆盖掉其中一个启动分区。但是经常会报告 flash 出错,然后系统也不稳定,决定要按照文档更新到 UBI。

步骤

请注意:更换文件系统操作比较危险,请先备份好数据,并做好变砖的心理准备。本文仅记录了作者编写时可行的更新操作,不代表读者在阅读时,依然可以按照这个顺序进行,请按照 https://github.com/dangowrt/owrt-ubi-installer 的文档进行操作。

基本按照文档一步一步走。初始状态是一个 non-UBI 版本的 OpenWRT 固件:

  1. 下载官方的 1.0 固件:https://downloads.linksys.com/support/assets/firmware/FW_E8450_1.0.01.101415_prod.img
  2. 在 luci 中,刷入官方 1.0 固件,这时候进入了官方固件的系统
  3. 登录官方固件网页,恢复出厂设置
  4. 下载 openwrt ubi recovery 固件 然后在官方固件里刷入
  5. 这时候进入了 recovery 固件,下载 ubi 固件,继续在网页里刷入
  6. 这时候固件就更新完成了。ssh root@192.168.1.1,然后进去安装 luci 等软件,恢复配置即可。

ESXi 网络配置

用过 ESXi 的大家都知道,它网页版对网络的配置功能有限,特别是 IPv6 的部分,有的事情无法实现。更好的办法是 SSH 到 ESXi 上直接用命令行进行配置。

可能会用到的一些命令:

  1. esxcfg-vmknic: 用来给 vmkernel 配置地址
  2. esxcfg-route: 设置系统路由表
  3. esxcli: 大杂烩,很多功能都在里面
  4. tcpdump-uw:魔改版 tcpdump

一些例子:

设置 IPv6 默认路由:

[root@esxi:~]esxcfg-route -f V6 -a default $IPV6

删除 vmkernel 的 IPv6 地址:

[root@esxi:~]esxcli network ip interface ipv6 address remove -i $VMKERNEL -I $IPV6/$PREFIX

参考:https://kb.vmware.com/s/article/1002662

IP 和 UDP Checksum 的增量更新问题

之前在写 IP Checksum 的增量更新,就是当 TTL -= 1 的时候,Checksum 应该增加 0x0100,但是这样会有问题,在于,如果按照原来的 IP Checksum 计算方法,是不会出现 0xFFFF 的(求和,进位,然后取反写入),这种加法就有可能出现 0xFFFF。于是翻阅了相关的 RFC:

首先是 RFC 1141 相关部分:

unsigned long sum;
ipptr->ttl--;                  /* decrement ttl */
sum = ipptr->Checksum + 0x100;  /* increment checksum high byte*/
ipptr->Checksum = (sum + (sum>>16)) /* add carry */

这也是比较直接能想到的一种方法,但是会出现刚才提到的问题。于是 RFC 1624 纠正了这个问题:

  Although this equation appears to work, there are boundary conditions
   under which it produces a result which differs from the one obtained
   by checksum computation from scratch.  This is due to the way zero is
   handled in one's complement arithmetic.

   In one's complement, there are two representations of zero: the all
   zero and the all one bit values, often referred to as +0 and -0.
   One's complement addition of non-zero inputs can produce -0 as a
   result, but never +0.  Since there is guaranteed to be at least one



Rijsinghani                                                     [Page 2]

RFC 1624             Incremental Internet Checksum              May 1994


   non-zero field in the IP header, and the checksum field in the
   protocol header is the complement of the sum, the checksum field can
   never contain ~(+0), which is -0 (0xFFFF).  It can, however, contain
   ~(-0), which is +0 (0x0000).

   RFC 1141 yields an updated header checksum of -0 when it should be
   +0.  This is because it assumed that one's complement has a
   distributive property, which does not hold when the result is 0 (see
   derivation of [Eqn. 2]).

   The problem is avoided by not assuming this property.  The correct
   equation is given below:

          HC' = ~(C + (-m) + m')    --    [Eqn. 3]
              = ~(~HC + ~m + m')

只要把代码简单修改一下就可以了,或者遇到 0xFFFF 时设为 0,这时候就解决了这个问题。

但是,仔细研究了一下发现,UDP Checksum 又是这么定义的(RFC 768):

If the computed  checksum  is zero,  it is transmitted  as all ones (the
equivalent  in one's complement  arithmetic).   An all zero  transmitted
checksum  value means that the transmitter  generated  no checksum  (for
debugging or for higher level protocols that don't care).

刚好和 IP Checksum 相反,这就很有意思了。

OpenWRT 上配置 Gandi DDNS

一直想给自己的 OpenWRT 路由器添加 DDNS 功能,但 Gandi 不在官方的 ddns-scripts 列表中,自己在网上找了一些脚本,发现是 Python 写的,尝试把 Python 安装到路由器上又发现空间不够,虽然可以安装到 USB 上,但总归是麻烦。

最后找到了官方的一个脚本,非常适合我的需求。简单修改一下,然后安装一下支持 HTTPS 的 cURL:

$ opkg update
$ opkg install ca-bundle
$ opkg install curl

然后把脚本添加到 crontab 即可。

ETC 比赛无线网络搭建小记

正好上着李贺武老师的《无线移动网络技术》课,然后今天又给 ETC 比赛搭建无线网络,于是周二的时候找老师咨询了一下意见。我们大概给老师讲了一下场地的样子和尺寸(当时估计的大了),然后老师给我们提供了一个可以供参考的部署方案,包括几个 AP 分别放在哪,这些 AP 的 2.4GHz 都用哪个 channel,然后都用多少功率。并且很友善地在周五的时候让助教来场地帮我们 survey 一下无线网络的状况。我们用 Fluke Networks 的 AirCheck Wi-Fi Tester 看了一下场地的各个频道的 AP 数量和信号强度,发现虽然 AP 挺多的,但是它们的信号都比我们的小不少,而且我们也都开到了 50mW,所以干扰不大。

然后昨天下午一点半钟来到场地,用板车把设备都运到计算机开放实验室,然后开始部署无线网络。我们提前来踩过几次点,这边的网络是没有 DHCP 服务器的,通过一个指定的网关出去。我们发现可以利用已有的这些交换机从地下连到各个电脑上的网线,来连接我们的 AP 和交换机,这样我们就免去了走线的麻烦。于是我们先定下放 AP 的位置,然后用寻线器找到网线插到了交换机的哪一个端口上,标记好后换成连接到我们自己的交换机的网线上,从而可以通过 PoE 把我们的 AP 给启动起来。然后用我自己的路由器,把 LAN 口插到交换机上,给 AP 分发 DHCP 地址,然后把下发的默认网关配置为真实的网关(DHCP Option 3) ,没做但是也可以顺带做的是把 WLC 的地址分发下去(DHCP Option 43 Type 241)。

当然,这个过程也遇到了一点小坑,就是交换机还保留了之前的配置,所以这次把新的几个端口划到了一个 VLAN 下,命令自然是记不住的要现查。然后起来以后还挺稳定的,也没出现什么问题,网络带宽也足够用,离千兆还有蛮多距离,一天也才跑了接近俩 TB 的流量。

使用 veth 实现 IPv6-only 的 Brouter 功能

最近从 @shankerwangmiao 学到了一个方法:通过 veth 把两个 bridge 的 IPv6 桥接起来。方法如下:

$ ip link add veth-v6-in type veth peer name veth-v6-out
$ brctl addif br-in veth-v6-in
$ brctl addif br-out veth-v6-out
$ ebtables -t filter -A FORWARD -p ! IPv6 -o veth-v6-in -j DROP
$ ebtables -t filter -A FORWARD -p ! IPv6 -o veth-v6-out -j DROP

这样就可以看到 veth 上仅有 IPv6 的流量了。

在 Android 上打开 LTE 的 IPv6

听闻北京移动给 LTE 配置了 SLAAC,但现在需要手动打开,方法如下:

Settings -> Network & Internet -> Mobile Network -> Advanced -> Access Point Names -> 中国移动 GPRS (China Mobile) -> 把 APN procotol 和 APN roaming protocol 两项都改成 IPv4/IPv6

然后在 test-ipv6.com 上可以看到确实分配了 IPv6 地址,不过目前评分只有 1/10。也就是说可用性还不佳。

而在 iOS 上,通过 HE 的 Network Tools 能看到,确实拿到了 IPv6 的地址,但是出不去,怀疑是运营商没有下发相关配置,所以还不能使用,只能继续等。

2018-11-06 更新:现在 iOS 用户也有 LTE 的 v6 了。评分是 9/10。目前可用性已经可以了,就是国内互联还不大好。

在 LEDE(OpenWrt)上启用 wpad

WPAD(Web Proxy Auto-Discovery Protocol)是一个可以利用 dhcp 分发 pac 配置的协议。方法如下:

$ # ssh to router first
$ vim /etc/dnsmasq.conf
dhcp-option=252,"http://router_ip/wpad.dat"
$ vim /www/wpad.dat # put pac here
$ service dnsmasq restart
$ # ensure proxy is available to lan
$ # enable wpad on devices

参考文档:

  1. Web Proxy Auto-Discovery Protocol
  2. Automatic Proxy Configuration with WPAD
  3. Deployment Options
  4. Example PAC File

通过 Ipfilter Extension 实现 RFC8367

前几天无聊闲逛看到了一个很有趣的 RFC8367 - Wrongful Termination of Internet Protocol (IP) Packets ,看到日期大家应该都懂了,这是个粥客,不过里面还是反映了一些事情,咳。

之前看到闪客实现了 shankerwangmiao/xt_PROTO ,想到自己也可以做一个 iptables 扩展,于是就写了 jiegec/xt_EQUALIZE 。它是这样使用的:

$ git clone git@github.com:jiegec/xt_EQUALIZE.git
$ make
$ sudo make install
$ sudo iptables -t filter -A INPUT -j EQUALIZE
$ sudo dmesg -w &
$ # Make some random network requests to see the effect!
$ ping 1.1.1.1
$ ping 8.8.8.8
$ ping ::1

目前还没有把参数都变成可以配置的。如果真的有人需要这个模块的话,我再改吧(逃

在 macOS 上 TAP Interface 上启用 IPv6 自动配置

由于 macOS 对 TAP Interface 不会自动出现一个设置中对应的服务,所以需要手动进行配置。一番测试后,发现可以通过:

$ sudo ipconfig set [tap_if] automatic-v6
$ sudo ipconfig set [tap_if] dhcp

启用系统自带的 dhcp 和 ra 功能。也许有方法可以把这些 tap 搬到系统的设置中去。

UPDATE:

可以把 TAP Interface 加到系统的设置中去。方法参考Virtual network interface in Mac OS X。完成以后可以直接通过系统设置界面进行配置。

在 macOS 下实现 GRETAP

由于没有找到 macOS 下现成的 GRETAP 实现,我就想到自己实现一个。由于tuntaposx提供了一个和 Linux 下基本一样的 TAP Interface,于是自己利用 raw socket 和 TAP Interface 实现了一下,主要方法:

  1. 打开 raw socket,读取收到的 proto 47 的包,判断是否为 GRETAP 包,是,则写入内层包到打开的 TAP Interface 中。
  2. 从 TAP Interface 中读入包,自己加上 GRE 头和 IP 头,然后发送。

主要的难度是在 raw socket 部分,macOS 继承了 BSD,与 Linux 不大一样。于是参考了SOCK_RAW Demystified,成功地实现了这个功能。

代码放在jiegec/gretapmac。写得并不高效,仅仅可用,用了一百多行。

UPDATE: 之后又随手实现了一个类似的协议,L2TPv3 over UDP。代码在jiegc/l2tpv3udptap

在 WireGuard 构建的 Overlay Network 上跑 babel 路由协议

Run Babeld over Wireguard - Fugoes's BlogRoute-based VPN on Linux with WireGuard 启发,自己也想尝试一下,在一个有多个结点的网络中,如何通过 WireGuard 构建一个 overlay network,并通过 babel 自动进行结点发现和路径选择。

首先建立点对点的 WireGuard Tunnel。由于我们用 babel 进行路由,所以我们不能采用 Wiregurad 本身基于目的地址的端口复用,所以每一个 WireGuard interface 都只有一个 Peer。

配置一个点对点的 WireGuard Tunnel:

$ # for wg-quick
$ cat wg0.conf
[Interface]
Address = IPV4/32, fe80::ID/64
PrivateKey = REDACTED
ListenPort = PORT1
Table = off # ask wg-quick not to insert peer address into routing table

[Peer]
PublicKey = REDACTED
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = REDACTED:PORT2

这里的 IPV4 和 ID 在同一设备上的不同 WireGuard Tunnel 上相同。只是通过 wg interface 编号来区分。

接着配置 babeld:

$ cat babeld.conf

router-id ID
local-port 33123 # for babelweb2

# one line for each wg interface
interface wg0 type tunnel rtt-max 512

redistribute ip PREFIX/LEN ge LEN le 32 local allow # tunnel neighbors
redistribute proto 42 # routes installed by babeld
redistribute local deny
# consult babeld man page for more

然后通过 BabelWeb2(很难用)进行可视化,然后通过手动触发一些网络波动即可达到效果。

向 Lenovo y1s 刷入 OpenWRT 17.01.5 固件,并把 IPv6 bridge 到内网中和配置认证脚本

首先参照OpenWRT Wiki - Lenovo Y1 v1找到刷固件教程:

  1. 下载Lenovo y1s 的固件备用
  2. 断开电源,等待一段时间,插入电源同时快速按下重置按钮,如果面板双闪,则说明进入了恢复模式
  3. 电脑连接到四个 LAN 口中任意一个,配置静态地址在 192.168.1.0/24 网段
  4. 打开 192.168.1.1 可以看到刷固件的页面
  5. 上传固件,等待路由器重启
  6. 配置 IP 地址为 DHCP 模式,打开 192.168.1.1 进行配置

然后就是常规的密码设置,opkg 源设置为 tuna 的源,配置 ssh 和 公钥。

接下来,我们为了使用学校的 SLAAC,采用 ebtables 直接把学校的 IPv6 bridge 进来,而 IPv4 由于准入系统,需要 NAT。

参考 Bridge IPv6 connections to WAN,下载 v6brouter_openwrt.sh 到某个地方,然后修改一下里面的一些参数:

# For Lenovo y1s
WAN_DEV=eth0.2
BRIDGE=br-lan
# the rest remain unchanged

然后跑起来之后,自己的电脑可以成功拿到原生的 IPv6 地址了,不需要用难用的 NAT66 技术。

下一步是采用z4yx/GoAuthing

$ go get -u -v github.com/z4yx/GoAuthing
$ cd $GOPATH/src/github.com/z4yx/GoAuthing/cli
$ env GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build main.go
$ mipsel-linux-gnu-strip main
$ scp main root@192.168.1.1:~/GoAuthing
$ ssh root@192.168.1.1
$ opkg install ca-certificates
$ ./GoAuthing

这里参考了解决 GO 语言编译程序在 openwrt(mipsle 架构) 上运行提示 Illegal instruction 问题,配置了 GOMIPS 环境变量。为了访问 HTTPS 网站,参考了OpenWRT Wiki - SSL and Certificates in wget。有毒的是,这个环境变量,在 macOS 上不能正常工作,而在 Linux 机子上是没有问题的。

然后就可以成功地跑起来 GoAuthing,解决了上校园网认证的问题。

感谢宇翔编写的 GoAuthing 小工具。

更新:简化了一下 v6brouter 脚本:

#!/bin/sh
BRIDGE=br-lan
WAN_DEV=$(/sbin/uci get network.wan.ifname)
WHITELIST1="00:11:22:33:44:55"
WHITELIST2="55:44:33:22:11:00"

brctl addbr $BRIDGE 2> /dev/null
brctl addif $BRIDGE $WAN_DEV
ip link set $BRIDGE down
ip link set $BRIDGE up
brctl show

ebtables -F
ebtables -P FORWARD ACCEPT
ebtables -L

uci set dhcp.lan.ra='disabled'
uci set dhcp.lan.dhcpv6='disabled'
uci commit
/etc/init.d/odhcpd restart

echo 2 > /proc/sys/net/ipv6/conf/$BRIDGE/accept_ra
ebtables -t broute -F
ebtables -t broute -A BROUTING -i $WAN_DEV -p ! ipv6 -j DROP
ebtables -t broute -A BROUTING -s $WHITELIST1 -p ipv6 -j ACCEPT
ebtables -t broute -A BROUTING -d $WHITELIST1 -p ipv6 -j ACCEPT
ebtables -t broute -A BROUTING -s $WHITELIST2 -p ipv6 -j ACCEPT
ebtables -t broute -A BROUTING -d $WHITELIST2 -p ipv6 -j ACCEPT
ebtables -t broute -A BROUTING -p ipv6 -j DROP
ebtables -t broute -L

注意,这里添加了两个 WHITELIST 的 MAC 地址,表示只让这两个 MAC 地址的设备访问 v6。一般来说,外面网关的 MAC 地址也要放进来,不然可能接收不到 RA。如果不需要白名单的话,可以去掉 ebtables 的后几行规则。

通过 iptables 在同一个端口根据源地址解复用(demux)

现在遇到一个场景,原来的一个服务只给一个客户端用,但现在增加了一个客户端,由于客户端配置相同,但是服务端需要区别对待两个客户端的服务端配置,所以利用 iptables 根据源地址做了一个端口转发,实现了 demux。

假设:服务器在 192.168.0.1,客户端分别在 192.168.0.2 和 192.168.0.3。客户端配置的服务端地址是 192.168.0.1:8000。之前,在服务器上只跑了一个服务,监听着 8000 端口。

现在,在服务器上再跑一个服务,监听 8001 端口,同时根据需求进行相应的配置。然后,加上如下 iptables 规则:

$ sudo iptables -t nat -A PREROUTING -s 192.168.0.3 -d 192.168.0.1 -p tcp -m tcp --dport 8000 -j REDIRECT --to-ports 8001

这样,在不需要更改客户端的情况下,完成了需要的效果。

Wireguard 隧道搭建

随着 Wireguard Go 版本的开发,在 macOS 上起 WireGuard Tunnel 成为现实。于是,搭建了一个 macOS 和 Linux 之间的 WireGuard Tunnel。假设 Linux 端为服务端,macOS 端为客户端。

macOS 端:

$ brew install wireguard-tools
$ cd /usr/local/etc/wireguard
$ wg genkey > privatekey
$ wg pubkey < privatekey > publickey
$ vim tunnel.conf
[Interface]
PrivateKey = MACOS_PRIVATE_KEY

[Peer]
PublicKey = LINUX_PUBLIC_KEY # Generated below
AllowedIPs = 192.168.0.0/24
Endpoint = LINUX_PUBLIC_IP:12345
$ vim up.sh
#!/bin/bash
# change interface name when necessary
sudo wireguard-go utun0
sudo wg setconf utun0 tunnel.conf
sudo ifconfig utun0 192.168.0.2 192.168.0.1
$ chmod +x up.sh
$ ./up.sh

配置 Linux 端:

$ git clone https://git.zx2c4.com/WireGuard
$ make
$ sudo make install
$ sudo fish
$ cd /etc/wireguard
$ wg genkey > privatekey
$ wg pubkey < privatekey > publickey
$ vim wg0.conf
[Interface]
Address = 192.168.0.1/24
PrivateKey = LINUX_PRIVATE_KEY
ListenPort = 12345

[Peer]
PublicKey = MACOS_PUBLIC_KEY
AllowedIPs = 192.168.0.2/24
$ wg-quick up wg0

经过测试,两边可以互相 ping 通。

后续尝试在 Android 上跑通 WireGuard。

UPDATE 2018-07-11:

成功在 Android 上跑通 WireGuard。在 Google Play 上下载官方的 App 即可。麻烦在于,将 Android 上生成的 Public Key 和服务器的 Public Key 进行交换。

然后又看到WireGuard 在 systemd-networkd上的配置方案,自己也实践了一下。首先,如果用的是 stretch,请首先打开 stretch-backports 源并把 systemd 升级到 237 版本。

然后,根据上面这个连接进行配置,由于都是 ini 格式,基本就是复制粘贴就可以配置了。有一点要注意,就是,要保护 PrivateKey 的安全,注意配置 .netdev 文件的权限。

在 macOS 和 Linux 之间搭建 tinc 网络

一直听说 tinc 比较科学,所以尝试自己用 tinc 搭建一个网络。这里,macOS 这段没有固定 IP 地址,Linux 机器有固定 IP 地址 linux_ip。假设网络名称为 example , macOS 端名为 macos 地址为 192.168.0.2, linux 端名为 linux 地址为 192.168.0.1。

2018-11-11 注:本文用的 tinc 版本为 1.0.x,而不是 1.1-pre,两个分支命令不同,但协议可以兼容。

在 macOS 上配置:

brew install tinc
mkdir -p /usr/local/etc/tinc/example

新建 /usr/local/etc/tinc/example/tinc.conf:

Name = macos
Device = utun0 # use an unused number
ConnectTo = linux

编辑 /usr/local/etc/tinc/example/tinc-up:

#!/bin/sh
ifconfig $INTERFACE 192.168.0.2 192.168.0.1 mtu 1500 netmask 255.255.255.255

和 /usr/local/etc/tinc/example/tinc-down:

#!/bin/sh
ifconfig $INTERFACE down

还有 /usr/local/etc/tinc/example/subnet-up:

#!/bin/sh
[ "$NAME" = "$NODE" ] && exit 0
/usr/local/opt/iproute2mac/bin/ip route add $SUBNET dev $INTERFACE

以及 /usr/local/etc/tinc/example/subnet-down:

#!/bin/sh
[ "$NAME" = "$NODE" ] && exit 0
/usr/local/opt/iproute2mac/bin/ip route del $SUBNET dev $INTERFACE

然后将它们都设为可执行的:

chmod +x tinc-up
chmod +x tinc-down
chmod +x subnet-down
chmod +x subnet-down

编辑 /usr/local/etc/tinc/example/macos:

Port = 655
Subnet = 192.168.0.1/24

执行 tincd -n example -K 生成密钥。

到 Linux 机器上: 编辑以下文件:

$ mkdir -p /etc/tinc/example/hosts
$ cat /etc/tinc/example/tinc.conf
Name = linux
$ cat /etc/tinc/example/tinc-up
$!/bin/sh
ip link set $INTERFACE up
ip addr add 192.168.0.1/24 dev $INTERFACE
$ cat /etc/tinc/example/tinc-down
$!/bin/sh
ip addr del 192.168.0.1/24 dev $INTERFACE
ip link set $INTERFACE down
$ cat /etc/tinc/example/hosts/linux
Address = linux_ip
Port = 655
Subnet = 192.168.0.1/24
$ tincd -n example -K

接着,把 linux 上 /etc/tinc/example/hosts/linux 拷贝到 macos 的 /usr/local/etc/tinc/example/hosts/linux,然后把 macos 上 /usr/local/etc/tinc/example/hosts/macos 拷贝到 /etc/tinc/example/hosts/macos。在两台机器上都 tinc -n example -D -d3 即可看到连接的建立,通过 ping 即可验证网络建立成功。

2018-05-29 Update: Android 上,利用 Tinc GUI 也可以把 Tinc 运行起来,只是配置不大一样:

$ cat tinc.conf
Name = example
Device = /dev/tun
Mode = switch
ConnectTo = remote
ScriptsInterpreter = /system/bin/sh
$ cat tinc-up
#!/bin/sh
ip link set $INTERFACE up
ip addr add local_ip/24 dev $INTERFACE
$ cat tinc-down
#!/bin/sh
ip addr del local_ip/24 dev $INTERFACE
ip link set $INTERFACE down
$ cat subnet-up
$!/bin/bash
[ "$NAME" = "$NODE" ] && exit 0
ip route add $SUBNET dev $INTERFACE metric $WEIGHT table local
$ cat subnet-down
#!/bin/bash
[ "$NAME" = "$NODE" ] && exit 0
ip route del $SUBNET dev $INTERFACE table local

注意 table local 的使用。需要 Root。

搭建 FTP server behind NAT

我们出现新的需求,要把以前的 FTP 服务器迁移到 NAT 之后的一台机器上。但是,FTP 不仅用到 20 21 端口,PASV 还会用到高端口,这给端口转发带来了一些麻烦。我们一开始测试,直接在 Router 上转发 20 和 21 端口到 Server 上。但是很快发现,Filezilla 通过 PASV 获取到地址为(内网地址,端口高 8 位,端口低 8 位),然后,Filezilla 检测出这个地址是内网地址,于是转而向 router_ip:port 发包,这自然是不会得到结果的。

此时我们去网上找了找资料,找到了一个很粗暴的方法:

iptables -A PREROUTING -i external_interface -p tcp -m tcp --dport 20 -j DNAT --to-destination internal_ip:20
iptables -A PREROUTING -i external_interface -p tcp -m tcp --dport 21 -j DNAT --to-destination internal_ip:21
iptables -A PREROUTING -i external_interface -p tcp -m tcp --dport 1024:65535 -j DNAT --to-destination internal_ip:1024-65535

有趣地是,macOS 自带的 ftp 命令(High Sierra 似乎已经删去)可以正常使用。研究发现,它用 EPSV(Extended Passive Mode)代替 PASV,这里并没有写内网地址,因而可以正常使用。

这么做,Filezilla 可以成功访问了。但是,用其它客户端的时候,它会直连那个内网地址而不是 Router 的地址,于是还是连不上。而且,使用了 1024-65535 的所有端口,这个太浪费而且会影响我们其它的服务。

我们开始研究我们 FTP 服务器 (pyftpdlib) 的配置。果然,找到了适用于 FTP behind NAT 的相关配置:

     - (str) masquerade_address:
        the "masqueraded" IP address to provide along PASV reply when
        pyftpdlib is running behind a NAT or other types of gateways.
        When configured pyftpdlib will hide its local address and
        instead use the public address of your NAT (default None).
     - (dict) masquerade_address_map:
        in case the server has multiple IP addresses which are all
        behind a NAT router, you may wish to specify individual
        masquerade_addresses for each of them. The map expects a
        dictionary containing private IP addresses as keys, and their
        corresponding public (masquerade) addresses as values.
     - (list) passive_ports:
        what ports the ftpd will use for its passive data transfers.
        Value expected is a list of integers (e.g. range(60000, 65535)).
        When configured pyftpdlib will no longer use kernel-assigned
        random ports (default None).

于是,我们配置了 masquerade_address 使得 FTP 服务器会在 PASV 中返回 Router 的地址,并且在 passive_ports 中缩小了 pyftpdlib 使用的端口范围。

进行配置以后,我们在前述的 iptables 命令中相应修改了端口范围,现在工作一切正常。

使用 Nginx 转发 VMware ESXi

我们的 VMware ESXi 在一台 NAT Router 之后,但是我们希望通过域名可以直接访问 VMware ESXi。我们首先的尝试是,把 8443 转发到它的 443 端口,比如:

socat TCP-LISTEN:8443,reuseaddr,fork TCP:esxi_addr:443

它能工作地很好(假的,如果你把 8443 换成 9443 它就不工作了),但是,我们想要的是,直接通过 esxi.example.org 就可以访问它。于是,我们需要 Nginx 在其中做一个转发的功能。在这个过程中遇到了很多的坑,最后终于是做好了(VMware Remote Console 等功能还不行,需要继续研究)。

首先讲讲为啥把 8443 换成 9443 不能工作吧 -- 很简单,ESXi 的网页界面会请求 8443 端口。只是恰好我用 8443 转发到 443,所以可以正常工作。这个很迷,但是测试的结果确实如此。VMware Remote Console 还用到了别的端口,我还在研究之中。

来谈谈怎么配置这个 Nginx 转发吧。首先是 80 跳转 443:

server {
        listen 80;
        listen 8080;
        server_name esxi.example.org;

        return 301 https://$host$request_uri;
}

这个很简单,接下来是转发 443 端口:

server {
        listen 443 ssl;
        server_name esxi.example.org;
        ssl_certificate /path/to/ssl/cert.pem;
        ssl_certificate_key /path/to/ssl/key.pem;

        location / {
                proxy_pass https://esxi_addr;
                proxy_ssl_verify off;
                proxy_ssl_session_reuse on;
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
        }
}

此时,打开 https://esxi.example.org 就能看到登录界面了。但是仍然无法登录。从 DevTools 看错误,发现它请求了 8443 端口。于是进行转发:

server {
        listen 8443 ssl;
        server_name esxi.example.org;
        ssl_certificate /path/to/ssl/cert.pem;
        ssl_certificate_key /path/to/ssl/key.pem;


        location / {
                if ($request_method = 'OPTIONS') {
                        add_header 'Access-Control-Allow-Origin' 'https://esxi.example.org';
                        add_header 'Access-Control-Allow-Credentials' 'true';
                        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
                        add_header 'Access-Control-Max-Age' 1728000;
                        add_header 'Access-Control-Allow-Headers' 'VMware-CSRF-Token,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Cookie,SOAPAction';
                        add_header 'Content-Type' 'text/plain; charset=utf-8';
                        add_header 'Content-Length' 0;
                        return 204;
                }

                add_header 'Access-Control-Allow-Origin' 'https://esxi.example.org';
                add_header 'Access-Control-Allow-Credentials' 'true';
                proxy_pass https://esxi_addr:443;
                proxy_ssl_verify off;
                proxy_ssl_session_reuse on;
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
        }
}

主要麻烦的是配置 CORS 的相关策略。我也是看了 DevTools 的错误提示半天才慢慢写出来的。这样配置以后,就可以成功登录 VMware ESXi 了。

20:02 更新:现在做了 WebSocket 转发,目前可以在浏览器中打开 Web Console 了。但是,在访问 https://esxi.example.org/ 的时候还是会出现一些问题,然而 https://esxi.example.org:8443/ 是好的。

转发 WebSocket:

map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
}

server {
        listen 8443 ssl;
        server_name esxi.example.org;
        ssl_certificate /path/to/ssl/cert.pem;
        ssl_certificate_key /path/to/ssl/key.pem;


        location / {

                if ($request_method = 'OPTIONS') {
                        add_header 'Access-Control-Allow-Origin' 'https://esxi.example.org';
                        add_header 'Access-Control-Allow-Credentials' 'true';
                        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
                        add_header 'Access-Control-Max-Age' 1728000;
                        add_header 'Access-Control-Allow-Headers' 'VMware-CSRF-Token,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Cookie,SOAPAction';
                        add_header 'Content-Type' 'text/plain; charset=utf-8';
                        add_header 'Content-Length' 0;
                        return 204;
                }

                add_header 'Access-Control-Allow-Origin' 'https://esxi.example.org' always;
                add_header 'Access-Control-Allow-Credentials' 'true' always;

                proxy_pass https://esxi_addr:443;
                proxy_ssl_verify off;
                proxy_ssl_session_reuse on;
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection $connection_upgrade;
        }
}

20:29 更新:找到了 VMware Remote Console 的端口:902,用 iptables 进行 DNAT 即可:

iptables -A PREROUTING -i wan_interface -p tcp -m tcp --dport 902 -j DNAT --to-destination esxi_addr:902

2018-05-09 08:07 更新:最后发现,还是直接隧道到内网访问 ESXi 最科学。或者,让 443 重定向到 8443:

server {
        listen 443 ssl;
        server_name esxi.example.org;
        ssl_certificate /path/to/ssl/cert.pem;
        ssl_certificate_key /path/to/ssl/key.pem;

        return 301 https://$host:8443$request_uri;
}
这样,前面也不用写那么多 CORS 的东西了。

使用 iptables 和策略路由进行带源地址的 forwarding

陈老师打开他的服务器,突然发现 CPU 莫名高负载,然后发现是有一个用户被远程登录拿来挖矿了。但是这台机器在 NAT 后,所以登录的源地址全是 NAT 路由,所以不知道对方的地址是什么。我们为了能使用 fail2ban 来禁用多次尝试失败的 IP,但又不想因为别人把 NAT 路由的地址给禁了,这样我们自己也用不了了。所以必须要让这台机器能够知道 ssh 的源地址,我们现在简单的 socat 方案不能满足这个需求。

需求:

  1. 可以在外网连 NAT 路由的高端口(如 2222)来访问这台机器。
  2. 在内网中,既可以直接连它的内网地址,也可以连 NAT 路由的高端口来访问这台服务器。此时,由于连 ssh 的机器就在同一个子网中,如果保留了源地址,服务器发的包会直接回来不经过 NAT。所以我们还是保留了 socat 的方案。

实现方法:

在 NAT Router 上配置 DNAT,这样发到 NAT Router 上的包就可以转发到服务器上:

iptables -t nat -A PREROUTING -i external_interface -p tcp -m tcp --dport 2222 -j DNAT --to-destination internal_server_ip:22

但是,从服务器回来的包到了 NAT Router 上后,由于路由表的配置问题,默认的路由并不能把包送达对方。

方法 1: 我们首先给包打上 mark:

iptables -t mangle -A PREROUTING -i internal_interface -p tcp -m tcp --sport 22 -j MARK --set-mark 0x2222

然后配置策略路由:

ip rule add fwmark 0x2222 table 2222
ip route add table 2222 default via gateway_address

方法 2: (UPD 2018-07-07) 利用 ip rule 直接达成同样的效果

ip rule add from internal_ip/prefix table 2222
# or
ip rule add iif internal_interface table 2222
ip route add table 2222 default via gateway_address

这样就可以保证 ssh 的回包可以原路返回了。

由于前面提到的原因,上面我们配置的 DNAT 规则只对外网过来的包有效。为了内网的访问,我们仍然采用了 socat 的方式:

socat TCP-LISTEN:2222,reuseaddr,fork TCP:internal_server_ip:22

从不同的机器测试,都可以在 who 看到,地址确实是我们想看到的源地址。接下来配置 fail2ban即可。

利用 UPnP 协议进行 mosh NAT 穿透的研究

由于经常要从宿舍、教室等不同的 Wi-Fi 之间切换,但是 ssh 连接又总是断,所以想用 mosh 代替 ssh。但是 mosh 也有它的问题:

  1. 不能滚动。这个可以在 mosh 中嵌套一层 tmux 解决。我目前写了一些自动 mosh 后打开 tmux 并且开启鼠标支持的脚本,但还是有缺陷。
  2. 在高端口 60000+ 监听 UDP,这使得 NAT 后的服务器难以直接通过端口转发。如果直接转发到 NAT 后的机器,那么 NAT 后面如果有多台机器,这又失效了。

于是找了找网上的 NAT 穿透的一些文章,看到了 UPnP 的方法。大致就是,用户可以向路由器注册一个临时的转发规则,路由会自动在 iptables 上配置转发。但是,这样也会遇到一个问题:路由上的 mosh-server 不知道这个转发的存在,所以它可能会尝试监听同样的端口。解决方案下面会提到。

需求:

Server <---> NAT Router <---> My Laptop
On NAT Router, port 8022 is forwarded to Server:22
1. mosh router # works
2. mosh --ssh="ssh -p 8022" router # works

首先在 NAT Router 上配置 miniupnpd(以 Debian 为例)

sudo apt install miniupnpd
# you will get a dialog upon installation
# input your wan interface and listening ip accordingly
sudo vim /etc/default/miniupnpd
# edit START_DAEMON=0 to START_DAEMON=1
sudo vim /etc/miniupnpd/miniupnpd.conf
# edit ext_ifname, listening_ip accordingly
# set secure_mode=yes
# add 'allow 60000-60023 internal_ip/prefix 60000-60023'
# before the last line 'deny 0-65535 0.0.0.0/0 0-65535'
sudo systemctl enable --now miniupnpd

现在,复制 我修改的 mosh-wrapper.js 到用户的 home 目录下,在 Server 安装 miniupnpc 然后通过:

mosh --ssh="ssh -p 8022" --server=~/mosh-wrapper.js user@router

这样,mosh 首先会通过 ssh 和 Server 协商一个 AES 的密钥和 UDP 端口(如 60001),之后的通信都通过 UDP 端口走加密后的流量。我的 mosh-wrapper.js 通过 miniupnpc 向路由器请求把该 UDP 端口转发到 Server 上,这样, mosh-server 就能通过 NAT 路由穿透到后面的 Server 上。

等会!问题来了:

mosh 默认的 IP 范围是 60000-61000 ,根据我的观察,它会从 60001 开始尝试监听本机地址,如果已经被占用,则 60002, 60003, ... 但是!Router 和 Server 实际上占用了相同的端口空间,并且 mosh 只知道本机哪些端口被占用了,而不知道 Router 和 Server 共同占用了多少端口。

我想到了一些可能的解决方案:

  1. 在 Router 上让 miniupnpd 监听对应的端口,占住这个坑。这样,Router 上的 mosh-server 就不会用和 Server 相同的端口
  2. 如果有多个 Server,则会出现抢夺相同端口的情况。我目前的想法是,让 upnpc 去询问 Router 找空闲的端口,然后再传给 mosh-server 使用。另一种方法则是,给不同的 Server 划分不同的端口范围,比如 Router 用 60001-60005, 然后 Server1 用 60006-60010, Server2 用 60011-60015 如此下去。

然后,新的问题又发现了:

当我在和 Server 同一个子网的时候,由于 miniupnpd 配置的 iptables 规则中来源只有 WAN interface,所以我在内网发的包是不会被转发的。当然,既然在内网了,为啥不直接用内网 IP 呢,不知道 mosh 有没有提供设置备用 IP 的功能。

使用 Cisco AC + AP 组合搭建网络实践

有一台已配置好直接可用的 AC 在地址 ac-address。我们需要搭建交换机 + AP 的网络,并且用一台 Linux 服务器进行 DHCP 从而给 AP 分发 AC 的地址。这里以 systemd-networkd 为例。

我们约定,vlan 2 上联外网,vlan 3 为 Linux 服务器和 AP 的内部网络。

接下来,配置交换机给 Linux 服务器的端口为 trunk 口,然后将下联 Cisco AP 的端口都设为 access vlan 3 模式。接下来在 Linux 服务器上配置 DHCP 服务器和 NAT。

如果 Linux 服务器的 interface 名称为 eno1 :

配置两个 VLAN interface:

$ cat /etc/systemd/network/eno1.network
[Match]
Name=eno1

[Network]
VLAN=eno1.2
VLAN=eno1.3

相应添加 VLAN 配置:

$ cat /etc/systemd/network/eno1.2.network
[NetDev]
Name=eno1.2

[VLAN]
Id=2
$ cat /etc/systemd/network/eno1.3.network
[NetDev]
Name=eno1.3

[VLAN]
Id=3

配置上行的 eno1.2 interface 的静态地址:

$ cat /etc/systemd/network/eno1.2.network
[Match]
Name=eno1.2

[Network]
Address=123.123.123.123/24
Gateway=123.123.123.1
DNS=1.2.4.8

配置内部网络 eno1.3 interface:

$ cat /etc/systemd/network/eno1.3.network
[Match]
Name=eno1.3

[Network]
Address=192.168.1.1/24

配置 dhcpd (isc-dhcp-server):

$ cat /etc/default/isc-dhcp-server
INTERFACESv4="eno1.3"
$ cat /etc/dhcpd.conf
option space Cisco_LWAPP_AP;
option Cisco_LWAPP_AP.server-address code 241 = array of ip-address;

subnet 192.168.1.0 netmask 255.255.255.0 {
  range 192.168.1.100 192.168.1.200;
  option routers 192.168.1.1;
  vendor-option-space Cisco_LWAPP_AP;
  option Cisco_LWAPP_AP.server-address $ac-address;
}

配置 iptables 做 NAT:

iptables -t nat -A POSTROUTING -o eno1.2 -j MASQUERADE
iptables-save > /etc/iptables/iptables.rules

打开 ipv4 forwarding:

echo 1 > /proc/sys/net/ipv4/ip_forward
echo 'net.ipv4.conf.all.forwarding=1' >> /etc/sysctl.d/99-ipv4-forwarding.conf