aio-plugin-portal 0.1.0 → 0.2.1-dev.6.gd90568e1c0fd
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 +43 -5
- package/config/aio-portal.example.json +42 -1
- package/dist/cli.mjs +610 -34
- package/package.json +20 -2
- package/skills/aio-plugin-portal/SKILL.md +47 -10
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 端口门户
|
|
2
2
|
|
|
3
|
-
把「别名 + 内网端口」登记成一条 `https://<别名>.<suffix
|
|
4
|
-
|
|
3
|
+
把「别名 + 内网端口」登记成一条 `https://<别名>.<suffix>`,并在**一套期望态**里同时驱动
|
|
4
|
+
DNS、反代、证书、访问策略四类能力。内网直达本机,公网按登记的策略决定是否可达。
|
|
5
5
|
|
|
6
6
|
## 安装与使用
|
|
7
7
|
|
|
@@ -14,13 +14,46 @@ aio-plugin-portal --help
|
|
|
14
14
|
必填域名后缀,其余按需设置;样例见 `config/aio-portal.example.json`。
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
|
-
aio-plugin-portal add tc --port 8111 --host
|
|
17
|
+
aio-plugin-portal add tc --port 8111 --host 127.0.0.1 --note TeamCity
|
|
18
|
+
aio-plugin-portal add only-public --port 9000 --visibility public --authorized-by alice
|
|
18
19
|
aio-plugin-portal list
|
|
19
|
-
aio-plugin-portal
|
|
20
|
+
aio-plugin-portal plan # 只出计划,不写任何东西
|
|
21
|
+
aio-plugin-portal sync # 渲染产物 + 执行 provider + 校验
|
|
22
|
+
aio-plugin-portal sync --apply # 允许 provider 真实写入
|
|
20
23
|
aio-plugin-portal doctor
|
|
21
24
|
aio-plugin-portal remove tc
|
|
22
25
|
```
|
|
23
26
|
|
|
27
|
+
## 内网 / 公网边界
|
|
28
|
+
|
|
29
|
+
每条登记都有 `visibility`,它是安全边界而不是展示字段:
|
|
30
|
+
|
|
31
|
+
- `internal`(默认):**永不产生公网 DNS 记录**;Caddy 站点同时要求 `bind <入口地址>` 与
|
|
32
|
+
`remote_ip` 来源白名单,未命中直接 `respond 404`。缺 `caddy.bind` 时 `verify` 判失败。
|
|
33
|
+
- `public`:必须同时满足 `allowPublicWrite: true`、有凭据、且登记了 `authorizedBy`,否则计划被标记
|
|
34
|
+
为 `blocked`,`sync --apply` 拒绝执行。缺任意一项宁可不动,也不“先写上去再说”。
|
|
35
|
+
|
|
36
|
+
`blocked` 的计划一律不 apply;`verify` 重新读取远端/产物,不用本地计划冒充成功。
|
|
37
|
+
`sync` 只有在全部 provider 计划可执行且校验通过时才报告成功。
|
|
38
|
+
|
|
39
|
+
> 边界代价:只在经过本机 Caddy 的路径上成立。若同名域名在公网 DNS 或别的入口仍指向旧隧道,
|
|
40
|
+
> 那部分流量不归本机管。真正“仅内网”还要清掉对应公网记录,这一步本工具不会替你猜。
|
|
41
|
+
|
|
42
|
+
## provider 抽象
|
|
43
|
+
|
|
44
|
+
四类 provider 各自独立组合,登记项可用 `providers` 字段单条覆盖:
|
|
45
|
+
|
|
46
|
+
| 类别 | 可用值 | 说明 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `dns` | `local-dnsmasq`、`cloudflare`、`dnspod`(腾讯云)、`huawei`(华为云) | 本地只写解析片段;云厂商走 `list → create/update/noop` 幂等协调 |
|
|
49
|
+
| `connectivity` | `caddy`、`aio-tunnel`、`cloudflared` | `caddy` 生成站点片段;隧道类只给计划,要求由隧道侧真实校验 |
|
|
50
|
+
| `certificate` | `internal-ca`、`acme` | `internal-ca` 由本地 Caddy internal issuer 签发;`acme` 需外部可达,未授权即 blocked |
|
|
51
|
+
| `policy` | `lan-allowlist`、`aio-auth`、`public` | `lan-allowlist` 生成 `remote_ip` 白名单;`aio-auth` 交给应用自身 |
|
|
52
|
+
|
|
53
|
+
云 DNS 凭据只从环境变量读取(`credentialsEnv` 指向变量名),不写进注册表或配置。
|
|
54
|
+
腾讯云 DNSPod 用 `Record.List/Create/Modify`;华为云使用 Zone ID + IAM `X-Auth-Token`,
|
|
55
|
+
**长期 AK/SK 签名尚未实现**,未配置 token 时只会给出可审计的计划。
|
|
56
|
+
|
|
24
57
|
## 让整片局域网无感走内网
|
|
25
58
|
|
|
26
59
|
客户端零配置的前提是**网关替所有设备做 DNS 覆盖**:把网关上游 DNS 指向本机解析器。
|
|
@@ -33,7 +66,12 @@ aio-plugin-portal warden --password '<网关密码>' --restore # 回滚
|
|
|
33
66
|
|
|
34
67
|
- 默认只读,写入必须显式 `--apply`;网关改动会影响**所有**经它上网的设备。
|
|
35
68
|
- 只想让个别机器走内网时,不动网关,改用客户端按域名分流(如 macOS `/etc/resolver/<suffix>`)。
|
|
36
|
-
|
|
69
|
+
|
|
70
|
+
## 与配对机制的边界
|
|
71
|
+
|
|
72
|
+
“配对”不是本工具的职责。设备配对复用 `aio-plugin-device` 的一次性短时票据闭环
|
|
73
|
+
(签发 → 登录确认 → 轮询换取凭据 → 心跳/租约 → 可撤销)。本工具只负责把已经存在的
|
|
74
|
+
目标端口登记成可达的站点,并把它的可见范围钉死在内网。
|
|
37
75
|
|
|
38
76
|
发布与市场配置见 [AIO 自动发布说明](AIO.md)。
|
|
39
77
|
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
"resolver": {
|
|
7
7
|
"port": 53,
|
|
8
8
|
"upstreams": ["223.5.5.5", "114.114.114.114"],
|
|
9
|
-
"service": "aio-portal-dns"
|
|
9
|
+
"service": "aio-portal-dns",
|
|
10
|
+
"provider": "dnsmasq",
|
|
11
|
+
"fragment": true,
|
|
12
|
+
"configFile": "/etc/dnsmasq.d/intranet/20-aio-portal.conf",
|
|
13
|
+
"testCommand": ["dnsmasq", "--test"]
|
|
10
14
|
},
|
|
11
15
|
"portal": {
|
|
12
16
|
"root": "/opt/aio-portal",
|
|
@@ -17,5 +21,42 @@
|
|
|
17
21
|
"address": "192.168.1.1",
|
|
18
22
|
"adapter": "xiaomi",
|
|
19
23
|
"fallbackDns": "223.5.5.5"
|
|
24
|
+
},
|
|
25
|
+
"caddy": {
|
|
26
|
+
"sitesDir": "/opt/aio-portal/caddy/sites",
|
|
27
|
+
"prefix": "portal-",
|
|
28
|
+
"bind": "192.168.1.10",
|
|
29
|
+
"allow": ["192.168.1.0/24", "10.144.144.0/24"],
|
|
30
|
+
"denyStatus": 404,
|
|
31
|
+
"binary": "caddy"
|
|
32
|
+
},
|
|
33
|
+
"providers": {
|
|
34
|
+
"dns": "local-dnsmasq",
|
|
35
|
+
"connectivity": "caddy",
|
|
36
|
+
"certificate": "internal-ca",
|
|
37
|
+
"policy": "lan-allowlist",
|
|
38
|
+
"cloud": {
|
|
39
|
+
"cloudflare": {
|
|
40
|
+
"enabled": false,
|
|
41
|
+
"zone": "example.internal",
|
|
42
|
+
"ttl": 120,
|
|
43
|
+
"credentialsEnv": "CLOUDFLARE_API_TOKEN",
|
|
44
|
+
"allowPublicWrite": false
|
|
45
|
+
},
|
|
46
|
+
"dnspod": {
|
|
47
|
+
"enabled": false,
|
|
48
|
+
"zone": "example.internal",
|
|
49
|
+
"ttl": 120,
|
|
50
|
+
"credentialsEnv": "TENCENT_CLOUD_SECRET_ID",
|
|
51
|
+
"allowPublicWrite": false
|
|
52
|
+
},
|
|
53
|
+
"huawei": {
|
|
54
|
+
"enabled": false,
|
|
55
|
+
"zone": "example.internal",
|
|
56
|
+
"ttl": 120,
|
|
57
|
+
"credentialsEnv": "HUAWEI_CLOUD_TOKEN",
|
|
58
|
+
"allowPublicWrite": false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
20
61
|
}
|
|
21
62
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
|
|
6
6
|
// src/portal/commands.ts
|
|
7
|
-
import { existsSync as
|
|
7
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
8
8
|
|
|
9
9
|
// src/config/index.ts
|
|
10
10
|
import { homedir } from "node:os";
|
|
@@ -15,14 +15,20 @@ var DEFAULTS = () => ({
|
|
|
15
15
|
mode: "lan",
|
|
16
16
|
lanIp: "",
|
|
17
17
|
hostsFile: "/etc/hosts",
|
|
18
|
-
resolver: { port: 53, upstreams: ["223.5.5.5", "114.114.114.114"] },
|
|
18
|
+
resolver: { port: 53, upstreams: ["223.5.5.5", "114.114.114.114"], provider: "dnsmasq" },
|
|
19
19
|
portal: { root: join(homedir(), ".config", "aio-portal"), mapPrefix: "10-alias", reload: true },
|
|
20
|
-
gateway: { adapter: "xiaomi", fallbackDns: "223.5.5.5" }
|
|
20
|
+
gateway: { adapter: "xiaomi", fallbackDns: "223.5.5.5" },
|
|
21
|
+
caddy: { prefix: "portal-", allow: ["127.0.0.1/32"], denyStatus: 404 },
|
|
22
|
+
providers: {
|
|
23
|
+
dns: "local-dnsmasq",
|
|
24
|
+
connectivity: "caddy",
|
|
25
|
+
certificate: "internal-ca",
|
|
26
|
+
policy: "lan-allowlist"
|
|
27
|
+
}
|
|
21
28
|
});
|
|
22
29
|
function loadSettings(rootOverride) {
|
|
23
30
|
const base = DEFAULTS();
|
|
24
31
|
const root = resolve(rootOverride ?? process.env.AIO_PORTAL_HOME ?? base.portal.root);
|
|
25
|
-
base.portal.root = root;
|
|
26
32
|
const configFile = join(root, "aio-portal.json");
|
|
27
33
|
let file = {};
|
|
28
34
|
if (existsSync(configFile)) file = JSON.parse(readFileSync(configFile, "utf8"));
|
|
@@ -31,20 +37,27 @@ function loadSettings(rootOverride) {
|
|
|
31
37
|
...file,
|
|
32
38
|
resolver: { ...base.resolver, ...file.resolver },
|
|
33
39
|
portal: { ...base.portal, ...file.portal, root },
|
|
34
|
-
gateway: { ...base.gateway, ...file.gateway }
|
|
40
|
+
gateway: { ...base.gateway, ...file.gateway },
|
|
41
|
+
caddy: { ...base.caddy, ...file.caddy },
|
|
42
|
+
providers: { ...base.providers, ...file.providers }
|
|
35
43
|
};
|
|
36
44
|
if (!merged.suffix) throw new Error(`\u7F3A\u5C11\u57DF\u540D\u540E\u7F00\u914D\u7F6E\uFF0C\u8BF7\u5728 ${configFile} \u4E2D\u8BBE\u7F6E suffix`);
|
|
37
45
|
merged.portal.root = root;
|
|
38
46
|
merged.resolver.configFile ??= join(root, "dns", "aio-portal.conf");
|
|
39
47
|
merged.portal.aliasesDir ??= join(root, "nginx", "aliases.d");
|
|
48
|
+
if (merged.caddy && !merged.caddy.sitesDir) merged.caddy.sitesDir = join(root, "caddy", "sites");
|
|
40
49
|
return merged;
|
|
41
50
|
}
|
|
42
51
|
var registryFile = (s) => join(s.portal.root, "aliases.json");
|
|
52
|
+
function normalizeAlias(item) {
|
|
53
|
+
const visibility = item.visibility ?? (item.lan === false ? "public" : "internal");
|
|
54
|
+
return { ...item, visibility, lan: visibility === "internal" };
|
|
55
|
+
}
|
|
43
56
|
function loadRegistry(s) {
|
|
44
57
|
const file = registryFile(s);
|
|
45
58
|
if (!existsSync(file)) return { aliases: [] };
|
|
46
59
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
47
|
-
return { aliases: parsed.aliases ?? [] };
|
|
60
|
+
return { aliases: (parsed.aliases ?? []).map(normalizeAlias) };
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
// src/portal/gateway.ts
|
|
@@ -163,6 +176,10 @@ function writeRegistry(s, registry) {
|
|
|
163
176
|
`, { mode: 384 });
|
|
164
177
|
renameSync(tmp, file);
|
|
165
178
|
}
|
|
179
|
+
function snapshot(s) {
|
|
180
|
+
const file = registryFile(s);
|
|
181
|
+
return existsSync2(file) ? readFileSync2(file, "utf8") : null;
|
|
182
|
+
}
|
|
166
183
|
function upsert(s, entry) {
|
|
167
184
|
const registry = loadRegistry(s);
|
|
168
185
|
const next = registry.aliases.filter((item) => item.alias !== entry.alias);
|
|
@@ -186,13 +203,475 @@ function readAliases(s) {
|
|
|
186
203
|
|
|
187
204
|
// src/portal/render.ts
|
|
188
205
|
import { execFileSync } from "node:child_process";
|
|
189
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
206
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
190
207
|
import { join as join2 } from "node:path";
|
|
208
|
+
|
|
209
|
+
// src/portal/providers.ts
|
|
210
|
+
var fqdn2 = (settings2, alias) => `${alias}.${settings2.suffix}`;
|
|
211
|
+
var isInternal = (alias) => (alias.visibility ?? (alias.lan === false ? "public" : "internal")) === "internal";
|
|
212
|
+
var cloudConfig = (settings2, alias) => {
|
|
213
|
+
const provider = alias.providers?.dns ?? settings2.providers?.dns ?? "local-dnsmasq";
|
|
214
|
+
return { provider, config: settings2.providers?.cloud?.[provider] };
|
|
215
|
+
};
|
|
216
|
+
var credential = (settings2, alias) => {
|
|
217
|
+
const { config } = cloudConfig(settings2, alias);
|
|
218
|
+
return config?.credentialsEnv ? process.env[config.credentialsEnv] : void 0;
|
|
219
|
+
};
|
|
220
|
+
var timeout = () => AbortSignal.timeout(15e3);
|
|
221
|
+
var redact = (text) => text.replace(/(token|key|secret)[^&\s"]*[=:]\s*[^&\s"]+/gi, "$1=<redacted>");
|
|
222
|
+
var CloudDnsProvider = class {
|
|
223
|
+
kind = "dns";
|
|
224
|
+
async plan(alias, settings2) {
|
|
225
|
+
const zone = this.zone(settings2, alias);
|
|
226
|
+
const { config } = cloudConfig(settings2, alias);
|
|
227
|
+
const warnings = [];
|
|
228
|
+
const changes = [];
|
|
229
|
+
if (!zone) {
|
|
230
|
+
warnings.push(`\u672A\u914D\u7F6E ${this.name}.zone\uFF0C\u65E0\u6CD5\u4EA7\u751F\u4E91 DNS \u8BA1\u5212`);
|
|
231
|
+
return { provider: this.name, kind: this.kind, changes, warnings, blocked: true };
|
|
232
|
+
}
|
|
233
|
+
if (isInternal(alias)) {
|
|
234
|
+
return {
|
|
235
|
+
provider: this.name,
|
|
236
|
+
kind: this.kind,
|
|
237
|
+
changes: [{ kind: "noop", resource: fqdn2(settings2, alias.alias), detail: "internal \u522B\u540D\u7981\u6B62\u521B\u5EFA\u516C\u7F51\u8BB0\u5F55" }],
|
|
238
|
+
warnings
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (!config?.allowPublicWrite) {
|
|
242
|
+
warnings.push("\u516C\u7F51 DNS \u5199\u5165\u672A\u663E\u5F0F\u5F00\u542F\uFF08allowPublicWrite=false\uFF09\uFF0C\u4EC5\u751F\u6210\u8BA1\u5212");
|
|
243
|
+
return { provider: this.name, kind: this.kind, changes, warnings, blocked: true };
|
|
244
|
+
}
|
|
245
|
+
if (!alias.authorizedBy) {
|
|
246
|
+
warnings.push("public \u522B\u540D\u7F3A\u5C11 authorizedBy\uFF0C\u62D2\u7EDD\u516C\u7F51\u5199\u5165");
|
|
247
|
+
return { provider: this.name, kind: this.kind, changes, warnings, blocked: true };
|
|
248
|
+
}
|
|
249
|
+
const token = credential(settings2, alias);
|
|
250
|
+
if (!token) {
|
|
251
|
+
warnings.push(`\u7F3A\u5C11\u51ED\u636E\u73AF\u5883\u53D8\u91CF ${config.credentialsEnv ?? "\uFF08\u672A\u914D\u7F6E\uFF09"}\uFF0C\u4E0D\u6267\u884C\u771F\u5B9E\u5199\u5165`);
|
|
252
|
+
return { provider: this.name, kind: this.kind, changes, warnings, blocked: true };
|
|
253
|
+
}
|
|
254
|
+
const name = fqdn2(settings2, alias.alias);
|
|
255
|
+
const value = settings2.lanIp ?? "";
|
|
256
|
+
if (!value) {
|
|
257
|
+
warnings.push("\u672A\u914D\u7F6E lanIp\uFF0C\u516C\u7F51 A \u8BB0\u5F55\u503C\u5C06\u4E3A\u7A7A");
|
|
258
|
+
return { provider: this.name, kind: this.kind, changes, warnings, blocked: true };
|
|
259
|
+
}
|
|
260
|
+
const record = { name, value, ttl: config.ttl ?? 120 };
|
|
261
|
+
try {
|
|
262
|
+
const existing = await this.list(zone, name, token);
|
|
263
|
+
if (!existing) {
|
|
264
|
+
changes.push({ kind: "create", resource: `${this.name}:${name}`, detail: `A ${value} ttl=${record.ttl}` });
|
|
265
|
+
} else if (existing.value === value) {
|
|
266
|
+
changes.push({ kind: "noop", resource: `${this.name}:${name}`, detail: `\u5DF2\u662F A ${value}` });
|
|
267
|
+
} else {
|
|
268
|
+
changes.push({ kind: "update", resource: `${this.name}:${name}`, detail: `A ${existing.value} -> ${value}` });
|
|
269
|
+
}
|
|
270
|
+
return { provider: this.name, kind: this.kind, changes, warnings };
|
|
271
|
+
} catch (error) {
|
|
272
|
+
warnings.push(`\u8BFB\u53D6\u8FDC\u7AEF\u5931\u8D25\uFF0C\u672A\u751F\u6210\u5199\u5165\u8BA1\u5212\uFF1A${redact(error instanceof Error ? error.message : String(error))}`);
|
|
273
|
+
return { provider: this.name, kind: this.kind, changes, warnings, blocked: true };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async apply(alias, settings2, plan2) {
|
|
277
|
+
if (plan2.blocked) throw new Error(`${this.name}: \u8BA1\u5212\u672A\u901A\u8FC7\u5B89\u5168\u68C0\u67E5\uFF0C\u62D2\u7EDD\u5199\u5165`);
|
|
278
|
+
const zone = this.zone(settings2, alias);
|
|
279
|
+
const token = credential(settings2, alias);
|
|
280
|
+
if (!zone || !token) throw new Error(`${this.name}: \u7F3A\u5C11 zone \u6216\u51ED\u636E`);
|
|
281
|
+
const { config } = cloudConfig(settings2, alias);
|
|
282
|
+
const name = fqdn2(settings2, alias.alias);
|
|
283
|
+
const record = { name, value: settings2.lanIp ?? "", ttl: config?.ttl ?? 120 };
|
|
284
|
+
const change = plan2.changes.find((item) => item.kind !== "noop");
|
|
285
|
+
if (!change) return;
|
|
286
|
+
const existing = await this.list(zone, name, token);
|
|
287
|
+
if (change.kind === "create") {
|
|
288
|
+
await this.create(zone, record, token);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (change.kind === "update" && existing) {
|
|
292
|
+
await this.update(zone, record, existing, token);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
throw new Error(`${this.name}: \u8FDC\u7AEF\u72B6\u6001\u4E0E\u8BA1\u5212\u4E0D\u4E00\u81F4\uFF08${change.kind}\uFF09\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C plan`);
|
|
296
|
+
}
|
|
297
|
+
async verify(alias, settings2) {
|
|
298
|
+
if (isInternal(alias)) return { ok: true, detail: "internal \u522B\u540D\u4E0D\u68C0\u67E5\u516C\u7F51 DNS" };
|
|
299
|
+
const zone = this.zone(settings2, alias);
|
|
300
|
+
const token = credential(settings2, alias);
|
|
301
|
+
if (!zone || !token) return { ok: false, detail: "\u7F3A\u5C11 zone \u6216\u51ED\u636E\uFF0C\u65E0\u6CD5\u6821\u9A8C\u516C\u7F51\u8BB0\u5F55" };
|
|
302
|
+
try {
|
|
303
|
+
const existing = await this.list(zone, fqdn2(settings2, alias.alias), token);
|
|
304
|
+
const value = settings2.lanIp ?? "";
|
|
305
|
+
if (!existing) return { ok: false, detail: "\u516C\u7F51\u8BB0\u5F55\u4E0D\u5B58\u5728" };
|
|
306
|
+
return existing.value === value ? { ok: true, detail: `\u516C\u7F51 A ${existing.value} \u5DF2\u5B58\u5728` } : { ok: false, detail: `\u516C\u7F51 A \u4E3A ${existing.value}\uFF0C\u671F\u671B ${value}` };
|
|
307
|
+
} catch (error) {
|
|
308
|
+
return { ok: false, detail: `\u516C\u7F51\u6821\u9A8C\u5931\u8D25\uFF1A${redact(error instanceof Error ? error.message : String(error))}` };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
async function jsonRequest(url, init) {
|
|
313
|
+
const response = await fetch(url, { ...init, signal: timeout() });
|
|
314
|
+
const text = await response.text();
|
|
315
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${redact(text).slice(0, 500)}`);
|
|
316
|
+
return text ? JSON.parse(text) : {};
|
|
317
|
+
}
|
|
318
|
+
var CloudflareDnsProvider = class extends CloudDnsProvider {
|
|
319
|
+
name = "cloudflare";
|
|
320
|
+
zone(settings2, alias) {
|
|
321
|
+
return cloudConfig(settings2, alias).config?.zone;
|
|
322
|
+
}
|
|
323
|
+
base(zone) {
|
|
324
|
+
return `https://api.cloudflare.com/client/v4/zones/${zone}/dns_records`;
|
|
325
|
+
}
|
|
326
|
+
headers(token) {
|
|
327
|
+
return { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
328
|
+
}
|
|
329
|
+
async list(zone, name, token) {
|
|
330
|
+
const data = await jsonRequest(`${this.base(zone)}?type=A&name=${encodeURIComponent(name)}`, { headers: this.headers(token) });
|
|
331
|
+
const rows = data.result ?? [];
|
|
332
|
+
const row = rows[0];
|
|
333
|
+
if (!row) return null;
|
|
334
|
+
return { id: String(row.id), value: String(row.content) };
|
|
335
|
+
}
|
|
336
|
+
async create(zone, record, token) {
|
|
337
|
+
await jsonRequest(this.base(zone), {
|
|
338
|
+
method: "POST",
|
|
339
|
+
headers: this.headers(token),
|
|
340
|
+
body: JSON.stringify({ type: "A", name: record.name, content: record.value, ttl: record.ttl, proxied: false })
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
async update(zone, record, existing, token) {
|
|
344
|
+
await jsonRequest(`${this.base(zone)}/${existing.id}`, {
|
|
345
|
+
method: "PATCH",
|
|
346
|
+
headers: this.headers(token),
|
|
347
|
+
body: JSON.stringify({ type: "A", name: record.name, content: record.value, ttl: record.ttl, proxied: false })
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
var DnspodDnsProvider = class extends CloudDnsProvider {
|
|
352
|
+
name = "dnspod";
|
|
353
|
+
zone(settings2, alias) {
|
|
354
|
+
return cloudConfig(settings2, alias).config?.zone;
|
|
355
|
+
}
|
|
356
|
+
async call(path, form, token) {
|
|
357
|
+
const body = new URLSearchParams({ login_token: token, format: "json", lang: "cn", ...form });
|
|
358
|
+
const response = await fetch(`https://dnsapi.cn/${path}`, {
|
|
359
|
+
method: "POST",
|
|
360
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
361
|
+
body,
|
|
362
|
+
signal: timeout()
|
|
363
|
+
});
|
|
364
|
+
const text = await response.text();
|
|
365
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${redact(text).slice(0, 500)}`);
|
|
366
|
+
const data = JSON.parse(text);
|
|
367
|
+
if (data.status?.code !== "1") throw new Error(`DNSPod: ${data.status?.code ?? "?"} ${data.status?.message ?? text}`);
|
|
368
|
+
return data;
|
|
369
|
+
}
|
|
370
|
+
split(zone, name) {
|
|
371
|
+
if (name === zone) return { domain: zone, sub: "@" };
|
|
372
|
+
if (!name.endsWith(`.${zone}`)) throw new Error(`DNSPod: ${name} \u4E0D\u5C5E\u4E8E zone ${zone}`);
|
|
373
|
+
return { domain: zone, sub: name.slice(0, -(zone.length + 1)) };
|
|
374
|
+
}
|
|
375
|
+
async list(zone, name, token) {
|
|
376
|
+
const { domain, sub } = this.split(zone, name);
|
|
377
|
+
const data = await this.call("Record.List", { domain, sub_domain: sub, record_type: "A" }, token);
|
|
378
|
+
const rows = data.records ?? [];
|
|
379
|
+
const row = rows.find((item) => item.type === "A");
|
|
380
|
+
if (!row) return null;
|
|
381
|
+
return { id: String(row.id), value: String(row.value) };
|
|
382
|
+
}
|
|
383
|
+
async create(zone, record, token) {
|
|
384
|
+
const { domain, sub } = this.split(zone, record.name);
|
|
385
|
+
await this.call("Record.Create", {
|
|
386
|
+
domain,
|
|
387
|
+
sub_domain: sub,
|
|
388
|
+
record_type: "A",
|
|
389
|
+
record_line: "\u9ED8\u8BA4",
|
|
390
|
+
value: record.value,
|
|
391
|
+
ttl: String(record.ttl)
|
|
392
|
+
}, token);
|
|
393
|
+
}
|
|
394
|
+
async update(zone, record, existing, token) {
|
|
395
|
+
const { domain, sub } = this.split(zone, record.name);
|
|
396
|
+
await this.call("Record.Modify", {
|
|
397
|
+
domain,
|
|
398
|
+
record_id: existing.id,
|
|
399
|
+
sub_domain: sub,
|
|
400
|
+
record_type: "A",
|
|
401
|
+
record_line: "\u9ED8\u8BA4",
|
|
402
|
+
value: record.value,
|
|
403
|
+
ttl: String(record.ttl)
|
|
404
|
+
}, token);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
var HuaweiDnsProvider = class extends CloudDnsProvider {
|
|
408
|
+
name = "huawei";
|
|
409
|
+
zone(settings2, alias) {
|
|
410
|
+
return cloudConfig(settings2, alias).config?.zone;
|
|
411
|
+
}
|
|
412
|
+
base(zone) {
|
|
413
|
+
return `https://dns.myhuaweicloud.com/v2/zones/${zone}/recordsets`;
|
|
414
|
+
}
|
|
415
|
+
headers(token) {
|
|
416
|
+
return { "x-auth-token": token, "content-type": "application/json" };
|
|
417
|
+
}
|
|
418
|
+
async list(zone, name, token) {
|
|
419
|
+
const url = `${this.base(zone)}?name=${encodeURIComponent(`${name}.`)}&type=A`;
|
|
420
|
+
const data = await jsonRequest(url, { headers: this.headers(token) });
|
|
421
|
+
const rows = data.recordsets ?? [];
|
|
422
|
+
const row = rows.find((item) => item.type === "A");
|
|
423
|
+
if (!row) return null;
|
|
424
|
+
const values = row.records ?? [];
|
|
425
|
+
return { id: String(row.id), value: String(values[0] ?? "") };
|
|
426
|
+
}
|
|
427
|
+
async create(zone, record, token) {
|
|
428
|
+
await jsonRequest(this.base(zone), {
|
|
429
|
+
method: "POST",
|
|
430
|
+
headers: this.headers(token),
|
|
431
|
+
body: JSON.stringify({ name: `${record.name}.`, type: "A", ttl: record.ttl, records: [record.value] })
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
async update(zone, record, existing, token) {
|
|
435
|
+
await jsonRequest(`${this.base(zone)}/${existing.id}`, {
|
|
436
|
+
method: "PUT",
|
|
437
|
+
headers: this.headers(token),
|
|
438
|
+
body: JSON.stringify({ name: `${record.name}.`, type: "A", ttl: record.ttl, records: [record.value] })
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
var LocalDnsmasqProvider = class {
|
|
443
|
+
name = "local-dnsmasq";
|
|
444
|
+
kind = "dns";
|
|
445
|
+
async plan(alias, settings2) {
|
|
446
|
+
const name = fqdn2(settings2, alias.alias);
|
|
447
|
+
if (!isInternal(alias)) {
|
|
448
|
+
return { provider: this.name, kind: this.kind, changes: [], warnings: ["public \u522B\u540D\u4E0D\u4F7F\u7528\u5185\u7F51 DNS \u8986\u76D6"] };
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
provider: this.name,
|
|
452
|
+
kind: this.kind,
|
|
453
|
+
changes: [{ kind: "create", resource: name, detail: `address=/${name}/${settings2.lanIp ?? ""}` }],
|
|
454
|
+
warnings: settings2.lanIp ? [] : ["\u672A\u914D\u7F6E lanIp"],
|
|
455
|
+
blocked: !settings2.lanIp
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
async apply() {
|
|
459
|
+
}
|
|
460
|
+
async verify(alias, settings2) {
|
|
461
|
+
return { ok: isInternal(alias) && Boolean(settings2.lanIp), detail: isInternal(alias) ? "\u7B49\u5F85 render \u540E\u6821\u9A8C" : "public \u522B\u540D\u4E0D\u5199\u5185\u7F51 DNS" };
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
var CaddyConnectivityProvider = class {
|
|
465
|
+
name = "caddy";
|
|
466
|
+
kind = "connectivity";
|
|
467
|
+
async plan(alias, settings2) {
|
|
468
|
+
const name = fqdn2(settings2, alias.alias);
|
|
469
|
+
const warnings = [];
|
|
470
|
+
if (isInternal(alias) && !settings2.caddy?.bind) warnings.push("internal \u522B\u540D\u7F3A\u5C11 caddy.bind\uFF0C\u516C\u7F51\u5165\u53E3\u4ECD\u53EF\u80FD\u547D\u4E2D");
|
|
471
|
+
if (isInternal(alias) && !settings2.caddy?.allow?.length) warnings.push("internal \u522B\u540D\u7F3A\u5C11 caddy.allow\uFF0C\u6765\u6E90\u4E0D\u53D7\u9650");
|
|
472
|
+
return {
|
|
473
|
+
provider: this.name,
|
|
474
|
+
kind: this.kind,
|
|
475
|
+
changes: [{ kind: "update", resource: `caddy:${name}`, detail: `reverse_proxy ${alias.scheme}://${alias.host}:${alias.port}` }],
|
|
476
|
+
warnings,
|
|
477
|
+
blocked: isInternal(alias) && (!settings2.caddy?.bind || !settings2.caddy?.allow?.length)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async apply() {
|
|
481
|
+
}
|
|
482
|
+
async verify(alias, settings2) {
|
|
483
|
+
if (!isInternal(alias)) return { ok: true, detail: "public \u522B\u540D\u4E0D\u505A\u6765\u6E90\u9650\u5236" };
|
|
484
|
+
const ok = Boolean(settings2.caddy?.bind && settings2.caddy?.allow?.length);
|
|
485
|
+
return { ok, detail: ok ? "internal \u7AD9\u70B9\u5DF2\u914D\u7F6E bind + allowlist" : "\u7F3A\u5C11 bind \u6216 allowlist" };
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var InternalCaProvider = class {
|
|
489
|
+
name = "internal-ca";
|
|
490
|
+
kind = "certificate";
|
|
491
|
+
async plan() {
|
|
492
|
+
return { provider: this.name, kind: this.kind, changes: [], warnings: ["\u8BC1\u4E66\u7531 localmesh Caddy \u7684 internal issuer \u7EDF\u4E00\u7B7E\u53D1"] };
|
|
493
|
+
}
|
|
494
|
+
async apply() {
|
|
495
|
+
}
|
|
496
|
+
async verify() {
|
|
497
|
+
return { ok: true, detail: "internal-ca \u7531 Caddy \u8D1F\u8D23" };
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
var AcmeProvider = class {
|
|
501
|
+
name = "acme";
|
|
502
|
+
kind = "certificate";
|
|
503
|
+
async plan() {
|
|
504
|
+
return { provider: this.name, kind: this.kind, changes: [], warnings: ["ACME \u9700\u8981\u5916\u90E8\u53EF\u8FBE\u7684 80/443 \u4E0E DNS \u51ED\u636E\uFF0C\u672C\u5730\u4E0D\u4F2A\u9020\u7B7E\u53D1"], blocked: true };
|
|
505
|
+
}
|
|
506
|
+
async apply() {
|
|
507
|
+
throw new Error("ACME \u672A\u914D\u7F6E\u6388\u6743\uFF0C\u62D2\u7EDD\u5199\u5165");
|
|
508
|
+
}
|
|
509
|
+
async verify() {
|
|
510
|
+
return { ok: false, detail: "ACME \u9700\u8981\u771F\u5B9E\u8FDC\u7AEF\u9A8C\u8BC1" };
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
var LanAllowlistProvider = class {
|
|
514
|
+
name = "lan-allowlist";
|
|
515
|
+
kind = "policy";
|
|
516
|
+
async plan(alias, settings2) {
|
|
517
|
+
if (!isInternal(alias)) return { provider: this.name, kind: this.kind, changes: [], warnings: ["public \u522B\u540D\u9700\u663E\u5F0F\u6388\u6743"] };
|
|
518
|
+
return {
|
|
519
|
+
provider: this.name,
|
|
520
|
+
kind: this.kind,
|
|
521
|
+
changes: [{ kind: "noop", resource: fqdn2(settings2, alias.alias), detail: `allow ${settings2.caddy?.allow?.join(" ") ?? ""} deny ${settings2.caddy?.denyStatus ?? 404}` }],
|
|
522
|
+
warnings: []
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
async apply() {
|
|
526
|
+
}
|
|
527
|
+
async verify(alias, settings2) {
|
|
528
|
+
if (!isInternal(alias) && !alias.authorizedBy) return { ok: false, detail: "public \u522B\u540D\u7F3A\u5C11 authorizedBy" };
|
|
529
|
+
return { ok: true, detail: "\u7B56\u7565\u6EE1\u8DB3" };
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
function dnsProvider(settings2, alias) {
|
|
533
|
+
const name = alias.providers?.dns ?? settings2.providers?.dns ?? "local-dnsmasq";
|
|
534
|
+
switch (name) {
|
|
535
|
+
case "local-dnsmasq":
|
|
536
|
+
return new LocalDnsmasqProvider();
|
|
537
|
+
case "cloudflare":
|
|
538
|
+
return new CloudflareDnsProvider();
|
|
539
|
+
case "dnspod":
|
|
540
|
+
return new DnspodDnsProvider();
|
|
541
|
+
case "huawei":
|
|
542
|
+
return new HuaweiDnsProvider();
|
|
543
|
+
default:
|
|
544
|
+
throw new Error(`\u4E0D\u652F\u6301\u7684 DNS provider\uFF1A${String(name)}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function connectivityProvider(settings2, alias) {
|
|
548
|
+
const name = alias.providers?.connectivity ?? settings2.providers?.connectivity ?? "caddy";
|
|
549
|
+
switch (name) {
|
|
550
|
+
case "caddy":
|
|
551
|
+
return new CaddyConnectivityProvider();
|
|
552
|
+
case "aio-tunnel":
|
|
553
|
+
case "cloudflared":
|
|
554
|
+
return {
|
|
555
|
+
name,
|
|
556
|
+
kind: "connectivity",
|
|
557
|
+
async plan() {
|
|
558
|
+
return {
|
|
559
|
+
provider: name,
|
|
560
|
+
kind: "connectivity",
|
|
561
|
+
changes: [],
|
|
562
|
+
warnings: [`${name} \u7531 AIO \u8BBE\u5907\u96A7\u9053/\u5916\u90E8\u96A7\u9053\u7BA1\u7406\uFF1B\u8BF7\u5728\u96A7\u9053\u4FA7\u7528\u8BBE\u5907\u914D\u5BF9\u4E0E\u77ED\u65F6\u7968\u636E\u6821\u9A8C\uFF0C\u4E0D\u5728\u672C\u5730 Caddy \u4F2A\u9020\u6210\u529F`],
|
|
563
|
+
blocked: true
|
|
564
|
+
};
|
|
565
|
+
},
|
|
566
|
+
async apply() {
|
|
567
|
+
throw new Error(`${name} \u9700\u8981\u5916\u90E8\u96A7\u9053\u7F16\u6392\uFF0C\u62D2\u7EDD\u672C\u5730\u5199\u5165`);
|
|
568
|
+
},
|
|
569
|
+
async verify() {
|
|
570
|
+
return { ok: false, detail: `${name} \u9700\u8981\u771F\u5B9E\u96A7\u9053\u5065\u5EB7\u68C0\u67E5\uFF08AIO \u8BBE\u5907\u5728\u7EBF + \u76EE\u6807\u5141\u8BB8 + \u7968\u636E\u6709\u6548\uFF09` };
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
default:
|
|
574
|
+
throw new Error(`\u4E0D\u652F\u6301\u7684\u8FDE\u901A\u6027 provider\uFF1A${String(name)}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function certificateProvider(settings2, alias) {
|
|
578
|
+
const name = alias.providers?.certificate ?? settings2.providers?.certificate ?? "internal-ca";
|
|
579
|
+
return name === "acme" ? new AcmeProvider() : new InternalCaProvider();
|
|
580
|
+
}
|
|
581
|
+
function policyProvider(settings2, alias) {
|
|
582
|
+
const name = alias.providers?.policy ?? settings2.providers?.policy ?? "lan-allowlist";
|
|
583
|
+
if (name === "public") {
|
|
584
|
+
return {
|
|
585
|
+
name,
|
|
586
|
+
kind: "policy",
|
|
587
|
+
async plan() {
|
|
588
|
+
return { provider: name, kind: "policy", changes: [], warnings: ["\u9700\u663E\u5F0F public \u6388\u6743"] };
|
|
589
|
+
},
|
|
590
|
+
async apply() {
|
|
591
|
+
},
|
|
592
|
+
async verify() {
|
|
593
|
+
return { ok: true, detail: "public \u7B56\u7565\u7531\u6CE8\u518C\u8868\u6388\u6743\u5B57\u6BB5\u4FDD\u8BC1" };
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (name === "aio-auth") {
|
|
598
|
+
return {
|
|
599
|
+
name,
|
|
600
|
+
kind: "policy",
|
|
601
|
+
async plan() {
|
|
602
|
+
return { provider: name, kind: "policy", changes: [], warnings: ["AIO \u8BA4\u8BC1\u7531\u5E94\u7528\u81EA\u8EAB\u8D1F\u8D23"] };
|
|
603
|
+
},
|
|
604
|
+
async apply() {
|
|
605
|
+
},
|
|
606
|
+
async verify() {
|
|
607
|
+
return { ok: true, detail: "AIO \u8BA4\u8BC1\u7531\u5E94\u7528\u8D1F\u8D23" };
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
return new LanAllowlistProvider();
|
|
612
|
+
}
|
|
613
|
+
function providersFor(settings2, alias) {
|
|
614
|
+
return [
|
|
615
|
+
dnsProvider(settings2, alias),
|
|
616
|
+
connectivityProvider(settings2, alias),
|
|
617
|
+
certificateProvider(settings2, alias),
|
|
618
|
+
policyProvider(settings2, alias)
|
|
619
|
+
];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/portal/render.ts
|
|
191
623
|
var writeLines = (path, lines) => {
|
|
192
624
|
writeFileSync2(path, lines.length ? `${lines.join("\n")}
|
|
193
625
|
` : "");
|
|
194
626
|
};
|
|
195
|
-
|
|
627
|
+
var quote = (value) => /^[A-Za-z0-9_.:/\[\]-]+$/.test(value) ? value : JSON.stringify(value);
|
|
628
|
+
function caddySite(settings2, item) {
|
|
629
|
+
const name = aliasFqdn(settings2, item.alias);
|
|
630
|
+
const target = `${item.scheme}://${item.host}:${item.port}`;
|
|
631
|
+
const lines = [`# generated by aio-plugin-portal; alias=${item.alias} visibility=${item.visibility ?? "internal"}`, `${name} {`, " import defaults"];
|
|
632
|
+
if (isInternal(item)) {
|
|
633
|
+
const bind = settings2.caddy?.bind;
|
|
634
|
+
if (bind) lines.push(` bind ${quote(bind)}`);
|
|
635
|
+
const allow = settings2.caddy?.allow?.length ? settings2.caddy.allow : ["127.0.0.1/32"];
|
|
636
|
+
lines.push(` @internal remote_ip ${allow.join(" ")}`);
|
|
637
|
+
lines.push(" handle @internal {", " encode gzip", ` reverse_proxy ${quote(target)}`, " }");
|
|
638
|
+
lines.push(" handle {", ` respond ${settings2.caddy?.denyStatus ?? 404}`, " }");
|
|
639
|
+
} else {
|
|
640
|
+
lines.push(" encode gzip", ` reverse_proxy ${quote(target)}`);
|
|
641
|
+
}
|
|
642
|
+
lines.push("}", "");
|
|
643
|
+
return lines.join("\n");
|
|
644
|
+
}
|
|
645
|
+
function cleanGenerated(dir, prefix) {
|
|
646
|
+
if (!existsSync3(dir)) return [];
|
|
647
|
+
const removed = [];
|
|
648
|
+
for (const name of readdirSync(dir)) {
|
|
649
|
+
if (!name.startsWith(prefix) || !name.endsWith(".caddy")) continue;
|
|
650
|
+
const path = join2(dir, name);
|
|
651
|
+
const text = readFileSync3(path, "utf8");
|
|
652
|
+
if (text.startsWith("# generated by aio-plugin-portal")) {
|
|
653
|
+
rmSync(path);
|
|
654
|
+
removed.push(path);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return removed;
|
|
658
|
+
}
|
|
659
|
+
function renderCaddy(settings2) {
|
|
660
|
+
const caddy = settings2.caddy;
|
|
661
|
+
if (!caddy?.sitesDir) return [];
|
|
662
|
+
const prefix = caddy.prefix ?? "portal-";
|
|
663
|
+
mkdirSync2(caddy.sitesDir, { recursive: true });
|
|
664
|
+
cleanGenerated(caddy.sitesDir, prefix);
|
|
665
|
+
const files = [];
|
|
666
|
+
for (const item of readAliases(settings2)) {
|
|
667
|
+
const path = join2(caddy.sitesDir, `${prefix}${item.alias}.caddy`);
|
|
668
|
+
writeFileSync2(path, caddySite(settings2, item));
|
|
669
|
+
files.push(path);
|
|
670
|
+
}
|
|
671
|
+
return files;
|
|
672
|
+
}
|
|
673
|
+
function render(settings2) {
|
|
674
|
+
const s = settings2;
|
|
196
675
|
const aliases = readAliases(s);
|
|
197
676
|
const dir = s.portal.aliasesDir;
|
|
198
677
|
mkdirSync2(dir, { recursive: true });
|
|
@@ -216,9 +695,10 @@ function render(s) {
|
|
|
216
695
|
writeLines(files[2], host);
|
|
217
696
|
const resolver = s.resolver.configFile;
|
|
218
697
|
mkdirSync2(join2(resolver, ".."), { recursive: true });
|
|
219
|
-
const overrides = s.mode === "none" || !s.lanIp ? [] : aliases.filter((item) => item
|
|
220
|
-
const
|
|
221
|
-
|
|
698
|
+
const overrides = s.mode === "none" || !s.lanIp ? [] : aliases.filter((item) => isInternal(item)).map((item) => `address=/${aliasFqdn(s, item.alias)}/${s.lanIp}`);
|
|
699
|
+
const header = `# \u7531 aio-portal \u751F\u6210\uFF0C\u8BF7\u52FF\u624B\u6539\uFF1B\u6539 ${aliasFqdn(s, "aio-portal")} \u7684\u6CE8\u518C\u8868\u540E\u6267\u884C sync`;
|
|
700
|
+
const lines = s.resolver.fragment ? [header, ...overrides] : [
|
|
701
|
+
header,
|
|
222
702
|
`port=${s.resolver.port}`,
|
|
223
703
|
`listen-address=${s.lanIp || "127.0.0.1"}`,
|
|
224
704
|
"bind-interfaces",
|
|
@@ -230,7 +710,7 @@ function render(s) {
|
|
|
230
710
|
];
|
|
231
711
|
writeFileSync2(resolver, `${lines.join("\n")}
|
|
232
712
|
`);
|
|
233
|
-
return { aliases: aliases.length, files, resolver };
|
|
713
|
+
return { aliases: aliases.length, files, resolver, caddy: renderCaddy(s) };
|
|
234
714
|
}
|
|
235
715
|
var run = (program, args) => {
|
|
236
716
|
try {
|
|
@@ -241,16 +721,76 @@ var run = (program, args) => {
|
|
|
241
721
|
return { code: err.status ?? 1, out: `${err.stdout ?? ""}${err.stderr ?? ""}`.trim() || (err.message ?? "") };
|
|
242
722
|
}
|
|
243
723
|
};
|
|
244
|
-
function
|
|
245
|
-
const
|
|
246
|
-
|
|
724
|
+
function validateCaddy(settings2) {
|
|
725
|
+
const s = settings2;
|
|
726
|
+
if (!s.caddy?.binary) return "\u5DF2\u8DF3\u8FC7 caddy validate\uFF08\u672A\u914D\u7F6E caddy.binary\uFF09";
|
|
727
|
+
const config = s.caddy.sitesDir ? join2(s.caddy.sitesDir, "..", "Caddyfile") : "";
|
|
728
|
+
if (!existsSync3(config)) return `Caddy \u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A\u627E\u4E0D\u5230 ${config}`;
|
|
729
|
+
const args = ["validate", "--config", config];
|
|
730
|
+
const test = run(s.caddy.binary, args);
|
|
731
|
+
return test.code === 0 ? "Caddy \u914D\u7F6E\u6821\u9A8C\u901A\u8FC7" : `Caddy \u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${test.out}`;
|
|
732
|
+
}
|
|
733
|
+
function reloadCaddy(settings2) {
|
|
734
|
+
const command = settings2.caddy?.reload;
|
|
735
|
+
if (!command) return "\u5DF2\u8DF3\u8FC7 Caddy \u70ED\u52A0\u8F7D\uFF08\u672A\u914D\u7F6E caddy.reload\uFF09";
|
|
736
|
+
const [program, ...args] = command.trim().split(/\s+/);
|
|
737
|
+
const result = run(program, args);
|
|
738
|
+
return result.code === 0 ? "Caddy \u5DF2\u70ED\u52A0\u8F7D" : `Caddy \u70ED\u52A0\u8F7D\u5931\u8D25\uFF1A${result.out}`;
|
|
739
|
+
}
|
|
740
|
+
function reloadResolver(settings2) {
|
|
741
|
+
if (settings2.resolver.provider === "none") return "\u5DF2\u8DF3\u8FC7\u5185\u7F51\u89E3\u6790\u5237\u65B0\uFF08resolver.provider=none\uFF09";
|
|
742
|
+
const binary = settings2.resolver.dnsmasqBinary ?? "dnsmasq";
|
|
743
|
+
const test = run(binary, ["--test", `--conf-file=${settings2.resolver.configFile}`]);
|
|
247
744
|
if (test.code !== 0) return `\u89E3\u6790\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${test.out}`;
|
|
248
|
-
if (
|
|
249
|
-
const
|
|
250
|
-
return
|
|
745
|
+
if (settings2.resolver.service) {
|
|
746
|
+
const reload = run("systemctl", ["reload-or-restart", settings2.resolver.service]);
|
|
747
|
+
return reload.code === 0 ? "\u5185\u7F51\u89E3\u6790\u5DF2\u5237\u65B0" : `\u5185\u7F51\u89E3\u6790\u5237\u65B0\u5931\u8D25\uFF1A${reload.out}`;
|
|
748
|
+
}
|
|
749
|
+
return "\u89E3\u6790\u914D\u7F6E\u5DF2\u751F\u6210\uFF08\u672A\u914D\u7F6E resolver.service\uFF0C\u672A\u89E6\u78B0\u5BBF\u4E3B\u670D\u52A1\uFF09";
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/portal/reconcile.ts
|
|
753
|
+
async function reconcile(settings2, options = {}) {
|
|
754
|
+
const aliases = readAliases(settings2).filter((item) => !options.alias || item.alias === options.alias);
|
|
755
|
+
const items = [];
|
|
756
|
+
for (const item of aliases) {
|
|
757
|
+
const plans = [];
|
|
758
|
+
const applied = [];
|
|
759
|
+
const verify = [];
|
|
760
|
+
for (const provider of providersFor(settings2, item)) {
|
|
761
|
+
const plan2 = await provider.plan(item, settings2);
|
|
762
|
+
plans.push(plan2);
|
|
763
|
+
if (options.apply && !plan2.blocked) {
|
|
764
|
+
await provider.apply(item, settings2, plan2);
|
|
765
|
+
applied.push(provider.name);
|
|
766
|
+
}
|
|
767
|
+
const result = await provider.verify(item, settings2);
|
|
768
|
+
verify.push({ provider: provider.name, ...result });
|
|
769
|
+
}
|
|
770
|
+
items.push({
|
|
771
|
+
alias: item.alias,
|
|
772
|
+
visibility: item.visibility ?? "internal",
|
|
773
|
+
plans,
|
|
774
|
+
applied,
|
|
775
|
+
verify,
|
|
776
|
+
ok: plans.every((plan2) => !plan2.blocked) && verify.every((result) => result.ok)
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return { items, ok: items.every((item) => item.ok) };
|
|
780
|
+
}
|
|
781
|
+
function formatReport(report) {
|
|
782
|
+
const lines = [];
|
|
783
|
+
for (const item of report.items) {
|
|
784
|
+
lines.push(`${item.alias} (${item.visibility}) ${item.ok ? "OK" : "NEEDS ATTENTION"}`);
|
|
785
|
+
for (const plan2 of item.plans) {
|
|
786
|
+
const changes = plan2.changes.length ? plan2.changes : [{ kind: "noop", resource: plan2.provider, detail: "\u65E0\u53D8\u66F4" }];
|
|
787
|
+
for (const change of changes) lines.push(` [${plan2.provider}] ${change.kind} ${change.resource} ${change.detail}`);
|
|
788
|
+
for (const warning of plan2.warnings) lines.push(` [${plan2.provider}] warning: ${warning}`);
|
|
789
|
+
}
|
|
790
|
+
for (const result of item.verify) lines.push(` [${result.provider}] verify ${result.ok ? "ok" : "failed"}: ${result.detail}`);
|
|
251
791
|
}
|
|
252
|
-
|
|
253
|
-
return
|
|
792
|
+
lines.push(report.ok ? "\u534F\u8C03\u5B8C\u6210" : "\u5B58\u5728\u672A\u6EE1\u8DB3\u7684 provider \u8BA1\u5212\u6216\u6821\u9A8C");
|
|
793
|
+
return lines.join("\n");
|
|
254
794
|
}
|
|
255
795
|
|
|
256
796
|
// src/portal/commands.ts
|
|
@@ -267,18 +807,21 @@ function add(options) {
|
|
|
267
807
|
if (!isAlias(options.alias)) throw new Error("\u522B\u540D\u53EA\u80FD\u4F7F\u7528\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u8FDE\u5B57\u7B26");
|
|
268
808
|
if (!isHost(options.host)) throw new Error("\u76EE\u6807\u4E3B\u673A\u4E0D\u5408\u6CD5");
|
|
269
809
|
if (options.scheme !== "http" && options.scheme !== "https") throw new Error("\u534F\u8BAE\u53EA\u80FD\u662F http \u6216 https");
|
|
810
|
+
if (options.visibility === "public" && !options.authorizedBy) throw new Error("public \u522B\u540D\u5FC5\u987B\u63D0\u4F9B --authorized-by\uFF0C\u660E\u786E\u8BB0\u5F55\u6388\u6743\u4EBA");
|
|
270
811
|
const entry = {
|
|
271
812
|
alias: options.alias,
|
|
272
813
|
host: options.host,
|
|
273
814
|
port: parsePort(options.port),
|
|
274
815
|
scheme: options.scheme,
|
|
275
|
-
|
|
816
|
+
visibility: options.visibility,
|
|
817
|
+
lan: options.visibility === "internal" && s.mode !== "none",
|
|
818
|
+
authorizedBy: options.authorizedBy,
|
|
276
819
|
note: options.note ?? ""
|
|
277
820
|
};
|
|
278
821
|
const registry = upsert(s, entry);
|
|
279
822
|
const result = render(s);
|
|
280
823
|
return [
|
|
281
|
-
`\u5DF2\u767B\u8BB0 ${aliasFqdn(s, entry.alias)} -> ${entry.scheme}://${entry.host}:${entry.port}\uFF08\u5171 ${result.aliases} \u6761\uFF09`,
|
|
824
|
+
`\u5DF2\u767B\u8BB0 ${aliasFqdn(s, entry.alias)} -> ${entry.scheme}://${entry.host}:${entry.port}\uFF08${entry.visibility}\uFF0C\u5171 ${result.aliases} \u6761\uFF09`,
|
|
282
825
|
renderReload(s)
|
|
283
826
|
].join("\n");
|
|
284
827
|
}
|
|
@@ -293,12 +836,31 @@ function list(options) {
|
|
|
293
836
|
const s = settings(options.home);
|
|
294
837
|
const aliases = readAliases(s);
|
|
295
838
|
if (!aliases.length) return "\uFF08\u7A7A\uFF09";
|
|
296
|
-
return aliases.map((item) => ` ${aliasFqdn(s, item.alias).padEnd(30)} -> ${item.scheme}://${item.host}:${item.port}
|
|
839
|
+
return aliases.map((item) => ` ${aliasFqdn(s, item.alias).padEnd(30)} -> ${item.scheme}://${item.host}:${item.port} ${item.visibility ?? "internal"} ${item.note ?? ""}`).join("\n");
|
|
297
840
|
}
|
|
298
|
-
function sync(options) {
|
|
841
|
+
async function sync(options) {
|
|
299
842
|
const s = settings(options.home);
|
|
843
|
+
const before = snapshot(s);
|
|
300
844
|
const result = render(s);
|
|
301
|
-
|
|
845
|
+
const report = await reconcile(s, { apply: options.apply });
|
|
846
|
+
const after = snapshot(s);
|
|
847
|
+
if (before !== after) throw new Error("\u540C\u6B65\u8FC7\u7A0B\u4E2D\u6CE8\u518C\u8868\u88AB\u5176\u5B83\u8FDB\u7A0B\u4FEE\u6539\uFF0C\u8BF7\u91CD\u8BD5");
|
|
848
|
+
const lines = [
|
|
849
|
+
`\u5DF2\u751F\u6210 ${result.aliases} \u6761\u6620\u5C04\u4E0E ${result.caddy.length} \u4E2A Caddy \u7AD9\u70B9`,
|
|
850
|
+
renderReload(s),
|
|
851
|
+
validateCaddy(s),
|
|
852
|
+
reloadCaddy(s),
|
|
853
|
+
formatReport(report)
|
|
854
|
+
];
|
|
855
|
+
if (!report.ok) {
|
|
856
|
+
throw new Error(`${lines.join("\n")}
|
|
857
|
+
\u540C\u6B65\u88AB\u62D2\u7EDD\uFF1A\u5B58\u5728\u672A\u6EE1\u8DB3\u7684 provider \u8BA1\u5212\u6216\u6821\u9A8C\uFF0C\u672A\u53D1\u5E03\u4EFB\u4F55\u53D8\u66F4`);
|
|
858
|
+
}
|
|
859
|
+
return lines.join("\n");
|
|
860
|
+
}
|
|
861
|
+
async function plan(options) {
|
|
862
|
+
const s = settings(options.home);
|
|
863
|
+
return formatReport(await reconcile(s, { apply: false }));
|
|
302
864
|
}
|
|
303
865
|
function renderReload(s) {
|
|
304
866
|
if (!s.portal.reload) return "\u5DF2\u8DF3\u8FC7\u670D\u52A1\u70ED\u52A0\u8F7D\uFF08portal.reload=false\uFF09";
|
|
@@ -306,8 +868,8 @@ function renderReload(s) {
|
|
|
306
868
|
}
|
|
307
869
|
function doctor(options) {
|
|
308
870
|
const s = settings(options.home);
|
|
309
|
-
const resolved =
|
|
310
|
-
const rendered =
|
|
871
|
+
const resolved = existsSync4(s.resolver.configFile);
|
|
872
|
+
const rendered = existsSync4(`${s.portal.aliasesDir}/${s.portal.mapPrefix}.map`);
|
|
311
873
|
const lines = [
|
|
312
874
|
` \u57DF\u540D\u540E\u7F00 ${s.suffix}`,
|
|
313
875
|
` \u89E3\u6790\u6A21\u5F0F ${s.mode}`,
|
|
@@ -315,9 +877,13 @@ function doctor(options) {
|
|
|
315
877
|
` \u6CE8\u518C\u8868 ${readAliases(s).length} \u6761`,
|
|
316
878
|
` \u6620\u5C04\u7247\u6BB5 ${rendered ? "\u5DF2\u751F\u6210" : "\u7F3A\u5931"}`,
|
|
317
879
|
` \u89E3\u6790\u914D\u7F6E ${resolved ? "\u5DF2\u751F\u6210" : "\u7F3A\u5931"}`,
|
|
880
|
+
` Caddy provider ${s.providers?.connectivity ?? "caddy"} sites=${s.caddy?.sitesDir ?? "\uFF08\u672A\u914D\u7F6E\uFF09"} bind=${s.caddy?.bind ?? "\uFF08\u672A\u914D\u7F6E\uFF09"}`,
|
|
318
881
|
` \u7F51\u5173\u9002\u914D\u5668 ${s.gateway.adapter}${s.gateway.address ? ` @ ${s.gateway.address}` : "\uFF08\u672A\u914D\u7F6E\u5730\u5740\uFF09"}`
|
|
319
882
|
];
|
|
320
|
-
const
|
|
883
|
+
const internal = readAliases(s).filter((item) => (item.visibility ?? "internal") === "internal");
|
|
884
|
+
const guard = internal.every((item) => Boolean(s.caddy?.bind) && Boolean(s.caddy?.allow?.length));
|
|
885
|
+
if (!guard) lines.push(" \u5185\u7F51\u9632\u62A4 internal \u522B\u540D\u7F3A\u5C11 caddy.bind \u6216 caddy.allow");
|
|
886
|
+
const ok = rendered && resolved && Boolean(s.suffix) && Boolean(s.lanIp || s.mode !== "lan") && guard;
|
|
321
887
|
return [...lines, ok ? "\u4F53\u68C0\u901A\u8FC7" : "\u5B58\u5728\u7F3A\u5931\u9879\uFF0C\u8BF7\u5148\u6267\u884C sync \u6216\u8865\u9F50\u914D\u7F6E"].join("\n");
|
|
322
888
|
}
|
|
323
889
|
async function warden(options) {
|
|
@@ -353,10 +919,13 @@ async function warden(options) {
|
|
|
353
919
|
var USAGE = `aio-plugin-portal <\u547D\u4EE4> [\u9009\u9879]
|
|
354
920
|
|
|
355
921
|
\u547D\u4EE4:
|
|
356
|
-
add <\u522B\u540D> --port <\u7AEF\u53E3> [--host <\u4E3B\u673A>] [--scheme http|https] [--note <\u5907\u6CE8>]
|
|
922
|
+
add <\u522B\u540D> --port <\u7AEF\u53E3> [--host <\u4E3B\u673A>] [--scheme http|https] [--note <\u5907\u6CE8>]
|
|
923
|
+
add <\u522B\u540D> --port <\u7AEF\u53E3> --visibility public --authorized-by <\u6388\u6743\u4EBA>
|
|
924
|
+
\uFF08public \u5FC5\u987B\u663E\u5F0F\u6388\u6743\uFF1B\u65E7 --no-lan \u53EA\u8868\u793A\u4E0D\u505A\u5185\u7F51 DNS\uFF0C\u4E0D\u518D\u81EA\u52A8\u7B97\u6388\u6743\uFF09
|
|
357
925
|
remove <\u522B\u540D>
|
|
358
926
|
list
|
|
359
|
-
|
|
927
|
+
plan
|
|
928
|
+
sync [--apply]
|
|
360
929
|
doctor
|
|
361
930
|
warden [--password <\u7F51\u5173\u5BC6\u7801>] [--apply] [--restore]
|
|
362
931
|
|
|
@@ -376,6 +945,8 @@ async function main() {
|
|
|
376
945
|
note: { type: "string" },
|
|
377
946
|
lan: { type: "boolean", default: true },
|
|
378
947
|
"no-lan": { type: "boolean", default: false },
|
|
948
|
+
visibility: { type: "string" },
|
|
949
|
+
"authorized-by": { type: "string" },
|
|
379
950
|
home: { type: "string" },
|
|
380
951
|
password: { type: "string" },
|
|
381
952
|
apply: { type: "boolean", default: false },
|
|
@@ -384,7 +955,7 @@ async function main() {
|
|
|
384
955
|
});
|
|
385
956
|
const [command, argument] = positionals;
|
|
386
957
|
if (values.version) {
|
|
387
|
-
console.log("0.1.
|
|
958
|
+
console.log("0.2.1-dev.6.gd90568e1c0fd");
|
|
388
959
|
return;
|
|
389
960
|
}
|
|
390
961
|
if (values.help || !command) {
|
|
@@ -395,8 +966,10 @@ async function main() {
|
|
|
395
966
|
case "add": {
|
|
396
967
|
if (!argument) throw new Error("add \u9700\u8981\u522B\u540D");
|
|
397
968
|
if (values.port === void 0) throw new Error("add \u9700\u8981 --port");
|
|
398
|
-
const
|
|
399
|
-
|
|
969
|
+
const visibility = values.visibility ?? (values["no-lan"] || !values.lan ? "public" : "internal");
|
|
970
|
+
if (visibility !== "internal" && visibility !== "public") throw new Error("--visibility \u53EA\u80FD\u662F internal \u6216 public");
|
|
971
|
+
const authorizedBy = values["authorized-by"];
|
|
972
|
+
console.log(add({ alias: argument, host: values.host, port: values.port, scheme: values.scheme, note: values.note, visibility, authorizedBy, home: values.home }));
|
|
400
973
|
return;
|
|
401
974
|
}
|
|
402
975
|
case "remove": {
|
|
@@ -407,8 +980,11 @@ async function main() {
|
|
|
407
980
|
case "list":
|
|
408
981
|
console.log(list({ home: values.home }));
|
|
409
982
|
return;
|
|
983
|
+
case "plan":
|
|
984
|
+
console.log(await plan({ home: values.home }));
|
|
985
|
+
return;
|
|
410
986
|
case "sync":
|
|
411
|
-
console.log(sync({ home: values.home }));
|
|
987
|
+
console.log(await sync({ home: values.home, apply: values.apply }));
|
|
412
988
|
return;
|
|
413
989
|
case "doctor":
|
|
414
990
|
console.log(doctor({ home: values.home }));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aio-plugin-portal",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1-dev.6.gd90568e1c0fd",
|
|
4
|
+
"description": "在期望态里协调 DNS(本地/Cloudflare/腾讯云/华为云)、Caddy 反代、证书与内网访问策略,把别名站点的可见范围钉死在登记表里。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -32,5 +32,23 @@
|
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
34
|
"url": "git+https://github.com/org-aio/aio-plugin-portal.git"
|
|
35
|
+
},
|
|
36
|
+
"aio": {
|
|
37
|
+
"cli": {
|
|
38
|
+
"id": "aio-plugin-portal",
|
|
39
|
+
"title": "端口门户",
|
|
40
|
+
"command": "aio-plugin-portal",
|
|
41
|
+
"platforms": [
|
|
42
|
+
"macos",
|
|
43
|
+
"linux"
|
|
44
|
+
],
|
|
45
|
+
"setup": [],
|
|
46
|
+
"uninstall": []
|
|
47
|
+
},
|
|
48
|
+
"source": {
|
|
49
|
+
"repository": "org-aio/aio-plugin-portal",
|
|
50
|
+
"revision": "d90568e1c0fd0fa30b4299b40222968678d204e6",
|
|
51
|
+
"reference": "refs/heads/main"
|
|
52
|
+
}
|
|
35
53
|
}
|
|
36
54
|
}
|
|
@@ -1,36 +1,73 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aio-plugin-portal
|
|
3
|
-
description: 维护端口门户:登记“别名→内网 host:port
|
|
3
|
+
description: 维护端口门户:登记“别名→内网 host:port 映射”,在一套期望态里协调 DNS(本地 dnsmasq / Cloudflare / 腾讯云 DNSPod / 华为云)、反向代理(Caddy)、证书与访问策略,让指定域名仅内网可达。当用户要新增/删除 xxx.addzero.site 之类的别名站点、排查内外网解析差异、要求“仅内网访问不允许公网访问”、或让同事整片局域网零配置走内网时使用。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 端口门户
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
一次登记同时产生并校验:内网 DNS 覆盖(只覆盖 `visibility=internal`)、按 Host 分流的反代站点、
|
|
9
|
+
证书来源、访问策略。**内网直达本机,公网是否可达由登记项的 `visibility` 决定。**
|
|
9
10
|
|
|
10
11
|
先执行 `aio-plugin-portal --help` 核对当前命令,`aio-plugin-portal --version` 确认版本。未全局安装时用 `npx -y aio-plugin-portal`。
|
|
11
12
|
|
|
12
13
|
## 常用命令
|
|
13
14
|
|
|
14
15
|
```sh
|
|
15
|
-
aio-plugin-portal add tc --port 8111 --host
|
|
16
|
-
aio-plugin-portal add only-public --port 9000 --
|
|
16
|
+
aio-plugin-portal add tc --port 8111 --host 127.0.0.1 --note TeamCity
|
|
17
|
+
aio-plugin-portal add only-public --port 9000 --visibility public --authorized-by alice
|
|
17
18
|
aio-plugin-portal list
|
|
18
19
|
aio-plugin-portal remove tc
|
|
19
|
-
aio-plugin-portal
|
|
20
|
-
aio-plugin-portal
|
|
21
|
-
aio-plugin-portal
|
|
22
|
-
aio-plugin-portal
|
|
23
|
-
aio-plugin-portal warden --password '<网关密码>'
|
|
20
|
+
aio-plugin-portal plan # 只出计划,不写任何东西
|
|
21
|
+
aio-plugin-portal sync # 渲染产物 + 执行 provider + 校验
|
|
22
|
+
aio-plugin-portal sync --apply # 允许 provider 真实写入
|
|
23
|
+
aio-plugin-portal doctor # 配置与产物体检
|
|
24
|
+
aio-plugin-portal warden --password '<网关密码>' # 只读探测网关
|
|
25
|
+
aio-plugin-portal warden --password '<网关密码>' --apply # 让整片局域网走内网
|
|
26
|
+
aio-plugin-portal warden --password '<网关密码>' --restore # 回滚网关 DNS
|
|
24
27
|
```
|
|
25
28
|
|
|
29
|
+
## 内网 / 公网边界
|
|
30
|
+
|
|
31
|
+
`visibility` 是安全边界,不是展示字段:
|
|
32
|
+
|
|
33
|
+
- `internal`(默认):永不产生公网 DNS 记录;Caddy 站点要求 `bind <入口地址>` + `remote_ip` 白名单,
|
|
34
|
+
未命中 `respond 404`。缺 `caddy.bind` 时 verify 判失败。
|
|
35
|
+
- `public`:必须同时满足 `allowPublicWrite: true` + 有凭据 + 登记了 `authorizedBy`,否则计划被标记
|
|
36
|
+
`blocked`,`sync --apply` 拒绝执行。
|
|
37
|
+
|
|
38
|
+
`blocked` 的计划一律不 apply;verify 重新读取远端/产物,不用本地计划冒充成功。
|
|
39
|
+
|
|
40
|
+
> 边界代价:只在经过本机 Caddy 的路径上成立。若同名域名在公网 DNS 或别的入口仍指向旧隧道,
|
|
41
|
+
> 那部分流量不归本机管;真正“仅内网”还要清掉对应公网记录,先确认目标记录再动。
|
|
42
|
+
|
|
43
|
+
## provider 组合
|
|
44
|
+
|
|
45
|
+
四类 provider 独立组合,登记项可用 `providers` 字段单条覆盖:
|
|
46
|
+
|
|
47
|
+
| 类别 | 可用值 |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `dns` | `local-dnsmasq`、`cloudflare`、`dnspod`(腾讯云)、`huawei`(华为云) |
|
|
50
|
+
| `connectivity` | `caddy`、`aio-tunnel`、`cloudflared` |
|
|
51
|
+
| `certificate` | `internal-ca`、`acme` |
|
|
52
|
+
| `policy` | `lan-allowlist`、`aio-auth`、`public` |
|
|
53
|
+
|
|
54
|
+
云 DNS 凭据只从环境变量读取(`credentialsEnv` 指向变量名),不写进注册表或配置文件。
|
|
55
|
+
腾讯云 DNSPod 走 `Record.List/Create/Modify`;华为云用 Zone ID + IAM `X-Auth-Token`,
|
|
56
|
+
长期 AK/SK 签名尚未实现,未配置 token 时只给可审计的计划。
|
|
57
|
+
|
|
26
58
|
## 配置
|
|
27
59
|
|
|
28
60
|
宿主相关的值全部来自配置,源码不含域名、网段、账号或部署目录:
|
|
29
61
|
|
|
30
62
|
- 默认读取 `~/.config/aio-portal/aio-portal.json`,可用 `--home` 或环境变量 `AIO_PORTAL_HOME` 覆盖。
|
|
31
|
-
- 必填 `suffix`(域名后缀);`mode`、`lanIp`、`resolver
|
|
63
|
+
- 必填 `suffix`(域名后缀);`mode`、`lanIp`、`resolver.*`、`portal.*`、`gateway.*`、`caddy.*`、`providers.*` 均可配置。
|
|
32
64
|
- 新增站点只改注册表(`aio-plugin-portal add` 或编辑 `aliases.json`),无需改代码。
|
|
33
65
|
|
|
66
|
+
## 与配对机制的边界
|
|
67
|
+
|
|
68
|
+
设备配对复用 `aio-plugin-device` 的短时票据闭环(签发 → 登录确认 → 轮询换取凭据 → 心跳/租约 → 可撤销)。
|
|
69
|
+
本工具只负责把已存在的目标端口登记成可达站点并钉死可见范围,不另造隧道。
|
|
70
|
+
|
|
34
71
|
## 约定
|
|
35
72
|
|
|
36
73
|
- 网关改动默认只读,写入必须显式 `--apply`,并支持 `--restore` 回滚;会同时影响经该网关上网的所有设备。
|