DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Tailnet Gateway — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins
T

dsh-tailnet-gateway

Tailnet Gateway

从您自己的 Tailscale 设备访问绑定到回环地址的 DeepSeek Harness,并由 tailscaled 的身份标记进行访问控制。无依赖插件;仅绑定回环地址。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:OutLawZhangSan-liii/dsh-tailnet-gateway#f86368149dd81b521033a9df972a346ed10965e8
README兼容性版本

兼容性与来源证明

Tailnet Gateway 以 dsh-tailnet-gateway 发布,当前版本为 0.1.0。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
any
发布来源
github
Registry 更新时间
2026/9/28

版本

0.1.0stable
2026/9/28

相关插件

正在加载相关插件…

最新版
0.1.0
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
any
许可证
MIT
发布源
github
GitHub
★ 0
周下载
0
最近提交
2026/9/28
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 security-access 分类下经过校验的插件。

Cc Safety Netcc-safety-net编码代理 CLI 钩子——阻止破坏性命令和访问机密文件Doctor@linxin666/dsh-doctorDSH 配置档案的事务性救援模式,配备受监督的启动器、隔离的恢复容器、确定性修复、健康监控以及本地 Web 恢复控制台Mobiledsh-mobileDeepSeek Harness 移动端适配与安全访问插件,支持局域网、远程连接、Android App 和手机浏览器。Codex Subscriptiondsh-codex-subscription通过 OAuth 在 DeepSeek Harness 中使用 ChatGPT 和 Codex 订阅,支持配额管理、安全重置、网页搜索、图像和快速模式

README

dsh-tailnet-gateway

Control the DeepSeek Harness running on your own computer from your phone — or any other device on your private Tailscale network — without exposing dsh, your LAN, or the public internet.

This repository contains two cooperating pieces:

PieceWhat it is
lib/index.jsA DSH plugin: an authenticated reverse proxy that publishes a loopback-bound dsh to your tailnet
grant-access.mjsA companion tool that authorizes a browser you cannot hand a cookie to by hand

Developed and verified against dsh 0.1.5-rc.1 (Web profile, Windows).


The control logic

The whole design follows from one fact about dsh: its browser surface is loopback-only on purpose, and it defends that with two separate fences.

FenceWhat it guardsCan --trusted-host open it?
Request trust (dsh-client-connection)The Host header must be loopback or a configured trusted authority. Blocks DNS rebinding and cross-site calls.Yes
Privileged methodssettings.*, credentials.*, agentPreset.*, host.pickDirectory, llm.discoverModels are served only to loopback callers, and that branch re-runs the check with an empty trust list.No

So the obvious approaches fail in a specific way:

  • dsh web --host 0.0.0.0 is refused outright. Its own error text says it "would expose remote code execution to the network".
  • Binding wider plus --trusted-host still leaves a half-working app: chat works, but Settings/Models/Plugins report "unavailable in this browser", because dsh-client-ui-settings picks its store via ctx.remote.$host.isLoopback ? "host" : "memory", and "memory" publishes an "unavailable" status without ever issuing an RPC.

This plugin does not widen either fence. It puts a second front door in front of them.

The link chain

   phone (Tailscale app)
      │
      │  1. WireGuard, end-to-end encrypted, tailnet members only
      ▼
   tailscaled  ──  2. terminates TLS for the *.ts.net name
      │            3. stamps Tailscale-User-Login + X-Forwarded-For,
      │               OVERWRITING whatever the client sent
      ▼
   tailscale serve :443  ──  4. published with: tailscale serve --bg 7242
      │
      ▼
   gateway  127.0.0.1:7242   ──  5. this plugin. Two gates:
      │                             - the stamped login must be allowed
      │                             - optional per-device tailnet IP allowlist
      │                         6. rewrites Host/Origin/Referer to the
      │                            loopback authority, then forwards
      ▼
   dsh web  127.0.0.1:3080   ──  7. sees a genuine loopback call from this
                                     machine, so the privileged fence is
                                     satisfied honestly — not bypassed

Three properties make this sound rather than clever:

  1. The identity is evidence, not a claim. tailscaled overwrites the identity headers on every proxied request, so a tailnet peer cannot name itself. A request arriving without them is refused.
  2. Nothing is exposed. Both listeners stay on 127.0.0.1. The gateway's bind address is clamped to loopback in two places, because a gateway reachable from a network interface would hand loopback trust to anyone who asked — and a direct caller can forge those headers.
  3. A refused request never reaches dsh — not even as a probe. The decision happens before any upstream connection is opened.

Security model

