mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-11 19:05:11 +08:00
444 lines
14 KiB
Bash
Executable File
444 lines
14 KiB
Bash
Executable File
#!/bin/sh /etc/rc.common
|
||
#
|
||
# Copyright (C) 2015 OpenWrt-dist
|
||
#
|
||
# This is free software, licensed under the GNU General Public License v3.
|
||
# See /LICENSE for more information.
|
||
#
|
||
|
||
START=30
|
||
|
||
CONFIG=miaplus
|
||
|
||
# 非 procd 脚本:status 命令不在 rc.common 默认命令表里,需显式注册
|
||
EXTRA_COMMANDS="status"
|
||
EXTRA_HELP=" status Show service status (auto fw3/fw4)"
|
||
|
||
. /lib/functions.sh
|
||
|
||
# 检测当前使用的防火墙框架:fw4 (nftables) 还是 fw3 (iptables)
|
||
# 注意:fw4 与 fw3 共用 /etc/config/firewall,不存在 /etc/config/fw4,
|
||
# 只能靠 fw4 / nft 命令是否存在来判定。
|
||
have_fw4() {
|
||
command -v fw4 >/dev/null 2>&1 && command -v nft >/dev/null 2>&1
|
||
}
|
||
|
||
uci_export_section_name() {
|
||
local ret=$(uci -n export $CONFIG | grep "config $1" | awk '{print $3}' | awk -F\' '{print $2}')
|
||
echo ${ret:=$2}
|
||
}
|
||
uci_get_by_name() {
|
||
local ret=$(uci get $CONFIG.$1.$2 2>/dev/null)
|
||
echo ${ret:=$3}
|
||
}
|
||
uci_get_by_type() {
|
||
local index=0
|
||
if [ -n "$4" ]; then
|
||
index=$4
|
||
fi
|
||
local ret=$(uci get $CONFIG.@$1[$index].$2 2>/dev/null)
|
||
echo ${ret:=$3}
|
||
}
|
||
uci_get_mac_by_template(){
|
||
local ret=""
|
||
for i in $(seq 0 100)
|
||
do
|
||
local enable=$(uci_get_by_type macbind enable '' $i)
|
||
local macaddr=$(uci_get_by_type macbind macaddr '' $i)
|
||
local template=$(uci_get_by_type macbind template '' $i)
|
||
if [ -z $enable ] || [ -z $macaddr ] || [ -z $template ]; then
|
||
break
|
||
fi
|
||
if [ "$enable" == "1" ]; then
|
||
if [ -z "$1" ] || [ "$template" == "$1" ]; then
|
||
if [ "$ret" == "" ]; then
|
||
ret=$macaddr
|
||
else
|
||
ret="$ret $macaddr"
|
||
fi
|
||
fi
|
||
fi
|
||
done
|
||
echo ${ret:=$2}
|
||
}
|
||
|
||
add_rules(){
|
||
config_load "$CONFIG"
|
||
config_foreach add_template_rule templates
|
||
}
|
||
add_template_rule(){
|
||
local section="$1"
|
||
local enable
|
||
|
||
config_get enable "$section" enable 0
|
||
[ "$enable" = "1" ] && add_rule "$section"
|
||
}
|
||
# 分发到具体实现
|
||
add_rule(){
|
||
if have_fw4; then
|
||
add_rule_nft "$1"
|
||
else
|
||
add_rule_ipt "$1"
|
||
fi
|
||
}
|
||
|
||
# =====================================================================
|
||
# fw3 (iptables / ip6tables)
|
||
# =====================================================================
|
||
add_rule_ipt(){
|
||
ipv6enable=$(uci -q get miaplus.@basic[0].ipv6enable)
|
||
local macaddrs=$(uci_get_mac_by_template $1 '')
|
||
if [ -z "$macaddrs" ]; then
|
||
return 0
|
||
fi
|
||
for macaddr in $macaddrs
|
||
do
|
||
iptables -t filter -A MIAPLUS -m mac --mac-source $macaddr -j DROP
|
||
[ "${ipv6enable:-0}" -eq 1 ] && ip6tables -t filter -A MIAPLUS -m mac --mac-source $macaddr -j DROP
|
||
done
|
||
for i in $(seq 0 100)
|
||
do
|
||
local enable=$(uci_get_by_type $1 enable '' $i)
|
||
local timeon=$(uci_get_by_type $1 timeon '' $i)
|
||
local timeoff=$(uci_get_by_type $1 timeoff '' $i)
|
||
local z1=$(uci_get_by_type $1 z1 '' $i)
|
||
local z2=$(uci_get_by_type $1 z2 '' $i)
|
||
local z3=$(uci_get_by_type $1 z3 '' $i)
|
||
local z4=$(uci_get_by_type $1 z4 '' $i)
|
||
local z5=$(uci_get_by_type $1 z5 '' $i)
|
||
local z6=$(uci_get_by_type $1 z6 '' $i)
|
||
local z7=$(uci_get_by_type $1 z7 '' $i)
|
||
[ "$z1" == "1" ] && Z1="Mon,"
|
||
[ "$z2" == "1" ] && Z2="Tue,"
|
||
[ "$z3" == "1" ] && Z3="Wed,"
|
||
[ "$z4" == "1" ] && Z4="Thu,"
|
||
[ "$z5" == "1" ] && Z5="Fri,"
|
||
[ "$z6" == "1" ] && Z6="Sat,"
|
||
[ "$z7" == "1" ] && Z7="Sun"
|
||
|
||
if [ -z $enable ] || [ -z $timeoff ] || [ -z $timeon ]; then
|
||
break
|
||
fi
|
||
if [ "$enable" == "1" ]; then
|
||
for macaddr in $macaddrs
|
||
do
|
||
iptables -t filter -I MIAPLUS -m mac --mac-source $macaddr -m time --kerneltz --timestart $timeon --timestop $timeoff --weekdays $Z1$Z2$Z3$Z4$Z5$Z6$Z7 -j ACCEPT
|
||
[ "${ipv6enable:-0}" -eq 1 ] && ip6tables -t filter -I MIAPLUS -m mac --mac-source $macaddr -m time --kerneltz --timestart $timeon --timestop $timeoff --weekdays $Z1$Z2$Z3$Z4$Z5$Z6$Z7 -j ACCEPT
|
||
done
|
||
fi
|
||
for n in $(seq 1 7)
|
||
do
|
||
unset "Z$n"
|
||
done
|
||
done
|
||
}
|
||
|
||
start_ipt(){
|
||
enable=$(uci -q get miaplus.@basic[0].enable)
|
||
[ "${enable:-0}" -eq 0 ] && return 0
|
||
iptables -t filter -N MIAPLUS
|
||
iptables -I INPUT -p udp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS
|
||
iptables -I INPUT -p tcp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS
|
||
iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 53 -m comment --comment "Rule For Control"
|
||
iptables -t nat -A PREROUTING -p tcp --dport 53 -j REDIRECT --to-ports 53 -m comment --comment "Rule For Control"
|
||
strict=$(uci -q get miaplus.@basic[0].strict)
|
||
[ "${strict:-0}" -eq 1 ] && iptables -t filter -I FORWARD -m comment --comment "Rule For Control" -j MIAPLUS
|
||
|
||
ipv6enable=$(uci -q get miaplus.@basic[0].ipv6enable)
|
||
if [ "${ipv6enable:-0}" -eq 1 ]; then
|
||
ip6tables -t filter -N MIAPLUS
|
||
ip6tables -I INPUT -p udp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS
|
||
ip6tables -I INPUT -p tcp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS
|
||
ip6tables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 53 -m comment --comment "Rule For Control"
|
||
ip6tables -t nat -A PREROUTING -p tcp --dport 53 -j REDIRECT --to-ports 53 -m comment --comment "Rule For Control"
|
||
[ "${strict:-0}" -eq 1 ] && ip6tables -t filter -I FORWARD -m comment --comment "Rule For Control" -j MIAPLUS
|
||
fi
|
||
add_rules
|
||
}
|
||
status_ipt(){
|
||
if iptables -t filter -S MIAPLUS >/dev/null 2>&1; then
|
||
echo "running"
|
||
return 0
|
||
fi
|
||
|
||
echo "not running"
|
||
return 1
|
||
}
|
||
stop_ipt(){
|
||
iptables -t filter -D FORWARD -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
||
iptables -D INPUT -p udp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
||
iptables -D INPUT -p tcp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
||
iptables -t nat -D PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 53 -m comment --comment "Rule For Control" 2>/dev/null
|
||
iptables -t nat -D PREROUTING -p tcp --dport 53 -j REDIRECT --to-ports 53 -m comment --comment "Rule For Control" 2>/dev/null
|
||
iptables -t filter -F MIAPLUS 2>/dev/null
|
||
iptables -t filter -X MIAPLUS 2>/dev/null
|
||
|
||
notfound=$(type ip6tables | grep not)
|
||
if [ -z "$notfound" ]; then
|
||
ip6tables -t filter -D FORWARD -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
||
ip6tables -D INPUT -p udp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
||
ip6tables -D INPUT -p tcp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
||
ip6tables -t nat -D PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 53 -m comment --comment "Rule For Control" 2>/dev/null
|
||
ip6tables -t nat -D PREROUTING -p tcp --dport 53 -j REDIRECT --to-ports 53 -m comment --comment "Rule For Control" 2>/dev/null
|
||
ip6tables -t filter -F MIAPLUS 2>/dev/null
|
||
ip6tables -t filter -X MIAPLUS 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# =====================================================================
|
||
# fw4 (nftables)
|
||
# 使用独立表 inet miaplus,避免与 fw4 自身的 nftables 规则冲突。
|
||
#
|
||
# table inet miaplus {
|
||
# chain input { type filter hook input priority 0; } # 仅匹配 53 端口
|
||
# chain prerouting{ type nat hook prerouting priority dstnat; } # DNS 重定向到本机
|
||
# }
|
||
#
|
||
# DNS 语义与 fw3 一致:只对发往 53 端口的流量做上网控制。
|
||
#
|
||
# NAT 竞争说明:本表 prerouting 链与 fw4 主表(inet fw4)的 PREROUTING/dstnat 同为
|
||
# priority -100(dstnat)。nf_tables 允许多表同优先级共存,但同一数据包的同一 hook
|
||
# 优先级内按表加载顺序处理,conntrack binding(redirect/redirect)先到先得。若 fw4
|
||
# 自身对 DNS(53) 也做了重定向/端口转发,两者可能互抢,现象以先加载者为准。
|
||
# 一般情况下 DNS 默认直通本机,二者不冲突;如遇规则不生效,优先排查此项。
|
||
# =====================================================================
|
||
|
||
# 构造 nft 的星期集合片段,如 "{ 1, 2 }" / "5" / 空(不限制)
|
||
# z1..z7 => 周一..周日;nft meta day: 0=Sun,1=Mon,...6=Sat => z1->1 ...
|
||
nft_dayterm() {
|
||
local days="" n=1 day
|
||
for z in "$1" "$2" "$3" "$4" "$5" "$6" "$7"; do
|
||
[ "$z" = "1" ] || { n=$((n+1)); continue; }
|
||
day=$(( n % 7 )) # z1..z7 -> 1,2,3,4,5,6,0
|
||
days="${days:+$days, }$day"
|
||
n=$((n+1))
|
||
done
|
||
if [ -z "$days" ]; then
|
||
echo ""
|
||
elif echo "$days" | grep -q ,; then
|
||
echo "{ $days }"
|
||
else
|
||
echo "$days"
|
||
fi
|
||
}
|
||
|
||
# 去掉前导零(防空串转成 "0"),规避 08/09 被当八进制的算术陷阱
|
||
_digits() {
|
||
echo "$1" | sed -e 's/^0*//' -e 's/^$/0/'
|
||
}
|
||
# HH:MM -> 分钟数(输入可为 H:M 或 HH:MM,非零填充)
|
||
_mins() {
|
||
local h m
|
||
h=$(_digits "${1%%:*}")
|
||
m=$(_digits "${1#*:}")
|
||
echo $(( ${h:-0} * 60 + ${m:-0} ))
|
||
}
|
||
# 任意 "H:M"/"HH:MM" -> 规范 "HH:MM"
|
||
_pad() {
|
||
local h m
|
||
h=$(_digits "${1%%:*}")
|
||
m=$(_digits "${1#*:}")
|
||
printf '%02d:%02d' "${h:-0}" "${m:-0}"
|
||
}
|
||
|
||
# 把一个 [t_on, t_off] 窗口(H:M/HH:MM)转成 nft 时间匹配表达式(行),
|
||
# 输出到全局变量 NFT_FRAGS。nft 的 meta hour 原生支持 "HH:MM"-"HH:MM"。
|
||
# 统一语义:结束分钟视为完整包含(闭区间含其末秒),故上界一律补 :59 —
|
||
# 非跨午夜 [t_on, t_off] -> "HH:MM"-"t_off:59";跨午夜拆 [t_on,23:59:59]+[00:00,t_off:59]。
|
||
nft_split_window() {
|
||
local t_on t_off
|
||
NFT_FRAGS=""
|
||
[ -n "$1" ] && [ -n "$2" ] || return 0
|
||
t_on=$(_pad "$1"); t_off=$(_pad "$2")
|
||
|
||
if [ "$(_mins "$t_on")" -le "$(_mins "$t_off")" ]; then
|
||
NFT_FRAGS="meta hour \"$t_on\"-\"$t_off:59\""
|
||
else
|
||
# 跨午夜:拆成 [on,23:59:59] + [00:00,off:59]
|
||
NFT_FRAGS="meta hour \"$t_on\"-\"23:59:59\"
|
||
meta hour \"00:00\"-\"$t_off:59\""
|
||
fi
|
||
}
|
||
|
||
# 统一执行 nft 规则命令并检查返回码。
|
||
# 失败时记日志、立即回滚删表并置全局 NFT_FAIL=1(供调用方中止后续写入),
|
||
# 使最终状态收敛为"完整规则表"或"空表",避免留下只 DROP 无 ACCEPT 的断网中间态。
|
||
nft_rule() {
|
||
# 已失败则短路,避免对已回滚删除的表继续逐条报错刷屏
|
||
[ -n "$NFT_FAIL" ] && return 1
|
||
"$@" 2>/dev/null || {
|
||
logger -t miaplus "nft rule failed: $*"
|
||
stop_nft
|
||
NFT_FAIL=1
|
||
}
|
||
}
|
||
|
||
# 组合规则前缀:链决定是否只限 53 端口;ipv6enable=0 时限定 ipv4
|
||
nft_prefix() {
|
||
local chain=$1
|
||
local pref=""
|
||
[ "$chain" = "forward" ] || pref="meta l4proto { tcp, udp } th dport 53 "
|
||
if [ "$ipv6enable" != "1" ]; then
|
||
pref="meta nfproto ipv4 $pref"
|
||
fi
|
||
NFT_PREFIX="$pref"
|
||
}
|
||
|
||
# 插入一条白名单 ACCEPT 规则(insert 到链首,兜底 DROP 在尾)
|
||
# 参数:MAC day_term [链名=当前]
|
||
nft_insert_accept() {
|
||
local mac=$1 day=$2 chain=${3:-input}
|
||
nft_prefix "$chain"
|
||
local cond
|
||
while IFS= read -r frag; do
|
||
[ -n "$frag" ] || continue
|
||
cond="$NFT_PREFIX ether saddr $mac $frag"
|
||
[ -n "$day" ] && cond="$cond meta day $day"
|
||
nft_rule nft insert rule inet miaplus $chain $cond accept
|
||
[ -n "$NFT_FAIL" ] && return 1
|
||
done <<EOF
|
||
$NFT_FRAGS
|
||
EOF
|
||
}
|
||
|
||
# 插入一条兜底 DROP 规则(append 到链尾)
|
||
# 参数:MAC [链名=当前]
|
||
nft_add_drop() {
|
||
local mac=$1 chain=${2:-input}
|
||
nft_prefix "$chain"
|
||
nft_rule nft add rule inet miaplus $chain $NFT_PREFIX ether saddr $mac drop
|
||
}
|
||
|
||
# 需要写入的链列表:非严格=仅 input;严格=input+forward
|
||
nft_target_chains() {
|
||
if [ "${strict:-0}" -eq 1 ]; then
|
||
echo "input forward"
|
||
else
|
||
echo "input"
|
||
fi
|
||
}
|
||
|
||
add_rule_nft(){
|
||
local ipv6enable=$(uci -q get miaplus.@basic[0].ipv6enable)
|
||
local macaddrs=$(uci_get_mac_by_template $1 '')
|
||
if [ -z "$macaddrs" ]; then
|
||
return 0
|
||
fi
|
||
|
||
local chains="$(nft_target_chains)"
|
||
local macaddr i enable timeon timeoff z1 z2 z3 z4 z5 z6 z7
|
||
|
||
# 先为每个绑定 MAC 加兜底 DROP(每条链一次,不重复)
|
||
for macaddr in $macaddrs; do
|
||
[ -n "$NFT_FAIL" ] && return 1
|
||
local ch
|
||
for ch in $chains; do
|
||
nft_add_drop "$macaddr" "$ch"
|
||
done
|
||
done
|
||
|
||
# 再按时间段插入 ACCEPT 白名单
|
||
for i in $(seq 0 100); do
|
||
[ -n "$NFT_FAIL" ] && return 1
|
||
enable=$(uci_get_by_type $1 enable '' $i)
|
||
timeon=$(uci_get_by_type $1 timeon '' $i)
|
||
timeoff=$(uci_get_by_type $1 timeoff '' $i)
|
||
z1=$(uci_get_by_type $1 z1 '' $i)
|
||
z2=$(uci_get_by_type $1 z2 '' $i)
|
||
z3=$(uci_get_by_type $1 z3 '' $i)
|
||
z4=$(uci_get_by_type $1 z4 '' $i)
|
||
z5=$(uci_get_by_type $1 z5 '' $i)
|
||
z6=$(uci_get_by_type $1 z6 '' $i)
|
||
z7=$(uci_get_by_type $1 z7 '' $i)
|
||
|
||
[ -n "$enable" ] || break
|
||
[ -n "$timeon" ] || break
|
||
[ -n "$timeoff" ] || break
|
||
[ "$enable" = "1" ] || continue
|
||
|
||
local dayspec=$(nft_dayterm "$z1" "$z2" "$z3" "$z4" "$z5" "$z6" "$z7")
|
||
|
||
nft_split_window "$timeon" "$timeoff"
|
||
for macaddr in $macaddrs; do
|
||
local ch
|
||
for ch in $chains; do
|
||
nft_insert_accept "$macaddr" "$dayspec" "$ch"
|
||
done
|
||
done
|
||
done
|
||
}
|
||
|
||
start_nft(){
|
||
enable=$(uci -q get miaplus.@basic[0].enable)
|
||
[ "${enable:-0}" -eq 0 ] && return 0
|
||
strict=$(uci -q get miaplus.@basic[0].strict)
|
||
local ipv6enable=$(uci -q get miaplus.@basic[0].ipv6enable)
|
||
|
||
NFT_FAIL=""
|
||
|
||
# 建表/建链也走 nft_rule,使最根本的"建表/建链失败"同样记日志并可回滚
|
||
nft_rule nft add table inet miaplus
|
||
nft_rule nft add chain inet miaplus input '{ type filter hook input priority 0; policy accept; }'
|
||
nft_rule nft add chain inet miaplus prerouting '{ type nat hook prerouting priority dstnat; policy accept; }'
|
||
# DNS 劫持遵循 ipv6enable:关闭时仅重定向 IPv4 的 53,与 filter 链/DROP 语义对齐
|
||
if [ "${ipv6enable:-0}" -eq 1 ]; then
|
||
nft_rule nft add rule inet miaplus prerouting meta l4proto { tcp, udp } th dport 53 redirect to :53
|
||
else
|
||
nft_rule nft add rule inet miaplus prerouting meta nfproto ipv4 meta l4proto { tcp, udp } th dport 53 redirect to :53
|
||
fi
|
||
[ -n "$NFT_FAIL" ] && return 1
|
||
|
||
# 严格模式:额外对 FORWARD 全流量做同样控制
|
||
if [ "${strict:-0}" -eq 1 ]; then
|
||
nft_rule nft add chain inet miaplus forward '{ type filter hook forward priority 0; policy accept; }'
|
||
[ -n "$NFT_FAIL" ] && return 1
|
||
fi
|
||
|
||
add_rules
|
||
|
||
# 任一规则写入失败:回滚整个表,避免留下只有 DROP、无 ACCEPT 白名单的断网状态
|
||
if [ -n "$NFT_FAIL" ]; then
|
||
logger -t miaplus "fw4 rules incomplete, rolling back"
|
||
stop_nft
|
||
return 1
|
||
fi
|
||
}
|
||
status_nft(){
|
||
if nft list table inet miaplus >/dev/null 2>&1; then
|
||
echo "running"
|
||
return 0
|
||
fi
|
||
echo "not running"
|
||
return 1
|
||
}
|
||
stop_nft(){
|
||
nft delete table inet miaplus 2>/dev/null
|
||
return 0
|
||
}
|
||
|
||
# =====================================================================
|
||
# 统一入口
|
||
# =====================================================================
|
||
start(){
|
||
stop
|
||
if have_fw4; then
|
||
start_nft
|
||
else
|
||
start_ipt
|
||
fi
|
||
}
|
||
status(){
|
||
if have_fw4; then
|
||
status_nft
|
||
else
|
||
status_ipt
|
||
fi
|
||
}
|
||
stop(){
|
||
if have_fw4; then
|
||
stop_nft
|
||
else
|
||
stop_ipt
|
||
fi
|
||
}
|