背景

不知道大家管理 Linux 服务器时有没有开启防火墙的习惯,出于安全性考虑我一般会配置防火墙只开放公开的服务端口入站访问,其余端口一律拒绝访问,这样做可以避免其它服务有漏洞被破解。

前阵子遇到一个难题,我管理数据库用的是 phpMyAdmin,我希望我的 phpMyAdmin 仅允许我的电脑访问;将 phpMyAdmin 的监听端口设定为非 80/443 端口,配置 UFW 防火墙规则仅允许指定 IP 可以访问该端口可以满足我的需求。可是我用的是家庭宽带,没有固定 IP 地址,一旦客户端的出口 IP 发生变化原有的防火墙规则将不再适用。

解决方案

为了解决这个问题,我写了一个脚本实现客户端获取出口 IP 地址并同步修改服务器 UFW 防火墙的规则,这样一来如果客户端的出口 IP 发生变化无法访问时,只需要运行一下脚本就可以了。

这个方案的实施分为两部分

客户端批处理的作用:用于获取当前出口 IP 地址,获取后保存 IP 地址为文件并通过 SCP 传输到服务器指定的目录,然后通过 SSH 运行预置在服务器的脚本。

服务器端 Shell 脚本的作用:运行后会检查防火墙特定端口的规则,读取客户端上传的 IP 地址文件,将文件中的 IP 地址与防火墙规则中允许访问的 IP 地址进行比对,如果最新的客户端 IP 地址与防火墙规则中的 IP 地址不一致则会更新防火墙规则,反之会跳过更新防火墙规则的操作。

对了,这个方案适用于使用 UFW 防火墙的 Linux 服务器系统和 Windows 10+ 客户端系统,如果你使用其它工具管理服务器,需要对脚本中的部分命令进行修改。我使用的是 Debian 系统、root 用户。

这个方案同样适用于没有公网 IP 地址的联通、移动宽带,因为这个脚本获取的是出口 IP 地址。脚本中用于修改防火墙规则的命令支持 IPv4/IPv6,具体可以看脚本内容。

有一个需要注意的地方:客户端批处理需要使用 RSA 密钥连接 SSH 服务器,因此你需要先准备好一个可用的 SSH RSA 密钥,如何配置 RSA 密钥登录 SSH 可以看这篇文章

服务器端的操作

1.创建脚本存储目录

mkdir -p ~/UFW_update_script

2.创建 Shell 脚本文件

nano ~/UFW_update_script/update_client_ip.sh

写入以下代码:

#!/bin/bash

# 定义需要使用脚本更新防火墙规则的端口
port_1=8888

# 获取当前脚本所在目录的绝对路径
script_dir=$(dirname $(readlink -f $0))
# 从存储了客户端最新IP地址的文件中读取数据并存储在变量 client_ip 中。
client_ip=$(cat "$script_dir/client_ip.txt")

# 使用 ufw status 命令获取当前防火墙规则,筛选针对特定端口的允许入站规则,然后在输出的规则列表提取文本数据中第 6 列内容(IP地址),并存储为 ufw_ip 变量。
ufw_ip=$(ufw status numbered | grep $port_1 | grep "ALLOW IN" | awk '{print $6}') 

# 检查 ufw_ip 是否为空,用于检查是否存在旧的允许特定IP的规则
if [ -n "$ufw_ip" ]; then
    # 如果UFW规则中原有的IP和文件中的IP不一样,则删除旧规则并添加新规则
    if [ "$ufw_ip" != "$client_ip" ]; then
        # 删除旧规则
        ufw delete allow from $ufw_ip to any port $port_1
        # 添加新规则
        ufw allow from $client_ip to any port $port_1
    fi
# 如果不存在旧的允许特定IP的规则,则直接添加新规则
else
    # 添加新规则
    ufw allow from $client_ip to any port $port_1
fi

以上代码一般只需要修改防火墙规则的端口,也就是这部分代码:

# 定义需要使用脚本更新防火墙规则的端口
port_1=8888

3.删除防火墙规则

如果当前防火墙规则对所有 IP 进行开放,需要先删除原规则。

ufw delete allow 8888

客户端部分的操作

1.编写批处理文件

根据说明设置环境变量参数,保存文件名称为 UFW_update_Tool.cmd

@echo off
rem 开始一个局部化环境,变量的更改将在脚本结束时被撤消,启用变量延迟扩展。
setlocal enabledelayedexpansion && echo.

