threadi / tcp-analyzer
Requires
- php: >=8.2
Requires (Dev)
- automattic/vipwpcs: ^3.0
- dealerdirect/phpcodesniffer-composer-installer: ^1.2
- matthiasmullie/minify: ^1.3
- php-stubs/wp-cli-stubs: ^2.11
- phpcompatibility/phpcompatibility-wp: ^2.1
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11
- wp-coding-standards/wpcs: ^3.4.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-03 09:09:32 UTC
README
This repository contains the source code for the Composer package "TCP Analyzer".
How it works
The package ships a set of independent test classes (TcpAnalyzer\Tests\*), each
checking one thing (external IP, DNS resolution, raw TCP connect, HTTP timing,
traceroute). The TcpAnalyzer class configures and runs a chosen subset of them.
Every test returns a TcpAnalyzer\Result object - a purely structured, language-free
result: a Status enum (success / warning / error / skipped), the requested
value (e.g. the external IP, a millisecond value, a HTTP status code), additional
data, and - on failure - a short, stable error_code string (e.g.
tcp_connect_timeout) instead of an error message. The consuming project maps these
codes to its own text/translations.
Custom tests can be added via register_test() as long as they implement
TcpAnalyzer\Test_Interface (easiest: extend TcpAnalyzer\Tests_Base).
Requirements
- composer to install this package.
Installation
composer require threadi/tcp-analyzer- Add the following code in your project to run tests:
$tcp_analyzer = new \TcpAnalyzer\TcpAnalyzer(); $tcp_analyzer->set_tests( array( 'external_ip' => array(), 'dns' => array( 'host' => 'example.com' ), 'tcp_connect' => array( 'host' => 'example.com', 'port' => 443 ), 'http' => array( 'url' => 'https://example.com' ), 'traceroute' => array( 'host' => 'example.com' ), ) ); $tcp_analyzer->run(); // Result objects, e.g. $results['dns']->get_status(), ->get_value(), ->get_error_code(). $results = $tcp_analyzer->get_results(); // Or as plain arrays, e.g. for json_encode() / an API response. $results_as_array = $tcp_analyzer->get_results_as_array();
get_results_as_array() for the above example looks roughly like this - note
there is no language-specific text anywhere, only stable, machine-readable data:
{
"external_ip": { "slug": "external_ip", "status": "success", "value": "203.0.113.42", "data": { "provider": "https://api.ipify.org?format=json" }, "error_code": null, "duration_ms": 87.3 },
"dns": { "slug": "dns", "status": "success", "value": "93.184.216.34", "data": { "host": "example.com" }, "error_code": null, "duration_ms": 12.1 },
"tcp_connect": { "slug": "tcp_connect", "status": "error", "value": null, "data": { "host": "example.com", "port": 443, "errno": 0 }, "error_code": "tcp_connect_timeout", "duration_ms": 8000.0 },
"traceroute": { "slug": "traceroute", "status": "skipped", "value": null, "data": [], "error_code": "shell_exec_disabled", "duration_ms": null }
}
Parameters
Each test's supported config keys are documented in its class docblock
(src/Tests/*.php), e.g. dns requires host, tcp_connect requires host
and port, http requires url.
Restricting targets
The package does not restrict what a test may talk to - checking an internal host is a legitimate use of a diagnostic tool. As soon as a host or URL originates from user input, though, the tests could be abused to probe the internal network of the server (SSRF): which hosts exist, which ports are open, what the cloud metadata service answers. In that case set a target filter:
$tcp_analyzer = new \TcpAnalyzer\TcpAnalyzer(); // Only publicly routable addresses: no loopback, private, link-local // (169.254.169.254), carrier-grade NAT (100.64.0.0/10), ... - IPv4 and IPv6. $tcp_analyzer->set_target_filter( \TcpAnalyzer\Target_Filter::public_only() );
The filter is any callable. It is asked with the configured host, the IP that
host resolved to and the port (null for tests without one), and has to
return exactly true to accept the target:
$tcp_analyzer->set_target_filter( static function ( string $host, string $ip, ?int $port ): bool { return \TcpAnalyzer\Target_Filter::is_public_ip( $ip ) && in_array( $port, array( 80, 443, null ), true ); } );
What a filter guarantees:
- The host is resolved once. The IP the filter has accepted is the IP the
test connects to (
tcp_connect,traceroute) resp. the IP the HTTP request is pinned to (http,external_ip) - a second DNS answer cannot redirect the test somewhere else (DNS rebinding).dnsdoes not report an IP the filter has rejected. - A rejected target yields the error code
target_rejected, a host that does not resolvedns_resolution_failed, and a host that is neither a plain ASCII hostname nor an IP address in its usual notationinvalid_host(internationalized names have to be given as punycode;2130706433,127.1or0x7f000001are refused). - With a filter, an HTTP request never uses a proxy (curl would otherwise pick one up from the environment), as a proxy resolves the host on its own.
A filter can also be set for a single test via its target_filter config
key, which takes precedence over the one of the analyzer (null opts that
test out). So never build the config of a test from user input as a whole,
only pass on the single values.
One thing a filter cannot hide: target_rejected and dns_resolution_failed
are different answers, so they still tell whether a name exists in the
internal DNS. Show both as the same message if that matters to you.
Independent of any filter, the HTTP based tests (http, external_ip):
- only request
http://andhttps://URLs - anything else yieldsurl_scheme_not_allowed, an incomplete URL (no scheme, no host, port 0, whitespace)invalid_url; - never follow redirects. The result describes the URL that was given: a
301is reported as301(and counts as success), the redirect target is not requested; - read not more than 1 MB of a response body, the rest is cut off;
- treat the timeout as the limit for the whole request. One exception: the stream fallback (used if curl is missing) cannot limit the time a server takes to send its response headers as a whole, only every single read;
- do not repeat credentials given in the URL (
https://user:pass@...) in the result data.
traceroute only accepts a plain hostname or IP address as host, anything
else yields invalid_host. tcp_connect only accepts the ports 1 to 65535,
anything else yields invalid_port.
Limits and failures
The tests are built to end in reasonable time, and a failing test does not take the others down:
traceroutelimitsmax_hopsto 1 to 64 andwaitto 1 to 10 seconds. The built-in socket method additionally stops after 30 seconds and reports the hops found until then (the second constructor argument ofSocket_Methodsets another budget). The shell method runs as long as thetraceroutebinary does.- A required config value that is not a string, number or boolean yields
invalid_config, an optional one of the wrong type falls back to its default. - A test that throws - a custom test, a target filter - does not stop the
run: it is reported with the error code
test_exceptionand the class of what has been thrown indata.exception, the other tests still run.
Adding a custom test
class MyCustomTest extends \TcpAnalyzer\Tests_Base { public function get_slug(): string { return 'my_custom_test'; } protected function execute(): void { // ... do the check, then: $this->set_result( \TcpAnalyzer\Enums\Status::SUCCESS, 'some-value' ); } } $tcp_analyzer->register_test( 'my_custom_test', MyCustomTest::class ); $tcp_analyzer->set_tests( array( 'my_custom_test' => array() ) );
For package developers
Initialize
composer install
Analyze with PHPStan
vendor/bin/phpstan analyse