一台家用多用途服务器的设计和配置

需求

一台用于非IT专业人士家的小服务器,需要运行诸如DNS分流和代理之类的软件供内网使用。它处于Full Cone NAT之后,并且需要能被远程管理和重新配置。它应当不发出噪音,并且尽量节约用电。

设备选型

考虑到这台设备上会跑一些网络应用,我需要一个比较能用的CPU加上多于一个的以太网端口。在淘宝上购买了某家专门做无风扇small form factor机器厂的产品,拆掉里面不靠谱的垃圾配件,组成了以下配置:

  • Intel Celeron J1900
  • 自己换的8G三星内存
  • HP拆机的Intel S3610 200G,存储Hypervisor和关键应用数据
  • Samsung 860 EVO mSATA 256G,存储非关键应用数据
  • 两个Intel I211网卡
  • 12V 3A外置电源

其实本来想选择一个支持ECC内存的CPU的,但是这个价格上确实也不好找,遂放弃。J1900让我不满的另一点是不支持SR-IOV,这样机子上自带的一块Intel 5100 Wi-Fi网卡就算是废了,只能把它拆掉了。不用的端口,例如COM和音频输入输出口可以用相应的防尘塞堵上;拆掉网卡以后露出来的天线口用黑色的电工胶布贴了一下,防止进灰。然后买了一个VGA的假负载用于绕过一个固件bug。

固件配置

这台设备因为是深圳小作坊出品,其UEFI固件基本上是Aptio提供给Bay Trail设备的参考固件,设置非常全面,一个都没隐藏。固件版本为GBYT9 V1.02,坑有几个:

  • 使用UEFI GOP的时候,HDMI输出不可用,只能用VGA
  • 如果没有连接显示器,固件会死机,无法启动系统
  • 如果设备上连接了多个USB键盘,只有第一个被枚举的能在固件中使用,因此尽量不要在机子上插没用的键盘或者无线接收器之类的玩意

固件里面需要修改的设置不多:

  • 关掉Legacy Option ROM(然后需要重启一下)
  • 关掉CSM
  • 打开Secure Boot(ESXi 6.7支持Secure Boot和TPM2.0了,虽然没有vCenter的话TPM用不上)
  • 在南桥设置中,设置掉电后自动开机
  • 在北桥设置中,把显卡的睿频关掉,显存分配全部设为最小

Hypervisor配置

考虑到设备后期扩展和维护的需要,这台服务器上会运行一个Hypervisor,所有应用软件放进虚拟机。那么常见的免费Hypervisor有这么几个选项:

  • Microsoft Hyper-V Server
  • Proxmox VE
  • VMWare ESXi

Proxmox VE的不稳定和设计糟糕是所有用过的人都明白的。Microsoft Hyper-V Server在非Active Directory域环境中很不好使,远程管理配置困难,命令行体验也非常糟糕。最后我决定用ESXi,相对来讲比较轻量级,对各种操作系统的支持都比较好,远程管理处于能用水平,并且免费版提供的功能对家用环境来说也够用。

VMWare ESXi安装

很幸运,Intel I211网卡是ESXi 6.7原生驱动支持的网卡。如果系统上没有ESXi自带驱动的网卡的话,你就得去第三方网站上找找有没有相应的驱动了。

安装过程感觉没什么好讲的,安装向导一步步走完就是了。唯一需要注意的是,ESXi默认会把管理网所在的vmk0挂到第一个枚举到的网卡下面,对于多网卡设备,可能需要试一下具体是哪个网卡。为了方便区分,我打印了一张贴纸贴在机器外壳上。

在我这个配置的机子上,全新安装的ESXi 6.7启动时间约2分钟。

另外ESXi 6.7 Build 13006603及之前的版本好像和Chrome 73及以上版本有一些兼容性问题,如果ESXi网页打不开,可以试一下Firefox或者其它非Chrome内核的浏览器。

内核参数