rem 本脚本依赖 curl,Win10、Win11 自带 curl,Win 7 系统使用本脚本需要自行下载 curl.exe 配合使用。curl 下载地址:https://curl.se/windows/
rem 通过 SCP 上传文件以及通过 SSH 连接服务器执行命令需要使用密钥文件才能实现免密码认证,请事先准备好 RSA 密钥。建议将 RSA 密钥保存在用户主目录 C:\Users\username\目录下,避免出现权限问题。

rem 设置变量-远程服务器域名或IP地址
set "remote_server=123.123.123.123"
rem 设置变量-远程服务器 SSH 用户名称
set "remote_user=root"
rem 设置变量-远程服务器 SSH 用户认证的密钥文件路径
set "remote_key=C:\Users\Hansen\SSH_Key\Debian.key"
rem 设置变量-定义 update.sh 脚本的所在目录(是目录路径不是文件路径,以 / 符号结尾。
set "remote_path=/root/UFW_update_script/"
rem 设置变量-客户端计划通过 IPv4 或 IPv6 访问服务器的设定(参数:4/6)
set "ip_ver=4"

rem =======不要随意修改以下代码=======
rem 输出正在检查网络的提示。
echo Checking network status && echo.
rem 发送一个 ping 请求以测试与指定主机的连接,并且将命令的输出信息重定向到空设备。(不显示任何输出)
ping www.baidu.com -n 1 > nul
rem 判断错误代码是否大于或等于 1(如果 ping 命令成功,并且计算机能够与百度网站建立连接,那么退出代码通常为 0;否则,当无法连接时,退出代码会大于等于 1。)
if errorlevel 1 (
    rem 错误代码大于或等于 1 时,输出网络未连接的提示。
    echo Not connected to the internet, Please check the network and try again. && echo.
) else (
    if "%ip_ver%"=="4" (
        rem 输出获取客户端 IPv4 地址的提示。
        echo Getting the client IPv4 address. && echo.
        rem 通过 curl 获取公网 IPv4 地址并存储在 current_ip 变量中,用于记录最新获取到的公网IP地址。
        for /f "usebackq delims=" %%i in (`curl.exe -s -L https://ip.3322.net`) do set "current_ip=%%i"
    ) else if "%ip_ver%"=="6" (
        rem 输出获取客户端 IPv6 地址的提示。
        echo Getting the client IPv6 address. && echo.
        rem 通过 curl 获取公网 IPv6 地址并存储在 current_ip 变量中,用于记录最新获取到的公网IP地址。
        for /f "usebackq delims=" %%i in (`curl.exe https://speed.neu6.edu.cn/getIP.php`) do set "current_ip=%%i"
    ) else (
        rem 输出 ip_ver 变量配置错误的提示。
        echo The ip_ver variable is incorrectly set, Please check the variable settings and try again. && echo.
        rem 结束局部化环境,撤消所有本地更改的变量。
        endlocal
        rem 按任意键退出脚本
        pause
        rem 结束运行脚本
        exit
    )
    rem 延迟读取 current_ip 变量,将最新获取到的公网IP地址写入到临时目录的 client_ip.txt 文件中。
    <nul set /p "= !current_ip!" > "%TEMP%\client_ip.txt"
    rem 输出传输 client_ip.txt 到服务器的提示。
    echo Transfer client_ip.txt to the server. && echo.
    rem 使用 scp 命令将 client_ip.txt 文件上传到远程服务器的指定路径。
    scp -i %remote_key% %TEMP%\client_ip.txt %remote_user%@%remote_server%:%remote_path%client_ip.txt && echo.
    rem 输出执行服务器 Shell 脚本的提示。
    echo Execute the server Shell script to update UFW rules. && echo.
    rem 通过 SSH 执行服务器的脚本文件,更新防火墙规则。
    ssh -i %remote_key% %remote_user%@%remote_server% "bash %remote_path%update.sh" && echo.
)
rem 清理临时文件
del /Q %TEMP%\client_ip.txt
rem 结束局部化环境,撤消所有本地更改的变量。
endlocal
rem 输出任务完成的提示。
echo Completed && echo.
rem 按任意键退出脚本
pause

2.客户端执行批处理

首次连接的服务器需要输入 yes 确认,如果公网IP发生变化导致规则不适用,再次执行一次批处理即可。

Last modification:December 31, 2023
If you think my article is useful to you, please feel free to appreciate