From 80bfbc6d3b05ce4280b8ca7a919e29e0de9580fe Mon Sep 17 00:00:00 2001 From: Ihor Horobets Date: Fri, 10 Jul 2026 23:17:41 +0300 Subject: [PATCH 1/2] feat: optional TLS for P2P connections (#1420) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encrypt peer traffic with rustls for privacy against passive observers. TLS is opt-in via p2p_config.tls_enabled (default false) so the existing plaintext network remains compatible. When enabled, peers use self-signed certificates (auto-generated under the node data dir if paths are not set) and clients intentionally skip certificate authentication — encryption for privacy only, matching the issue intent. Wire a plain/TLS Stream abstraction through handshake, codec, and conn I/O, and add an integration test for TLS peer handshake + ping/pong. --- Cargo.lock | 78 +++++++ config/src/comments.rs | 11 + p2p/Cargo.toml | 3 + p2p/src/codec.rs | 10 +- p2p/src/conn.rs | 10 +- p2p/src/handshake.rs | 9 +- p2p/src/lib.rs | 1 + p2p/src/peer.rs | 9 +- p2p/src/serv.rs | 40 +++- p2p/src/stream.rs | 400 ++++++++++++++++++++++++++++++++++++ p2p/src/types.rs | 16 ++ p2p/tests/peer_handshake.rs | 52 ++++- 12 files changed, 617 insertions(+), 22 deletions(-) create mode 100644 p2p/src/stream.rs diff --git a/Cargo.lock b/Cargo.lock index a9dbe7c413..e7e29b1a6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,6 +177,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.8.3" @@ -599,6 +605,12 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "derive_more" version = "2.1.1" @@ -1185,6 +1197,9 @@ dependencies = [ "lru-cache", "num 0.2.1", "rand 0.6.5", + "rcgen", + "rustls", + "rustls-pemfile", "serde", "serde_derive", "tempfile", @@ -2009,6 +2024,12 @@ dependencies = [ "num-traits 0.2.19", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-integer" version = "0.1.46" @@ -2186,6 +2207,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2270,6 +2301,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2508,6 +2545,19 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -3077,6 +3127,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + [[package]] name = "tinystr" version = "0.8.3" @@ -3561,6 +3630,15 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.3" diff --git a/config/src/comments.rs b/config/src/comments.rs index 0e9ac707da..fbf9d47df2 100644 --- a/config/src/comments.rs +++ b/config/src/comments.rs @@ -324,6 +324,17 @@ fn comments() -> HashMap { # A preferred dandelion_peer, mainly used for testing dandelion # dandelion_peer = \"10.0.0.1:13144\" +# Encrypt P2P connections with TLS (privacy against passive observers). +# Both peers must enable TLS to connect. Certificates are self-signed by default +# (authentication is not the goal — encryption for privacy is). Default: false. +#tls_enabled = false + +# Optional PEM certificate / private key for P2P TLS. +# If omitted while tls_enabled = true, a self-signed cert is auto-generated +# under the node data directory (p2p_tls/). +#tls_certificate_file = \"/path/to/p2p.crt\" +#tls_certificate_key = \"/path/to/p2p.key\" + ######################################### ### MEMPOOL CONFIGURATION ### ######################################### diff --git a/p2p/Cargo.toml b/p2p/Cargo.toml index bb1da12c26..c2923688d7 100644 --- a/p2p/Cargo.toml +++ b/p2p/Cargo.toml @@ -21,6 +21,9 @@ tempfile = "3.1" log = "0.4" chrono = { version = "0.4.11", features = ["serde"] } bytes = "0.5" +rustls = { version = "0.23.41", default-features = false, features = ["logging", "ring", "std", "tls12"] } +rustls-pemfile = "1.0" +rcgen = "0.13" grin_core = { path = "../core", version = "5.5.1-alpha.0" } grin_store = { path = "../store", version = "5.5.1-alpha.0" } diff --git a/p2p/src/codec.rs b/p2p/src/codec.rs index 7891e6f852..112e7d59bb 100644 --- a/p2p/src/codec.rs +++ b/p2p/src/codec.rs @@ -28,12 +28,12 @@ use crate::{ core::core::block::{BlockHeader, UntrustedBlockHeader}, msg::HeadersData, }; +use crate::stream::Stream; use bytes::{Buf, BufMut, Bytes, BytesMut}; use core::ser::Reader; use std::cmp::min; use std::io::Read; use std::mem; -use std::net::TcpStream; use std::sync::Arc; use std::time::{Duration, Instant}; use MsgHeaderWrapper::*; @@ -65,14 +65,14 @@ impl State { pub struct Codec { pub version: ProtocolVersion, - stream: TcpStream, + stream: Stream, buffer: BytesMut, state: State, bytes_read: usize, } impl Codec { - pub fn new(version: ProtocolVersion, stream: TcpStream) -> Self { + pub fn new(version: ProtocolVersion, stream: Stream) -> Self { Self { version, stream, @@ -82,8 +82,8 @@ impl Codec { } } - /// Destroy the codec and return the reader - pub fn stream(self) -> TcpStream { + /// Destroy the codec and return the underlying stream + pub fn stream(self) -> Stream { self.stream } diff --git a/p2p/src/conn.rs b/p2p/src/conn.rs index 578dd962b2..ac2dbedfa2 100644 --- a/p2p/src/conn.rs +++ b/p2p/src/conn.rs @@ -23,11 +23,12 @@ use crate::codec::{Codec, BODY_IO_TIMEOUT}; use crate::core::ser::ProtocolVersion; use crate::msg::{write_message, Consumed, Message, Msg}; +use crate::stream::Stream; use crate::types::Error; use crate::util::{RateCounter, RwLock}; use std::fs::File; use std::io::{self, Write}; -use std::net::{Shutdown, TcpStream}; +use std::net::Shutdown; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::RecvTimeoutError; use std::sync::{mpsc, Arc}; @@ -172,7 +173,7 @@ impl Tracker { /// the current thread, instead just returns a future and the Connection /// itself. pub fn listen( - stream: TcpStream, + stream: Stream, version: ProtocolVersion, tracker: Arc, handler: H, @@ -209,7 +210,7 @@ where } fn poll( - conn: TcpStream, + conn: Stream, conn_handle: ConnHandle, version: ProtocolVersion, handler: H, @@ -220,7 +221,8 @@ fn poll( where H: MessageHandler, { - // Split out tcp stream out into separate reader/writer halves. + // Split out stream into separate reader/writer halves. + // Plain TCP uses OS-level clone; TLS shares a mutex-backed session. let reader = conn.try_clone().expect("clone conn for reader failed"); let mut writer = conn.try_clone().expect("clone conn for writer failed"); let reader_stopped = stopped.clone(); diff --git a/p2p/src/handshake.rs b/p2p/src/handshake.rs index eb661381c2..26f4b1dba0 100644 --- a/p2p/src/handshake.rs +++ b/p2p/src/handshake.rs @@ -18,13 +18,14 @@ use crate::core::pow::Difficulty; use crate::core::ser::ProtocolVersion; use crate::msg::{read_message, user_agent, write_message, Hand, Msg, Shake, Type}; use crate::peer::Peer; +use crate::stream::Stream; use crate::types::{ Capabilities, Direction, Error, NetAdapter, P2PConfig, PeerAddr, PeerInfo, PeerLiveInfo, }; use crate::util::RwLock; use rand::{thread_rng, Rng}; use std::collections::VecDeque; -use std::net::{SocketAddr, TcpStream}; +use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; @@ -99,7 +100,7 @@ impl Handshake { capabilities: Capabilities, total_difficulty: Difficulty, self_addr: PeerAddr, - conn: &mut TcpStream, + conn: &mut Stream, ) -> Result { // Set explicit timeouts on the tcp stream for hand/shake messages. // Once the peer is up and running we will set new values for these. @@ -170,7 +171,7 @@ impl Handshake { &self, capab: Capabilities, total_difficulty: Difficulty, - conn: &mut TcpStream, + conn: &mut Stream, adapter: &Arc, ) -> Result { // Set explicit timeouts on the tcp stream for hand/shake messages. @@ -258,7 +259,7 @@ impl Handshake { } /// Resolve the correct peer_addr based on the connection and the advertised port. -fn resolve_peer_addr(advertised: PeerAddr, conn: &TcpStream) -> PeerAddr { +fn resolve_peer_addr(advertised: PeerAddr, conn: &Stream) -> PeerAddr { let port = advertised.0.port(); if let Ok(addr) = conn.peer_addr() { PeerAddr(SocketAddr::new(addr.ip(), port)) diff --git a/p2p/src/lib.rs b/p2p/src/lib.rs index e0d8bc10cf..1bb68ebad9 100644 --- a/p2p/src/lib.rs +++ b/p2p/src/lib.rs @@ -45,6 +45,7 @@ mod peers; mod protocol; mod serv; pub mod store; +mod stream; pub mod types; pub use crate::conn::SEND_CHANNEL_CAP; diff --git a/p2p/src/peer.rs b/p2p/src/peer.rs index b455fd38c4..22bacf3282 100644 --- a/p2p/src/peer.rs +++ b/p2p/src/peer.rs @@ -16,7 +16,7 @@ use crate::util::{Mutex, RwLock}; use lru_cache::LruCache; use std::fmt; use std::fs::File; -use std::net::{Shutdown, TcpStream}; +use std::net::Shutdown; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -34,6 +34,7 @@ use crate::msg::{ self, BanReason, GetPeerAddrs, Locator, Msg, Ping, SegmentRequest, TxHashSetRequest, Type, }; use crate::protocol::Protocol; +use crate::stream::Stream; use crate::types::{ Capabilities, ChainAdapter, Error, HeaderSegmentAcceptance, NetAdapter, P2PConfig, PeerAddr, PeerInfo, ReasonForBan, TxHashSetRead, @@ -76,7 +77,7 @@ impl fmt::Debug for Peer { impl Peer { // Only accept and connect can be externally used to build a peer - fn new(info: PeerInfo, conn: TcpStream, adapter: Arc) -> std::io::Result { + fn new(info: PeerInfo, conn: Stream, adapter: Arc) -> std::io::Result { let state = Arc::new(RwLock::new(State::Connected)); let state_sync_requested = Arc::new(AtomicBool::new(false)); let tracking_adapter = TrackingAdapter::new(adapter); @@ -101,7 +102,7 @@ impl Peer { } pub fn accept( - mut conn: TcpStream, + mut conn: Stream, capab: Capabilities, total_difficulty: Difficulty, hs: &Handshake, @@ -126,7 +127,7 @@ impl Peer { } pub fn connect( - mut conn: TcpStream, + mut conn: Stream, capab: Capabilities, total_difficulty: Difficulty, self_addr: PeerAddr, diff --git a/p2p/src/serv.rs b/p2p/src/serv.rs index 57ac9631c5..2000d9fe32 100644 --- a/p2p/src/serv.rs +++ b/p2p/src/serv.rs @@ -15,7 +15,7 @@ use std::fs::File; use std::io; use std::net::{IpAddr, Shutdown, SocketAddr, SocketAddrV4, TcpListener, TcpStream}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::thread; use std::time::Duration; @@ -31,6 +31,7 @@ use crate::handshake::Handshake; use crate::peer::Peer; use crate::peers::Peers; use crate::store::PeerStore; +use crate::stream::{default_tls_dir, Stream, TlsContext}; use crate::types::{ Capabilities, ChainAdapter, Error, HeaderSegmentAcceptance, NetAdapter, P2PConfig, PeerAddr, PeerInfo, ReasonForBan, TxHashSetRead, @@ -47,9 +48,10 @@ pub struct Server { handshake: Arc, pub peers: Arc, stop_state: Arc, + /// Optional TLS context when `p2p_config.tls_enabled` is set. + tls: Option, } -// TODO TLS impl Server { /// Creates a new idle p2p server with no peers pub fn new( @@ -60,12 +62,24 @@ impl Server { genesis: Hash, stop_state: Arc, ) -> Result { + let tls = if config.tls_enabled { + let cert = config.tls_certificate_file.as_ref().map(Path::new); + let key = config.tls_certificate_key.as_ref().map(Path::new); + let auto = Some(default_tls_dir(db_root)); + Some(TlsContext::new(cert, key, auto.as_deref())?) + } else { + None + }; + if tls.is_some() { + info!("P2P TLS enabled (privacy-only, self-signed certificates accepted)"); + } Ok(Server { config: config.clone(), capabilities, handshake: Arc::new(Handshake::new(genesis, config.clone())), peers: Arc::new(Peers::new(PeerStore::new(db_root)?, adapter, config)), stop_state, + tls, }) } @@ -196,10 +210,11 @@ impl Server { addr ); match TcpStream::connect_timeout(&addr.0, Duration::from_secs(10)) { - Ok(stream) => { + Ok(tcp) => { let addr = SocketAddr::new(self.config.host, self.config.port); let total_diff = self.peers.total_difficulty()?; + let stream = self.wrap_outbound(tcp)?; let peer = Peer::connect( stream, self.capabilities, @@ -228,12 +243,13 @@ impl Server { } } - fn handle_new_peer(&self, stream: TcpStream) -> Result<(), Error> { + fn handle_new_peer(&self, tcp: TcpStream) -> Result<(), Error> { if self.stop_state.is_stopped() { return Err(Error::ConnectionClose); } let total_diff = self.peers.total_difficulty()?; + let stream = self.wrap_inbound(tcp)?; // accept the peer and add it to the server map let peer = Peer::accept( stream, @@ -246,6 +262,22 @@ impl Server { Ok(()) } + /// Wrap an accepted TCP connection, optionally upgrading to TLS. + fn wrap_inbound(&self, tcp: TcpStream) -> Result { + match &self.tls { + Some(tls) => tls.accept(tcp).map_err(Error::Connection), + None => Ok(Stream::plain(tcp)), + } + } + + /// Wrap an outbound TCP connection, optionally upgrading to TLS. + fn wrap_outbound(&self, tcp: TcpStream) -> Result { + match &self.tls { + Some(tls) => tls.connect(tcp).map_err(Error::Connection), + None => Ok(Stream::plain(tcp)), + } + } + /// Checks whether there's any reason we don't want to accept an incoming peer /// connection. There can be a few of them: /// 1. Accepting the peer connection would exceed the configured maximum allowed diff --git a/p2p/src/stream.rs b/p2p/src/stream.rs new file mode 100644 index 0000000000..83d38c28e4 --- /dev/null +++ b/p2p/src/stream.rs @@ -0,0 +1,400 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Optional TLS wrapping for P2P connections. +//! +//! When enabled, Grin P2P traffic is encrypted with TLS for privacy against +//! passive observers (packet inspection). Authentication is intentionally +//! *not* performed: peers use self-signed certificates and clients accept any +//! certificate. This matches the goal of issue #1420 (privacy, not PKI). +//! +//! Both peers must enable TLS to connect. Plaintext remains the default for +//! compatibility with the existing network. + +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; +use rustls::pki_types::{ + CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, UnixTime, +}; +use rustls::{ + ClientConfig, ClientConnection, DigitallySignedStruct, Error as TlsError, ServerConfig, + ServerConnection, SignatureScheme, StreamOwned, +}; +use std::fs::{self, File}; +use std::io::{self, BufReader, Read, Write}; +use std::net::{Shutdown, SocketAddr, TcpStream}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +/// Ensure the rustls crypto provider is installed once. +fn ensure_crypto_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); +} + +/// TLS context shared by the P2P server (server + client configs). +#[derive(Clone)] +pub struct TlsContext { + server_config: Arc, + client_config: Arc, +} + +impl TlsContext { + /// Build a TLS context from PEM certificate / key paths. + /// If the files do not exist and `auto_dir` is set, generate a self-signed + /// certificate there. + pub fn new( + cert_path: Option<&Path>, + key_path: Option<&Path>, + auto_dir: Option<&Path>, + ) -> io::Result { + ensure_crypto_provider(); + + let (cert_path, key_path) = match (cert_path, key_path, auto_dir) { + (Some(c), Some(k), _) => (c.to_path_buf(), k.to_path_buf()), + (_, _, Some(dir)) => { + fs::create_dir_all(dir)?; + let cert = dir.join("cert.pem"); + let key = dir.join("key.pem"); + if !cert.exists() || !key.exists() { + generate_self_signed(&cert, &key)?; + info!( + "Generated self-signed P2P TLS certificate at {}", + cert.display() + ); + } + (cert, key) + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "tls_enabled requires certificate paths or a data directory for auto-generation", + )); + } + }; + + let certs = load_certs(&cert_path)?; + let key = load_private_key(&key_path)?; + + let server_config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + // Privacy-only: accept any peer certificate (self-signed OK). + let client_config = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoCertificateVerification)) + .with_no_client_auth(); + + Ok(TlsContext { + server_config: Arc::new(server_config), + client_config: Arc::new(client_config), + }) + } + + /// Perform a TLS server handshake over an accepted TCP stream. + pub fn accept(&self, tcp: TcpStream) -> io::Result { + let conn = ServerConnection::new(Arc::clone(&self.server_config)) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let mut stream = StreamOwned::new(conn, tcp); + // Complete handshake by forcing a write/read cycle. + handshake_server(&mut stream)?; + Ok(Stream::tls_server(stream)) + } + + /// Perform a TLS client handshake over an outbound TCP stream. + pub fn connect(&self, tcp: TcpStream) -> io::Result { + // Server name is unused for verification (privacy-only TLS). + let name = ServerName::try_from("grin-p2p") + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid server name"))? + .to_owned(); + let conn = ClientConnection::new(Arc::clone(&self.client_config), name) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let mut stream = StreamOwned::new(conn, tcp); + handshake_client(&mut stream)?; + Ok(Stream::tls_client(stream)) + } +} + +fn handshake_server(stream: &mut StreamOwned) -> io::Result<()> { + // Drive the handshake until complete. + while stream.conn.is_handshaking() { + if stream.conn.wants_read() { + stream.conn.read_tls(&mut stream.sock)?; + stream + .conn + .process_new_packets() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + } + if stream.conn.wants_write() { + stream.conn.write_tls(&mut stream.sock)?; + } + } + Ok(()) +} + +fn handshake_client(stream: &mut StreamOwned) -> io::Result<()> { + while stream.conn.is_handshaking() { + if stream.conn.wants_write() { + stream.conn.write_tls(&mut stream.sock)?; + } + if stream.conn.wants_read() { + stream.conn.read_tls(&mut stream.sock)?; + stream + .conn + .process_new_packets() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + } + } + Ok(()) +} + +/// Unified plain / TLS peer stream used throughout the p2p crate. +pub struct Stream { + inner: StreamInner, +} + +enum StreamInner { + Plain(TcpStream), + Tls(Arc>), +} + +enum TlsInner { + Client(StreamOwned), + Server(StreamOwned), +} + +impl Stream { + pub fn plain(tcp: TcpStream) -> Stream { + Stream { + inner: StreamInner::Plain(tcp), + } + } + + fn tls_client(s: StreamOwned) -> Stream { + Stream { + inner: StreamInner::Tls(Arc::new(Mutex::new(TlsInner::Client(s)))), + } + } + + fn tls_server(s: StreamOwned) -> Stream { + Stream { + inner: StreamInner::Tls(Arc::new(Mutex::new(TlsInner::Server(s)))), + } + } + + pub fn try_clone(&self) -> io::Result { + match &self.inner { + StreamInner::Plain(s) => Ok(Stream::plain(s.try_clone()?)), + StreamInner::Tls(t) => Ok(Stream { + inner: StreamInner::Tls(Arc::clone(t)), + }), + } + } + + pub fn set_read_timeout(&self, dur: Option) -> io::Result<()> { + match &self.inner { + StreamInner::Plain(s) => s.set_read_timeout(dur), + StreamInner::Tls(t) => { + let mut g = t.lock().map_err(|_| { + io::Error::new(io::ErrorKind::Other, "tls stream mutex poisoned") + })?; + match &mut *g { + TlsInner::Client(s) => s.sock.set_read_timeout(dur), + TlsInner::Server(s) => s.sock.set_read_timeout(dur), + } + } + } + } + + pub fn set_write_timeout(&self, dur: Option) -> io::Result<()> { + match &self.inner { + StreamInner::Plain(s) => s.set_write_timeout(dur), + StreamInner::Tls(t) => { + let mut g = t.lock().map_err(|_| { + io::Error::new(io::ErrorKind::Other, "tls stream mutex poisoned") + })?; + match &mut *g { + TlsInner::Client(s) => s.sock.set_write_timeout(dur), + TlsInner::Server(s) => s.sock.set_write_timeout(dur), + } + } + } + } + + pub fn peer_addr(&self) -> io::Result { + match &self.inner { + StreamInner::Plain(s) => s.peer_addr(), + StreamInner::Tls(t) => { + let g = t.lock().map_err(|_| { + io::Error::new(io::ErrorKind::Other, "tls stream mutex poisoned") + })?; + match &*g { + TlsInner::Client(s) => s.sock.peer_addr(), + TlsInner::Server(s) => s.sock.peer_addr(), + } + } + } + } + + pub fn shutdown(&self, how: Shutdown) -> io::Result<()> { + match &self.inner { + StreamInner::Plain(s) => s.shutdown(how), + StreamInner::Tls(t) => { + let g = t.lock().map_err(|_| { + io::Error::new(io::ErrorKind::Other, "tls stream mutex poisoned") + })?; + match &*g { + TlsInner::Client(s) => s.sock.shutdown(how), + TlsInner::Server(s) => s.sock.shutdown(how), + } + } + } + } + +} + +impl Read for Stream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match &mut self.inner { + StreamInner::Plain(s) => s.read(buf), + StreamInner::Tls(t) => { + let mut g = t.lock().map_err(|_| { + io::Error::new(io::ErrorKind::Other, "tls stream mutex poisoned") + })?; + match &mut *g { + TlsInner::Client(s) => s.read(buf), + TlsInner::Server(s) => s.read(buf), + } + } + } + } +} + +impl Write for Stream { + fn write(&mut self, buf: &[u8]) -> io::Result { + match &mut self.inner { + StreamInner::Plain(s) => s.write(buf), + StreamInner::Tls(t) => { + let mut g = t.lock().map_err(|_| { + io::Error::new(io::ErrorKind::Other, "tls stream mutex poisoned") + })?; + match &mut *g { + TlsInner::Client(s) => s.write(buf), + TlsInner::Server(s) => s.write(buf), + } + } + } + } + + fn flush(&mut self) -> io::Result<()> { + match &mut self.inner { + StreamInner::Plain(s) => s.flush(), + StreamInner::Tls(t) => { + let mut g = t.lock().map_err(|_| { + io::Error::new(io::ErrorKind::Other, "tls stream mutex poisoned") + })?; + match &mut *g { + TlsInner::Client(s) => s.flush(), + TlsInner::Server(s) => s.flush(), + } + } + } + } +} + +/// Verifier that accepts any certificate (privacy-only P2P TLS). +#[derive(Debug)] +struct NoCertificateVerification; + +impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + rustls::crypto::ring::default_provider() + .signature_verification_algorithms + .supported_schemes() + } +} + +fn load_certs(path: &Path) -> io::Result>> { + let file = File::open(path)?; + let mut reader = BufReader::new(file); + let certs = rustls_pemfile::certs(&mut reader) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + Ok(certs.into_iter().map(CertificateDer::from).collect()) +} + +fn load_private_key(path: &Path) -> io::Result> { + let file = File::open(path)?; + let mut reader = BufReader::new(file); + let keys = rustls_pemfile::pkcs8_private_keys(&mut reader) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + if keys.len() != 1 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected a single PKCS#8 private key", + )); + } + Ok(PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(keys[0].clone()))) +} + +fn generate_self_signed(cert_path: &Path, key_path: &Path) -> io::Result<()> { + let key_pair = rcgen::KeyPair::generate() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + let mut params = rcgen::CertificateParams::new(vec!["grin-p2p".to_string()]) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + params + .distinguished_name + .push(rcgen::DnType::CommonName, "grin-p2p"); + let cert = params + .self_signed(&key_pair) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + + fs::write(cert_path, cert.pem())?; + fs::write(key_path, key_pair.serialize_pem())?; + Ok(()) +} + +/// Default subdirectory under the node data root for auto-generated certs. +pub fn default_tls_dir(db_root: &str) -> PathBuf { + Path::new(db_root).join("p2p_tls") +} diff --git a/p2p/src/types.rs b/p2p/src/types.rs index 03acd8ecdd..e9eed464a8 100644 --- a/p2p/src/types.rs +++ b/p2p/src/types.rs @@ -430,6 +430,19 @@ pub struct P2PConfig { pub peer_listener_buffer_count: Option, pub dandelion_peer: Option, + + /// Encrypt P2P traffic with TLS (privacy against passive observers). + /// Both peers must enable TLS to connect. Default: false (plaintext). + #[serde(default)] + pub tls_enabled: bool, + + /// Path to PEM certificate file for P2P TLS (server side). + /// If unset while `tls_enabled`, a self-signed cert is auto-generated + /// under the node data directory (`p2p_tls/`). + pub tls_certificate_file: Option, + + /// Path to PEM private key for the P2P TLS certificate. + pub tls_certificate_key: Option, } /// Default address for peer-to-peer connections. @@ -450,6 +463,9 @@ impl Default for P2PConfig { peer_min_preferred_outbound_count: None, peer_listener_buffer_count: None, dandelion_peer: None, + tls_enabled: false, + tls_certificate_file: None, + tls_certificate_key: None, } } } diff --git a/p2p/tests/peer_handshake.rs b/p2p/tests/peer_handshake.rs index 860bc87340..9a90fbd8d7 100644 --- a/p2p/tests/peer_handshake.rs +++ b/p2p/tests/peer_handshake.rs @@ -40,7 +40,8 @@ fn open_port() -> u16 { // Setup test with AutomatedTesting chain_type; fn test_setup() { // Set "global" chain type here as we spawn peer threads for read/write. - global::init_global_chain_type(global::ChainTypes::AutomatedTesting); + // Use set (override) so multiple tests in this binary can share init. + global::set_global_chain_type(global::ChainTypes::AutomatedTesting); util::init_test_logger(); } @@ -53,6 +54,16 @@ fn p2p_server( peers_allow: Vec, peers_deny: Vec, port: Option, +) -> (SocketAddr, Arc) { + p2p_server_with_tls(dir, peers_allow, peers_deny, port, false) +} + +fn p2p_server_with_tls( + dir: &str, + peers_allow: Vec, + peers_deny: Vec, + port: Option, + tls_enabled: bool, ) -> (SocketAddr, Arc) { let p2p_config = p2p::P2PConfig { host: "127.0.0.1".parse().unwrap(), @@ -67,6 +78,7 @@ fn p2p_server( } else { Some(PeerAddrs { peers: peers_deny }) }, + tls_enabled, ..p2p::P2PConfig::default() }; let net_adapter = Arc::new(p2p::DummyAdapter {}); @@ -188,3 +200,41 @@ fn peer_handshake() { assert_eq!(server.peers.iter().connected().count(), 0); } } + +#[test] +fn peer_handshake_tls() { + test_setup(); + let test_dir = "target/peer_handshake_tls"; + clean_output_dir(test_dir); + + // Both peers with TLS enabled should complete handshake and exchange ping/pong. + { + let dir_a = format!("{}/a", test_dir); + let dir_b = format!("{}/b", test_dir); + let (_, server) = p2p_server_with_tls(&dir_a, vec![], vec![], None, true); + let (peer_addr, _) = p2p_server_with_tls(&dir_b, vec![], vec![], None, true); + + let peer = server.connect(PeerAddr(peer_addr)).unwrap(); + + let git_hash = + built_info::GIT_COMMIT_HASH_SHORT.map_or_else(|| "".to_owned(), |v| ".".to_owned() + v); + assert!(peer + .info + .user_agent + .ends_with(format!("{}{}", env!("CARGO_PKG_VERSION"), git_hash).as_str())); + + thread::sleep(time::Duration::from_secs(1)); + + peer.send_ping(Difficulty::min_dma(), 0).unwrap(); + thread::sleep(time::Duration::from_secs(1)); + + let server_peer = server + .peers + .get_connected_peer(PeerAddr(peer_addr)) + .unwrap(); + assert_eq!(server_peer.info.total_difficulty(), Difficulty::min_dma()); + assert!(server.peers.iter().connected().count() > 0); + } + + clean_output_dir(test_dir); +} From 0efc883919a610c38bf121c862a1d1e211e3cdc9 Mon Sep 17 00:00:00 2001 From: iho Date: Sat, 11 Jul 2026 01:23:19 +0300 Subject: [PATCH 2/2] p2p: add TLS capability and tls_required peer selection Advertise Capabilities::TLS when tls_enabled so peers can discover encrypted endpoints. Add tls_required to filter outbound attempts and peer-list requests to TLS-capable peers (seeds/preferred/allow still used for bootstrap). --- config/src/comments.rs | 7 +++++ p2p/src/types.rs | 39 +++++++++++++++++++++++++++ p2p/tests/capabilities.rs | 54 +++++++++++++++++++++++++++++++++++++ p2p/tests/peer_handshake.rs | 12 ++++++++- servers/src/grin/seed.rs | 52 +++++++++++++++++++++++------------ servers/src/grin/server.rs | 11 +++++++- 6 files changed, 156 insertions(+), 19 deletions(-) diff --git a/config/src/comments.rs b/config/src/comments.rs index fbf9d47df2..d7dc95322a 100644 --- a/config/src/comments.rs +++ b/config/src/comments.rs @@ -327,8 +327,15 @@ fn comments() -> HashMap { # Encrypt P2P connections with TLS (privacy against passive observers). # Both peers must enable TLS to connect. Certificates are self-signed by default # (authentication is not the goal — encryption for privacy is). Default: false. +# When enabled, this node advertises the TLS capability so others can discover it. #tls_enabled = false +# Only connect outbound to peers known to advertise the TLS capability +# (and request TLS-capable peers in peer-list queries). Requires tls_enabled. +# Seeds, preferred, and allow-listed addresses are still tried for bootstrap. +# Default: false. +#tls_required = false + # Optional PEM certificate / private key for P2P TLS. # If omitted while tls_enabled = true, a self-signed cert is auto-generated # under the node data directory (p2p_tls/). diff --git a/p2p/src/types.rs b/p2p/src/types.rs index e9eed464a8..ada3f6c890 100644 --- a/p2p/src/types.rs +++ b/p2p/src/types.rs @@ -433,9 +433,17 @@ pub struct P2PConfig { /// Encrypt P2P traffic with TLS (privacy against passive observers). /// Both peers must enable TLS to connect. Default: false (plaintext). + /// When true, this node advertises the `TLS` capability. #[serde(default)] pub tls_enabled: bool, + /// When true (and `tls_enabled`), only attempt outbound connections to peers + /// known to advertise the `TLS` capability, and request TLS-capable peers + /// in peer-list queries. Seeds, preferred, and allow-listed addresses still + /// get connection attempts for bootstrap. Default: false. + #[serde(default)] + pub tls_required: bool, + /// Path to PEM certificate file for P2P TLS (server side). /// If unset while `tls_enabled`, a self-signed cert is auto-generated /// under the node data directory (`p2p_tls/`). @@ -464,6 +472,7 @@ impl Default for P2PConfig { peer_listener_buffer_count: None, dandelion_peer: None, tls_enabled: false, + tls_required: false, tls_certificate_file: None, tls_certificate_key: None, } @@ -501,6 +510,32 @@ impl P2PConfig { self.peer_listener_buffer_count .unwrap_or_else(|| PEER_LISTENER_BUFFER_COUNT) } + + /// Whether outbound peer selection should require the `TLS` capability. + /// Only meaningful when TLS transport is enabled. + pub fn tls_peers_required(&self) -> bool { + self.tls_enabled && self.tls_required + } + + /// Capabilities filter used when asking peers for more peer addresses. + pub fn peer_list_request_capabilities(&self) -> Capabilities { + if self.tls_peers_required() { + Capabilities::PEER_LIST | Capabilities::TLS + } else { + Capabilities::PEER_LIST + } + } + + /// Whether a peer with the given stored capabilities is eligible for an + /// automatic outbound connection attempt under the current TLS policy. + /// Peers with unknown capabilities fail the check when TLS is required. + pub fn accepts_outbound_peer_capabilities(&self, caps: Capabilities) -> bool { + if self.tls_peers_required() { + caps.contains(Capabilities::TLS) + } else { + true + } + } } /// Type of seeding the server will use to find other peers on the network. @@ -545,6 +580,10 @@ bitflags! { const PIBD_HIST_1 = 0b0100_0000; /// Can provide deterministic historical header segments. const PIHD_HIST = 0b1000_0000; + /// Supports TLS-encrypted P2P transport (privacy against passive observers). + /// Advertised when `p2p_config.tls_enabled` is true. Not part of default + /// capabilities — TLS is opt-in for network compatibility. + const TLS = 0b1_0000_0000; } } diff --git a/p2p/tests/capabilities.rs b/p2p/tests/capabilities.rs index c6a8bf74c1..d3bbda6dc9 100644 --- a/p2p/tests/capabilities.rs +++ b/p2p/tests/capabilities.rs @@ -55,4 +55,58 @@ fn default_capabilities() { | Capabilities::PIBD_HIST_1 | Capabilities::PIHD_HIST ); + + // TLS is opt-in and not part of default capabilities. + assert_eq!(false, x.contains(Capabilities::TLS)); +} + +#[test] +fn tls_capability_bit() { + let tls = Capabilities::TLS; + assert!(tls.contains(Capabilities::TLS)); + assert!(tls.contains(Capabilities::UNKNOWN)); + assert_eq!(false, tls.contains(Capabilities::PEER_LIST)); + + let combined = Capabilities::PEER_LIST | Capabilities::TLS; + assert!(combined.contains(Capabilities::PEER_LIST)); + assert!(combined.contains(Capabilities::TLS)); + assert!(combined.contains(Capabilities::PEER_LIST | Capabilities::TLS)); + assert_eq!(false, Capabilities::PEER_LIST.contains(combined)); +} + +#[test] +fn tls_peer_selection_helpers() { + use grin_p2p::P2PConfig; + + let mut cfg = P2PConfig::default(); + assert_eq!(false, cfg.tls_peers_required()); + assert_eq!( + cfg.peer_list_request_capabilities(), + Capabilities::PEER_LIST + ); + assert!(cfg.accepts_outbound_peer_capabilities(Capabilities::UNKNOWN)); + assert!(cfg.accepts_outbound_peer_capabilities(Capabilities::PEER_LIST)); + + // tls_required alone does nothing without tls_enabled. + cfg.tls_required = true; + assert_eq!(false, cfg.tls_peers_required()); + + cfg.tls_enabled = true; + assert!(cfg.tls_peers_required()); + assert_eq!( + cfg.peer_list_request_capabilities(), + Capabilities::PEER_LIST | Capabilities::TLS + ); + assert_eq!( + false, + cfg.accepts_outbound_peer_capabilities(Capabilities::UNKNOWN) + ); + assert_eq!( + false, + cfg.accepts_outbound_peer_capabilities(Capabilities::PEER_LIST) + ); + assert!(cfg.accepts_outbound_peer_capabilities(Capabilities::TLS)); + assert!(cfg.accepts_outbound_peer_capabilities( + Capabilities::PEER_LIST | Capabilities::TLS + )); } diff --git a/p2p/tests/peer_handshake.rs b/p2p/tests/peer_handshake.rs index 9a90fbd8d7..334b0abecd 100644 --- a/p2p/tests/peer_handshake.rs +++ b/p2p/tests/peer_handshake.rs @@ -81,11 +81,17 @@ fn p2p_server_with_tls( tls_enabled, ..p2p::P2PConfig::default() }; + // Mirror server.rs: advertise TLS capability when transport encryption is on. + let capabilities = if tls_enabled { + p2p::Capabilities::TLS + } else { + p2p::Capabilities::UNKNOWN + }; let net_adapter = Arc::new(p2p::DummyAdapter {}); let server = Arc::new( p2p::Server::new( dir, - p2p::Capabilities::UNKNOWN, + capabilities, p2p_config.clone(), net_adapter.clone(), Hash::from_vec(&vec![]), @@ -223,6 +229,9 @@ fn peer_handshake_tls() { .user_agent .ends_with(format!("{}{}", env!("CARGO_PKG_VERSION"), git_hash).as_str())); + // Remote peer advertised TLS capability during handshake. + assert!(peer.info.capabilities.contains(p2p::Capabilities::TLS)); + thread::sleep(time::Duration::from_secs(1)); peer.send_ping(Difficulty::min_dma(), 0).unwrap(); @@ -233,6 +242,7 @@ fn peer_handshake_tls() { .get_connected_peer(PeerAddr(peer_addr)) .unwrap(); assert_eq!(server_peer.info.total_difficulty(), Difficulty::min_dma()); + assert!(server_peer.info.capabilities.contains(p2p::Capabilities::TLS)); assert!(server.peers.iter().connected().count() > 0); } diff --git a/servers/src/grin/seed.rs b/servers/src/grin/seed.rs index bfe77c66ef..ed883da93d 100644 --- a/servers/src/grin/seed.rs +++ b/servers/src/grin/seed.rs @@ -217,9 +217,11 @@ fn monitor_peers( let default_peers = PeerAddrs::default(); let enough_outbound = peers.enough_outbound_peers(); + let peer_list_caps = config.peer_list_request_capabilities(); + if !enough_outbound { // loop over connected peers that can provide peer lists - // ask them for their list of peers + // ask them for their list of peers (optionally TLS-capable only) let mut connected_peers: Vec = vec![]; for p in peers .iter() @@ -227,16 +229,17 @@ fn monitor_peers( .connected() { trace!( - "monitor_peers: {}:{} ask {} for more peers", + "monitor_peers: {}:{} ask {} for more peers ({:?})", config.host, config.port, p.info.addr, + peer_list_caps, ); - let _ = p.send_peer_request(p2p::Capabilities::PEER_LIST); + let _ = p.send_peer_request(peer_list_caps); connected_peers.push(p.info.addr) } - // Attempt to connect to any preferred peers. + // Attempt to connect to any preferred peers (bootstrap; ignore TLS filter). let peers_preferred = config.peers_preferred.as_ref().unwrap_or(&default_peers); for p in peers_preferred.peers.iter() { if !connected_peers.is_empty() { @@ -250,6 +253,7 @@ fn monitor_peers( } let peers_deny = config.peers_deny.as_ref().unwrap_or(&default_peers); + let tls_required = config.tls_peers_required(); // find some peers from our db // and queue them up for a connection attempt @@ -264,6 +268,7 @@ fn monitor_peers( .filter(|p| { !peers_deny.matches_addr(&p.addr) && peers.get_connected_peer(p.addr).is_none() + && config.accepts_outbound_peer_capabilities(p.capabilities) && (!enough_outbound || Utc::now().timestamp() - p.last_attempt >= max_attempt_delay) }) @@ -272,10 +277,15 @@ fn monitor_peers( new_peers.push(hp.addr); } // always check min 32 (max 96, if there are no healthy) random unknown peers received from peer list request. - let req_unk_count = cmp::max( - max_peer_attempts / 2 - new_peers.len() + max_peer_attempts / 4, - max_peer_attempts / 4, - ); + // When TLS is required, skip unknown peers — we only know TLS support after a successful handshake. + let req_unk_count = if tls_required { + 0 + } else { + cmp::max( + max_peer_attempts / 2 - new_peers.len() + max_peer_attempts / 4, + max_peer_attempts / 4, + ) + }; for upa in unknown .iter() .filter(|p| !peers_deny.matches_addr(p)) @@ -284,19 +294,21 @@ fn monitor_peers( new_peers.push(*upa); } debug!( - "monitor_peers: check {} healthy, {} unknown, {} defuncts", + "monitor_peers: check {} healthy, {} unknown, {} defuncts (tls_required={})", cmp::min( new_peers.len() as i32, ((new_peers.len() - req_unk_count) as i32).abs() ), cmp::min(new_peers.len(), req_unk_count), - max_peer_attempts - new_peers.len() + max_peer_attempts - new_peers.len(), + tls_required, ); // check min 32 (max 128, if there are no healthy and unknown) random defunct peers no more often than 1 hour per peer. for dp in defuncts .iter() .filter(|p| { !peers_deny.matches_addr(&p.addr) + && config.accepts_outbound_peer_capabilities(p.capabilities) && (!enough_outbound || Utc::now().timestamp() - p.last_attempt >= max_attempt_delay) }) @@ -305,7 +317,8 @@ fn monitor_peers( new_peers.push(dp.addr); } - // If the peer db is stale or mostly defunct, include seeds as recovery candidates. + // If the peer db is stale or mostly defunct, include seeds as recovery candidates + // (bootstrap; seeds are tried even when TLS is required). if !enough_outbound { for addr in seed_addrs { if !peers_deny.matches_addr(&addr) && !new_peers.contains(&addr) { @@ -349,8 +362,13 @@ fn connect_to_seeds_and_peers( } // check if we have some peers in db - // look for peers that are able to give us other peers (via PEER_LIST capability) - let peers = peers.find_peers(p2p::State::Healthy, p2p::Capabilities::PEER_LIST, 128); + // look for peers that are able to give us other peers (via PEER_LIST capability), + // optionally also requiring the TLS capability when tls_required is set. + let peers = peers.find_peers( + p2p::State::Healthy, + config.peer_list_request_capabilities(), + 128, + ); // if so, get their addresses, otherwise use our seeds let mut peer_addrs = if peers.len() > 3 { @@ -451,6 +469,7 @@ fn listen_for_addrs( let peers_c = peers.clone(); let p2p_c = p2p.clone(); + let peer_list_caps = p2p.config.peer_list_request_capabilities(); thread::Builder::new() .name("peer_connect".to_string()) .spawn(move || match p2p_c.connect(addr) { @@ -458,11 +477,10 @@ fn listen_for_addrs( if peers_c.enough_outbound_peers() { return; } - // If peer advertizes PEER_LIST then ask it for more peers that support PEER_LIST. - // We want to build a local db of possible peers to connect to. - // We do not necessarily care (at this point in time) what other capabilities these peers support. + // If peer advertizes PEER_LIST then ask it for more peers. + // When tls_required, request PEER_LIST|TLS so we learn TLS-capable addresses. if p.info.capabilities.contains(p2p::Capabilities::PEER_LIST) { - let _ = p.send_peer_request(p2p::Capabilities::PEER_LIST); + let _ = p.send_peer_request(peer_list_caps); } } Err(_) => { diff --git a/servers/src/grin/server.rs b/servers/src/grin/server.rs index 976d2081af..7cf2b0f9b6 100644 --- a/servers/src/grin/server.rs +++ b/servers/src/grin/server.rs @@ -227,11 +227,20 @@ impl Server { // Initialize our capabilities. // Currently either "default" or with optional "archive_mode" (block history) support enabled. - let capabilities = if let Some(true) = config.archive_mode { + // When P2P TLS is enabled, advertise the TLS capability so peers can select us for encrypted links. + let mut capabilities = if let Some(true) = config.archive_mode { Capabilities::default() | Capabilities::BLOCK_HIST } else { Capabilities::default() }; + if config.p2p_config.tls_enabled { + capabilities |= Capabilities::TLS; + } + if config.p2p_config.tls_required && !config.p2p_config.tls_enabled { + warn!( + "p2p_config.tls_required is set but tls_enabled is false; TLS peer filter is ignored" + ); + } debug!("Capabilities: {:?}", capabilities); if let Some(ref server_tx) = server_tx {