Files
2026-10-06 18:54:03 +08:00

43 KiB
Raw Permalink Blame History

ProxyCat 功能特性

文档导航:返回 README · 详细配置 · 使用手册 · 报错排查手册 · 更新日志

出口代理服务

  • 双协议监听:同一个端口同时接受 HTTP 与 SOCKS5 入站请求。
  • 三类上游代理:支持带认证的 HTTP / HTTPS / SOCKS5 代理服务器。
  • 两种轮换模式:cycle 按清单顺序使用(忽略负载,前一条满了才用下一条), loadbalance 按实时负载挑压力最小的出口。
  • 三种代理来源:local 读本地列表、api 实时取接口、pool 直接用内置代理池。
  • 多出口并发:一次取回多个上游代理同时使用,请求默认按各出口的实时负载分摊 (local 的 cycle 模式按清单顺序挑,不按负载),单个出口失败会被移出轮换并自动 补货 —— 上游按出口 IP 限流时,并发随出口数成倍放大(见上游出口与并发)。
  • 访问控制:客户端 IP 白名单 / 黑名单,同时命中时按 ip_auth_priority 决定放行还是拦截; 另有绕过名单:命中的目标地址不经代理、直接连接。
  • 出口认证:支持用户名 / 密码认证,账号表为空即关闭认证。

换来源用「代理配置」页顶部的来源切换按钮,改模式与寿命、调出口容量则在「出口轮换」组里完成,保存后即时生效:

出口轮换

上图是「维护池代理」来源下的「出口轮换」组:更换方式、出口寿命(按时间与按次数两条独立 规则)、在用出口数与无可用出口时的排队时长。

内置代理池

  • 插件化抓取:抓取插件放进 modules/proxypool/plugins/,池服务启动时扫描加载; 新增插件需重启池服务,已在列表中的插件改完可在面板里点「重载」热加载。
  • 先验证再入库:端口初筛淘汰无人监听的地址,存活候选再做完整验证,确认可用的才写进库。
  • 三档验证强度:liveness 只判通不通、identity 补身份信息、full 全量评估。 自动重验证在这三档之间按策略自行选择;面板上的批量验证可手选 liveness / full, 传 auto 表示仍按策略决定(identity 不是批量接口的可选值)。
  • 健康评分:按延迟 25%、成功率 25%、稳定性 15%、可用时长 15%、匿名度 10%、存活时长 10% 合成为 0-100 分,成功率维度取 Wilson 置信下界,样本少时自动向低收缩。
  • 离线归属地:本地离线库给出中英两套地区名,不发外部查询请求。
  • 自动重验证与自动清理:按周期补测,并按健康分、连续失败、失效保留期清理。
  • 使用反馈:运行期真实请求的成败回流到池,连续失败达阈值即标记失效 (见使用反馈是怎么算的)。

池的启动条件与运行机制见下文代理池机制;插件开发的完整契约见 modules/proxypool/README.md。

面板与运维

  • Web 管理面板:状态总览、配置编辑、代理列表、名单管理、服务启停一屏完成; 配置项按用途分组,调优参数收在单独的「高级选项」组,不把调优参数和常用开关混在一屏。
  • 日志中心:按用途分为访问 / 代理 / 主程序 / 代理池四类,支持级别与关键词过滤、历史文件回看、导出、清理。 注意日志级别与访问日志是联动的:成功访问行是 INFO,级别调到 WARNING 以上后访问日志里 只会剩失败与错误,面板会就地给出提示。
  • 访问记录:逐条记录每次代理请求,可按时间 / 上游 / 目标 / 结果过滤并导出 CSV / JSON。
  • 域名统计:按「目标域名 × 上游代理」累计成败次数,可下钻查看。
  • 多语言:面板支持中文 / 英文切换,语言是服务端设置,整站文案一起切换。
  • 凭据管理:上游取代理接口的凭据可存多套并一键切换。
  • 容器化部署:Docker 一键部署,内置健康检查会按实际面板端口探活。
  • 版本检查:从 GitHub 的发布记录取最新版本,默认并发请求官方发布源、国内可达的镜像与官方 API,谁先返回用谁;启动时比对一次,之后每 24 小时一次;结果落盘,24 小时内重启不重查, 也不会因为打开面板而等待网络。侧边栏版本号旁的按钮可立即检查一次,结果显示在原处 (失败时连原因一起显示)。地址可用 version_check_url 换成自有镜像。

面板四个页签(代理配置 / 代理池 / 访问控制 / 运行日志)的逐页说明与截图见 使用手册。

日志与访问记录

日志按用途分流到 logs/ 下的四个文件,互不混杂,各自按大小轮转:

文件 内容
logs/main.log 主程序:启动、配置、面板地址、关闭
logs/access.log 代理请求:时间、客户端、上游代理、目标域名、成败、耗时
logs/proxy.log 代理生命周期:加载、切换、验证、重试
logs/error.log 以上各类别中 ERROR 及以上的汇总,排障时先看这里

访问日志每行有三种结果,含义各不相同:

