Skip to content

Commit 5a878a3

Browse files
fix(plugin-auth): an IPv6 CIDR in the allowed IP ranges admits the IPv6 clients inside it, at the sign-in gate and the save check (#30392)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgDpDyoNB672KbyavvfQQJzv)_ domain:server#2 · os-bill · session_01Utp6CXwcgsAkqebfzjnfiS Fixes #30320 Clause-②: no — a fix: an IPv6 CIDR entry now admits the clients the list already names; no export, no new refusal, no settings key or manifest shape changes. Metadata-protocol: none ## 维护者速读 - 改了什么:「允许的 IP 段」里的 IPv6 网段(如 `2001:db8::/32`)现在按网段生效:登录闸门放行网段内的 IPv6 用户,设置页保存时也认得出保存者的 IPv6 地址在网段内。精确的 IPv6 地址不论大小写或缩写写法都能匹配。 - 为什么改:设置说明写的是「CIDR 网段」,但匹配只认 IPv4 网段,IPv6 网段被当成一串文字比对,永远匹配不上;结果是网段里的 IPv6 用户全部登录被拒,IPv6 管理员保存这份列表也被拒。 - 风险与回滚:只放宽,不收紧。IPv4 条目的读法一字未改;IPv4 网段不会放行 IPv6 地址,IPv6 网段也不会放行 IPv4 地址;写错的 IPv6 前缀只匹配它的基地址,和以前一样不会放大。两道门共用同一个匹配函数,判法一致。回滚即撤本 PR。 - 先看:`packages/plugins/plugin-auth/src/auth-manager.ts` 的 `ipMatchesRange` 与 `readMatchedAddress`(地址族判定与前缀比较);`packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts` 末尾的 `#30320` 用例(经真实设置路由保存和登录闸门两道门)。 - 两分钟验证:`pnpm --filter @objectstack/plugin-auth exec vitest run src/allowed-ip-ranges.pin.test.ts`,预期 1 个文件、11 条全绿。 - 需要你做的:无,席位落地。 ## What changes **The rule: an allowed-IP entry is read as the address or range it names, in its own family, at every door that judges the list.** - **`ipMatchesRange` reads IPv6 CIDR (`@objectstack/plugin-auth`).** It read IPv4 CIDR and exact addresses only; an IPv6 entry such as `2001:db8::/32` fell to an exact text comparison against its base and never matched a real client. Both sides are now read as the address they denote (`readMatchedAddress`): an IPv4 address as a uint32, any other IPv6 address as eight 16-bit groups through the file's existing `parseIpv6Literal` (`::` elision, embedded IPv4 tail). An IPv6 range compares the first `n` of 128 bits. The sign-in gate (`AuthManager.isClientIpAllowed`) and the settings save check (`allowed-ip-ranges.ts`, from #30309) both call it, so they keep judging the same address the same way. - **Exact IPv6 entries compare by value.** `2001:DB8:0:0::1` matches `2001:db8::1`; `0:0:0:0:0:0:0:1` matches `::1`. - **IPv4-mapped addresses stay IPv4.** A mapped address on either side, dotted (`::ffff:203.0.113.5`, as before) or hex (`::ffff:cb00:7105`, now too), is the IPv4 address it maps, so a dual-stack listener's IPv4 peers keep matching IPv4 ranges. - **Families never cross.** An IPv4 range never admits a pure IPv6 address (`0.0.0.0/0` does not admit `::1`), and an IPv6 range never admits an IPv4 one (`::/0` does not admit `203.0.113.5` nor its mapped form). - **Zone identifiers are dropped.** `fe80::1%eth0` is judged as `fe80::1`. - **Malformed entries do not widen.** An IPv6 prefix must be a whole number from 0 to 128; any other prefix (`/129`, an empty `/`, `/3x`) leaves the entry matching only its base address, which is what the old exact-text fallback did. An entry that is not an address is still compared as text, which an address the server established never equals. **IPv4 entries are read exactly as before**, prefix parsing included, so no IPv4 list changes what it admits. - `unmapIpv4` is gone (its job moved into `readMatchedAddress`; no other caller). The `isClientIpAllowed` docblock and one sentence of the `client-address-canonical.ts` header, which said the allow-list compares exact IPv6 entries as text, are corrected. - Not changed: the `allowed_ip_ranges` help text (it promises CIDR ranges unqualified, which now holds for both families), `auth-plugin.ts`, `packages/spec`. ## Why #30320: the setting promises CIDR ranges, and an IPv6 range locked out every IPv6 client inside it at sign-in and, after #30309, refused the save for an administrator connected from inside it, with copy saying the list does not include an address it textually covers. ## Verification Measured first, at `d1d90b70` with the new pins: 6 failed | 7 passed (the two door pins, the CIDR pin, the exact-spelling pin, the cross-family pin through the hex-mapped case, and `isClientIpAllowed`); the malformed-prefix pin passed already, which is the preservation it states. <details><summary>Tests</summary> - `auth-manager.test.ts` › IP allow-list (ADR-0069 D5): five new cases — IPv6 CIDR inside/outside (`/32`, `/56`, `/128`, `::/0`, `::1`, a zone id); exact IPv6 by value; cross-family (IPv4 range vs `::1` and `2001:db8::5`, IPv6 range vs IPv4 and mapped IPv4, hex-mapped vs an IPv4 range, mapped base keeps its IPv4 prefix); malformed IPv6 prefixes never widen past the base; `isClientIpAllowed` with an IPv6 range plus `::1`. - `allowed-ip-ranges.pin.test.ts` › `#30320` (the shared harness: real `AuthPlugin`, real `SettingsService` with the shipped auth manifest, real settings routes): the gate admits an IPv6 client inside an allowed IPv6 range and refuses one outside and an IPv4 one; a save from an IPv6 address outside is refused (`400 SETTINGS_VALIDATION`, `constraint.clientAddress`) and stores nothing, from inside is stored and enforced. - `pnpm --filter @objectstack/plugin-auth test`: Test Files 222 passed (222), Tests 3925 passed | 10 skipped (3935), at `d9855b3c` and again at `aa57f6dd`. `typecheck`: exit 0. </details> <details><summary>Ablations (scripts/ablation-replace.mjs; tests import src directly, no build between legs)</summary> | # | Mutation | Result | |:--|:--|:--| | A1 | IPv4 branch: drop the address-family check | `never matches across address families` red (1 failed) | | A2 | IPv6 branch: an IPv4 address passes the family check | gate door pin, cross-family, `isClientIpAllowed` red (3 failed) | | A3 | IPv6 prefix read with `Number()` (empty prefix becomes `/0`) | `never widens a malformed IPv6 range` red (1 failed) | | A4 | prefix comparison skipped | both door pins and the CIDR pin red (3 failed) | Each mutation landed (anchor 1 to 0, blob changed); each restore leg reported blob equal to HEAD and `git diff HEAD` empty. </details> <details><summary>Gates (head aa57f6d, after merging main fdad178)</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 62 commands derived for 5 paths (189 changed lines). Each run with its exit code recorded before any pipe; `--ran`: "62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED". All 62 exit 0. - Also run, outside the derivation: the four artifact-roster gates whose roster sits under a changed directory (`check:authz-resolver`, `check:changeset-fixed`, `check:error-code-casing`, `check:filter-alias-parity`) and `pnpm lint`: all exit 0. - `pnpm --filter @objectstack/plugin-auth test` after rebuilding the closure on the merged head: Test Files 222 passed (222), Tests 3925 passed | 10 skipped (3935); `typecheck` exit 0. - No title `!` and no `(narrowing)` arm, so no full dogfood run is owed. </details> ## Acceptance notes - Node's `net.BlockList` was measured and not used: checked against an IPv4 subnet it admits the mapped IPv6 form, and checked as IPv4 against a `::ffff:0:0/96` subnet it admits a plain IPv4 address, so its family rules are not this matcher's; the file already carries an IPv6 parser. - A zone identifier is dropped on both sides rather than refused: no save-time refusal is added here. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic (untouched). - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is Chinese by the Communication rule). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6CXwcgsAkqebfzjnfiS --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ad0fbfd commit 5a878a3

5 files changed

Lines changed: 160 additions & 29 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
fix(plugin-auth): an IPv6 CIDR in the allowed IP ranges admits the IPv6 clients inside it, at the sign-in gate and at the settings save check
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`auth.allowed_ip_ranges` (ADR-0069 D5) promises CIDR ranges, but its matcher read only IPv4 CIDR and exact addresses: an IPv6 entry such as `2001:db8::/32` was compared as text, so it never matched a real client. The sign-in gate then refused every IPv6 client the list named, and the settings save check refused the save for an administrator connected over IPv6 from inside it. `ipMatchesRange` now reads an IPv6 CIDR as the first `n` of 128 bits, and both doors use it, so they keep judging the same address the same way.
11+
12+
- An exact IPv6 entry matches the address it denotes however it is spelled (`2001:DB8:0:0::1` matches `2001:db8::1`).
13+
- An IPv4-mapped address, dotted (`::ffff:203.0.113.5`) or hex (`::ffff:cb00:7105`), is still judged as the IPv4 address it maps. A zone identifier on an address (`fe80::1%eth0`) is dropped before matching.
14+
- Families never cross: an IPv4 range never admits a pure IPv6 address, and an IPv6 range never admits an IPv4 one.
15+
- An IPv6 prefix must be a whole number from 0 to 128. An entry with any other prefix matches only its base address, as it did before. IPv4 entries are read exactly as before.

‎packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,31 @@ describe('#30203 the recovery the refusal names', () => {
229229
expect(recovered.manager.isClientIpAllowed('::1')).toBe(true);
230230
});
231231
});
232+
233+
describe('#30320 an IPv6 range is judged at both doors as the range it names', () => {
234+
const OFFICE_V6 = '2001:db8:1200::/40';
235+
236+
it('the sign-in gate admits an IPv6 client inside an allowed IPv6 range and refuses one outside', async () => {
237+
const { svc, manager } = await boot();
238+
await svc.set('auth', 'allowed_ip_ranges', OFFICE_V6, {});
239+
await vi.waitFor(() => expect(manager.isClientIpAllowed('::1')).toBe(false));
240+
expect(manager.isClientIpAllowed('2001:db8:1234::7')).toBe(true);
241+
expect(manager.isClientIpAllowed('2001:db8:1300::7')).toBe(false);
242+
expect(manager.isClientIpAllowed('203.0.113.7')).toBe(false);
243+
});
244+
245+
it('a save from an IPv6 address inside the range is stored, and one from outside is refused', async () => {
246+
const { svc, manager, save } = await boot();
247+
const outside = await save({ allowed_ip_ranges: OFFICE_V6 }, { remoteAddress: '2001:db8:1300::7' });
248+
expect(outside.status).toBe(400);
249+
expect(outside.body.error.code).toBe('SETTINGS_VALIDATION');
250+
expect(outside.body.error.details.fields[0].constraint).toEqual({ clientAddress: '2001:db8:1300::7' });
251+
expect(await stored(svc)).toBeNull();
252+
253+
const inside = await save({ allowed_ip_ranges: OFFICE_V6 }, { remoteAddress: '2001:db8:1234::7' });
254+
expect(inside.status).toBe(200);
255+
expect(await stored(svc)).toBe(OFFICE_V6);
256+
await vi.waitFor(() => expect(manager.isClientIpAllowed('::1')).toBe(false));
257+
expect(manager.isClientIpAllowed('2001:db8:1234::7')).toBe(true);
258+
});
259+
});

