mirror of
https://github.com/EasyTier/EasyTier.git
synced 2025-10-10 19:20:30 +08:00
Added RPC portal whitelist function, allowing only local access by default to enhance security (#929)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user