我这台设备比较诡异,安装程序启动和安装完成第一次重启的时候,需要在bootloader界面按Shift+O,在内核参数的末尾添加ignoreHeadless=TRUE,否则启动内核的时候会卡住。

第一次启动成功后,通过console shell或者SSH,把这个参数存入配置文件:

esxcfg-advcfg --set-kernel "TRUE" ignoreHeadless

然后重启一下,确认系统能在无用户干预情况下自动启动。

主机名

前往Networking -> TCP/IP stacks -> Default TCP/IP stack -> Edit settings,在Basic DNS configuration里面选择Manually configure the settings for this TCP/IP stack,在Host name一项中输入你想要的主机名。应用配置后,再把设置改回Use DHCP DNS。这样网络中的DHCP设置会继续生效,但是主机名已经改成你自己设置的了。

如果你考虑给ESXi加Active Directory域的话,建议主机名长度不要超过15字符,不然和NetBIOS协议会有兼容性问题。

NTP

为了保证在中国大陆的可用性,设置了以下NTP服务器:

time.asia.apple.com, ntp.felixc.at, time.nist.gov, cn.pool.ntp.org

设置完以后需要手工启动一下NTP服务。同步需要过几分钟才会完成,不需着急。

第二块硬盘

前往Storage -> Datastores,新建一个datastore即可,也没有什么可以说的。

SFCBD

启用SFCBD(Small Footprint CIM Broker Daemon)我们才能读到一些硬件的健康数据。不过这个比较看机型,我这台机子上就基本上读不到有效信息。通过console shell或者SSH执行以下命令:

esxcli system wbem set --enable true

用户

如果有需要的话,前往Host -> Manage -> Security & users -> Users创建用户。需要注意的是,ESXi默认会禁止最近一段时间登录失败次数过多的用户登录,所以如果你只有默认的root用户并且机子还暴露在公网上的话,很可能因为有人爆破密码而把你自己关在外面,因此建议至少创建一个用户名不常见的用户。

虚拟机配置

我们只有8GiB内存,硬件会占用大概0.11GiB,ESXi本身会吃掉1.22GiB(如果打开swap会稍微少一点),那么满打满算剩下来的还有6.67GiB。在这点内存里面要塞进大量功能,那么每一点都不能浪费。FlexVPN之类的方案虽然非常好用,但是实在是太吃内存,暂时不予考虑。

远程管理方案1:RouterOS

选择RouterOS的唯一原因是,它能只用256MiB内存启动,并且有各种路由功能,非常省资源,如果应急需要用它来当路由器,它也能撑一阵子。RouterOS的缺点是bug多,而且修bug比较慢。除去应急时可以用的路由功能不谈,RouterOS的OpenVPN可以提供一个同地区不同站点之间稳定的站到站连接。

因为这台虚拟机是作为远程管理用的,它不应当依赖任何当前站点的服务。

安装RouterOS

前往官网下载最新的Cloud Hosted Router的VMDK镜像,传到ESXi上,然后把VMDK转成ESXi支持的flat格式:

[[email protected]:/vmfs/volumes/system/RouterOS] vmkfstools -i chr-6.43.14.vmdk chr-6.43.14-new.vmdk
Destination disk format: VMFS zeroedthick
Cloning disk 'chr-6.43.14.vmdk'...
Clone: 100% done.

这样会生成两个文件,一个结尾有flat(这个文件在网页管理面板上看不到),一个没有,这两个文件组成了整个flat格式的VMDK。如果觉得在ESXi上操作麻烦,qemu-img也可以做相应的格式转换。文件创建出来以后就不能重命名,否则会从系统中消失。

接下来只要正常创建一个Other Linux 3.x (64bit)类型的虚拟机,挂载现有硬盘启动就可以了。

配置网络

启用ipv6包,重启一下。然后在ether1上配一个DHCP客户端就完事了。

/ip dhcp-client
add disabled=no interface=ether1

DNS和NTP服务器分别配置成公共服务,不要依赖本地服务。

