wss-proxy: replaced by a rust version

Changelog-Changed: wss-proxy.py was replaced by a rust version with support for multiple `wss-bind-addr`. If you install CLN from pre-compiled binaries you must remove the old wss-proxy directory first before installing CLN, usually
it is located in `/usr/local/libexec/c-lightning/plugins/wss-proxy`. If you compile from source `make` will take care of this automatically.
This commit is contained in:
daywalker90
2025-02-11 16:05:44 +01:00
committed by ShahanaFarooqui
parent e398c63ea5
commit 2e7181d04f
24 changed files with 798 additions and 1180 deletions

View File

@@ -0,0 +1,28 @@
[package]
name = "wss-proxy"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "WSS Proxy plugin"
homepage = "https://github.com/ElementsProject/lightning/tree/master/plugins"
repository = "https://github.com/ElementsProject/lightning"
[dependencies]
anyhow = "1"
log = { version = "0.4", features = ['std'] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version="1", features = ['io-std', 'rt-multi-thread', 'sync', 'macros', 'io-util'] }
rcgen = "0.13"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
tokio-tungstenite = { version = "0.26", features = ["tokio-rustls"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"]}
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "logging", "tls12"]}
log-panics = "2"
cln-plugin = { version = "0.4", path = "../../plugins" }
cln-rpc = { version = "0.4", path = "../../cln-rpc" }

View File

@@ -0,0 +1,8 @@
clnwssproxy-wrongdir:
$(MAKE) -C ../.. clnwssproxy-all
clnwssproxy_EXAMPLES :=
DEFAULT_TARGETS += $(clnwssproxy_EXAMPLES)
clnwssproxy-all: ${clnwssproxy_EXAMPLES}

View File

