前言

本文记录了我在 NEC 8代小主机上折腾 PVE 的过程,也希望能为想搭建类似内外网环境的朋友提供一些思路

需求

使用 vconet.top 这个我拥有的域名,给本地服务都用上 SSL、同时异地组网 实现在外访问本地服务,还需要把一些服务公布到互联网上

与此同时,还要保证在各个网络环境下都能保持一致的 URL

配置 PVE

PVE 的安装和基本配置教程网上有很多,不再复述

NAT!

出于种种原因,我在 PVE 上额外划分了一个 10.22.33.0/24 网段,用于虚拟机和 LXC 容器

配置 SDN

创建 Simple 区域

转到 数据中心 ➡️ SDN ➡️ 区域 ➡️ 添加 ➡️ Simple

填一个名称,勾选高级并启用自动 DHCP

创建 VNets

转到 数据中心 ➡️ SDN ➡️ VNets ➡️ 创建

填写名称(刚才创建的 Simple 名称)

创建子网

VNets 右边的子网创建新子网,填写子网网段和网关,在 DHCP 范围选项卡内填写 DHCP 的 IP 范围

应用更改

转到 数据中心 ➡️ SDN,点击左上角的应用

启用 DHCP

要使 PVE 能真正的提供 DHCP 服务,需要在 PVE 上安装 dnsmasq 并禁用自带的服务

1
2
apt install dnsmasq
systemctl disable --now dnsmasq
自定义 DNS

因为内外网的 DNS 记录不一样,需要给10.22.33.0/24网段内的虚拟机和 LXC 提供自定义的 DNS 服务器,我将会在下文给出的 Co re + Powerdns 方案

为了让 dnsmasq 能在提供 DHCP 的同时,提供 自定义 DNS,需要编辑 /etc/pve/sdn/subnets.cfg 文件,添加 dhcp-dns-server 选项

1
2
3
4
5
6
subnet: pvenet-10.22.33.0-24
vnet vnet
dhcp-range start-address=10.22.33.2,end-address=10.22.33.253
gateway 10.22.33.1
snat 1
dhcp-dns-server 10.22.33.200 #你所需的 DNS 服务器 IP
防火墙

目前我并没有防火墙的需求,暂时跳过

内网域名解析

为了提供 vconet.top 等 TLD 域名的内部解析,需要一个自己的 DNS 服务器,同时还要方便维护

可以实现上面需求的,可选方案有很多:CoreDNS(两种方案)、Powerdns、TechnitiumDNS,甚至 小米路由器自带的 自定义 Hosts 功能也能满足需求

自定义 DNS 怎么选?

简单记录

如果你只需要简单的 xx.vconet.top对应 ip,那么大可选择小米路由器自带的 自定义 Hosts 功能,亦或是 CoreDNS(方案一)

该方案用到 CoreDNS的 hosts功能:

1
2
3
4
5
6
7
8
9
10
.:53 {
# lan.hosts为你需要的解析,格式和 hosts 文件一致
hosts /etc/CoreDNS/lan.hosts {
fallthrough
}
forward . 223.5.5.5 119.29.29.29
log
cache
errors
}
复杂记录

如果不仅仅需要 A 记录,还需要 PTR、MX 等,就需要 CoreDNS(方案二)、Powerdns、Technitiumdns 这些权威 DNS 了

CoreDNS(方案二)需要用到 CoreDNS 的 file功能:

1
2
3
4
5
6
7
8
9
10
. {
forward . 223.5.5.5
log
errors
}
vconet.top {
file /etc/CoreDNS/DOMAIN.zone #域名的Zone文件
log
errors
}

Powerdns、Technitiumdns 有对应的 Web 管理页面,直接添加就可

如果选择权威 DNS 给 vconet.top 做解析,如果本地 DNS 的记录不全,解析时会返回 NXDOMAIN

权威 DNS 在查询到不存在的记录时,会直接返回 NXDOMAIN,而不会继续向上递归查询,这可能导致内外网记录解析失败:

flowchart LR
    ldns --本地IP--> id1[Client]
    id1 --"local.vconet.top(存在于本地DNS)"-->ldns["本地 DNS"]
    
    ldns--NXDOMAIN-->id2[Client]
    id2 --"blog.vconet.top(不存在于本地)"--> ldns
    ldns x--"向上查找blog.xx"--x rdns["公共 DNS"]
解决 NXDOMAIN
  • 对于 Technitiumdns,你可以在创建新域名的时候,选择 条件转发

  • 对于 CoreDNS(方案二),需要自己编译含有 alternate 插件的 CoreDNS:

    1
    2
    3
    4
    5
    6
    vconet.top {
    file /etc/CoreDNS/DOMAIN.zone #域名的Zone文件
    alternate NXDOMAIN . 223.5.5.5 119.29.29.29
    log
    errors
    }