/ip dns
set query-total-timeout=20s servers=\
    114.114.114.114,114.114.115.115,223.5.5.5,223.6.6.6,1.2.4.8,210.2.4.8
/system ntp client
set enabled=yes server-dns-names=ntp.felixc.at,time.asia.apple.com

设置远程管理

创建一个OpenVPN客户端,连接到我的服务器上。

自动更新

虽然RouterOS的更新没那么靠谱,但是如果你在long-term发行通道上的话,鉴于它漏洞那么多,还是自动更新比较好。

/system script
add dont-require-permissions=no name=update owner=admin policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon source="/sy\
    stem package update\r\
    \ncheck-for-updates once\r\
    \n:delay 1s;\r\
    \n:if ( [get status] = \"New version is available\") do={ install }"
/system scheduler
add interval=1d name=autoupdate on-event="/system script run update" policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon \
    start-date=apr/28/2019 start-time=04:00:00

远程管理方案2:Linux + ZeroTier One

从节约资源的角度考虑,我选择了Debian 9,分配了2C/512MiB/32GB的资源,另外限制了30000IOPS。经验表明,在ESXi上,一旦你的Guest OS在疯狂IO(比如不小心开了swap然后不小心用完了内存),很有可能因为IO速度而把ESXi自己也一起拖死,因此一定要根据你的硬盘实际能力,对IOPS做相应的限制。

安装系统

需要安装standard system utilities + SSH server,然后关掉swap。

/etc/dhcp/dhclient.conf里写上:

prepend domain-name-servers 114.114.114.114,223.5.5.5,223.6.6.6,114.114.115.115,1.2.4.8,210.2.4.8;

/etc/systemd/timesyncd.conf里写上:

[Time]
NTP=ntp.felic.at time.asia.apple.com

设置一下sysctl:

kernel.sysrq=1
kernel.panic=10

安装VMWare Tools

sudo apt install open-vm-tools

配置ZeroTier One

sudo apt install curl
curl -s 'https://raw.githubusercontent.com/zerotier/download.zerotier.com/master/htdocs/contact%40zerotier.com.gpg' | gpg --import && \
if z=$(curl -s 'https://install.zerotier.com/' | gpg); then echo "$z" | sudo bash; fi
sudo zerotier-cli join xxxx
sudo zerotier-cli set xxxx allowGlobal=1

检查资源占用

$ free -wh 
              total        used        free      shared     buffers       cache   available
Mem:           473M        107M        126M        2.4M         13M        225M        351M
Swap:            0B          0B          0B

应用容器:Linux + Docker

这一台虚拟机给了2C/2GiB/64GB。安装系统和初始配置的方法和上一台Linux虚拟机类似,不再赘述。

代理网关

TCP部分可以在iptables的nat表上做REDIRECT;UDP部分可以使用iptable/netfilter的TPROXY功能实现。TPROXY需要Linux内核版本4.18以上,所以我在这台机子上用了内核版本4.19的Debian Buster。实现很简单。

首先打开系统的转发功能(注意持久化):

sysctl net.ipv4.ip_forward=1
sysctl net.ipv6.conf.all.forwarding=1

TCP这边的话,简单设置所有公网流量重定向到代理所在端口:

iptables -t nat -N PROXY

iptables -t nat -A PROXY -d 0.0.0.0/8 -j RETURN
iptables -t nat -A PROXY -d 10.0.0.0/8 -j RETURN
iptables -t nat -A PROXY -d 100.64.0.0/10 -j RETURN
iptables -t nat -A PROXY -d 127.0.0.0/8 -j RETURN
iptables -t nat -A PROXY -d 169.254.0.0/16 -j RETURN
iptables -t nat -A PROXY -d 172.16.0.0/12 -j RETURN
iptables -t nat -A PROXY -d 192.168.0.0/16 -j RETURN
iptables -t nat -A PROXY -d 224.0.0.0/4 -j RETURN
iptables -t nat -A PROXY -d 240.0.0.0/4 -j RETURN

