halkyon/packages/halkyon_base_server/test/unit/network_test.dart
Tizian 「ティツィアーン」 4007041c9e
fix(auth): an IPv6 visitor counts by their /64
One household's line or one server gets a whole /64 of IPv6 addresses,
and could turn through them past every per-network limit (the second
reading's M5). networkOf now counts an IPv6 visitor by their /64
(networkKeyOf), an IPv4 visitor by their address as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 14:12:45 +02:00

155 lines
4.3 KiB
Dart

import 'package:halkyon_base_server/base.dart';
import 'package:test/test.dart';
void main() {
group('The proxies the operator trusts (TRUSTED_PROXIES)', () {
test('are none where unset or empty', () {
expect(trustedProxiesFrom(null), isEmpty);
expect(trustedProxiesFrom(''), isEmpty);
expect(trustedProxiesFrom(' '), isEmpty);
});
test('are IP addresses, separated by commas or spaces', () {
expect(trustedProxiesFrom('172.30.80.2'), {'172.30.80.2'});
expect(trustedProxiesFrom(' 172.30.80.2, 2001:db8::2 10.0.0.1 '), {
'172.30.80.2',
'2001:db8::2',
'10.0.0.1',
});
});
test('take an IPv4 address written as IPv6 as the IPv4 address', () {
expect(trustedProxiesFrom('::ffff:172.30.80.2'), {'172.30.80.2'});
});
test('stop the server for anything but addresses', () {
expect(() => trustedProxiesFrom('caddy'), throwsStateError);
expect(() => trustedProxiesFrom('172.30.80.0/24'), throwsStateError);
expect(
() => trustedProxiesFrom('172.30.80.2, 300.1.1.1'),
throwsStateError,
);
});
});
group("A request's network", () {
const caddy = {'172.30.80.2'};
test("is the connection's address where no proxy is trusted, whatever "
'X-Forwarded-For says', () {
expect(
visitorAddress(
socket: '203.0.113.9',
forwardedFor: ['198.51.100.7'],
trustedProxies: const {},
),
'203.0.113.9',
);
});
test('is the last address of X-Forwarded-For, the one the trusted proxy '
'saw itself, for a connection from that proxy', () {
expect(
visitorAddress(
socket: '172.30.80.2',
forwardedFor: ['198.51.100.7'],
trustedProxies: caddy,
),
'198.51.100.7',
);
// What a visitor sent along is in front of it, never believed.
expect(
visitorAddress(
socket: '172.30.80.2',
forwardedFor: ['10.9.9.9, 192.0.2.1 , 198.51.100.7'],
trustedProxies: caddy,
),
'198.51.100.7',
);
expect(
visitorAddress(
socket: '172.30.80.2',
forwardedFor: ['10.9.9.9', '2001:db8::7'],
trustedProxies: caddy,
),
'2001:db8::7',
);
});
test('is never X-Forwarded-For from a connection the operator trusts '
'not', () {
expect(
visitorAddress(
socket: '203.0.113.9',
forwardedFor: ['198.51.100.7'],
trustedProxies: caddy,
),
'203.0.113.9',
);
});
test("knows the proxy by its IPv4 address on the server's IPv6 socket", () {
expect(
visitorAddress(
socket: '::ffff:172.30.80.2',
forwardedFor: ['::ffff:198.51.100.7'],
trustedProxies: caddy,
),
'198.51.100.7',
);
});
test("is the proxy's own address where its header is missing or no "
'address', () {
for (final header in [
null,
<String>[],
[''],
['198.51.100.7, '],
['unknown'],
['198.51.100.7:4711'],
['[2001:db8::7]'],
]) {
expect(
visitorAddress(
socket: '172.30.80.2',
forwardedFor: header,
trustedProxies: caddy,
),
'172.30.80.2',
reason: '$header',
);
}
});
test("is an IPv6 visitor's /64, as one household or server holds a "
'whole /64 and could turn through its addresses past every limit; '
'an IPv4 visitor its address', () {
expect(
networkKeyOf('2001:db8:1:2:aaaa:bbbb:cccc:1'),
'2001:db8:1:2::/64',
);
expect(
networkKeyOf('2001:db8:1:2::ffff'),
networkKeyOf('2001:db8:1:2:9::1'),
);
expect(
networkKeyOf('2001:db8:1:3::1'),
isNot(networkKeyOf('2001:db8:1:2::1')),
);
expect(networkKeyOf('198.51.100.7'), '198.51.100.7');
expect(networkKeyOf(null), 'null');
});
test('is none without a connection (tests, future calls)', () {
expect(
visitorAddress(
socket: null,
forwardedFor: ['198.51.100.7'],
trustedProxies: caddy,
),
isNull,
);
});
});
}