diff --git a/scripts/init.sh b/scripts/init.sh index 00d7a4b8..46295b78 100644 --- a/scripts/init.sh +++ b/scripts/init.sh @@ -77,7 +77,7 @@ rm -rf "$CRASHDIR"/starts/shellcrash.openrc command -v bash >/dev/null 2>&1 && shtype=bash [ -x /bin/ash ] && shtype=ash #批量授权 -for file in start.sh starts/bfstart.sh starts/afstart.sh starts/fw_stop.sh menu.sh menus/task_cmd.sh menus/bot_tg.sh; do +for file in start.sh starts/bfstart.sh starts/afstart.sh starts/fw_stop.sh starts/fw_ipv6_watch.sh menu.sh menus/task_cmd.sh menus/bot_tg.sh; do sed -i "s|/bin/sh|/bin/$shtype|" "$CRASHDIR/$file" 2>/dev/null chmod +x "$CRASHDIR/$file" 2>/dev/null done diff --git a/scripts/libs/set_cron.sh b/scripts/libs/set_cron.sh index a586d1d2..11eb8d83 100644 --- a/scripts/libs/set_cron.sh +++ b/scripts/libs/set_cron.sh @@ -1,41 +1,95 @@ - crondir="$(crond -h 2>&1 | grep -oE 'Default:.*' | awk -F ":" '{print $2}'| tr -d ' ')" [ ! -w "$crondir" ] && crondir="/etc/storage/cron/crontabs" [ ! -w "$crondir" ] && crondir="/var/spool/cron/crontabs" [ ! -w "$crondir" ] && crondir="/var/spool/cron" -[ -z "$USER" ] && USER=$(whoami 2>/dev/null) +[ -z "$USER" ] && USER=$(id -un) [ -z "$TASKCFGDIR" ] && TASKCFGDIR="$CRASHDIR"/configs/task -tmpcron=/tmp/cron_tmp -touch "$tmpcron" -cronadd() { #定时任务工具 - if crontab -h 2>&1 | grep -q '\-l'; then - crontab "$1" - elif [ -w "$crondir" ];then - cat "$1" >"$crondir"/"$USER" && cru a REFRESH "0 0 1 1 * /bin/true" 2>/dev/null - else - echo "找不到可用的crond或者crontab应用!No available crond or crontab application can be found!" - fi -} -cronload() { #定时任务工具 - if crontab -h 2>&1 | grep -q '\-l'; then - crontab -l - elif [ -f "$crondir/$USER" ];then - cat "$crondir"/"$USER" 2>/dev/null - else +# ShellCrash 使用的 ash/bash(以及 dash)均支持 test -O。 +# shellcheck disable=SC3067 +croncheck() { #在任何写操作前检查实例管理权限,保留普通用户自己的安装 + cron_uid=$(id -u) || return 1 + [ "$cron_uid" = 0 ] && return 0 + if [ ! -O "$CRASHDIR/configs/ShellCrash.cfg" ] || [ ! -w "$CRASHDIR/configs/ShellCrash.cfg" ]; then + echo '无权管理此 ShellCrash 实例,请使用安装用户或 root。' >&2 return 1 fi + for cron_pid in $(pidof CrashCore 2>/dev/null); do + if [ ! -O "/proc/$cron_pid" ]; then + echo '无权管理其他用户启动的 CrashCore。' >&2 + return 1 + fi + done } -cronset() { #定时任务设置 - # 参数1代表要移除的关键字,参数2代表要添加的任务语句 - cronload | grep -v '^$' | grep -vF "$1" >"$tmpcron" - [ -n "$2" ] && echo "$2" >>"$tmpcron" - cronadd "$tmpcron" - #华硕/Padavan固件存档在本地,其他则删除 - if [ -d /jffs ] || [ -d /etc/storage/ShellCrash ];then - mkdir -p "$TASKCFGDIR" - mv -f "$tmpcron" "$TASKCFGDIR"/cron + +cronadd() ( #定时任务工具;子 shell 隔离临时变量和清理 trap + croncheck || exit 1 + [ -f "$1" ] && [ -r "$1" ] || exit 1 + if crontab -h 2>&1 | grep -q '\-l'; then + crontab "$1" + elif [ -w "$crondir" ]; then + cron_user=$(id -un) || exit 1 + cron_stage=$(mktemp "$crondir/.shellcrash.XXXXXX") || exit 1 + trap 'rm -f "$cron_stage"' 0 + trap 'exit 1' 1 2 3 15 + [ ! -e "$crondir/$cron_user" ] || cp -p "$crondir/$cron_user" "$cron_stage" || exit 1 + cat "$1" >"$cron_stage" && mv -f "$cron_stage" "$crondir/$cron_user" || exit 1 + if command -v cru >/dev/null 2>&1; then + cru a REFRESH "0 0 1 1 * /bin/true" + fi else - rm -f "$tmpcron" + echo '找不到可用的 crond 或 crontab!No available crond or crontab!' >&2 + exit 1 fi -} +) + +cronload() ( + cron_user=$(id -un) || exit 1 + if crontab -h 2>&1 | grep -q '\-l'; then + cron_text=$(LC_ALL=C crontab -l 2>&1) + cron_status=$? + if [ "$cron_status" = 0 ]; then + [ -z "$cron_text" ] || printf '%s\n' "$cron_text" + exit 0 + fi + #首次使用没有 crontab 是合法空表,其他错误必须传回调用者。 + case "$cron_text" in + "no crontab for $cron_user" | "crontab: can't open '$cron_user': No such file or directory") exit 0 ;; + esac + printf '%s\n' "$cron_text" >&2 + exit "$cron_status" + elif [ -d "$crondir" ] && [ -r "$crondir" ] && [ -x "$crondir" ]; then + [ -e "$crondir/$cron_user" ] || [ -L "$crondir/$cron_user" ] || exit 0 + cat "$crondir/$cron_user" + else + echo '无法读取原有 crontab,已取消修改。' >&2 + exit 1 + fi +) + +cronupdate() ( #成功读取、生成后才提交;参数是读取 stdin 的过滤命令 + croncheck || exit 1 + cron_tmp=$(mktemp -d /tmp/ShellCrash-cron.XXXXXX) || exit 1 + cron_saved= + trap 'rm -rf "$cron_tmp"; [ -z "$cron_saved" ] || rm -f "$cron_saved"' 0 + trap 'exit 1' 1 2 3 15 + cronload >"$cron_tmp/old" || exit 1 + "$@" <"$cron_tmp/old" >"$cron_tmp/new" || exit 1 + #先准备华硕/Padavan 的持久化副本;任何提交前的写入失败都保留原表。 + if [ "$cron_persist" = true ] && { [ -d /jffs ] || [ -d /etc/storage/ShellCrash ]; }; then + mkdir -p "$TASKCFGDIR" || exit 1 + cron_saved=$(mktemp "$TASKCFGDIR/.cron.XXXXXX") || exit 1 + cat "$cron_tmp/new" >"$cron_saved" || exit 1 + fi + cronadd "$cron_tmp/new" || exit 1 + [ -z "$cron_saved" ] || mv -f "$cron_saved" "$TASKCFGDIR/cron" +) + +cronset() ( #参数1:移除的文字;参数2:新增任务。空结果也是合法 crontab。 + cron_persist=true + # shellcheck disable=SC2016 + CRON_KEY="$1" CRON_TASK="$2" cronupdate awk ' + NF && !index($0, ENVIRON["CRON_KEY"]) { print } + END { if (ENVIRON["CRON_TASK"] != "") print ENVIRON["CRON_TASK"] } + ' +) diff --git a/scripts/menus/5_task.sh b/scripts/menus/5_task.sh index 5fb7f363..735773c6 100644 --- a/scripts/menus/5_task.sh +++ b/scripts/menus/5_task.sh @@ -23,7 +23,7 @@ set_cron() { "0) $TASK5_NO" read -r -p "$COMMON_INPUT> " res if [ "$res" = '1' ]; then - task_txt="$min $hour * * $week $CRASHDIR/task/task.sh $task_id $cron_time$task_name" + task_txt="$min $hour * * $week $CRASHDIR/task/task.sh $task_id \"$cron_time$task_name\"" cronset "$cron_time$task_name" "$task_txt" msg_alert -t 0 "$TASK5_TASK_PREFIX$cron_time$task_name$TASK5_TASK_ADDED" fi @@ -38,11 +38,11 @@ set_service() { [ -s "$task_file" ] && sed -i "/$3/d" "$task_file" # 运行时每分钟执行的任务特殊处理 if [ "$1" = "running" ]; then - task_txt="$4 $CRASHDIR/task/task.sh $2 $3" + task_txt="$4 $CRASHDIR/task/task.sh $2 \"$3\"" echo "$task_txt" >>"$task_file" [ -n "$(pidof CrashCore)" ] && cronset "$3" "$task_txt" else - echo "$CRASHDIR/task/task.sh $2 $3" >>"$task_file" + echo "$CRASHDIR/task/task.sh $2 \"$3\"" >>"$task_file" fi content_line "【$3】\033[32m$COMMON_SUCCESS\033[0m" sleep 1 @@ -144,11 +144,12 @@ task_del() { # 删除定时任务 cronset "$1" # 删除条件任务 - sed -i "/$1/d" "$TASKCFGDIR"/cron 2>/dev/null - sed -i "/$1/d" "$TASKCFGDIR"/bfstart 2>/dev/null - sed -i "/$1/d" "$TASKCFGDIR"/afstart 2>/dev/null - sed -i "/$1/d" "$TASKCFGDIR"/running 2>/dev/null - sed -i "/$1/d" "$TASKCFGDIR"/affirewall 2>/dev/null + [ -f "$TASKCFGDIR"/cron ] && sed -i "/$1/d" "$TASKCFGDIR"/cron 2>/dev/null + [ -f "$TASKCFGDIR"/bfstart ] && sed -i "/$1/d" "$TASKCFGDIR"/bfstart 2>/dev/null + [ -f "$TASKCFGDIR"/afstart ] && sed -i "/$1/d" "$TASKCFGDIR"/afstart 2>/dev/null + [ -f "$TASKCFGDIR"/running ] && sed -i "/$1/d" "$TASKCFGDIR"/running 2>/dev/null + [ -f "$TASKCFGDIR"/affirewall ] && sed -i "/$1/d" "$TASKCFGDIR"/affirewall 2>/dev/null + return 0 } # 任务条件选择菜单 @@ -293,7 +294,10 @@ task_manager() { break fi else - task_des=$(echo "$task_txt" | awk '{print $2}') + task_des=${task_txt#* } + task_des=${task_des#"${task_des%%[! ]*}"} + task_des=${task_des#\"} + task_des=${task_des%\"} task_name=$(cat "$CRASHDIR"/task/task_${i18n}.list "$TASKCFGDIR"/task.user 2>/dev/null | grep "$task_id" | awk -F '#' '{print $3}') comp_box "$TASK5_CURRENT_TASK\033[36m$task_des\033[0m" btm_box "1) $TASK5_EDIT_TASK" \ @@ -359,7 +363,7 @@ task_recom() { separator_line "=" set_service running "106" "$TASK_RECOM_ITEM_1" "*/10 * * * *" set_service afstart "107" "$TASK_RECOM_ITEM_2" - cronset "$TASK_RECOM_ITEM_3" "0 3 * * * ${CRASHDIR}/task/task.sh 103 $TASK_RECOM_ITEM_3" && + cronset "$TASK_RECOM_ITEM_3" "0 3 * * * ${CRASHDIR}/task/task.sh 103 \"$TASK_RECOM_ITEM_3\"" && content_line "【$TASK_RECOM_ITEM_3】\033[32m$COMMON_SUCCESS\033[0m" separator_line "=" } diff --git a/scripts/start.sh b/scripts/start.sh index 8fa387aa..f404de7d 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -6,10 +6,11 @@ cd $(dirname $0) pwd ) +. "$CRASHDIR"/libs/set_cron.sh +croncheck || exit 1 . "$CRASHDIR"/libs/get_config.sh #加载工具 . "$CRASHDIR"/libs/set_config.sh -. "$CRASHDIR"/libs/set_cron.sh . "$CRASHDIR"/libs/check_cmd.sh . "$CRASHDIR"/libs/check_autostart.sh . "$CRASHDIR"/libs/compare.sh @@ -36,7 +37,9 @@ start_l(){ case "$1" in start) - [ -n "$(pidof CrashCore)" ] && $0 stop #禁止多实例 + if [ -n "$(pidof CrashCore)" ]; then #禁止多实例 + "$0" stop || exit 1 + fi stop_firewall #清理路由策略 rm -f "$CRASHDIR"/\.start_error #移除自启失败标记 #使用不同方式启动服务 @@ -69,9 +72,7 @@ stop) logger ShellCrash服务即将关闭...... [ -n "$(pidof CrashCore)" ] && web_save #保存面板配置 #清理定时任务 - cronload | grep -vE '^$|start_legacy_wd.sh|运行时每' > "$TMPDIR"/cron_tmp - cronadd "$TMPDIR"/cron_tmp - rm -f "$TMPDIR"/cron_tmp + cronupdate awk 'NF && !/start_legacy_wd.sh|运行时每/' || exit 1 #停止tg_bot . "$CRASHDIR"/menus/bot_tg_service.sh && bot_tg_stop #多种方式结束进程 @@ -96,8 +97,7 @@ stop) rm -rf "$TMPDIR"/CrashCore ;; restart) - $0 stop - $0 start + "$0" stop && "$0" start ;; init) . "$CRASHDIR"/starts/general_init.sh @@ -110,7 +110,9 @@ daemon) fi ;; debug) - [ -n "$(pidof CrashCore)" ] && $0 stop >/dev/null #禁止多实例 + if [ -n "$(pidof CrashCore)" ]; then #禁止多实例 + "$0" stop >/dev/null || exit 1 + fi stop_firewall >/dev/null #清理路由策略 bfstart if [ -n "$2" ]; then diff --git a/scripts/starts/afstart.sh b/scripts/starts/afstart.sh index d798a517..27c8216c 100644 --- a/scripts/starts/afstart.sh +++ b/scripts/starts/afstart.sh @@ -7,11 +7,12 @@ cd .. pwd ) +. "$CRASHDIR"/libs/set_cron.sh +croncheck || exit 1 . "$CRASHDIR"/libs/get_config.sh #加载工具 . "$CRASHDIR"/libs/check_cmd.sh . "$CRASHDIR"/libs/logger.sh -. "$CRASHDIR"/libs/set_cron.sh #缺省值 [ -z "$firewall_area" ] && firewall_area=1 #延迟启动 @@ -37,14 +38,24 @@ if [ -n "$test" -o -n "$(pidof CrashCore)" ]; then } & ckcmd mtd_storage.sh && mtd_storage.sh save >/dev/null 2>&1 #Padavan保存/etc/storage #加载定时任务 - cronload | grep -v '^$' >"$TMPDIR"/cron_tmp - [ -s "$TASKCFGDIR"/cron ] && cat "$TASKCFGDIR"/cron >>"$TMPDIR"/cron_tmp - [ -s "$TASKCFGDIR"/running ] && cat "$TASKCFGDIR"/running >>"$TMPDIR"/cron_tmp - [ "$bot_tg_service" = ON ] && echo "* * * * * /bin/sh $CRASHDIR/starts/start_legacy_wd.sh bot_tg #ShellCrash-TG_BOT守护进程" >>"$TMPDIR"/cron_tmp - [ "$start_old" = ON ] && echo "* * * * * /bin/sh $CRASHDIR/starts/start_legacy_wd.sh shellcrash #ShellCrash保守模式守护进程" >>"$TMPDIR"/cron_tmp - awk '!x[$0]++' "$TMPDIR"/cron_tmp >"$TMPDIR"/cron_tmp2 #删除重复行 - cronadd "$TMPDIR"/cron_tmp2 - rm -f "$TMPDIR"/cron_tmp "$TMPDIR"/cron_tmp2 + set -- - + for cron_file in "$TASKCFGDIR/cron" "$TASKCFGDIR/running"; do + [ ! -e "$cron_file" ] || set -- "$@" "$cron_file" + done + # shellcheck disable=SC2016 + CRON_BOT="$bot_tg_service" CRON_LEGACY="$start_old" CRON_DIR="$CRASHDIR" cronupdate awk ' + NF && !seen[$0]++ { print } + END { + if (ENVIRON["CRON_BOT"] == "ON") { + line = "* * * * * /bin/sh " ENVIRON["CRON_DIR"] "/starts/start_legacy_wd.sh bot_tg #ShellCrash-TG_BOT守护进程" + if (!seen[line]++) print line + } + if (ENVIRON["CRON_LEGACY"] == "ON") { + line = "* * * * * /bin/sh " ENVIRON["CRON_DIR"] "/starts/start_legacy_wd.sh shellcrash #ShellCrash保守模式守护进程" + if (!seen[line]++) print line + } + } + ' "$@" || exit 1 #加载条件任务 [ -s "$TASKCFGDIR"/afstart ] && { . "$TASKCFGDIR"/afstart; } & [ -s "$TASKCFGDIR"/affirewall -a -s /etc/init.d/firewall -a ! -f /etc/init.d/firewall.bak ] && { diff --git a/scripts/starts/clash_modify.sh b/scripts/starts/clash_modify.sh index b67a0330..f2b8439f 100644 --- a/scripts/starts/clash_modify.sh +++ b/scripts/starts/clash_modify.sh @@ -7,7 +7,7 @@ prepare_clash_base_config() { external="external-controller: 0.0.0.0:$db_port" if [ "$redir_mod" = "Mix" -o "$redir_mod" = "Tun" ]; then [ "$crashcore" = 'meta' ] && tun_meta=', device: utun, auto-route: false, auto-detect-interface: false' - tun="tun: {enable: true, stack: system$tun_meta}" + tun="tun: {enable: true, stack: mips$tun_meta}" else tun='tun: {enable: false}' fi @@ -216,6 +216,25 @@ add_custom_inbounds_and_rules() { } } +preserve_geodata_yaml() { + #保留订阅规则依赖的Geo数据设置,本地自定义同名配置优先 + : >"$TMPDIR"/geodata_base.yaml + : >"$TMPDIR"/geodata.yaml + for char in geodata-mode geodata-loader geosite-matcher geo-auto-update geo-update-interval geox-url; do + awk -v key="$char" ' + /^[^[:space:]#]/ { emit = ($0 ~ ("^" key ":")) } + emit { print } + ' "$core_config" >>"$TMPDIR"/geodata_base.yaml + done + for char in geodata-mode geodata-loader geosite-matcher geo-auto-update geo-update-interval geox-url; do + grep -qE "^$char:" "$CRASHDIR"/yamls/user.yaml "$CRASHDIR"/yamls/others.yaml 2>/dev/null && continue + awk -v key="$char" ' + /^[^[:space:]#]/ { emit = ($0 ~ ("^" key ":")) } + emit { print } + ' "$TMPDIR"/geodata_base.yaml >>"$TMPDIR"/geodata.yaml + done +} + merger_yaml() { #mix和route模式生成rule-providers [ "$dns_mod" = "mix" ] || [ "$dns_mod" = "route" ] && ! grep -Eq '^[[:space:]]*cn:' "$TMPDIR"/rule-providers.yaml && ! grep -q '^rule-providers' "$CRASHDIR"/yamls/others.yaml 2>/dev/null && { @@ -244,8 +263,9 @@ merger_yaml() { yaml_add="$yaml_add $TMPDIR/${char}.yaml" } done + preserve_geodata_yaml #合并完整配置文件 - cut -c 1- "$TMPDIR"/set.yaml $yaml_dns $yaml_hosts $yaml_user $yaml_others $yaml_add >"$TMPDIR"/config.yaml + cut -c 1- "$TMPDIR"/set.yaml "$TMPDIR"/geodata.yaml $yaml_dns $yaml_hosts $yaml_user $yaml_others $yaml_add >"$TMPDIR"/config.yaml } test_yaml() { @@ -259,7 +279,7 @@ test_yaml() { sed -i "/#自定义策略组开始/,/#自定义策略组结束/d" "$TMPDIR"/proxy-groups.yaml mv -f "$TMPDIR"/set_bak.yaml "$TMPDIR"/set.yaml >/dev/null 2>&1 #合并基础配置文件 - cut -c 1- "$TMPDIR"/set.yaml $yaml_dns $yaml_add >"$TMPDIR"/config.yaml + cut -c 1- "$TMPDIR"/set.yaml "$TMPDIR"/geodata_base.yaml $yaml_dns $yaml_add >"$TMPDIR"/config.yaml sed -i "/#自定义/d" "$TMPDIR"/config.yaml fi } @@ -268,7 +288,7 @@ finalize_clash_yaml() { #建立软连接 [ ""$TMPDIR"" = ""$BINDIR"" ] || ln -sf "$TMPDIR"/config.yaml "$BINDIR"/config.yaml 2>/dev/null || cp -f "$TMPDIR"/config.yaml "$BINDIR"/config.yaml #清理缓存 - for char in $yaml_char set set_bak dns hosts; do + for char in $yaml_char set set_bak dns hosts geodata geodata_base; do rm -f "$TMPDIR"/${char}.yaml done } diff --git a/scripts/starts/fw_getlanip.sh b/scripts/starts/fw_getlanip.sh index 2d9e3a39..d5812e49 100644 --- a/scripts/starts/fw_getlanip.sh +++ b/scripts/starts/fw_getlanip.sh @@ -1,34 +1,171 @@ +ipv6_route_prefixes() { + awk ' + function is_global_prefix(value) { + return value ~ /^[23][0-9A-Fa-f]*:/ && value ~ /\/[0-9]+$/ + } + $1 == "default" { + for (i = 1; i < NF; i++) { + if ($i == "from" && is_global_prefix($(i + 1))) + print $(i + 1) + } + next + } + is_global_prefix($1) { + via = 0 + for (i = 1; i <= NF; i++) { + if ($i == "via") + via = 1 + } + if (!via) + print $1 + }' +} + +get_lan_ifaces() { + ip route show scope link 2>/dev/null | + grep -Ev 'default|ppp|wan|utun|iot|peer|docker|podman|virbr|vnet|ovs|vmbr|veth|vmnic|vboxnet|lxcbr|xenbr|vEthernet|wgs|multicast|anycast' | + awk '{for(i=1;i<=NF;i++) if($i=="dev") {print $(i+1); break}}' | + grep -v '^lo$' | + sort -u +} + +get_openwrt_lan_ifaces() { + if command -v ubus >/dev/null 2>&1; then + ubus -S call network.interface.lan status 2>/dev/null | + awk -F '"' '$2 == "l3_device" {print $4}' + fi + printf '%s\n' $lan_ifaces +} + +get_ipv6_prefixes_openwrt() { + ipv6_prefixes=$( + for ipv6_iface in $(get_openwrt_lan_ifaces | sort -u); do + ip -6 route show dev "$ipv6_iface" 2>/dev/null | + grep -v '^default' | + ipv6_route_prefixes + done + ) + # Some OpenWrt variants do not expose the LAN route through BusyBox ip. + # In that case, use netifd's source-specific default route as a fallback. + [ -n "$ipv6_prefixes" ] || ipv6_prefixes=$( + ip -6 route show default 2>/dev/null | ipv6_route_prefixes + ) + printf '%s\n' "$ipv6_prefixes" | sort -u +} + +get_ipv6_prefixes_linux() { + ipv6_prefixes=$( + for ipv6_iface in $lan_ifaces; do + ip -6 route show dev "$ipv6_iface" 2>/dev/null | grep -v '^default' + done | ipv6_route_prefixes | sort -u + ) + # A host may use one interface for both its uplink and LAN, or may not + # have an IPv4 LAN route. Fall back to directly connected global routes. + [ -n "$ipv6_prefixes" ] || ipv6_prefixes=$( + ip -6 route show 2>/dev/null | + grep -Ev 'unreachable|prohibit|blackhole|ppp|wan|utun|iot|peer|docker|podman|virbr|vnet|ovs|vmbr|veth|vmnic|vboxnet|lxcbr|xenbr|vEthernet|wgs|multicast|anycast' | + ipv6_route_prefixes | + sort -u + ) + printf '%s\n' "$ipv6_prefixes" +} + +get_ipv6_prefixes_router() { + # Padavan/Asuswrt and other BusyBox based firmware do not consistently + # expose route protocol metadata. Only depend on route shape and devices. + ipv6_prefixes=$( + for ipv6_iface in $lan_ifaces; do + ip -6 route show dev "$ipv6_iface" 2>/dev/null | + grep -v '^default' | + ipv6_route_prefixes + done + ) + [ -n "$ipv6_prefixes" ] || ipv6_prefixes=$( + ip -6 route show default 2>/dev/null | ipv6_route_prefixes + ) + printf '%s\n' "$ipv6_prefixes" | sort -u +} + +get_ipv6_prefixes_fallback() { + # Generic fallback for systems unknown to ShellCrash. Avoid fixed field + # positions and route protocol names, which differ across ip variants. + ip -6 route show 2>/dev/null | ipv6_route_prefixes | sort -u +} + +get_ipv6_platform() { + case "$systype" in + mi_snapshot | ng_snapshot) + echo openwrt + return + ;; + Padavan | asusrouter) + echo router + return + ;; + container) + echo linux + return + ;; + esac + + if [ -f /etc/openwrt_release ] || { [ -f /etc/rc.common ] && command -v uci >/dev/null 2>&1; }; then + echo openwrt + elif [ "$(uname -s 2>/dev/null)" = Linux ]; then + echo linux + else + echo fallback + fi +} + +get_ipv6_prefixes() { + [ -n "$lan_ifaces" ] || lan_ifaces=$(get_lan_ifaces) + case "$(get_ipv6_platform)" in + openwrt) get_ipv6_prefixes_openwrt ;; + router) get_ipv6_prefixes_router ;; + linux) get_ipv6_prefixes_linux ;; + *) get_ipv6_prefixes_fallback ;; + esac +} + +build_host_ipv6() { + host_ipv6="fe80::/10 fd00::/8 $1" + [ "$ts_service" = ON ] && host_ipv6="$host_ipv6 fd7a:115c:a1e0::/48" + if [ "$wg_service" = ON ]; then + [ -f "$CRASHDIR"/configs/gateway.cfg ] && . "$CRASHDIR"/configs/gateway.cfg + [ -n "$wg_ipv6" ] && host_ipv6="$host_ipv6 $wg_ipv6" + fi + # Only pass prefix-shaped values to firewall generators and keep the + # result stable so the watcher can compare it cheaply. + host_ipv6=$(printf '%s\n' "$host_ipv6" | + tr ' ' '\n' | + awk '/^[0-9A-Fa-f:]+\/[0-9]+$/ {print}' | + sort -u | + tr '\n' ' ' | + sed 's/ $//') +} + getlanip() { #获取局域网host地址 i=1 while [ "$i" -le "20" ]; do #ipv4局域网网段 host_ipv4=$(ip route show scope link | grep -Ev 'default|wan|utun|iot|peer|docker|podman|virbr|vnet|ovs|vmbr|veth|vmnic|vboxnet|lxcbr|xenbr|vEthernet|wgs|multicast|anycast' | awk '{print $1}') - #ipv6局域网网段 - 从IPv4已识别的LAN接口获取全局IPv6前缀 - [ "$ipv6_redir" = "ON" ] && { - lan_ifaces=$(ip route show scope link | grep -Ev 'ppp|wan|utun|iot|peer|docker|podman|virbr|vnet|ovs|vmbr|veth|vmnic|vboxnet|lxcbr|xenbr|vEthernet|wgs|multicast|anycast' | awk '{for(i=1;i<=NF;i++) if($i=="dev") {print $(i+1); break}}' | grep -v '^lo$' | sort -u) - host_ipv6=$( - for iface in $lan_ifaces; do - ip -6 addr show dev $iface 2>/dev/null - done | grep 'scope global' | awk '{print $2}' | tr '\n' ' ' | sed 's/ $//' - ) - [ -z "$host_ipv6" ] && host_ipv6=$(ip -6 route show | grep -Ev 'default|unreachable|fe80::/|wan|ppp|utun|iot|peer|docker|podman|virbr|vnet|ovs|vmbr|veth|vmnic|vboxnet|lxcbr|xenbr|vEthernet|wgs|multicast|anycast' | awk '{print $1}' | tr '\n' ' ' | sed 's/ $//') - } + #ipv6局域网网段 + if [ "$ipv6_redir" = "ON" ]; then + lan_ifaces=$(get_lan_ifaces) + detected_host_ipv6=$(get_ipv6_prefixes) + fi [ -f "$TMPDIR"/ShellCrash.log ] && break - [ -n "$host_ipv4" -a "$ipv6_redir" != "ON" ] && break - [ -n "$host_ipv4" -a -n "$host_ipv6" ] && break + [ -n "$host_ipv4" ] && [ "$ipv6_redir" != "ON" ] && break + [ -n "$host_ipv4" ] && [ -n "$detected_host_ipv6" ] && break sleep 1 && i=$((i + 1)) done #Tailscale - [ "$ts_service" = ON ] && { - ts_host_ipv4=' 100.64.0.0/10' - ts_host_ipv6=' fd7a:115c:a1e0::/48' - } + [ "$ts_service" = ON ] && ts_host_ipv4=' 100.64.0.0/10' #Wireguard - [ "$wg_service" = ON ] && { + if [ "$wg_service" = ON ]; then . "$CRASHDIR"/configs/gateway.cfg - wg_host_ipv4=' $wg_ipv4' - [ -n "$wg_ipv6" ] && wg_host_ipv6=' $wg_ipv6' - } + wg_host_ipv4=" $wg_ipv4" + fi #添加自定义ipv4局域网网段 if [ "$replace_default_host_ipv4" = "ON" ]; then host_ipv4="$cust_host_ipv4" @@ -40,7 +177,7 @@ getlanip() { #获取局域网host地址 host_ipv4='192.168.0.0/16 10.0.0.0/12 172.16.0.0/12' logger "无法获取本地LAN-IPV4网段,请前往流量过滤设置界面设置自定义网段!" 31 } - host_ipv6="fe80::/10 fd00::/8 $host_ipv6$ts_host_ipv6$wg_host_ipv6" + build_host_ipv6 "$detected_host_ipv6" #获取本机出口IP地址 local_ipv4=$(ip route 2>&1 | grep -Ev 'utun|iot|docker|linkdown' | grep -Eo 'src.*' | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | sort -u) [ -z "$local_ipv4" ] && local_ipv4=$(ip route 2>&1 | grep -Eo 'src.*' | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | sort -u) diff --git a/scripts/starts/fw_ipv6_watch.sh b/scripts/starts/fw_ipv6_watch.sh new file mode 100755 index 00000000..dd66ee92 --- /dev/null +++ b/scripts/starts/fw_ipv6_watch.sh @@ -0,0 +1,123 @@ +#!/bin/sh +# Keep the nftables LAN IPv6 prefix set in sync with dynamic prefixes. + +[ -z "$CRASHDIR" ] && CRASHDIR=$( + cd "$(dirname "$0")" + cd .. + pwd +) +. "$CRASHDIR"/libs/get_config.sh +. "$CRASHDIR"/starts/fw_getlanip.sh + +[ -z "$TMPDIR" ] && TMPDIR=/tmp/ShellCrash +ipv6_watch_pid="$TMPDIR/ipv6_watch.pid" +ipv6_watch_fifo="$TMPDIR/ipv6_route_monitor.$$" +ipv6_nft_family=${SHELLCRASH_NFT_FAMILY:-inet} +ipv6_nft_table=${SHELLCRASH_NFT_TABLE:-shellcrash} +ipv6_nft_set=${SHELLCRASH_IPV6_SET:-lan_ip6} + +stop_ipv6_watch() { + [ -s "$ipv6_watch_pid" ] || return 0 + ipv6_watch_old_pid=$(cat "$ipv6_watch_pid" 2>/dev/null) + case "$ipv6_watch_old_pid" in + '' | *[!0-9]*) ;; + *) + if [ ! -r "/proc/$ipv6_watch_old_pid/cmdline" ] || + tr '\0' ' ' <"/proc/$ipv6_watch_old_pid/cmdline" 2>/dev/null | grep -q 'fw_ipv6_watch.sh'; then + kill "$ipv6_watch_old_pid" 2>/dev/null + fi + ;; + esac + rm -f "$ipv6_watch_pid" +} + +cleanup_ipv6_watch() { + trap - EXIT INT TERM + [ -n "$ipv6_monitor_pid" ] && kill "$ipv6_monitor_pid" 2>/dev/null + [ -p "$ipv6_watch_fifo" ] && rm -f "$ipv6_watch_fifo" + if [ "$(cat "$ipv6_watch_pid" 2>/dev/null)" = "$$" ]; then + rm -f "$ipv6_watch_pid" + fi + exit 0 +} + +update_ipv6_set() { + lan_ifaces=$(get_lan_ifaces) + detected_host_ipv6=$(get_ipv6_prefixes) + build_host_ipv6 "$detected_host_ipv6" + ipv6_watch_state="$host_ipv6" + + if [ "$ipv6_watch_state" = "$ipv6_watch_last_state" ] && + nft list set "$ipv6_nft_family" "$ipv6_nft_table" "$ipv6_nft_set" >/dev/null 2>&1; then + return 0 + fi + + ipv6_watch_elements=$(echo "$host_ipv6" | sed 's/[[:space:]]\+/, /g') + [ -n "$ipv6_watch_elements" ] || return 1 + nft list set "$ipv6_nft_family" "$ipv6_nft_table" "$ipv6_nft_set" >/dev/null 2>&1 || return 1 + + # nft processes a batch atomically, so packets never observe a partially + # updated set while an ISP prefix is being replaced. + printf 'flush set %s %s %s\nadd element %s %s %s { %s }\n' \ + "$ipv6_nft_family" "$ipv6_nft_table" "$ipv6_nft_set" \ + "$ipv6_nft_family" "$ipv6_nft_table" "$ipv6_nft_set" "$ipv6_watch_elements" | + nft -f - >/dev/null 2>&1 || return 1 + ipv6_watch_last_state="$ipv6_watch_state" +} + +run_iproute2_monitor() { + rm -f "$ipv6_watch_fifo" + mkfifo "$ipv6_watch_fifo" 2>/dev/null || return 1 + ip -6 monitor route >"$ipv6_watch_fifo" 2>/dev/null & + ipv6_monitor_pid=$! + while IFS= read -r _ipv6_route_event; do + # Coalesce delete/add notifications emitted during renumbering. + sleep 1 + update_ipv6_set + done <"$ipv6_watch_fifo" + wait "$ipv6_monitor_pid" 2>/dev/null + ipv6_monitor_pid= + rm -f "$ipv6_watch_fifo" + return 1 +} + +run_ipv6_watch() { + mkdir -p "$TMPDIR" + echo $$ >"$ipv6_watch_pid" + trap cleanup_ipv6_watch EXIT INT TERM + ipv6_watch_last_state= + update_ipv6_set + + # Full iproute2 has event monitoring. BusyBox/router variants that do not + # support it use a small polling fallback instead. + if ip -Version 2>&1 | grep -qi 'iproute2'; then + run_iproute2_monitor + fi + while :; do + sleep 30 + update_ipv6_set + done +} + +case "$1" in +start) + mkdir -p "$TMPDIR" + stop_ipv6_watch + /bin/sh "$0" run >/dev/null 2>&1 & + echo $! >"$ipv6_watch_pid" + ;; +stop) + stop_ipv6_watch + ;; +run) + run_ipv6_watch + ;; +once) + ipv6_watch_last_state= + update_ipv6_set + ;; +*) + echo "Usage: $0 {start|stop|run|once}" >&2 + exit 1 + ;; +esac diff --git a/scripts/starts/fw_nftables.sh b/scripts/starts/fw_nftables.sh index c1d5e2c9..66cd831e 100644 --- a/scripts/starts/fw_nftables.sh +++ b/scripts/starts/fw_nftables.sh @@ -2,13 +2,22 @@ # Copyright (C) Juewuy RESERVED_IP=$(echo $reserve_ipv4 | sed 's/[[:space:]]\+/, /g') -RESERVED_IP6=$(echo "$reserve_ipv6 $host_ipv6" | sed 's/[[:space:]]\+/, /g') +RESERVED_IP6=$(echo "$reserve_ipv6" | sed 's/[[:space:]]\+/, /g') + +start_nft_ipv6_set() { + HOST_IP6=$(echo "$host_ipv6" | sed 's/[[:space:]]\+/, /g') + nft list set inet shellcrash lan_ip6 >/dev/null 2>&1 || + nft add set inet shellcrash lan_ip6 { type ipv6_addr \; flags interval \; auto-merge \; } + nft flush set inet shellcrash lan_ip6 2>/dev/null + [ -n "$HOST_IP6" ] && nft add element inet shellcrash lan_ip6 { $HOST_IP6 } +} add_ip6_route(){ - #过滤保留地址及本机地址 + #过滤保留地址及当前局域网地址 nft add rule inet shellcrash $1 ip6 daddr {$RESERVED_IP6} return - #仅代理本机局域网网段流量 - nft add rule inet shellcrash $1 ip6 saddr != {$HOST_IP6} return + nft add rule inet shellcrash $1 ip6 daddr @lan_ip6 return + #OUTPUT只包含本机流量,无需依赖动态公网源前缀 + [ "$1" = 'prerouting' ] && nft add rule inet shellcrash $1 ip6 saddr != @lan_ip6 return #绕过CN_IPV6 [ "$dns_mod" != "fake-ip" -a "$cn_ip_route" = "ON" -a -f "$BINDIR"/cn_ipv6.txt ] && { CN_IP6=$(awk '{printf "%s, ",$1}' "$BINDIR"/cn_ipv6.txt) @@ -83,10 +92,8 @@ start_nft_route() { #nftables-route通用工具 } #局域网ipv6支持 if [ "$ipv6_redir" = "ON" -a "$1" = 'prerouting' -a "$firewall_area" != 5 ]; then - HOST_IP6=$(echo $host_ipv6 | sed 's/[[:space:]]\+/, /g') add_ip6_route "$1" elif [ "$ipv6_redir" = "ON" -a "$1" = 'output' -a \( "$firewall_area" = 2 -o "$firewall_area" = 3 \) ]; then - HOST_IP6="::1, $(echo $host_ipv6 | sed 's/[[:space:]]\+/, /g')" add_ip6_route "$1" else nft add rule inet shellcrash $1 meta nfproto ipv6 return @@ -106,7 +113,6 @@ start_nft_route() { #nftables-route通用工具 start_nft_dns() { #nftables-dns [ "$1" = 'prerouting' ] && { HOST_IP=$(echo $host_ipv4 | sed 's/[[:space:]]\+/, /g') - HOST_IP6=$(echo $host_ipv6 | sed 's/[[:space:]]\+/, /g') } [ "$1" = 'output' ] && HOST_IP="127.0.0.0/8, $(echo $local_ipv4 | sed 's/[[:space:]]\+/, /g')" [ "$1" = 'prerouting_vm' ] && HOST_IP="$(echo $vm_ipv4 | sed 's/[[:space:]]\+/, /g')" @@ -119,7 +125,7 @@ start_nft_dns() { #nftables-dns nft add rule inet shellcrash "$1"_dns meta skgid { 453, 7890 } return [ "$firewall_area" = 5 ] && nft add rule inet shellcrash "$1"_dns ip saddr $bypass_host return nft add rule inet shellcrash "$1"_dns ip saddr != {$HOST_IP} return #屏蔽外部请求 - [ "$1" = 'prerouting' ] && nft add rule inet shellcrash "$1"_dns ip6 saddr != {$HOST_IP6} return #屏蔽外部请求 + [ "$1" = 'prerouting' ] && nft add rule inet shellcrash "$1"_dns ip6 saddr != @lan_ip6 return #屏蔽外部请求 #过滤局域网设备 [ "$1" = 'prerouting' ] && [ -s "$CRASHDIR"/configs/mac ] && { MAC=$(awk '{printf "%s, ",$1}' "$CRASHDIR"/configs/mac) @@ -134,7 +140,6 @@ start_nft_dns() { #nftables-dns } start_nft_wan() { #nftables公网防火墙 HOST_IP=$(echo $host_ipv4 | sed 's/[[:space:]]\+/, /g') - HOST_IP6=$(echo $host_ipv6 | sed 's/[[:space:]]\+/, /g') nft add chain inet shellcrash input { type filter hook input priority -100 \; } nft add rule inet shellcrash input iif lo accept #本机请求全放行 #端口放行 @@ -148,7 +153,7 @@ start_nft_wan() { #nftables公网防火墙 #端口拦截 reject_ports="{ $mix_port, $db_port }" nft add rule inet shellcrash input ip saddr {$HOST_IP} accept - nft add rule inet shellcrash input ip6 saddr {$HOST_IP6} accept + nft add rule inet shellcrash input ip6 saddr @lan_ip6 accept nft add rule inet shellcrash input tcp dport $reject_ports reject nft add rule inet shellcrash input udp dport $reject_ports reject #fw4特殊处理 @@ -160,6 +165,7 @@ start_nftables() { #nftables配置总入口 #初始化nftables nft add table inet shellcrash 2>/dev/null nft flush table inet shellcrash 2>/dev/null + start_nft_ipv6_set #公网访问防火墙 [ "$fw_wan" != OFF ] && [ "$systype" != 'container' ] && start_nft_wan #启动DNS劫持 diff --git a/scripts/starts/fw_start.sh b/scripts/starts/fw_start.sh index 6a6927ab..c81a9c05 100644 --- a/scripts/starts/fw_start.sh +++ b/scripts/starts/fw_start.sh @@ -1,6 +1,8 @@ #!/bin/sh # Copyright (C) Juewuy +#停止旧的IPv6前缀监听,避免防火墙重建期间并发更新 +[ -f "$CRASHDIR"/starts/fw_ipv6_watch.sh ] && /bin/sh "$CRASHDIR"/starts/fw_ipv6_watch.sh stop #获取局域网host地址 . "$CRASHDIR"/starts/fw_getlanip.sh && getlanip #缺省值 @@ -46,6 +48,9 @@ #防火墙配置 [ "$firewall_mod" = 'iptables' ] && . "$CRASHDIR"/starts/fw_iptables.sh && start_iptables [ "$firewall_mod" = 'nftables' ] && . "$CRASHDIR"/starts/fw_nftables.sh && start_nftables +#动态更新nftables中的IPv6局域网前缀集合 +[ "$firewall_mod" = 'nftables' -a "$ipv6_redir" = "ON" ] && + /bin/sh "$CRASHDIR"/starts/fw_ipv6_watch.sh start #修复部分虚拟机dns查询失败的问题 [ "$firewall_area" = 2 -o "$firewall_area" = 3 ] && [ -z "$(grep '127.0.0.1' /etc/resolv.conf 2>/dev/null)" ] && [ "$systype" != 'container' ] && { line=$(grep -n 'nameserver' /etc/resolv.conf | awk -F: 'FNR==1{print $1}') diff --git a/scripts/starts/fw_stop.sh b/scripts/starts/fw_stop.sh index 2a5d99c6..354f138d 100644 --- a/scripts/starts/fw_stop.sh +++ b/scripts/starts/fw_stop.sh @@ -5,6 +5,8 @@ [ -z "$CRASHDIR" ] && CRASHDIR=$( cd $(dirname $0);cd ..;pwd) . "$CRASHDIR"/libs/get_config.sh . "$CRASHDIR"/libs/check_cmd.sh +#停止IPv6前缀监听 +[ -f "$CRASHDIR"/starts/fw_ipv6_watch.sh ] && /bin/sh "$CRASHDIR"/starts/fw_ipv6_watch.sh stop . "$CRASHDIR"/starts/fw_getlanip.sh && getlanip #获取局域网host地址 #缺省值 [ -z "$common_ports" ] && common_ports='ON' @@ -172,4 +174,3 @@ ckcmd nft && nft delete table inet shellcrash >/dev/null 2>&1 [ -s /etc/init.d/firewall.bak ] && mv -f /etc/init.d/firewall.bak /etc/init.d/firewall #others sed -i '/shellcrash-dns-repair/d' /etc/resolv.conf 2>/dev/null - diff --git a/scripts/starts/snapshot_init.sh b/scripts/starts/snapshot_init.sh index 9445b7cf..514c6aa6 100644 --- a/scripts/starts/snapshot_init.sh +++ b/scripts/starts/snapshot_init.sh @@ -4,7 +4,6 @@ CRASHDIR="$(uci get firewall.ShellCrash.path | sed 's/\/starts.*//')" #防止提前自启 /etc/init.d/shellcrash disable -crontab -l | grep -v 'start_legacy_wd.sh shellcrash' | crontab - #防止usb未加载 i=0 while [ ! -f "$CRASHDIR/configs/ShellCrash.cfg" ]; do @@ -13,6 +12,8 @@ while [ ! -f "$CRASHDIR/configs/ShellCrash.cfg" ]; do sleep 3 done . "$CRASHDIR"/configs/ShellCrash.cfg +. "$CRASHDIR"/libs/set_cron.sh +cronset 'start_legacy_wd.sh shellcrash' || exit 1 autoSSH() { #自动开启SSH diff --git a/tests/clash_geodata.sh b/tests/clash_geodata.sh new file mode 100644 index 00000000..98ddda98 --- /dev/null +++ b/tests/clash_geodata.sh @@ -0,0 +1,93 @@ +#!/bin/sh +# Run with sh tests/clash_geodata.sh [path/to/clash_modify.sh]. +set -e +script=${1:-$(dirname "$0")/../scripts/starts/clash_modify.sh} +. "$script" +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT HUP INT TERM + +setup() { + CRASHDIR="$test_dir/$1" + TMPDIR="$CRASHDIR/tmp" + BINDIR="$CRASHDIR/bin" + mkdir -p "$CRASHDIR/yamls" "$TMPDIR" "$BINDIR" + core_config="$CRASHDIR/yamls/config.yaml" + dns_mod=redir_host + yaml_char='rules proxy-groups' + yaml_user= yaml_others= yaml_dns= yaml_hosts= + printf 'mode: Rule\n' >"$TMPDIR/set.yaml" + printf ' - GEOSITE,GOOGLE,DIRECT\n' >"$TMPDIR/rules.yaml" + : >"$TMPDIR/proxy-groups.yaml" + cat >"$core_config" <<'EOF' +geodata-mode: true +geodata-loader: memconservative +geosite-matcher: succinct +geo-auto-update: true +geo-update-interval: 24 +geox-url: + geoip: https://example.invalid/geoip.dat + geosite: >- + https://example.invalid/geosite.dat + mmdb: https://example.invalid/Country.mmdb +rules: + - GEOSITE,GOOGLE,DIRECT +mixed-port: 1234 +EOF +} + +assert_line() { + grep -qFx -- "$1" "$TMPDIR/config.yaml" || { + echo "Missing expected line: $1" >&2 + exit 1 + } +} + +setup inherited +merger_yaml +assert_line 'geodata-mode: true' +assert_line 'geodata-loader: memconservative' +assert_line 'geosite-matcher: succinct' +assert_line 'geo-auto-update: true' +assert_line 'geo-update-interval: 24' +assert_line ' https://example.invalid/geosite.dat' +[ "$(grep -c '^rules:' "$TMPDIR/config.yaml")" = 1 ] +! grep -q '^mixed-port: 1234' "$TMPDIR/config.yaml" +finalize_clash_yaml +[ ! -e "$TMPDIR/geodata.yaml" ] +[ ! -e "$TMPDIR/geodata_base.yaml" ] + +setup overrides +printf 'geodata-mode: false\n' >"$CRASHDIR/yamls/user.yaml" +printf 'geox-url: {geosite: https://override.invalid/geosite.dat}\n' >"$CRASHDIR/yamls/others.yaml" +merger_yaml +assert_line 'geodata-mode: false' +assert_line 'geox-url: {geosite: https://override.invalid/geosite.dat}' +[ "$(grep -c '^geodata-mode:' "$TMPDIR/config.yaml")" = 1 ] +[ "$(grep -c '^geox-url:' "$TMPDIR/config.yaml")" = 1 ] +! grep -q 'example.invalid' "$TMPDIR/config.yaml" + +# A rejected custom configuration must fall back to the subscription settings. +printf '#!/bin/sh\nexit 1\n' >"$TMPDIR/CrashCore" +chmod +x "$TMPDIR/CrashCore" +logger() { :; } +set +e +test_yaml +set -e +assert_line 'geodata-mode: true' +assert_line ' https://example.invalid/geosite.dat' +[ "$(grep -c '^geox-url:' "$TMPDIR/config.yaml")" = 1 ] +! grep -q 'override.invalid' "$TMPDIR/config.yaml" + +setup inline +printf 'geox-url: {geosite: https://inline.invalid/geosite.dat}\ngeo-auto-update: false\n---\nmixed-port: 1234\n' >"$core_config" +merger_yaml +assert_line 'geox-url: {geosite: https://inline.invalid/geosite.dat}' +assert_line 'geo-auto-update: false' +! grep -qE '^---|^mixed-port: 1234' "$TMPDIR/config.yaml" + +setup absent +printf 'rules:\n - MATCH,DIRECT\n' >"$core_config" +merger_yaml +! grep -qE '^geo(data|site|x|-)' "$TMPDIR/config.yaml" +assert_line 'mode: Rule' +echo 'PASS: inherited geodata, overrides, fallback, inline maps, cleanup and absent settings' diff --git a/tests/test_fw_ipv6_prefix.sh b/tests/test_fw_ipv6_prefix.sh new file mode 100755 index 00000000..63e7982f --- /dev/null +++ b/tests/test_fw_ipv6_prefix.sh @@ -0,0 +1,83 @@ +#!/bin/sh +set -eu + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +. "$ROOT/scripts/starts/fw_getlanip.sh" + +assert_equal() { + if [ "$1" != "$2" ]; then + echo "expected: [$1]" >&2 + echo "actual: [$2]" >&2 + exit 1 + fi +} + +route_output='default via fe80::1 dev eth0 +default from 240e:1234:5600::/56 via fe80::1 dev pppoe-wan +2408:8266:501:2443::/64 dev enp2s0 proto kernel +2606:4700::/48 via fe80::2 dev eth0 +unreachable 240e:1234:5600::/48 dev lo' +actual=$(printf '%s\n' "$route_output" | ipv6_route_prefixes | sort -u) +expected='2408:8266:501:2443::/64 +240e:1234:5600::/56' +assert_equal "$expected" "$actual" + +# OpenWrt prefers the LAN assignment over the wider delegated prefix. +systype=mi_snapshot +lan_ifaces='br-lan' +ubus() { + printf '%s\n' '{ "l3_device": "br-lan" }' +} +ip() { + case "$*" in + '-6 route show dev br-lan') + echo 'default from 240e:1234:5600::/56 via fe80::1 dev br-lan' + echo '240e:1234:5600:1::/64 dev br-lan proto static' + ;; + '-6 route show default') echo 'default from 240e:1234:5600::/56 via fe80::1 dev pppoe-wan' ;; + esac +} +actual=$(get_ipv6_prefixes) +assert_equal '240e:1234:5600:1::/64' "$actual" + +# OpenWrt falls back to source-specific routes when its LAN route is hidden. +lan_ifaces= +ubus() { + return 0 +} +ip() { + case "$*" in + '-6 route show default') echo 'default from 240e:1234:5600::/56 via fe80::1 dev pppoe-wan' ;; + esac +} +actual=$(get_ipv6_prefixes) +assert_equal '240e:1234:5600::/56' "$actual" + +# Standard Linux uses the directly connected route on the discovered LAN. +systype=container +lan_ifaces='enp2s0' +ip() { + case "$*" in + '-6 route show dev enp2s0') echo '2408:8266:501:2443::/64 dev enp2s0 proto kernel metric 256' ;; + esac +} +actual=$(get_ipv6_prefixes) +assert_equal '2408:8266:501:2443::/64' "$actual" + +# BusyBox based router firmware does not need route protocol metadata. +systype=Padavan +lan_ifaces='br0' +ip() { + case "$*" in + '-6 route show dev br0') echo '2001:db8:abcd:1::/64 dev br0 metric 256' ;; + esac +} +actual=$(get_ipv6_prefixes) +assert_equal '2001:db8:abcd:1::/64' "$actual" + +ts_service=ON +wg_service=OFF +build_host_ipv6 '2408:8266:501:2443::/64 invalid-value' +assert_equal '2408:8266:501:2443::/64 fd00::/8 fd7a:115c:a1e0::/48 fe80::/10' "$host_ipv6" + +echo 'IPv6 prefix tests passed' diff --git a/tests/test_fw_nftables_ipv6.sh b/tests/test_fw_nftables_ipv6.sh new file mode 100755 index 00000000..4da01b46 --- /dev/null +++ b/tests/test_fw_nftables_ipv6.sh @@ -0,0 +1,78 @@ +#!/bin/sh +set -eu + +ROOT=$(cd "$(dirname "$0")/.." && pwd) + +reserve_ipv4='0.0.0.0/8 127.0.0.0/8' +reserve_ipv6='::/128 ::1/128 fe80::/10 ff00::/8' +host_ipv4='192.168.1.0/24' +local_ipv4='192.168.1.1' +host_ipv6='2001:db8:1::/64 fd00::/8 fe80::/10' +common_ports=OFF +ports= +PORTS= +mix_port=7890 +redir_port=7892 +tproxy_port=7893 +dns_redir_port=1053 +routing_mark=7894 +fwmark=7892 +firewall_area=3 +macfilter_type='黑名单' +cn_ip_route=OFF +dns_mod=route +ipv6_redir=ON +quic_rj=OFF +lan_proxy=true +local_proxy=true +redir_mod=Tproxy +tun_statu=false +vm_redir=OFF +fw_wan=ON +fw_wan_ports= +vms_port= +sss_port= +db_port=9090 +systype=linux +CRASHDIR=/nonexistent +BINDIR=/nonexistent + +nft() { + if [ "$1" = list ] && [ "$2" = set ]; then + return 1 + fi + printf '%s' "$1" + shift + printf ' %s' "$@" + printf '\n' +} + +modprobe() { + return 0 +} + +. "$ROOT/scripts/starts/fw_nftables.sh" +rules=$(start_nftables || :) + +require_rule() { + if ! printf '%s\n' "$rules" | grep -Fq "$1"; then + echo "missing generated rule: $1" >&2 + exit 1 + fi +} + +require_rule 'add set inet shellcrash lan_ip6' +require_rule 'flags interval ; auto-merge' +require_rule 'add element inet shellcrash lan_ip6' +require_rule 'add rule inet shellcrash prerouting ip6 daddr @lan_ip6 return' +require_rule 'add rule inet shellcrash prerouting ip6 saddr != @lan_ip6 return' +require_rule 'add rule inet shellcrash prerouting_dns ip6 saddr != @lan_ip6 return' +require_rule 'add rule inet shellcrash input ip6 saddr @lan_ip6 accept' +require_rule 'add rule inet shellcrash output ip6 daddr @lan_ip6 return' + +if printf '%s\n' "$rules" | grep -Fq 'add rule inet shellcrash output ip6 saddr'; then + echo 'output must not depend on a dynamic IPv6 source prefix' >&2 + exit 1 +fi + +echo 'nftables IPv6 rule generation tests passed'