我的方案

因需要给 PVE 提供 DNS 相关服务,只能选择 Powerdns 解析内网记录,而 Powerdns 本身只用能作权威 DNS,还需要搭配 powerdns-recurosr 之类的前置

recursor 并不能非常方便的解决上面 NXDOMAIN 的问题,因此我选择 CoreDNS 而非 powerdns-recursor

CoreDNS 配置:

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
26
27
28
29
30
31
32
33
34
35
36
####################
# 修改 /etc/coredns/Corefile 文件
# 127.0.0.1:54 为 Powerdns
####################

vconet.top {
forward . 127.0.0.1:54 {
policy sequential
}
alternate NXDOMAIN . 223.5.5.5 119.29.29.29
log
errors
}

####################
# 这两部分是用于 PVE 实例的反向查询和内网域名解析
33.22.10.in-addr.arpa {
forward . 127.0.0.1:54
log
errors
}
pve.lan {
forward . 127.0.0.1:54
log
errors
}
####################

# 其他请求
.:53 {
#bind 10.22.33.200
forward . 223.5.5.5 119.29.29.29
cache
log
errors
}

Powerdns 配置:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 在 /etc/powerdns/pdns.conf 下追加下面这一段

# AXFR
allow-axfr-ips=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
# DNS UPDATE
allow-dnsupdate-from=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
# DNS Notify
allow-notify-from=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
# 是否允许未签名 Notify
allow-unsigned-notify=no
# 额外通知服务器
#also-notify=10.0.0.10,192.168.31.10,[fc00::1]

#daemon 启动
daemon=yes
disable-axfr=no
guardian=no
#是否master
master=yes
#是否slave
slave=no
#启动权限
setgid=pdns
setuid=pdns
#打印日志
log-dns-details=yes
log-dns-queries=no
loglevel=6
log-timestamp=yes
logging-facility=0
#开启api
api=yes
api-key=非常长的随机字符串
#启动webserver 监控
webserver=yes
webserver-address=0.0.0.0
webserver-allow-from=0.0.0.0/0
webserver-port=9190
#监听的地址端口
local-address=0.0.0.0
local-port=54
#记录query 日志
query-logging=yes

请把api-keylocal-portwebserver-port修改为自己所需的值

打通内外网

在这一部分开始之前,我需要介绍一下我的网络拓展:

flowchart TD
    %% ISP 到家庭网络
    ISP((ISP))
    WIFI[小米路由器
192.168.31.0/24] %% 局域网 NB[笔记本
192.168.31.10] PVE[PVE 宿主机
192.168.31.210] %% PVE 内部网络 SUBNET1[[内部网络
10.22.33.0/24]] VM1["虚拟机1(核心)
10.22.33.200"] VM2["虚拟机2(Docker服务)"] LXC1["LXC 1"] LXC2["LXC 2"] %% ZeroTier 网络 ZT[[ZeroTier 网络
192.168.192.0/24]] %% 拓扑关系 ISP --> WIFI WIFI --> NB WIFI --> PVE PVE --> SUBNET1 SUBNET1 --> VM1 SUBNET1 --> VM2 SUBNET1 --> LXC1 SUBNET1 --> LXC2 VM1 <--> ZT

本地访问

在前面配置中,我们已经在 NAT 环境下新增了一层虚拟网络

接下来要解决的问题是:如何让 192.168.31.0/24 网段内的设备能够访问 10.22.33.0/24 网段的虚拟机和 LXC 容器?

静态路由

什么是路由?

网络路由是选择一个或多个网络上的路径的过程。路由原理可以应用于从电话网络到公共交通的任何类型的网络。在诸如互联网等数据包交换网络中,路由选择互联网协议 (IP) 数据包从其起点到目的地的路径。这些互联网路由决定由称为路由器的专用网络硬件做出。

为了实现这个需求,需要在自己的路由器里添加一条静态路由:

目标地址10.22.33.0网关为 PVE 的 IP 192.168.31.210掩码255.255.255.0

小米 AX3000T 在开启 ssh 服务后,编辑 /etc/config/network和防火墙 /etc/config/firewall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# /etc/config/network
# 添加新的静态路由,格式如下
config route
option interface 'lan'
option target '10.22.33.0'
option netmask '255.255.255.0'
option gateway '192.168.31.210'

# /etc/config/network
# 请看准是否为 defaults 部分的防火墙配置!
# 将 forward 改为 ACCEPT
config defaults
option syn_flood '0'
option input 'ACCEPT'
option output 'ACCEPT'
option forward 'ACCEPT'
...

请自行查找其他路由器添加静态路由的方法

