Landscape Router 使用实践 (长期更新)
Renew | 更新
V.0.18.0
V.0.18.0 更新了 DDNS 功能
以托管在 Cloudflare 为例配置项如下所示

这里我们只需要获取 API TOKEN 一项
Cloudflare 有以下两种选择二选一即可
- 用户 API 令牌
- 账户 API 令牌
创建 DDNS 如下图所示

效果如下所示
V4

V6

1.First | 序
本篇着重记录个人部署和使用 Landscape Router 一些实践
承接以下内容且预计长期更新

2. Type | 类型
以下类型可能会有依赖和联系
一家之言仅供参考
2.1. Directory Tree | 目录树
Landscape Router 文件我将其放置在 /opt/LandscapeRouter 目录下
╭────┬────────────────────────────────────┬─────────┬──────────┬────────────────╮
│ # │ name │ type │ size │ modified │
├────┼────────────────────────────────────┼─────────┼──────────┼────────────────┤
│ 0 │ cert.pem │ file │ 534 B │ 2 weeks ago │
│ 1 │ container_apps │ dir │ 338 B │ 4 days ago │
│ 2 │ geo_tmp │ dir │ 12 B │ 2 weeks ago │
│ 3 │ key.pem │ file │ 241 B │ 2 weeks ago │
│ 4 │ landscape-webserver-x86_64 │ file │ 51.0 MB │ 3 weeks ago │
│ 5 │ landscape-webserver-x86_64-latest │ symlink │ 34 B │ 19 minutes ago │
│ 6 │ landscape-webserver-x86_64-v0.10.0 │ file │ 51.1 MB │ 2 weeks ago │
│ 7 │ landscape-webserver-x86_64-v0.10.1 │ file │ 51.1 MB │ a week ago │
│ 8 │ landscape.toml │ file │ 30 B │ 2 weeks ago │
│ 9 │ landscape_api_token │ file │ 125 B │ 11 hours ago │
│ 10 │ landscape_db.sqlite │ file │ 212.9 kB │ 2 days ago │
│ 11 │ landscape_init.lock │ file │ 683 B │ 2 weeks ago │
│ 12 │ landscape_init.toml │ file │ 7.1 kB │ 2 weeks ago │
│ 13 │ logs │ dir │ 288 B │ 11 hours ago │
│ 14 │ static │ dir │ 32 B │ 3 weeks ago │
│ 15 │ static-latest │ symlink │ 14 B │ 2 minutes ago │
│ 16 │ static-v0.10.0 │ dir │ 32 B │ 2 weeks ago │
│ 17 │ static-v0.10.1 │ dir │ 32 B │ 8 minutes ago │
│ 18 │ unix_link │ dir │ 26 B │ 11 hours ago │
╰────┴────────────────────────────────────┴─────────┴──────────┴────────────────╯
-
symlink
因为
Landscape Router更新迭代速度较快
所以我为 landscape-webserver & static 文件分别创建了软链接
当程序更新时只需重新指向而无需频繁修改 .service 文件 -
container_apps
此文件夹用于存放 Landscape Router 所需的(流出口)容器文件
2.2. .service
.service 文件官方给的比较简约
[Unit]
Description=Landscape Router
[Service]
ExecStart=/root/landscape-webserver <- 记得修改此处
Restart=always
User=root
LimitMEMLOCK=infinity
[Install]
WantedBy=multi-user.target
自定义版本如下所示:
[Unit]
Description=Landscape - Linux Router Configuration Tool
Documentation=https://github.com/ThisSeanZhang/landscape
After=network.target podman-docker-socket.service
Wants=podman-docker-socket.service
StartLimitIntervalSec=24h
StartLimitBurst=240
[Service]
User=root
Type=simple
Environment="LANDSCAPE_CONF_PATH=/opt/LandscapeRouter"
Environment="LANDSCAPE_WEB_ROOT=/opt/LandscapeRouter/static-latest"
Environment="LANDSCAPE_WEB_ADDR=0.0.0.0"
Environment="LANDSCAPE_WEB_HTTPS_PORT=6443"
Environment="LOG_OUTPUT_IN_TERMINAL=true"
Environment="RUST_BACKTRACE=1"
Environment="LANDSCAPE_ADMIN_USER=xzllll"
Environment="LANDSCAPE_ADMIN_PASS=xzllll.com"
Environment="LANDSCAPE_DEBUG=true"
ExecStart=/opt/LandscapeRouter/landscape-webserver-x86_64-latest
ExecStartPost=/bin/bash -c 'sleep 5; for k in 1 2 3; do if [ -d "/sys/class/net/ppp-enp1s0f0-CU" ]; then exit 0; fi; if [ $k -lt 3 ]; then sleep 20; fi; done; exit 1'
TimeoutStartSec=90
Restart=on-failure
RestartPreventExitStatus=23
RestartSec=120
LimitMEMLOCK=infinity
CPUSchedulingPolicy=rr
CPUSchedulingPriority=50
[Install]
WantedBy=multi-user.target
2.2.1. [unit]
-
After= & Wants=
After=network.target podman-docker-socket.service Wants=podman-docker-socket.service
因为我使用 Podman 而不是 Docker 来运行容器
所以这里显示依赖 podman-docker-socket.service 服务
此服务
.service文件如下[Unit] Description=Create Symlink from podman.sock to docker.sock After=podman.socket Requires=podman.socket [Service] Type=simple ExecStartPre=-/bin/rm -f /var/run/docker.sock ExecStart=/bin/bash -c 'while true; do [ -L /var/run/docker.sock ] || ln -sf /run/podman/podman.sock /var/run/docker.sock; sleep 2; done' ExecStartPost=/bin/bash -c 'for i in {1..20}; do [ -L /var/run/docker.sock ] && exit 0; sleep 0.5; done; exit 1' Restart=always [Install] WantedBy=multi-user.target可以看到此服务仅仅用来创建并循环验证
.sock以供 Landscape Router 使用 -
StartLimitIntervalSec= & StartLimitBurst=
StartLimitIntervalSec=24h StartLimitBurst=240
以上是一些 启动逻辑 且适用于所有形式的启动
- 由 Restart=on-failure 触发的自动重启
- 管理员手动执行的 systemctl start
- 由 Timer(定时器)或 Socket(套接字)触发的激活
2.2.2. [Service]
-
Environment=
这里我选择以环境变量的形式传递配置而不直接写进
landscape.toml可用的环境变量如下:
Usage: landscape-webserver-x86_64-v0.10.0 [OPTIONS] [COMMAND] Commands: db Database-related operations help Print this message or the help of the given subcommand(s) Options: -w, --web <WEB> Static HTML location [default: /root/.landscape-router/static] [env: LANDSCAPE_WEB_ROOT=] -p, --port <PORT> Listen HTTP port [default: 6300] [env: LANDSCAPE_WEB_HTTP_PORT=] -s, --https <HTTPS_PORT> Listen HTTPS port [default: 6300] [env: LANDSCAPE_WEB_HTTPS_PORT=] -a, --address <ADDRESS> Listen address [default: 0.0.0.0] [env: LANDSCAPE_WEB_ADDR=] -e, --export-manager Controls whether the WAN IP can be used to access the management interface [default: false] -c, --config-dir <CONFIG_DIR> All Config DIR, Not file Path [default: /root/.landscape-router] [env: LANDSCAPE_CONF_PATH=] --log_path <LOG_PATH> Log File location [default: /root/.landscape-router/logs] [env: LANDSCAPE_LOG_PATH=] --db_url <DATABASE_PATH> Database URL, SQLite Connect Like Default sqlite://<path> [default: sqlite:///root/.landscape-router/landscape_db.sqlite] [env: DATABASE_URL=] --ebpf-map-space <EBPF_MAP_SPACE> ebpf map space [default: default] [env: LANDSCAPE_EBPF_MAP_SPACE=] [default: default] --user <ADMIN_USER> Manager user [default: root] [env: LANDSCAPE_ADMIN_USER=] --pass <ADMIN_PASS> Manager pass [default: root] [env: LANDSCAPE_ADMIN_PASS=] --debug <DEBUG> Debug mode [default: false] [env: LANDSCAPE_DEBUG=] [possible values: true, false] --log-output-in-terminal <LOG_OUTPUT_IN_TERMINAL> Log output location [default: false] [env: LANDSCAPE_LOG_PATH=] [possible values: true, false] --max-log-files <MAX_LOG_FILES> Max log files number [default: 7] [env: LANDSCAPE_LOG_FILE_LIMIT=] -h, --help Print help -V, --version Print version -
ExecStart=
启动 Landscape Router 时执行的命令
-
ExecStartPost= & TimeoutStartSec=
Landscape Router 目前在 ppp server 发生以下问题时没有相应的重播机制
- Remote message: Limit Users Err
- Remote message: Passwd_Err
- Timeout waiting for PADO packets
所以需要加入一些 启动校验和重启逻辑
-
LimitMEMLOCK=infinity
解除此进程对 MEMLOCK 额度限制
若有需要可以通过 cgroup v2 限制此值
-
CPUSchedulingPolicy= & CPUSchedulingPriority=
使用实时调度策略 RR(Round-Robin)
实时调度优先级(1–99)这里设置为50
2.2.3. [Install]
-
WantedBy=
WantedBy=multi-user.target在系统进入 multi-user.target 时自动启动
2.3. Podman
Podman 可以配合 Landscape Router 使用
但需要提供 .sock 文件 (2.2.1 小节已描述)
另外如果你需要用到 Landscape Router 分流控制 将特定流量导入至容器进行处理
那么必须查看 nft 规则, 因为 Podman 会使用 NETAVARK 自动生成 nft rules 且默认会在 netavark table 中显示设置 ct state invalid drop
chain FORWARD {
type filter hook forward priority filter; policy accept;
ct state invalid drop
jump NETAVARK-ISOLATION-1
ip daddr 10.88.0.0/16 ct state established,related accept
ip saddr 10.88.0.0/16 accept
}
暂时不清楚为什么进入容器的数据包被标记为 invalid 并随即丢弃
所以我们需要在 FORWARD 链的最顶部插入一条规则以便强制放行从容器回来的流量
chain FORWARD {
type filter hook forward priority filter; policy accept;
iifname "podman0" oifname "enp1s0f3" accept
ct state invalid drop
jump NETAVARK-ISOLATION-1
ip daddr 10.88.0.0/16 ct state established,related accept
ip saddr 10.88.0.0/16 accept
}
其中 enp1s0f3 为 LAN 口网卡名
[Unit]
Description=Fix Podman Netavark Routing for Local LAN
After=podman.service
Wants=podman.service
PartOf=podman.service
[Service]
Type=oneshot
RemainAfterExit=yes
Environment="NFT_FAMILY=inet"
Environment="NFT_TABLE=netavark"
Environment="NFT_CHAIN=FORWARD"
Environment="IF_IN=podman0"
Environment="IF_OUT=enp1s0f3"
Environment="NFT_COMMENT=custom-lan-route-fix"
ExecStartPre=-/bin/sh -c 'nft -a list chain ${NFT_FAMILY} ${NFT_TABLE} ${NFT_CHAIN} \
| grep "${NFT_COMMENT}" \
| grep -oE "handle [0-9]+" \
| cut -d " " -f 2 \
| xargs -r -n 1 nft delete rule ${NFT_FAMILY} ${NFT_TABLE} ${NFT_CHAIN} handle'
ExecStart=/usr/sbin/nft insert rule ${NFT_FAMILY} ${NFT_TABLE} ${NFT_CHAIN} index 0 iifname "${IF_IN}" oifname "${IF_OUT}" accept comment "${NFT_COMMENT}"
ExecStop=/bin/sh -c 'nft -a list chain ${NFT_FAMILY} ${NFT_TABLE} ${NFT_CHAIN} \
| grep "${NFT_COMMENT}" \
| grep -oE "handle [0-9]+" \
| cut -d " " -f 2 \
| xargs -r -n 1 nft delete rule ${NFT_FAMILY} ${NFT_TABLE} ${NFT_CHAIN} handle'
[Install]
WantedBy=multi-user.target
nushell 版[Unit]
Description=Fix Podman Netavark Routing for Local LAN (Nushell Optimized)
After=podman.service
Wants=podman.service
PartOf=podman.service
[Service]
Type=oneshot
RemainAfterExit=yes
Environment="NFT_FAMILY=inet"
Environment="NFT_TABLE=netavark"
Environment="NFT_CHAIN=FORWARD"
Environment="IF_IN=podman0"
Environment="IF_OUT=enp1s0f3"
Environment="NFT_COMMENT=custom-lan-route-fix"
ExecStartPre=-/usr/bin/nu -c 'nft -j -a list chain ${NFT_FAMILY} ${NFT_TABLE} ${NFT_CHAIN} \
| from json \
| get nftables.rule? \
| compact \
| where comment? == "${NFT_COMMENT}" \
| get handle \
| each { |h| nft delete rule ${NFT_FAMILY} ${NFT_TABLE} ${NFT_CHAIN} handle $$h }'
ExecStart=/usr/sbin/nft insert rule ${NFT_FAMILY} ${NFT_TABLE} ${NFT_CHAIN} index 0 iifname "${IF_IN}" oifname "${IF_OUT}" accept comment "${NFT_COMMENT}"
ExecStop=/usr/bin/nu -c 'nft -j -a list chain ${NFT_FAMILY} ${NFT_TABLE} ${NFT_CHAIN} \
| from json \
| get nftables.rule? \
| compact \
| where comment? == "${NFT_COMMENT}" \
| get handle \
| each { |h| nft delete rule ${NFT_FAMILY} ${NFT_TABLE} ${NFT_CHAIN} handle $$h }'
[Install]
WantedBy=multi-user.target
podman0
众所周知: 当容器有相关需求时Podman会在宿主机上创建虚拟网卡 podman0
[
{
"name": "podman",
"id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9",
"driver": "bridge",
"network_interface": "podman0",
"created": "2026-01-12T08:08:45.444877976+08:00",
"subnets": [
{
"subnet": "10.88.0.0/16",
"gateway": "10.88.0.1"
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": false,
"ipam_options": {
"driver": "host-local"
}
]
目前版本(0.10.1 ( UI 版本: 0.10.1 ))发现: 当你在 Landscape Router UI 界面配置了 podman0 虚拟网卡软负载后 Landscape Router 会接管此网卡从而导致 podman0 IP 地址 reset Container 没有网关 Host 没有路由
DEBUG landscape: setting podman0 to mode: Undefined
INFO landscape::service::route_wan: restart podman0 Route Wan service
INFO landscape::service::ipconfig: restart podman0 IfaceIp service
INFO landscape::service::route_lan: restart podman0 RouteLan service
当前解决方案如下:
-
重新配置网卡软负载为 0此方案无法解决,因为配置文件中仍会体现此项
-
在 UI 指定
网卡运行服务配置,干脆让 Landscape 接管此网卡[[ipconfigs]] iface_name = "podman0" enable = true update_at = 1766807376555.0 [ipconfigs.ip_model] t = "static" default_router_ip = "x.x.x.x" default_router = false ipv4 = "10.88.0.1" ipv4_mask = 16 -
如若你能够舍弃
网卡软负载功能的话,你可以在 UI 界面导出 landscape_init.toml 文件并删除掉里面有关 podman0 的配置(别忘了导入新配置)[[ifaces]] name = "podman0" create_dev_type = "bridge" zone_type = "undefined" enable_in_boot = true wifi_mode = "undefined" update_at = 1766619881071.0 [ifaces.xps_rps] xps = "0" rps = "0" [[ifaces]] name = "veth0" create_dev_type = "no_need_to_create" zone_type = "undefined" enable_in_boot = true wifi_mode = "undefined" update_at = 1766619888167.0 [ifaces.xps_rps] xps = "0" rps = "0" [[ipconfigs]] iface_name = "podman0" enable = true update_at = 1766807376555.0 [ipconfigs.ip_model] t = "static" default_router_ip = "x.x.x.x" default_router = false ipv4 = "10.88.0.1" ipv4_mask = 16此配置内容因人而异,如果你并未让 Landscape Router 接管 Podman0 那么就不会有
ipconfigs.~~配置项,完全删除 Podman0 配置内容即可
2.3.1. Release
Landscape Router v0.11.2
v0.11.2 版本后因 handler 转发程序不兼容现需要更新
如果你使用的是官方提供的镜像则需要将镜像版本更新至 ghcr.io/thisseanzhang/landscape-edge:amd64-19
如果你使用自定义容器则需要更新 redirect_pkg_handler
以使用官方镜像 By Podman 且 With Quadlet 为例
......
[Container]
AddCapability=NET_ADMIN BPF PERFMON
ContainerName=xxxxxx
Image=ghcr.io/thisseanzhang/landscape-edge:amd64-19
PodmanArgs=--privileged
Sysctl=net.ipv4.conf.lo.accept_local=1
Volume=/opt/LanscapeRoute/unix_link/:/ld_unix_link/:ro
Volume=/opt/LanscapeRoute/container_apps:/app/server
Volume=/etc/localtime:/etc/localtime:ro
......
2.4. ppp
这里仅针对 openSUSE TW 系统
suse 中的 ppp 软件包并没有像 debian 一样为了兼容做了软链接
所以需要使用 ln -s 命令创建软链接以便 Landscape Router 识别到 rp-pppoe.so
╭──────┬─────────────────┬───────────┬────────────┬────────────┬─────────────┬─────────────┬─────────┬────────┬────────┬──────────┬──────────────┬───────────────┬───────────────╮
│ # │ name │ type │ target │ readonly │ mode │ num_links │ inode │ user │ group │ size │ created │ accessed │ modified │
├──────┼─────────────────┼───────────┼────────────┼────────────┼─────────────┼─────────────┼─────────┼────────┼────────┼──────────┼──────────────┼───────────────┼───────────────┤
│ 0 │ minconn.so │ file │ │ false │ rwxr-xr-x │ 1 │ 30511 │ root │ root │ 14.3 kB │ 3 weeks ago │ 2 months ago │ 2 months ago │
│ 1 │ openl2tp.so │ file │ │ false │ rwxr-xr-x │ 1 │ 30512 │ root │ root │ 14.2 kB │ 3 weeks ago │ 2 months ago │ 2 months ago │
│ 2 │ passwordfd.so │ file │ │ false │ rwxr-xr-x │ 1 │ 30513 │ root │ root │ 14.4 kB │ 3 weeks ago │ 2 months ago │ 2 months ago │
│ 3 │ pppoatm.so │ file │ │ false │ rwxr-xr-x │ 1 │ 30514 │ root │ root │ 22.9 kB │ 3 weeks ago │ 2 months ago │ 2 months ago │
│ 4 │ pppoe.so │ file │ │ false │ rwxr-xr-x │ 1 │ 30515 │ root │ root │ 40.0 kB │ 3 weeks ago │ 2 months ago │ 2 months ago │
│ 5 │ pppol2tp.so │ file │ │ false │ rwxr-xr-x │ 1 │ 30516 │ root │ root │ 19.2 kB │ 3 weeks ago │ 2 months ago │ 2 months ago │
│ 6 │ radattr.so │ file │ │ false │ rwxr-xr-x │ 1 │ 30517 │ root │ root │ 14.3 kB │ 3 weeks ago │ 2 months ago │ 2 months ago │
│ 7 │ radius.so │ file │ │ false │ rwxr-xr-x │ 1 │ 30518 │ root │ root │ 65.4 kB │ 3 weeks ago │ 2 months ago │ 2 months ago │
│ 8 │ radrealms.so │ file │ │ false │ rwxr-xr-x │ 1 │ 30519 │ root │ root │ 18.5 kB │ 3 weeks ago │ 2 months ago │ 2 months ago │
│ 9 │ rp-pppoe.so │ symlink │ pppoe.so │ false │ rwxrwxrwx │ 1 │ 32596 │ root │ root │ 8 B │ 3 weeks ago │ 3 weeks ago │ 3 weeks ago │
│ 10 │ winbind.so │ file │ │ false │ rwxr-xr-x │ 1 │ 30520 │ root │ root │ 22.6 kB │ 3 weeks ago │ 2 months ago │ 2 months ago │
╰──────┴─────────────────┴───────────┴────────────┴────────────┴─────────────┴─────────────┴─────────┴────────┴────────┴──────────┴──────────────┴───────────────┴───────────────╯
另外需要注意添加 PPPD 配置时
网卡 ppp网口名称 受限于 Linux 网卡名称长度限制: 15 个字符
并且不能为中文

2.5. systemd-networkd
Landscape Router 与 systemd-networkd 可以协同工作但不应同时运行 systemd-resolved
例如: 40-enp1s0f0.network 配置文件如下所示
[Match]
Name=enp1s0f0
[Network]
DHCP=no
IPv6AcceptRA=no
LinkLocalAddressing=no
[Link]
RequiredForOnline=carrier
- 此配置匹配名为 enp1s0f0 网路接口
- 禁用 DHCP Client
- 禁用 IPv6 路由通告
- 禁用链路本地地址
- 定义在线状态为 carrier (告诉 systemd-networkd-wait-online 服务,只要检测到网线插好了(有载波信号 carrier),就算这个网卡准备好了)
此配置 systemd-networkd 负责执行 ip link set dev <$name> up 或 down
随后的配置交由 Landscape Router
ActivationPolicy=如果想让 Landscape Router 执行 ip link set dev <$name> up 或 down
理论上可以将 ActivationPolicy= 显示指定为 manual (默认为 up)
[Match]
Name=enp1s0f0
[Network]
DHCP=no
IPv6AcceptRA=no
LinkLocalAddressing=no
[Link]
RequiredForOnline=carrier
ActivationPolicy=manual
不过需要注意的是此配置应用时需要查验 landscape_init.toml 配置
相应网卡 enable_in_boot 选项需为 true
[[ifaces]]
name = "enp1s0f0"
create_dev_type = "no_need_to_create"
zone_type = "wan"
enable_in_boot = true
wifi_mode = "undefined"
update_at = 1766480823939.0
RequiredForOnline=如若系统的某些服务依赖 network-online.target
那么这里的 RequiredForOnline= 参数很重要
自身环境 /etc/systemd/network 目录文件如下
4.0K ┌── 40-eno1.network │████████████████ │ 11%
4.0K ├── 40-enp1s0f0.network │████████████████ │ 11%
4.0K ├── 40-enp1s0f0.network.bak│████████████████ │ 11%
4.0K ├── 40-enp1s0f1.network │████████████████ │ 11%
4.0K ├── 40-enp1s0f2.network │████████████████ │ 11%
4.0K ├── 40-enp1s0f3.network │████████████████ │ 11%
4.0K ├── 40-enp1s0f3.network.bak│████████████████ │ 11%
4.0K ├── 40-nlmon.netdev │████████████████ │ 11%
4.0K ├── 40-nlmon.network │████████████████ │ 11%
36K ┌─┴ . │████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████ │ 100%
如上所示可以看到定义六个网络接口配置
其用途如下所示:
- eno1
- 闲置
- enp1s0f0
- WAN
- enp1s0f1
- 闲置
- enp1s0f2
- 闲置
- enp1s0f3
- LAN
- nlmon
- Bind Kernel Netlink message
其 .network 配置如下所示
# eno1
[Match]
Name=eno1
[Network]
DHCP=yes
[Link]
RequiredForOnline=no
# enp1s0f0
[Match]
Name=enp1s0f0
[Network]
DHCP=no
IPv6AcceptRA=no
LinkLocalAddressing=no
[Link]
RequiredForOnline=carrier
# enp1s0f1
[Match]
Name=enp1s0f1
[Network]
DHCP=no
IPv6AcceptRA=no
LinkLocalAddressing=no
[Link]
RequiredForOnline=no
# enp1s0f2
[Match]
Name=enp1s0f2
[Network]
DHCP=no
IPv6AcceptRA=no
LinkLocalAddressing=no
[Link]
RequiredForOnline=no
# enp1s0f3
[Match]
Name=enp1s0f3
[Network]
DHCP=no
IPv6AcceptRA=no
LinkLocalAddressing=no
[Link]
RequiredForOnline=routable
RequiredFamilyForOnline=any
由以上配置可见当完全匹配以下条件后才会将系统网络标记为在线状态
- 忽略 eno1 & enp1s0f1 & enp1s0f2
- enp1s0f0 网卡有物理链接
- enp1s0f3 网卡有路由配置(V4&V6均可)
