@xcanwin/manyoyo 8.5.0 → 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.
@@ -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
- // 把 `wide` 当作放开限制:整个 /8 以上的网段(含 0.0.0.0/0)、公共后缀通配(*.co.uk 之类),效果接近 open
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 isWideRule(cidr) {
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.egress.rules.filter(rule => isWideRule(rule.cidr)).map(rule => `rule:${rule.cidr}|${rule.ports}|${rule.proto}`),
35
- ...policy.egress.domains.filter(domain => PUBLIC_SUFFIX_WILDCARD_RE.test(domain)).map(domain => `domain:${domain}`)
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
- // 需要二次确认的风险:切到 open(等于不加规则)、新增非 loopback 的端口暴露、新增范围极宽的出站规则
51
+ // 需要二次确认的风险:切到自定义、新增非 loopback 的端口暴露、新增范围极宽的允许规则(自定义里本来就不设限,不再逐条问)
40
52
  function pendingRisks(before, after) {
41
53
  const risks = [];
42
- if (after.preset === 'open' && before.preset !== 'open') risks.push('open');
54
+ if (after.preset === 'custom' && before.preset !== 'custom') risks.push('custom');
43
55
  const hadWide = new Set(wideKeys(before));
44
- if (after.preset !== 'open' && wideKeys(after).some(key => !hadWide.has(key))) risks.push('wide');
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 => !domainAllowed(policy.egress.domains, record.host))
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.egress.domains.includes(domain));
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
- for (const related of await manager.relatedContainers(target.name)) {
174
- await manager.apply(related).catch(e => ctx.logger.warn('related container network apply failed', { related, message: e && e.message }));
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 next = normalizePolicy({ ...body.policy, autostartOnServe: body.policy && body.policy.autostartOnServe !== undefined ? body.policy.autostartOnServe : before.autostartOnServe });
191
- for (const entry of next.peers.inbound) {
192
- if (entry.from === target.id || !state_.stateExists(homeDir, entry.from)) {
193
- sendJson(res, 400, { error: `peers.inbound 里的容器不存在: ${entry.from}` });
194
- return;
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.preset !== 'open');
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.preset !== 'allowlist') {
217
- sendJson(res, 400, { error: '只有「仅白名单」出站模式才需要放行域名' });
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));