# exclude docker
iptables -t nat -A PROXYCORE -s 172.17.0.0/16 -j RETURN
iptables -t nat -A POSTROUTING -o ens192 -j MASQUERADE

iptables -t nat -A PROXY -p tcp -j REDIRECT --to-ports 1919
iptables -t nat -A PREROUTING -p tcp -j PROXY

ip rule add fwmark 0x01 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100

ip6tables -t nat -N PROXY
ip6tables -t nat -A PROXY -p tcp -d 2000::/3 -j REDIRECT --to-ports 1919
ip6tables -t nat -A PREROUTING -p tcp -j PROXY

ip -6 rule add fwmark 0x01 lookup 100
ip -6 route add local default dev lo table 100

UDP则是使用TPROXY规则在mangle上拦截一下:

iptables -t mangle -N PROXY_UDP
iptables -t mangle -A PROXY_UDP -d 0.0.0.0/8 -j RETURN
iptables -t mangle -A PROXY_UDP -d 10.0.0.0/8 -j RETURN
iptables -t mangle -A PROXY_UDP -d 100.64.0.0/10 -j RETURN
iptables -t mangle -A PROXY_UDP -d 127.0.0.0/8 -j RETURN
iptables -t mangle -A PROXY_UDP -d 169.254.0.0/16 -j RETURN
iptables -t mangle -A PROXY_UDP -d 172.16.0.0/12 -j RETURN
iptables -t mangle -A PROXY_UDP -d 192.168.0.0/16 -j RETURN
iptables -t mangle -A PROXY_UDP -d 224.0.0.0/4 -j RETURN
iptables -t mangle -A PROXY_UDP -d 240.0.0.0/4 -j RETURN
iptables -t mangle -A PROXY_UDP -p udp -j TPROXY --on-port 1919 --tproxy-mark 0x01/0x01
iptables -t mangle -A PROXY_UDP -p udp -j MARK --set-mark 1
iptables -t mangle -A PREROUTING -j PROXY_UDP

ip6tables -t mangle -N PROXY_UDP
ip6tables -t mangle -A PROXY_UDP -p udp -d 2000::/3 -j TPROXY --on-port 1919 --tproxy-mark 0x01/0x01

ip6tables -t mangle -A PREROUTING -p udp -j PROXY_UDP

然后分别在1919端口上启动相应代理程序的TCP和UDP监听即可。需要注意的是,代理程序自己的流量不能走这条规则,那么就要自行想办法做区分。调整iptables规则的时候要注意一下不要把Docker的默认NAT规则清掉了。另外如果你的IPv6是SLAAC配置的话,给用户下发网关可能比较麻烦,需要自己想一下办法。

DNS分流:dnsdist-autoconf + Pi-Hole

我们首先需要安装Docker。Docker有一个大坑就是它默认会把iptables的forwarding chain的默认规则改成DROP,这样你的路由器莫名其妙就坏了。我们需要配置Docker不要没事折腾iptables:

$ cat /etc/docker/daemon.json
{
    "iptables": false
}

配置完以后重启一下Docker daemon。这样配置有安全风险,请自行考量。

安装dnsdist-autoconf很简单,写一个基础配置:

# global config

# if an error is encountered, stop
quit_on_error = false

listen = [
    "0.0.0.0:53",
    "[::]:53",
]

# default upstream
upstreams = [
    "202.141.162.123:53",
    "182.254.242.15:53",    
    "208.67.222.222:443",                 
    "208.67.220.220:443",
]

allowed_client_subnets = [
    # everywhere - potentially unsafe, use with caution
    "0.0.0.0/0",
    "::/0",
]

# set to true if you use Active Directory and allows DNS update
allow_ddns_updates = false

# EDNS0 Client Subnet
[ecs]
enabled = true
default_prefix_v4 = 24
default_prefix_v6 = 48
# if DNS request source IP is not a public routable IP, still forward its ECS information to upstream
keep_private_ip = false

