diff --git a/scripts/gateway.sh.j2 b/scripts/gateway.sh.j2 index 4f04c5a..4d1e637 100644 --- a/scripts/gateway.sh.j2 +++ b/scripts/gateway.sh.j2 @@ -74,6 +74,14 @@ install_package() { # 0. 前置检查与环境初始化 # ============================================================================== # check_root +# 释放53端口 + +sudo systemctl stop systemd-resolved +sudo systemctl disable systemd-resolved +sudo rm /etc/resolv.conf +# 创建新的 resolv.conf 文件,将本机地址作为唯一的 DNS 服务器 +echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf > /dev/null + log_info "开始网关主机配置脚本..." log_info "更新系统软件包列表..." @@ -151,7 +159,6 @@ max-lease-time ${DHCP_LEASE_TIME}; authoritative; # A slightly different configuration for an internal subnet. -{% set net_base = gateway_lan_ip.split('.') | slice(3) | join('.') %} subnet {{ net_base }}.0 netmask 255.255.255.0 { range ${DHCP_START_IP} ${DHCP_END_IP}; option routers ${GATEWAY_LAN_IP}; @@ -211,18 +218,20 @@ fi log_info "SSH SOCKS5 代理已通过 Systemd 启动,并在 127.0.0.1:${LOCAL_SOCKS5_PORT} 监听,支持自动重连。" # ============================================================================== -# 4. 配置 redsocks (透明 SOCKS5 代理) (保持不变) +# 4. 配置 redsocks (透明 SOCKS5 代理) (请替换为以下修正代码!) # ============================================================================== log_info "配置 redsocks 透明代理..." # 备份现有配置 sudo cp /etc/redsocks.conf /etc/redsocks.conf.bak_$(date +%Y%m%d%H%M%S) || log_warn "备份 redsocks.conf 失败。" +# ---------------------------------------------------------------------------------- +# ❗❗ 关键修正:移除所有注释和分号,以避免 redsocks 配置文件解析错误 ❗❗ cat < /dev/null base { log_debug = off; log_info = on; log = "syslog:daemon"; - daemon = on; # 后台运行 + daemon = on; redirector = iptables; } @@ -230,11 +239,12 @@ redsocks { local_ip = 127.0.0.1; local_port = ${REDSOCKS_PORT}; - ip = 127.0.0.1; # SSH SOCKS5 代理的地址 - port = ${LOCAL_SOCKS5_PORT}; # SSH SOCKS5 代理的端口 - type = socks5; # SOCKS5 代理 + ip = 127.0.0.1; + port = ${LOCAL_SOCKS5_PORT}; + type = socks5; } EOF +# ---------------------------------------------------------------------------------- sudo systemctl restart redsocks || log_error "重启 redsocks 服务失败。" sudo systemctl enable redsocks || log_error "启用 redsocks 服务失败。" @@ -263,6 +273,7 @@ server={{ domestic_dns2 }} # 国外域名通过 gfwlist2new 生成的配置文件转发到代理 # dnsmasq will read additional configuration files from /etc/dnsmasq.d conf-dir=/etc/dnsmasq.d +user=root EOF # 注意:DHCP 服务器功能由 isc-dhcp-server 提供,dnsmasq 不再提供 DHCP。 @@ -275,7 +286,7 @@ sudo systemctl enable dnsmasq || log_error "启用 dnsmasq 服务失败。" log_info "dnsmasq 智能 DNS 转发已初步配置。" # ============================================================================== -# 6. 安装和配置 gfwlist2new (保持不变) +# 6. 安装和配置 gfwlist2new (已修正 crontab 参数和 PySocks 依赖) # ============================================================================== log_info "集成和配置 GFW 规则生成器 (gfw_rules_generator.py)..." LOCAL_SCRIPT_NAME="gfw_rules_generator.py" @@ -284,15 +295,12 @@ if [ ! -d "${GFWLIST2NEW_DIR}" ]; then log_info "创建脚本目录..." sudo mkdir -p "${GFWLIST2NEW_DIR}" || log_error "创建目录 ${GFWLIST2NEW_DIR} 失败。" fi -cp $LOCAL_SCRIPT_NAME ${GFWLIST2NEW_DIR} +sudo cp $LOCAL_SCRIPT_NAME ${GFWLIST2NEW_DIR} -# 确保安装 requests 依赖 -log_info "安装 Python 依赖:requests..." -sudo pip3 install requests || log_error "安装 requests 依赖失败。" +# 确保安装 requests 和 PySocks 依赖 +log_info "安装 Python 依赖:requests 和 PySocks..." +sudo pip3 install requests PySocks || log_error "安装 requests/PySocks 依赖失败。" -# 写入新的 Python 脚本(假设您已将上面的内容保存到临时文件并复制到这里) -# 推荐您手动将上面的 Python 代码保存到 ${GFWLIST2NEW_DIR}/${LOCAL_SCRIPT_NAME} -# 如果要用脚本自动写入,代码会非常长,这里跳过写入步骤,假设文件已存在。 log_info "请确保 gfw_rules_generator.py 文件已存在于 ${GFWLIST2NEW_DIR}/" cd "${GFWLIST2NEW_DIR}" @@ -301,14 +309,18 @@ cd "${GFWLIST2NEW_DIR}" SOCKS5_SERVER_ADDR="127.0.0.1:${LOCAL_SOCKS5_PORT}" log_info "执行 GFW 规则生成脚本..." -# 使用 -p, -f, -d 参数执行新脚本 -sudo python3 "${LOCAL_SCRIPT_NAME}" -p "${SOCKS5_SERVER_ADDR}" -f "{{ foreign_dns1 }} {{foreign_dns2 }}" -d "{{ domestic_dns1 }} {{ domestic_dns2 }}" || log_error "规则生成脚本运行失败。" +# 修正后的参数传递方式 (注意双引号和变量组) +sudo python3 "${LOCAL_SCRIPT_NAME}" \ + -p "${SOCKS5_SERVER_ADDR}" \ + -f {{ foreign_dns1 }} {{ foreign_dns2 }} \ + -d {{ domestic_dns1 }} {{ domestic_dns2 }} \ + || log_error "规则生成脚本运行失败。" sudo systemctl restart dnsmasq || log_error "重启 dnsmasq (gfwlist) 失败。" log_info "设置 GFW 规则定时更新任务 (每天凌晨 3:00)..." -# 设置定时任务 -(sudo crontab -l 2>/dev/null; echo "0 3 * * * cd ${GFWLIST2NEW_DIR} && sudo python3 ${LOCAL_SCRIPT_NAME} -p ${SOCKS5_SERVER_ADDR} -f "{{ foreign_dns1 }} {{ foreign_dns2 }}" -d "{{ domestic_dns1 }} {{domestic_dns1}}" && sudo systemctl restart dnsmasq") | sudo crontab - +# 设置定时任务(已修正国内 DNS 变量) +(sudo crontab -l 2>/dev/null; echo "0 3 * * * cd ${GFWLIST2NEW_DIR} && sudo python3 ${LOCAL_SCRIPT_NAME} -p ${SOCKS5_SERVER_ADDR} -f {{ foreign_dns1 }} {{ foreign_dns2 }} -d {{ domestic_dns1 }} {{ domestic_dns2 }} && sudo systemctl restart dnsmasq") | sudo crontab - log_info "规则生成器配置完成并设置定时更新。" cd - > /dev/null @@ -351,35 +363,32 @@ sudo iptables -t nat -A PREROUTING -d 127.0.0.0/8 -j RETURN || log_error "排除 # 3.2 排除局域网内流量 (不代理内网互访) sudo iptables -t nat -A PREROUTING -i {{ lan_interface }} -d {{ gateway_lan_cidr }} -j RETURN || log_error "排除内网流量失败。" -# 3.3 【❗ 补充:排除远程 SSH 服务器 IP ❗】 -# 避免 SSH 连接本身被重定向,防止无限循环。 -# 假设变量 REMOTE_SSH_IP 已经正确替换。 +# 3.3 排除远程 SSH 服务器 IP (防止 SSH 连接本身被重定向) sudo iptables -t nat -A PREROUTING -d {{ remote_ssh_ip }} -j RETURN || log_error "排除远程 SSH IP 失败。" log_info "已添加规则:排除远程 SSH 服务器 {{ remote_ssh_ip }}。" - -# 3.4 【❗ 补充:排除国内 IP ❗】 -# 排除目标 IP 在 china_ip 集合中的流量(国内流量直连) -# 假设 china_ip 集合通过其他脚本/步骤创建 -# 注意:您的 gfwlist2new/gfw_rules_generator 脚本只处理 gfwlist,这里不需要 china_ip 的 ipset 排除 -# 而是依赖 gfwlist ipset 的精准捕获。我们移除这个容易出错的步骤,只依赖 gfwlist。 -# 如果需要排除国内 IP 段,需要有外部脚本维护 china_ip 集合。 - # --- 重定向规则 --- -# 3.5 核心重定向规则: +# 3.4 核心重定向规则: # 只有流量目标 IP 在 gfwlist ipset 集合中,才重定向到 redsocks。 # 注意:该规则必须在排除规则之后。 sudo iptables -t nat -A PREROUTING -i {{ lan_interface }} -p tcp -s {{ gateway_lan_cidr }} -m set --match-set gfwlist dst -j REDIRECT --to-ports ${REDSOCKS_PORT} log_info "已配置透明代理重定向规则 (目标IP在 gfwlist 中的内网TCP流量到 redsocks)。" +# ❗❗ 关键修正:必须排除远程 SSH 服务器的 IP ❗❗ +# 确保 SSH 隧道本身可以直连建立,不被代理。 +sudo iptables -t nat -A OUTPUT -d {{ remote_ssh_ip }} -p tcp -j RETURN +log_info "已配置排除远程 SSH 服务器 {{ remote_ssh_ip }} 的规则。" + # 4. 排除 redsocks 自身流量循环 (OUTPUT 链) -# 这一步非常重要,避免 redsocks/ssh 客户端自己产生流量又被 iptables 捕获 # 排除目标地址是 127.0.0.1 的流量,它们是 SSH 和 Redsocks 的内部通信。 sudo iptables -t nat -A OUTPUT -d 127.0.0.1 -p tcp -j RETURN log_info "已配置排除 redsocks 自身流量循环的规则。" +# 核心 OUTPUT 重定向规则:捕获所有目标IP在 gfwlist ipset 集合中的网关主机自身TCP流量,重定向到 redsocks。 +sudo iptables -t nat -A OUTPUT -p tcp -m set --match-set gfwlist dst -j REDIRECT --to-ports ${REDSOCKS_PORT} + # 保存 iptables 规则 sudo netfilter-persistent save || log_error "保存 iptables 规则失败。" sudo systemctl enable netfilter-persistent || log_error "启用 netfilter-persistent 服务失败。" diff --git a/scripts/gfw_rules_generator.py b/scripts/gfw_rules_generator.py index 8dc4abc..8d92cad 100644 --- a/scripts/gfw_rules_generator.py +++ b/scripts/gfw_rules_generator.py @@ -8,116 +8,124 @@ import sys GFWLIST_URL = "https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt" def log_info(message): - print(f"[INFO] {message}") + print(f"[INFO] {message}") def generate_rules(proxy_server, foreign_dns_list, domestic_dns_list, ipset_name="gfwlist"): - """ - 生成 Dnsmasq 和 IPSet 规则文件。 - """ - if not foreign_dns_list or not domestic_dns_list: - log_info("错误:必须提供至少一个国内 DNS 和一个国外 DNS。") - sys.exit(1) - - # --- 代理配置:通过本地 SOCKS5 隧道下载 GFWList --- - proxies = { - 'http': f'socks5h://{proxy_server}', # socks5h 使用远程 DNS 解析 - 'https': f'socks5h://{proxy_server}' - } - log_info(f"配置代理:使用 {proxy_server} 通过隧道下载 GFWList...") - # ---------------------------------------------------- - - try: - log_info(f"正在从 {GFWLIST_URL} 下载 GFWList...") - # 传入 proxies 参数,强制 requests 走代理 - response = requests.get(GFWLIST_URL, timeout=30, proxies=proxies) - response.raise_for_status() - - # 1. 解码 GFWList - try: - content = base64.b64decode(response.text).decode('utf-8') - except: - # 如果内容不是 base64 编码,则直接使用 - content = response.text - - except requests.exceptions.RequestException as e: - log_info(f"致命错误:通过代理下载 GFWList 失败,请检查 SSH SOCKS5 隧道状态!: {e}") - # 如果下载失败,中止脚本以避免生成空文件 - sys.exit(1) + """ + 生成 Dnsmasq 和 IPSet 规则文件。 + """ + if not foreign_dns_list or not domestic_dns_list: + log_info("错误:必须提供至少一个国内 DNS 和一个国外 DNS。") + sys.exit(1) + + # --- 代理配置:通过本地 SOCKS5 隧道下载 GFWList --- + proxies = { + 'http': f'socks5h://{proxy_server}', # socks5h 使用远程 DNS 解析 + 'https': f'socks5h://{proxy_server}' + } + log_info(f"配置代理:使用 {proxy_server} 通过隧道下载 GFWList...") + # ---------------------------------------------------- + + try: + log_info(f"正在从 {GFWLIST_URL} 下载 GFWList...") + # 传入 proxies 参数,强制 requests 走代理 + response = requests.get(GFWLIST_URL, timeout=30, proxies=proxies) + response.raise_for_status() + + # 1. 解码 GFWList + try: + content = base64.b64decode(response.text).decode('utf-8') + except: + # 如果内容不是 base64 编码,则直接使用 + content = response.text + + except requests.exceptions.RequestException as e: + log_info(f"致命错误:通过代理下载 GFWList 失败,请检查 SSH SOCKS5 隧道状态!: {e}") + # 如果下载失败,中止脚本以避免生成空文件 + sys.exit(1) - domains = set() - for line in content.splitlines(): - line = line.strip() - if not line or line.startswith(('#', '!')): - continue - - # 简单提取域名 - if line.startswith('||'): - domain = line[2:] - elif line.startswith('.'): - domain = line[1:] - elif '/' in line: - continue - else: - domain = line + domains = set() + for line in content.splitlines(): + line = line.strip() + if not line or line.startswith(('#', '!')): + continue + + # 1. 清理前缀 + if line.startswith('||'): + domain = line[2:] + elif line.startswith('.'): + domain = line[1:] + else: + domain = line + + # 2. 移除 URL 路径和查询参数 + if '/' in domain: + domain = domain.split('/')[0] + + # 3. 过滤掉 IP 地址和通配符,只保留域名 + if domain and '.' in domain and not domain.startswith('*.'): + # dnsmasq只接受不带前导点的域名,例如: example.com + if domain.startswith('.'): + domain = domain[1:] + + # 确保域名不包含非法字符 + if not any(c in domain for c in ('&', '?', '=', ':', '@', '%')): + domains.add(domain) - # 过滤掉 IP 地址,只保留域名 - if domain and '.' in domain and not domain.startswith('*.'): - domains.add(domain) + # 2. 生成 DNSMasq 配置文件 + dnsmasq_conf_path = "/etc/dnsmasq.d/gfwlist_router.conf" + + # 代理的上游 DNS (只使用提供的第一个国外 DNS) + primary_foreign_dns = foreign_dns_list[0] + proxy_port = proxy_server.split(':')[1] + proxy_dns_server = f"{primary_foreign_dns}#53" - # 2. 生成 DNSMasq 配置文件 - dnsmasq_conf_path = "/etc/dnsmasq.d/gfwlist_router.conf" - - # 代理的上游 DNS (只使用提供的第一个国外 DNS) - primary_foreign_dns = foreign_dns_list[0] - proxy_port = proxy_server.split(':')[1] - proxy_dns_server = f"{primary_foreign_dns}#{proxy_port}" + try: + with open(dnsmasq_conf_path, 'w') as f: + f.write(f"# Dnsmasq rules generated by {os.path.basename(__file__)} on {os.uname().nodename}\n") + + # 写入所有国内 DNS 服务器 + for dns in domestic_dns_list: + f.write(f"server={dns}\n") + + # GFWList 域名设置 + for domain in sorted(list(domains)): + # 1. 将域名加入 IPSet 集合 (由 dnsmasq 动态处理) + f.write(f"ipset=/{domain}/{ipset_name}\n") + # 2. 通过代理的 DNS 服务器解析 (流量会走 redsocks/ssh) + f.write(f"server=/{domain}/{proxy_dns_server}\n") + + log_info(f"已生成 Dnsmasq 规则到 {dnsmasq_conf_path},包含 {len(domains)} 个域名。") + except IOError as e: + log_info(f"写入 Dnsmasq 文件失败: {e}") + sys.exit(1) - try: - with open(dnsmasq_conf_path, 'w') as f: - f.write(f"# Dnsmasq rules generated by {os.path.basename(__file__)} on {os.uname().nodename}\n") - - # 写入所有国内 DNS 服务器 - for dns in domestic_dns_list: - f.write(f"server={dns}\n") - - # GFWList 域名设置 - for domain in sorted(list(domains)): - # 1. 将域名加入 IPSet 集合 (由 dnsmasq 动态处理) - f.write(f"ipset=/{domain}/{ipset_name}\n") - # 2. 通过代理的 DNS 服务器解析 (流量会走 redsocks/ssh) - f.write(f"server=/{domain}/{proxy_dns_server}\n") - - log_info(f"已生成 Dnsmasq 规则到 {dnsmasq_conf_path},包含 {len(domains)} 个域名。") - except IOError as e: - log_info(f"写入 Dnsmasq 文件失败: {e}") - sys.exit(1) - - # 3. 确保 IPSet 集合存在 (必须在 Dnsmasq 启动前存在) - try: - # 使用 hash:ip 集合,如果集合已存在则清除其内容 (flush) - os.system(f"sudo ipset create {ipset_name} hash:ip family inet hashsize 1024 maxelem 65536 &>/dev/null || sudo ipset flush {ipset_name}") - log_info(f"已确保 IPSet 集合 '{ipset_name}' 存在。") - except Exception as e: - log_info(f"创建 IPSet 失败: {e}") - sys.exit(1) - - return True + # 3. 确保 IPSet 集合存在 (必须在 Dnsmasq 启动前存在) + try: + # 使用 hash:ip 集合,如果集合已存在则清除其内容 (flush) + os.system(f"sudo ipset create {ipset_name} hash:ip family inet hashsize 1024 maxelem 65536 &>/dev/null || sudo ipset flush {ipset_name}") + log_info(f"已确保 IPSet 集合 '{ipset_name}' 存在。") + except Exception as e: + log_info(f"创建 IPSet 失败: {e}") + sys.exit(1) + + return True if __name__ == "__main__": - parser = argparse.ArgumentParser(description="GFWList Rules Generator for Dnsmasq/IPSet") - - # 允许接受多个参数 (nargs='+') - parser.add_argument("-p", "--proxy", required=True, help="SOCKS5 Proxy server (e.g., 127.0.0.1:1086)") - parser.add_argument("-f", "--foreign-dns", nargs='+', required=True, help="Foreign DNS IP(s) (e.g., 8.8.8.8 9.9.9.9)") - parser.add_argument("-d", "--domestic-dns", nargs='+', required=True, help="Domestic DNS IP(s) (e.g., 223.5.5.5 114.114.114.114)") - parser.add_argument("-n", "--name", default="gfwlist", help="IPSet Name") + parser = argparse.ArgumentParser(description="GFWList Rules Generator for Dnsmasq/IPSet") + + # 允许接受多个参数 (nargs='+') + parser.add_argument("-p", "--proxy", required=True, help="SOCKS5 Proxy server (e.g., 127.0.0.1:1086)") + parser.add_argument("-f", "--foreign-dns", nargs='+', required=True, help="Foreign DNS IP(s) (e.g., 8.8.8.8 9.9.9.9)") + parser.add_argument("-d", "--domestic-dns", nargs='+', required=True, help="Domestic DNS IP(s) (e.g., 223.5.5.5 114.114.114.114)") + parser.add_argument("-n", "--name", default="gfwlist", help="IPSet Name") - args = parser.parse_args() - - # 在开始生成规则之前,必须确保 requests 库已经安装 socks5 支持 - # 确保在 Bash 脚本中已经执行: sudo pip3 install requests PySocks - - log_info("开始生成 GFWList 规则...") - generate_rules(args.proxy, args.foreign_dns, args.domestic_dns, args.name) - log_info("规则生成完成。") + args = parser.parse_args() + + # 在开始生成规则之前,必须确保 requests 库已经安装 socks5 支持 + # 确保在 Bash 脚本中已经执行: sudo pip3 install requests PySocks + + log_info("开始生成 GFWList 规则...") + generate_rules(args.proxy, args.foreign_dns, args.domestic_dns, args.name) + log_info("规则生成完成。") diff --git a/scripts/render.py b/scripts/render.py index f6456b6..f13486f 100644 --- a/scripts/render.py +++ b/scripts/render.py @@ -12,6 +12,7 @@ config_vars = { # 网关主机独有变量 "dhcp_start_ip": "192.168.2.100", "dhcp_end_ip": "192.168.2.200", + "net_base": "192.168.2", "dhcp_lease_time": "7200", "remote_ssh_user": "kww", "remote_ssh_ip": "8.222.165.87", # !!! 替换为你的远程SSH服务器IP