dsh-proxy-egress
把「只接受无凭据代理地址的消费方」接到「需要账号密码的上游代理」上。
消费方 ──► http://127.0.0.1:8899 ──► 本插件(注入 Proxy-Authorization)──► 上游代理 ──► 目标
(无认证,消费方只填这个) (账号密码只在这里出现一次)
MIT · 独立社区插件 · 与 DeepSeek、DeepSeek Harness 无隶属关系,亦未获其赞助或背书 安全与权限 · 卸载与残留
中文 | English
这是什么,适不适合你
有些消费方只接受一个不带账号密码的代理地址,而你的上游代理偏偏要认证。本插件站在中间:消费方连本地一个无凭据地址,账号密码由插件在上游那一跳注入,只出现一次。
典型情境:你在企业出口后面,公司代理要账号密码;Harness 里某个插件只让你填 http://host:port,没有用户名密码字段。把那个字段填成 http://127.0.0.1:8899,认证交给本插件。
它不做什么:不做系统级全局代理,不做多上游轮换,不做流量审计,也不支持 SOCKS5(写了会明确报错)。
它的两个硬特征:零构建、零运行时依赖(plugin-src/ 即真源);凭据不进模型通道(工具只查状态、只跑验证,回传里只有指纹——sha256 前 8 位)。
它能保证什么,不能保证什么
| 层 | 内容 | 可证明性 |
|---|---|---|
| L1 代理层 | 进入本地监听的每一条连接只经上游转发;失败不创建到目标的直连 | 代码级,可测(scripts/smoke.mjs + scripts/faultcheck.mjs) |
| L2 消费方层 | 消费方是否真的把请求交给本监听、失败后是否自己 fallback | 本插件管不了,只能取证 |
| L3 系统层 | 整机强制出口 | 未实现、未验收。需要防火墙白名单或独立网络环境 |
说人话:本插件保证「进来的一定走代理」,不保证「该进来的都进来了」。
同一条纪律的另一种说法:失败即断联——上游不通、认证失败、被拒、超时,一律关闭连接并给出分类原因。没有直连降级,没有兜底重试,代码里不存在可以打开直连的开关(配置里写 allowDirectFallback 会直接报错)。
安装前准备
| 要求 | 取值 |
|---|---|
| 宿主 | 任一可用 DSH profile(下文以 web 为例) |
| Node.js | >=22.19(package.json 的 engines) |
| 上游代理 | 一个支持 CONNECT 的 HTTP 代理,且你持有账号密码 |
| 本地端口 | 默认 127.0.0.1:8899,被占用时不会自增换端口(见 V6) |
校验规则(不满足就不启动,不监听任何端口)
| 编号 | 规则 |
|---|---|
| V1 | upstream.host 不能含端口、不能带协议前缀;IPv6 必须写 [地址] |
| V2 | username / password 必须同时提供或同时留空 |
| V3 | 任何输出(状态、日志、错误)都不含密码,也不含完整账号 |
| V4 | 域名做 IDN → punycode 归一 |
| V5 | 配置变了,旧的检查结果要作废 |
| V6 | 校验不过就不启动;端口被占用也不自增换端口 |
安装
本插件是一个 DSH bundle(package.json 声明了 dsh.bundle.patch)。
A · 从 npm 安装(推荐)
dsh plugin --profile web add dsh-proxy-egress
一条命令做两件事:装包,并自动把它登记进 dsh.profile.bundles。DSH 装完会回读新增依赖的 dsh.bundle 声明再行登记;没有该声明的包只当普通依赖装上,终端会给出 installed as a plain dependency, not a profile layer 的警告。把 web 换成你自己的 profile 名。
命令里写包名而不写版本号,所以发版不需要改这份文档。
B · 从源码安装(改造或调试时用)
- 把本仓库放到本机任意目录。
- profile 的
package.json:dependencies加"dsh-proxy-egress": "link:<本仓库绝对路径>",并在dsh.profile.bundles里加"dsh-proxy-egress"。 - 装出符号链接:
dsh plugin --profile web install。(bundles是手改的,但链接不装出来,加载器就解析不到这个包。)
两种方式共同的收尾
- 在 profile 的
cordis.patch.yml加同 id 覆盖行(见设置面板 → 配置覆盖行)。包自带的 patch 里上游主机与凭据路径是占位符——不写覆盖行插件不会启动(但四个工具仍可用,proxy_egress_status会把缺什么直接列出来)。 - 重启前预检:
dsh --profile web --dump-config应当只看到一行proxy-egress,且带patched by …cordis.patch.yml。这一步能验证 YAML 语法与包解析,不用等重启才发现。 - 重启宿主,然后问模型
proxy_egress_status。
不要手写
$DSH_HOME下的插件包,也不要在 profile 目录里跑 pnpm —— 那是 install_bundle 的活。
想撤掉它:见更新、回滚、卸载。卸载不等于清凭据文件与设置文件。
快速开始:跑通一次真实出口
装好不等于能用。下面四步的落点是一次可验证的出口请求——诊断通过、面板显示 listening 都只说明进程起来了。
- 给什么输入 —— 上游协议/主机/端口、账号密码,以及你想用的本地端口。
- 做什么操作 —— 在 设置 → ANB 工具 → 出口代理 里填上游并录入凭据(面板是唯一合法入口,见设置面板);保存。然后把消费方的代理地址填成
http://127.0.0.1:8899——不带任何账号密码。 - 结果出现在哪 —— 面板显示监听状态与上游(脱敏);工具
proxy_egress_status里proxy.state为listening、connections.total随请求增长。 - 怎样判断符合预期 —— 跑
proxy_egress_selfcheck:它经本地监听 → 上游 → 回显服务量一次实际出口 IP。返回OK即通;EGRESS_IP_MISMATCH说明真实出口不是expectedEgressIps里的那个;UPSTREAM_AUTH_FAILED说明账号密码不对。看到OK才算成功。
日常使用
主线,端到端。 消费方填本地无凭据地址 → 请求进本地监听 → 插件注入上游认证 → 上游转发 → 响应回到消费方。
第二次及以后的最短路径。 什么都不用做——配置与凭据持久化,重启后自动监听。想确认它还活着,跑一次 proxy_egress_status。
会改变状态的三件事:
- 改上游或改密码 → 保存后新连接即用新配置;已建立的隧道不受影响。
- 上游不通 → 连接直接失败断开,不降级直连。这是设计取舍,不是故障。
- 进入阻断态(出口 IP 不匹配且
egressMismatchAction: block)→ 新连接被拒,已建立的隧道不被中断(原因见必须说清楚的限制第 3 条)。
设置面板
装好后在 设置 → ANB 工具 → 出口代理 有一个面板(零构建客户端,plugin-src/client/client.js)。
「ANB 工具」是本插件自带的栏目,不是需要另装的东西。只装这一个插件时栏目里只有一个标签(选中态),不会出现空壳;以后装更多 ANB 系列插件,它们出现在同一栏目的标签行里。
| 分区 | 作用 |
|---|---|
| 状态 | 监听、上游(脱敏)、凭据是否已配置(只给指纹)、连接计数、最近失败 |
| 验证 | 代理链路检查 / 故障注入检查 / 重启 |
| 上游与监听 | 协议、主机、端口、本地端口、期望出口 IP、不匹配时 warn/block |
| 凭据 | 录入账号密码(宿主写进凭据文件并重启代理);清除(删凭据文件,上游要认证时代理会停止,这是预期状态) |
三条设计约束,读者需要知道:
- 面板是全项目唯一合法的凭据写入口。 模型工具永远收不到密码——工具参数会进对话记录、调用日志与宿主审计;面板走本机 HTTP,只在人的通道里。
- 凭据只写不读。 保存后输入框清空,界面只显示指纹。
- 面板的改动存在插件自己的状态文件
$DSH_HOME/proxy-egress/settings.json,不写进宿主的启动配置——面板写启动配置,一次手误就是宿主起不来,而纠正它的入口恰恰是这个面板。两个来源并存时,面板会标出每个字段当前来自「面板」还是「配置文件」。
配置覆盖行
# ⚠️ 用「同 id 顶层覆盖」,不要写 `- insert:`。
# 本包自带的 patch 已经插入过这一行;再 insert 一次会变成两个同 id 的行,
# 两个实例抢同一个端口(症状是 EADDRINUSE)。
- id: proxy-egress
config:
listen: { host: '127.0.0.1', port: 8899 } # 只允许回环
upstream:
type: 'http' # http | https
host: '203.0.113.9' # 只写主机名/IP,端口另写
port: 8080
credentialSource: 'file'
credentialFile: '<凭据文件绝对路径>'
allowConnectPorts: [443]
expectedEgressIps: ['203.0.113.9']
egressMismatchAction: 'warn' # warn | block
selfCheck:
enabled: true
url: 'https://api.ipify.org?format=json'
凭据文件(UTF-8):
{ "username": "<账号>", "password": "<密码>" }
出问题时怎么办
服务端失败时回 X-Egress-Reason 头,按下表定位:
X-Egress-Reason |
含义 | 下一步 |
|---|---|---|
UPSTREAM_UNREACHABLE |
连不上上游(网络/端口/超时) | 核对主机端口;确认本机能出网到该地址 |
UPSTREAM_AUTH_FAILED |
上游回 401 或 407:账号密码不对,或凭据改过但没重启 | 重录凭据并重启代理,再跑自检 |
UPSTREAM_REJECTED |
上游明确拒绝该目标 | 确认上游是否允许访问这个目标 |
UPSTREAM_MALFORMED_RESPONSE |
上游响应解析不出来(按失败处理,不当成功) | 检查上游是否为兼容的 HTTP 代理 |
TUNNEL_HANDSHAKE_TIMEOUT |
上游接受了连接但不说话 | 排查上游负载或链路 |
HANDSHAKE_HEADER_TOO_LARGE |
上游响应头超过上限 | 上游返回异常,换上游或修上游 |
CONNECT_PORT_NOT_ALLOWED |
目标端口不在白名单 | 需要就加进 allowConnectPorts |
PROXY_BLOCKED |
处于阻断态(出口 IP 不匹配等) | 查 proxy_egress_status 的 blocked 字段 |
EGRESS_IP_MISMATCH |
观测出口不在 expectedEgressIps |
先确认真实出口,再改期望值 |
CHECK_SERVICE_UNAVAILABLE |
回显服务本身不可用(不代表代理坏) | 换一个自检 URL 再试 |
别把自检当成一切:proxy_egress_selfcheck 的分类结果几乎能直接定位问题,值得贴进 issue——但请先删掉上游凭据、抹掉真实出口 IP(那是你这个网络的身份信息)。
更新、回滚、卸载
| 动作 | 怎么做 | 你的数据会怎样 |
|---|---|---|
| 更新 | 同一条 dsh plugin --profile web add dsh-proxy-egress 重装 |
设置与凭据保留 |
| 回滚 | 装回上一个精确版本 | 设置保留;旧版本不认识的字段被忽略,不会被删 |
| 卸载 | dsh plugin --profile web remove dsh-proxy-egress |
插件代码移除;凭据文件与 settings.json 保留 |
| 清理设置 | 手动删除 $DSH_HOME/proxy-egress/settings.json |
不可恢复 |
| 清理凭据 | 手动删除你配置的凭据文件 | 不可恢复;删前确认没有别的插件在用它 |
卸载与残留什么
卸载不是清空配置:凭据文件与状态文件仍留在磁盘上,因为移除一个插件不该静默销毁你可能还需要的数据。要一起清掉,见上表"清理"两行。
数据、权限与隐私
- 读什么 —— 插件配置(上游地址与端口、端口白名单、期望出口 IP、自检 URL);你指定凭据文件里的上游账号密码。
- 是否外发及目标域名 —— 只向你配置的上游代理地址转发;自检时另向
selfCheck.url(默认https://api.ipify.org?format=json)请求一次。不向本项目维护者发送任何数据,没有遥测端点。 - 是否落盘及位置 —— 面板改动在
$DSH_HOME/proxy-egress/settings.json;覆盖行在 profile 的cordis.patch.yml;凭据在你配置的文件里。除此之外不写别处。 - 谁可读写 —— 只有运行 DSH 的那个操作系统账户。请把凭据文件当密码对待。
- 关键默认开关 ——
listen默认127.0.0.1:8899(只允许回环);allowConnectPorts默认只放 443;egressMismatchAction默认warn;上游认证始终注入,没有"关闭认证"开关。 - 凭据存哪、是否进日志 —— 只在凭据文件里;状态输出只给指纹与来源类型,不打印密码,也不含完整账号(V3)。
兼容性:要求、已验证、仍未确认
- 要求 —— Node
>=22.19;DSH 提供dsh-client-ui-slots、dsh-client-connection、dsh-client-ui-settings-general三个注入点(package.json的dsh.client.inject)。 - 已验证 —— 本机在当前 DSH 版本上加载、面板出图、
proxy_egress_selfcheck返回OK(2026-10-12,出口 IP 与expectedEgressIps一致)。 - 仍未确认 —— 跨 DSH 版本矩阵未系统验证;
http上游在公网链路下的账号安全(见限制第 4 条);SOCKS5 未实现。
验证与开发
npm test # 纯逻辑 + 沙箱里真跑客户端面板(不需要宿主、不联网)
npm run smoke # 离线端到端(假上游 + 假目标)
npm run faultcheck # 证明上游坏掉时不会直连
npm run verify # 发布契约断言(入口存在、白名单自洽、无安装期脚本…)
npm run scan # 公开前脱敏扫描(词表由 DSH_PRIVATE_TERMS 挂入,未设置则跳过)
npm run check # 上面全部串起来跑;prepublishOnly 用的就是它
npm test 覆盖六个层次:
| 文件 | 守什么 |
|---|---|
test/parse.test.mjs |
IPv6 三分法、CONNECT 状态行、Basic、逐跳头剥离 |
test/config.test.mjs |
V1–V6、直连降级开关被拒、端口白名单非空 |
test/credentials.test.mjs |
凭据文件只有两个键、密码逐字、BOM 容忍 |
test/overrides.test.mjs |
改端口不能把上游账号搞丢、白名单外的键不生效、来源标注 |
test/rpc.test.mjs |
面板 RPC 信封、错误分级、业务 ok:false 不走错误通道 |
test/client.test.mjs |
客户端文件能在沙箱里执行、只 require react、面板挂载与首帧渲染 |
smoke.mjs 用假上游代理 + TCP 回声目标跑六件事:
- 正常隧道建立、认证头由本插件注入
- 上游 407 →
502 UPSTREAM_AUTH_FAILED,目标零连接 - 上游不可达 →
502 UPSTREAM_UNREACHABLE,目标零连接 - 非白名单端口 →
403 CONNECT_PORT_NOT_ALLOWED - 普通 HTTP 走绝对形式,业务状态码原样透传(429 不被改成 502)
- 停止后端口释放(再连是
ECONNREFUSED)
装到宿主后还有两个模型工具:proxy_egress_selfcheck(量实际出口 IP)、proxy_egress_faultcheck(独立实例 + 诱饵端口,验证不直连)。
必须说清楚的限制
- 一次检查不能替代持续约束。 自检只证明"那一刻、那条路径"。
- 回显服务只观测它自己的出口。 上游若按目标分流,不能推论模型 API 走同一出口。
- 阻断态不中断已建立的隧道。 隧道里的字节已经看不到边界;要更强的效果只能用系统隔离。
- HTTP 上游的 Basic 认证只是 Base64。 上游若在公网,账号密码在链路上可被还原。走公网请优先
type: 'https'。 - SOCKS5 尚未实现。 写了会明确报错,不会静默忽略。
反馈
遇到问题或想要新功能,提一个 issue。
报问题时写上 DSH 版本和插件版本(版本看包清单或 CHANGELOG.md,本文不重复记录),修起来最快。贴 proxy_egress_selfcheck 结果前请删掉上游凭据、抹掉真实出口 IP。
许可
MIT,全文见 LICENSE。作者 yiyunet。与 DeepSeek、DeepSeek Harness 无隶属关系,亦未获其赞助或背书。
No comments yet. Be the first to write one.