*/ public static function defaultAddressProvider(): iterable { yield 'loopback v4' => ['127.0.0.1', true]; yield 'loopback v6' => ['::1', true]; // RFC1918 sources are no longer allowed by default — F25. yield 'rfc1918 10/8 rejected by default' => ['10.5.6.7', false]; yield 'rfc1918 172.16/12 rejected by default' => ['172.16.42.1', false]; yield 'rfc1918 172.31/12 (boundary) rejected by default' => ['172.31.255.255', false]; yield 'just outside 172.16/12' => ['172.32.0.1', false]; yield 'rfc1918 192.168/16 rejected by default' => ['192.168.1.1', false]; yield 'public 1.1.1.1' => ['1.1.1.1', false]; yield 'public v4' => ['203.0.113.4', false]; yield 'public v6' => ['2001:db8::1', false]; yield 'malformed' => ['not-an-ip', false]; yield 'empty' => ['', false]; } #[DataProvider('defaultAddressProvider')] public function testDefaultLoopbackOnlyGate(string $remoteAddr, bool $shouldPass): void { $this->assertGate(new InternalNetworkMiddleware(new ResponseFactory()), $remoteAddr, $shouldPass); } public function testNullAllowedCidrsFallsBackToLoopbackDefault(): void { $middleware = new InternalNetworkMiddleware(new ResponseFactory(), null); $this->assertGate($middleware, '127.0.0.1', true); $this->assertGate($middleware, '10.0.0.1', false); } public function testEmptyAllowedCidrsFallsBackToLoopbackDefault(): void { $middleware = new InternalNetworkMiddleware(new ResponseFactory(), []); $this->assertGate($middleware, '::1', true); $this->assertGate($middleware, '192.168.1.1', false); } public function testCustomAllowlistAdmitsConfiguredSourcesOnly(): void { // Operator opt-in: extend allowlist to a single bridge IP. $middleware = new InternalNetworkMiddleware( new ResponseFactory(), ['127.0.0.1/32', '::1/128', '172.20.0.5/32'], ); $this->assertGate($middleware, '127.0.0.1', true); $this->assertGate($middleware, '::1', true); $this->assertGate($middleware, '172.20.0.5', true); // Still narrow — 172.20.0.6 is one off and rejected. $this->assertGate($middleware, '172.20.0.6', false); // The wider 10/8 block was not configured. $this->assertGate($middleware, '10.0.0.1', false); } public function testInvalidCidrInConstructorFailsClosed(): void { $this->expectException(\Throwable::class); new InternalNetworkMiddleware(new ResponseFactory(), ['not-a-cidr']); } public function testParseCidrListAcceptsMixedSeparators(): void { self::assertSame( ['127.0.0.1/32', '::1/128', '10.0.0.0/8'], InternalNetworkMiddleware::parseCidrList('127.0.0.1/32, ::1/128 10.0.0.0/8'), ); } public function testParseCidrListReturnsEmptyForBlank(): void { self::assertSame([], InternalNetworkMiddleware::parseCidrList('')); self::assertSame([], InternalNetworkMiddleware::parseCidrList(' ')); } public function testParseCidrListThrowsOnInvalidEntry(): void { $this->expectException(InvalidCidrException::class); InternalNetworkMiddleware::parseCidrList('127.0.0.1/32, garbage'); } private function assertGate(InternalNetworkMiddleware $middleware, string $remoteAddr, bool $shouldPass): void { $req = (new ServerRequestFactory())->createServerRequest( 'POST', '/internal/jobs/tick', ['REMOTE_ADDR' => $remoteAddr], ); $passthrough = new class () implements RequestHandlerInterface { public bool $reached = false; public function handle(ServerRequestInterface $request): ResponseInterface { $this->reached = true; return (new ResponseFactory())->createResponse(204); } }; $response = $middleware->process($req, $passthrough); if ($shouldPass) { self::assertSame(204, $response->getStatusCode(), $remoteAddr . ' should be allowed'); self::assertTrue($passthrough->reached); } else { self::assertSame(404, $response->getStatusCode(), $remoteAddr . ' should be denied'); self::assertFalse($passthrough->reached, 'handler must not see disallowed sources'); } } }