@xcanwin/manyoyo 8.5.1 → 8.6.0
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/bin/manyoyo.js +4 -4
- package/lib/container-network.js +100 -48
- package/lib/container-run.js +1 -1
- package/lib/egress-proxy.js +146 -64
- package/lib/egress-sidecar-main.js +23 -5
- package/lib/egress-sidecar.js +22 -12
- package/lib/network-endpoints.js +18 -19
- package/lib/network-policy.js +296 -77
- package/lib/web/AGENTS.md +1 -1
- package/lib/web/container-manage.js +68 -30
- package/lib/web/index.html +80 -81
- package/lib/web/server.js +12 -18
- package/manyoyo.example.json +9 -6
- package/package.json +1 -1
|
@@ -4,13 +4,12 @@ const os = require('os');
|
|
|
4
4
|
const net = require('net');
|
|
5
5
|
const state_ = require('../container-state');
|
|
6
6
|
const { resolveContainerId } = require('../container-exec');
|
|
7
|
-
const { normalizePolicy } = require('../network-policy');
|
|
7
|
+
const { normalizePolicy, resolveContainerRefs, isUnrestricted, domainRuleAllows, targetKind } = require('../network-policy');
|
|
8
8
|
const { parseEnvEntry } = require('../runtime-normalizers');
|
|
9
9
|
const { envUrlDomains } = require('../network-endpoints');
|
|
10
10
|
const { egressPaths } = require('../egress-sidecar');
|
|
11
11
|
const deniedLib = require('../egress-denied');
|
|
12
12
|
const { isBrowserBackground } = require('../browser-background');
|
|
13
|
-
const { domainAllowed } = require('../network-policy');
|
|
14
13
|
|
|
15
14
|
// 容器管理接口:环境变量、自启动、网络(含端口暴露)、孤儿状态目录。
|
|
16
15
|
// 全部挂在全局认证网关之后;非只读请求由 handleWebApi 统一校验 X-Requested-With。
|
|
@@ -20,33 +19,52 @@ function isLoopbackBind(bind) {
|
|
|
20
19
|
return bind === '127.0.0.1' || bind === '::1';
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
//
|
|
22
|
+
// `wide`:新增的“允许”规则范围极宽,效果接近不设限:任何地址 / 内网 / 元数据、其他容器(入站)、宿主机全部端口、
|
|
23
|
+
// /8 以上的网段(含 0.0.0.0/0)、公共后缀通配(*.co.uk 之类)
|
|
24
24
|
const PUBLIC_SUFFIX_WILDCARD_RE = /^\*\.(co|com|net|org|gov|edu|ac|or|ne|go)\.[a-z]{2}$/;
|
|
25
|
+
const WIDE_OUTBOUND_VARS = ['@any', '@private', '@metadata'];
|
|
26
|
+
const WIDE_INBOUND_VARS = ['@any', '@containers'];
|
|
25
27
|
|
|
26
|
-
function
|
|
28
|
+
function isWideCidr(cidr) {
|
|
27
29
|
const m = /\/(\d+)$/.exec(cidr);
|
|
28
30
|
const prefix = m ? Number(m[1]) : 32;
|
|
29
31
|
return cidr.includes(':') ? prefix <= 16 : prefix <= 8;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
function isWideRule(rule, direction) {
|
|
35
|
+
const target = direction === 'inbound' ? rule.source : rule.target;
|
|
36
|
+
if (rule.action !== 'allow' || !rule.enabled) return false;
|
|
37
|
+
const kind = targetKind(target);
|
|
38
|
+
if (kind === 'var') return (direction === 'inbound' ? WIDE_INBOUND_VARS : WIDE_OUTBOUND_VARS).includes(target) || (direction !== 'inbound' && target === '@host' && !rule.ports);
|
|
39
|
+
if (kind === 'cidr') return isWideCidr(target);
|
|
40
|
+
return PUBLIC_SUFFIX_WILDCARD_RE.test(target);
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
function wideKeys(policy) {
|
|
44
|
+
const key = (rule, direction) => `${direction}:${rule.action}|${direction === 'inbound' ? rule.source : rule.target}|${rule.ports}|${rule.proto}`;
|
|
33
45
|
return [
|
|
34
|
-
...policy.
|
|
35
|
-
...policy.
|
|
46
|
+
...policy.outbound.filter(rule => isWideRule(rule, 'outbound')).map(rule => key(rule, 'outbound')),
|
|
47
|
+
...policy.inbound.filter(rule => isWideRule(rule, 'inbound')).map(rule => key(rule, 'inbound'))
|
|
36
48
|
];
|
|
37
49
|
}
|
|
38
50
|
|
|
39
|
-
//
|
|
51
|
+
// 需要二次确认的风险:切到自定义、新增非 loopback 的端口暴露、新增范围极宽的允许规则(自定义里本来就不设限,不再逐条问)
|
|
40
52
|
function pendingRisks(before, after) {
|
|
41
53
|
const risks = [];
|
|
42
|
-
if (after.preset === '
|
|
54
|
+
if (after.preset === 'custom' && before.preset !== 'custom') risks.push('custom');
|
|
43
55
|
const hadWide = new Set(wideKeys(before));
|
|
44
|
-
if (after.preset !== '
|
|
56
|
+
if (after.preset !== 'custom' && wideKeys(after).some(key => !hadWide.has(key))) risks.push('wide');
|
|
45
57
|
const had = new Set(before.expose.filter(e => !isLoopbackBind(e.bind)).map(e => `${e.bind}:${e.hostPort}`));
|
|
46
58
|
if (after.expose.some(e => !isLoopbackBind(e.bind) && !had.has(`${e.bind}:${e.hostPort}`))) risks.push('publicBind');
|
|
47
59
|
return risks;
|
|
48
60
|
}
|
|
49
61
|
|
|
62
|
+
function peerIp(info) {
|
|
63
|
+
const networks = (info && info.NetworkSettings && info.NetworkSettings.Networks) || {};
|
|
64
|
+
const pick = networks.manyoyo || Object.values(networks).find(n => n && n.IPAddress);
|
|
65
|
+
return (pick && pick.IPAddress) || '';
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
function parseTail(value, fallback = 16 * 1024) {
|
|
51
69
|
const n = Number(value);
|
|
52
70
|
if (!Number.isInteger(n) || n < 1) return fallback;
|
|
@@ -79,6 +97,16 @@ function createContainerManageRoutes({ ctx, state, req, res, deps }) {
|
|
|
79
97
|
return { name, id };
|
|
80
98
|
}
|
|
81
99
|
|
|
100
|
+
// 名称 → 受管容器 id(只认运行时里真实存在的)
|
|
101
|
+
async function containerIdLookup() {
|
|
102
|
+
try {
|
|
103
|
+
const byName = new Map((await getNetworkManager().listManaged()).map(item => [item.name, item.id]));
|
|
104
|
+
return name => byName.get(name) || '';
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return () => '';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
82
110
|
function readPolicy(id) {
|
|
83
111
|
return normalizePolicy(state_.readNetworkRaw(homeDir, id) || undefined);
|
|
84
112
|
}
|
|
@@ -125,7 +153,7 @@ function createContainerManageRoutes({ ctx, state, req, res, deps }) {
|
|
|
125
153
|
function deniedPayload(id, policy) {
|
|
126
154
|
try {
|
|
127
155
|
return deniedLib.read(egressPaths(homeDir).denied, id)
|
|
128
|
-
.filter(record => !
|
|
156
|
+
.filter(record => !domainRuleAllows(policy, record.host))
|
|
129
157
|
.map(record => ({ ...record, background: isBrowserBackground(record.host) }));
|
|
130
158
|
} catch (e) {
|
|
131
159
|
return [];
|
|
@@ -137,17 +165,23 @@ function createContainerManageRoutes({ ctx, state, req, res, deps }) {
|
|
|
137
165
|
const manager = getNetworkManager();
|
|
138
166
|
let peers = [];
|
|
139
167
|
try {
|
|
140
|
-
peers = (await manager.listManaged()).filter(item => item.id !== target.id).map(item => ({ id: item.id, name: item.name, running: item.running }));
|
|
168
|
+
peers = (await manager.listManaged()).filter(item => item.id !== target.id).map(item => ({ id: item.id, name: item.name, running: item.running, ip: (item.running && peerIp(item.info)) || '' }));
|
|
141
169
|
} catch (e) {
|
|
142
170
|
peers = [];
|
|
143
171
|
}
|
|
172
|
+
let derived = [];
|
|
173
|
+
try {
|
|
174
|
+
derived = typeof manager.derivedOf === 'function' ? await manager.derivedOf(target.name) : [];
|
|
175
|
+
} catch (e) {
|
|
176
|
+
derived = [];
|
|
177
|
+
}
|
|
144
178
|
// 建议放行的域名:来自当前环境变量(含容器里改过的)里的 URL。只是建议,要用户自己点才会加进策略
|
|
145
179
|
// (容器可写的 env 不能直接决定防火墙)
|
|
146
180
|
const envLines = [
|
|
147
181
|
...state_.readEnv(homeDir, target.id).entries.map(entry => `${entry.key}=${entry.value}`),
|
|
148
182
|
...state_.envFileEntries(homeDir, target.id).map(entry => `${entry.key}=${entry.value}`)
|
|
149
183
|
];
|
|
150
|
-
const suggestedDomains = envUrlDomains(envLines).filter(domain => !policy
|
|
184
|
+
const suggestedDomains = envUrlDomains(envLines).filter(domain => !domainRuleAllows(policy, domain));
|
|
151
185
|
return {
|
|
152
186
|
legacy: false,
|
|
153
187
|
id: target.id,
|
|
@@ -158,20 +192,24 @@ function createContainerManageRoutes({ ctx, state, req, res, deps }) {
|
|
|
158
192
|
status: state_.readNetStatus(homeDir, target.id),
|
|
159
193
|
forwards: getPortForwarder().list(target.id),
|
|
160
194
|
peers,
|
|
195
|
+
derived,
|
|
196
|
+
unrestricted: isUnrestricted(policy),
|
|
161
197
|
...extra
|
|
162
198
|
};
|
|
163
199
|
}
|
|
164
200
|
|
|
165
201
|
// 保存策略后:下发规则(容器在运行时)、同步端口转发、重算有 peers 关系的容器
|
|
166
|
-
async function applyPolicy(target, policy) {
|
|
202
|
+
async function applyPolicy(target, policy, relatedBefore = []) {
|
|
167
203
|
const out = { applyError: '', forwardFailures: [] };
|
|
168
204
|
const running = isRunning(target.name);
|
|
169
205
|
if (running) {
|
|
170
206
|
const manager = getNetworkManager();
|
|
171
207
|
try {
|
|
172
208
|
await manager.apply(target.name, { expectId: target.id });
|
|
173
|
-
|
|
174
|
-
|
|
209
|
+
// 旧规则关联的容器也要重算:撤掉“允许 @container:Y”后,Y 出站里原来的派生行得去掉
|
|
210
|
+
const related = [...new Set([...relatedBefore, ...await manager.relatedContainers(target.name)])];
|
|
211
|
+
for (const other of related) {
|
|
212
|
+
await manager.apply(other).catch(e => ctx.logger.warn('related container network apply failed', { related: other, message: e && e.message }));
|
|
175
213
|
}
|
|
176
214
|
} catch (e) {
|
|
177
215
|
out.applyError = e.message;
|
|
@@ -187,21 +225,25 @@ function createContainerManageRoutes({ ctx, state, req, res, deps }) {
|
|
|
187
225
|
return;
|
|
188
226
|
}
|
|
189
227
|
const before = readPolicy(target.id);
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
228
|
+
const normalized = normalizePolicy({ ...body.policy, autostartOnServe: body.policy && body.policy.autostartOnServe !== undefined ? body.policy.autostartOnServe : before.autostartOnServe });
|
|
229
|
+
let next;
|
|
230
|
+
try {
|
|
231
|
+
next = resolveContainerRefs(normalized, await containerIdLookup());
|
|
232
|
+
} catch (e) {
|
|
233
|
+
sendJson(res, 400, { error: e.message });
|
|
234
|
+
return;
|
|
196
235
|
}
|
|
197
236
|
const risks = pendingRisks(before, next);
|
|
198
237
|
if (risks.length && body.confirmRisk !== true) {
|
|
199
238
|
sendJson(res, 400, { error: '该改动会放开网络限制,需要确认', needsConfirm: true, risks });
|
|
200
239
|
return;
|
|
201
240
|
}
|
|
241
|
+
// 保存前先取旧规则关联的容器(保存后旧规则就读不到了)
|
|
242
|
+
let relatedBefore = [];
|
|
243
|
+
try { relatedBefore = isRunning(target.name) ? await getNetworkManager().relatedContainers(target.name) : []; } catch (e) { relatedBefore = []; }
|
|
202
244
|
writePolicy(target.id, next);
|
|
203
|
-
state_.setNetRequired(homeDir, target.id, next
|
|
204
|
-
const result = await applyPolicy(target, next);
|
|
245
|
+
state_.setNetRequired(homeDir, target.id, !isUnrestricted(next));
|
|
246
|
+
const result = await applyPolicy(target, next, relatedBefore);
|
|
205
247
|
const payload = await networkPayload(target, { applyError: result.applyError, forwardFailures: result.forwardFailures });
|
|
206
248
|
// 策略已保存;规则下发失败(502)或端口暴露没起来(409,如端口被占)都要明确报错
|
|
207
249
|
if (result.applyError) sendJson(res, 502, { error: result.applyError, ...payload });
|
|
@@ -209,16 +251,12 @@ function createContainerManageRoutes({ ctx, state, req, res, deps }) {
|
|
|
209
251
|
else sendJson(res, 200, payload);
|
|
210
252
|
}
|
|
211
253
|
|
|
212
|
-
//
|
|
254
|
+
// 一键放行被拦截的域名:在出站规则最前面插入“允许 <域名>”并立即下发(任何模式都可以)
|
|
213
255
|
async function postAllow(target, body) {
|
|
214
256
|
const domain = String(body.domain || '').trim().toLowerCase();
|
|
215
257
|
const policy = readPolicy(target.id);
|
|
216
|
-
if (policy
|
|
217
|
-
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
if (!policy.egress.domains.includes(domain)) {
|
|
221
|
-
await putNetwork(target, { policy: { ...policy, egress: { ...policy.egress, domains: [...policy.egress.domains, domain] } }, confirmRisk: body.confirmRisk });
|
|
258
|
+
if (!domainRuleAllows(policy, domain)) {
|
|
259
|
+
await putNetwork(target, { policy: { ...policy, outbound: [{ action: 'allow', target: domain, ports: '', proto: 'all', enabled: true }, ...policy.outbound] }, confirmRisk: body.confirmRisk });
|
|
222
260
|
return;
|
|
223
261
|
}
|
|
224
262
|
sendJson(res, 200, await networkPayload(target));
|