@@ -0,0 +1,139 @@
use anyhow::{anyhow, Error};
use rcgen::{CertificateParams, DistinguishedName, Ia5String, KeyPair};
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::ServerConfig;
use std::fs;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::options::WssproxyOptions;
pub fn generate_certificates(certs_path: &PathBuf, wss_host: &[String]) -> Result<(), Error> {
/* Generate the CA certificate */
let mut ca_params = CertificateParams::new(vec![
"cln Root wss-proxy CA".to_string(),
"cln".to_string(),
"localhost".to_string(),
])?;
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
let ca_key = KeyPair::generate()?;
let ca_cert = ca_params.self_signed(&ca_key)?;
fs::create_dir_all(certs_path)?;
fs::write(certs_path.join("ca.pem"), ca_cert.pem())?;
fs::write(
certs_path.join("ca-key.pem"),
ca_key.serialize_pem().as_bytes(),
)?;
/* Generate the server certificate signed by the CA */
let mut server_params = CertificateParams::new(vec![
format!("cln wss-proxy server"),
"cln".to_string(),
"localhost".to_string(),
])?;
server_params.is_ca = rcgen::IsCa::NoCa;
server_params.distinguished_name = DistinguishedName::new();
server_params
.distinguished_name
.push(rcgen::DnType::CommonName, "cln wss-proxy server");
/* It is convention to not include [] for ipv6 addresses in certificate SAN's */
for host in wss_host.iter() {
let host_stripped = if host.starts_with('[') && host.ends_with(']') {
host[1..host.len() - 1].to_string()
} else {
host.to_owned()
};
if let Ok(ip) = host_stripped.parse::<IpAddr>() {
server_params
.subject_alt_names
.push(rcgen::SanType::IpAddress(ip));
} else if let Ok(dns) = Ia5String::try_from(host.to_owned()) {
server_params
.subject_alt_names
.push(rcgen::SanType::DnsName(dns));
}
}
let server_key = KeyPair::generate()?;
let server_pem = server_params
.signed_by(&server_key, &ca_cert, &ca_key)?
.pem();
fs::write(certs_path.join("server.pem"), server_pem)?;
fs::write(
certs_path.join("server-key.pem"),
server_key.serialize_pem().as_bytes(),
)?;
/* Generate the client certificate signed by the CA */
let mut client_params = CertificateParams::new(vec![
format!("cln wss-proxy client"),
"cln".to_string(),
"localhost".to_string(),
])?;
client_params.is_ca = rcgen::IsCa::NoCa;
client_params.distinguished_name = DistinguishedName::new();
client_params
.distinguished_name
.push(rcgen::DnType::CommonName, "cln wss-proxy client");
let client_key = KeyPair::generate()?;
let client_pem = client_params
.signed_by(&client_key, &ca_cert, &ca_key)?
.pem();
fs::write(certs_path.join("client.pem"), client_pem)?;
fs::write(
certs_path.join("client-key.pem"),
client_key.serialize_pem().as_bytes(),
)?;
Ok(())
}
pub fn do_certificates_exist(cert_dir: &Path) -> bool {
let required_files = [
"server.pem",
"server-key.pem",
"client.pem",
"client-key.pem",
"ca.pem",
"ca-key.pem",
];
required_files.iter().all(|file| {
let path = cert_dir.join(file);
path.exists() && path.metadata().map(|m| m.len() > 0).unwrap_or(false)
})
}
pub async fn get_tls_config(wss_proxy_options: &WssproxyOptions) -> Result<ServerConfig, Error> {
let max_retries = 20;
let mut retries = 0;
while retries < max_retries && !do_certificates_exist(&wss_proxy_options.certs_dir) {
log::debug!("Certificates incomplete. Retrying...");
tokio::time::sleep(Duration::from_millis(500)).await;
retries += 1;
}
if !do_certificates_exist(&wss_proxy_options.certs_dir) {
log::debug!("Certificates still not existing after retries. Generating...");
generate_certificates(&wss_proxy_options.certs_dir, &wss_proxy_options.wss_domains)?;
}
let certs = CertificateDer::pem_file_iter(wss_proxy_options.certs_dir.join("server.pem"))
.unwrap()
.map(|cert| cert.unwrap())
.collect();
let private_key =
PrivateKeyDer::from_pem_file(wss_proxy_options.certs_dir.join("server-key.pem")).unwrap();
rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, private_key)
.map_err(|e| anyhow!("{}", e))
}

View File