结果 含义
[成功] 请求确实走通了(HTTPS 隧道要等上游有数据回来才算,仅写回 200 不算)
[失败] 这次请求失败,原因写在行尾
[中断] 请求在没有得出成败结论前就结束(如客户端未发数据就断开、隧道双方都没传过数据、请求被取消),不计入成功率与失败率

行尾的状态码是写回给客户端的 HTTP 状态码,它可能与结果不一致:[成功] 行记录的是上游 原样返回的状态码,上游回 4xx/5xx 也会记为 [成功];[中断] 行会保留已经写回的状态码 (如 200),但结果既不算成功也不算失败;而隧道已建立但上游从不发送数据这类 [失败], 写日志时不再保留状态码,状态显示为 -。因此看结果列,不要只看状态码。 上游代理地址中的密码一律以 *** 替代后落盘,不会记录明文凭据。 代理池自身的日志仍写在 modules/proxypool/logs/proxy_pool.log。

访问统计按上游代理汇总,展开某个代理可以看到该代理访问过的每个域名。聚合键是 「域名 × 上游代理」而不是只看域名 —— 同一个域名经不同代理访问的结果可能完全不同, 只看域名会把「这个代理访问不了这个站」这条最有用的信息抹平;统计结果落 SQLite 持久化, 重启后累计值不丢。

上游出口与并发

ProxyCat 维护一个有目标容量的出口池:一直维持 exit_count 个上游代理同时在用, 而不是只压一个 IP,也不是攒一批换一批。

池子怎么维持:

  1. 调一次来源接口,取回的地址先放候补队列(带过期时间与长度上限)。

  2. 从候补里取候选进名册,每轮最多取 4 条并发校验(不是一条一条取),通过的按顺序 进名册 —— 直到名册满 exit_count;校验不合格的丢弃,通过但这一轮没用上的送回候补。

  3. 某个出口被判失效时只换掉它一个,其余照常服务。取代理与换出口由请求驱动: 有请求在过出口时,后台才去来源补货、才把到期的换新;一轮没进展不会当场空转重试, 而是把重试间隔翻倍退避(最多 60 秒)后再试,且始终只在近期有流量时才发起。 流量停下之后(约 10 秒宽限)就不再补货:到期的出口被移除而不是换新 (continuous 不依赖流量,见下条),池子可能因此被抽空 —— 冷启动的第一个请求 要等一次取货 + 校验。

  4. 每个出口各有各的寿命(所有来源、四种运行模式都生效):入池满 interval 秒就到期, 到期的全部被换掉(不是每隔 interval 只换一个 —— 那样最坏的出口能活 出口数 × interval)。一批同时入池的会同时到期,所以到点是一次集中的替换; 替换始终是「先加后减」,而且候补不够时宁可先把旧的那个留着,名册不会因此出现空窗。 request_interval 是同一件事按请求数算(每个出口自己数被分配出去多少次), 它与 interval 是两条各自独立的规则,同时生效:两个都填时谁先到就换谁, 不是二选一。

    换的时机由运行模式决定:continuous(持续更换)与是否空闲无关,后台一直按寿命 换掉到期的并补满,池子恒定为 exit_count;request(触发更换)只在收到新任务时才 取货、换出口,一个任务都没有时到期的出口直接移除、池子会空掉,下一个请求到达 时才重新取货填满。本地来源的两项(cycle / loadbalance)管的是挑谁,换的时机 与「触发更换」一样由请求驱动;本地换上来的是文件里的下一条,文件里没有备用条目时 原地重开一次寿命(否则它会挂着「剩余 0」一直用下去)。

    换的这一刻如果候补取不到货,池子会短暂缺人 —— 这时的请求排队等(最多 exit_wait_timeout 秒,见下),而不是当场 503;等到了照常服务,超时了才回 503。

取几个由 exit_count 决定,与来源无关:每次取货按名册缺额的 min(缺额 × 8, 24) 超量 取回、放进候补,再从候补取候选校验填充到 exit_count;api 与 pool 都按这个超量值取, api 返回的行中超出该上限的部分会被截断丢弃、不会进候补,local 直接用文件本身、 以 exit_count 为上限。面板的「代理状态」区会逐一列出每个出口(地址、在用/上限、 剩余寿命),顶部那根进度条看的是最近到期的那个;命令行打印「上游出口: N/目标」 加每个出口一行,同样带各自的剩余寿命。到点而没流量时(continuous 除外)出口不再换新: 到期的会被逐个移出名册,直到下一个请求到达才重新取货填满。

请求默认按各出口的实时活跃数分配(不是简单轮询)—— 隧道寿命长短不一,按请求数轮询 会让负载倾斜;local 的 cycle 模式按清单顺序挑,不看负载。