Load-bearing behaviors, each pinned by a test in test/gateway.test.js:

  • Loopback-clamped bind: resolveConfig clamps, startGateway throws.
  • Fail-closed admission: a missing identity header is a refusal, not a pass; a peer allowlist that cannot be evaluated refuses.
  • X-Forwarded-For is read from the first entry (the original client). Reading the last would let a client that can set the header choose its identity.
  • Host, Origin and Referer are rewritten to the loopback authority. Left as the tailnet URL, dsh reads the call as cross-origin and rejects it.
  • The WebSocket event stream is gated identically on the upgrade path.
  • close() settles with tunnels open. Node's closeAllConnections() does not include upgraded sockets, so both ends of every tunnel are destroyed explicitly.

What this does NOT protect against

  • A local process on the machine. It can reach 127.0.0.1:7242 directly and forge the identity headers, because it never passes through tailscaled. This is inherent to any loopback-trusting hop and is exactly why the loopback clamp is load-bearing. Such a process could already reach dsh on 127.0.0.1:3080, but note the honest difference: dsh's own privileged fence would still refuse it the privileged methods, while this gateway would grant them. Do not treat this as a boundary against code already running as you.
  • A compromised tailnet device, or anyone who obtains your Tailscale account. Tailscale ACLs and device authorization are the boundary there. allowedLogins and allowedPeers narrow it further.
  • Data at rest on the phone. The session cookie is HttpOnly and SameSite=Strict and travels inside TLS, but it is bearer-equivalent for its lifetime (30 days by default).

What works from the phone

CapabilityOver the tailnet
Chat, run agents, tools, jobs, workflowsWorks
File browse / upload, session and goal viewsWorks
Settings, Models, Plugins, Agent presetsUnavailable in the browser UI

The last row is deliberate. This plugin implements the access half of the gateway, not the client-side bundle override: those pages degrade to read-only rather than erroring, and the server-side fence stays in force. Widening it would mean patching a build artifact, which breaks on upgrade and trades a real security property for a convenience.


Install

1. The plugin

The plugin is a single dependency-free ES module. Mount it from the Web profile's own patch layer — %USERPROFILE%\.dsh\profiles\web\cordis.patch.yml:

- insert:
    - id: tailnet-gateway
      name: '<absolute path to this repo>/lib/index.js'
      config:
        upstreamPort: 3080

The path must name an entry file, not a directory: the loader hands it to Node's ESM resolver, which rejects directory imports (ERR_UNSUPPORTED_DIR_IMPORT).

If the profile declares patchReload: live, the running dsh picks the row up without a restart. Verify rather than assume:

Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -in 3080, 7242 } |
  Select-Object LocalAddress, LocalPort, OwningProcess

Both listeners must show 127.0.0.1 and the same dsh PID. A 0.0.0.0 there is a bug worth reporting, not a convenience.

2. Tailscale

Two one-time browser approvals are required, and neither is scriptable:

  1. Enable Serve for the tailnet. tailscale serve --bg 7242 prints Serve is not enabled on your tailnet plus an authorization link.
  2. Enable HTTPS certificates at https://login.tailscale.com/admin/dns under HTTPS Certificates. Until this is on, tailscale cert fails with 500 ... does not support getting TLS certs and tailscale serve fails with unexpected state: NoState — the same root cause, because Serve provisions a real certificate for the *.ts.net name.

Then publish the gateway:

tailscale serve --bg 7242
tailscale serve status

3. Authorize a browser

$env:DSH_TAILNET_ORIGIN = 'https://<machine>.<tailnet>.ts.net'
node grant-access.mjs

It prints the one-shot link and writes a QR of it, then instructs you to publish the grant route if needed:

tailscale serve --bg --set-path=/grant http://127.0.0.1:7245

Open the link on the device you want to authorize. Afterwards the only URL that matters is the bare origin:

https://<machine>.<tailnet>.ts.net/

Add it to the home screen. The cookie lasts 30 days and survives dsh web restarts, because the signing secret is durable — you do not need a token again on that device.

The 30-day expiry, and what to do about it

The session cookie is an absolute 30-day credential. It is not a sliding window: using the app every day does not extend it. On day 31 that browser gets the 401 page again.

What you will see

The 401 page reading dsh web authentication required; reopen the URL printed by dsh web. Nothing is broken and nothing is lost — the browser simply has no valid session any more. Conversations, workspaces and settings are all server side and unaffected.

What to do (one command, per device)

Re-run the grant tool and open the new link in that device's browser:

$env:DSH_TAILNET_ORIGIN = 'https://<machine>.<tailnet>.ts.net'
node grant-access.mjs

It prints a fresh one-shot link and writes grant-qr.png. Scan the QR (the iOS Camera app opens it in Safari) or open the link directly, and the device is good for another 30 days. Repeat per device — cookies are per-browser, and each device holds its own.

