Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add upnp support #1022

Merged
merged 15 commits into from
Dec 1, 2023
Merged

feat: add upnp support #1022

merged 15 commits into from
Dec 1, 2023

Conversation

qizhou
Copy link
Contributor

@qizhou qizhou commented Nov 1, 2023

What was wrong?

Currently, Trin cannot support a private node running behind a firewall/gateway. Inspired by libp2p, this PR adds UPnP support, which is the default NAT used in libp2p.

How was it fixed?

UPnP protocol will ask the gateway to map a private IP/port to an external IP/port and return the external address to Trin. By encoding the external address in ENR, Trin will allow any node to access a Trin node running behind the gateway.

Test:

  • Run a Trin node behind a gateway with an empty bootnode. The node will print the ENR with an external IP/port mapped by UPnP.
  • Run another Trin node using the bootnode's ENR, which connects the bootnode.
  • Call portal_historyGossip (using test vector) from one node and check that another node receives the data.

To-Do

@KolbyML
Copy link
Member

KolbyML commented Nov 3, 2023

The guy at nimbus who was pushing for 4444s told me why aren't you guys just using upnp, so I think this is a good first direction.

As Qi said on discord in general channel we will probably end up supporting

  • UPnP (default)
  • rendezvous
  • relay

In that order of priority.

This will also majorly support our effort to having a large network.

Thank you Qi for looking into this I believe this PR solves or at least work towards #22 Carver can correct me if I am wrong on that

I guess I am saying I support this change

portalnet/src/socket.rs Outdated Show resolved Hide resolved
ethportal-api/src/types/cli.rs Outdated Show resolved Hide resolved
@@ -170,6 +177,7 @@ impl Default for TrinConfig {
bootnodes: Bootnodes::Default,
external_addr: None,
no_stun: false,
no_upnp: false,
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems like this means we're enabling upnp by default? Is that the desired behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The behavior is borrowed from geth's default behavior, where UPnP is used by default (and there is no flag to disable it). Feel free to let me know if you have better thoughts.

Copy link
Member

Choose a reason for hiding this comment

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

I believe enable by default is the desired behavior

portalnet/src/discovery.rs Outdated Show resolved Hide resolved
portalnet/src/socket.rs Outdated Show resolved Hide resolved
@qizhou qizhou changed the title add upnp support feat: add upnp support Nov 13, 2023
Copy link
Member

@ogenev ogenev left a comment

Choose a reason for hiding this comment

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

Added some questions/comments. I think we should also remove the port mapping on ctr-c or panic. I don't think we have a panic hooks right now for the main Trin process but adding a remove port mapping function and a call in main.rs should be ok for now.

portalnet/src/socket.rs Outdated Show resolved Hide resolved
portalnet/src/socket.rs Outdated Show resolved Hide resolved
"new_port",
) {
Ok(()) => {
thread::spawn(move || loop {
Copy link
Member

Choose a reason for hiding this comment

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

What is the reason for spawning this thread and the loop in the background? If gateway.add_port returns Ok(()), the mapping should be successfull ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This thread is used to renew the port periodically so that Trin can own the port as long as it is alive, while the port will be released in < 60 mins if Trin quits in any matter (crash or gracefully shutdown).

Copy link
Member

@ogenev ogenev Nov 29, 2023

Choose a reason for hiding this comment

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

Ahh, it makes sense. It will be nice to have some comments with a short description of what is happening here.

Let's use tokio to spawn the thread with tokio sleep:

Suggested change
thread::spawn(move || loop {
tokio::spawn(async move { loop {
tokio::time::sleep(time::Duration::from_secs(UPNP_MAPPING_TIMEOUT)).await;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A comment is added.

I did some study on tokio::spawn, and I believe that it may not be a good way because gateway.add_port into the UPnP library is a blocking IO, which may block other async tasks. I tried to use tokio::tasks::spawn_blocking, but I encountered an issue that SIGINT (ctrl + c) cannot exit Trin gracefully. As a result, I have to revert to the dedicated thread method for this purpose.

Please let me know if you have better ideas.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I didn't know that gateway.add_port is a blocking IO. In this case we have two options tokio spawn_blocking or a dedicated thread but because we expect this thread to run forever, I think std::thread::spawn is the way to go 👍 .

portalnet/src/socket.rs Outdated Show resolved Hide resolved
use tracing::{debug, info, warn};

// This stun server is part of the testnet infrastructure.
// If you are unable to connect, please create an issue.
const STUN_SERVER: &str = "159.223.0.83:3478";

/// The duration in seconds of an external port mapping by UPnP.
const UPNP_MAPPING_DURATION: u32 = 3600;
Copy link
Member

Choose a reason for hiding this comment

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

Why are we mapping UPnP for only 60 minutes? I think we want to use an infinite value here, which in this case should be 0.

Copy link
Member

Choose a reason for hiding this comment

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

I am slightly in the camp of having a limit unless we're absolutely sure that these will expire after a period of disuse. We don't want to be opening ports up on people's home networks indefinitely. Seems worth the effort to implement periodic renewal of the mapping to make sure we aren't being bad citizens on user machines.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am slightly in the camp of having a limit unless we're absolutely sure that these will expire after a period of disuse. We don't want to be opening ports up on people's home networks indefinitely. Seems worth the effort to implement periodic renewal of the mapping to make sure we aren't being bad citizens on user machines.

Agree. The current design will initially map the port for 60 minutes, and then periodically (every 30 minutes) renew the port for another 60 minutes. That said if Trin is down for no matter what reasons (crash or gracefully closed), the port reserved by Trin will be automatically released in < 60 minutes.

}
};

let local_ip = match TcpStream::connect(gateway.addr) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if I'm in love with the idea of establishing a TCP stream to the gateway and getting the local IP address from there.

I don't know if https://docs.rs/local-ip-address/0.5.6/local_ip_address/fn.local_ip.html guarantees to return the correct IP address if more than one default interface is enabled. (if it is comparing the metric value from the routing table info, should be ok).

Copy link
Contributor Author

@qizhou qizhou Nov 29, 2023

Choose a reason for hiding this comment

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

Actually, the previous version of the PR uses local_ip and local_ip_v6 to find the local IP address as below:

local_addr = match local_ip_address::local_ip().or(local_ip_address::local_ipv6())  ...

However, as @carver points out that this may be problematic if multiple interfaces exist. That is why I switch to a TCP way to detect the local IP.

Note that the UPnP protocol also uses an HTTP/TCP connection to the gateway. Unfortunately, the UPnP library does not return which local IP address is used to talk to the gateway. So, without modifying UPnP library, making a TCP connection to the gateway is an easy solution to determine the local IP address

group = "external-ips",
help = "Do not use UPnP to determine an external port."
)]
pub no_upnp: bool,
Copy link
Member

@ogenev ogenev Nov 28, 2023

Choose a reason for hiding this comment

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

I would prefer allow_upnp with the default value true, I think it is less mentally overwhelming that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to change it to allow_upnp, but looks like clap does not support setting a boolean flag to false. Below is the error message I got

$ trin --allow-upnp=false

error: unexpected value 'false' for '--allow-upnp' found; no more were expected

Usage: trin --allow-upnp

For more information, try '--help'.

portalnet/src/socket.rs Outdated Show resolved Hide resolved
Copy link
Member

@ogenev ogenev left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@ogenev ogenev merged commit 3f24614 into ethereum:master Dec 1, 2023
1 of 2 checks passed
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.

6 participants