@@ -0,0 +1,164 @@
use std::{net::SocketAddr, process, sync::Arc};
use anyhow::anyhow;
use certs::get_tls_config;
use cln_plugin::{options::ConfigOption, Builder};
use futures_util::{SinkExt, StreamExt};
use options::{parse_options, WssproxyOptions, OPT_WSS_BIND_ADDR, OPT_WSS_CERTS_DIR};
use rustls::ServerConfig;
use tokio::net::{TcpListener, TcpStream};
use tokio_rustls::{server::TlsStream, TlsAcceptor};
use tokio_tungstenite::{accept_async, WebSocketStream};
mod certs;
mod options;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
log_panics::init();
std::env::set_var(
"CLN_PLUGIN_LOG",
"cln_plugin=info,cln_rpc=info,wss_proxy=debug,warn",
);
let opt_wss_proxy_bind_addr = ConfigOption::new_str_arr_no_default(
OPT_WSS_BIND_ADDR,
"WSS proxy address to connect with WS",
);
let default_certs_dir = std::env::current_dir()?;
let default_certs_dir_str = default_certs_dir
.to_str()
.ok_or_else(|| anyhow!("Invalid working directory: {:?}", default_certs_dir))?;
let opt_wss_proxy_certs = ConfigOption::new_str_with_default(
OPT_WSS_CERTS_DIR,
default_certs_dir_str,
"Certificate location for WSS proxy",
);
let conf_plugin = match Builder::new(tokio::io::stdin(), tokio::io::stdout())
.option(opt_wss_proxy_bind_addr)
.option(opt_wss_proxy_certs)
.dynamic()
.configure()
.await?
{
Some(p) => p,
None => return Ok(()),
};
let wss_proxy_options = match parse_options(&conf_plugin).await {
Ok(opts) => opts,
Err(e) => return conf_plugin.disable(&e.to_string()).await,
};
let plugin = conf_plugin.start(()).await?;
let tls_config = match get_tls_config(&wss_proxy_options).await {
Ok(tls) => tls,
Err(err) => {
log_error(err.to_string());
process::exit(1)
}
};
for wss_address in wss_proxy_options.wss_addresses.clone().into_iter() {
let options_clone = wss_proxy_options.clone();
let tls_clone = tls_config.clone();
tokio::spawn(async move {
match start_proxy(options_clone, wss_address, tls_clone).await {
Ok(_) => (),
Err(err) => {
log_error(err.to_string());
process::exit(1)
}
}
});
}
plugin.join().await
}
async fn start_proxy(
wss_proxy_options: WssproxyOptions,
wss_address: SocketAddr,
tls_config: ServerConfig,
) -> Result<(), anyhow::Error> {
let listener = TcpListener::bind(wss_address).await?;
log::info!("Websocket Secure Server Started at {}", wss_address);
loop {
if let Ok((stream, _)) = listener.accept().await {
let tls_acceptor = TlsAcceptor::from(Arc::new(tls_config.clone()));
let tls_stream = match tls_acceptor.accept(stream).await {
Ok(o) => o,
Err(e) => {
log::debug!("Error upgrading to tls: {}", e);
continue;
}
};
let wss_stream = match accept_async(tls_stream).await {
Ok(o) => o,
Err(e) => {
log::debug!("Error upgrading to websocket: {}", e);
continue;
}
};
tokio::spawn(async move {
match relay_messages(wss_stream, wss_proxy_options.ws_address).await {
Ok(_) => (),
Err(e) => log::info!("Error relaying messages: {}", e),
}
});
} else {
return Err(anyhow!("TCP Listener closed!"));
}
}
}
async fn relay_messages(
wss_stream: WebSocketStream<TlsStream<TcpStream>>,
ws_address: SocketAddr,
) -> Result<(), anyhow::Error> {
let (ws_stream, _ws_response) =
tokio_tungstenite::connect_async(format!("ws://{}", ws_address)).await?;
let (mut wss_sender, mut wss_receiver) = wss_stream.split();
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
/* Relay from WSS to WS */
tokio::spawn(async move {
while let Some(writer) = wss_receiver.next().await {
if let Ok(msg) = writer {
if let Err(e) = ws_sender.send(msg.clone()).await {
log::debug!("Error sending message to WS server: {}", e);
break;
}
}
}
});
/* Relay from WS to WSS */
tokio::spawn(async move {
while let Some(msg) = ws_receiver.next().await {
if let Ok(msg) = msg {
if let Err(e) = wss_sender.send(msg.clone()).await {
log::debug!("Error sending message to WSS client: {}", e);
break;
}
}
}
});
Ok(())
}
/* Workaround: Using log crate right before plugin exit will not print */
fn log_error(error: String) {
println!(
"{}",
serde_json::json!({"jsonrpc": "2.0",
"method": "log",
"params": {"level":"info", "message":error}})
);
}

View File

@@ -0,0 +1,135 @@
use std::{
net::{SocketAddr, ToSocketAddrs},
path::{Path, PathBuf},
};
use anyhow::anyhow;
use cln_plugin::ConfiguredPlugin;
use cln_rpc::{model::requests::ListconfigsRequest, ClnRpc};
pub const OPT_WSS_BIND_ADDR: &str = "wss-bind-addr";
pub const OPT_WSS_CERTS_DIR: &str = "wss-certs";
#[derive(Debug, Clone)]
pub struct WssproxyOptions {
pub wss_addresses: Vec<SocketAddr>,
pub wss_domains: Vec<String>,
pub ws_address: SocketAddr,
pub certs_dir: PathBuf,
}
pub async fn parse_options(
plugin: &ConfiguredPlugin<(), tokio::io::Stdin, tokio::io::Stdout>,
) -> Result<WssproxyOptions, anyhow::Error> {
let wss_address_val = plugin
.option_str(OPT_WSS_BIND_ADDR)?
.ok_or_else(|| anyhow!("`{}` option is not configured", OPT_WSS_BIND_ADDR))?;
let wss_address_str = wss_address_val
.as_str_arr()
.ok_or_else(|| anyhow!("{} is not a string array!", OPT_WSS_BIND_ADDR))?;
let mut wss_domains = Vec::new();
let mut wss_addresses = Vec::new();
for addr in wss_address_str.iter() {
wss_domains.push(
addr.rsplit_once(':')
.ok_or_else(|| anyhow!("WSS host missing port. Current Value: {}.", addr))?
.0
.to_owned(),
);
wss_addresses.extend(addr.to_socket_addrs().map_err(|_| {
anyhow!(
"WSS host should be a valid IP or resolvable domain. Current Value: {}.",
addr
)
})?);
}
if wss_addresses.is_empty() {
return Err(anyhow!(
"WSS host is missing a valid IP or resolvable domain."
));
}
for socket_addr in wss_addresses.iter() {
if !validate_port(socket_addr.port()) {
return Err(anyhow!(
"WSS port should be a valid available port between 1024 and 65535. \
Current Value: {}.",
socket_addr.port()
));
}
}
let certs_dir_val = plugin
.option_str(OPT_WSS_CERTS_DIR)?
.ok_or_else(|| anyhow!("{} is not set!", OPT_WSS_CERTS_DIR))?;
let certs_dir_str = certs_dir_val
.as_str()
.ok_or_else(|| anyhow!("{} is not a string!", OPT_WSS_CERTS_DIR))?;
let certs_dir = PathBuf::from(certs_dir_str);
let mut rpc = ClnRpc::new(
Path::new(&plugin.configuration().lightning_dir).join(plugin.configuration().rpc_file),
)
.await?;
let ws_addr_config = rpc
.call_typed(&ListconfigsRequest {
config: Some("bind-addr".to_string()),
})
.await?
.configs
.ok_or_else(|| anyhow!("Could not get configs object. CLN version too old?"))?
.bind_addr;
let mut ws_address: Option<SocketAddr> = None;
let ws_address_conf = ws_addr_config.ok_or_else(|| anyhow!("`bind-addr` not set!"))?;
for addr in ws_address_conf.values_str.iter() {
if let Some(addr_stripped) = addr.strip_prefix("ws:") {
let ws_address_ips = addr_stripped.to_socket_addrs().map_err(|_| {
anyhow!(
"`bind-addr` with `ws:` IP should be a valid IP or resolvable domain. \
Current Value: {}.",
addr_stripped
)
})?;
/* Prefer ipv4 here like connectd does */
for add in ws_address_ips.into_iter() {
if add.is_ipv6() && ws_address.is_none() {
ws_address = Some(add)
}
if add.is_ipv4() {
ws_address = Some(add);
break;
}
}
}
}
let ws_address = ws_address.ok_or_else(|| anyhow!("`bind-addr` with `ws:` not set!"))?;
if !validate_port(ws_address.port()) {
return Err(anyhow!(
"`bind-addr` with `ws` port should be a valid available port \
between 1024 and 65535. Current Value: {}.",
ws_address.port()
));
}
log::debug!("Connecting to ws-server via: {}", ws_address);
Ok(WssproxyOptions {
wss_addresses,
wss_domains,
ws_address,
certs_dir,
})
}
fn validate_port(port: u16) -> bool {
if (1024..=65535).contains(&port) {
return true;
}
false
}