net/portmapper: be smarter about selecting a UPnP device

Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.

Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.

Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.

RELNOTE=Improve UPnP portmapping when multiple UPnP services exist

Updates #8364

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
This commit is contained in:
Andrew Dunham
2023-12-06 11:55:49 -05:00
parent 971fa8dc56
commit bac4890467
4 changed files with 697 additions and 109 deletions

View File

@@ -1015,6 +1015,30 @@ var (
// received a UPnP response from a port other than the UPnP port.
metricUPnPResponseAlternatePort = clientmetric.NewCounter("portmap_upnp_response_alternate_port")
// metricUPnPSelectSingle counts the number of times that only a single
// UPnP device was available in selectBestService.
metricUPnPSelectSingle = clientmetric.NewCounter("portmap_upnp_select_single")
// metricUPnPSelectMultiple counts the number of times that we need to
// select from among multiple UPnP devices in selectBestService.
metricUPnPSelectMultiple = clientmetric.NewCounter("portmap_upnp_select_multiple")
// metricUPnPSelectExternalPublic counts the number of times that
// selectBestService picked a UPnP device with an external public IP.
metricUPnPSelectExternalPublic = clientmetric.NewCounter("portmap_upnp_select_external_public")
// metricUPnPSelectExternalPrivate counts the number of times that
// selectBestService picked a UPnP device with an external private IP.
metricUPnPSelectExternalPrivate = clientmetric.NewCounter("portmap_upnp_select_external_private")
// metricUPnPSelectUp counts the number of times that selectBestService
// picked a UPnP device that was up but with no external IP.
metricUPnPSelectUp = clientmetric.NewCounter("portmap_upnp_select_up")
// metricUPnPSelectNone counts the number of times that selectBestService
// picked a UPnP device that is not up.
metricUPnPSelectNone = clientmetric.NewCounter("portmap_upnp_select_none")
// metricUPnPParseErr counts the number of times we failed to parse a UPnP response.
metricUPnPParseErr = clientmetric.NewCounter("portmap_upnp_parse_err")