If the grant route is no longer published, re-attach it first:

tailscale serve --bg --set-path=/grant http://127.0.0.1:7245

Choosing a different lifetime

node grant-access.mjs --days 90      # longer: less renewal, bigger blast radius
node grant-access.mjs --days 7       # shorter: more renewal, smaller blast radius

The trade is the usual one for a bearer credential: the cookie is HttpOnly + SameSite=Strict and travels inside TLS, but anyone holding it can act as you for its whole lifetime. It is stored per-browser, so it cannot be revoked on one device only — see below.

Revoking early

There is no per-device logout. dsh's own model is that the browser cookie is signed with the owner-scoped client-connection/browser-session record, so the way to invalidate every device at once is to make that record stop matching:

  1. Stop dsh.
  2. Delete the client-connection/browser-session record from %USERPROFILE%\.dsh\.credentials.yaml.
  3. Start dsh. A new signing secret is created on activation, and every existing cookie fails signature verification.
  4. Re-grant each device you still want.

Note that dsh reads this record once during Connection activation, so step 3 is required — deleting it while dsh runs does not revoke anything until the next start.

Configuration

KeyDefaultMeaning
enabledtrueRun the listener at all.
host127.0.0.1Bind address. Clamped to loopback; a non-loopback value is refused.
port7242The port tailscale serve publishes. 0 requests an OS-assigned port.
requireLogintrueDemand the Tailscale identity. Turning this off makes the listener an open loopback proxy.
allowedLogins[]Tailscale logins allowed. Empty means any account on the tailnet.
allowedPeers[]Optional per-device gate: tailnet IPs as tailscale status reports them.
upstreamPort3080The dsh web port being fronted.

Hardening

On a single-user tailnet the defaults are already correct. To narrow further, fill allowedPeers with your phone's tailnet address — that is what makes "my phone, never that VPS" expressible even when the VPS is signed in as you:

      config:
        upstreamPort: 3080
        allowedLogins: ['you@example.com']
        allowedPeers: ['100.64.0.7']

Fill it before relying on it: a peer allowlist that does not match the machine you are calling from locks you out of the tailnet surface. The local desktop at 127.0.0.1:3080 still works, so the fix is a local edit.

Verify

node --test test/gateway.test.js     # 15 unit + real-socket integration tests
node verify.mjs all                  # live gateway + full TLS chain
node verify.mjs cookie "<pair>"      # a specific cookie authenticates dsh

Three widening rings, so a failure localises the problem: live failing means the plugin is not mounted or is bound wrongly; chain failing means Tailscale, DNS or the certificate; cookie failing means the cookie's authority does not match this instance.

chain treats a 401 from / as success. It proves dsh's own session check answered, so every hop in front of it worked. A 502 or 403 there means the chain is broken.

Operational notes

The dsh process must stay running. tailscale serve reaches the gateway on this machine, so closing the terminal that started dsh web takes the tailnet surface down with it. start-dsh-web.ps1 starts it detached (surviving the terminal), captures the tokenized URL to a file, and prints the phone-ready link.

Do not skip the Tailscale GUI process. tailscale-ipn.exe must be running. Without it the service is up but reports BackendState: NoState and every tailscale command fails, including Serve. A reinstall or upgrade commonly kills it:

Start-Process "C:\Program Files\Tailscale\tailscale-ipn.exe"   # no elevation needed

There is often no autostart entry for it, so enable Run unattended in the tray menu if you want remote access to survive reboot.

tailscale cert writes a private key into the current directory. Serve does not need those copies (tailscaled keeps its own), so delete any *.key it drops.

Non-obvious Serve behavior, learned the hard way: --set-path=/grant strips the prefix before forwarding, so a browser opening /grant/<secret> arrives at the grant server as /<secret>. grant-access.mjs accepts both shapes so they never have to agree. A mismatched assumption here makes the server consume the grant while answering 404 — a genuinely confusing failure.

Troubleshooting

SymptomCauseFix
tailscale status says "starting", NoStatetailscale-ipn.exe not runningstart it (see above)
tailscale cert 500, serve NoStateHTTPS certificates not enabledenable at admin/dns
Port-qualified URL shows 404 page not foundonly :443 is published; --set-path routes 404 at their rootuse the bare origin
401 page, but /check-style probe shows no cookiethat browser was never granted a session (cookie stores are per-app, and private windows discard them)open a grant link in that specific browser
401 on the bare origin after a grantcookie authority mismatch, or the grant went to a different browserre-run grant-access.mjs, open it in the same browser

Rollback

Remove the tailnet-gateway insert from cordis.patch.yml and restart dsh, then tailscale serve --https=443 off. The plugin writes no files and needs no package install, so nothing else on the machine changes.

License

MIT