From ac0404b83f915eb376c42c3f145b5e0c70ce00ab Mon Sep 17 00:00:00 2001 From: Soham Zemse <22412996+zemse@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:58:04 +0530 Subject: [PATCH 1/3] feat: add CORS support for HTTP API and metrics endpoints --- Cargo.lock | 1 + anchor/client/src/cli.rs | 15 +++++++++++++-- anchor/client/src/config.rs | 8 ++++++++ anchor/client/src/lib.rs | 7 ++++++- anchor/http_api/Cargo.toml | 1 + anchor/http_api/src/lib.rs | 14 ++++++++++++-- anchor/http_metrics/src/lib.rs | 18 +++++++++++++----- 7 files changed, 54 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e9421f14..4e41e10fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3749,6 +3749,7 @@ dependencies = [ "ssv_types", "task_executor", "tokio", + "tower-http", "tracing", "version", ] diff --git a/anchor/client/src/cli.rs b/anchor/client/src/cli.rs index a3710da19..6d96c4ab5 100644 --- a/anchor/client/src/cli.rs +++ b/anchor/client/src/cli.rs @@ -323,8 +323,19 @@ pub struct Node { help_heading = FLAG_HEADER )] pub enable_high_validator_count_metrics: bool, - // TODO: Metrics CORS Origin - // https://github.com/sigp/anchor/issues/249 + + #[clap( + long, + value_name = "ORIGIN", + help = "Set the value of the Access-Control-Allow-Origin response HTTP header \ + for the metrics server. Use * to allow any origin (not recommended in production). \ + If no value is supplied, the CORS allowed origin is set to the listen \ + address of this server (e.g., http://localhost:5164).", + display_order = 0, + requires = "metrics" + )] + pub metrics_allow_origin: Option, + #[clap( long, global = true, diff --git a/anchor/client/src/config.rs b/anchor/client/src/config.rs index 81c2ca120..573c82939 100644 --- a/anchor/client/src/config.rs +++ b/anchor/client/src/config.rs @@ -271,6 +271,14 @@ pub fn from_cli(cli_args: &Node, global_config: GlobalConfig) -> Result>) -> Result<() return Ok(()); } + let origin = config + .allow_origin + .map(|o| AllowOrigin::exact(o.parse().expect("validated in config"))) + .unwrap_or(AllowOrigin::any()); + + let cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(origin); + // Generate the axum routes - let router = router::new(shared_state); + let router = router::new(shared_state).layer(cors); // Set up a listening address - let socket = SocketAddr::new(config.listen_addr, config.listen_port); let listener = TcpListener::bind(socket).await.map_err(|e| e.to_string())?; diff --git a/anchor/http_metrics/src/lib.rs b/anchor/http_metrics/src/lib.rs index a761b4b1e..526ed9e2e 100644 --- a/anchor/http_metrics/src/lib.rs +++ b/anchor/http_metrics/src/lib.rs @@ -25,7 +25,7 @@ use prometheus_client::encoding::text::encode; use serde::{Deserialize, Serialize}; use slot_clock::{SlotClock, SystemTimeSlotClock}; use tokio::net::TcpListener; -use tower_http::cors::{Any, CorsLayer}; +use tower_http::cors::{AllowOrigin, CorsLayer}; use tracing::error; use types::EthSpec; use validator_services::duties_service::DutiesService; @@ -60,12 +60,19 @@ impl Default for Config { } } -fn create_router(shared_state: Arc>>) -> Router { +fn create_router( + shared_state: Arc>>, + allow_origin: Option, +) -> Router { + let origin = allow_origin + .map(|o| AllowOrigin::exact(o.parse().expect("validated in config"))) + .unwrap_or(AllowOrigin::any()); + let cors = CorsLayer::new() // allow `GET` and `POST` when accessing the resource .allow_methods([Method::GET, Method::POST]) - // allow requests from any origin - .allow_origin(Any); + // allow requests from custom origin + .allow_origin(origin); Router::new() .route("/metrics", get(metrics_handler)) @@ -148,10 +155,11 @@ async fn metrics_handler( pub async fn serve( listener: TcpListener, shared_state: Arc>>, + allow_origin: Option, shutdown: impl Future + Send + Sync + 'static, ) { // Generate the axum routes - let router = create_router(shared_state); + let router = create_router(shared_state, allow_origin); // Start the http api server if let Err(e) = axum::serve(listener, router) From ad74bc2b152957cb24f2de039f66680e5fef3dd9 Mon Sep 17 00:00:00 2001 From: Soham Zemse <22412996+zemse@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:57:48 +0530 Subject: [PATCH 2/3] refactor: store AllowOrigin directly in config instead of String - Parse CLI args directly into AllowOrigin in from_cli - Remove duplicate parsing in http_api and http_metrics - Replace hyper with tower-http dependency in client crate Co-Authored-By: Claude Opus 4.5 --- anchor/client/Cargo.toml | 2 +- anchor/client/src/config.rs | 16 +++++++--------- anchor/http_api/src/config.rs | 8 ++++---- anchor/http_api/src/lib.rs | 9 ++------- anchor/http_metrics/src/lib.rs | 19 ++++++------------- 5 files changed, 20 insertions(+), 34 deletions(-) diff --git a/anchor/client/Cargo.toml b/anchor/client/Cargo.toml index 397faf3b5..c92eda246 100644 --- a/anchor/client/Cargo.toml +++ b/anchor/client/Cargo.toml @@ -22,7 +22,6 @@ fork = { workspace = true } global_config = { workspace = true } http_api = { workspace = true } http_metrics = { workspace = true } -hyper = { workspace = true } keygen = { workspace = true } logging = { workspace = true } message_receiver = { workspace = true } @@ -46,6 +45,7 @@ ssv_types = { workspace = true } subnet_service = { workspace = true } task_executor = { workspace = true } tokio = { workspace = true } +tower-http = { workspace = true } tracing = { workspace = true } types = { workspace = true } validator_metrics = { workspace = true } diff --git a/anchor/client/src/config.rs b/anchor/client/src/config.rs index 573c82939..b54d50815 100644 --- a/anchor/client/src/config.rs +++ b/anchor/client/src/config.rs @@ -12,6 +12,7 @@ use network_utils::unused_port::{ }; use sensitive_url::SensitiveUrl; use ssv_types::OperatorId; +use tower_http::cors::AllowOrigin; use tracing::{error, warn}; use crate::cli::Node; @@ -249,12 +250,10 @@ pub fn from_cli(cli_args: &Node, global_config: GlobalConfig) -> Result Result, + pub allow_origin: AllowOrigin, } impl Default for Config { @@ -19,7 +19,7 @@ impl Default for Config { enabled: false, listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 5062, - allow_origin: None, + allow_origin: AllowOrigin::any(), } } } diff --git a/anchor/http_api/src/lib.rs b/anchor/http_api/src/lib.rs index 29fca05d6..042c010dd 100644 --- a/anchor/http_api/src/lib.rs +++ b/anchor/http_api/src/lib.rs @@ -10,7 +10,7 @@ use parking_lot::RwLock; use slot_clock::SlotClock; use task_executor::TaskExecutor; use tokio::{net::TcpListener, sync::watch}; -use tower_http::cors::{AllowOrigin, CorsLayer}; +use tower_http::cors::CorsLayer; use tracing::info; /// A wrapper around all the items required to spawn the HTTP server. /// @@ -40,14 +40,9 @@ pub async fn run(config: Config, shared_state: Arc>) -> Result<() return Ok(()); } - let origin = config - .allow_origin - .map(|o| AllowOrigin::exact(o.parse().expect("validated in config"))) - .unwrap_or(AllowOrigin::any()); - let cors = CorsLayer::new() .allow_methods([Method::GET, Method::POST]) - .allow_origin(origin); + .allow_origin(config.allow_origin); // Generate the axum routes let router = router::new(shared_state).layer(cors); diff --git a/anchor/http_metrics/src/lib.rs b/anchor/http_metrics/src/lib.rs index 526ed9e2e..c9f57206e 100644 --- a/anchor/http_metrics/src/lib.rs +++ b/anchor/http_metrics/src/lib.rs @@ -22,7 +22,6 @@ use axum::{ use libp2p::metrics::Registry; use parking_lot::RwLock; use prometheus_client::encoding::text::encode; -use serde::{Deserialize, Serialize}; use slot_clock::{SlotClock, SystemTimeSlotClock}; use tokio::net::TcpListener; use tower_http::cors::{AllowOrigin, CorsLayer}; @@ -41,12 +40,12 @@ pub struct Shared { } /// Configuration for the HTTP server. -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct Config { pub enabled: bool, pub listen_addr: IpAddr, pub listen_port: u16, - pub allow_origin: Option, + pub allow_origin: AllowOrigin, } impl Default for Config { @@ -55,24 +54,18 @@ impl Default for Config { enabled: false, listen_addr: IpAddr::V4(Ipv4Addr::LOCALHOST), listen_port: 5164, - allow_origin: None, + allow_origin: AllowOrigin::any(), } } } fn create_router( shared_state: Arc>>, - allow_origin: Option, + allow_origin: AllowOrigin, ) -> Router { - let origin = allow_origin - .map(|o| AllowOrigin::exact(o.parse().expect("validated in config"))) - .unwrap_or(AllowOrigin::any()); - let cors = CorsLayer::new() - // allow `GET` and `POST` when accessing the resource .allow_methods([Method::GET, Method::POST]) - // allow requests from custom origin - .allow_origin(origin); + .allow_origin(allow_origin); Router::new() .route("/metrics", get(metrics_handler)) @@ -155,7 +148,7 @@ async fn metrics_handler( pub async fn serve( listener: TcpListener, shared_state: Arc>>, - allow_origin: Option, + allow_origin: AllowOrigin, shutdown: impl Future + Send + Sync + 'static, ) { // Generate the axum routes From a4b514639f65cf655f92139cc0d561f4002b25b8 Mon Sep 17 00:00:00 2001 From: Soham Zemse <22412996+zemse@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:45:11 +0530 Subject: [PATCH 3/3] fix: default CORS origin to listen address Change default AllowOrigin from `any()` to the listen address and port, matching Lighthouse's behavior for safer defaults. Co-Authored-By: Claude Opus 4.6 --- anchor/client/src/config.rs | 4 ++-- anchor/client/src/lib.rs | 2 +- anchor/http_api/src/config.rs | 17 +++++++++++++++-- anchor/http_api/src/lib.rs | 2 +- anchor/http_metrics/src/lib.rs | 17 +++++++++++++++-- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/anchor/client/src/config.rs b/anchor/client/src/config.rs index b54d50815..5d1833c2b 100644 --- a/anchor/client/src/config.rs +++ b/anchor/client/src/config.rs @@ -253,7 +253,7 @@ pub fn from_cli(cli_args: &Node, global_config: GlobalConfig) -> Result Result, } impl Default for Config { @@ -19,7 +19,20 @@ impl Default for Config { enabled: false, listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 5062, - allow_origin: AllowOrigin::any(), + allow_origin: None, } } } + +impl Config { + /// Returns the configured `AllowOrigin`, or falls back to the listen address and port. + pub fn allow_origin(&self) -> AllowOrigin { + self.allow_origin.clone().unwrap_or_else(|| { + AllowOrigin::exact( + format!("http://{}:{}", self.listen_addr, self.listen_port) + .parse() + .expect("listen address and port should produce a valid header value"), + ) + }) + } +} diff --git a/anchor/http_api/src/lib.rs b/anchor/http_api/src/lib.rs index 042c010dd..61473392e 100644 --- a/anchor/http_api/src/lib.rs +++ b/anchor/http_api/src/lib.rs @@ -42,7 +42,7 @@ pub async fn run(config: Config, shared_state: Arc>) -> Result<() let cors = CorsLayer::new() .allow_methods([Method::GET, Method::POST]) - .allow_origin(config.allow_origin); + .allow_origin(config.allow_origin()); // Generate the axum routes let router = router::new(shared_state).layer(cors); diff --git a/anchor/http_metrics/src/lib.rs b/anchor/http_metrics/src/lib.rs index c9f57206e..537fe3ed1 100644 --- a/anchor/http_metrics/src/lib.rs +++ b/anchor/http_metrics/src/lib.rs @@ -45,7 +45,7 @@ pub struct Config { pub enabled: bool, pub listen_addr: IpAddr, pub listen_port: u16, - pub allow_origin: AllowOrigin, + pub allow_origin: Option, } impl Default for Config { @@ -54,11 +54,24 @@ impl Default for Config { enabled: false, listen_addr: IpAddr::V4(Ipv4Addr::LOCALHOST), listen_port: 5164, - allow_origin: AllowOrigin::any(), + allow_origin: None, } } } +impl Config { + /// Returns the configured `AllowOrigin`, or falls back to the listen address and port. + pub fn allow_origin(&self) -> AllowOrigin { + self.allow_origin.clone().unwrap_or_else(|| { + AllowOrigin::exact( + format!("http://{}:{}", self.listen_addr, self.listen_port) + .parse() + .expect("listen address and port should produce a valid header value"), + ) + }) + } +} + fn create_router( shared_state: Arc>>, allow_origin: AllowOrigin,