故障怎么算:每个出口带一个滑动窗口(最近 10 次结果),判定看窗口里的失败占比, 不是「连续失败几次」。窗口不记「干净」时期的成功 —— 出口在还没出现过失败之前,成功 不进窗口,所以它的第一次失败就会把占比顶满、当场排到健康出口后面;有了失败记录之后, 成功才进窗口参与稀释:

  • 失败居多就先让别人上(占比过半):分配请求时排在健康出口后面,但仍然可用 —— 全都可疑时照用不误。它没有冷却时间:每次成功都会记回窗口、把占比往下压,压回过半以下 就自然回到竞争里(窗口里失败仍占多数时,一次成功还翻不过来)。占比过半还会让它 在一次真实失败后被独立探测一次:探测失败就把它移出名册(此时占比可以远低于九成、 窗口也可能不足 5 条),探测通过则继续留着。
  • 占比到九成、且窗口里至少有 5 次结果,也会把它移出名册 —— 这是失败滑窗自己的 淘汰阈值,不是移出名册的唯一路径。缺口由后台补货填上(取来源受 switch_cooldown 限流,从候补填充不限流)。用占比而不是连续次数,是为了把「一次抖动同时掐断几条隧道」 和「这个出口就是坏的」分开:前者留下的失败会被随后的成功稀释掉,后者一直涨。反过来, 一次掐断的隧道足够多、把整个窗口填满失败时,它也会被当场停用 —— 那一刻它确实对几十个 请求一条都没成功。
  • 名册里的最后一个出口永远不会被停用。停用它等于全线 503,而本地来源还会被「刚停用」 记忆挡住、几分钟内补不回来;让它继续挨着比没有出口强。
  • 记账不折叠,折叠只发生在日志层:proxy_failure_cooldown 窗口内同一类告警只写一条, 并记下被折叠了多少条。出口的每一次失败都如实进窗口 —— 在记账上折叠,会抹掉「这个出口 一直在失败」这个事实,而那正是决定还要不要给它派活的依据。

检测怎么做:check_proxies_on_use 控制的是填充槽位时的校验 —— 每条候补在进名册 之前真的探一次(发一次请求),不是只看端口通不通;来源给的地址可能一分钟就失效,端口开着 不代表还能用。它不是按请求校验:出口在服役期间失效,靠上面的失败淘汰兜底。

启动服务时(ProxyCat.py 入口或从面板启动服务)还会对当前名册做一次完整检测 (check_proxies_on_startup),不通的逐个移出、缺的交给补货;代理文件重载后的 检测只有命令行入口会做 —— ProxyCat.py 发现 ip.txt 被外部修改而重载时顺带检测, 而在面板上保存本地代理列表只重载名册、不触发检测(app.py 也不监听 ip.txt 的变化)。 面板上的「检测代理」按钮检测的是源清单(local 来源下即 ip.txt 里的全部条目), 所以已经被停用的坏出口也查得到。

并发怎么算:

实际同时打到上游的请求数 ≈ min(max_concurrent_requests, 出口数 × max_concurrent_per_proxy)
  • max_concurrent_per_proxy 默认 0(不限制),此时并发只受 max_concurrent_requests 约束, 与「只用一条上游」的时代行为一致。上游按出口 IP 限流时,把它调成 单 IP 能承受的并发数(例如 20),超出部分会排队而不是把上游打挂。
  • 它数的是活跃连接:HTTP 请求在转发期间算,隧道建立之后只有真的在传数据才算。 空闲隧道(浏览器保活、客户端僵死、上游半死)不占额度,所以它是一道准入控制 而不是「同时在挂的上游连接数」—— 空闲隧道重新有数据时,那一刻的活跃数可以略高于 这个值(如实反映「确实有这么多条在传」)。
  • max_concurrent_requests 是全局信号量,对三类请求一视同仁,按「隧道」而不是「请求」估: 它才是「本机愿意同时挂多少条隧道」的硬上限,每出口额度之和远超它时,真正生效的是它。

性能与并发

上图是面板「代理配置 → 性能与并发」组:全局并发上限与单出口并发上限; 「单出口并发上限」填 0 时既不限制、自动扩容也不会扩,但已经张开的扩容状态照常收回。

「在用」怎么算:面板与命令行的「在用」是上面那个活跃口径 —— 正在处理请求的连接, 加上最近有中继数据的隧道。空闲但没断开的隧道不显示(它仍占着一条上游连接与一个名额, 只是没有数据在流),所以请求停下之后数字会回落,不必等对端关连接。

资源怎么还回来:一次传输结束(隧道任一端关闭、响应发完)时,占用的出口名额立即归还, 不等到连接对象被回收。客户端失联(断网、NAT 超时、进程被杀)的隧道则由分钟级的 TCP keepalive 兜底:系统默认是两小时,那意味着一条被遗弃的隧道要占着出口名额两小时,连续 请求之下会把额度一点点占满 —— 现在 60 秒空闲开始探活、10 秒一次,失联的连接在一两分钟 内被内核判死并释放。活着但空闲的连接(keep-alive 复用)探测会被正常应答,不受影响。

连接复用:普通 HTTP 转发走每个出口各自的 HTTP 连接池,连接的空闲回收时间由 client_keepalive_expiry 控制;缓存这些连接的客户端对象另按 client_idle_timeout (面板不展示)回收。CONNECT 与 SOCKS5 隧道做不到复用 —— 一条上游连接建立隧道后 就被该隧道独占,无法归还给别的请求。