#[control_socket]
#listen = "0.0.0.0"
#key = ""

#[web_server]
#listen = "127.0.0.1:8083"
#password = "supersecretpassword"
#api_key = "supersecretAPIkey"

# enable cache
# note: it eats memory
[cache]
enabled = true
max_entries = 16384

# Rules

[[match]]
domains = [
    "music.httpdns.c.163.com",
    "zzhc.vnet.cn",
]
action = "block"

[[match]]
provider = "dnsmasq-china-list"
upstreams = [
    "223.5.5.5:53",
    "223.6.6.6:53",
    "114.114.114.114:53",   
    "114.114.115.115:53",  
]

然后写一个简单的systemd服务来启动它:

[Unit]
Description=Auto configured DNS loadbalancer in Docker
Requires=docker.service
Conflicts=systemd-resolved.service,dnsdist.service

[Service]
ExecStartPre=-/usr/bin/docker kill dnsdist-autoconf_1
ExecStartPre=-/usr/bin/docker rm dnsdist-autoconf_1
ExecStart=/usr/bin/docker run --rm --name=dnsdist-autoconf_1 --memory=512m -p=127.0.0.1:5302:53/udp -p=127.0.0.1:5302:53/tcp -p=8083:80/tcp -v=/etc/dnsdist:/etc/dnsdist --dns=114.114.114.114 --dns=223.5.5.5 jamesits/dnsdist-autoconf:1.3.7
ExecStop=/usr/bin/docker stop dnsdist-autoconf_1
ExecReload=/usr/bin/docker restart dnsdist-autoconf_1
TimeoutStartSec=infinity

[Install]
WantedBy=multi-user.target

(注意这里监听了5302端口,因为我们之后会用Pi-Hole来间接使用它。)

安装Pi-Hole,继续用Docker+systemd:

[Unit]
Description=Pi-Hole
Requires=docker.service

[Service]
ExecStartPre=-/usr/bin/docker kill pihole
ExecStartPre=-/usr/bin/docker rm pihole
ExecStart=/usr/bin/docker run --rm --name=pihole --memory=512m -v /etc/pihole:/etc/pihole -v /etc/dnsmasq.d:/etc/dnsmasq.d -e TZ="Asia/Hongkong" -e WEBPASSWORD="password" -e ServerIP=192.168.1.2  -e DNS1="127.0.0.1#5302" -e DNS2="127.0.0.1#5301" --dns=127.0.0.1 --dns=114.114.114.114 --dns=223.5.5.5 --dns 114.114.115.115 --dns 223.6.6.6 --net=host  --cap-add=NET_ADMIN pihole/pihole:4.2.2-1
ExecStop=/usr/bin/docker stop pihole
ExecReload=/usr/bin/docker restart pihole
TimeoutStartSec=infinity

[Install]
WantedBy=multi-user.target

(注意这玩意设计很糟糕,需要硬编码本机IP地址。上游DNS必须写在环境变量里,不能用网页设置。我这里设了用dnsdist-autoconf开出来的localhost:5302和用另一个fallback程序开出来的localhost:5301。)

检查资源占用

$ free -wh
              total        used        free      shared     buffers       cache   available
Mem:           1.9G        440M        1.1G        5.8M         17M        382M        1.4G
Swap:            0B          0B          0B

测试

  • Hypervisor能够在不插键盘和显示器的情况下正常启动
  • 虚拟机能在Hypervisor启动之后自动启动
  • 虚拟机中各个应用程序工作正常,并且不依赖虚拟机的启动顺序
  • 所有远程访问方式能够正常工作

清理工作

  • 关掉不必要的Hypervisor服务:console shell,SSH
  • 给所有虚拟机做个snapshot

On site安装工作

  • 网关虚拟机需要固定IP
  • Pi-Hole需要设置网关IP

参考:

1 thought on “一台家用多用途服务器的设计和配置

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据