Skip to content

Conversation

laanwj
Copy link
Member

@laanwj laanwj commented Apr 30, 2024

The overarching goal here is to increase the number of connectable nodes that are not in the big datacenters.

Context: IPv6 doesn't have NAT. Computers behind a router tend to get a globally routable address. However, by default this address is usually completely firewalled for incoming connections, as a security measure. See issue #17012.

This is where "pinholing" comes in. By sending a request, a machine on the network can request a port to be opened. This is similar to requesting a port forward for IPv4 but simpler.

This PR implements the so-called PCP (Port Control Protocol) from RFC6887. This is a simple UDP based protocol with fixed-size packets, so is safe to possibly even enable by default. Much simpler than UPnP which also has commands to open a pinhole, but relies on SSDP, HTTP, and XML parsing (and miniupnpc has had serious RCEs in the past). This implementation is self-contained, no external dependency is added.

Before integrating it into Bitcoin Core i would first like to investigate whether this implementation is correct and whether routers support it. So this adds a binary, ipv6-pinhole-test, which:

  • Enumerates local publicly routable IPv6 addresses
  • Gets the default gateway to get the PCP endpoint
  • Requests pinholes for 100 seconds to port 1234 on all addreses, and prints the result.

i've tested this on two routers myself--Turris Omnia and Fritz!Box, where it worked. Please help testing, by just running it behind the router of your choice!

For now, this is for Linux only. Implementing it for other platforms requires implementing a way to get the default route. i'll get to this later.

Be careful with publicly posting the full output of this program-it will contain your IP address information.

[skip ci]

@DrahtBot
Copy link
Contributor

DrahtBot commented Apr 30, 2024

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept NACK fanquake
Concept ACK Sjors

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #30040 (util, refactor: Switch to value-initialization by hebasto)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@laanwj laanwj force-pushed the 2024-04-pcp-pinhole-test branch from a263748 to 39593f8 Compare April 30, 2024 11:36
@laanwj laanwj requested a review from Sjors April 30, 2024 11:48
@Sjors
Copy link
Member

Sjors commented Apr 30, 2024

Tried with a router running OPNsense:

2024-04-30T12:16:13Z [net:debug] pcp6: gateway: ...
2024-04-30T12:16:13Z [net:warning] pcp6: Opening pinhole for addr: ...
2024-04-30T12:16:14Z [net:debug] pcp6: Timeout
2024-04-30T12:16:14Z [net:debug] pcp6: Retrying (1)
2024-04-30T12:16:15Z [net:debug] pcp6: Timeout
2024-04-30T12:16:15Z [net:debug] pcp6: Retrying (2)
2024-04-30T12:16:16Z [net:debug] pcp6: Timeout
2024-04-30T12:16:16Z [net:debug] pcp6: Giving up after 3 tries
2024-04-30T12:16:16Z [net:warning] pcp6: Opening pinhole for addr: ...
2024-04-30T12:16:17Z [net:debug] pcp6: Timeout
2024-04-30T12:16:17Z [net:debug] pcp6: Retrying (1)
2024-04-30T12:16:18Z [net:debug] pcp6: Timeout
2024-04-30T12:16:18Z [net:debug] pcp6: Retrying (2)
2024-04-30T12:16:19Z [net:debug] pcp6: Timeout
2024-04-30T12:16:19Z [net:debug] pcp6: Giving up after 3 tries
2024-04-30T12:16:19Z [net:warning] pcp6: Opening pinhole for addr: ...
2024-04-30T12:16:20Z [net:debug] pcp6: Timeout
2024-04-30T12:16:20Z [net:debug] pcp6: Retrying (1)
2024-04-30T12:16:21Z [net:debug] pcp6: Timeout
2024-04-30T12:16:21Z [net:debug] pcp6: Retrying (2)
2024-04-30T12:16:22Z [net:debug] pcp6: Timeout
2024-04-30T12:16:22Z [net:debug] pcp6: Giving up after 3 tries

I probably have to actively turn on some permission.

@laanwj
Copy link
Member Author

laanwj commented Apr 30, 2024

