dsh-vpn 0.1.2 → 0.1.3

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 CHANGED
@@ -25,7 +25,24 @@ dsh plugin --profile web add dsh-vpn
25
25
  1. 填写服务器 IP、SSH 端口、用户名、密码,点「保存」和「测试连接」。
26
26
  2. 勾选要安装的协议,选择证书类型(推荐「IP 证书」),保存后点「安装 / 更新」。
27
27
  3. 在「节点二维码」里点「刷新二维码」,用手机或电脑上的客户端扫码导入。
28
- 4. 在云服务商控制台的安全组里放行页面提示的端口。用 IP 证书时,TCP 80 端口要一直开放,用于续期。
28
+ 4. 服务器防火墙的端口由插件自动放行,不用手动操作;安装完成后提示框会列出放行了哪些端口。
29
+
30
+ ## 防火墙
31
+
32
+ 安装时插件会检测 VPS 上启用的防火墙,自动放行所选协议的端口(使用 IP 证书时还有 TCP 80):
33
+
34
+ | 防火墙 | 做法 |
35
+ |---|---|
36
+ | ufw | `ufw allow` |
37
+ | firewalld | 放行端口(永久规则和运行时规则都加) |
38
+ | iptables(如 Oracle Cloud 镜像自带的 REJECT 规则) | 在 INPUT 链最前面插入放行规则,带 `dsh-vpn` 标签 |
39
+ | nftables(原生规则集) | 在各个 input 链插入放行规则,带 `dsh-vpn` 标签 |
40
+ | 没有启用防火墙 | 不做任何改动 |
41
+
42
+ - 规则写在 `/etc/dsh-vpn/firewall.sh` 里,服务每次启动都会重新应用,所以 iptables/nftables 规则重启后也还在。
43
+ - 重新安装时,先关闭上次放行的端口,再放行新的,所以取消勾选的协议端口会被关掉。卸载时会删除插件添加的所有规则。
44
+ - 插件只添加和删除自己的规则,从不启用防火墙、修改默认策略或改动 SSH 端口。
45
+ - **云服务商的安全组**(阿里云、腾讯云、AWS 等)在服务器外部,只能在服务商控制台修改,插件改不了;如果有安全组,仍需在控制台放行同样的端口。
29
46
 
30
47
  ## 证书
31
48
 
@@ -100,6 +117,12 @@ limactl start --name=vps --vm-type=vz --network=vzNAT --set '.mounts=[]' templat
100
117
  DSH_VPN_E2E_HOST=192.168.64.2 DSH_VPN_E2E_USER=root DSH_VPN_E2E_PASSWORD=… pnpm test:e2e
101
118
  ```
102
119
 
120
+ 防火墙测试会依次启用 ufw、firewalld、iptables、nftables,**只能在测试虚拟机上运行**:
121
+
122
+ ```bash
123
+ DSH_VPN_E2E_FIREWALL=1 DSH_VPN_E2E_HOST=… DSH_VPN_E2E_USER=root DSH_VPN_E2E_PASSWORD=… pnpm test:e2e
124
+ ```
125
+
103
126
  IP 证书的测试需要一个本地 ACME 服务器,用 Let's Encrypt 官方的 [Pebble](https://github.com/letsencrypt/pebble) 模拟。**只能在测试虚拟机上运行**,因为它会把 Pebble 的根证书加入系统信任:
104
127
 
105
128
  ```bash
@@ -116,6 +139,6 @@ DSH_VPN_E2E_ACME=https://localhost:14000/dir DSH_VPN_E2E_HOST=… pnpm test:e2e
116
139
  - [ ] 可自定义 Reality 的 SNI、协议端口
117
140
  - [ ] 证书使用自己的域名(DNS 验证:Cloudflare / 阿里云)
118
141
  - [ ] IPv6 的 IP 证书(目前只在 IPv4 上验证过)
119
- - [ ] 支持 OpenRC(Alpine)和 iptables/nftables
142
+ - [ ] 支持 OpenRC(Alpine)
120
143
  - [ ] 支持 GitHub 下载镜像
121
144
  - [ ] 在真实 VPS 上跑端到端测试(Debian 12 / Ubuntu 24.04 / Rocky 9)
package/assets/vpn-ops.md CHANGED
@@ -9,7 +9,7 @@ Deploy and operate a personal sing-box server on the user's VPS. The plugin logs
9
9
  - Installed and active → offer `vpn_links` instead of reinstalling.
10
10
  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
11
  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. **Remind** the user to open the reported ports in the cloud provider's security group, using the right transport for each protocol.
12
+ 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
13
 
14
14
  ## Certificates
15
15
 
@@ -38,7 +38,7 @@ The settings page offers **self-signed** (default) or **IP certificate** (recomm
38
38
  | `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
39
  | `port80_in_use` | Another program (nginx, …) holds port 80 | Stop it, or use self-signed |
40
40
  | 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 or upstream firewall | Open the TCP/UDP ports in the cloud console |
41
+ | 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 |
42
42
  | Reality fails, others work | SNI site blocked | Reinstall with another SNI (future option) |
43
43
  | UDP protocols fail, TCP works | ISP blocks or throttles UDP | Use Reality or AnyTLS |
44
44
  | Everything stopped after working | IP blocked | `vpn_install` with `regenerate: true` for new ports; if it is still blocked, the IP must change |
package/lib/client.js CHANGED
@@ -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
- cloudFirewall: "\u63D0\u9192\uFF1A\u8FD8\u9700\u8981\u5728\u4E91\u670D\u52A1\u5546\u63A7\u5236\u53F0\u7684\u5B89\u5168\u7EC4\u91CC\u653E\u884C\u4EE5\u4E0B\u7AEF\u53E3\uFF1A{ports}",
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",
@@ -212,7 +215,10 @@ var en = {
212
215
  copy: "Copy link",
213
216
  copied: "Copied",
214
217
  portLabel: "Port {port}",
215
- cloudFirewall: "Reminder: also open these ports in your cloud provider's security group: {ports}",
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 portsReminder = (l) => t("cloudFirewall", {
1111
- ports: [
1112
- ...l.items.map((i) => transportsFor(i.id).map((tr) => `${i.port}/${tr.toUpperCase()}`).join(" ")),
1113
- ...l.cert === "ip" ? ["80/TCP"] : []
1114
- ].join(", ")
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")} ${v.links ? portsReminder(v.links) : ""}` };
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vpn",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "DeepSeek Harness plugin: one-click sing-box deployment to a VPS over SSH, with a settings page and QR codes",
5
5
  "keywords": [
6
6
  "dsh-plugin",
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
- open_firewall() {
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
- if command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q 'Status: active'; then
440
- log "opening ports in ufw: ${rules[*]}"
441
- for r in "${rules[@]}"; do ufw allow "$r" >/dev/null; done
442
- elif command -v firewall-cmd >/dev/null && firewall-cmd --state >/dev/null 2>&1; then
443
- log "opening ports in firewalld: ${rules[*]}"
444
- for r in "${rules[@]}"; do firewall-cmd -q --permanent --add-port="$r"; done
445
- firewall-cmd -q --reload
446
- fi
447
- # TODO: raw iptables/nftables; cloud security groups must still be opened by the user.
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
- result "{\"ok\":true,\"action\":\"status\",\"installed\":true,\"active\":$active,\"version\":\"$version\",\"protocols\":\"$PROTOCOLS\",\"cert\":$cert,\"listening\":{${listening[*]}}}"
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"