本文章主要记录一些VPS的设置,使得购买的VPS及域名能够支持科学上网,服务器为Ubuntu 20.04。在执行任何操作之前,切换到/root目录,并使用apt update && apt upgrade来更新系统。
事前准备
服务器准备
部分软件:
apt install vim git curl wget -y启用 BBR TCP 拥塞控制算法:
echo "net.core.default_qdisc=fq" >> /etc/sysctl.confecho "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.confsysctl -p设置时区:
timedatectl set-timezone Asia/ShanghaiCloudflare准备
将Cloudflare中SSL/TLS加密模式设置为完全(严格)。
在Cloudflare创建一个新的令牌(参考cf插件主页配置),权限为Zone.Zone.Read; Zone.DNS.Edit,并将令牌保存到本地。
在Caddy官方下载地址,选择dns.provider.cloudflare,下载到本地准备。
设置密钥登陆
生成密钥
连接到服务器后,使用ssh-keygen来生成密钥,并把私钥保存到本地。
使用mv /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys更改公钥名称。使用如下命令来更改权限:
chmod 700 ~/.sshchmod 600 ~/.ssh/authorized_keys修改登录方式
使用vim /etc/ssh/sshd_config来修改SSH配置文件,将PasswordAuthentication改为no,将PubkeyAuthentication改为yes,并systemctl restart ssh重启SSH服务。
如有必要,可以更改ssh的端口号,修改
Port即可。
安装Caddy
apt install -y debian-keyring debian-archive-keyring apt-transport-httpscurl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpgcurl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.listapt updateapt install caddy把之前下载的Caddy传到root下,
mv ./caddy_linux_amd64_custom ./caddychmod +x ./caddy./caddy list-modules | grep dns如果正常,会显示出dns.providers.cloudflare模块。
在替换官方的Caddy:
mv ./caddy /usr/bin/caddy寻找合适的伪装站
示例关键字:intext:登录 Cloudreve
配置Caddy
启动Caddy服务:
systemctl enable --now caddy通过如下命令查看Caddy的日志:
journalctl -u caddy --no-pager | less +Gvim /etc/caddy/Caddyfile改为如下:
example.com { encode gzip
tls { dns cloudflare 你的API token protocols tls1.2 tls1.3 }
reverse_proxy /xui* 127.0.0.1:port { header_up Host {host} header_up X-Real-IP {remote_host} }
# ws # reverse_proxy /ray* 127.0.0.1:port { # header_up Host {host} # header_up X-Real-IP {remote_host} # }
# xhttp reverse_proxy /ray* h2c://127.0.0.1:port { header_up Host {host} header_up X-Real-IP {remote_host} }
# 伪装网址 reverse_proxy https://demo.cloudreve.org { header_up Host {upstream_hostport} }}奇怪的是xui只能在Chrome匿名模式下进入
caddy run查看tls是否生效:
tls.obtain certificate obtained successfullysudo systemctl reload caddy3X-UI
安装3X-UI
参考官方仓库:
curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh | sudo bash配置XUI
进入面板,把监听IP改为127.0.0.1,端口改为上面定义的xui端口。添加端口为ray端口的节点。
配置节点 vmess+ws
监听设置为127.0.0.1,端口设置为ray的端口,传输方式为WebSocket,路径填/ray
配置节点 vless+xhttp
监听设置为127.0.0.1,端口设置为ray的端口,传输方式为xhttp,路径填/ray
优选ip
到这里使用假节点vless://[email protected]:443?encryption=none&security=tls&sni=aaa.bbb.ccc&type=websocket&path=%2F123&#temp%20-%2054,然后使用如下脚本拿到ip:
import base64import jsonimport urllib.parseimport os
INPUT_FILE = "cf_ips.txt"OUTPUT_FILE = "new_ips.txt"
def extract_ips(): ips = [] if not os.path.exists(INPUT_FILE): print(f"[!] 找不到 {INPUT_FILE},请确保该文件存在。") return
with open(INPUT_FILE, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue
if line.startswith("vless://"): try: parsed = urllib.parse.urlparse(line) netloc = parsed.netloc # 解析如 uuid@ip:port 的格式 if "@" in netloc: host_port = netloc.split("@")[-1] ip = host_port.split(":")[0] ips.append(ip) except Exception as e: print(f"[!] VLESS 解析失败: {line[:30]}... 错误信息: {e}")
elif line.startswith("vmess://"): try: payloadstr = line[8:] padding = len(payloadstr) % 4 if padding: payloadstr += '=' * (4 - padding) config = json.loads(base64.b64decode(payloadstr).decode('utf-8')) ip = config.get("add") if ip: ips.append(ip) except Exception as e: print(f"[!] VMESS 解析失败: {line[:30]}... 错误信息: {e}")
# 去重处理(如有需要可取消注释下面这行进行去重过滤,但为了保持与源文件等量这里原样输出) # ips = list(dict.fromkeys(ips))
if ips: with open(OUTPUT_FILE, "w", encoding="utf-8") as f: for ip in ips: f.write(ip + "\n") print(f"[+] 成功提取 {len(ips)} 个 IP 地址,已保存至 {OUTPUT_FILE}") else: print("[!] 未能从文件中提取到任何 IP。")
if __name__ == "__main__": extract_ips()v2rayn
拿到new_ips.txt后,新建一个ips.txt,把内容复制进去,然后使用如下脚本生成对应的链接,然后进行测速:
import urllib.parseimport osimport base64import json
# ==========================================# ⚙️ 配置区# ==========================================# 你的 3x-ui 生成的“本地母体链接”BASE_URI = "vless://..."# 域名# TODO: 替换为你的真实域名DOMAIN = "aaa.bbb.ccc"# 节点名称# TODO: 替换为你喜欢的节点名称前缀NODE_NAME_PREFIX = "MyNode"
TXT_FILE = "ips.txt"OUTPUT_FILE = "v2rayn.txt"# ==========================================
def build_links(): if not os.path.exists(TXT_FILE): print(f"[!] 找不到 {TXT_FILE},请先创建。") return
is_vmess = BASE_URI.startswith("vmess://") is_vless = BASE_URI.startswith("vless://")
if not is_vmess and not is_vless: print("[!] 初始化失败,母体链接必须以 vless:// 或 vmess:// 开头") return
# 1. 拆解母体链接 if is_vless: parsed_base = urllib.parse.urlparse(BASE_URI) qs = urllib.parse.parse_qs(parsed_base.query)
uuid = parsed_base.username
# 提取核心固定参数(如果母体里有,就继承;没有就用默认值) network_type = qs.get('type', ['xhttp'])[0] path = qs.get('path', ['/'])[0] mode = qs.get('mode', ['auto'])[0] encryption = qs.get('encryption', ['none'])[0] else: # 解析 vmess payloadstr = BASE_URI[8:] padding = len(payloadstr) % 4 if padding: payloadstr += '=' * (4 - padding) try: base_config = json.loads(base64.b64decode(payloadstr).decode('utf-8')) except Exception as e: print(f"[!] VMESS 解析失败: {e}") return
links_list = [] index = 1
# 2. 遍历优选 IP 列表 with open(TXT_FILE, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue
parts = line.split() ip = parts[0] domain = DOMAIN # 跟 build_nodes.py 保持一致 node_name_base = f"{NODE_NAME_PREFIX} - {index}"
if is_vless: # 3. 组装全新的查询参数 (Query String) new_qs = { 'encryption': encryption, 'security': 'tls', # 强制开启 TLS 'sni': domain, # 注入真实域名 'type': network_type, 'path': path, 'mode': mode, # 'alpn': 'h2,http/1.1', # 注入 CF 兼容的 ALPN # 'fp': 'chrome', # 注入浏览器指纹 # 'host': domain, # 注入 Host 头 }
# 如果母体带有 extra 的 padding 参数,原样继承 if 'extra' in qs: new_qs['extra'] = qs['extra'][0] if 'x_padding_bytes' in qs: new_qs['x_padding_bytes'] = qs['x_padding_bytes'][0]
# 4. URL 编码参数 encoded_qs = urllib.parse.urlencode(new_qs, doseq=True)
# 5. 组装节点名称(备注名),与 build_nodes.py 保持一致 node_name = urllib.parse.quote(node_name_base)
# 6. 拼接最终的 URI final_uri = f"vless://{uuid}@{ip}:443?{encoded_qs}#{node_name}" links_list.append(final_uri) else: # VMESS 处理逻辑 config = base_config.copy() config['add'] = ip config['port'] = 443 # 使用 CF 的 TLS 端口 config['tls'] = 'tls' # 强制 TLS config['sni'] = domain # 注入真实域名 config['ps'] = node_name_base # 节点名称
b64str = base64.b64encode(json.dumps(config).encode('utf-8')).decode('utf-8') links_list.append(f"vmess://{b64str}")
index += 1
# 追加最后一个直连域名的节点 node_name_base = f"{NODE_NAME_PREFIX} - {index}" if is_vless: new_qs['sni'] = DOMAIN new_qs['host'] = DOMAIN encoded_qs = urllib.parse.urlencode(new_qs, doseq=True) node_name = urllib.parse.quote(node_name_base) final_uri = f"vless://{uuid}@{DOMAIN}:443?{encoded_qs}#{node_name}" links_list.append(final_uri) else: config = base_config.copy() config['add'] = DOMAIN config['port'] = 443 config['tls'] = 'tls' config['sni'] = DOMAIN config['host'] = DOMAIN config['ps'] = node_name_base b64str = base64.b64encode(json.dumps(config).encode('utf-8')).decode('utf-8') links_list.append(f"vmess://{b64str}")
# 7. 导出到文件 if links_list: with open(OUTPUT_FILE, 'w', encoding='utf-8') as f: for link in links_list: f.write(link + '\n') schema_name = "VMESS" if is_vmess else "VLESS" print(f"[+] 成功生成 {len(links_list)} 个标准 {schema_name} 链接,已保存至 {OUTPUT_FILE}")
if __name__ == '__main__': build_links()clash
vmess://ew0KICAidiI6ICIyIiwNCiAgInBzIjogIlJFUExBQ0VNRSIsDQogICJhZGQiOiAiMTA0LjE4LjExOS4yMCIsDQogICJwb3J0IjogIjQ0MyIsDQogICJpZCI6ICIwY2MzMjY3ZS1kNmIwLTRiMjctYTM0Zi0zOGY3MGI5ZjhjZGMiLA0KICAiYWlkIjogIjAiLA0KICAic2N5IjogImF1dG8iLA0KICAibmV0IjogIndzIiwNCiAgInR5cGUiOiAibm9uZSIsDQogICJob3N0IjogImFhYS5iYmIiLA0KICAicGF0aCI6ICIvYWJjZGVmZyIsDQogICJ0bHMiOiAidGxzIiwNCiAgInNuaSI6ICIiLA0KICAiYWxwbiI6ICIiDQp9
使用上面的假链接到这里生成订阅链接,获取配置文件,保存到configs/mini_adplus.yaml。然后使用如下的脚本生成:
import urllib.parseimport urllib.requestimport jsonimport yamlimport osimport base64import ssl
# ==========================================# ⚙️ 配置区# ==========================================# 你的母体链接,支持 vless:// 或 vmess://BASE_URI = "vless://..."
# 域名DOMAIN = "aaa.bbb.ccc"# 节点名称NODE_NAME_PREFIX = "MyNode"
TXT_FILE = "ips.txt"OUTPUT_YAML = "optimized_nodes.yaml"
# mini_adplus 配置模板MINI_ADPLUS_YAML = os.path.join(os.path.dirname(os.path.abspath(__file__)), "configs", "mini_adplus.yaml")MINI_ADPLUS_URL = ( "https://api.wcc.best/sub?target=clash&url=vmess%3A%2F%2Few0KICAidiI6ICIyIiwNCiAgInBzIjogIlJFUExBQ0VNRSIsDQogICJhZGQiOiAiMTA0LjE4LjExOS4yMCIsDQogICJwb3J0IjogIjQ0MyIsDQogICJpZCI6ICIwY2MzMjY3ZS1kNmIwLTRiMjctYTM0Zi0zOGY3MGI5ZjhjZGMiLA0KICAiYWlkIjogIjAiLA0KICAic2N5IjogImF1dG8iLA0KICAibmV0IjogIndzIiwNCiAgInR5cGUiOiAibm9uZSIsDQogICJob3N0IjogImFhYS5iYmIiLA0KICAicGF0aCI6ICIvYWJjZGVmZyIsDQogICJ0bHMiOiAidGxzIiwNCiAgInNuaSI6ICIiLA0KICAiYWxwbiI6ICIiDQp9%7C&insert=false&config=https%3A%2F%2Fraw.githubusercontent.com%2FACL4SSR%2FACL4SSR%2Fmaster%2FClash%2Fconfig%2FACL4SSR_Online_Mini_AdblockPlus.ini")# ==========================================
def build_node_yaml(protocol, ip, uuid, network, path, mode, domain, index, config_dict): # 构造 Mihomo 标准 YAML 字典 proxy = { 'name': f"{NODE_NAME_PREFIX} - {index}", 'type': protocol, 'server': ip, 'port': 443, # 强制使用 HTTPS 端口 'uuid': uuid, # 继承母体的 UUID 'network': network, 'udp': True, 'tls': True, # 强制开启 TLS # 'alpn': ['h2', 'http/1.1'], # 强制指定 ALPN,部分 CF 节点非常挑剔 # 'sni': domain, 'servername': domain, # 许多受限内核 VLESS 配置只认 servername 'skip-cert-verify': False, 'client-fingerprint': 'chrome', # 极其重要:伪装 TLS 指纹,防 CF 阻断 }
if protocol == 'vmess': proxy['alterId'] = 0 proxy['cipher'] = config_dict.get('scy', 'auto')
if network == 'xhttp': proxy['xhttp-opts'] = { 'mode': mode, 'path': path, # 'headers': { # 'Host': [domain] # } } if protocol == 'vless' and 'extra' in config_dict: try: extra_json = json.loads(urllib.parse.unquote(config_dict['extra'][0])) proxy['xhttp-opts']['extra'] = extra_json except: pass elif network == 'ws': proxy['ws-opts'] = { 'path': path, # 'headers': { # 'Host': domain # ws-opts 的 Host 在 Mihomo 中通常是字符串 # } }
return proxy
def download_mini_adplus(): """检测 mini_adplus.yaml 是否存在,不存在则从远程下载""" if os.path.exists(MINI_ADPLUS_YAML): print(f"[✓] 配置模板已存在: {MINI_ADPLUS_YAML}") return True
print(f"[*] 配置模板不存在,正在从远程下载...") os.makedirs(os.path.dirname(MINI_ADPLUS_YAML), exist_ok=True)
try: # 跳过 SSL 验证(subconverter API 可能证书不完整) ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE
req = urllib.request.Request(MINI_ADPLUS_URL, headers={ 'User-Agent': 'ClashForAndroid/2.5.12' }) with urllib.request.urlopen(req, context=ctx, timeout=30) as resp: data = resp.read()
with open(MINI_ADPLUS_YAML, 'wb') as f: f.write(data)
print(f"[✓] 下载成功: {MINI_ADPLUS_YAML} ({len(data)} bytes)") return True except Exception as e: print(f"[✗] 下载失败: {e}") return False
def build_clash(): # 0. 确保 mini_adplus.yaml 模板存在 if not download_mini_adplus(): print("[!] 无法获取配置模板,退出。") return
if not os.path.exists(TXT_FILE): print(f"[!] 找不到 {TXT_FILE},请先创建。") return
# 1. 拆解母体链接 if BASE_URI.startswith('vmess://'): protocol = 'vmess' b64_str = BASE_URI[8:] b64_str += "=" * ((4 - len(b64_str) % 4) % 4) config_dict = json.loads(base64.b64decode(b64_str).decode('utf-8')) uuid = config_dict.get('id') network = config_dict.get('net', 'ws') path = config_dict.get('path', '/') mode = 'auto' elif BASE_URI.startswith('vless://'): protocol = 'vless' parsed_base = urllib.parse.urlparse(BASE_URI) config_dict = urllib.parse.parse_qs(parsed_base.query) uuid = parsed_base.username network = config_dict.get('type', ['tcp'])[0] path = urllib.parse.unquote(config_dict.get('path', ['/'])[0]) mode = config_dict.get('mode', ['auto'])[0] else: print("[!] 不支持的协议类型,只能处理 vmess:// 或 vless://") return
proxies_list = [] index = 1
# 2. 读取优选列表并进行批量克隆 with open(TXT_FILE, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue
parts = line.split() ip = parts[0] domain = DOMAIN
proxy = build_node_yaml(protocol, ip, uuid, network, path, mode, domain, index, config_dict) index += 1 proxies_list.append(proxy)
# 追加最后一个兜底真实域名的节点 proxy = build_node_yaml(protocol, DOMAIN, uuid, network, path, mode, DOMAIN, index, config_dict) proxies_list.append(proxy)
# 3. 基于 mini_adplus.yaml 模板生成最终配置 FINAL_CONFIG = "final_config.yaml"
if proxies_list: proxy_names = [p['name'] for p in proxies_list]
try: from ruamel.yaml import YAML yaml_parser = YAML() yaml_parser.preserve_quotes = True with open(MINI_ADPLUS_YAML, 'r', encoding='utf-8') as f: config_data = yaml_parser.load(f) except ImportError: with open(MINI_ADPLUS_YAML, 'r', encoding='utf-8') as f: config_data = yaml.safe_load(f) yaml_parser = None
# 替换 proxies 为新生成的节点 if 'proxy-providers' in config_data: del config_data['proxy-providers'] config_data['proxies'] = proxies_list
# mode 值需要小写 if 'mode' in config_data: config_data['mode'] = config_data['mode'].lower() # 期望的策略组结构(用于变更检测) EXPECTED_GROUPS = [ '🚀 节点选择', '♻️ 自动选择', '🎯 全球直连', '🛑 全球拦截', '🐟 漏网之鱼', ] # 需要注入新节点的策略组 INJECT_GROUPS = {'🚀 节点选择', '♻️ 自动选择'} # 需要清理旧节点但不注入新节点的策略组(只保留分组引用) CLEAN_GROUPS = {'🐟 漏网之鱼'}
if 'proxy-groups' in config_data: # ⚠️ 变更检测:检查模板的策略组是否与预期一致 actual_groups = [g['name'] for g in config_data['proxy-groups']] if actual_groups != EXPECTED_GROUPS: print("=" * 60) print("⚠️ 警告: 模板策略组结构已发生变化!") print(f" 期望: {EXPECTED_GROUPS}") print(f" 实际: {actual_groups}") added = set(actual_groups) - set(EXPECTED_GROUPS) removed = set(EXPECTED_GROUPS) - set(actual_groups) if added: print(f" 新增: {added}") if removed: print(f" 缺失: {removed}") if actual_groups != EXPECTED_GROUPS and not (added or removed): print(f" 顺序不同") print(" 请检查是否需要更新脚本逻辑!") print("=" * 60)
# 收集内置策略和分组名,用于区分"具体节点"和"引用" builtin_names = {'DIRECT', 'REJECT'} group_names = {g['name'] for g in config_data['proxy-groups']} skip_names = builtin_names | group_names
for group in config_data['proxy-groups']: gname = group['name']
# 处理 use 字段(proxy-provider 引用) if 'use' in group: del group['use']
if gname in INJECT_GROUPS: # 节点选择 / 自动选择:替换为新节点 if 'proxies' in group: kept = [p for p in group['proxies'] if p in skip_names] group['proxies'] = proxy_names + kept else: group['proxies'] = proxy_names[:] elif gname in CLEAN_GROUPS: # 漏网之鱼:只删掉具体节点名(REPLACEME等),保留分组引用 if 'proxies' in group: group['proxies'] = [p for p in group['proxies'] if p in skip_names]
# 修补 rules 尾部,防止 DNS 泄露 if 'rules' in config_data: rules = config_data['rules'] new_rules = [] for r in rules: rule_str = str(r) # GEOIP,CN 加上 no-resolve if rule_str.startswith('GEOIP,CN,') and 'no-resolve' not in rule_str: new_rules.append(rule_str + ',no-resolve') else: new_rules.append(r) # 在 MATCH 之前插入 GEOSITE,CN(如果不存在) has_geosite_cn = any('GEOSITE,CN,' in str(r) for r in new_rules) if not has_geosite_cn: # 找到 MATCH 规则的位置,插入到它前面 match_idx = next((i for i, r in enumerate(new_rules) if str(r).startswith('MATCH,')), len(new_rules)) new_rules.insert(match_idx, 'GEOSITE,CN,🎯 全球直连') config_data['rules'] = new_rules
# ⚠️ 检查所有 IP 类规则是否都有 no-resolve IP_RULE_PREFIXES = ('IP-CIDR,', 'IP-CIDR6,', 'GEOIP,') missing = [] for i, r in enumerate(new_rules): rule_str = str(r) if any(rule_str.startswith(p) for p in IP_RULE_PREFIXES) and 'no-resolve' not in rule_str: missing.append((i + 1, rule_str)) if missing: print("=" * 60) print(f"⚠️ 警告: 发现 {len(missing)} 条 IP 类规则缺少 no-resolve,可能导致 DNS 泄露!") for idx, rule in missing[:10]: # 最多显示10条 print(f" 第 {idx} 条: {rule}") if len(missing) > 10: print(f" ... 还有 {len(missing) - 10} 条") print("=" * 60) # 写入最终配置 try: if yaml_parser is not None: with open(FINAL_CONFIG, 'w', encoding='utf-8') as f: yaml_parser.dump(config_data, f) else: with open(FINAL_CONFIG, 'w', encoding='utf-8') as f: yaml.dump(config_data, f, allow_unicode=True, sort_keys=False, indent=2) print(f"[+] 成功生成配置: {FINAL_CONFIG} (基于 mini_adplus.yaml, 类型: {protocol}, 包含 {len(proxies_list)} 个节点)") except Exception as e: print(f"[✗] 写入配置失败: {e}")
if __name__ == '__main__': build_clash()注意事项
如果clash遇到wsl无网络的问题,在最前面加入mtu设置:
mixed-port: 7890allow-lan: truemode: rulelog-level: infoexternal-controller: :9090
tun: mtu: 1280