mirror of
https://github.com/EasyTier/EasyTier.git
synced 2025-10-18 14:50:43 +08:00
Added RPC portal whitelist function, allowing only local access by default to enhance security (#929)
This commit is contained in:
@@ -304,6 +304,15 @@ const bool_flags: BoolFlag[] = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||
<div class="flex flex-col gap-2 grow p-fluid">
|
||||
<label for="">{{ t('rpc_portal_whitelists') }}</label>
|
||||
<AutoComplete id="rpc_portal_whitelists" v-model="curNetwork.rpc_portal_whitelists"
|
||||
:placeholder="t('chips_placeholder', ['127.0.0.0/8'])" class="w-full" multiple fluid
|
||||
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="dev_name">{{ t('dev_name') }}</label>
|
||||
|
@@ -18,6 +18,7 @@ advanced_settings: 高级设置
|
||||
basic_settings: 基础设置
|
||||
listener_urls: 监听地址
|
||||
rpc_port: RPC端口
|
||||
rpc_portal_whitelists: RPC白名单
|
||||
config_network: 配置网络
|
||||
running: 运行中
|
||||
error_msg: 错误信息
|
||||
|
@@ -18,6 +18,7 @@ advanced_settings: Advanced Settings
|
||||
basic_settings: Basic Settings
|
||||
listener_urls: Listener URLs
|
||||
rpc_port: RPC Port
|
||||
rpc_portal_whitelists: RPC Whitelist
|
||||
config_network: Config Network
|
||||
running: Running
|
||||
error_msg: Error Message
|
||||
|
@@ -65,6 +65,8 @@ export interface NetworkConfig {
|
||||
|
||||
enable_magic_dns?: boolean
|
||||
enable_private_mode?: boolean
|
||||
|
||||
rpc_portal_whitelists: string[]
|
||||
}
|
||||
|
||||
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
@@ -123,6 +125,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
mapped_listeners: [],
|
||||
enable_magic_dns: false,
|
||||
enable_private_mode: false,
|
||||
rpc_portal_whitelists: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -37,6 +37,9 @@ core_clap:
|
||||
rpc_portal:
|
||||
en: "rpc portal address to listen for management. 0 means random port, 12345 means listen on 12345 of localhost, 0.0.0.0:12345 means listen on 12345 of all interfaces. default is 0 and will try 15888 first"
|
||||
zh-CN: "用于管理的RPC门户地址。0表示随机端口,12345表示在localhost的12345上监听,0.0.0.0:12345表示在所有接口的12345上监听。默认是0,首先尝试15888"
|
||||
rpc_portal_whitelist:
|
||||
en: "rpc portal whitelist, only allow these addresses to access rpc portal, e.g.: 127.0.0.1,127.0.0.0/8,::1/128"
|
||||
zh-CN: "RPC门户白名单,仅允许这些地址访问RPC门户,例如:127.0.0.1/32,127.0.0.0/8,::1/128"
|
||||
listeners:
|
||||
en: |+
|
||||
listeners to accept connections, allow format:
|
||||
|
@@ -5,6 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use cidr::IpCidr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
@@ -87,6 +88,9 @@ pub trait ConfigLoader: Send + Sync {
|
||||
fn get_rpc_portal(&self) -> Option<SocketAddr>;
|
||||
fn set_rpc_portal(&self, addr: SocketAddr);
|
||||
|
||||
fn get_rpc_portal_whitelist(&self) -> Option<Vec<IpCidr>>;
|
||||
fn set_rpc_portal_whitelist(&self, whitelist: Option<Vec<IpCidr>>);
|
||||
|
||||
fn get_vpn_portal_config(&self) -> Option<VpnPortalConfig>;
|
||||
fn set_vpn_portal_config(&self, config: VpnPortalConfig);
|
||||
|
||||
@@ -243,6 +247,7 @@ struct Config {
|
||||
console_logger: Option<ConsoleLoggerConfig>,
|
||||
|
||||
rpc_portal: Option<SocketAddr>,
|
||||
rpc_portal_whitelist: Option<Vec<IpCidr>>,
|
||||
|
||||
vpn_portal_config: Option<VpnPortalConfig>,
|
||||
|
||||
@@ -544,6 +549,14 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
self.config.lock().unwrap().rpc_portal = Some(addr);
|
||||
}
|
||||
|
||||
fn get_rpc_portal_whitelist(&self) -> Option<Vec<IpCidr>> {
|
||||
self.config.lock().unwrap().rpc_portal_whitelist.clone()
|
||||
}
|
||||
|
||||
fn set_rpc_portal_whitelist(&self, whitelist: Option<Vec<IpCidr>>) {
|
||||
self.config.lock().unwrap().rpc_portal_whitelist = whitelist;
|
||||
}
|
||||
|
||||
fn get_vpn_portal_config(&self) -> Option<VpnPortalConfig> {
|
||||
self.config.lock().unwrap().vpn_portal_config.clone()
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use cidr::IpCidr;
|
||||
use clap::Parser;
|
||||
|
||||
use easytier::{
|
||||
@@ -176,6 +177,14 @@ struct Cli {
|
||||
)]
|
||||
rpc_portal: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_RPC_PORTAL_WHITELIST",
|
||||
value_delimiter = ',',
|
||||
help = t!("core_clap.rpc_portal_whitelist").to_string(),
|
||||
)]
|
||||
rpc_portal_whitelist: Option<Vec<IpCidr>>,
|
||||
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
@@ -616,6 +625,8 @@ impl TryFrom<&Cli> for TomlConfigLoader {
|
||||
};
|
||||
cfg.set_rpc_portal(rpc_portal);
|
||||
|
||||
cfg.set_rpc_portal_whitelist(cli.rpc_portal_whitelist.clone());
|
||||
|
||||
if let Some(external_nodes) = cli.external_node.as_ref() {
|
||||
let mut old_peers = cfg.get_peers();
|
||||
old_peers.push(PeerConfig {
|
||||
|
@@ -298,12 +298,13 @@ impl NicPacketFilter for MagicDnsServerInstanceData {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RpcServerHook for MagicDnsServerInstanceData {
|
||||
async fn on_new_client(&self, tunnel_info: Option<TunnelInfo>) {
|
||||
println!("New client connected: {:?}", tunnel_info);
|
||||
async fn on_new_client(&self, tunnel_info: Option<TunnelInfo>)-> Result<Option<TunnelInfo>, anyhow::Error> {
|
||||
tracing::info!(?tunnel_info, "New client connected");
|
||||
Ok(tunnel_info)
|
||||
}
|
||||
|
||||
async fn on_client_disconnected(&self, tunnel_info: Option<TunnelInfo>) {
|
||||
println!("Client disconnected: {:?}", tunnel_info);
|
||||
tracing::info!(?tunnel_info, "Client disconnected");
|
||||
let Some(tunnel_info) = tunnel_info else {
|
||||
return;
|
||||
};
|
||||
|
@@ -1,11 +1,11 @@
|
||||
use std::any::Any;
|
||||
use std::collections::HashSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use anyhow::Context;
|
||||
use cidr::Ipv4Inet;
|
||||
use cidr::{IpCidr, Ipv4Inet};
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::{sync::Mutex, task::JoinSet};
|
||||
@@ -29,8 +29,9 @@ use crate::peers::rpc_service::PeerManagerRpcService;
|
||||
use crate::peers::{create_packet_recv_chan, recv_packet_from_chan, PacketRecvChanReceiver};
|
||||
use crate::proto::cli::VpnPortalRpc;
|
||||
use crate::proto::cli::{GetVpnPortalInfoRequest, GetVpnPortalInfoResponse, VpnPortalInfo};
|
||||
use crate::proto::common::TunnelInfo;
|
||||
use crate::proto::peer_rpc::PeerCenterRpcServer;
|
||||
use crate::proto::rpc_impl::standalone::StandAloneServer;
|
||||
use crate::proto::rpc_impl::standalone::{RpcServerHook, StandAloneServer};
|
||||
use crate::proto::rpc_types;
|
||||
use crate::proto::rpc_types::controller::BaseController;
|
||||
use crate::tunnel::tcp::TcpTunnelListener;
|
||||
@@ -155,6 +156,58 @@ impl NicCtxContainer {
|
||||
|
||||
type ArcNicCtx = Arc<Mutex<Option<NicCtxContainer>>>;
|
||||
|
||||
pub struct InstanceRpcServerHook {
|
||||
rpc_portal_whitelist: Vec<IpCidr>,
|
||||
}
|
||||
|
||||
impl InstanceRpcServerHook {
|
||||
pub fn new(rpc_portal_whitelist: Option<Vec<IpCidr>>) -> Self {
|
||||
let rpc_portal_whitelist = rpc_portal_whitelist
|
||||
.unwrap_or_else(|| vec!["127.0.0.0/8".parse().unwrap(), "::1/128".parse().unwrap()]);
|
||||
InstanceRpcServerHook {
|
||||
rpc_portal_whitelist,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RpcServerHook for InstanceRpcServerHook {
|
||||
async fn on_new_client(
|
||||
&self,
|
||||
tunnel_info: Option<TunnelInfo>,
|
||||
) -> Result<Option<TunnelInfo>, anyhow::Error> {
|
||||
let tunnel_info = tunnel_info.ok_or_else(|| anyhow::anyhow!("tunnel info is None"))?;
|
||||
|
||||
let remote_url = tunnel_info
|
||||
.remote_addr
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("remote_addr is None"))?;
|
||||
|
||||
let url_str = &remote_url.url;
|
||||
let url = url::Url::parse(url_str)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse remote URL '{}': {}", url_str, e))?;
|
||||
|
||||
let host = url
|
||||
.host_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("No host found in remote URL '{}'", url_str))?;
|
||||
|
||||
let ip_addr: IpAddr = host
|
||||
.parse()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse IP address '{}': {}", host, e))?;
|
||||
|
||||
for cidr in &self.rpc_portal_whitelist {
|
||||
if cidr.contains(&ip_addr) {
|
||||
return Ok(Some(tunnel_info));
|
||||
}
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"Rpc portal client IP {} not in whitelist: {:?}, ignoring client.",
|
||||
ip_addr,
|
||||
self.rpc_portal_whitelist
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Instance {
|
||||
inst_name: String,
|
||||
|
||||
@@ -674,6 +727,10 @@ impl Instance {
|
||||
);
|
||||
}
|
||||
|
||||
s.set_hook(Arc::new(InstanceRpcServerHook::new(
|
||||
self.global_ctx.config.get_rpc_portal_whitelist(),
|
||||
)));
|
||||
|
||||
let _g = self.global_ctx.net_ns.guard();
|
||||
Ok(s.serve().await.with_context(|| "rpc server start failed")?)
|
||||
}
|
||||
@@ -726,3 +783,129 @@ impl Instance {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{instance::instance::InstanceRpcServerHook, proto::rpc_impl::standalone::RpcServerHook};
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rpc_portal_whitelist() {
|
||||
use cidr::IpCidr;
|
||||
|
||||
struct TestCase {
|
||||
remote_url: String,
|
||||
whitelist: Option<Vec<IpCidr>>,
|
||||
expected_result: bool,
|
||||
}
|
||||
|
||||
let test_cases:Vec<TestCase> = vec![
|
||||
// Test default whitelist (127.0.0.0/8, ::1/128)
|
||||
TestCase {
|
||||
remote_url: "tcp://127.0.0.1:15888".to_string(),
|
||||
whitelist: None,
|
||||
expected_result: true,
|
||||
},
|
||||
TestCase {
|
||||
remote_url: "tcp://127.1.2.3:15888".to_string(),
|
||||
whitelist: None,
|
||||
expected_result: true,
|
||||
},
|
||||
TestCase {
|
||||
remote_url: "tcp://192.168.1.1:15888".to_string(),
|
||||
whitelist: None,
|
||||
expected_result: false,
|
||||
},
|
||||
|
||||
// Test custom whitelist
|
||||
TestCase {
|
||||
remote_url: "tcp://192.168.1.10:15888".to_string(),
|
||||
whitelist: Some(vec![
|
||||
"192.168.1.0/24".parse().unwrap(),
|
||||
"10.0.0.0/8".parse().unwrap(),
|
||||
]),
|
||||
expected_result: true,
|
||||
},
|
||||
TestCase {
|
||||
remote_url: "tcp://10.1.2.3:15888".to_string(),
|
||||
whitelist: Some(vec![
|
||||
"192.168.1.0/24".parse().unwrap(),
|
||||
"10.0.0.0/8".parse().unwrap(),
|
||||
]),
|
||||
expected_result: true,
|
||||
},
|
||||
TestCase {
|
||||
remote_url: "tcp://172.16.0.1:15888".to_string(),
|
||||
whitelist: Some(vec![
|
||||
"192.168.1.0/24".parse().unwrap(),
|
||||
"10.0.0.0/8".parse().unwrap(),
|
||||
]),
|
||||
expected_result: false,
|
||||
},
|
||||
|
||||
// Test empty whitelist (should reject all connections)
|
||||
TestCase {
|
||||
remote_url: "tcp://127.0.0.1:15888".to_string(),
|
||||
whitelist: Some(vec![]),
|
||||
expected_result: false,
|
||||
},
|
||||
|
||||
// Test broad whitelist (0.0.0.0/0 and ::/0 accept all IP addresses)
|
||||
TestCase {
|
||||
remote_url: "tcp://8.8.8.8:15888".to_string(),
|
||||
whitelist: Some(vec![
|
||||
"0.0.0.0/0".parse().unwrap(),
|
||||
]),
|
||||
expected_result: true,
|
||||
},
|
||||
|
||||
// Test edge case: specific IP whitelist
|
||||
TestCase {
|
||||
remote_url: "tcp://192.168.1.5:15888".to_string(),
|
||||
whitelist: Some(vec![
|
||||
"192.168.1.5/32".parse().unwrap(),
|
||||
]),
|
||||
expected_result: true,
|
||||
},
|
||||
TestCase {
|
||||
remote_url: "tcp://192.168.1.6:15888".to_string(),
|
||||
whitelist: Some(vec![
|
||||
"192.168.1.5/32".parse().unwrap(),
|
||||
]),
|
||||
expected_result: false,
|
||||
},
|
||||
|
||||
// Test invalid URL (this case will fail during URL parsing)
|
||||
TestCase {
|
||||
remote_url: "invalid-url".to_string(),
|
||||
whitelist: None,
|
||||
expected_result: false,
|
||||
},
|
||||
|
||||
// Test URL without IP address (this case will fail during IP parsing)
|
||||
TestCase {
|
||||
remote_url: "tcp://localhost:15888".to_string(),
|
||||
whitelist: None,
|
||||
expected_result: false,
|
||||
},
|
||||
];
|
||||
|
||||
for case in test_cases {
|
||||
let hook = InstanceRpcServerHook::new(case.whitelist.clone());
|
||||
let tunnel_info = Some(crate::proto::common::TunnelInfo {
|
||||
remote_addr: Some(crate::proto::common::Url {
|
||||
url: case.remote_url.clone(),
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = hook.on_new_client(tunnel_info).await;
|
||||
if case.expected_result {
|
||||
assert!(result.is_ok(), "Expected success for remote_url:{},whitelist:{:?},but got: {:?}", case.remote_url, case.whitelist, result);
|
||||
} else {
|
||||
assert!(result.is_err(), "Expected failure for remote_url:{},whitelist:{:?},but got: {:?}", case.remote_url, case.whitelist, result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -527,6 +527,20 @@ impl NetworkConfig {
|
||||
.with_context(|| format!("failed to parse rpc portal port: {:?}", self.rpc_port))?,
|
||||
);
|
||||
|
||||
if self.rpc_portal_whitelists.is_empty() {
|
||||
cfg.set_rpc_portal_whitelist(None);
|
||||
} else {
|
||||
cfg.set_rpc_portal_whitelist(Some(
|
||||
self.rpc_portal_whitelists
|
||||
.iter()
|
||||
.map(|s| {
|
||||
s.parse()
|
||||
.with_context(|| format!("failed to parse rpc portal whitelist: {}", s))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
));
|
||||
}
|
||||
|
||||
if self.enable_vpn_portal.unwrap_or_default() {
|
||||
let cidr = format!(
|
||||
"{}/{}",
|
||||
|
@@ -21,7 +21,12 @@ use super::service_registry::ServiceRegistry;
|
||||
#[async_trait::async_trait]
|
||||
#[auto_impl::auto_impl(Arc, Box)]
|
||||
pub trait RpcServerHook: Send + Sync {
|
||||
async fn on_new_client(&self, _tunnel_info: Option<TunnelInfo>) {}
|
||||
async fn on_new_client(
|
||||
&self,
|
||||
tunnel_info: Option<TunnelInfo>,
|
||||
) -> Result<Option<TunnelInfo>, anyhow::Error> {
|
||||
Ok(tunnel_info)
|
||||
}
|
||||
async fn on_client_disconnected(&self, _tunnel_info: Option<TunnelInfo>) {}
|
||||
}
|
||||
|
||||
@@ -72,7 +77,13 @@ impl<L: TunnelListener + 'static> StandAloneServer<L> {
|
||||
let inflight_server = inflight.clone();
|
||||
let hook = hook.clone();
|
||||
|
||||
hook.on_new_client(tunnel_info.clone()).await;
|
||||
let tunnel_info = match hook.on_new_client(tunnel_info).await {
|
||||
Ok(info) => info,
|
||||
Err(e) => {
|
||||
tracing::warn!(?e, "standalone hook.on_new_client failed");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
inflight_server.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
tasks.lock().unwrap().spawn(async move {
|
||||
|
@@ -66,6 +66,8 @@ message NetworkConfig {
|
||||
|
||||
optional bool enable_magic_dns = 42;
|
||||
optional bool enable_private_mode = 43;
|
||||
|
||||
repeated string rpc_portal_whitelists = 44;
|
||||
}
|
||||
|
||||
message MyNodeInfo {
|
||||
|
Reference in New Issue
Block a user