diff --git a/.gitattributes b/.gitattributes index e535307..ae9d46b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ +.gitattributes text eol=lf *.sh text eol=lf *.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..521298a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install networking test dependencies + run: | + sudo apt-get update + sudo apt-get install -y iproute2 iptables nftables + + - name: Check Bash syntax + run: | + bash -n nftpf.sh + bash -n tests/namespace-integration.sh + + - name: Run renderer self-test + run: bash nftpf.sh --self-test + + - name: Run isolated nftables integration test + run: sudo bash tests/namespace-integration.sh ./nftpf.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cc709fd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +All notable changes to this project are documented in this file. + +## [0.2.0] - 2026-07-10 + +### Added + +- Added `nftpf --apply` for non-interactive validation, atomic loading, and boot persistence. +- Added `nftpf --self-test`, an isolated namespace integration test, and GitHub Actions CI. +- Added separate live-rule and boot-persistence status indicators. +- Added a `SHA256SUMS` release asset and a security-reporting policy. + +### Changed + +- Moved managed NAT rules from shared `ip nat` / `ip6 nat` tables into `nftpf_nat` tables. +- Replaced global service restarts with a validated atomic nftables transaction. +- Successful applies now enable `nftables.service` for reboot persistence. +- v0.1.x upgrades remove only nftpf's legacy lowercase NAT chains. + +### Fixed + +- Preserved unrelated nftables and iptables-nft rules used by Phantun, Docker, fail2ban, and other tools. +- Made stop and uninstall remove only nftpf-managed tables instead of flushing the global ruleset. +- Added rollback for failed rule, line, access-control, backup-import, and service-enable operations. +- Normalized duplicate forwarding keys in `/etc/sysctl.conf` without loose regular-expression matching. diff --git a/NFT_Port_Forwarding_Tool_PRD.md b/NFT_Port_Forwarding_Tool_PRD.md index 0c4d95f..f710bd4 100644 --- a/NFT_Port_Forwarding_Tool_PRD.md +++ b/NFT_Port_Forwarding_Tool_PRD.md @@ -1,5 +1,7 @@ # NFT Port Forwarding Tool PRD +> Status: historical design document. It records the original implementation plan and v0.1.x baseline. See `README.md` and `CHANGELOG.md` for current v0.2.x behavior. + ## 1. Document Info Product name: NFT Port Forwarding Tool @@ -8,7 +10,7 @@ Document type: Product Requirements Document Target platform: Linux servers using nftables -Primary script: `nft_helper.sh` +Primary script: `nftpf.sh` Baseline version: Initial IPv4-only script from `C:/Users/endin/Downloads/nft_helper.sh` @@ -101,7 +103,9 @@ Secondary users: 9. As a user, I can enter a DDNS domain as the target address in a future version and have the tool keep it updated. -## 8. Current Baseline Behavior +## 8. Historical v0.1.x Baseline Behavior + +The global `flush ruleset` structure below is retained only as historical context. v0.2.0 and later use namespaced `nftpf_*` tables and atomically replace only those managed tables. The initial script uses this default nftables structure: diff --git a/README.md b/README.md index 685d9af..0e57021 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,10 @@ - Add single-port forwarding rules for TCP and UDP. - Add port-range forwarding rules with 1:1 or offset mapping. - Support IPv4, IPv6, and domain/DDNS targets. -- Automatically validate nftables configuration before applying changes. -- Automatically apply changes by starting or restarting the nftables service. +- Validate the complete nftables transaction before changing live rules. +- Atomically replace only `nftpf_*` tables without restarting the global nftables service. +- Enable nftables at boot whenever rules are applied, while reporting live-rule and boot-persistence status separately. +- Coexist with rules managed by iptables-nft, Phantun, Docker, fail2ban, and other tools. - Detect and repair managed nftables configuration drift on startup. - Support DDNS refresh with optional systemd timer automation. - Support mutually exclusive whitelist/blacklist access control for managed forwarding ports. @@ -24,7 +26,10 @@ ## Quick Start ```bash -curl -L -o nftpf.sh https://github.com/endview/nftpf/releases/latest/download/nftpf.sh +curl -fL --proto '=https' --tlsv1.2 -o nftpf.sh https://github.com/endview/nftpf/releases/latest/download/nftpf.sh +curl -fL --proto '=https' --tlsv1.2 -o SHA256SUMS https://github.com/endview/nftpf/releases/latest/download/SHA256SUMS +sha256sum -c SHA256SUMS +bash -n nftpf.sh chmod +x nftpf.sh sudo bash nftpf.sh ``` @@ -35,15 +40,13 @@ After the first run, the tool installs a shortcut: nftpf ``` -## Important Notice +## Safe Rule Ownership And Persistence -The generated nftables configuration contains: +Starting with v0.2.0, generated configuration does not contain `flush ruleset`. `nftpf` owns only tables whose names begin with `nftpf_`, validates a delete-and-recreate transaction first, and then commits that transaction atomically. Unrelated nftables and iptables-nft tables remain loaded. -```nft -flush ruleset -``` +When upgrading a v0.1.x configuration, the one-time migration removes only the legacy lowercase `prerouting` and `postrouting` chains created by `nftpf` inside `table ip nat` / `table ip6 nat`. Uppercase iptables-nft chains such as `PREROUTING` and `POSTROUTING` are preserved. -That means this tool rewrites the current nftables ruleset when applying managed configuration. Do not use it on hosts where other firewall tools or applications also manage nftables rules unless you understand and accept that behavior. +Every successful apply also enables `nftables.service` for boot persistence. Use `nftpf --apply` to validate, atomically load, and persist the managed rules. Avoid manually restarting the global nftables service on a host shared with other firewall managers, because some distribution service units flush the complete live ruleset during restart. ## DDNS Refresh @@ -77,7 +80,26 @@ The default line mode only binds the entry interface and does not change system ## Backup And Rollback -Before changing forwarding rules or access-control settings, `nftpf` automatically creates a backup under `/etc/nft-port-forward/backups`. The menu also provides manual backup, import, and rollback to the latest automatic backup. +Before changing forwarding rules or access-control settings, `nftpf` automatically creates a backup under `/etc/nft-port-forward/backups`. The menu also provides manual backup, import, and rollback to the latest automatic backup. Failed applies restore the previous state files, generated configuration, and service-enable state. + +For non-destructive checks, run `nftpf --self-test`. This verifies Bash syntax and renderer invariants without changing system rules. + +## Tests + +Run the fast checks on any Linux host: + +```bash +bash -n nftpf.sh +bash nftpf.sh --self-test +``` + +The integration test requires root plus `iproute2`, `iptables`, and `nftables`. It creates an isolated network namespace, migrates a simulated v0.1.x ruleset, and verifies that foreign iptables-nft rules survive migration, reapply, failed transactions, and cleanup. It does not modify the host network namespace. + +```bash +sudo bash tests/namespace-integration.sh ./nftpf.sh +``` + +The same checks run automatically through GitHub Actions. ## Script Update @@ -85,13 +107,17 @@ Use menu item `17. 更新脚本` or run `nftpf --update` to download the latest ## Uninstall -Use menu item `18. 卸载脚本` or run `nftpf --uninstall` to remove nftpf. The uninstall flow clears the current nftables ruleset, resets nftpf-managed configuration to an empty ruleset, removes DDNS timers/services, removes managed policy-route services and rules, removes state files, and deletes the installed script/shortcut. Backup deletion is optional and defaults to `N`. +Use menu item `18. 卸载脚本` or run `nftpf --uninstall` to remove nftpf. The uninstall flow removes only nftpf-managed tables, resets nftpf-managed configuration to an empty file, removes DDNS timers/services, removes managed policy-route services and rules, removes state files, and deletes the installed script/shortcut. Backup deletion is optional and defaults to `N`. The uninstall flow does not remove the `nftables` package and does not disable system IP forwarding sysctl settings, because those may be used by other services. ## Files - `nftpf.sh`: Main script. +- `tests/namespace-integration.sh`: Isolated migration and coexistence regression test. +- `.github/workflows/ci.yml`: GitHub Actions validation workflow. +- `CHANGELOG.md`: Versioned change history. +- `SECURITY.md`: Supported versions and private vulnerability-reporting guidance. - `NFT_Port_Forwarding_Tool_PRD.md`: Product requirements and design notes. ## Requirements @@ -101,6 +127,7 @@ The uninstall flow does not remove the `nftables` package and does not disable s - `bash`. - `nftables`. - `iproute2`. +- Debian/Ubuntu for automatic nftables installation. On other systemd distributions, install nftables manually first. - Optional: `flock` from `util-linux` for DDNS refresh locking. ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index 3ceb339..b4a0974 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -9,8 +9,10 @@ - 添加单端口 TCP+UDP 转发规则。 - 添加端口段转发规则,支持 1:1 映射和偏移映射。 - 支持 IPv4、IPv6、域名/DDNS 目标。 -- 写入配置前自动校验 nftables 语法。 -- 规则变更后自动启动或重启 nftables 服务,使配置立即生效。 +- 修改实时规则前校验完整 nftables 事务。 +- 原子替换 `nftpf_*` 托管表,不重启全局 nftables 服务。 +- 每次成功应用规则时启用 nftables 开机自启,并分别显示实时规则与开机持久化状态。 +- 可与 iptables-nft、Phantun、Docker、fail2ban 等工具管理的规则共存。 - 启动时自动检测并修复本工具托管的 nftables 配置漂移。 - 支持 DDNS 手动刷新和 systemd timer 自动刷新。 - 支持白名单/黑名单二选一的访问控制,只限制本工具托管的转发端口。 @@ -24,7 +26,10 @@ ## 快速开始 ```bash -curl -L -o nftpf.sh https://github.com/endview/nftpf/releases/latest/download/nftpf.sh +curl -fL --proto '=https' --tlsv1.2 -o nftpf.sh https://github.com/endview/nftpf/releases/latest/download/nftpf.sh +curl -fL --proto '=https' --tlsv1.2 -o SHA256SUMS https://github.com/endview/nftpf/releases/latest/download/SHA256SUMS +sha256sum -c SHA256SUMS +bash -n nftpf.sh chmod +x nftpf.sh sudo bash nftpf.sh ``` @@ -35,15 +40,13 @@ sudo bash nftpf.sh nftpf ``` -## 重要提示 +## 规则隔离与持久化 -本工具生成的 nftables 配置包含: +从 v0.2.0 开始,生成的配置不再包含 `flush ruleset`。`nftpf` 只管理名称以 `nftpf_` 开头的表;应用前会校验“删除旧托管表并创建新托管表”的完整事务,校验通过后一次性原子提交,不会清除其它 nftables 或 iptables-nft 表。 -```nft -flush ruleset -``` +从 v0.1.x 升级时,一次性迁移只删除 `nftpf` 旧版在 `table ip nat` / `table ip6 nat` 中创建的小写 `prerouting`、`postrouting` 链;iptables-nft 使用的大写 `PREROUTING`、`POSTROUTING` 等链会保留。 -这意味着应用本工具托管配置时,会重写当前 nftables 规则集。如果你的服务器上还有 Docker、fail2ban、防火墙面板或其他程序也在管理 nftables,请先确认你能接受这个行为后再使用。 +每次成功应用都会启用 `nftables.service` 开机自启。可以运行 `nftpf --apply` 校验、原子加载并持久化托管规则。如果主机同时运行其它防火墙管理程序,请避免手动重启全局 nftables 服务,因为部分发行版的 service 在重启时会清空整个实时 ruleset。 ## DDNS 刷新 @@ -81,7 +84,26 @@ Unit=nftpf-ddns.service ## 备份和回滚 -每次修改转发规则或访问控制设置前,`nftpf` 会自动在 `/etc/nft-port-forward/backups` 下创建备份。菜单也提供手动备份、导入备份,以及回滚到上一次自动备份。 +每次修改转发规则或访问控制设置前,`nftpf` 会自动在 `/etc/nft-port-forward/backups` 下创建备份。菜单也提供手动备份、导入备份,以及回滚到上一次自动备份。应用失败时会恢复之前的状态文件、生成配置和服务启用状态。 + +可以运行 `nftpf --self-test` 做无侵入检查;该命令验证 Bash 语法和配置渲染约束,不修改系统规则。 + +## 测试 + +普通 Linux 主机可以运行快速检查: + +```bash +bash -n nftpf.sh +bash nftpf.sh --self-test +``` + +集成测试需要 root,以及 `iproute2`、`iptables`、`nftables`。测试会创建隔离网络命名空间,模拟 v0.1.x 规则迁移,并验证外部 iptables-nft 规则在迁移、重复应用、错误事务和清理后仍保持不变;不会修改宿主网络命名空间。 + +```bash +sudo bash tests/namespace-integration.sh ./nftpf.sh +``` + +GitHub Actions 会自动运行同一组检查。 ## 脚本更新 @@ -89,13 +111,17 @@ Unit=nftpf-ddns.service ## 卸载 -可以使用菜单 `18. 卸载脚本`,也可以运行 `nftpf --uninstall`。卸载流程会清空当前 nftables ruleset,将 nftpf 托管配置重置为空规则,删除 DDNS timer/service、旧 cron 任务、多网卡托管回程 service 和 nftpf 创建的 fwmark/ip rule/路由表,删除状态文件,并删除已安装脚本和快捷命令。是否删除备份文件会单独询问,默认 `N` 保留。 +可以使用菜单 `18. 卸载脚本`,也可以运行 `nftpf --uninstall`。卸载流程只删除 nftpf 托管表,将 nftpf 托管配置重置为空文件,删除 DDNS timer/service、旧 cron 任务、多网卡托管回程 service 和 nftpf 创建的 fwmark/ip rule/路由表,删除状态文件,并删除已安装脚本和快捷命令。是否删除备份文件会单独询问,默认 `N` 保留。 卸载不会删除 `nftables` 软件包,也不会关闭系统 IP 转发 sysctl,因为这些可能被其它服务使用。 ## 文件说明 - `nftpf.sh`:主脚本。 +- `tests/namespace-integration.sh`:隔离的迁移与共存回归测试。 +- `.github/workflows/ci.yml`:GitHub Actions 自动校验流程。 +- `CHANGELOG.md`:按版本维护的变更记录。 +- `SECURITY.md`:支持版本和私密漏洞报告说明。 - `NFT_Port_Forwarding_Tool_PRD.md`:产品需求和设计说明。 ## 环境要求 @@ -105,6 +131,7 @@ Unit=nftpf-ddns.service - `bash`。 - `nftables`。 - `iproute2`。 +- Debian/Ubuntu 可自动安装 nftables;其它 systemd 发行版请先手动安装。 - 可选:`util-linux` 中的 `flock`,用于 DDNS 刷新防重叠执行。 ## 许可证 diff --git a/README.zh-TW.md b/README.zh-TW.md index 80e37b9..8c51862 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -9,8 +9,10 @@ - 新增單連接埠 TCP+UDP 轉發規則。 - 新增連接埠區段轉發規則,支援 1:1 映射和偏移映射。 - 支援 IPv4、IPv6、網域/DDNS 目標。 -- 寫入設定前自動校驗 nftables 語法。 -- 規則變更後自動啟動或重新啟動 nftables 服務,使設定立即生效。 +- 修改即時規則前驗證完整 nftables 交易。 +- 原子替換 `nftpf_*` 託管表,不重新啟動全域 nftables 服務。 +- 每次成功套用規則時啟用 nftables 開機自啟,並分別顯示即時規則與開機持久化狀態。 +- 可與 iptables-nft、Phantun、Docker、fail2ban 等工具管理的規則共存。 - 啟動時自動偵測並修復本工具託管的 nftables 設定漂移。 - 支援 DDNS 手動刷新和 systemd timer 自動刷新。 - 支援白名單/黑名單二選一的存取控制,只限制本工具託管的轉發連接埠。 @@ -24,7 +26,10 @@ ## 快速開始 ```bash -curl -L -o nftpf.sh https://github.com/endview/nftpf/releases/latest/download/nftpf.sh +curl -fL --proto '=https' --tlsv1.2 -o nftpf.sh https://github.com/endview/nftpf/releases/latest/download/nftpf.sh +curl -fL --proto '=https' --tlsv1.2 -o SHA256SUMS https://github.com/endview/nftpf/releases/latest/download/SHA256SUMS +sha256sum -c SHA256SUMS +bash -n nftpf.sh chmod +x nftpf.sh sudo bash nftpf.sh ``` @@ -35,15 +40,13 @@ sudo bash nftpf.sh nftpf ``` -## 重要提示 +## 規則隔離與持久化 -本工具產生的 nftables 設定包含: +從 v0.2.0 開始,產生的設定不再包含 `flush ruleset`。`nftpf` 只管理名稱以 `nftpf_` 開頭的表;套用前會驗證「刪除舊託管表並建立新託管表」的完整交易,驗證通過後一次原子提交,不會清除其它 nftables 或 iptables-nft 表。 -```nft -flush ruleset -``` +從 v0.1.x 升級時,一次性遷移只刪除 `nftpf` 舊版在 `table ip nat` / `table ip6 nat` 中建立的小寫 `prerouting`、`postrouting` 鏈;iptables-nft 使用的大寫 `PREROUTING`、`POSTROUTING` 等鏈會保留。 -這表示套用本工具託管設定時,會重寫目前 nftables 規則集。如果你的伺服器上還有 Docker、fail2ban、防火牆面板或其他程式也在管理 nftables,請先確認你能接受這個行為後再使用。 +每次成功套用都會啟用 `nftables.service` 開機自啟。可以執行 `nftpf --apply` 驗證、原子載入並持久化託管規則。如果主機同時執行其它防火牆管理程式,請避免手動重新啟動全域 nftables 服務,因為部分發行版的 service 在重新啟動時會清空整個即時 ruleset。 ## DDNS 刷新 @@ -81,7 +84,26 @@ Unit=nftpf-ddns.service ## 備份和回滾 -每次修改轉發規則或存取控制設定前,`nftpf` 會自動在 `/etc/nft-port-forward/backups` 下建立備份。選單也提供手動備份、匯入備份,以及回滾到上一次自動備份。 +每次修改轉發規則或存取控制設定前,`nftpf` 會自動在 `/etc/nft-port-forward/backups` 下建立備份。選單也提供手動備份、匯入備份,以及回滾到上一次自動備份。套用失敗時會還原之前的狀態檔、產生設定和服務啟用狀態。 + +可以執行 `nftpf --self-test` 做無侵入檢查;該命令驗證 Bash 語法和設定渲染約束,不修改系統規則。 + +## 測試 + +一般 Linux 主機可以執行快速檢查: + +```bash +bash -n nftpf.sh +bash nftpf.sh --self-test +``` + +整合測試需要 root,以及 `iproute2`、`iptables`、`nftables`。測試會建立隔離網路命名空間,模擬 v0.1.x 規則遷移,並驗證外部 iptables-nft 規則在遷移、重複套用、錯誤交易和清理後仍保持不變;不會修改宿主網路命名空間。 + +```bash +sudo bash tests/namespace-integration.sh ./nftpf.sh +``` + +GitHub Actions 會自動執行同一組檢查。 ## 腳本更新 @@ -89,13 +111,17 @@ Unit=nftpf-ddns.service ## 卸載 -可以使用選單 `18. 卸載腳本`,也可以執行 `nftpf --uninstall`。卸載流程會清空目前 nftables ruleset,將 nftpf 託管設定重置為空規則,刪除 DDNS timer/service、舊 cron 任務、多網卡託管回程 service 和 nftpf 建立的 fwmark/ip rule/路由表,刪除狀態檔案,並刪除已安裝腳本和快捷命令。是否刪除備份檔案會單獨詢問,預設 `N` 保留。 +可以使用選單 `18. 卸載腳本`,也可以執行 `nftpf --uninstall`。卸載流程只刪除 nftpf 託管表,將 nftpf 託管設定重置為空檔案,刪除 DDNS timer/service、舊 cron 任務、多網卡託管回程 service 和 nftpf 建立的 fwmark/ip rule/路由表,刪除狀態檔案,並刪除已安裝腳本和快捷命令。是否刪除備份檔案會單獨詢問,預設 `N` 保留。 卸載不會刪除 `nftables` 套件,也不會關閉系統 IP 轉發 sysctl,因為這些可能被其它服務使用。 ## 檔案說明 - `nftpf.sh`:主腳本。 +- `tests/namespace-integration.sh`:隔離的遷移與共存回歸測試。 +- `.github/workflows/ci.yml`:GitHub Actions 自動校驗流程。 +- `CHANGELOG.md`:按版本維護的變更記錄。 +- `SECURITY.md`:支援版本和私密漏洞回報說明。 - `NFT_Port_Forwarding_Tool_PRD.md`:產品需求和設計說明。 ## 環境需求 @@ -105,6 +131,7 @@ Unit=nftpf-ddns.service - `bash`。 - `nftables`。 - `iproute2`。 +- Debian/Ubuntu 可自動安裝 nftables;其它 systemd 發行版請先手動安裝。 - 選用:`util-linux` 中的 `flock`,用於 DDNS 刷新防重疊執行。 ## 授權 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e4c1b53 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +## Supported Versions + +Security fixes are provided for the latest minor release only. + +| Version | Supported | +| --- | --- | +| 0.2.x | Yes | +| 0.1.x | No | + +## Reporting A Vulnerability + +Please do not open a public issue for a suspected vulnerability. Use GitHub's private vulnerability reporting for this repository: + + + +Include the affected nftpf version, Linux distribution, nftables version, reproduction steps, expected impact, and any suggested mitigation. Do not include production credentials, private keys, server passwords, or an unredacted ruleset. + +## Operational Safety + +Before applying firewall changes remotely, keep an independent SSH session open and create an out-of-band recovery path. Review generated configuration with `nft -c -f` and back up `/etc/nftables.conf` plus `/etc/nft-port-forward`. diff --git a/nftpf.sh b/nftpf.sh index 2f4af12..290c036 100644 --- a/nftpf.sh +++ b/nftpf.sh @@ -5,7 +5,7 @@ GREEN='\033[0;32m' YELLOW='\033[0;33m' PLAIN='\033[0m' -NFTPF_VERSION="${NFTPF_VERSION:-0.1.3}" +NFTPF_VERSION="${NFTPF_VERSION:-0.2.0}" UPDATE_URL="${UPDATE_URL:-https://github.com/endview/nftpf/releases/latest/download/nftpf.sh}" CONFIG_FILE="${CONFIG_FILE:-/etc/nftables.conf}" STATE_DIR="${STATE_DIR:-/etc/nft-port-forward}" @@ -24,15 +24,16 @@ SERVICE_NAME="nftables" REAL_NFT_CMD="${REAL_NFT_CMD:-}" APPLY_RESULT="" CONFIG_AUTO_REBUILT=0 -CONFIG_RENDER_VERSION="3" +CONFIG_RENDER_VERSION="4" TRACK_TIMEOUT="${TRACK_TIMEOUT:-30m}" IPV6_ROUTE_MARK="${IPV6_ROUTE_MARK:-100}" IPV6_ROUTE_TABLE="${IPV6_ROUTE_TABLE:-100}" NFTPF_IPV6_ROUTEFIX="${NFTPF_IPV6_ROUTEFIX:-auto}" +MIGRATE_LEGACY_NAT=0 if [[ "${NFT_HELPER_SKIP_ROOT:-0}" != "1" && $EUID -ne 0 ]]; then case "${1:-}" in - --help|-h|--tool-help|--version) ;; + --help|-h|--tool-help|--version|--self-test) ;; *) echo -e "${RED}错误: 必须使用 root 用户运行此脚本!${PLAIN}" exit 1 @@ -87,6 +88,81 @@ nft_run() { "$REAL_NFT_CMD" "$@" } +managed_table_specs() { + cat <<'EOF' +ip nftpf_track +ip6 nftpf_track +ip nftpf_access +ip6 nftpf_access +inet nftpf_route +ip6 nftpf_routefix +ip nftpf_nat +ip6 nftpf_nat +EOF +} + +append_managed_cleanup_commands() { + local output=$1 + local family + local table + + while read -r family table; do + if nft_run list table "$family" "$table" >/dev/null 2>&1; then + printf 'delete table %s %s\n' "$family" "$table" >> "$output" + fi + done < <(managed_table_specs) + + if [[ "$MIGRATE_LEGACY_NAT" == "1" ]]; then + for family in ip ip6; do + for table in prerouting postrouting; do + if nft_run list chain "$family" nat "$table" >/dev/null 2>&1; then + printf 'flush chain %s nat %s\n' "$family" "$table" >> "$output" + printf 'delete chain %s nat %s\n' "$family" "$table" >> "$output" + fi + done + done + fi +} + +build_apply_transaction() { + local config_file=$1 + local output=$2 + + : > "$output" || return 1 + echo '#!/usr/sbin/nft -f' >> "$output" || return 1 + append_managed_cleanup_commands "$output" || return 1 + sed '/^#!\/usr\/sbin\/nft -f$/d' "$config_file" >> "$output" +} + +build_cleanup_transaction() { + local output=$1 + + : > "$output" || return 1 + echo '#!/usr/sbin/nft -f' >> "$output" || return 1 + append_managed_cleanup_commands "$output" +} + +managed_rules_are_loaded() { + nft_run list table ip nftpf_nat >/dev/null 2>&1 || + nft_run list table ip6 nftpf_nat >/dev/null 2>&1 || + { [[ "$MIGRATE_LEGACY_NAT" == "1" ]] && nft_run list chain ip nat prerouting >/dev/null 2>&1; } +} + +unload_managed_rules() { + local transaction + + transaction=$(mktemp) || return 1 + if ! build_cleanup_transaction "$transaction"; then + rm -f "$transaction" + return 1 + fi + if ! nft_run -c -f "$transaction" || ! nft_run -f "$transaction"; then + rm -f "$transaction" + return 1 + fi + rm -f "$transaction" +} + pause_and_return() { echo "" echo -e "${YELLOW}按下任意键返回主菜单...${PLAIN}" @@ -176,18 +252,25 @@ create_state_backup() { ensure_sysctl_setting() { local key=$1 local value=$2 + local key_pattern + local matching_lines + local desired_lines if [ ! -f /etc/sysctl.conf ]; then touch /etc/sysctl.conf fi - if ! grep -q "^$key=$value$" /etc/sysctl.conf; then - sed -i "/^$key=/d" /etc/sysctl.conf - echo "$key=$value" >> /etc/sysctl.conf - return 0 + key_pattern=${key//./\.} + matching_lines=$(grep -Ec "^[[:space:]]*$key_pattern[[:space:]]*=" /etc/sysctl.conf 2>/dev/null || true) + desired_lines=$(grep -Ec "^[[:space:]]*$key_pattern[[:space:]]*=[[:space:]]*$value([[:space:]]*(#.*)?)?$" /etc/sysctl.conf 2>/dev/null || true) + + if [ "$matching_lines" -eq 1 ] && [ "$desired_lines" -eq 1 ]; then + return 1 fi - return 1 + sed -i "/^[[:space:]]*$key_pattern[[:space:]]*=/d" /etc/sysctl.conf + printf '%s=%s\n' "$key" "$value" >> /etc/sysctl.conf + return 0 } enable_ip_forward() { @@ -287,7 +370,6 @@ check_dependencies() { fi apt-get update apt-get install -y nftables - systemctl enable nftables fi find_nft_cmd || return 1 fi @@ -302,6 +384,10 @@ check_dependencies() { if apply_config_changes; then show_apply_result else + if [ -f "${CONFIG_FILE}.bak.last" ]; then + cp "${CONFIG_FILE}.bak.last" "$CONFIG_FILE" + echo -e "${YELLOW}自动升级应用失败,已恢复升级前配置文件。${PLAIN}" + fi return 1 fi fi @@ -1171,11 +1257,11 @@ import_existing_rules_from_config() { id=1 while IFS= read -r line; do - if echo "$line" | grep -q '^table ip nat {'; then + if echo "$line" | grep -Eq '^table ip (nftpf_)?nat {'; then family="ipv4" continue fi - if echo "$line" | grep -q '^table ip6 nat {'; then + if echo "$line" | grep -Eq '^table ip6 (nftpf_)?nat {'; then family="ipv6" continue fi @@ -1529,14 +1615,12 @@ cat < "$tmp_config" + transaction=$(mktemp) || { rm -f "$tmp_config"; return 1; } + if ! generate_config_from_rules "$rules_file" > "$tmp_config"; then + rm -f "$tmp_config" "$transaction" + echo -e "${RED}错误:无法生成 nftables 配置。${PLAIN}" + return 1 + fi + if ! build_apply_transaction "$tmp_config" "$transaction"; then + rm -f "$tmp_config" "$transaction" + echo -e "${RED}错误:无法构建 nftables 应用事务。${PLAIN}" + return 1 + fi - if ! nft_run -c -f "$tmp_config"; then - rm -f "$tmp_config" + if ! nft_run -c -f "$transaction"; then + rm -f "$tmp_config" "$transaction" echo -e "${RED}错误:生成的 nftables 配置校验失败,未覆盖当前配置。${PLAIN}" return 1 fi + rm -f "$transaction" if [ -f "$CONFIG_FILE" ]; then - cp "$CONFIG_FILE" "${CONFIG_FILE}.bak.last" + had_config=1 + if ! cp "$CONFIG_FILE" "${CONFIG_FILE}.bak.last"; then + rm -f "$tmp_config" + echo -e "${RED}错误:无法备份当前 nftables 配置,已取消覆盖。${PLAIN}" + return 1 + fi fi - mv "$tmp_config" "$CONFIG_FILE" - chmod +x "$CONFIG_FILE" + if ! mv "$tmp_config" "$CONFIG_FILE"; then + rm -f "$tmp_config" + echo -e "${RED}错误:无法写入 nftables 配置。${PLAIN}" + return 1 + fi + if ! chmod +x "$CONFIG_FILE"; then + if [ "$had_config" -eq 1 ]; then + cp "${CONFIG_FILE}.bak.last" "$CONFIG_FILE" 2>/dev/null || true + else + rm -f "$CONFIG_FILE" + fi + echo -e "${RED}错误:无法设置 nftables 配置权限。${PLAIN}" + return 1 + fi } init_config() { @@ -1607,6 +1721,9 @@ init_config() { local access_mode ensure_state_dir + if [ -f "$CONFIG_FILE" ] && grep -Eq 'NFTPF_RENDER_VERSION=([123])([^0-9]|$)' "$CONFIG_FILE"; then + MIGRATE_LEGACY_NAT=1 + fi import_existing_rules_from_config if [ ! -f "$CONFIG_FILE" ]; then @@ -1619,6 +1736,8 @@ init_config() { access_mode=$(get_access_mode) ! grep -q "NFTPF_RENDER_VERSION=$CONFIG_RENDER_VERSION" "$CONFIG_FILE" && needs_rebuild=1 + ! grep -q "table ip nftpf_nat" "$CONFIG_FILE" && needs_rebuild=1 + ! grep -q "table ip6 nftpf_nat" "$CONFIG_FILE" && needs_rebuild=1 ! grep -q "IPV4_MARKER_START" "$CONFIG_FILE" && needs_rebuild=1 ! grep -q "IPV6_MARKER_START" "$CONFIG_FILE" && needs_rebuild=1 ! grep -q "ct status dnat masquerade" "$CONFIG_FILE" && needs_rebuild=1 @@ -1638,17 +1757,34 @@ init_config() { } validate_nft_config() { - nft_run -c -f "$CONFIG_FILE" -} + local transaction + local rc -service_is_running() { - systemctl is-active --quiet "$SERVICE_NAME" + transaction=$(mktemp) || return 1 + if ! build_apply_transaction "$CONFIG_FILE" "$transaction"; then + rm -f "$transaction" + return 1 + fi + nft_run -c -f "$transaction" + rc=$? + rm -f "$transaction" + return "$rc" } apply_config_changes() { + local transaction + local service_was_enabled=0 APPLY_RESULT="" - if ! validate_nft_config; then + transaction=$(mktemp) || return 1 + if ! build_apply_transaction "$CONFIG_FILE" "$transaction"; then + rm -f "$transaction" + echo -e "${RED}错误:无法构建 nftables 应用事务,未应用。${PLAIN}" + return 1 + fi + + if ! nft_run -c -f "$transaction"; then + rm -f "$transaction" echo -e "${RED}错误:当前 nftables 配置校验失败,未应用。${PLAIN}" return 1 fi @@ -1657,53 +1793,96 @@ apply_config_changes() { ensure_ipv6_dnat_policy_route if [[ "${NFT_HELPER_TEST_MODE:-0}" == "1" ]]; then + rm -f "$transaction" APPLY_RESULT="tested" return 0 fi - if service_is_running; then - if systemctl restart "$SERVICE_NAME"; then - APPLY_RESULT="restarted" - return 0 - fi - else - if systemctl start "$SERVICE_NAME"; then - APPLY_RESULT="started" - return 0 + if systemctl is-enabled --quiet "$SERVICE_NAME" 2>/dev/null; then + service_was_enabled=1 + fi + + if ! systemctl enable "$SERVICE_NAME" >/dev/null 2>&1; then + rm -f "$transaction" + if [ "$service_was_enabled" -eq 0 ]; then + systemctl disable "$SERVICE_NAME" >/dev/null 2>&1 || true fi + echo -e "${RED}错误:无法启用 $SERVICE_NAME 开机自启,配置未应用。${PLAIN}" + return 1 + fi + + if nft_run -f "$transaction"; then + rm -f "$transaction" + APPLY_RESULT="applied_enabled" + return 0 fi - systemctl status "$SERVICE_NAME" --no-pager -l 2>/dev/null || true - echo -e "${RED}错误:服务启动或重启失败,请查看上方错误信息。${PLAIN}" + rm -f "$transaction" + if [ "$service_was_enabled" -eq 0 ]; then + systemctl disable "$SERVICE_NAME" >/dev/null 2>&1 || true + fi + echo -e "${RED}错误:原子应用 nftpf 规则失败,实时 ruleset 保持原状。${PLAIN}" return 1 } show_apply_result() { case "$APPLY_RESULT" in tested) echo -e "${GREEN}配置已通过校验(测试模式未操作服务)。${PLAIN}" ;; - started) echo -e "${GREEN}配置已校验通过并自动应用,服务已启动。${PLAIN}" ;; - restarted) echo -e "${GREEN}配置已校验通过并自动应用,服务已重启。${PLAIN}" ;; + applied_enabled) echo -e "${GREEN}配置已原子应用,并已启用 nftables 开机自启。${PLAIN}" ;; *) echo -e "${GREEN}配置已应用。${PLAIN}" ;; esac } commit_rules_file() { local tmp_rules=$1 + local rollback_dir + local had_rules=0 + local had_config=0 create_state_backup "before-rules-change" 1 + rollback_dir=$(mktemp -d) || { rm -f "$tmp_rules"; return 1; } + if [ -f "$RULES_FILE" ]; then + if ! cp "$RULES_FILE" "$rollback_dir/rules.db"; then + rm -f "$tmp_rules" + rm -rf "$rollback_dir" + echo -e "${RED}错误:无法创建规则回滚快照,已取消修改。${PLAIN}" + return 1 + fi + had_rules=1 + fi + if [ -f "$CONFIG_FILE" ]; then + if ! cp "$CONFIG_FILE" "$rollback_dir/nftables.conf"; then + rm -f "$tmp_rules" + rm -rf "$rollback_dir" + echo -e "${RED}错误:无法创建配置回滚快照,已取消修改。${PLAIN}" + return 1 + fi + had_config=1 + fi if ! write_config_from_rules "$tmp_rules"; then rm -f "$tmp_rules" + rm -rf "$rollback_dir" return 1 fi - mv "$tmp_rules" "$RULES_FILE" + if ! mv "$tmp_rules" "$RULES_FILE"; then + if [ "$had_config" -eq 1 ]; then cp "$rollback_dir/nftables.conf" "$CONFIG_FILE"; else rm -f "$CONFIG_FILE"; fi + rm -f "$tmp_rules" + rm -rf "$rollback_dir" + echo -e "${RED}错误:无法更新规则库,配置文件已回滚。${PLAIN}" + return 1 + fi if apply_config_changes; then + rm -rf "$rollback_dir" show_apply_result return 0 else - echo -e "${YELLOW}规则已保存,但服务未成功应用;修复问题后可使用“重启服务”。${PLAIN}" + if [ "$had_rules" -eq 1 ]; then cp "$rollback_dir/rules.db" "$RULES_FILE"; else rm -f "$RULES_FILE"; fi + if [ "$had_config" -eq 1 ]; then cp "$rollback_dir/nftables.conf" "$CONFIG_FILE"; else rm -f "$CONFIG_FILE"; fi + rm -rf "$rollback_dir" + echo -e "${YELLOW}规则应用失败,规则库和配置文件已自动回滚;实时 ruleset 保持原状。${PLAIN}" return 1 fi } @@ -2251,19 +2430,53 @@ line_in_use() { commit_lines_file() { local tmp_lines=$1 + local rollback_dir + local had_lines=0 + local had_config=0 create_state_backup "before-line-change" 1 + rollback_dir=$(mktemp -d) || { rm -f "$tmp_lines"; return 1; } + if [ -f "$LINES_FILE" ]; then + if ! cp "$LINES_FILE" "$rollback_dir/lines.db"; then + rm -f "$tmp_lines" + rm -rf "$rollback_dir" + echo -e "${RED}错误:无法创建线路回滚快照,已取消修改。${PLAIN}" + return 1 + fi + had_lines=1 + fi + if [ -f "$CONFIG_FILE" ]; then + if ! cp "$CONFIG_FILE" "$rollback_dir/nftables.conf"; then + rm -f "$tmp_lines" + rm -rf "$rollback_dir" + echo -e "${RED}错误:无法创建配置回滚快照,已取消修改。${PLAIN}" + return 1 + fi + had_config=1 + fi if LINES_FILE="$tmp_lines" write_config_from_rules "$RULES_FILE"; then - mv "$tmp_lines" "$LINES_FILE" + if ! mv "$tmp_lines" "$LINES_FILE"; then + if [ "$had_config" -eq 1 ]; then cp "$rollback_dir/nftables.conf" "$CONFIG_FILE"; else rm -f "$CONFIG_FILE"; fi + rm -f "$tmp_lines" + rm -rf "$rollback_dir" + echo -e "${RED}错误:无法更新线路库,配置文件已回滚。${PLAIN}" + return 1 + fi if apply_config_changes; then + rm -rf "$rollback_dir" show_apply_result return 0 fi + if [ "$had_lines" -eq 1 ]; then cp "$rollback_dir/lines.db" "$LINES_FILE"; else rm -f "$LINES_FILE"; fi + if [ "$had_config" -eq 1 ]; then cp "$rollback_dir/nftables.conf" "$CONFIG_FILE"; else rm -f "$CONFIG_FILE"; fi + rm -rf "$rollback_dir" + echo -e "${YELLOW}线路应用失败,线路库和配置文件已自动回滚。${PLAIN}" return 1 fi rm -f "$tmp_lines" + rm -rf "$rollback_dir" return 1 } @@ -2795,22 +3008,19 @@ manage_service() { local action=$1 case "$action" in - enable) systemctl enable "$SERVICE_NAME" && echo -e "${GREEN}已设置开机自启。${PLAIN}" ;; - disable) systemctl disable "$SERVICE_NAME" && echo -e "${GREEN}已取消开机自启。${PLAIN}" ;; - start) - if validate_nft_config && systemctl start "$SERVICE_NAME"; then - echo -e "${GREEN}服务已启动。${PLAIN}" - else - systemctl status "$SERVICE_NAME" --no-pager -l 2>/dev/null || true - echo -e "${RED}服务启动失败。${PLAIN}" - fi - ;; - stop) systemctl stop "$SERVICE_NAME" && echo -e "${GREEN}服务已停止。${PLAIN}" ;; - restart) + enable|start|restart) if apply_config_changes; then show_apply_result fi ;; + disable) systemctl disable "$SERVICE_NAME" && echo -e "${GREEN}已取消开机自启。${PLAIN}" ;; + stop) + if unload_managed_rules; then + echo -e "${GREEN}已卸载 nftpf 实时规则;未停止全局 nftables 服务,其他工具规则保持不变。${PLAIN}" + else + echo -e "${RED}卸载 nftpf 实时规则失败。${PLAIN}" + fi + ;; esac pause_and_return } @@ -2849,8 +3059,6 @@ write_empty_uninstall_config() { tmp_config=$(mktemp) || return 1 cat > "$tmp_config" </dev/null 2>&1 || echo -e "${YELLOW}警告:执行 nft flush ruleset 失败,请手动检查 nftables。${PLAIN}" + unload_managed_rules >/dev/null 2>&1 || echo -e "${YELLOW}警告:卸载 nftpf 托管表失败,请手动检查 nftables。${PLAIN}" else - echo -e "${YELLOW}警告:未找到 nft 命令,无法立即 flush 当前 ruleset。${PLAIN}" + echo -e "${YELLOW}警告:未找到 nft 命令,无法立即卸载 nftpf 托管表。${PLAIN}" fi if is_nftpf_managed_config || [ ! -f "$CONFIG_FILE" ]; then @@ -3007,7 +3219,7 @@ uninstall_script() { clear echo -e "${RED}=== 卸载 nftpf ===${PLAIN}" echo "将执行以下清理:" - echo "1. 清空当前 nftables ruleset,并将 nftpf 托管配置重置为空规则。" + echo "1. 仅卸载 nftpf 托管表,并将 nftpf 托管配置重置为空规则。" echo "2. 删除 DDNS 自动刷新 timer/service、旧 cron 任务。" echo "3. 删除多网卡托管回程 route service,并清理 nftpf 创建的 fwmark/ip rule/路由表。" echo "4. 删除规则库、线路库、访问控制配置和访问记录。" @@ -3376,20 +3588,54 @@ confirm_whitelist_family_coverage() { commit_access_file() { local tmp_access=$1 + local rollback_dir + local had_access=0 + local had_config=0 create_state_backup "before-access-change" 1 + rollback_dir=$(mktemp -d) || { rm -f "$tmp_access"; return 1; } + if [ -f "$ACCESS_FILE" ]; then + if ! cp "$ACCESS_FILE" "$rollback_dir/access.conf"; then + rm -f "$tmp_access" + rm -rf "$rollback_dir" + echo -e "${RED}错误:无法创建访问控制回滚快照,已取消修改。${PLAIN}" + return 1 + fi + had_access=1 + fi + if [ -f "$CONFIG_FILE" ]; then + if ! cp "$CONFIG_FILE" "$rollback_dir/nftables.conf"; then + rm -f "$tmp_access" + rm -rf "$rollback_dir" + echo -e "${RED}错误:无法创建配置回滚快照,已取消修改。${PLAIN}" + return 1 + fi + had_config=1 + fi if ACCESS_FILE="$tmp_access" write_config_from_rules "$RULES_FILE"; then backup_file "$ACCESS_FILE" - mv "$tmp_access" "$ACCESS_FILE" + if ! mv "$tmp_access" "$ACCESS_FILE"; then + if [ "$had_config" -eq 1 ]; then cp "$rollback_dir/nftables.conf" "$CONFIG_FILE"; else rm -f "$CONFIG_FILE"; fi + rm -f "$tmp_access" + rm -rf "$rollback_dir" + echo -e "${RED}错误:无法更新访问控制文件,配置文件已回滚。${PLAIN}" + return 1 + fi if apply_config_changes; then + rm -rf "$rollback_dir" show_apply_result return 0 fi + if [ "$had_access" -eq 1 ]; then cp "$rollback_dir/access.conf" "$ACCESS_FILE"; else rm -f "$ACCESS_FILE"; fi + if [ "$had_config" -eq 1 ]; then cp "$rollback_dir/nftables.conf" "$CONFIG_FILE"; else rm -f "$CONFIG_FILE"; fi + rm -rf "$rollback_dir" + echo -e "${YELLOW}访问控制应用失败,访问控制文件和配置文件已自动回滚。${PLAIN}" return 1 fi rm -f "$tmp_access" + rm -rf "$rollback_dir" return 1 } @@ -3529,6 +3775,12 @@ restore_backup_path() { local backup_path=$1 local tmp_dir local tmp_access + local rollback_dir + local file + local had_file + local source + local target + local import_copy_ok=1 if [ ! -f "$backup_path" ]; then echo -e "${RED}错误:备份文件不存在。${PLAIN}" @@ -3564,24 +3816,62 @@ restore_backup_path() { fi create_state_backup "before-restore" 1 + rollback_dir=$(mktemp -d) || { rm -rf "$tmp_dir"; return 1; } + for file in rules.db lines.db access.conf access-history.log nftables.conf; do + had_file=0 + case "$file" in + rules.db) source=$RULES_FILE ;; + lines.db) source=$LINES_FILE ;; + access.conf) source=$ACCESS_FILE ;; + access-history.log) source=$ACCESS_HISTORY_FILE ;; + nftables.conf) source=$CONFIG_FILE ;; + esac + if [ -f "$source" ]; then + if ! cp "$source" "$rollback_dir/$file"; then + rm -rf "$tmp_dir" "$rollback_dir" + echo -e "${RED}错误:无法创建导入前回滚快照,已取消导入。${PLAIN}" + return 1 + fi + had_file=1 + fi + echo "$had_file" > "$rollback_dir/$file.present" + done if ACCESS_FILE="$tmp_dir/access.conf" LINES_FILE="$tmp_dir/lines.db" write_config_from_rules "$tmp_dir/rules.db"; then - cp "$tmp_dir/rules.db" "$RULES_FILE" - cp "$tmp_dir/lines.db" "$LINES_FILE" - cp "$tmp_dir/access.conf" "$ACCESS_FILE" + cp "$tmp_dir/rules.db" "$RULES_FILE" || import_copy_ok=0 + cp "$tmp_dir/lines.db" "$LINES_FILE" || import_copy_ok=0 + cp "$tmp_dir/access.conf" "$ACCESS_FILE" || import_copy_ok=0 if [ -f "$tmp_dir/access-history.log" ]; then - cp "$tmp_dir/access-history.log" "$ACCESS_HISTORY_FILE" - chmod 600 "$ACCESS_HISTORY_FILE" 2>/dev/null || true + cp "$tmp_dir/access-history.log" "$ACCESS_HISTORY_FILE" || import_copy_ok=0 + if [ "$import_copy_ok" -eq 1 ]; then + chmod 600 "$ACCESS_HISTORY_FILE" 2>/dev/null || true + fi fi - if apply_config_changes; then + if [ "$import_copy_ok" -eq 1 ] && apply_config_changes; then show_apply_result echo -e "${GREEN}备份已导入并应用: $backup_path${PLAIN}" - rm -rf "$tmp_dir" + rm -rf "$tmp_dir" "$rollback_dir" return 0 fi + [ "$import_copy_ok" -eq 1 ] || echo -e "${RED}错误:备份状态文件写入不完整,未应用规则。${PLAIN}" fi - rm -rf "$tmp_dir" + for file in rules.db lines.db access.conf access-history.log nftables.conf; do + case "$file" in + rules.db) target=$RULES_FILE ;; + lines.db) target=$LINES_FILE ;; + access.conf) target=$ACCESS_FILE ;; + access-history.log) target=$ACCESS_HISTORY_FILE ;; + nftables.conf) target=$CONFIG_FILE ;; + esac + if [ "$(cat "$rollback_dir/$file.present")" = "1" ]; then + cp "$rollback_dir/$file" "$target" + else + rm -f "$target" + fi + done + rm -rf "$tmp_dir" "$rollback_dir" + echo -e "${YELLOW}备份导入失败,规则库、访问控制、线路和配置文件已自动回滚。${PLAIN}" return 1 } @@ -3660,6 +3950,55 @@ backup_restore_menu() { esac } +self_test() { + local tmp_dir + local tmp_rules + local tmp_config + local saved_rules_file=$RULES_FILE + local saved_lines_file=$LINES_FILE + local saved_access_file=$ACCESS_FILE + local saved_state_dir=$STATE_DIR + + tmp_dir=$(mktemp -d) || return 1 + tmp_rules="$tmp_dir/rules.db" + tmp_config="$tmp_dir/nftables.conf" + STATE_DIR="$tmp_dir/state" + RULES_FILE="$tmp_rules" + LINES_FILE="$tmp_dir/lines.db" + ACCESS_FILE="$tmp_dir/access.conf" + mkdir -p "$STATE_DIR" + : > "$RULES_FILE" + : > "$LINES_FILE" + echo 'mode=off' > "$ACCESS_FILE" + + if ! bash -n "${BASH_SOURCE[0]}"; then + rm -rf "$tmp_dir" + return 1 + fi + + generate_config_from_rules "$RULES_FILE" > "$tmp_config" + grep -q '^# NFTPF_RENDER_VERSION=4$' "$tmp_config" || { echo 'self-test: render version mismatch' >&2; rm -rf "$tmp_dir"; return 1; } + grep -q '^table ip nftpf_nat {' "$tmp_config" || { echo 'self-test: IPv4 managed NAT table missing' >&2; rm -rf "$tmp_dir"; return 1; } + grep -q '^table ip6 nftpf_nat {' "$tmp_config" || { echo 'self-test: IPv6 managed NAT table missing' >&2; rm -rf "$tmp_dir"; return 1; } + if grep -q '^flush ruleset$' "$tmp_config"; then + echo 'self-test: generated config must not flush the global ruleset' >&2 + rm -rf "$tmp_dir" + return 1 + fi + if build_apply_transaction "$tmp_dir/missing.conf" "$tmp_dir/missing-transaction.nft" 2>/dev/null; then + echo 'self-test: transaction build must fail when its config input is missing' >&2 + rm -rf "$tmp_dir" + return 1 + fi + + RULES_FILE=$saved_rules_file + LINES_FILE=$saved_lines_file + ACCESS_FILE=$saved_access_file + STATE_DIR=$saved_state_dir + rm -rf "$tmp_dir" + echo "[OK] nftpf self-test passed." +} + # ----------------------------------------------------------------------------- # Status and interactive menu # ----------------------------------------------------------------------------- @@ -3677,8 +4016,11 @@ get_status() { INSTALL_STATUS="${RED}未安装${PLAIN}" fi - RUN_STATUS="${RED}未运行${PLAIN}" - command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet "$SERVICE_NAME" && RUN_STATUS="${GREEN}运行中${PLAIN}" + RUN_STATUS="${RED}未加载${PLAIN}" + find_nft_cmd && managed_rules_are_loaded && RUN_STATUS="${GREEN}已加载${PLAIN}" + + BOOT_STATUS="${RED}未启用${PLAIN}" + command -v systemctl >/dev/null 2>&1 && systemctl is-enabled --quiet "$SERVICE_NAME" && BOOT_STATUS="${GREEN}已启用${PLAIN}" local ip4 local ip6 @@ -3726,12 +4068,13 @@ main_menu() { echo -e "# NFT端口转发简易化工具 #" echo -e "################################################" echo -e "Nftables 状态: ${INSTALL_STATUS}" - echo -e "服务运行 状态: ${RUN_STATUS}" + echo -e "实时规则 状态: ${RUN_STATUS}" + echo -e "开机自启 状态: ${BOOT_STATUS}" echo -e "IP转发 状态: ${FW_STATUS}" echo -e "访问控制 状态: ${ACCESS_STATUS}" echo -e "入口线路 状态: ${LINE_STATUS}" echo -e "${YELLOW}提示: 输入 nftpf 可快速启动本脚本${PLAIN}" - echo -e "${YELLOW}注意: 本工具生成配置时包含 flush ruleset,会清空当前 nftables 规则集。${PLAIN}" + echo -e "${YELLOW}说明: 本工具仅替换 nftpf_* 托管表,不清除其他 nftables/iptables-nft 规则。${PLAIN}" echo -e "################################################" show_rules_overview echo -e "################################################" @@ -3745,8 +4088,8 @@ main_menu() { echo -e " 7. 设置开机自启" echo -e " 8. 取消开机自启" echo -e " 9. 启动服务" - echo -e "10. 停止服务" - echo -e "11. 重启服务" + echo -e "10. 卸载本工具实时规则" + echo -e "11. 重新原子应用规则" echo -e "------------------------------------------------" echo -e "12. 访问控制(白名单/黑名单)" echo -e "13. 刷新 DDNS 规则" @@ -3798,6 +4141,19 @@ run_cli() { apply_managed_routes exit $? ;; + --apply) + check_dependencies || exit 1 + [ "$CONFIG_AUTO_REBUILT" -eq 1 ] && exit 0 + if apply_config_changes; then + show_apply_result + exit 0 + fi + exit 1 + ;; + --self-test) + self_test + exit $? + ;; --update) NFTPF_CLI_MODE=1 update_script @@ -3817,6 +4173,8 @@ run_cli() { echo " nftpf Open interactive forwarding menu" echo " nftpf --refresh-ddns Refresh DDNS/domain forwarding targets" echo " nftpf --apply-routes Apply managed multi-NIC policy routes" + echo " nftpf --apply Validate, atomically apply, and persist nftpf rules" + echo " nftpf --self-test Run local syntax and renderer checks" echo " nftpf --update Download and install latest nftpf script" echo " nftpf --uninstall Uninstall nftpf and clean managed state" echo " nftpf --version Show current nftpf version" @@ -3829,6 +4187,8 @@ run_cli() { echo " nftpf Open interactive forwarding menu" echo " nftpf --refresh-ddns Refresh DDNS/domain forwarding targets" echo " nftpf --apply-routes Apply managed multi-NIC policy routes" + echo " nftpf --apply Validate, atomically apply, and persist nftpf rules" + echo " nftpf --self-test Run local syntax and renderer checks" echo " nftpf --update Download and install latest nftpf script" echo " nftpf --uninstall Uninstall nftpf and clean managed state" echo " nftpf --version Show current nftpf version" diff --git a/tests/namespace-integration.sh b/tests/namespace-integration.sh new file mode 100644 index 0000000..b3c0250 --- /dev/null +++ b/tests/namespace-integration.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -euo pipefail + +script=${1:?usage: namespace-integration.sh /path/to/nftpf.sh} +[ "$EUID" -eq 0 ] || { echo 'namespace integration test must run as root' >&2; exit 1; } +for command in bash grep ip iptables ip6tables nft unshare; do + command -v "$command" >/dev/null 2>&1 || { echo "missing test dependency: $command" >&2; exit 1; } +done + +bash -n "$script" +bash "$script" --self-test + +unshare -n bash -s -- "$script" <<'TEST' +set -euo pipefail + +script=$1 +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +ip link set lo up + +cat > "$work/legacy.nft" <<'EOF' +table ip nat { + chain prerouting { + type nat hook prerouting priority dstnat; policy accept; + tcp dport 4060 counter dnat to 127.0.0.1:9 + } + chain postrouting { + type nat hook postrouting priority srcnat; policy accept; + ct status dnat masquerade + } +} + +table ip6 nat { + chain prerouting { + type nat hook prerouting priority dstnat; policy accept; + } + chain postrouting { + type nat hook postrouting priority srcnat; policy accept; + } +} + +table inet foreign_guard { + chain input { + type filter hook input priority filter; policy accept; + counter comment "foreign-guard" + } +} +EOF +nft -f "$work/legacy.nft" +iptables -t nat -A PREROUTING -i lo -p tcp --dport 4567 -m comment --comment foreign-phantun \ + -j DNAT --to-destination 127.0.0.1 +iptables -t nat -A POSTROUTING -o lo -m comment --comment foreign-phantun-post -j MASQUERADE +iptables -A FORWARD -i lo -m comment --comment foreign-filter -j ACCEPT +ip6tables -t nat -A PREROUTING -i lo -p tcp --dport 4567 -m comment --comment foreign-ip6 \ + -j DNAT --to-destination ::1 +ip6tables -t nat -A POSTROUTING -o lo -m comment --comment foreign-ip6-post -j MASQUERADE +iptables-save 2>/dev/null | grep '^-A' > "$work/iptables-baseline.txt" +ip6tables-save 2>/dev/null | grep '^-A' > "$work/ip6tables-baseline.txt" + +export NFT_HELPER_SKIP_ROOT=1 +source <(sed '/^run_cli "\$@"$/,$d' "$script") + +CONFIG_FILE="$work/nftables.conf" +STATE_DIR="$work/state" +RULES_FILE="$STATE_DIR/rules.db" +LINES_FILE="$STATE_DIR/lines.db" +ACCESS_FILE="$STATE_DIR/access.conf" +BACKUP_DIR="$STATE_DIR/backups" +ACCESS_HISTORY_FILE="$STATE_DIR/access-history.log" +REAL_NFT_CMD=$(command -v nft) +NFTPF_IPV6_ROUTEFIX=off +mkdir -p "$STATE_DIR" "$BACKUP_DIR" +printf '%s\n' '1|ipv4||4060|4060|ip|127.0.0.1|127.0.0.1|9|9|single|tcp_udp||none' > "$RULES_FILE" +: > "$LINES_FILE" +: > "$ACCESS_HISTORY_FILE" +printf '%s\n' 'mode=off' > "$ACCESS_FILE" + +generate_config_from_rules "$RULES_FILE" > "$CONFIG_FILE" +MIGRATE_LEGACY_NAT=1 +build_apply_transaction "$CONFIG_FILE" "$work/apply.nft" +grep -q '^delete chain ip nat prerouting$' "$work/apply.nft" +grep -q '^delete chain ip6 nat postrouting$' "$work/apply.nft" +if grep -q '^delete chain ip nat PREROUTING$' "$work/apply.nft"; then + echo 'uppercase foreign chain scheduled for deletion' >&2 + exit 1 +fi +nft -c -f "$work/apply.nft" +nft -f "$work/apply.nft" + +nft list chain ip nat PREROUTING >/dev/null +nft list chain ip nat POSTROUTING >/dev/null +nft list chain ip6 nat PREROUTING >/dev/null +nft list table inet foreign_guard | grep 'foreign-guard' >/dev/null +iptables-save 2>/dev/null | grep '^-A' > "$work/iptables-after-migration.txt" +ip6tables-save 2>/dev/null | grep '^-A' > "$work/ip6tables-after-migration.txt" +cmp -s "$work/iptables-baseline.txt" "$work/iptables-after-migration.txt" +cmp -s "$work/ip6tables-baseline.txt" "$work/ip6tables-after-migration.txt" +if nft list chain ip nat prerouting >/dev/null 2>&1; then + echo 'legacy IPv4 chain survived migration' >&2 + exit 1 +fi +if nft list chain ip6 nat postrouting >/dev/null 2>&1; then + echo 'legacy IPv6 chain survived migration' >&2 + exit 1 +fi +nft list table ip nftpf_nat | grep 'dport 4060' >/dev/null + +MIGRATE_LEGACY_NAT=0 +build_apply_transaction "$CONFIG_FILE" "$work/reapply.nft" +nft -c -f "$work/reapply.nft" +nft -f "$work/reapply.nft" +nft list chain ip nat PREROUTING >/dev/null +nft list table inet foreign_guard | grep 'foreign-guard' >/dev/null +iptables-save 2>/dev/null | grep '^-A' > "$work/iptables-after-reapply.txt" +cmp -s "$work/iptables-baseline.txt" "$work/iptables-after-reapply.txt" + +nft list ruleset > "$work/before-failure.nft" +cp "$CONFIG_FILE" "$work/invalid.conf" +printf '%s\n' 'this is invalid nft syntax' >> "$work/invalid.conf" +build_apply_transaction "$work/invalid.conf" "$work/invalid-apply.nft" +if nft -f "$work/invalid-apply.nft" >/dev/null 2>&1; then + echo 'invalid transaction unexpectedly succeeded' >&2 + exit 1 +fi +nft list ruleset > "$work/after-failure.nft" +cmp -s "$work/before-failure.nft" "$work/after-failure.nft" + +unload_managed_rules +if nft list table ip nftpf_nat >/dev/null 2>&1; then + echo 'managed IPv4 table survived unload' >&2 + exit 1 +fi +nft list chain ip nat PREROUTING >/dev/null +nft list table inet foreign_guard | grep 'foreign-guard' >/dev/null +iptables-save 2>/dev/null | grep '^-A' > "$work/iptables-after-unload.txt" +cmp -s "$work/iptables-baseline.txt" "$work/iptables-after-unload.txt" + +echo '[OK] namespace migration, coexistence, atomicity, and cleanup tests passed.' +TEST