终于又到了传统艺能折腾homelab的时刻了,虽然之前的服务已经稳定运行了4年时间,期间小修小补已经初具所谓的云原生的形态,然而在日志、可观测性等方面还存在不少提升的空间。另外整了一个ROS盒子,终于可以将网关分割出home server了,这样远程维护就不用担心把网干了。最后就是上k8s,因为之前就基本容器化了,所以这一步反而是相对来说比较简单的。这次升级完感觉这套配置基本已经到头了,下一轮升级应该就是直接上刀片服务器、FTTR、10G内网这些了,估摸着还要再打几年工才能凑够(笑)。

文章内容会比较长,这边分两部分。首先是硬件升级,然后是各种基建服务的搭建。这个系列默认读者具有完备的计算机基础知识,不会手把手教学,请RTFD。

顶层设计

首先整理下目前homelab的特点:

  • 云服务分布广:和大厂不同,homelab一般包括私有云和公有云两部分。私有云就是跑在家里的服务器,而公有云可能由各大云厂商的小鸡组成。当然你也可以足够牛逼(rich),直接整一个ASN和ip段(
  • SLA保证不同:公有云的SLA一般都在几个9,问题不是很大,差别在于配置不同。而私有云因为一般都是家宽,由于运营商、地域、IP过期时间、(自己的折腾)、停电维护等等原因,很难保证高SLA。这也是homelab与商业化私有云非常大的区别之一。
  • 国内特色:大部分人无法规避的一点是需要适应国内特色网络环境,不止是国际出口,高峰期的国内QoS也不可忽视。

综合以上这些特点和之前的环境体验,简单画了一个升级后的整体家庭云拓扑图:

喵云拓扑图

总体设计和之前一样,安全性依然是第一优先,并针对homelab特点进行专门的优化,具体的设计思路会在下面详细介绍,

网络

首先是香港的Server 1,作为承载主站的节点,SLA和速度要求是最高的。因此只有一条数据上报的连接流向国内。网络因为换成了昂贵的CN2 GIA,带宽并不大,老老实实挂站吧。

第二台是一直在用的美国Server 2,量大管饱,因此作为主要代理服务器使用,变化不大。

而国内外连接方式是本次升级重点,所有流量目前均走IPLC专线出网。不得不说感受了专线的极致稳定和快速,很难再回到之前直连的各种抽风体验了。尤其是对于监控数据上报和游戏加速,稳定不丢包才是神中神。

那么古尔丹,代价是什么呢?
贵。

大内网组网选择

网上不少教程建议使用tailscale之类的组网工具,将所有服务器组成一个大内网,方便各种管理。而本次升级并没有采用这种方式,主要有以下三个方面的原因:

  1. 最重要的一点,目前基于wireguard的组网技术基本都会尝试使用P2P,而tailscale更为激进,有自己的一套打洞方式。这在理论上是最优的,但是却不符合“国内特色”。即使代理服务器通过专线接入虚拟局域网,与国内客户端建立连接时依然会尝试走P2P,而这在长期运行时反而会劣化网络速度,是我们不愿看到的。但是截止目前,tailscale并未提供强制关闭P2P的选项
  2. 个人需求,私有云中会存在高危网段和公共网段,虚拟化好办用一个vlan就能隔离。但是组网根据设计会放在网关上做,因此需要一层额外的防火墙或ACL策略。加上目前VPS的防火墙策略使用的是云厂商的安全组而不是iptables,因此组内网后所有安全组均会失效,风险比较高。
  3. 我们需要思考下,大内网真的需要么?组建大内网就是为了方便互访,然而公有云一般都会提供公网IP,有无数种方式可以实现跨网访问。唯一比较有用的在于多个私有云之间的组网,可以解决动态ip变化的问题。不过目前我就一台homelab主机,所以大内网其实用处不大。

服务器升级

原先只有一块1T的970EVO Plus作为宿主机系统盘,后来发现Windows、MacOS的系统吃硬盘还是太猛了,加上几个Linux虚机基本容量已经捉襟见肘了。本次升级主要是加了一块2T的990EVO Plus,将原本作为zfs缓存的SSD U盘换到了E50UG上做docker的外置存储(见下文),老的1T目前存储还够用就作为新的zfs缓存好了。

SSD全家福

加硬盘这个还是要说道说道,由于HPE gen10 plus没有M2槽位,之前用的是PCIE转出来的M2。但是现在多加一条硬盘,需要更换为双口的扩展卡。目前市面上主要有两种扩展卡,一种是不带芯片的,比较便宜但是需要BIOS支持PCIE拆分;另一种是带芯片的,价格翻了好几倍但是不需要BIOS支持。虽然理论上来说这台服务器是支持拆分的,但是查阅了一些前人的帖子发现坑非常的大,且新版的BIOS似乎把这个功能给阉割了。所以最后还是忍痛花了600多大洋买了EB-LINK的自带芯片双口扩展卡,很难理解为啥这么个玩意儿卖这么贵……

ROS配置

本次比较重大的升级是将原来All in boom的openwrt虚拟机换成了MikroTik的E50UG,主要解决的是每次升级运维服务器都需要假期回家才能搞的问题(否则万一出什么问题没上线就直接失联了),将网关拆分出来就能利用iLO随便远程管理了。因为透明代理、frp端口转发等是必须的,所以支持了docker的ROS可玩性还是比较高的。本体非常小小一只,搭建完后大概长这样:

ROS

稳定运行后的温度比较高,略烫手的程度。系统配置请仔细阅读官方文档,顺便也能复习下计算机网络哈哈:

https://help.mikrotik.com/docs/spaces/ROS/pages/328059/RouterOS

下面的步骤教程仅限于7.20版本,后续更新不保证可用。

网段配置

为了减少二级NAT造成的性能损失和各种奇葩问题,路由器和服务器啥的全部做成桥接模式,整个网络比较扁平。由于也没有啥智能家居设备,就不费事在ros上划vlan了,所有ROS网口直接用一个网桥全连起来,然后用防火墙做隔离。

  • 10.0.0.1/16:核心内网,直连主机(T0)
  • 10.20.0.1/16:VPN(T20)
  • 10.40.0.1/16:ROS docker(T40)
  • 10.80.0.1/16:PVE(T80)
  • 10.160.0.1/16:PVE(T160)
  • 10.190.0.1/16:PVE(T190)

主动访问的网络限制如下表:

级别出方向代理隔离
T0ANY
T20ANY
T40WAN
T80WAN
T160WAN
T190WAN

IPV6因为懒得配防火墙,暂时只给10.0.0.1/24

另外提一嘴,由于HPE服务器只插了一根网线到ROS,因此只有这个需要配置VLAN实现网段隔离,否则ROS上的DHCP无法给PVE中的主机分配正确安全区的IP。

OpenVPN

为了能远程回家,这里直接用自带的OpenVPN即可,参考文章:https://www.simaek.com/archives/275/

首先签一个证书

1
2
3
4
5
6
7
8
/certificate
add name=ovpn-ca common-name=ovpn-ca days-valid=3650 key-size=2048 key-usage=crl-sign,key-cert-sign
add name=ovpn-server common-name=ovpn-server days-valid=3650 key-size=2048 key-usage=digital-signature,key-encipherment,tls-server
add name=ovpn-client common-name=ovpn-client days-valid=3650 key-size=2048 key-usage=tls-client

sign ovpn-ca name=ovpn-ca
sign ovpn-server name=ovpn-server ca=ovpn-ca
sign ovpn-client name=ovpn-client ca=ovpn-ca

给VPN网段分配10.20.0.1/24网段,并添加用户

1
2
3
4
5
/ip pool add name=ovpn ranges=10.20.0.2-10.20.0.254
/ppp profile add name=ovpn local-address=10.20.0.1 dns-server=10.20.0.1 remote-address=ovpn use-compression=yes use-encryption=yes use-ipv6=no

# change to your own
/ppp secret add name=xxx password=xxx profile=ovpn service=ovpn

然后添加vpn服务,注意这里需要有 push-routes 参数把10.0.0.0/8路由推送给客户端,否则需要手动添加路由

1
/interface ovpn-server server add name=ovpn port=1194 certificate=ovpn-server default-profile=ovpn protocol=tcp netmask=24 mode=ip require-client-certificate=yes auth=sha256,sha384,sha512 cipher=aes128-gcm,aes192-gcm,aes256-gcm disabled=no push-routes="10.0.0.0 255.0.0.0"

接着是一些教程里没讲到的,如果和我一样配的是default drop的防火墙策略,那么不仅要允许1194端口,还得允许10.20.0.1/24的input,否则无法从vpn访问router

1
2
/ip firewall filter add chain=input protocol=tcp dst-port=1194 action=accept place-before=1 comment="Allow OpenVPN"
/ip firewall filter add chain=input action=accept place-before=1 src-address="10.20.0.1/24" comment="Allow OpenVPN"

最后导出.ovpn文件下载,这里导出key必须要有 export-passphrase 密码,如果我们不想每次都输入密码,那么可以下载后用openssl把密码删了

1
2
/certificate export-certificate ovpn-ca
/certificate export-certificate ovpn-client export-passphrase=12345678
1
openssl rsa -in cert_export_ovpn-client.key -out cert_export_ovpn-client.key

DNS配置

本次升级另一个重要的点是自建了DNS基础服务,并通过DoH向私有云提供加密解析服务。由于ROS没有默认根证书,直接添加会无法验证tls证书,如果不验证那么和裸奔也没区别,所以我们需要导入根证书。另外默认会采用运营商DNS,所以还需要禁用掉这个配置(有关DoH的DNS服务可以参考下一篇)。由于DoH自身用的是域名,去掉默认DNS后将会无法解析,所以需要手动加个静态解析。

1
2
3
4
5
6
/interface pppoe-client set 0 use-peer-dns=no

/ip dns
# change to your own
set use-doh-server="https://domain.com/dns-query" verify-doh-cert=yes
static add name=domain.com address=ip

导入根证书步骤:

  • 浏览器访问域名,点击小锁图标查看证书
  • 下载根证书和中间证书
  • Files 上传然后 System-Certificates 导入,注意根证书有效期到了要重新下载。

DNS解析空数据问题

DNS服务搭建用的是大佬的easymosdns,搭建好后测试发现运行一阵子后会出现一些域名解析为空的情况,而且由于ROS自身的域名缓存规则,空解析也会被缓存上,导致这些网站一段时间将会无法访问。这个奇怪的问题困扰了我一段时间,一开始以为是ROS的问题,找了半天文档也没看出个所以然。后来发现竟然是DoH服务的问题。

easymosdns的DoH服务运行在9053端口,通过反代暴露到443,tcpdump抓包空解析的域名如下:

1
410	4.990542	127.0.0.1	127.0.0.1	DoH	347	Standard query response 0xbab3 No such name HTTPS page.aliyun.com SOA fake-ns.mosdns.fake.root

竟然返回了一个fake的地址,而这个报错也非常有意思 No such name HTTPS page。查找资料发现DNS支持一个HTTPS(TYPE65)记录,用于返回web页面的地址,而这个在easymosdns里出于科学上网的原因是被屏蔽的。一般情况下,找不到type65应该会fallback到正常的解析,但是ROS似乎有某种race condition,导致有些时候的type65结果会被缓存。这里我们需要改配置解除type65限制才能获得正常的返回:

1
157	11.101001	127.0.0.1	127.0.0.1	DoH	598	Standard query response 0xe97a HTTPS page.aliyun.com CNAME page.aliyun.com.gds.alibabadns.com CNAME na61-na62.wagbridge.alibaba.aliyun.com CNAME na61-na62.wagbridge.alibaba.aliyun.com.gds.alibabadns.com SOA gdsns1.alibabadns.com

ipv6

江苏电信给了/60的ipv6,可以搞SLAAC,参考文章:https://wu.renjie.im/blog/network/ros-dhcpv6/zh-cn/

注意由于ipv6也有运营商默认DNS,只设置 use-peer-dns=no 似乎没有效果,需要加上 accept-router-advertisements=no

1
2
3
4
5
6
7
8
9
/ipv6 settings set accept-router-advertisements=no
/ipv6 dhcp-client add add-default-route=yes interface=pppoe-out1 pool-name=delegation pool-prefix-length=62 prefix-hint=::/60 request=prefix use-peer-dns=no
/ipv6 address add address=::1 from-pool=delegation interface=bridge

/ipv6 dhcp-server add name=default prefix-pool=delegation interface=bridge lease-time=1d
/ipv6 nd set [ find default=yes ] other-configuration=yes
/ipv6 dhcp-server option add code=23 name=dns value=0xfd000000000000000000000000000001
/ipv6 address add address=fd00::1 advertise=no interface=bridge
/ipv6 dhcp-server set default dhcp-option=dns

ntp

时间服务应该算是少有的可以使用国内服务器的服务,直接设置成中科院授时中心的就行,然后给本地广播。

1
2
/system ntp client set enabled=yes servers="ntp.ntsc.ac.cn"
/system ntp server set enabled=yes multicast=yes manycast=yes

防火墙策略

搞安全的自然要设置严格的防火墙策略咯~主要有下面一些设置:

  • 默认策略drop
  • 禁ping
  • 禁默认的ipsec相关端口
  • 关闭ros除winbox以外端口
  • 用户登录白名单ip
  • 安全区接口IP绑定

DDNS

ros自带的DDNS在国内肯定是不能指望的,网上搜了下也没有啥靠谱的方案(那些教人把AKSK传到自己服务器的真有人敢用么……),干脆用golang手搓了一个,大家可以自己部署:https://github.com/MXWXZ/ros-ddns

步骤很简单,找台服务器把docker跑起来,然后在Script里面加一个脚本,最后添加Scheduler5分钟执行一次即可。具体可以参考项目Readme。

容器

尝试ROS最主要的一点就是看中了新版本支持自定义容器,提供了很高的可玩性。参考文档:https://help.mikrotik.com/docs/spaces/ROS/pages/84901929/Container

折腾了一会发现这东西的坑还是比较大的,首先默认只有128M的存储,对于动不动上G的容器镜像来说肯定是不够的,不过还好由于新加了一块SSD,之前做ZFS缓存的CHIPFANCIER可以拔下来插到E50UG上做外置存储,格式化后添加进Disk就行。

然后第二个坑就是创建时的rootdir必须保证不存在,由系统自动创建,手动创的话会报莫名其妙的错误。另外容器设置完你在winbox里就不能访问rootdir和mountdir,动态改文件会比较麻烦,这里推荐使用SFTP进行管理,可以绕开这些限制。

就算你把这些都做完了,跑提供了arm的镜像依然会报类似 Illegal instruction 的错误(例如某mihomo),查了一会资料发现其实他这个cpu只开启了arm32v5的支持,而现在正常的镜像基本都在arm32v7。按照帖子里的说法其实这个CPU理论上是可以支持的,不知道为啥没有开启这个特性,所以我们得自己去编译镜像。

好在软件是go写的,交叉编译比较舒服,这边提供一个Dockerfile样例:

1
2
3
4
5
6
7
8
9
FROM arm32v5/debian:stable
LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/mihomo"

RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates tzdata iptables && rm -rf /var/lib/apt/lists/*

VOLUME ["/root/.config/mihomo/"]

COPY bin/mihomo-linux-armv5 /mihomo
ENTRYPOINT [ "/mihomo" ]
1
2
docker build --platform linux/arm/v5 -t arm32v5/mihomo .
docker save arm32v5/mihomo -o mihomo.tar

上传后挂载就能跑起来了。

透明代理1

由于经常需要获取原始ip,因此刚需不使用fakeip,而目前的容器环境下似乎并不支持tproxy,捣鼓了一阵redir-host模式,基本实现了透明代理。

首先mihomo配置需要添加:

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
dns:
enable: true
enhanced-mode: redir-host
use-hosts: true
use-system-hosts: true
ipv6: false
default-nameserver:
- system
nameserver:
- system
fallback:
- system
proxy-server-nameserver:
- system
direct-nameserver:
- system

tun:
enable: true
stack: mixed
auto-route: true
auto-redirect: false
auto-detect-interface: true
device: tun0
strict-route: true

这样在启动的时候会自动创建一个tun0的虚拟网卡,并且自动转发流量。假设分配给veth的ip是10.40.0.2,这个时候同网段下如果把网关配置成这个ip应该就能自动分流了。但是由于现在我们只给docker分配了24网段,因此还需要做一个路由配置,使得我们能够精确控制那些网段经过代理,并且不需要额外配置网关:

1
2
3
4
/routing table add name=proxy fib
/ip firewall mangle add chain=prerouting src-address-list=PROXY dst-address-list=!LAN in-interface-list=PROXY action=mark-routing new-routing-mark=proxy
/ip firewall mangle add chain=prerouting src-address-list=PROXY dst-address-list=PROXY-GW in-interface-list=PROXY action=mark-routing new-routing-mark=proxy protocol=udp dst-port=53
/ip route add dst-address=0.0.0.0/0 gateway=10.40.0.2 routing-table=proxy

主要的功能大概就是将PROXY列表中通往外网的流量搭上标记,然后将标记的流量包下一跳转到透明代理。注意由于DNS在内网,而要想让透明代理能够识别域名,还需要将所有通往PROXY-GW(其实就是ROS在不同子网的网关地址)的DNS流量也进行标记。
配完以后,我们标记的流量就会经过10.40.0.2了,curl www.google.com也能正常访问,并且nslookup可以正常得到真实IP。

然而很可惜,E50UG的CPU性能在容器环境下并不足以支持千兆网络的透明代理,实测即使在容器内wget或者采取手动设置proxy的方式,direct模式跑到300M就会占满CPU,加上防火墙标记的透明代理更是只能跑到50-80M。

因此目前而言,我们还是让ROS安静地做一个网关好了,代理还是需要用旁路由的方式虚拟化一台主机……

透明代理2

为了享受千兆网络,还是做旁路由吧……由于只跑一个mihomo,直接PVE开一个debian的CT容器就行。

安装直接用官网的deb包,注意目前PVE的内核并没有包含tproxy,如果想要使用的话需要自己去编译,我这边省点事直接用tun模式算了。然后修改lxc配置允许tun:

vim /etc/pve/lxc/xxx.conf

1
2
lxc.cgroup.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net dev/net none bind,create=dir

当采用这种方式,且源ip与旁路由ip不在一个网段时,下载速度(无论国内外)将会骤降至几十到几百K/s。如果采用http proxy的方式速度正常,因此问题应该是出在mark routing处。

如果不使用mark routing,而是修改routing rule,不会触发此问题。经过查找资料发现是fasttrack的问题,需要在fasttrack的filter中修改routing-mark为main只对默认路由处理即可。

这也是我非常讨厌某些厂商的“自研”非标准协议/特性的原因之一,你不知道什么时候会出现冲突和问题,需要去翻这家的文档,而如果同时文档很烂的话就只能靠猜了。

实测E-2236单核心即可跑满千兆,还得是x86(

总结

至此,硬件升级基本上是完成了,唯一美中不足的一点是还没有GPU跑LLM,目前泡沫比较大,等看什么时候有大显存便宜GPU再说吧……