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>
This commit is contained in:
parent
4a32f150db
commit
4007041c9e
3 changed files with 50 additions and 9 deletions
|
|
@ -48,7 +48,9 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
|
||||||
connection's, or, for a connection from a proxy the operator trusts
|
connection's, or, for a connection from a proxy the operator trusts
|
||||||
(`TRUSTED_PROXIES`), the last address of `X-Forwarded-For`, the one the proxy saw
|
(`TRUSTED_PROXIES`), the last address of `X-Forwarded-For`, the one the proxy saw
|
||||||
itself. Never the `Forwarded` header, which Caddy passes on as the visitor wrote it,
|
itself. Never the `Forwarded` header, which Caddy passes on as the visitor wrote it,
|
||||||
so never Serverpod's `remoteInfo`. Serverpod's own limit on anonymous accounts is off
|
so never Serverpod's `remoteInfo`. An IPv6 visitor counts by their /64
|
||||||
|
(`networkKeyOf`), which one line or server holds whole. Serverpod's own limit on
|
||||||
|
anonymous accounts is off
|
||||||
(it counts by the connection's address, behind a proxy the proxy's for everyone); the
|
(it counts by the connection's address, behind a proxy the proxy's for everyone); the
|
||||||
endpoint counts them itself.
|
endpoint counts them itself.
|
||||||
- Refusals are `PasswordRefusedException`, `GitHubSignInRefusedException`,
|
- Refusals are `PasswordRefusedException`, `GitHubSignInRefusedException`,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:serverpod/serverpod.dart';
|
import 'package:serverpod/serverpod.dart';
|
||||||
|
|
||||||
|
|
@ -13,16 +14,35 @@ import 'package:serverpod/serverpod.dart';
|
||||||
// Never the `Forwarded` header, which Caddy passes on as the visitor wrote
|
// Never the `Forwarded` header, which Caddy passes on as the visitor wrote
|
||||||
// it, and so never Serverpod's `remoteInfo`, which prefers that header.
|
// it, and so never Serverpod's `remoteInfo`, which prefers that header.
|
||||||
|
|
||||||
/// The network [session]'s request came from (see above): its visitor's
|
/// The network [session]'s request came from (see above), as the limits
|
||||||
/// address, or `null` for a session without a request (tests, future
|
/// count it ([networkKeyOf]): its visitor's address, an IPv6 visitor's /64,
|
||||||
/// calls), as the limits have always taken it.
|
/// or `null` for a session without a request (tests, future calls), as the
|
||||||
|
/// limits have always taken it.
|
||||||
String networkOf(Session session) {
|
String networkOf(Session session) {
|
||||||
final request = session.request;
|
final request = session.request;
|
||||||
return '${visitorAddress(
|
return networkKeyOf(
|
||||||
socket: request == null ? null : '${request.connectionInfo.remote.address}',
|
visitorAddress(
|
||||||
forwardedFor: request?.headers['x-forwarded-for'],
|
socket: request == null
|
||||||
trustedProxies: trustedProxies(),
|
? null
|
||||||
)}';
|
: '${request.connectionInfo.remote.address}',
|
||||||
|
forwardedFor: request?.headers['x-forwarded-for'],
|
||||||
|
trustedProxies: trustedProxies(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The network the limits count [address] in: an IPv4 address itself, an
|
||||||
|
/// IPv6 address's /64 (`2001:db8:1:2::/64`; the second reading's M5), since
|
||||||
|
/// one household's line or one server gets a whole /64 and could turn
|
||||||
|
/// through its addresses past every limit; `null` for none.
|
||||||
|
String networkKeyOf(String? address) {
|
||||||
|
final parsed = address == null ? null : InternetAddress.tryParse(address);
|
||||||
|
if (parsed == null || parsed.type != InternetAddressType.IPv6) {
|
||||||
|
return '$address';
|
||||||
|
}
|
||||||
|
final prefix = [...parsed.rawAddress.take(8), ...List.filled(8, 0)];
|
||||||
|
return '${InternetAddress.fromRawAddress(Uint8List.fromList(prefix)).address}'
|
||||||
|
'/64';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The address of the visitor whose connection came from [socket] (none
|
/// The address of the visitor whose connection came from [socket] (none
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,25 @@ void main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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)', () {
|
test('is none without a connection (tests, future calls)', () {
|
||||||
expect(
|
expect(
|
||||||
visitorAddress(
|
visitorAddress(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue