mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-19 12:56:12 +00:00
Add UPnP support for Lighthouse (#1587)
Adding UPnP support will help grow the DHT by allowing NAT traversal for peers with UPnP supported routers. ## Issue Addressed #927 ## Proposed Changes Using IGD library: https://docs.rs/igd/0.10.0/igd/ Adding the the libp2p tcp port and discovery udp port. If this fails it simply logs the attempt and moves on ## Additional Info Co-authored-by: Age Manning <Age@AgeManning.com>
This commit is contained in:
@@ -28,17 +28,17 @@ eth2_ssz_types = { path = "../../consensus/ssz_types" }
|
||||
tree_hash = "0.1.0"
|
||||
futures = "0.3.5"
|
||||
error-chain = "0.12.2"
|
||||
tokio = { version = "0.2.21", features = ["full"] }
|
||||
tokio = { version = "0.2.22", features = ["full"] }
|
||||
parking_lot = "0.11.0"
|
||||
smallvec = "1.4.1"
|
||||
# TODO: Remove rand crate for mainnet
|
||||
# NOTE: why?
|
||||
rand = "0.7.3"
|
||||
fnv = "1.0.6"
|
||||
rlp = "0.4.5"
|
||||
lazy_static = "1.4.0"
|
||||
lighthouse_metrics = { path = "../../common/lighthouse_metrics" }
|
||||
environment = { path = "../../lighthouse/environment" }
|
||||
igd = "0.11.1"
|
||||
itertools = "0.9.0"
|
||||
num_cpus = "1.13.0"
|
||||
lru_cache = { path = "../../common/lru_cache" }
|
||||
get_if_addrs = "0.5.3"
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod service;
|
||||
mod attestation_service;
|
||||
mod beacon_processor;
|
||||
mod metrics;
|
||||
mod nat;
|
||||
mod persisted_dht;
|
||||
mod router;
|
||||
mod sync;
|
||||
|
||||
154
beacon_node/network/src/nat.rs
Normal file
154
beacon_node/network/src/nat.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
//! This houses various NAT hole punching strategies.
|
||||
//!
|
||||
//! Currently supported strategies:
|
||||
//! - UPnP
|
||||
|
||||
use crate::{NetworkConfig, NetworkMessage};
|
||||
use get_if_addrs::get_if_addrs;
|
||||
use slog::{debug, info, warn};
|
||||
use std::net::{IpAddr, SocketAddr, SocketAddrV4};
|
||||
use tokio::sync::mpsc;
|
||||
use types::EthSpec;
|
||||
|
||||
/// Configuration required to construct the UPnP port mappings.
|
||||
pub struct UPnPConfig {
|
||||
/// The local tcp port.
|
||||
tcp_port: u16,
|
||||
/// The local udp port.
|
||||
udp_port: u16,
|
||||
/// Whether discovery is enabled or not.
|
||||
disable_discovery: bool,
|
||||
}
|
||||
|
||||
impl From<&NetworkConfig> for UPnPConfig {
|
||||
fn from(config: &NetworkConfig) -> Self {
|
||||
UPnPConfig {
|
||||
tcp_port: config.libp2p_port,
|
||||
udp_port: config.discovery_port,
|
||||
disable_discovery: config.disable_discovery,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to construct external port mappings with UPnP.
|
||||
pub fn construct_upnp_mappings<T: EthSpec>(
|
||||
config: UPnPConfig,
|
||||
network_send: mpsc::UnboundedSender<NetworkMessage<T>>,
|
||||
log: slog::Logger,
|
||||
) {
|
||||
debug!(log, "UPnP Initialising routes");
|
||||
match igd::search_gateway(Default::default()) {
|
||||
Err(e) => debug!(log, "UPnP not available"; "error" => %e),
|
||||
Ok(gateway) => {
|
||||
// Need to find the local listening address matched with the router subnet
|
||||
let mut local_ip = None;
|
||||
let interfaces = match get_if_addrs() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
debug!(log, "UPnP failed to get local interfaces"; "error" => %e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
for interface in interfaces {
|
||||
// Just use the first IP of the first interface that is not a loopback
|
||||
if !interface.is_loopback() {
|
||||
local_ip = Some(interface.ip());
|
||||
}
|
||||
}
|
||||
|
||||
if local_ip.is_none() {
|
||||
debug!(log, "UPnP failed to find local IP address");
|
||||
return;
|
||||
}
|
||||
|
||||
let local_ip = local_ip.expect("IP exists");
|
||||
|
||||
match local_ip {
|
||||
IpAddr::V4(address) => {
|
||||
let libp2p_socket = SocketAddrV4::new(address, config.tcp_port);
|
||||
let external_ip = gateway.get_external_ip();
|
||||
// We add specific port mappings rather than getting the router to arbitrary assign
|
||||
// one.
|
||||
// I've found this to be more reliable. If multiple users are behind a single
|
||||
// router, they should ideally try to set different port numbers.
|
||||
let tcp_socket = match gateway.add_port(
|
||||
igd::PortMappingProtocol::TCP,
|
||||
libp2p_socket.port(),
|
||||
libp2p_socket,
|
||||
0,
|
||||
"lighthouse-tcp",
|
||||
) {
|
||||
Err(e) => {
|
||||
debug!(log, "UPnP could not construct libp2p port route"; "error" => %e);
|
||||
None
|
||||
}
|
||||
Ok(_) => {
|
||||
info!(log, "UPnP TCP route established"; "external_socket" => format!("{}:{}", external_ip.as_ref().map(|ip| ip.to_string()).unwrap_or_else(|_| "".into()), config.tcp_port));
|
||||
external_ip
|
||||
.as_ref()
|
||||
.map(|ip| SocketAddr::new(ip.clone().into(), config.tcp_port))
|
||||
.ok()
|
||||
}
|
||||
};
|
||||
|
||||
let udp_socket = if !config.disable_discovery {
|
||||
let discovery_socket = SocketAddrV4::new(address, config.udp_port);
|
||||
match gateway.add_port(
|
||||
igd::PortMappingProtocol::UDP,
|
||||
discovery_socket.port(),
|
||||
discovery_socket,
|
||||
0,
|
||||
"lighthouse-udp",
|
||||
) {
|
||||
Err(e) => {
|
||||
debug!(log, "UPnP could not construct discovery port route"; "error" => %e);
|
||||
None
|
||||
}
|
||||
Ok(_) => {
|
||||
info!(log, "UPnP UDP route established"; "external_socket" => format!("{}:{}", external_ip.as_ref().map(|ip| ip.to_string()).unwrap_or_else(|_| "".into()), config.tcp_port));
|
||||
external_ip
|
||||
.map(|ip| SocketAddr::new(ip.into(), config.tcp_port))
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// report any updates to the network service.
|
||||
network_send.send(NetworkMessage::UPnPMappingEstablished{ tcp_socket, udp_socket })
|
||||
.unwrap_or_else(|e| warn!(log, "Could not send message to the network service"; "error" => %e));
|
||||
}
|
||||
_ => debug!(log, "UPnP no routes constructed. IPv6 not supported"),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Removes the specified TCP and UDP port mappings.
|
||||
pub fn remove_mappings(tcp_port: Option<u16>, udp_port: Option<u16>, log: &slog::Logger) {
|
||||
if tcp_port.is_some() || udp_port.is_some() {
|
||||
debug!(log, "Removing UPnP port mappings");
|
||||
match igd::search_gateway(Default::default()) {
|
||||
Ok(gateway) => {
|
||||
if let Some(tcp_port) = tcp_port {
|
||||
match gateway.remove_port(igd::PortMappingProtocol::TCP, tcp_port) {
|
||||
Ok(()) => debug!(log, "UPnP Removed TCP port mapping"; "port" => tcp_port),
|
||||
Err(e) => {
|
||||
debug!(log, "UPnP Failed to remove TCP port mapping"; "port" => tcp_port, "error" => %e)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(udp_port) = udp_port {
|
||||
match gateway.remove_port(igd::PortMappingProtocol::UDP, udp_port) {
|
||||
Ok(()) => debug!(log, "UPnP Removed UDP port mapping"; "port" => udp_port),
|
||||
Err(e) => {
|
||||
debug!(log, "UPnP Failed to remove UDP port mapping"; "port" => udp_port, "error" => %e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => debug!(log, "UPnP failed to remove mappings"; "error" => %e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ use eth2_libp2p::{MessageAcceptance, Service as LibP2PService};
|
||||
use futures::prelude::*;
|
||||
use rest_types::ValidatorSubscription;
|
||||
use slog::{debug, error, info, o, trace, warn};
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration};
|
||||
use store::HotColdDB;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::Delay;
|
||||
@@ -70,6 +70,13 @@ pub enum NetworkMessage<T: EthSpec> {
|
||||
/// The result of the validation
|
||||
validation_result: MessageAcceptance,
|
||||
},
|
||||
/// Called if a known external TCP socket address has been updated.
|
||||
UPnPMappingEstablished {
|
||||
/// The external TCP address has been updated.
|
||||
tcp_socket: Option<SocketAddr>,
|
||||
/// The external UDP address has been updated.
|
||||
udp_socket: Option<SocketAddr>,
|
||||
},
|
||||
/// Reports a peer to the peer manager for performing an action.
|
||||
ReportPeer { peer_id: PeerId, action: PeerAction },
|
||||
/// Disconnect an ban a peer, providing a reason.
|
||||
@@ -96,6 +103,12 @@ pub struct NetworkService<T: BeaconChainTypes> {
|
||||
store: Arc<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>,
|
||||
/// A collection of global variables, accessible outside of the network service.
|
||||
network_globals: Arc<NetworkGlobals<T::EthSpec>>,
|
||||
/// Stores potentially created UPnP mappings to be removed on shutdown. (TCP port and UDP
|
||||
/// port).
|
||||
upnp_mappings: (Option<u16>, Option<u16>),
|
||||
/// Keeps track of if discovery is auto-updating or not. This is used to inform us if we should
|
||||
/// update the UDP socket of discovery if the UPnP mappings get established.
|
||||
discovery_auto_update: bool,
|
||||
/// A delay that expires when a new fork takes place.
|
||||
next_fork_update: Option<Delay>,
|
||||
/// A timer for updating various network metrics.
|
||||
@@ -117,6 +130,20 @@ impl<T: BeaconChainTypes> NetworkService<T> {
|
||||
let network_log = executor.log().clone();
|
||||
// build the network channel
|
||||
let (network_send, network_recv) = mpsc::unbounded_channel::<NetworkMessage<T::EthSpec>>();
|
||||
|
||||
// try and construct UPnP port mappings if required.
|
||||
let upnp_config = crate::nat::UPnPConfig::from(config);
|
||||
let upnp_log = network_log.new(o!("service" => "UPnP"));
|
||||
let upnp_network_send = network_send.clone();
|
||||
if config.upnp_enabled {
|
||||
executor.spawn_blocking(
|
||||
move || {
|
||||
crate::nat::construct_upnp_mappings(upnp_config, upnp_network_send, upnp_log)
|
||||
},
|
||||
"UPnP",
|
||||
);
|
||||
}
|
||||
|
||||
// get a reference to the beacon chain store
|
||||
let store = beacon_chain.store.clone();
|
||||
|
||||
@@ -167,6 +194,8 @@ impl<T: BeaconChainTypes> NetworkService<T> {
|
||||
router_send,
|
||||
store,
|
||||
network_globals: network_globals.clone(),
|
||||
upnp_mappings: (None, None),
|
||||
discovery_auto_update: config.discv5_config.enr_update,
|
||||
next_fork_update,
|
||||
metrics_update,
|
||||
log: network_log,
|
||||
@@ -201,7 +230,6 @@ fn spawn_service<T: BeaconChainTypes>(
|
||||
"Persisting DHT to store";
|
||||
"Number of peers" => format!("{}", enrs.len()),
|
||||
);
|
||||
|
||||
match persist_dht::<T::EthSpec, T::HotStore, T::ColdStore>(service.store.clone(), enrs) {
|
||||
Err(e) => error!(
|
||||
service.log,
|
||||
@@ -214,6 +242,9 @@ fn spawn_service<T: BeaconChainTypes>(
|
||||
),
|
||||
}
|
||||
|
||||
// attempt to remove port mappings
|
||||
crate::nat::remove_mappings(service.upnp_mappings.0, service.upnp_mappings.1, &service.log);
|
||||
|
||||
info!(service.log, "Network service shutdown");
|
||||
return;
|
||||
}
|
||||
@@ -240,6 +271,24 @@ fn spawn_service<T: BeaconChainTypes>(
|
||||
NetworkMessage::SendError{ peer_id, error, id, reason } => {
|
||||
service.libp2p.respond_with_error(peer_id, id, error, reason);
|
||||
}
|
||||
NetworkMessage::UPnPMappingEstablished { tcp_socket, udp_socket} => {
|
||||
service.upnp_mappings = (tcp_socket.map(|s| s.port()), udp_socket.map(|s| s.port()));
|
||||
// If there is an external TCP port update, modify our local ENR.
|
||||
if let Some(tcp_socket) = tcp_socket {
|
||||
if let Err(e) = service.libp2p.swarm.peer_manager().discovery_mut().update_enr_tcp_port(tcp_socket.port()) {
|
||||
warn!(service.log, "Failed to update ENR"; "error" => e);
|
||||
}
|
||||
}
|
||||
// if the discovery service is not auto-updating, update it with the
|
||||
// UPnP mappings
|
||||
if !service.discovery_auto_update {
|
||||
if let Some(udp_socket) = udp_socket {
|
||||
if let Err(e) = service.libp2p.swarm.peer_manager().discovery_mut().update_enr_udp_socket(udp_socket) {
|
||||
warn!(service.log, "Failed to update ENR"; "error" => e);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
NetworkMessage::ValidationResult {
|
||||
propagation_source,
|
||||
message_id,
|
||||
|
||||
Reference in New Issue
Block a user