家里云网络基础设施

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-homevlan-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-domainserver-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/