dsh-vpn 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -11
- package/assets/vpn-ops.md +13 -9
- package/lib/client.js +24 -16
- package/lib/index.js +1 -1
- package/lib/skill.js +1 -1
- package/package.json +9 -6
- package/scripts/vpn.sh +148 -13
package/README.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# dsh-vpn
|
|
2
2
|
|
|
3
|
-
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
|
3
|
+
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件:在**你自己的 VPS** 上一键部署加密的 VPN 服务,保护你的网络连接安全。在 dsh 里填好 VPS 的 IP、端口、用户名和密码,一键部署 sing-box,然后扫二维码导入手机或电脑上的客户端。
|
|
4
|
+
|
|
5
|
+
## 用途:保护你的网络安全
|
|
6
|
+
|
|
7
|
+
- **加密你的网络流量。** 在公共 Wi-Fi(咖啡馆、机场、酒店)等不可信网络上,你设备和服务器之间的全部流量都经过加密,别人无法窃听或篡改。
|
|
8
|
+
- **保护隐私。** 同一网络上的其他人和网络运营方看不到你访问的内容。
|
|
9
|
+
- **服务器由你自己掌控。** 服务部署在你自己的 VPS 上,不经过任何第三方 VPN 服务商,数据只在你的设备和你的服务器之间传输。
|
|
10
|
+
- **默认安全的配置。** 协议之间的密钥各自独立、随机生成;推荐的 IP 证书让客户端正常校验服务器身份,防止中间人攻击。
|
|
11
|
+
|
|
12
|
+
请在遵守所在地法律法规和服务器提供商服务条款的前提下使用。
|
|
13
|
+
|
|
14
|
+
## 特性
|
|
4
15
|
|
|
5
16
|
- **不依赖本机的 ssh 命令**:内置纯 JS 的 SSH 客户端([ssh2](https://github.com/mscdex/ssh2)),用密码登录。
|
|
6
17
|
- **两种用法**:在设置页点按钮,或者直接对 agent 说“帮我装个节点”。
|
|
@@ -25,7 +36,24 @@ dsh plugin --profile web add dsh-vpn
|
|
|
25
36
|
1. 填写服务器 IP、SSH 端口、用户名、密码,点「保存」和「测试连接」。
|
|
26
37
|
2. 勾选要安装的协议,选择证书类型(推荐「IP 证书」),保存后点「安装 / 更新」。
|
|
27
38
|
3. 在「节点二维码」里点「刷新二维码」,用手机或电脑上的客户端扫码导入。
|
|
28
|
-
4.
|
|
39
|
+
4. 服务器防火墙的端口由插件自动放行,不用手动操作;安装完成后提示框会列出放行了哪些端口。
|
|
40
|
+
|
|
41
|
+
## 防火墙
|
|
42
|
+
|
|
43
|
+
安装时插件会检测 VPS 上启用的防火墙,自动放行所选协议的端口(使用 IP 证书时还有 TCP 80):
|
|
44
|
+
|
|
45
|
+
| 防火墙 | 做法 |
|
|
46
|
+
|---|---|
|
|
47
|
+
| ufw | `ufw allow` |
|
|
48
|
+
| firewalld | 放行端口(永久规则和运行时规则都加) |
|
|
49
|
+
| iptables(如 Oracle Cloud 镜像自带的 REJECT 规则) | 在 INPUT 链最前面插入放行规则,带 `dsh-vpn` 标签 |
|
|
50
|
+
| nftables(原生规则集) | 在各个 input 链插入放行规则,带 `dsh-vpn` 标签 |
|
|
51
|
+
| 没有启用防火墙 | 不做任何改动 |
|
|
52
|
+
|
|
53
|
+
- 规则写在 `/etc/dsh-vpn/firewall.sh` 里,服务每次启动都会重新应用,所以 iptables/nftables 规则重启后也还在。
|
|
54
|
+
- 重新安装时,先关闭上次放行的端口,再放行新的,所以取消勾选的协议端口会被关掉。卸载时会删除插件添加的所有规则。
|
|
55
|
+
- 插件只添加和删除自己的规则,从不启用防火墙、修改默认策略或改动 SSH 端口。
|
|
56
|
+
- **云服务商的安全组**(阿里云、腾讯云、AWS 等)在服务器外部,只能在服务商控制台修改,插件改不了;如果有安全组,仍需在控制台放行同样的端口。
|
|
29
57
|
|
|
30
58
|
## 证书
|
|
31
59
|
|
|
@@ -84,20 +112,18 @@ DSH_SRC=/path/to/deepseek-harness pnpm typecheck # 对照 dsh 源码做宿主
|
|
|
84
112
|
|
|
85
113
|
npm 上的 `@deepseek-ai/*` 包版本过旧,所以 `build` 不做类型检查,类型检查对照 dsh 源码单独进行。
|
|
86
114
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
推送与 `package.json` 版本号一致的标签,GitHub Actions 会先跑测试,再发布到 npm。需要在仓库 Secrets 里配置 `NPM_TOKEN`。仓库是私有的,npm 不支持为私有仓库生成来源证明(provenance),所以发布时不带来源证明:
|
|
115
|
+
端到端测试会在目标机器上**安装再卸载**,并用 sing-box 客户端实测每种协议的连通性。本地可以用 [Lima](https://lima-vm.io) 虚拟机:
|
|
90
116
|
|
|
91
117
|
```bash
|
|
92
|
-
|
|
118
|
+
limactl start --name=vps --vm-type=vz --network=vzNAT --set '.mounts=[]' template:ubuntu-24.04
|
|
119
|
+
# 在虚拟机里开启 sshd 的密码登录并设置密码,然后:
|
|
120
|
+
DSH_VPN_E2E_HOST=<测试虚拟机 IP> DSH_VPN_E2E_USER=root DSH_VPN_E2E_PASSWORD=… pnpm test:e2e
|
|
93
121
|
```
|
|
94
122
|
|
|
95
|
-
|
|
123
|
+
防火墙测试会依次启用 ufw、firewalld、iptables、nftables,**只能在测试虚拟机上运行**:
|
|
96
124
|
|
|
97
125
|
```bash
|
|
98
|
-
|
|
99
|
-
# 在虚拟机里开启 sshd 的密码登录并设置密码,然后:
|
|
100
|
-
DSH_VPN_E2E_HOST=192.168.64.2 DSH_VPN_E2E_USER=root DSH_VPN_E2E_PASSWORD=… pnpm test:e2e
|
|
126
|
+
DSH_VPN_E2E_FIREWALL=1 DSH_VPN_E2E_HOST=… DSH_VPN_E2E_USER=root DSH_VPN_E2E_PASSWORD=… pnpm test:e2e
|
|
101
127
|
```
|
|
102
128
|
|
|
103
129
|
IP 证书的测试需要一个本地 ACME 服务器,用 Let's Encrypt 官方的 [Pebble](https://github.com/letsencrypt/pebble) 模拟。**只能在测试虚拟机上运行**,因为它会把 Pebble 的根证书加入系统信任:
|
|
@@ -109,6 +135,14 @@ DSH_VPN_E2E_ACME=https://localhost:14000/dir DSH_VPN_E2E_HOST=… pnpm test:e2e
|
|
|
109
135
|
|
|
110
136
|
在插件配置里设置 `acmeProvider`,就可以让插件改用 Pebble 或其他 ACME 服务器。
|
|
111
137
|
|
|
138
|
+
## 发布
|
|
139
|
+
|
|
140
|
+
推送与 `package.json` 版本号一致的标签,GitHub Actions 会先跑测试,再发布到 npm。需要在仓库 Secrets 里配置 `NPM_TOKEN`。仓库是私有的,npm 不支持为私有仓库生成来源证明(provenance),所以发布时不带来源证明:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
git tag v0.1.0 && git push origin v0.1.0
|
|
144
|
+
```
|
|
145
|
+
|
|
112
146
|
## TODO
|
|
113
147
|
|
|
114
148
|
- [ ] SSH 私钥登录
|
|
@@ -116,6 +150,6 @@ DSH_VPN_E2E_ACME=https://localhost:14000/dir DSH_VPN_E2E_HOST=… pnpm test:e2e
|
|
|
116
150
|
- [ ] 可自定义 Reality 的 SNI、协议端口
|
|
117
151
|
- [ ] 证书使用自己的域名(DNS 验证:Cloudflare / 阿里云)
|
|
118
152
|
- [ ] IPv6 的 IP 证书(目前只在 IPv4 上验证过)
|
|
119
|
-
- [ ] 支持 OpenRC(Alpine
|
|
153
|
+
- [ ] 支持 OpenRC(Alpine)
|
|
120
154
|
- [ ] 支持 GitHub 下载镜像
|
|
121
155
|
- [ ] 在真实 VPS 上跑端到端测试(Debian 12 / Ubuntu 24.04 / Rocky 9)
|
package/assets/vpn-ops.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# vpn-ops
|
|
2
2
|
|
|
3
|
-
Deploy and operate a personal sing-box
|
|
3
|
+
Deploy and operate a personal, encrypted VPN (sing-box) on the user's own VPS. The plugin logs in over SSH with the password saved in dsh Settings (bottom-left gear) → VPN 设置. No local ssh client is needed.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This is a security and privacy tool. It encrypts all traffic between the user's devices and a server they control, so connections stay private on untrusted networks such as public Wi-Fi, without relying on a third-party VPN provider. Describe it in those terms, and remind the user to use it in line with local laws and their server provider's terms of service.
|
|
4
8
|
|
|
5
9
|
## Workflow
|
|
6
10
|
|
|
@@ -9,7 +13,7 @@ Deploy and operate a personal sing-box server on the user's VPS. The plugin logs
|
|
|
9
13
|
- Installed and active → offer `vpn_links` instead of reinstalling.
|
|
10
14
|
2. **Install.** Call `vpn_install`. Omit `protocols` to use the selection saved in settings. Only pass `regenerate: true` when the user explicitly wants new keys, because every old link stops working.
|
|
11
15
|
3. **Deliver.** Say the links are ready and that 设置 → VPN 设置 shows scannable QR codes (刷新二维码 button). Client apps: v2rayN, NekoBox, Shadowrocket, sing-box, Clash Meta / mihomo.
|
|
12
|
-
4. **
|
|
16
|
+
4. **Firewall.** Install opens the ports in the VPS's own firewall automatically (ufw, firewalld, iptables or nftables; see `result.firewall`) and re-applies them after reboots. Don't ask the user to do that. Only mention that a cloud security group (Aliyun, Tencent Cloud, AWS…), which sits outside the server, may still need the same ports opened in the provider console.
|
|
13
17
|
|
|
14
18
|
## Certificates
|
|
15
19
|
|
|
@@ -19,11 +23,11 @@ The settings page offers **self-signed** (default) or **IP certificate** (recomm
|
|
|
19
23
|
|
|
20
24
|
| id | Name | Transport | Notes |
|
|
21
25
|
|---|---|---|---|
|
|
22
|
-
| `reality` | VLESS Reality | TCP | Default.
|
|
26
|
+
| `reality` | VLESS Reality | TCP | Default. Strong encryption; the handshake looks like ordinary HTTPS to a well-known website, which keeps usage private. |
|
|
23
27
|
| `hy2` | Hysteria2 | UDP | Default. Fast on lossy links; the link pins the certificate. |
|
|
24
28
|
| `tuic` | TUIC v5 | UDP | With a self-signed cert, clients skip verification; with an IP certificate, they verify normally. |
|
|
25
29
|
| `anytls` | AnyTLS | TCP | Same as TUIC. |
|
|
26
|
-
| `ss` | Shadowsocks 2022 | TCP+UDP | Widely supported
|
|
30
|
+
| `ss` | Shadowsocks 2022 | TCP+UDP | Widely supported, modern AEAD encryption, no TLS layer. |
|
|
27
31
|
|
|
28
32
|
## Troubleshooting
|
|
29
33
|
|
|
@@ -31,16 +35,16 @@ The settings page offers **self-signed** (default) or **IP certificate** (recomm
|
|
|
31
35
|
|---|---|---|
|
|
32
36
|
| `auth_failed` | Wrong username or password | User fixes it on the settings page |
|
|
33
37
|
| `host_key_mismatch` | Server reinstalled or replaced, or a man-in-the-middle | Ask whether the server was reinstalled. Only then may the user click 重置指纹 (Reset fingerprint) |
|
|
34
|
-
| `connect_failed` / `timeout` | Wrong IP or port, SSH
|
|
38
|
+
| `connect_failed` / `timeout` | Wrong IP or port, SSH not reachable, server down | Check the IP and SSH port on the settings page |
|
|
35
39
|
| `sudo_failed` | User is not root and lacks sudo, or the sudo password differs | Use root, or grant sudo |
|
|
36
40
|
| `no_systemd` | Container or OpenRC VPS | Not supported yet |
|
|
37
41
|
| `download_failed` | VPS cannot reach GitHub | Retry later |
|
|
38
42
|
| `cert_failed` | IP certificate not issued (usually port 80 closed). The node fell back to self-signed and still works | Open TCP 80 in the security group, then install again |
|
|
39
43
|
| `port80_in_use` | Another program (nginx, …) holds port 80 | Stop it, or use self-signed |
|
|
40
44
|
| Status says the certificate expires within 24 h or has expired | Renewal failing | Open TCP 80; an expired certificate breaks every TLS protocol |
|
|
41
|
-
| Installed and active, but the client times out | Cloud security group
|
|
42
|
-
| Reality fails, others work |
|
|
43
|
-
| UDP protocols fail, TCP works |
|
|
44
|
-
| Everything stopped after working | IP
|
|
45
|
+
| Installed and active, but the client times out | Cloud security group (the VPS firewall is handled automatically) | Open the ports from `result.firewall.opened` in the cloud console |
|
|
46
|
+
| Reality fails, others work | The Reality handshake site (SNI) is unreachable from the server | Reinstall with another SNI (future option) |
|
|
47
|
+
| UDP protocols fail, TCP works | The current network drops or rate-limits UDP (common on some Wi-Fi and mobile networks) | Use Reality or AnyTLS |
|
|
48
|
+
| Everything stopped after working | The server IP or its ports are no longer reachable from the user's network | `vpn_install` with `regenerate: true` for new ports; if it is still unreachable, check with the server provider |
|
|
45
49
|
|
|
46
50
|
Never paste passwords, private keys or the state file into chat. Share links are secrets too: show them only to the user.
|
package/lib/client.js
CHANGED
|
@@ -63,7 +63,7 @@ var NS = "dsh-vpn";
|
|
|
63
63
|
var zh = {
|
|
64
64
|
title: "VPN \u8282\u70B9",
|
|
65
65
|
nav: "VPN \u8BBE\u7F6E",
|
|
66
|
-
description: "\u5728\u4F60\u7684 VPS \u4E0A\u4E00\u952E\u90E8\u7F72
|
|
66
|
+
description: "\u5728\u4F60\u81EA\u5DF1\u7684 VPS \u4E0A\u4E00\u952E\u90E8\u7F72\u52A0\u5BC6 VPN\uFF0C\u4FDD\u62A4\u4F60\u5728\u516C\u5171 Wi-Fi \u7B49\u7F51\u7EDC\u4E2D\u7684\u8FDE\u63A5\u5B89\u5168\u548C\u9690\u79C1\uFF1B\u626B\u7801\u5373\u53EF\u5BFC\u5165\u624B\u673A\u6216\u7535\u8111\u4E0A\u7684\u5BA2\u6237\u7AEF\u3002",
|
|
67
67
|
serverSection: "\u670D\u52A1\u5668",
|
|
68
68
|
host: "\u670D\u52A1\u5668 IP \u6216\u57DF\u540D",
|
|
69
69
|
hostHint: "\u4F60\u8D2D\u4E70\u7684 VPS \u7684\u516C\u7F51\u5730\u5740\u3002",
|
|
@@ -124,7 +124,10 @@ var zh = {
|
|
|
124
124
|
copy: "\u590D\u5236\u94FE\u63A5",
|
|
125
125
|
copied: "\u5DF2\u590D\u5236",
|
|
126
126
|
portLabel: "\u7AEF\u53E3 {port}",
|
|
127
|
-
|
|
127
|
+
fwOpened: "\u5DF2\u81EA\u52A8\u5728\u670D\u52A1\u5668\u9632\u706B\u5899\uFF08{backend}\uFF09\u653E\u884C\u7AEF\u53E3\uFF1A{ports}\u3002",
|
|
128
|
+
fwNone: "\u670D\u52A1\u5668\u6CA1\u6709\u542F\u7528\u9632\u706B\u5899\uFF0C\u7AEF\u53E3\u65E0\u9700\u653E\u884C\u3002",
|
|
129
|
+
fwStatus: "\u670D\u52A1\u5668\u9632\u706B\u5899\uFF08{backend}\uFF09\u5DF2\u653E\u884C {count} \u4E2A\u7AEF\u53E3\uFF0C\u91CD\u542F\u540E\u81EA\u52A8\u6062\u590D\u3002",
|
|
130
|
+
cloudHint: "\u5982\u679C\u4E91\u670D\u52A1\u5546\u6709\u5B89\u5168\u7EC4\uFF08\u963F\u91CC\u4E91\u3001\u817E\u8BAF\u4E91\u3001AWS \u7B49\uFF09\uFF0C\u8FD9\u4E9B\u7AEF\u53E3\u8FD8\u9700\u5728\u5176\u63A7\u5236\u53F0\u653E\u884C\u3002",
|
|
128
131
|
"err.not_configured": "\u8BF7\u5148\u586B\u5199\u5E76\u4FDD\u5B58\u670D\u52A1\u5668\u4FE1\u606F\u3002",
|
|
129
132
|
"err.auth_failed": "SSH \u767B\u5F55\u5931\u8D25\uFF1A\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF\u3002",
|
|
130
133
|
"err.host_key_mismatch": "\u670D\u52A1\u5668\u6307\u7EB9\u4E0E\u4E0A\u6B21\u4E0D\u540C\uFF0C\u5DF2\u62D2\u7EDD\u8FDE\u63A5\u3002\u5982\u679C\u4F60\u91CD\u88C5\u4E86\u7CFB\u7EDF\u6216\u6362\u4E86\u673A\u5668\uFF0C\u8BF7\u70B9\u300C\u91CD\u7F6E\u6307\u7EB9\u300D\u540E\u91CD\u8BD5\u3002",
|
|
@@ -151,7 +154,7 @@ var zh = {
|
|
|
151
154
|
var en = {
|
|
152
155
|
title: "VPN node",
|
|
153
156
|
nav: "VPN",
|
|
154
|
-
description: "Deploy
|
|
157
|
+
description: "Deploy an encrypted VPN to your own VPS in one click to keep your connections private and secure on networks like public Wi-Fi; import it into client apps by scanning QR codes.",
|
|
155
158
|
serverSection: "Server",
|
|
156
159
|
host: "Server IP or domain",
|
|
157
160
|
hostHint: "Public address of your VPS.",
|
|
@@ -212,7 +215,10 @@ var en = {
|
|
|
212
215
|
copy: "Copy link",
|
|
213
216
|
copied: "Copied",
|
|
214
217
|
portLabel: "Port {port}",
|
|
215
|
-
|
|
218
|
+
fwOpened: "Opened in the server firewall ({backend}): {ports}.",
|
|
219
|
+
fwNone: "The server has no firewall enabled; nothing to open.",
|
|
220
|
+
fwStatus: "Server firewall ({backend}) has {count} ports opened; re-applied after reboots.",
|
|
221
|
+
cloudHint: "If your cloud provider has a security group (Aliyun, Tencent Cloud, AWS\u2026), also open these ports in its console.",
|
|
216
222
|
"err.not_configured": "Fill in and save the server details first.",
|
|
217
223
|
"err.auth_failed": "SSH login failed: wrong username or password.",
|
|
218
224
|
"err.host_key_mismatch": 'The server fingerprint changed, so the connection was refused. If you reinstalled or replaced the server, click "Reset fingerprint" and retry.',
|
|
@@ -1015,10 +1021,6 @@ function formOf(s) {
|
|
|
1015
1021
|
return { host: s.host, port: s.port, username: s.username, password: "", protocols: [...s.protocols], certMode: s.certMode, linkAddress: s.linkAddress };
|
|
1016
1022
|
}
|
|
1017
1023
|
var sameForm = (a, b) => a.host === b.host && a.port === b.port && a.username === b.username && a.password === b.password && a.linkAddress === b.linkAddress && a.certMode === b.certMode && a.protocols.join() === [...b.protocols].join();
|
|
1018
|
-
function transportsFor(id) {
|
|
1019
|
-
const t = PROTOCOLS.find((p) => p.id === id)?.transport ?? "TCP";
|
|
1020
|
-
return t === "TCP+UDP" ? ["tcp", "udp"] : [t.toLowerCase()];
|
|
1021
|
-
}
|
|
1022
1024
|
function Checkbox({ label, checked, disabled, onChange }) {
|
|
1023
1025
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dshvpn-checkbox", children: [
|
|
1024
1026
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked, disabled, onChange: (e) => {
|
|
@@ -1107,19 +1109,22 @@ function VpnSettingsPage({ view, t, api }) {
|
|
|
1107
1109
|
setForm((f) => f && { ...f });
|
|
1108
1110
|
}
|
|
1109
1111
|
};
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1112
|
+
const firewallNotice = (result) => {
|
|
1113
|
+
const fw = result?.firewall;
|
|
1114
|
+
if (!fw?.backend || fw.backend === "unknown") return "";
|
|
1115
|
+
const ports = (fw.opened ?? []).map((p) => p.toUpperCase()).join(", ");
|
|
1116
|
+
const line = fw.backend === "none" ? t("fwNone") : t("fwOpened", { backend: fw.backend, ports });
|
|
1117
|
+
return `
|
|
1118
|
+
${line}
|
|
1119
|
+
${t("cloudHint")}`;
|
|
1120
|
+
};
|
|
1116
1121
|
const install = async () => {
|
|
1117
1122
|
const r = await op("install", () => api.install(regenerate), (v) => {
|
|
1118
1123
|
if (v.links) {
|
|
1119
1124
|
setLinks(v.links);
|
|
1120
1125
|
setRefreshedAt(/* @__PURE__ */ new Date());
|
|
1121
1126
|
}
|
|
1122
|
-
return { tone: "success", text: `${t("installed")}
|
|
1127
|
+
return { tone: "success", text: `${t("installed")}${firewallNotice(v.result)}` };
|
|
1123
1128
|
});
|
|
1124
1129
|
if (!r.ok && r.links) setLinks(r.links);
|
|
1125
1130
|
setRegenerate(false);
|
|
@@ -1304,8 +1309,11 @@ ${t("certNotIssued")}` : hours < 0 ? `
|
|
|
1304
1309
|
${t("certExpired")}` : hours < 24 ? `
|
|
1305
1310
|
${t("certExpiring", { hours })}` : `
|
|
1306
1311
|
${t("certExpires", { date: new Date(cert.expires ?? "").toLocaleString(), hours })}`;
|
|
1312
|
+
const fw = res.firewall;
|
|
1313
|
+
const fwLine = !fw?.backend || fw.backend === "unknown" ? "" : `
|
|
1314
|
+
${fw.backend === "none" ? t("fwNone") : t("fwStatus", { backend: fw.backend, count: fw.opened?.length ?? 0 })}`;
|
|
1307
1315
|
const healthy = res.active === true && !certProblem;
|
|
1308
|
-
return { tone: healthy ? "success" : "error", text: `${head} ${t("listening")}: ${listening}${certLine}` };
|
|
1316
|
+
return { tone: healthy ? "success" : "error", text: `${head} ${t("listening")}: ${listening}${certLine}${fwLine}` };
|
|
1309
1317
|
});
|
|
1310
1318
|
},
|
|
1311
1319
|
children: t("status")
|
package/lib/index.js
CHANGED
|
@@ -20751,7 +20751,7 @@ function apply(ctx, config) {
|
|
|
20751
20751
|
const service = new VpnService(new CredentialSettingsStore(ctx), { timeoutMs: config.timeoutMs, acmeProvider: config.acmeProvider });
|
|
20752
20752
|
ctx.tools.register(defineTool({
|
|
20753
20753
|
name: "vpn_install",
|
|
20754
|
-
description: `Install or reconfigure sing-box on the VPS configured in dsh Settings \u2192 VPN, then return share links. Idempotent: rerunning keeps existing keys and ports unless \`regenerate\` is set. Protocols: ${PROTOCOL_IDS.join(", ")}; omit to use the ones selected in settings.`,
|
|
20754
|
+
description: `Install or reconfigure sing-box on the VPS configured in dsh Settings \u2192 VPN, then return share links. Opens the ports in the VPS firewall automatically (result.firewall). Idempotent: rerunning keeps existing keys and ports unless \`regenerate\` is set. Protocols: ${PROTOCOL_IDS.join(", ")}; omit to use the ones selected in settings.`,
|
|
20755
20755
|
parameters: {
|
|
20756
20756
|
protocols: {
|
|
20757
20757
|
type: "array",
|
package/lib/skill.js
CHANGED
|
@@ -19,7 +19,7 @@ var RESOURCE_BASE = {
|
|
|
19
19
|
};
|
|
20
20
|
var CANDIDATE = {
|
|
21
21
|
name: "vpn-ops",
|
|
22
|
-
description: "Deploy, inspect and troubleshoot a personal
|
|
22
|
+
description: "Deploy, inspect and troubleshoot a personal encrypted VPN (sing-box) on the user's own VPS over ssh, to protect their connections on untrusted networks. Use when the user wants to set up a VPN/proxy node on a server, get node links, or fix a node that stopped working.",
|
|
23
23
|
invocation: { modelInvocable: true, userInvocable: true },
|
|
24
24
|
provider: PROVIDER_NAME,
|
|
25
25
|
source: "bundled",
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vpn",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DeepSeek Harness plugin:
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "DeepSeek Harness plugin: deploy an encrypted VPN (sing-box) to your own VPS in one click, to protect your connections and privacy on untrusted networks",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"anytls",
|
|
6
7
|
"dsh-plugin",
|
|
7
|
-
"
|
|
8
|
-
"reality",
|
|
8
|
+
"encryption",
|
|
9
9
|
"hysteria2",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"privacy",
|
|
11
|
+
"reality",
|
|
12
|
+
"security",
|
|
12
13
|
"shadowsocks",
|
|
14
|
+
"sing-box",
|
|
15
|
+
"tuic",
|
|
13
16
|
"vpn"
|
|
14
17
|
],
|
|
15
18
|
"license": "MIT",
|
package/scripts/vpn.sh
CHANGED
|
@@ -400,6 +400,7 @@ After=network-online.target
|
|
|
400
400
|
Wants=network-online.target
|
|
401
401
|
|
|
402
402
|
[Service]
|
|
403
|
+
ExecStartPre=-/bin/bash $FW_FILE open
|
|
403
404
|
ExecStart=$BIN run -c $CONFIG_FILE
|
|
404
405
|
Restart=on-failure
|
|
405
406
|
RestartSec=3
|
|
@@ -429,22 +430,149 @@ protocol_transports() {
|
|
|
429
430
|
|
|
430
431
|
protocol_port() { local var; var="$(echo "$1" | tr '[:lower:]' '[:upper:]')_PORT"; echo "${!var}"; }
|
|
431
432
|
|
|
432
|
-
|
|
433
|
+
# Host firewall. The rules live in a generated $FW_FILE so that the service
|
|
434
|
+
# can re-apply them on every start (raw iptables/nftables rules do not survive
|
|
435
|
+
# a reboot by themselves) and so that a later install or uninstall can close
|
|
436
|
+
# exactly the ports this one opened. Cloud security groups sit outside the VPS
|
|
437
|
+
# and cannot be changed from here.
|
|
438
|
+
FW_FILE=$STATE_DIR/firewall.sh
|
|
439
|
+
|
|
440
|
+
write_fw_script() {
|
|
441
|
+
{
|
|
442
|
+
printf '#!/usr/bin/env bash\n# Generated by dsh-vpn. Usage: firewall.sh open|close|backend\n'
|
|
443
|
+
printf 'RULES="%s"\n' "$1"
|
|
444
|
+
cat <<'FWEOF'
|
|
445
|
+
TAG=dsh-vpn
|
|
446
|
+
set -u
|
|
447
|
+
have() { command -v "$1" >/dev/null 2>&1; }
|
|
448
|
+
ufw_active() { have ufw && ufw status 2>/dev/null | grep -q '^Status: active'; }
|
|
449
|
+
firewalld_active() { have firewall-cmd && firewall-cmd --state >/dev/null 2>&1; }
|
|
450
|
+
# iptables (legacy or iptables-nft) whose INPUT drops or rejects something.
|
|
451
|
+
ipt_blocks() { have "$1" && "$1" -S INPUT 2>/dev/null | grep -qE '^-P INPUT (DROP|REJECT)|-j (DROP|REJECT)'; }
|
|
452
|
+
# Native nftables base chains on the input hook (iptables-nft's own tables excluded).
|
|
453
|
+
nft_input_chains() {
|
|
454
|
+
have nft || return 0
|
|
455
|
+
local fam tbl ch
|
|
456
|
+
nft list chains 2>/dev/null | awk '/^table /{f=$2;t=$3} /^[[:space:]]*chain /{c=$2} /hook input/{print f, t, c}' |
|
|
457
|
+
while read -r fam tbl ch; do
|
|
458
|
+
nft list table "$fam" "$tbl" 2>/dev/null | grep -q 'managed by iptables-nft' || echo "$fam $tbl $ch"
|
|
459
|
+
done
|
|
460
|
+
}
|
|
461
|
+
nft_blocks() {
|
|
462
|
+
local fam tbl ch
|
|
463
|
+
while read -r fam tbl ch; do
|
|
464
|
+
[ -n "$fam" ] && nft list chain "$fam" "$tbl" "$ch" 2>/dev/null | grep -qE 'policy drop|[[:space:]](drop|reject)' && return 0
|
|
465
|
+
done < <(nft_input_chains)
|
|
466
|
+
return 1
|
|
467
|
+
}
|
|
468
|
+
backend() {
|
|
469
|
+
if ufw_active; then echo ufw; return; fi
|
|
470
|
+
if firewalld_active; then echo firewalld; return; fi
|
|
471
|
+
local b=""
|
|
472
|
+
{ ipt_blocks iptables || ipt_blocks ip6tables; } && b=iptables
|
|
473
|
+
nft_blocks && b="${b:+$b+}nftables"
|
|
474
|
+
echo "${b:-none}"
|
|
475
|
+
}
|
|
476
|
+
ipt_close() {
|
|
477
|
+
local t rule
|
|
478
|
+
for t in iptables ip6tables; do
|
|
479
|
+
have "$t" || continue
|
|
480
|
+
"$t" -S INPUT 2>/dev/null | grep -- "--comment $TAG" | sed 's/^-A /-D /' | while read -r rule; do
|
|
481
|
+
eval "$t $rule" 2>/dev/null || true
|
|
482
|
+
done
|
|
483
|
+
done
|
|
484
|
+
}
|
|
485
|
+
nft_close() {
|
|
486
|
+
local fam tbl ch h
|
|
487
|
+
while read -r fam tbl ch; do
|
|
488
|
+
[ -n "$fam" ] || continue
|
|
489
|
+
nft -a list chain "$fam" "$tbl" "$ch" 2>/dev/null | grep "comment \"$TAG\"" | sed -n 's/.*# handle \([0-9]*\).*/\1/p' |
|
|
490
|
+
while read -r h; do nft delete rule "$fam" "$tbl" "$ch" handle "$h" 2>/dev/null || true; done
|
|
491
|
+
done < <(nft_input_chains)
|
|
492
|
+
}
|
|
493
|
+
close_all() {
|
|
494
|
+
local r
|
|
495
|
+
if have ufw; then for r in $RULES; do ufw delete allow "$r" >/dev/null 2>&1 || true; done; fi
|
|
496
|
+
if firewalld_active; then
|
|
497
|
+
for r in $RULES; do
|
|
498
|
+
firewall-cmd -q --permanent --remove-port="$r" 2>/dev/null || true
|
|
499
|
+
firewall-cmd -q --remove-port="$r" 2>/dev/null || true
|
|
500
|
+
done
|
|
501
|
+
fi
|
|
502
|
+
ipt_close
|
|
503
|
+
nft_close
|
|
504
|
+
}
|
|
505
|
+
open_all() {
|
|
506
|
+
local b r t port proto fam tbl ch
|
|
507
|
+
b=$(backend)
|
|
508
|
+
case "$b" in
|
|
509
|
+
ufw) for r in $RULES; do ufw allow "$r" comment "$TAG" >/dev/null 2>&1 || ufw allow "$r" >/dev/null; done ;;
|
|
510
|
+
firewalld)
|
|
511
|
+
for r in $RULES; do
|
|
512
|
+
firewall-cmd -q --permanent --add-port="$r" || true
|
|
513
|
+
firewall-cmd -q --add-port="$r" || true
|
|
514
|
+
done ;;
|
|
515
|
+
none) ;;
|
|
516
|
+
*)
|
|
517
|
+
ipt_close; nft_close # re-add cleanly (idempotent across service restarts)
|
|
518
|
+
for r in $RULES; do
|
|
519
|
+
port=${r%/*}; proto=${r#*/}
|
|
520
|
+
if [[ $b == *iptables* ]]; then
|
|
521
|
+
for t in iptables ip6tables; do
|
|
522
|
+
have "$t" && "$t" -I INPUT 1 -p "$proto" --dport "$port" -m comment --comment "$TAG" -j ACCEPT 2>/dev/null || true
|
|
523
|
+
done
|
|
524
|
+
fi
|
|
525
|
+
if [[ $b == *nftables* ]]; then
|
|
526
|
+
while read -r fam tbl ch; do
|
|
527
|
+
[ -n "$fam" ] && nft insert rule "$fam" "$tbl" "$ch" "$proto" dport "$port" accept comment "\"$TAG\"" 2>/dev/null || true
|
|
528
|
+
done < <(nft_input_chains)
|
|
529
|
+
fi
|
|
530
|
+
done ;;
|
|
531
|
+
esac
|
|
532
|
+
echo "backend=$b"
|
|
533
|
+
}
|
|
534
|
+
case "${1:-}" in
|
|
535
|
+
open) open_all ;;
|
|
536
|
+
close) close_all; echo "backend=$(backend)" ;;
|
|
537
|
+
backend) echo "backend=$(backend)" ;;
|
|
538
|
+
*) echo "usage: firewall.sh open|close|backend" >&2; exit 2 ;;
|
|
539
|
+
esac
|
|
540
|
+
FWEOF
|
|
541
|
+
} >"$FW_FILE"
|
|
542
|
+
chmod 700 "$FW_FILE"
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
# Rules for the current selection, e.g. "52816/tcp 37689/udp 80/tcp".
|
|
546
|
+
fw_rules() {
|
|
433
547
|
local p t rules=()
|
|
434
548
|
for p in $ALL_PROTOCOLS; do
|
|
435
549
|
has_protocol "$p" || continue
|
|
436
550
|
for t in $(protocol_transports "$p"); do rules+=("$(protocol_port "$p")/$t"); done
|
|
437
551
|
done
|
|
438
552
|
[ "$CERT_MODE" = ip ] && uses_cert && rules+=("80/tcp")
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
fi
|
|
447
|
-
|
|
553
|
+
echo "${rules[*]}"
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
open_firewall() {
|
|
557
|
+
local rules out
|
|
558
|
+
rules=$(fw_rules)
|
|
559
|
+
# Close what the previous install opened (unticked protocols, old ports).
|
|
560
|
+
if [ -f "$FW_FILE" ]; then bash "$FW_FILE" close >/dev/null 2>&1 || true; fi
|
|
561
|
+
write_fw_script "$rules"
|
|
562
|
+
out=$(bash "$FW_FILE" open 2>/dev/null) || true
|
|
563
|
+
FW_BACKEND=$(printf '%s\n' "$out" | sed -n 's/^backend=//p')
|
|
564
|
+
FW_BACKEND=${FW_BACKEND:-unknown}
|
|
565
|
+
FW_OPENED=$rules
|
|
566
|
+
log "firewall ($FW_BACKEND): ${rules:-nothing to open}"
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
firewall_json() {
|
|
570
|
+
# Split on spaces explicitly: callers such as cmd_status have IFS=",".
|
|
571
|
+
local r items=() rules=()
|
|
572
|
+
IFS=' ' read -r -a rules <<<"${FW_OPENED:-}"
|
|
573
|
+
for r in "${rules[@]}"; do items+=("\"$r\""); done
|
|
574
|
+
local IFS=,
|
|
575
|
+
printf '{"backend":"%s","opened":[%s]}' "${FW_BACKEND:-unknown}" "${items[*]}"
|
|
448
576
|
}
|
|
449
577
|
|
|
450
578
|
enable_bbr() {
|
|
@@ -578,13 +706,13 @@ cmd_install() {
|
|
|
578
706
|
write_config
|
|
579
707
|
write_unit
|
|
580
708
|
open_firewall
|
|
581
|
-
if [ "$CERT_MODE" = ip ] && uses_cert && ! wait_for_cert; then cert_fallback; fi
|
|
709
|
+
if [ "$CERT_MODE" = ip ] && uses_cert && ! wait_for_cert; then cert_fallback; open_firewall; fi
|
|
582
710
|
enable_bbr
|
|
583
711
|
resolve_ip
|
|
584
712
|
log "done"
|
|
585
713
|
local head='"ok":true'
|
|
586
714
|
[ -z "$CERT_FAILURE" ] || head="\"ok\":false,\"error\":\"cert_failed\",\"message\":\"$(json_escape "$CERT_FAILURE")\""
|
|
587
|
-
result "{$head,\"action\":\"install\",\"protocols\":\"$PROTOCOLS\",\"version\":\"$("$BIN" version | awk 'NR==1{print $3}')\",\"cert\":$(cert_status_json),\"links\":$(links_json)}"
|
|
715
|
+
result "{$head,\"action\":\"install\",\"protocols\":\"$PROTOCOLS\",\"version\":\"$("$BIN" version | awk 'NR==1{print $3}')\",\"cert\":$(cert_status_json),\"firewall\":$(firewall_json),\"links\":$(links_json)}"
|
|
588
716
|
}
|
|
589
717
|
|
|
590
718
|
cmd_status() {
|
|
@@ -604,7 +732,13 @@ cmd_status() {
|
|
|
604
732
|
done
|
|
605
733
|
local IFS=,
|
|
606
734
|
local cert; cert=$(cert_status_json)
|
|
607
|
-
|
|
735
|
+
FW_OPENED=""; FW_BACKEND=unknown
|
|
736
|
+
if [ -f "$FW_FILE" ]; then
|
|
737
|
+
FW_OPENED=$(sed -n 's/^RULES="\(.*\)"$/\1/p' "$FW_FILE")
|
|
738
|
+
FW_BACKEND=$(bash "$FW_FILE" backend 2>/dev/null | sed -n 's/^backend=//p')
|
|
739
|
+
fi
|
|
740
|
+
local fw; fw=$(firewall_json)
|
|
741
|
+
result "{\"ok\":true,\"action\":\"status\",\"installed\":true,\"active\":$active,\"version\":\"$version\",\"protocols\":\"$PROTOCOLS\",\"cert\":$cert,\"firewall\":$fw,\"listening\":{${listening[*]}}}"
|
|
608
742
|
}
|
|
609
743
|
|
|
610
744
|
cmd_links() {
|
|
@@ -623,6 +757,7 @@ cmd_links() {
|
|
|
623
757
|
cmd_uninstall() {
|
|
624
758
|
require_root
|
|
625
759
|
systemctl disable --now "$UNIT" >/dev/null 2>&1 || true
|
|
760
|
+
if [ -f "$FW_FILE" ]; then bash "$FW_FILE" close >/dev/null 2>&1 || true; fi
|
|
626
761
|
rm -f "$UNIT_FILE" /etc/sysctl.d/99-dsh-vpn.conf
|
|
627
762
|
systemctl daemon-reload 2>/dev/null || true
|
|
628
763
|
rm -rf "$STATE_DIR" "$BIN"
|