It may be some router setting to enable it, the more secure routers have these kind of protocols disabled by default. Or maybe it doesn't support it. There isn't any reply, not even 2 NOT_AUTHORIZED (but nice to see the retry code is working 😄 ).

@Sjors
Copy link
Member

Sjors commented Apr 30, 2024

Unfortunately I'm not able to find any documentation for this.

There is a UPnP plugin, but that's not what you're using (and probably best left uninstalled).

Anyway, people who run OPNsense will now how to manually forward a port, so that's not really the target demographic here.

@laanwj
Copy link
Member Author

laanwj commented Apr 30, 2024

There is a UPnP plugin, but that's not what you're using (and probably best left uninstalled).

Yea, according to this list, UPnP and PCP is the same plugin. It could be that they can be turned on and off seperately in the plugin configuration (it's similar for OpenWRT). The most well-known open source implementation of a PCP server is... ironically, miniupnpd.

@Sjors
Copy link
Member

Sjors commented May 2, 2024

Ah, that's confusing. The OPNsense is indeed just MiniUPnPd and described as "Universal Plug and Play (UPnP IGD & PCP/NAT-PMP) Service", in other words: all the things :-)

It lets you choose what to enable, in this case I selected "PCP/NAT-PMP Port Mapping".

Now your utility is happy.

On Ubuntu I additionally had to do sudo ufw allow 1234 though for the nc connection to succeed (ufw is disabled by default on new systems).

I tested that the machine was indeed reachable from the outside world (and that it no longer is after 120 seconds, existing connections are preserved).

Copy link
Member

@Sjors Sjors left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK. RFC 6887 is quite readable and simple for a client to support.

IPv6 always returns multiple addresses. I'm not sure what the logic behind that is, but should we gossiping and pinholing all of them? Or is one "supposed" to be used?

Why not also use PCP for IPv4 NAT? If it's widely supported enough, maybe it lets us drop both upnp and natpmp from the dependencies (immediately or at some point in the future if there's actually a problem with them).

This appears to be trivial:

When the address field holds an IPv4 address, an IPv4-mapped IPv6 address RFC4291 is used (::ffff:0:0/96).

One nice feature of PCP is that it gives us the external IP(v4) address. We could use that in lieu of / in addition to asking our peers for it. But note that section 11.6 recommends against doing this without also requesting a port forward / pinhole.

@laanwj
Copy link
Member Author

laanwj commented May 2, 2024

Thanks for the review!

IPv6 always returns multiple addresses. I'm not sure what the logic behind that is, but should we gossiping and pinholing all of them? Or is one "supposed" to be used?

That's a good question. Sometimes routers will give out temporary addresses as well as permanent addresses, i'm not sure it's possible to discover the intent of an address at application level. If so it is going to be OS specific. Currently in master the application Discover()s alll publicly routable IPv6 addresses, which seems like a sensible default.

More advanced networking users will hand-configure things anyway. And if specific addresses are being -listened on, we should heed that.

Why not also use PCP for IPv4 NAT? If it's widely supported enough, maybe it lets us drop both upnp and natpmp from the dependencies (immediately or at some point in the future if there's actually a problem with them).

i agree with this, but it's not as urgent. We have good coverage for IPv4 port mapping methods, i'm kind of hestitant to touch that, given how hard it already is to find people to test these kind of things 😅

So called "dual stack lite" are getting quite popular for ISPs, which essentially means no publicly routable IPv4 ("carrier-grade NAT") only IPv6. So for home users this is a better focus.

One nice feature of PCP is that it gives us the external IP(v4) address.

AFAIK, UPnP and natpmp also give you this information. In regard to IPv4, natpmp and pcp are essentially the same with slightly different packet layout.

@fanquake
Copy link
Member

fanquake commented May 2, 2024