client_max_connections(每出口连接上限)与 max_pool_size(缓存多少个客户端) 面板不再展示:前者按 单出口并发上限 × 2 推算(单出口不限并发时退回全局并发上限), 后者按 max(本值, 名册数 × 2, 出口数 × 4) 取用。两者的值仍写在 config.ini 里, 高于推算值时照常生效,只是不再需要用户理解它们。 client_max_keepalive(每出口保活连接数)也从面板撤下,默认 8 —— 空闲连接越多, httpx 分配请求时的全池扫描越贵,实测调大会让吞吐掉到三分之一。

高峰期自动扩容(auto_expand_enabled,默认开,可在面板「性能与并发」组里关掉):设了 max_concurrent_per_proxy 时,有两条触发路径:①立刻触发 —— 监听端口上此刻挂着的 连接数超过「在用出口数 × 单出口额度」时,当场把出口数翻倍(上限 2 倍)并一次补齐,不用 等水位;②兜底 —— 八成的出口跑到这个额度并持续一秒,后台逐格加出口,一拍加一个、 加上才作数。扩容只加出口(多几个上游 IP 一起分担)。名册最多到 max(exit_count, min(exit_count × 2, max_concurrent_requests)):出口比并发请求还多没有 意义,到这条线不再加出口,但单出口额度的放宽不受这条线约束,仍会单独推进(见下)。 高峰过去后一步收回:整池利用率回落到一半以下并持续 30 秒,就先恢复单出口额度、再 撤掉多出来的出口;还在传数据的出口会留着把请求跑完。

扩与收看的是两个不同的数:扩容问「够不够用」(多少出口顶在额度上),收回问「用掉了 多少」(整池利用率)。拿前者判收回会出事 —— 负载摊开之后可能一个出口都不再顶额,而池子 恰恰刚好够用,收回一格队列就会回来、下一拍又得加回去,出口数永远在浮动。

放宽单出口额度(最多到两倍)是最后手段:兜底路径下要有证据 —— 只有这一拍真的向 来源取过货、拿到了答复、却没能拿到一条能用的新候选时才放宽,冷却期内没去取货、取回的 候选校验没过、取货本身失败(超时、接口报错),都只算「这一拍没结论」,下一拍再试。即时 路径不受这条约束:监听侧连接已经挤爆、名册又已到顶时,它照样会把额度往上调(最高两 倍)—— 加出口是把负载分给更多 IP,放宽额度是把压力压回原有 IP,后者正是 max_concurrent_per_proxy 要防的。反过来,把出口池容量调小时,多出来的出口当场 就会撤掉(只撤没有在飞请求的),与水位无关;自动扩容收回后多出来的出口则不会立刻丢弃 —— 它们用满自己的寿命才逐个撤下。

它的水位是按配置里的额度算的,不是按放宽后的值 —— 否则「放宽」这个动作本身会把水位 压下去,紧接着就被判成高峰已过,来回震荡。max_concurrent_per_proxy 为 0(不限)时 这条功能不扩,但照常收回:不限并发不是留着膨胀状态的理由。

代理池机制

代理池不是独立项目 —— 它运行在 ProxyCat 进程内,与面板共用同一个 Web 端口,没有自己的 独立界面,管理入口是面板的「代理池」页签。

子模块文档:modules/proxypool/README.md —— 覆盖抓取插件开发、 验证策略、匿名度判定、归属地解析、自动清理等实现细节,内容较深,按需查阅。

validator.max_concurrent(池设置里的「同时验证多少个代理」)是这套子系统的资源旋钮: 它同时决定本机 HTTP 连接池上限与可打开的 socket 数 —— 一次验证会并发发出十几个请求。 机器或 ulimit -n 受限时把它调低;本机连接池排队造成的超时,存活探测阶段判为 「本轮不下结论」、不会记到代理头上,但存活通过之后的身份与明文探测若再因排队超时, 会记成一次不成功的探测样本,并直接拉低健康分的成功率维度。

什么时候启动

内置代理池只在真的要用它当代理来源时才启动,由 [Server] 的 proxy_source_mode 决定:

proxy_source_mode 池是否随服务启动
pool(维护池) 启动
local(本地文件) 不启动
api(API 获取) 不启动

配了 pool_remote_url(接入外部代理池)时也不启动 —— 那种情况根本不走进程内这一个。

用本地文件或 API 取代理时,池跑着只是白耗资源去联网抓取和验证代理,所以默认不起。 如果在面板里把来源切换成「维护池代理」,而池当前没在跑,会自动把它拉起来; 反过来切换走不会自动停,避免误停正在提供代理的池 —— 需要停的时候在「代理池」 页签里手动停。

内置池与外部池

