Skip to content

check_port: add provider failover and Globalping - #3245

Merged
xirvik merged 1 commit into
Novik:masterfrom
chiphead2332:feature/check-port-provider-failover-clean
Sep 2, 2026
Merged

check_port: add provider failover and Globalping#3245
xirvik merged 1 commit into
Novik:masterfrom
chiphead2332:feature/check-port-provider-failover-clean

Conversation

@chiphead2332

Copy link
Copy Markdown
Contributor

Summary

Adds configurable provider failover to the check_port plugin and adds
Globalping as a third port-check provider.

The primary provider remains unchanged by default:

  • IPv4: YouGetSignal
  • IPv6: PortChecker

Default fallbacks are:

  • IPv4: PortChecker → Globalping
  • IPv6: Globalping

Provider implementations are moved out of action.php into separate files
and registered through a shared provider registry. The dispatcher is
provider-agnostic: providers return 0 (unknown), 1 (closed), or 2 (open),
and the configured chain continues until a definitive result is available.

Globalping is used as the final fallback when earlier providers fail or return
an inconclusive result.

Testing

Tested on a live ruTorrent/rTorrent installation.

  • Verified IPv4 and IPv6 port checks.
  • Tested all three providers and provider rotation.
  • Tested failover by forcing the primary provider to fail and confirming Globalping supplied the result.
  • All changed PHP files pass php -l.
  • Unchanged frontend and localization files remain identical to upstream.

@xirvik

xirvik commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Tested it. TL;DR it reports all ports as open.

Long explanation from Claude:

Globalping's rawOutput uses tcp_conn=N as a per-attempt counter, and it
appears on the "No reply" lines as well as the reply lines. The open test is
preg_match('/tcp_conn=\d+/i', $raw), so it matches both. The closed test,
/connection refused|tcp_conn=.*refused/i, never fires, because the wording is
"No reply", not "connection refused".

Measured against the live API, sending exactly the request shape in this branch:

8.8.8.8:53   open       rawOutput: "PING dns.google (8.8.8.8) on port 53.
                        Reply from dns.google (8.8.8.8) on port 53:
                        tcp_conn=1 time=1.01 ms ..."
                        -> verdict 2 OPEN     correct

8.8.8.8:81   filtered   rawOutput: "PING dns.google (8.8.8.8) on port 81.
                        No reply from dns.google (8.8.8.8) on port 81:
                        tcp_conn=1 ..."
                        -> verdict 2 OPEN     wrong

1.1.1.1:81   filtered   rawOutput: "PING one.one.one.one (1.1.1.1) on port
                        81. No reply from one.one.one.one (1.1.1.1) on port
                        81: tcp_conn=1 ..."
                        -> verdict 2 OPEN     wrong

Every outcome is "open". Because Globalping is the final fallback, this fires
exactly when the other providers could not answer -- so a user whose port is
genuinely unreachable, on a day when the primary provider is down, is told the
port is open. That is worse than the "unknown" state they get today: it sends
them looking anywhere except at the actual problem, which is the one thing this
indicator exists to prevent.

The discriminator is "Reply from" versus "No reply from":

open    /^Reply from .*tcp_conn=\d+/mi
closed  /^No reply from/mi   (or the "100% packet loss" summary line)

Worth a test over both rawOutput shapes. The parsing is pure, so it needs no
network -- tests/php/YouGetSignalParseTest.php is the pattern.


Second: the shipped default and the code default disagree.

conf.php     $failoverProvidersIPv4 = ["portchecker", "globalping"]
action.php   fallback  array("globalping", "portchecker")

conf.php is user-editable and survives upgrades, so an existing install takes
the action.php order and gets the new provider first, while the description and
a fresh install say portchecker leads. Whichever order is intended, the two
should agree.


Third: worst-case latency.

With $checkPortTimeout = 15, a failing primary plus a failing secondary plus
Globalping's own 5-10s window and up to three 2s polls is roughly 40s for one
interface action, where today it is capped near 15. Worth bounding the chain, or
budgeting the timeout across the providers rather than per provider.


Smaller notes:

  • api.globalping.io is rate limited to 250 requests/hour per source IP when
    unauthenticated (x-ratelimit-limit: 250). Each install spends its own
    budget, so it is worth a line in conf.php beside the provider list.

  • The new files are space-indented in a tab-indented tree: 77 lines in
    providers/globalping.php, 15 in providers.php, 3 in conf.php.


What is right, for the record: the refactor keeps the Origin header and the
check_port_parse_yougetsignal() call intact, action.php still requires
parse.php so nothing dangles, the request shape sent to Globalping is valid
(202 with a measurement id), and the branch is level with master -- php -l
clean on every changed file, PHP suite rc=0 with 2132 assertions, PHPStan 0.

Not checked: IPv6 end to end, and the failover chain driven through action.php
rather than by calling the providers directly. The wrong verdict is
demonstrable at the provider level, so I stopped there.

@chiphead2332

Copy link
Copy Markdown
Contributor Author

Fixed the Globalping verdict parsing and pushed the changes.

  • Reply from ... tcp_conn=N → OPEN
  • No reply from ... tcp_conn=N → UNKNOWN
  • explicit connection refusal → CLOSED
  • added regression coverage for these cases
  • made the provider chain use a shared total timeout budget
  • aligned the IPv4 fallback defaults between conf.php and action.php

Also verified the corrected Globalping provider against known-open and filtered ports: 3/3 live checks passed.

One potentially interesting avenue for the future: Globalping has probe "pingers" distributed around the world, so selecting an appropriate probe/location could potentially reduce the latency of port checks substantially compared with an arbitrary probe.

@chiphead2332
chiphead2332 force-pushed the feature/check-port-provider-failover-clean branch from aa80982 to 6866338 Compare September 2, 2026 00:11
@chiphead2332
chiphead2332 force-pushed the feature/check-port-provider-failover-clean branch from 0a43624 to efb74a4 Compare September 2, 2026 00:23
@xirvik
xirvik merged commit ad82924 into Novik:master Sep 2, 2026
8 checks passed
xirvik added a commit that referenced this pull request Sep 2, 2026
The Globalping provider added in #3245 read an open port out of
rawOutput. Before it was corrected, the rule matched the tcp_conn=N
attempt counter anywhere in the output, and Globalping prints that
counter on its "No reply" lines too, so every port came back open --
including ports that never answered at all. Because Globalping runs
last in the failover chain, that verdict reached the user exactly when
no other provider could answer.

Nothing held the corrected rule in place: the verdict was inline in
check_port_globalping(), next to the HTTP call, so no test could reach
it without the network.

Move it to check_port_parse_globalping() in parse.php, beside
check_port_parse_yougetsignal(), and cover it with the two rawOutput
shapes the API actually returns -- a reply on 8.8.8.8:53 and no reply
on 8.8.8.8:81 -- plus a refusal, an empty body, and output in an
unfamiliar shape. Silence stays "unknown" rather than "closed": a probe
that heard nothing cannot tell a filtered port from a closed one.

Also check the shipped configuration against the provider registry.
action.php skips any provider that is unknown or that does not claim
the address family being checked, so a name that does not line up costs
no error -- the provider is simply never tried, and the chain is
quietly shorter than the comment in conf.php reads.

Each case was confirmed to fail with the check it guards removed.
@chiphead2332
chiphead2332 deleted the feature/check-port-provider-failover-clean branch September 2, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants