家里云网络基础设施
Table of contents
前言¶
不是哥们,你真的需要读这个?好好好,看来你的需求和我一样扭曲。
开门见山,我想要内网可控的DNS、CA、双栈网络,让每一个内网主机/服务都能有任意数量的FQDN用。在此基础上能有一定流量统计能力就好。
我曾用ArchLinux搓了个路由器,使用的是N3450 CPU的矿渣。那时候iptables仍是大众首选。 因为种种维护难题、加法做太多(All in Boom),这套系统最终被淘汰,而我也转向使用专用路由器/固件。 但是SOHO级路由器的能力……懂的都懂。 我也试过往OpenWRT里丢一些专用的DNS、DHCP配置,让内网访问更便捷,但经常出现奇奇怪怪的小问题。 虽然可靠性不是问题、不会断网,但用着总感觉很憋屈。 我内网有不少依赖内部域名的服务,需要更可靠、更灵活的DNS。
这次使用某个ARM开发板构建自己的家里云网络核心路由器,OS选用Armbian。 为什么不用ArchLinux?因为我不打算追新,也不想自己单独维护一份内核,或者引入什么莫名奇妙的兼容问题(没错我遇到过)。 实话说我不怎么喜欢Debian的一些设计偏好,不过习惯了也还行。
这是我使用Armbian构建家庭路由器的过程整理,并不包含中间踩坑调试过程。实际操作过程中,会有不少相互耦合的服务。开始吧!
欢迎电邮提问&留言!(不过M$挺容易吃邮件的啊……)
操作环境¶
- 某ARM开发板
- 至少要有两个网口,有三个会轻松不少
- Armbian(Debian Bookworm)
筋骨:网络划分¶
真的有人这么划分家庭网络么……
首先需要明白自己需要何种形式的网络结构。我的家里云设计比较扁平:使用一台核心路由器、网关交换机,划分3个VLAN,分别用于网络基建、家庭、实验网络。
- 调制解调器接入设备(光猫也是猫 (^ΦωΦ^) )
|
- 路由器(本文重点)
|
- 网管交换机
\_ Infra(wired)
|_ Home(wired & AP)
|_ Lab(wired)
Armbian默认采用Netplan配置网络。Netplan会为配置好的后端(systemd-networkd和NetworkManager)动态生成运行时配置。以systemd-networkd为例,系统默认配置位于/usr/lib/systemd/network
,Netplan生成的配置会放在/run/systemd/network
,而用户可以在/etc/systemd/network
自由编写自己的特殊配置、配置覆盖等等。应用优先级从低到高依次为/usr/lib
、/run
、/etc
。因此在配置合理的情况下,Netplan不会和用户自定义的systemd-networkd配置产生冲突。
有了Netplan,我的网络配置可以非常declarative:
# /etc/netplan/60-my-network-structure.yaml
network:
version: 2
renderer: networkd
ethernets:
enmetaport0:
match:
# use macaddress to be more robust
name: "en*"
macaddress: "00:00:00:aa:bb:cc"
# set a custom interface name
# take care of the prefix, so the rule can be matched again when
# re-applying the config
set-name: enmetaport0
vlans:
vlan-infra:
id: 1
link: enmetaport0
addresses:
- "10.101.0.1/16"
- "fcf4:abcd::1/112"
vlan-home:
id: 2
link: enmetaport0
addresses:
- "10.102.0.1/16"
- "fcf4:aeae::1/64"
vlan-lab:
id: 23
link: enmetaport0
addresses:
- "10.123.0.1/16"
- "fcf4:dfdf::1/64"
之后netplan try
试试看,没问题就netplan apply
即时应用,当然重启一下也行。
杂音:PPPoE接入ISP¶
听说PPPoE太老了不适合云原生所以没有存在的价值。
Netplan和systemd-networkd并不支持对PPP上网的直接配置。好在这部分手写也不是问题。
配置上游接口¶
在netplan里激活需要用到的上游接口,这样PPP服务脚本那里可以省掉启用接口的步骤:
# /etc/netplan/60-pppoe-interface.yaml
network:
version: 2
renderer: networkd
ethernets:
enupstream0:
# it's a different interface!
match:
name: "en*"
macaddress: "00:00:00:aa:bb:cd"
set-name: enupstream0
可以先netplan apply
一下。后面测试PPP需要用到这个接口。
配置PPP服务¶
首先安装ppp包:
apt install -y ppp
配置PPP服务。要写一份PPP配置及一份systemd服务脚本。
PPP配置¶
先来看PPP配置/etc/ppp/peers/enupstream0
:
# /etc/ppp/peers/enupstream0
# debug mode
#debug
user "your account name / phone number"
password "your account password"
# bind interface
plugin rp-pppoe.so enupstream0
# Ensure a PID entry with specified linkname is made pppoe
# e.g. /run/ppp-enupstream0.pid
linkname enupstream0
# specifiy interface number, e.g. ppp0, ppp101
unit 101
usepeerdns
persist
# retry 10 times before reporting error(exit)
maxfail 10
defaultroute
hide-password
noauth
我的配置文件命名和网络接口一样,这是我的习惯,同时也方便后面的服务脚本套用这个名字。 上面也并没有使用一些文章推荐方式将密码存到别的文件。我的上游用的是PAP验证,而非CHAP验证,按推荐配置卡了我很久,想来还是直接写单独一个配置里更轻松。
之前运行过netplan apply
的话,这里可以运行pon enupstream0
启用服务测试一下。如果能上网,那PPPoE大概没什么问题了。随后可以poff
停止pppd服务,因为后面要用systemd让pppd成为一个正经的服务。
让pppd成为一个正经的systemd服务¶
写个服务脚本,网上抄的加点佐料:
# /etc/systemd/system/[email protected]
[Unit]
Description=PPP link to %I
BindsTo=sys-subsystem-net-devices-$I.device
After=sys-subsystem-net-devices-$I.device
After=systemd-networkd.service
[Service]
Type=notify
ExecStart=/usr/sbin/pppd call %I nodetach up_sdnotify
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target
然后可以用systemctl enable --now pppd@enupstream0
启用这个pppd服务并激活自启动。
获取IPv6并分配前缀¶
我的ISP提供了一个::/60
前缀,可以直接通过DHCPv6/RA来获得,并不涉及PPPoE,但是相关配置需对应PPP的接口。之前ppp配置里写的是unit 101
,那接口就是ppp101
。
先启用ppp101
上的DHCPv6,注意配置最后一节中对此接口分配了一个可选的前缀:
# /etc/systemd/network/30-ppp101-ipv6.network
[Match]
Name=ppp101
Type=ppp
[Network]
DHCP=ipv6
DefaultRouteOnDevice=true
# necessary, to avoid messing with interface
KeepConfiguration=static
LinkLocalAddressing=ipv6
IPv6AcceptRA=yes
[DHCP]
RouteMetric=100
UseMTU=true
[DHCPv6]
# to hide our real hostname, explictly set one
Hostname=gate
# Don't use most of the other DHCP info
[DHCPv6]
#UseAddress=false
#UseDNS=false
UseNTP=false
UseHostname=false
UseDomains=false
# The lines below are optional, to also assign an address in the delegated prefix
# to the upstream interface. Uncomment the lines below if necessary.
[Network]
KeepConfiguration=yes
DHCPPrefixDelegation=yes
[DHCPPrefixDelegation]
UplinkInterface=:self
SubnetId=0
Announce=no
然后给另外一个接口加上前缀分配的配置。这个配置是对netplan生成配置的自定义覆盖,注意看文件路径。
# /etc/systemd/network/10-netplan-vlan-lab.network.d/ipv6-pd.conf
[Network]
# we are the router, ignore others
IPv6AcceptRA=no
DHCPv6PrefixDelegation=yes
# Uncomment below if networkd should handle DHCP and RA
# e.g. you don't use dnsmasq as the DHCP server
#[Network]
#IPv6SendRA=yes
#DHCPServer=yes
# Delegated prefix
# use :auto or specify the interface, e.g. ppp0, ppp101
[DHCPPrefixDelegation]
UplinkInterface=ppp101
SubnetId=1
Announce=yes
Token=::1
# Optional custom local prefix
[IPv6Prefix]
Prefix=fcf4:dfdf::/64
Assign=yes
Token=::1
此时用systemctl restart systemd-networkd
来重载配置。如果内网lab接口获取到了前缀,就说明配置成功啦!
连结:做个路由器吧!¶
Linux路由器,宁有种乎?
前面配置好后,Armbian并不会直接变成路由器的样子。为了对下游网络提供服务,需要开启转发功能、启用DHCP/DNS服务等等操作。
开启路由转发¶
需要打开一些内核功能。添加一个新的sysctl配置:
# /etc/sysctl.d/90-enable-ip-forwarding.conf
# This is IP forwarding
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
随后刷新配置(参考/etc/sysctl.d/README.sysctl
):
service procps force-reload
转发功能启用口扌立!
用dnsmasq作DHCP/DNS服务器¶
由于我的网络存在多个下游接口、划分了多个子网,同时开启多个dnsmasq实例是更合适的选择。此处以lab网络为例,介绍启用多dnsmasq实例的方法。
我计划将实例配置安置在/etc/dnsmasq.instance.d
。我需要按Debian的风格为每个实例配置好配置文件所在目录:
# 总之先干掉单独的dnsmasq服务
systemctl mask dnsmasq
# 为vlan-lab实例编写基本配置
# 类似/etc/default/dnsmasq,不过仅保留了配置目录设置
cat << EOF > /etc/default/dnsmasq.vlan-lab
# setup config dir, and config blacklists
CONFIG_DIR=/etc/dnsmasq.instance.d/vlan-lab,.dpkg-dist,.dpkg-old,.dpkg-new
EOF
# 激活服务开机自启动,但是先别启动
systemctl enable dnsmasq@vlan-lab
然后点一份dnsmasq配置:
# /etc/dnsmasq.instance.d/vlan-lab/main.conf
# Net prefix delegation is handled by systemd-networkd
# Dnsmasq only handles address allocation(DHCP) and hostname resolution
port=53
interface=vlan-lab
# do not bind loopback, otherwise other instance will fail to start
except-interface=lo
# the static addresses given to vlan-lab
listen-address=10.123.0.1
listen-address=fcf4:dfdf::1
#bind-interfaces
bind-dynamic
#no-hosts
#addn-hosts=/etc/banner_add_hosts
#expand-hosts
domain=lab
local=/lab/
resolv-file=/etc/resolv.conf
# IPv4
dhcp-range=10.123.100.0,10.123.200.0,255.255.0.0,1h
# router, DHCPv4, btw IPv6 uses RA not DHCPv6
# 0.0.0.0 is a special address referencing the machine dnsmasq runs on
dhcp-option=3,0.0.0.0
# DNS
dhcp-option=6,0.0.0.0
# IPv6
enable-ra
# custom prefix
dhcp-range=fcf4:dfdf::1:0, fcf4:dfdf::ffff:ffff, slaac, 64, 1h
# the prefix previously delegated
dhcp-range=::1:0, ::ffff:ffff, constructor:vlan-lab, slaac, 64, 1h
# RA on vlan-lab, interval 30 seconds, router lifetime is 1200
ra-param=vlan-lab,30,1200
# DNS, it will make the firewall rules very difficult to write if [::] is used,
# since one interface can have multiple independent IPv6 addresses.
dhcp-option=option6:dns-server,fcf4:dfdf::1
# Never forward plain names (without a dot or domain part)
domain-needed
# Never forward addresses in the non-routed address spaces.
bogus-priv
随后可以启动服务了:systemctl start dnsmasq@vlan-lab
。DHCP自动配置,我可太喜欢它了。
开启NAT¶
内网设备用内网地址无法直接上网,需要NAT。这一步用nftables实现。可以在/etc/nftables.conf
中添加下面的内容:
table inet simple_srcnat
flush table inet simple_srcnat
delete table inet simple_srcnat
table inet simple_srcnat {
define INTERNAL_NORMAL_IFS = {
"vlan-home",
"vlan-lab",
}
define INTERNAL_CRITICAL_IFS = {
"vlan-infra"
}
define INTERNAL_IFS = {
$INTERNAL_NORMAL_IFS,
$INTERNAL_CRITICAL_IFS,
}
define OUTGOING_IFS = {
"enupstream0",
"ppp101"
}
chain forward {
type filter hook forward priority mangle; policy accept;
# MSS clamp
iifname "ppp101" tcp flags syn tcp option maxseg size set rt mtu
oifname "ppp101" tcp flags syn tcp option maxseg size set rt mtu
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname $INTERNAL_CRITICAL_IFS drop
# masquerade local addresses
oifname $OUTGOING_IFS ip saddr 10.0.0.0/8 masquerade
oifname $OUTGOING_IFS ip6 saddr fcf4::/16 masquerade
}
}
简单说,上面的table启用了vlan-home
和vlan-lab
上所有内网地址的NAT,丢弃所有外部主动发往基建网络vlan-infra
的包,并在ppp出口应用MSS钳制。
有了NAT,下游设备就能通过路由器中继上网啦。
我的名字我做主:掌控自己的DNS¶
姓名能给人力量,但也是强大的束缚。
改造网络很大一部分原因是要设立自己的私有DNS服务器,以便灵活建设内网服务。我计划使用home.internal
做内网服务主要使用的域。
设立自己的Knot DNS¶
Knot DNS是一款开源的权威DNS服务软件。它不会像dnsmasq、smartdns之类向上游递归查询。用它就可以设立自己的内网顶级域名。为了保证兼容性,我打算拿下整个.internal
顶级域名(事实是如果只拿部分二级域名,中间的递归DNS服务会把请求发到根DNS去)。
Knot DNS服务配置,建议结合Knot DNS配置文档一同阅读:
# /etc/knot/knot.conf
# server listen at 5553
server:
rundir: "/run/knot"
user: knot:knot
listen: [ 0.0.0.0@5553, ::@5553 ]
log:
- target: syslog
any: info
database:
storage: "/var/lib/knot"
# some keys for standard DNS operations
key:
- id: naive
algorithm: hmac-sha512
secret: generate-your-own!
- id: home
algorithm: hmac-sha512
secret: generate-your-own!
# access control, for safety
acl:
- id: deny_all
deny: on
- id: address_only
action: update
update-type: [A, AAAA]
- id: naive_key
action: update
key: naive
- id: home_key
action: update
key: home
# zone template. zones defined in `zone:` can use templates herem and bring
# their own overrides
template:
- id: default
storage: "/var/lib/knot"
file: "%s.zone"
acl: address_only
acl: naive_key
zone:
- domain: INTERNAL
acl: deny_all
- domain: infra.INTERNAL
acl: deny_all
- domain: home.INTERNAL
acl: address_only
acl: home_key
生成key可以用keymgr
,会和Knot DNS一起装好,例子(别用这个key!):
# keymgr -t home hmac-sha512
# hmac-sha512:home:cVE3CZ0twnAmf6C7suhr8nJQTtdqo8RVmJkOC0WL5luandWazPrY7AtQJAEkE86HwzOcTla187fInNYZ5KkpPg==
key:
- id: home
algorithm: hmac-sha512
secret: cVE3CZ0twnAmf6C7suhr8nJQTtdqo8RVmJkOC0WL5luandWazPrY7AtQJAEkE86HwzOcTla187fInNYZ5KkpPg==
key的作用是管理DNS记录。如果没有相关需求,其实也可以忽略。不过我后面有自己设立一个DDNS服务。
为了让DNS工作正常,还需要添加SOA记录。
# /var/lib/knot/internal.zone
internal. 30 SOA ns.infra.internal. nomail.ns.infra.internal. 2025071601 3600 15 86400 3600
internal. 30 NS ns.infra.internal.
# /var/lib/knot/infra.internal.zone
infra.internal. 30 SOA ns.infra.internal. nomail.ns.infra.internal. 2025071601 3600 15 86400 3600
infra.internal. 30 NS ns.infra.internal.
# /var/lib/knot/home.internal.zone
home.internal. 30 SOA ns.infra.internal. nomail.ns.infra.internal. 2025071601 3600 15 86400 3600
home.internal. 30 NS ns.infra.internal.
用smartdns做中间件分流域名解析¶
做就要做彻底。要避免内网DNS请求发送到意外的地方去,就让smartdns处理好分流。
先开个smartdns在5552端口上:
# /etc/smartdns/smartdns.conf
# smartdns configuration
bind [::]:5552
# this name is the name of this server
# the optimal address of current host will be sent if querying this one
server-name ns.infra.internal
# same with above, but can be specified multiple times
ddns-domain anything.ns.infra.internal
server 119.29.29.29
server 223.5.5.5
# internal zone is handled by knot at 5553
server 127.0.0.1:5553 -group internal_knot -exclude-default-group
# including: infra.internal, home.internal
nameserver /internal/internal_knot
# somehow setting rr-ttl-min is required, otherwise the ttl will be set to 600
domain-rules /*.internal/ -no-cache -no-serve-expired -response-mode fastest-response -rr-ttl-min 30 -speed-check-mode none
再用nftables把内网指向本机53的流量劫持过来(不然端口分配有得烦):
# in /etc/nftables.conf
table inet simple_port_forward
flush table inet simple_port_forward
delete table inet simple_port_forward
table inet simple_port_forward {
define INTERNAL_INTERFACES = {
"vlan-sweethome",
"vlan-oeolab",
"vlan-infra"
}
chain local_hijack {
# Hijack DNS requests sent to this router
# redirect dns requests to smartdns instead of dnsmasq
# smartdns will forward back per subnet resolution to dnsmasq
tcp dport 53 redirect to :5552
udp dport 53 redirect to :5552
}
chain prerouting {
# priority: dstnat
type nat hook prerouting priority -100 ;
# internal requests sent to this router
iifname $INTERNAL_INTERFACES ip daddr & 255.0.255.255 == 10.0.0.1 jump local_hijack
iifname $INTERNAL_INTERFACES ip6 daddr & ffff::ffff:ffff:ffff:ffff == fcf4::1 jump local_hijack
}
}
这样smartdns就全权负责局域网的所有DNS请求啦,一切尽在掌握!
注意当经由dnsmasq向smartdns请求所配置的ddns-domain
、server-name
时会让结果变成回环地址,所以不建议走dnsmasq中转DNS请求到smartdns。
让本机的systemd-resolved也转发internal tld请求到自己的Knot DNS¶
因为netplan/systemd-networkd,默认用的DNS服务器是systemd-resolved,本机要额外考虑一下。
新增配置文件,注意IP地址要匹配:
# /etc/systemd/resolved.conf.d/dns_internal.conf
[Resolve]
DNS=127.0.0.1:5552
Domains=~.internal.
部分保留dnsmasq的主机名查询能力¶
只能保留本地域名的查询能力。
在/etc/smartdns/smartdns.conf
中添加下面的内容,注意IP地址要匹配:
# in /etc/smartdns/smartdns.conf
# Some requests are forwarded to related dnsmasq instance
#server 10.101.0.1:53 -group dq_infra -exclude-default-group
server 10.102.0.1:53 -group dq_home -exclude-default-group
server 10.123.0.1:53 -group dq_lab -exclude-default-group
domain-rules /*.home/ -g dq_home -no-cache -no-serve-expired
nameserver /home/dq_home
domain-rules /*.lab/ -g dq_lab -no-cache -no-serve-expired
nameserver /lab/dq_lab
设立自己的DDNS服务¶
直接用TSIG key有点麻烦。看到别人自己搓了个简单的DDNS服务,我也搓了一个。大概是从http请求中获取key和需要更新的域名、直接取请求来源的IP地址,自动更新域名信息。我的参考实现:ddns-server.py。
参考Config:
# config.toml
knot_key = "hmac-sha512:home:your_key_data“
参考更新指令:
# generate a new token, can be reused later
curl http://ns.infra.internal:5561/new_token/prefix.home.internal/the_super_token
# update DNS A record, the new ip address is the request source address
curl -4 http://ns.infra.internal:5561/update_ddns/test_name/the_token
结语:懒得写了¶
后续我进一步部署了Step CA、Prometheus等服务。
在我自己具体的实施中,有一些配置文件安排的细节,比如拆分文件到较小单元(nftables、smartdns),想全部写到文章里显得太冗杂。
啊不写了不写了。
参考资料¶
- https://lars.hupel.info/articles/personal-dyndns/
- https://blog.sww.moe/post/soft-router-network/#%E9%85%8D%E7%BD%AE-dhcpv6-pd
- https://major.io/p/dhcpv6-prefix-delegation-with-systemd-networkd/
- https://wiki.debian.org/IPv6PrefixDelegation
- https://wiki.archlinux.org/title/Router
- https://briandouglas.ie/sqlite-defaults/