默认使用进程内的池,开箱即用。如果你已经有别的代理池服务,可以把 pool_remote_url 指向它,让出口代理改用外部池:

  • 它是取用接口,不是管理接口:ProxyCat 会对该地址发一次 GET,响应体里的每一行都是 一个代理地址(例如 http://1.2.3.4:8080)。返回 200 且格式正确即采用,返回 404 表示暂时 没有可用代理。填另一个 ProxyCat 实例的 /api/pool/random?format=text&token=... 得到单个 出口;想要一次取多个,改用 /api/pool/get?status=valid&limit=20&format=text&token=... (也可以直接指向内置池的 /api/pool/get,此时走的是池的接口而不是进程内直调)。
  • 只在 proxy_source_mode=pool 时生效。
  • 设置之后进程内的池不会再自动启动(它是被外部池替代的关系)。
  • 使用反馈在外部池模式下不生效 —— 池不在本进程里,反馈无从回报。

使用反馈是怎么算的

出口代理在真实转发中如果发现某个出口不通,会先把这次失败记在那个出口头上。失败占比 过半的出口随后会被独立探测一次:探测失败就移出轮换(此时占比可以远低于九成、窗口也 不足 5 条),探测通过则继续用;占比到九成、窗口里至少 5 次结果时,由失败滑窗直接淘汰 (两条路径见上游出口与并发)。被探测出口的结果会回报给池;池那边连续 失败达到 use_feedback.invalid_threshold(默认 3 次)就把该代理标记为失效,期间任何一次 成功都会把计数清零。

出口的移出与池的标记失效是两件事:前者只影响本进程当前用哪几个出口(几秒内就会 补货),后者改的是池里那条代理的可用状态(持久)。前者快而轻,后者慢而重。

它刻意只动「是否可用」与「总检查次数」两个字段,不碰失败次数、验证时间、探测计数与 健康分 —— 那些是自动清理的判据,一旦被运行期的偶发失败污染,代理会被不可逆地删掉; 总检查次数清零正是为了把这条记录重新排进重验证候选 —— 重验证器会把检查计数为 0 的记录 收进候选,不受 auto_revalidation.only_valid_proxies 影响。检测分不清「代理坏了」和 「我们自己网络抖动」,所以反馈只做温和的降级。

因此:被使用反馈标记失效的代理会被列入下一轮自动重验证的候选,重验证测通后可用状态 自动翻回来;不想等的话,也可以在面板的「代理管理」里点「验证全部失效」立刻手动重验。

抓取插件

把插件文件放进 modules/proxypool/plugins/ 后,重启池服务生效(目录只在池服务启动时扫描 一次);已在列表中的插件改了代码,可在面板「代理池 → 代理管理」的插件列表里点「重载」热加载。 两条最容易踩的约定:

  • 抓取失败必须抛出异常。返回空列表会被当作「抓取成功但这一轮没抓到」,于是错误被吞掉、 调度周期照常推进,表现为永远抓不到东西。
  • 插件的文件名(不含扩展名)就是它的标识,配置、调度与来源记录都用这个名字。

完整的插件编写契约见 modules/proxypool/README.md。

对外网络请求

ProxyCat 会在下列场景主动向本机之外发起连接,供你核对「连去哪里、什么时候连、怎么关掉」—— 表中标直连的行为不经任何代理、以本机公网 IP 直接发出,标「经代理」的由出口池里的上游出口代为访问。

触发时机 目标地址 直连还是经代理 相关配置项 如何关闭
版本检查:后台线程从 GitHub 仓库的发布记录取版本号做更新提示,结果落盘 logs/version_check.json;并发请求三个内置地址、谁先返回可用的版本号就用谁(官方发布源无认证无限流;镜像国内可达性更好,但会看到请求 IP 且可能返回缓存的旧版本;官方 API 未认证时每 IP 每小时限 60 次),全失败约 8 秒返回。启动时(app.py 与 ProxyCat.py 两个入口都会拉起该线程)距上次检查尝试满 24 小时、从未查过、或记录版本与当前版本不符即立即查一次,此后每 600 秒判断一次、至多每 24 小时发一次;24 小时按「上次检查尝试」记账,失败也算时间。面板侧边栏的刷新按钮或 POST /api/version/check 可强制立即检查一次,同样会重置 24 小时计时 github.com(releases.atom)、gh-proxy.com、api.github.com(后两个是镜像与官方 API) 直连 version_check_url([Server],面板「代理配置 → 高级选项」):留空用内置三个地址,填入后只用该地址,可指向自有镜像 填一个自有地址把内置的第三方镜像换掉;要彻底不发就指向一个不可达地址并在网络层屏蔽,或停止进程。该请求走 httpx 默认客户端,系统设了 HTTP(S)_PROXY 环境变量时会改走环境代理
GeoNode 抓取插件分页抓公开代理列表(每页 500 条、最多 20 页):池启动后插件默认启用、尚无下次执行时间即抓一轮;此后每 60 秒扫描一次、按插件周期执行(新插件默认 60 分钟),失败按默认重试 2 次、基础等待 10 秒退避;面板插件列表里点「抓取」可立即触发 proxylist.geonode.com(硬编码) 直连 启停与周期存在池数据库(面板「代理池 → 代理管理」的插件列表);全局默认 plugins.scan_interval_seconds=60、plugins.default_interval_minutes=60、plugins.execution_timeout_seconds=300、plugins.max_retries_on_failure=2、plugins.retry_base_delay_seconds=10.0([Pool]) 面板插件列表里停用 geonode_plugin(写进池数据库、重启仍生效),或停止池服务;抓取地址硬编码,改地址需改代码
GitHub 抓取插件并发抓 12 个公开代理清单(6 个 http、5 个 socks5、1 个 https):与 GeoNode 插件共用同一套调度,池启动首次加载即抓一轮,之后默认每 60 分钟;面板插件列表里点「抓取」也可立即触发 raw.githubusercontent.com(12 个硬编码清单地址) 直连 同 plugins.*([Pool]);启停与周期存在池数据库 面板插件列表里停用 github_proxy_plugin,或停止池服务;地址硬编码,改地址需改代码
代理验证前的端点直连预筛:每次验证前不带任何代理直连探测各检测端点是否可达(本机 IP 直接对这些站点可见),结果缓存 300 秒;对不在缓存里的单个地址(如插件专属 test_url)随时补探一次 test_url(默认 https://www.baidu.com)、validator.identity_echo_apis(默认 httpbin.org、httpbingo.org、eu.httpbin.org)及其明文派生地址、validator.plain_identity_echo_apis(默认 postman-echo.com)、3 个硬编码连通性地址(华为云 / Cloudflare / Firefox 的固定连通性探测地址);明文一组仅 check_anonymity=true 时探测 直连 test_url([Server],派生 validator.target_url)、validator.identity_echo_apis、validator.plain_identity_echo_apis、validator.check_anonymity([Pool]) 无法整体关闭(是验证流程的一部分);可把 test_url 与两个端点列表换成自有地址;check_anonymity=false 可去掉明文与 3 个硬编码地址,但 test_url 与身份回显列表仍会被直连探测;清空 identity_echo_apis 会回落到内置默认(httpbin.org、httpbingo.org),不是关闭手段
本机公网出口 IP 查询:check_anonymity=true 且 validator.host_public_ip 留空时,直连 IP 查询站点取本机公网 IP(IPv4 / IPv6 两族各自逐个试端点),作为「透明代理」匿名度判定的对比基准;1800 秒(半小时)内只查一次,成功与失败都重新计时 validator.ip_check_apis 的地址(默认 9 个 IP 回显站点:icanhazip.com、checkip.amazonaws.com、api.ip.sb、realip.cc、httpbin.org、api.ipify.org、ipinfo.io、ipwho.is、www.cloudflare.com) 直连 validator.host_public_ip、validator.check_anonymity、validator.ip_check_apis([Pool]) 在池设置填上 host_public_ip(双栈逗号分隔;填了非法值会被当未配置、继续外查)→ 完全不发;或 check_anonymity=false;或清空 ip_check_apis
SOCKS5 出口存活检测:本机直接 TCP 连候选出口的 host:port,做 SOCKS5 握手(带认证则先认证),再把 test_url 的域名交给它 CONNECT、不发送 HTTP 数据。触发分四路:local 来源且 check_proxies_on_startup=true 的启动预检;候补进名册前 check_proxies_on_use=true 的逐条校验(三种来源都生效);出口疑似失效、退休与静默隧道的确认探测(不受两个开关约束);面板「检测代理」。模块内另有 10 秒结果缓存 SOCKS5 候选出口的 host:port(来自本地 ip.txt / api 取货 / 池 / 当前名册);CONNECT 目标是 test_url 的域名 直连 test_url、check_proxies_on_startup、check_proxies_on_use([Server]) 两个开关置 false 且不点面板检测按钮,可去掉启动预检与入池校验;失效确认探测仍会发(只在出现失败出口时触发);要全停就停止代理服务
入库前 TCP 连通性初筛:插件抓取入库、面板「导入」入库时,对每个候选代理的 ip:port 发一次 TCP 连接(只验端口是否可连),淘汰连不上的;插件开了 skip_validation 时这步与后续验证一起跳过、直接入库 候选代理的 host:port(来自抓取清单或导入内容,遍布公网) 直连 无独立开关;插件级 skip_validation 同时跳过初筛与验证(插件设置存在池数据库、面板可改) 禁用抓取插件、不导入即可不发生;或给插件开 skip_validation(代价是跳过验证直接入库)
api 来源取货:proxy_source_mode=api 且出口池需要取货时(名册为空、有近期流量或 continuous 模式时的后台补货循环、面板「刷新出口」、高峰扩容),按 api_proxy_url 发一次 GET,响应按行解析成上游代理地址(超时 15 秒、单批上限 500 条);出厂配置为空,此时 api 模式直接报配置错、不发请求 api_proxy_url(出厂为空;配置里连 [Server] 段都没有时回落到内置默认 example.com/getip) 直连 api_proxy_url([Server]);proxy_username / proxy_password 只拼在取回的代理地址上,不用于这次请求 把 api_proxy_url 留空(api 模式报配置错、不联网),或把 proxy_source_mode 改成 local / pool
远程代理池取货:proxy_source_mode=pool 且 pool_remote_url 非空时,出口池需要补货 / 轮换就 GET 一次外部池的取用接口,响应按行当代理地址;留空则不发,改用进程内代理池(它自己会跑抓取插件与验证,见其它行) pool_remote_url(出厂为空) 直连(httpx,10 秒超时,显式不走环境代理) pool_remote_url([Server]) 清空 pool_remote_url(回落到进程内代理池)
离线归属地库下载:仅手动触发(面板「数据库维护」页的「更新归属地库」,或命令行运行 geodb;没有自动更新),从 CDN 下载 GeoLite2-City.mmdb.gz 与 qqwry.ipdb 并换入 cdn.jsdelivr.net(硬编码) 直连(requests 默认客户端,会遵循 HTTP(S)_PROXY 环境变量) 无(地址硬编码、无自动触发路径) 不点「更新归属地库」就不会发;注意「全面检测」不是下载(它做的是全量代理验证),「重算归属地」纯离线不联网
绕过名单目标直连:客户端经本机代理端口发请求、目标 host 命中绕过名单(支持通配符)时,不经任何代理、以本机 IP 直连目标;明文 HTTP、CONNECT 隧道、SOCKS5 三条路径都有这个分支 客户端指定的目标地址(本机出口 IP 对目标可见) 直连 bypass_whitelist_file(默认 config/bypass_whitelist.txt,当前为空文件) 清空绕过名单里的条目(全部走上游出口);或不给客户端配置本代理
面板广告位:浏览器打开面板时加载广告图、点击广告链接时跳转(由用户浏览器发起,ProxyCat 进程不发请求) config/ads/*.json 的 image_url / link_url:当前启用的两份都指向 github.com 链接,其中之一另加载 camo.githubusercontent.com(GitHub 的图片代理) 直连(浏览器侧) 无配置键;改 config/ads/*.json 的 enabled / image_url / link_url 删除广告文件,或把对应 json 的 enabled 改为 false;image_url 置空则该条不加载图片
HTTP / HTTPS 出口存活检测:经被测出口访问 test_url(https 失败时自动降级为明文 http 再试一次,仍经同一出口)。触发与 SOCKS5 存活检测相同:启动预检;候补进名册前校验;失效、退休与静默隧道的确认探测(不受两个开关约束);面板「检测代理」逐条检测源清单。模块内另有 10 秒结果缓存 test_url(默认 https://www.baidu.com) 经代理 test_url、check_proxies_on_startup、check_proxies_on_use、proxy_check_ttl、check_cooldown([Server]) 两个开关置 false 且不点面板检测按钮,可去掉启动预检与入池校验;失效确认探测仍会发;test_url 换成自有可控地址可缩小影响;停止代理服务则全停
单代理质量验证的经代理探测组:穿候选 / 在用出口发存活(test_url)、身份回显、明文回显与明文连通性探测,产出出口 IP、匿名度与协议能力。触发:插件入库 / 面板导入(full);面板批量验证(「验证选中的」可指定档位、auto 按策略)与「全面检测」(full);自动重验证(默认每 360 分钟一轮、启动立即跑首轮;缺质量评估记录或距上次完整评估超过 1440 分钟的轮次按 full、缺真实 IP / 匿名度按 identity、其余只发存活) 经被测出口访问 test_url、validator.identity_echo_apis、validator.ip_check_apis、validator.plain_identity_echo_apis 与 3 个固定明文探测地址(身份回显在 full / identity 档发,明文一组需开启匿名度检测,3 个固定地址仅在 full 且 test_url 非 http 时发;各默认值见 [Pool] 相关键) 经代理 validator.target_url(由 test_url 派生)、validator.identity_echo_apis、validator.ip_check_apis、validator.plain_identity_echo_apis、validator.check_anonymity、validator.timeout_seconds、validator.connect_timeout_seconds([Pool]);auto_revalidation.enabled / interval_minutes / run_on_start / full_recheck_interval_minutes auto_revalidation.enabled=false 停定时任务;插件 skip_validation 跳过入库验证;不点批量验证按钮;但池一旦运行、只要发生验证,这一组无法单键全关——可把端点列表换成自有地址
api 来源出口真实 IP 探测:proxy_source_mode=api 且 access_records_real_ip_probe=true 时,后台补货循环每 1 秒扫描名册,对没有真实 IP 记录(或记录早于入池时间、失败已满 600 秒)的出口经它自己补探(不限于 config.ini 注释所写的「出口地址是网关时」,所有未探明真实 IP 的 api 出口都会探;每次最多新开 3 个探测、单请求 5 秒超时) 经该出口访问 validator.ip_check_apis 里的 https 地址(逐个试到拿到 200 并解析出 IP) 经代理 access_records_real_ip_probe(本仓库 config.ini 为 true,代码内置默认 false) 置 false;或不用 api 来源(local / pool 不发)
客户端流量转发(本职功能):客户端经本机代理端口(默认 1080)发来的 HTTP / HTTPS(CONNECT)/ SOCKS5 请求转给当前选定的上游出口,由上游访问目标;失败换出口重试(连接阶段最多 2 次),名册为空时先在请求路径取货 客户端指定的目标地址;中转站是当前选定的上游出口(来自本地文件 / api 取货 / 池) 经代理 无开关(核心功能);出口来源由 proxy_source_mode 决定;bypass_whitelist_file 决定哪些目标改走直连(见上一条) 停止代理服务(面板停止或退出程序);不给客户端配置本代理;经上游的转发是本职功能,无法关闭

表中会暴露本机公网 IP 的是标直连的行:版本检查、两个抓取插件、验证端点预筛、本机公网 IP 查询、SOCKS5 存活探测、入库 TCP 初筛、api 取货、外部池取货、归属地库下载、绕过名单直连,以及由浏览器侧发出的面板广告位(对目标站点暴露浏览器所在网络的公网 IP)。标「经代理」的行由上游出口代为连接,目标站点看到的是出口 IP 而不是本机 IP —— 客户端流量转发本身就是让上游替你访问目标。想让某一条停下来的最小操作见「如何关闭」列。

性能与基准

转发通路的吞吐表现:

性能测试

这张图是用 Yakit 经 http://neko:[email protected]:1080 压测的结果:3000 次请求、 500 并发,成功 3000 / 失败 0,全部 200,单次延迟约 550-670ms。图中那个 用户名:密码@主机:端口 的地址形式,也顺带演示了出口认证的用法(对应 [Users] 段的账号)。

这张图衡量的是 ProxyCat 自身的转发链路,不是你的上游代理质量 —— 上游慢,经 ProxyCat 一样慢。上游质量请在面板的「运行日志 → 访问统计」里按上游看成功率。

本机回环基准(同一台机器上的实测值):

场景 数字
并发空隧道(2000 条) 每连接 RSS 约 148 KB;空转 CPU < 0.5%
单隧道吞吐 约 520 MB/s(回环)
小报文往返 p50 0.27 ms / p95 0.47 ms
完整建链(CONNECT → 上游 200) 约 380 conn/s(出口写成域名、且域名第一个地址不通时的实测值;出口是 IP 字面量时约 1100 conn/s)

数字随机器变化,只用于同一台机器上的前后对比;跨机器比这些数字没有意义。

一处已知的平台成本:Windows 的 Proactor 事件循环给每个 socket 固定分配 64 KiB 读缓存(CPython 写死,应用层改不了),一条隧道两条 socket 就是约 128 KB 用户态缓冲。 换算到 2 万条隧道约 2.5 GB —— 这是「海量并发」在本平台上的主要内存来源。

项目结构

ProxyCat/
├── ProxyCat.py                 命令行入口:拉起出口服务与代理池
├── app.py                      Web 入口:面板 + 出口服务 + 代理池(Docker 的 CMD)
├── README.md / README-EN.md    中英文 README
├── API.md / API-EN.md          完整接口文档(面板 /api/* 与池 /api/pool/*)
├── config/
│   ├── config.ini              全部配置(含中文行内注释)
│   ├── getip.py                代理来源为 api 时的取址逻辑,可按接口格式修改
│   ├── ip.txt                  local 模式的代理列表
│   ├── ads/                    面板广告位数据
│   └── *.txt                   IP 黑白名单与绕过名单
├── modules/
│   ├── proxyserver.py          出口代理服务:入站解析、轮换、转发
│   ├── proxy_workers.py        出口名册:挑选、并发额度、失败滑窗与候补队列
│   ├── proxypool_service.py    代理池在本进程内的宿主(异步核心 + 同步门面)
│   ├── proxypool_api.py        代理池的 Web 适配层(Flask 蓝图)
│   ├── pool_config_ini.py      [Pool] 段与配置对象树之间的映射
│   ├── modules.py              宿主公共基础层:配置、国际化、启动横幅、代理检测
│   ├── logging_setup.py        日志装配与分类路由
│   ├── access_log.py           单次请求的记录与计时
│   ├── access_records.py       访问记录(逐条流水)
│   ├── domain_stats.py         域名统计(聚合计数)
│   ├── spool_store.py          「内存缓冲 + 后台批量落库」的共用骨架
│   ├── config_validation.py    [Server] 段的校验与规范化
│   ├── loop_noise_filter.py    Windows 上 asyncio 收尾噪声的抑制
│   ├── version_check.py        版本检查的调度与状态持久化
│   └── proxypool/              代理池子系统(见其 README.md / README-EN.md)
├── web/
│   ├── templates/index.html    面板单页
│   └── static/                 前端资源(含本地化的 jQuery 与 Font Awesome)
├── ProxyCat-Manual/            详细文档
│   ├── Features.md / Features-EN.md                    功能特性(中 / 英)
│   ├── Configuration.md / Configuration-EN.md          详细配置(中 / 英)
│   ├── Operation Manual.md / Operation Manual-EN.md    使用手册(中 / 英)
│   ├── Investigation Manual.md / Investigation Manual-EN.md  报错排查(中 / 英)
│   └── logs.md / logs-EN.md                            更新日志(中 / 英)
├── assets/                     README 与手册用图(面板截图在 assets/screenshots/)
└── LICENSE                     GPL-2.0