生效后,Linux 效果如下

1
2
3
user@archlinux$ route -n
Destination Gateway Genmask Flags Metric Ref Use Iface
10.22.33.0 192.168.31.210 255.255.255.0 UG 5000 0 0 wlp9s0

这样我们就实现了 192.168.31.0/24 网段访问 10.22.33.0/24 网段

flowchart LR
    id1["笔记本 192.168.31.10"] --"ping 10.22.33.200"---> id2["路由器 192.168.31.1"] --"下一跳192.168.31.210"-->id3["PVE .31.210和.33.1"]
    id3 --获得请求--> id4["虚拟机1(核心)10.22.33.200"] ---> id1

异地组网

异地组网可选的方案有很多,常用的有 TailscaleZeroTier,在此我选择功能更少的 ZeroTier

Tailscale 有很多独特的功能,其中就有内网 HTTPS,但这个功能不够强大,而且和下面配置的 Nginx 反代服务重复了

上面提到,我的 ZeroTier 是安装在 虚拟机1(核心)里的,这就意味着 当我的手机连接到 ZeroTier 后,只能访问 192.168.192.31这个 由 ZT 提供的 IP,并不能访问 10.22.33.0/24 网段

解决问题的办法其实很简单,在 ZT 的后台添加一条新的静态路由,但这次的网关是 ZT 提供给虚拟机1(核心)的 IP

ZT添加路由
ZT添加路由

但此时,我们还并不能 ping 通,因为从 ZT 发来的请求,PVE 接收到后并不知道返回的路由

因此需要在 PVE 宿主机上添加一条路由:

目标地址为 192.168.192.0/24 (ZeroTier 的网段),网关为 10.22.33.200(虚拟机 1 的 IP)

#

添加为系统服务,自动添加和删除

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=Static route for Zerotier
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/sbin/ip route add 192.168.192.0/24 via 10.22.33.200
ExecStop=/sbin/ip route del 192.168.192.0/24 via 10.22.33.200
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

最后,启用 虚拟机1(核心)的转发功能:

#

至此,在本地或使用 ZeroTier 时,都可以访问 10.22.33.0/24 网段的设备了

这么做也避免了给同一个域名添加两个不同网段 IP 的麻烦,方便维护

外网访问

当下,直接开放端口非 80/443 端口虽可以实现,但这么做容易暴露自己的 IP

一个解决办法是使用 CDN 回源,但免费版 Cloudflre 的 Origin Rule 只能有三条回源规则,数量有限还需要手动维护 IP 记录

那还有没有什么别的办法呢?

有的。那就是赛博活佛 Cloudflare 的 Zero Trust,Zero Trust 中的隧道 功能可以把内网服务公布到互联网上:

CF隧道
CF隧道

cloudflared 的安装和设置 在创建完隧道后有详细的介绍,不再赘述

各种服务

以下是我在内外网打通后部署的一些服务,属于扩展内容,可按需参考

略去了很多设置过程

基础服务

上文曾多次提到虚拟机1(核心),这台虚拟机承担了我内网环境的 Nginx 反代、DNS 服务器和 ZeroTier 组网

Nginx 反代

我尝试过 Nginx Proxy Manager 的网页管理,但决定手动管理,因其可选项比较少,不能灵活配置

Debian 系统可以直接安装 nginx 包和 nginx-full

#

在安装完后,按照下面的反代模板添加配置文件即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
listen 8443 ssl;
server_name pve.vconet.top;