Could you rebase this on master, now that we are using the latest version of miniupnpc (#29707).

@Sjors
Copy link
Member

Sjors commented May 2, 2024

i'm kind of hestitant to touch that, given how hard it already is to find people to test these kind of things

For now we could add support in the test, but stick to IPv6 in the implementation. OTOH both UPnP and NAP-PMP are off by default. Adding another off-by-default checkbox "IPv6 pinhole" to the GUI seems pretty confusing, and probably doesn't increase the chance of users trying this.

@laanwj
Copy link
Member Author

laanwj commented May 2, 2024

Could you rebase this on master, now that we are using the latest version of miniupnpc (#29707).

Yes, but first want to address @Sjors's comments. Mind that this adds an utility and is orthogonal to UPnP. Adding IPv6 pinholing through UPnP would be useful too (for routers that support that, and not PCP), so we'll likely want to be able to do both in the eventual design like we do for natpmp/UPnP for IPv4.

For now we could add support in the test, but stick to IPv6 in the implementation.

Sure, could. But mind that IPv4 will be mostly a different path. Default gateway discovery is different (mind that i still have to implement that for Windows and MacOS), port mapping semantics are slightly different from pinholing, the code will have to handle IPv4 addresses. This added complexity makes it harder to review, too.

Adding another off-by-default checkbox "IPv6 pinhole" to the GUI seems pretty confusing, and probably doesn't increase the chance of users trying this.

i would like it enabled by default, i am not sure people have that much confidence in my code lol. But it's a discussion for the integration PR, we'll probably aim to merge it with disabled-default first to move forward at all.

@fanquake
Copy link
Member

fanquake commented May 2, 2024

Mind that this adds an utility and is orthogonal to UPnP.

~0. Right, I have misunderstood what this is/does. This could be a useful tool (for some % of users of this software), but I don't think this repository is the right place for it to live, or that it fits into the set of things we should be maintaining here (I'm really suprised there isn't software that already exists, for doing the same thing, that we could just point users to?).

@laanwj
Copy link
Member Author

laanwj commented May 2, 2024

This is a temporary PR for people to test this code, i intend to integrate it in bitcoind just like the current IPv4 port mapping stuff. I did not intend it to be a useful tool to keep around. This is why the title says "nomerge".

@fanquake
Copy link
Member

fanquake commented May 2, 2024

I did not intend it to be a useful tool to keep around.

Fair enough, I'll -redirect my NACK to this thread: #30005 (comment).

@Sjors
Copy link
Member

Sjors commented May 2, 2024

Sure, could. But mind that IPv4 will be mostly a different path.

I see. I guess future UI confusion can ben prevented by calling this "PCP" with a note saying that it works with IPv6 only for now.

The GUI could also hide the kitchen sink of methods under some advanced toggle. In any case that doesn't affect testing.

@laanwj
Copy link
Member Author

laanwj commented May 2, 2024

I see. I guess future UI confusion can ben prevented by calling this "PCP" with a note saying that it works with IPv6 only for now.

OK, i see your point now. Let's replace NAT-PMP for IPv4 at the same time, so we keep the same number of options (PCP and UPnP), and lose one dependency (libnatpnp).

@Sjors
Copy link
Member

Sjors commented May 2, 2024

I'm really suprised there isn't software that already exists, for doing the same thing, that we could just point users to?

If there's a library that does the same thing, and doesn't have 1 million lines of code, then it seems fine to include it. We can also make it a library under the core repo, which perhaps other projects will find useful for its simplicity. At least now I have a sense of what that library should be doing.

I don't think we should ask users to install a separate piece of software. For HWI I understand the argument of not wanting to include USB support in our codebase. But this protocol is much simpler (one UDP message and one reply).

Some extra maintenance work seems justified here because there's at least a plausible risk that we're running short on listening nodes, if we turn on Asmap: #16599 (comment)

Also, I was under the assumption that the reason we added NAT-PMP originally was to eventually replace UPnP (see #11902). Have those concerns been reduced? Do you see PCP as a potential replacement for both NAT-PMP and UPnP? Or not sure, let's keep them all around for now?

@laanwj
Copy link
Member Author

laanwj commented May 2, 2024

i mean, there's probably some crappy C implementation of PCP out there that we could include, but this one is written in C++, it integrates with bitcoin's networking and logging, well commented, and it's straightforward enough. Besides, any code included from a third party would have to be reviewed just as well.

i've already agreed to add IPv4 support so it can replace libnatpmp.

@fanquake's comment was based on a misunderstanding.

@DrahtBot DrahtBot removed the CI failed label May 3, 2024
laanwj added 3 commits May 4, 2024 17:36
Still default to 10, of course. Need this for parsing a hexadecimal integer.
@laanwj laanwj force-pushed the 2024-04-pcp-pinhole-test branch from f66e2e8 to e3d4765 Compare May 4, 2024 15:46
@laanwj laanwj changed the title [PoC, nomerge] IPv6 PCP pinhole test [PoC, nomerge] PCP IPv4 portmap+IPv6 pinhole test May 4, 2024
@laanwj
Copy link
Member Author

laanwj commented May 4, 2024

@Sjors It creates a IPv4 as well as IPv6 mappings now. The added complexity isn't even too bad, mostly a matter of using CNetAddr/CService/Sock abstraction that we already have.

@laanwj
Copy link
Member Author

laanwj commented May 5, 2024

Continued in #30043.

@laanwj laanwj closed this May 5, 2024
achow101 added a commit that referenced this pull request Sep 30, 2024
…ntation

5c7cacf ci: Remove natpmp build option and libnatpmp dependency (laanwj)
7e7ec98 doc: Remove mention of natpmp build options (laanwj)
061c3e3 depends: Drop natpmp and associated option from depends (laanwj)
20a18bf build: Drop libnatpmp from build system (laanwj)
7b04709 qt: Changes for built-in PCP+NAT-PMP (laanwj)
52f8ef6 net: Replace libnatpmp with built-in NATPMP+PCP implementation in mapport (laanwj)
97c9717 net: Add PCP and NATPMP implementation (laanwj)
d72df63 net: Use GetLocalAddresses in Discover (laanwj)
e020304 net: Add netif utility (laanwj)
754e425 crypto: Add missing WriteBE16 function (laanwj)

Pull request description:

  Continues #30005. Closes #17012..

  This PR adds PCP (Port Control Protocol) from [RFC6887](https://datatracker.ietf.org/doc/html/rfc6887).  This adds, in addition to the existing IPv4 port mapping (which now uses PCP, with fallback to NAT-PMP), support for IPv6 pinholing-that is, opening a port on the firewall to make it reachable.

  PCP, like NAT-PMP is a simple UDP-based protocol, and the implementation is self-contained, so this gets rid of lthe libnatpnp dependency without adding a new one. It should otherwise be a drop-in replacement. NAT-PMP fallback is implemented so this will not make router support worse.

  For now it is disabled by default, though in the future (not in this PR) we could consider enable it by default to increase the number of connectable nodes without adding significant attack surface.

  To test:
  ```bash
  bitcoind -regtest -natpmp=1 -debug=net
  ```

  (most of the changes in this PR are, ironically, removing the libnatpmp dependency and associated build system and build docs)

  ## TODO

  - [x] Default gateway discovery on Linux / FreeBSD
  - [x] Default gateway discovery on Windows
  - [x] Default gateway discovery on MacOS
  - [x] Either solve FreeBSD compile issue (probably upstream issue) or remove FreeBSD support

  ## Things to consider for follow-up PRs

  - #30043 (comment) avoid unreachable nets (not given to -onlynet=)

  - #30043 (comment) could announce an addr:port where we do not listen (no -bind)

  - #30043 (comment) could announce the wrong port because it uses GetListenPort()

  - #30043 (comment) if we requested one port but another was assigned, then which one to use in the renewal?

  - #30043 (comment) Use `GetAdapterAddresses` to discover local addresses for Windows

ACKs for top commit:
  Sjors:
    ACK 5c7cacf
  achow101:
    ACK 5c7cacf
  vasild:
    ACK 5c7cacf

Tree-SHA512: e35b69e56d5f5449a3d48a799f9b7b65107c65eeb3e245c2c1e9d42221e469ca5ead90afae423160601cd664dd553a51c859e04f4492f335b064aae3bf23e3bc
@bitcoin bitcoin locked and limited conversation to collaborators May 5, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants