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:
committed by
ShahanaFarooqui
parent
e398c63ea5
commit
2e7181d04f
28
plugins/wss-proxy-plugin/Cargo.toml
Normal file
28
plugins/wss-proxy-plugin/Cargo.toml
Normal 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" }
|
||||
|
||||
8
plugins/wss-proxy-plugin/Makefile
Normal file
8
plugins/wss-proxy-plugin/Makefile
Normal file
@@ -0,0 +1,8 @@
|
||||
clnwssproxy-wrongdir:
|
||||
$(MAKE) -C ../.. clnwssproxy-all
|
||||
|
||||
clnwssproxy_EXAMPLES :=
|
||||
|
||||
DEFAULT_TARGETS += $(clnwssproxy_EXAMPLES)
|
||||
|
||||
clnwssproxy-all: ${clnwssproxy_EXAMPLES}
|
||||
139
plugins/wss-proxy-plugin/src/certs.rs
Normal file
139
plugins/wss-proxy-plugin/src/certs.rs
Normal 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))
|
||||
}
|
||||
164
plugins/wss-proxy-plugin/src/main.rs
Normal file
164
plugins/wss-proxy-plugin/src/main.rs
Normal 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}})
|
||||
);
|
||||
}
|
||||
135
plugins/wss-proxy-plugin/src/options.rs
Normal file
135
plugins/wss-proxy-plugin/src/options.rs
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user