ssl_certificate /root/.acme.sh/*.vconet.top_ecc/fullchain.cer;
ssl_certificate_key /root/.acme.sh/*.vconet.top_ecc/*.vconet.top.key;
add_header Strict-Transport-Security "max-age=31536000;";

proxy_redirect off;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass https://10.22.33.1:8006;
proxy_buffering off;
client_max_body_size 0;
proxy_connect_timeout 3600s;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
send_timeout 3600s;
}
}

我在这里使用8443而非443端口的原因在下面 Forgejo 部分有解释

DNS 服务器与 PVE 整合

在前面,我配置好了 Powerdns,下面是 PVE 启用 DNS 插件,实现自动添加虚拟机解析记录和 PTR 记录的方法

添加 DNS 插件

转到 数据中心 ➡️ SDN ➡️ 选项 ➡️ 添加

填写对应的 Powerdns API 地址和 API 密钥

补全 Simple 的信息

转到 数据中心 ➡️ SDN ➡️ 区域 ➡️ 添加 ➡️ Simple
勾选高级,补全下面的信息

在填写DNS 域之前,请在 powerdns 内创建此域

Docker 服务

使用 Docker 是因为方便更新和管理

自动追番

Emby

Emby 的 docker-compose 如下:

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
26
27
services:
emby:
container_name: emby
image: emby/embyserver
restart: always
devices:
- /dev/dri:/dev/dri # 挂载显卡用于转码
ports:
- 8096:8096
- 1900:1900/udp
volumes:
- ./emby:/config
- /mnt/emby:/mnt/emby
fontinass:
image: riderlty/fontinass:noproxy
container_name: fontinass
environment:
- PUID=100000
- PGID=100000
- EMBY_SERVER_URL=http://emby:8096
ports:
- 8011:8011
volumes:
- ./fonts:/fonts
- ./fiadata:/data
depends_on:
- emby

为了实现 Emby 观看番剧时,自动加载 ass 字幕所需的字体,避免本地安装 需要用到 fontInAss 并使用下面的 Nginx 反代

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
server {
listen 8012; #新的Emby访问端口
gzip on;
gzip_http_version 1.0;
gzip_comp_level 1;
gzip_types text/x-ssa;

location ~ /(socket|embywebsocket) {
proxy_pass $EMBY_SERVER_URL;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
}

location ~* /videos/(.*)/Subtitles/(.*)/Stream.vtt {
#适配emby网页播放SRT字幕,302直链时避免冲突
proxy_pass $EMBY_SERVER_URL;
}

location ~* /videos/(.*)/Subtitles/(.*)/(Stream.ass|Stream.srt|Stream) {
#仅匹配ass与srt字幕文件,Stream适配infuse
#修改为你的fontinass服务器地址
proxy_pass http://127.0.0.1:8011;
}

location ~* /web/bower_components/(.*)/subtitles-octopus.js {
#修改为你的fontinass服务器地址
#如不需要修改web端渲染,可删除此location
proxy_pass http://127.0.0.1:8011;
}

location ~* /web/modules/htmlvideoplayer/plugin.js {
#修改为你的fontinass服务器地址
#仅当需要web渲染,且通过https访问时,才需启用此location,否则可删除
#见 https://github.com/RiderLty/fontInAss/issues/43
proxy_pass http://127.0.0.1:8011;
}

location / {
#修改为你的Emby/Jellyfin服务器地址
proxy_pass $EMBY_SERVER_URL;
}
}
下载器

使用 qBittorrentEEPeerBanHelper

PeerBanHelper 用于阻止迅雷等吸血客户端

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
26
27
28
services:
qbittorrentee:
image: superng6/qbittorrentee:latest
container_name: qbittorrentee
environment:
- TZ=Asia/Shanghai
- WEBUIPORT=8080
- ENABLE_DOWNLOADS_PERM_FIX=true
volumes:
- ./qbitcnf:/config
- /mnt/emby:/downloads/
ports:
- 6881:6881
- 6881:6881/udp
- 8080:8080
restart: always
peerbanhelper:
image: ghostchu/peerbanhelper:latest
restart: unless-stopped
container_name: pbh
volumes:
- ./pbhcnf:/app/data
ports:
- 9898:9898
environment:
- TZ=Asia/Shanghai
depends_on:
- qbittorrentee
ASS自动追番

AniRSS 的使用,请阅读官方文档

Forgejo 反代下同域名 ssh

Git 服务器我选择使用 Gitea 的 fork:Forgejo
Forgejo 的安装 Gitea/Forgejo 都有详细的文档,可自行搜索

在这一节中,我需要解决一个问题:由于 Nginx 反代服务器和 Forgejo 并不在同一台虚拟机上,默认情况下我必须为 SSH 使用不同的域名。

经过探索,我发现可以通过 Nginx 的 stream 模块,在同一域名下根据流量类型分别提供 HTTP 反代和 SSH 反代。

nginx.confhttp 段前添加这样一段:

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
stream {
upstream https_backend {
server 127.0.0.1:8443; # 所有HTTPS流量转发到8443
}

upstream ssh_backend {
server 10.22.33.203:22; # SSH流量转发到目标服务器
}

# 基于协议检测进行流量分发
# SSL握手包会被检测为HTTPS,非SSL流量被认为是SSH
map $ssl_preread_protocol $upstream {
"" ssh_backend; # 非SSL流量 -> SSH
~. https_backend; # SSL流量 -> HTTPS
}

server {
listen 443;
ssl_preread on;
proxy_pass $upstream;
#proxy_timeout 10s;
#proxy_responses 1;
error_log /var/log/nginx/stream.log;
}
}

这也是我在前面选择使用 8443 端口的原因

虽然和使用单独的域名比,这个方法略显麻烦,但在此记录这个解决办法

结尾

至此,我的 NEC8 小主机在 PVE 上的内外网打通和基础服务搭建就告一段落了。希望本文能为有类似需求的朋友提供一些参考