‎packages/plugins/plugin-auth/src/auth-manager.test.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5082,6 +5082,58 @@ describe('AuthManager', () => {
50825082
expect(ipMatchesRange('2001:db8::1', '2001:db8::1')).toBe(true);
50835083
});
50845084

5085+
it('ipMatchesRange reads an IPv6 CIDR as the 128-bit prefix it names', () => {
5086+
expect(ipMatchesRange('2001:db8::5', '2001:db8::/32')).toBe(true);
5087+
expect(ipMatchesRange('2001:db8:ffff:ffff:ffff:ffff:ffff:ffff', '2001:db8::/32')).toBe(true);
5088+
expect(ipMatchesRange('2001:db9::5', '2001:db8::/32')).toBe(false);
5089+
expect(ipMatchesRange('2001:0DB8:0:0::5', '2001:db8::/32')).toBe(true);
5090+
expect(ipMatchesRange('2001:db8:1234:5600::1', '2001:db8:1234:5600::/56')).toBe(true);
5091+
expect(ipMatchesRange('2001:db8:1234:5700::1', '2001:db8:1234:5600::/56')).toBe(false);
5092+
expect(ipMatchesRange('2001:db8::1', '2001:db8::1/128')).toBe(true);
5093+
expect(ipMatchesRange('2001:db8::2', '2001:db8::1/128')).toBe(false);
5094+
expect(ipMatchesRange('2001:db8::5', '::/0')).toBe(true);
5095+
expect(ipMatchesRange('::1', '::1/128')).toBe(true);
5096+
expect(ipMatchesRange('::2', '::1/128')).toBe(false);
5097+
// A zone identifier names an interface, not an address: the address is judged.
5098+
expect(ipMatchesRange('fe80::1%eth0', 'fe80::/10')).toBe(true);
5099+
});
5100+
5101+
it('ipMatchesRange compares an exact IPv6 entry by the address it denotes, not by its spelling', () => {
5102+
expect(ipMatchesRange('2001:db8::1', '2001:DB8:0:0:0:0:0:1')).toBe(true);
5103+
expect(ipMatchesRange('::1', '0:0:0:0:0:0:0:1')).toBe(true);
5104+
expect(ipMatchesRange('2001:db8::2', '2001:db8::1')).toBe(false);
5105+
});
5106+
5107+
it('ipMatchesRange never matches across address families', () => {
5108+
// An IPv4 range never admits a pure IPv6 address, an IPv6 range never an IPv4 one.
5109+
expect(ipMatchesRange('::1', '0.0.0.0/0')).toBe(false);
5110+
expect(ipMatchesRange('2001:db8::5', '0.0.0.0/0')).toBe(false);
5111+
expect(ipMatchesRange('203.0.113.5', '::/0')).toBe(false);
5112+
expect(ipMatchesRange('::ffff:203.0.113.5', '::/0')).toBe(false);
5113+
// The IPv4-mapped form, dotted or hex, is still the IPv4 address it maps.
5114+
expect(ipMatchesRange('::ffff:cb00:7105', '203.0.113.0/24')).toBe(true);
5115+
expect(ipMatchesRange('203.0.113.5', '::ffff:203.0.113.0/24')).toBe(true);
5116+
expect(ipMatchesRange('203.0.114.5', '::ffff:203.0.113.0/24')).toBe(false);
5117+
});
5118+
5119+
it('ipMatchesRange never widens a malformed IPv6 range past its base address', () => {
5120+
expect(ipMatchesRange('2001:db8::5', '2001:db8::/129')).toBe(false);
5121+
expect(ipMatchesRange('2001:db8::5', '2001:db8::/')).toBe(false);
5122+
expect(ipMatchesRange('2001:db8::5', '2001:db8::/3x')).toBe(false);
5123+
expect(ipMatchesRange('2001:db8::5', '2001:db8:::/32')).toBe(false);
5124+
expect(ipMatchesRange('2001:db8::5', 'zz01:db8::/32')).toBe(false);
5125+
// A prefix the family does not read leaves the base address alone, as before.
5126+
expect(ipMatchesRange('2001:db8::', '2001:db8::/129')).toBe(true);
5127+
});
5128+
5129+
it('isClientIpAllowed admits an IPv6 client inside an allowed IPv6 range', () => {
5130+
const m = mgr({ allowedIpRanges: ['2001:db8::/32', '::1'] });
5131+
expect(m.isClientIpAllowed('2001:db8:abcd::1')).toBe(true);
5132+
expect(m.isClientIpAllowed('::1')).toBe(true);
5133+
expect(m.isClientIpAllowed('2001:db9::1')).toBe(false);
5134+
expect(m.isClientIpAllowed('203.0.113.5')).toBe(false);
5135+
});
5136+
50855137
it('resolveClientAddress believes forwarded headers only under trustProxy', () => {
50865138
const headers = { 'x-forwarded-for': '203.0.113.9, 10.0.0.2' };
50875139
expect(mgr({}).resolveClientAddress({ headers, remoteAddress: '10.0.0.2' })).toBe('10.0.0.2');

‎packages/plugins/plugin-auth/src/auth-manager.ts‎

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,40 +1174,76 @@ function ipv4ToInt(ip: string): number | null {
11741174
}
11751175

11761176
/**
1177-
* `::ffff:203.0.113.7` → `203.0.113.7`; anything else is returned trimmed.
1178-
*
1179-
* A dual-stack listener reports an IPv4 peer in this IPv4-mapped IPv6 form, and
1180-
* the ADR-0069 D5 allow-list judges the transport's peer address when no proxy
1181-
* is trusted. So both sides of a comparison are read as the IPv4 address they
1182-
* denote, or an IPv4 range could never admit an IPv4 client on such a socket.
1177+
* An address as the ADR-0069 D5 matcher compares it: an IPv4 address as a
1178+
* uint32, any other IPv6 address as its eight 16-bit groups, or `null` when
1179+
* the text is neither. An IPv4-mapped IPv6 address (`::ffff:0:0/96`, dotted
1180+
* or hex) is the IPv4 address it maps: a dual-stack listener reports an IPv4
1181+
* peer in that form, and the allow-list judges the transport's peer address
1182+
* when no proxy is trusted, so an IPv4 range must admit an IPv4 client on such
1183+
* a socket. An IPv6 zone identifier (`%eth0`) is dropped: it names the
1184+
* interface the address was reached on, not the address.
11831185
*/
1184-
function unmapIpv4(address: string): string {
1185-
const trimmed = address.trim();
1186-
const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i.exec(trimmed);
1187-
return mapped ? mapped[1] : trimmed;
1186+
type MatchedAddress = { v4: number } | { v6: number[] };
1187+
1188+
function readMatchedAddress(text: string): MatchedAddress | null {
1189+
const trimmed = text.trim();
1190+
const v4 = ipv4ToInt(trimmed);
1191+
if (v4 !== null) return { v4 };
1192+
const zoneAt = trimmed.indexOf('%');
1193+
const groups = parseIpv6Literal((zoneAt === -1 ? trimmed : trimmed.slice(0, zoneAt)).toLowerCase());
1194+
if (!groups) return null;
1195+
if (groups.slice(0, 5).every((g) => g === 0) && groups[5] === 0xffff) {
1196+
return { v4: ((groups[6]! << 16) >>> 0) + groups[7]! };
1197+
}
1198+
return { v6: groups };
1199+
}
1200+
1201+
/** Do the first `bits` bits of two eight-group IPv6 addresses agree? */
1202+
function ipv6PrefixEqual(a: readonly number[], b: readonly number[], bits: number): boolean {
1203+
for (let i = 0; i < 8 && bits > 0; i++, bits -= 16) {
1204+
const mask = bits >= 16 ? 0xffff : (0xffff << (16 - bits)) & 0xffff;
1205+
if ((a[i]! & mask) !== (b[i]! & mask)) return false;
1206+
}
1207+
return true;
11881208
}
11891209

11901210
/**
1191-
* ADR-0069 D5 — does `ip` match `range` (IPv4 CIDR `a.b.c.d/n`, or exact IP)?
1192-
* An IPv4-mapped IPv6 address on either side compares as its IPv4 address.
1211+
* ADR-0069 D5 — does `ip` match `range`? A range is an IPv4 CIDR
1212+
* (`a.b.c.d/n`), an IPv6 CIDR (`2001:db8::/32`: the first `n` of 128 bits),
1213+
* or one exact address. Both sides are read as the address they denote
1214+
* ({@link readMatchedAddress}), so an IPv4-mapped IPv6 address on either side
1215+
* compares as its IPv4 address, and an exact IPv6 entry matches however it is
1216+
* spelled. An address is matched only against a range of its own family: an
1217+
* IPv4 range never admits a pure IPv6 address, nor an IPv6 range an IPv4 one.
1218+
*
1219+
* A CIDR whose prefix is not one the family reads (an IPv6 prefix must be a
1220+
* whole number from 0 to 128) is read as its base address alone, and any
1221+
* other entry that is not an address is compared as text, which an address
1222+
* the server established never equals. The IPv4 prefix keeps its earlier
1223+
* reading (`Number`, 0 to 32).
1224+
*
1225+
* One matcher for every door that judges the list: the sign-in gate
1226+
* ({@link AuthManager.isClientIpAllowed}) and the settings save check
1227+
* (`allowed-ip-ranges.ts`) must judge the same address the same way.
11931228
*/
11941229
export function ipMatchesRange(ip: string, range: string): boolean {
1195-
const r = unmapIpv4(range || '');
1230+
const r = (range || '').trim();
11961231
if (!r) return false;
1197-
const address = unmapIpv4(ip);
1198-
if (r.includes('/')) {
1199-
const [rawBase, bitsStr] = r.split('/');
1200-
const base = unmapIpv4(rawBase);
1201-
const bits = Number(bitsStr);
1202-
const ipInt = ipv4ToInt(address);
1203-
const baseInt = ipv4ToInt(base);
1204-
if (ipInt === null || baseInt === null || !(bits >= 0 && bits <= 32)) {
1205-
return address === base; // non-IPv4-CIDR → exact-match fallback
1206-
}
1232+
const address = readMatchedAddress(ip);
1233+
const [rawBase, bitsStr] = r.split('/');
1234+
const base = readMatchedAddress(rawBase!);
1235+
if (!base || !address) return ip.trim() === rawBase!.trim();
1236+
if ('v4' in base) {
1237+
if (!('v4' in address)) return false;
1238+
const bits = bitsStr === undefined ? 32 : Number(bitsStr);
1239+
if (!(bits >= 0 && bits <= 32)) return address.v4 === base.v4; // not an IPv4 prefix → exact base
12071240
const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0;
1208-
return ((ipInt & mask) >>> 0) === ((baseInt & mask) >>> 0);
1241+
return ((address.v4 & mask) >>> 0) === ((base.v4 & mask) >>> 0);
12091242
}
1210-
return address === r;
1243+
if (!('v6' in address)) return false;
1244+
const prefix = bitsStr === undefined ? '128' : bitsStr.trim();
1245+
const bits = /^\d{1,3}$/.test(prefix) && Number(prefix) <= 128 ? Number(prefix) : 128; // not an IPv6 prefix → exact base
1246+
return ipv6PrefixEqual(address.v6, base.v6, bits);
12111247
}
12121248

12131249
/**
@@ -5893,7 +5929,7 @@ export class AuthManager {
58935929
* only for an address inside one of them, and FALSE for an undetermined
58945930
* address (`undefined` / empty): an allow-list is a gate, and a gate that
58955931
* opens for "unknown" is open to anyone who can make themselves unknown.
5896-
* Supports IPv4 CIDR + exact IPv4/IPv6 (IPv4-mapped IPv6 compares as IPv4).
5932+
* Supports IPv4 and IPv6 CIDR + exact IPv4/IPv6 ({@link ipMatchesRange}).
58975933
*
58985934
* `ip` must be the address the server established
58995935
* ({@link resolveClientAddress}), never a header read by the caller.

‎packages/plugins/plugin-auth/src/client-address-canonical.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
*
3232
* That module is a published entry the runtime's inbound rate limiter loads,
3333
* and it is kept free of imports; this one needs `node:net`. The trust rule
34-
* also feeds the ADR-0069 D5 allow-list, which compares exact IPv6 entries as
35-
* text, so rewriting the rule's own answer would change which configured
36-
* entries match. This module changes only what is stored.
34+
* also feeds the ADR-0069 D5 allow-list (`ipMatchesRange`), which reads each
35+
* entry and the address as the address they denote, whatever their spelling.
36+
* This module changes only what is stored.
3737
*
3838
* ## The form
3939
*

0 commit comments

Comments
 (0)