Added RPC portal whitelist function, allowing only local access by default to enhance security (#929)

This commit is contained in:
Mg Pig
2025-06-07 22:05:47 +08:00
committed by GitHub
parent 707963c0d9
commit 20a6025075
12 changed files with 260 additions and 8 deletions

View File

@@ -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);
}
}
}
}