feat(remoteingress (edge/hub/protocol)): add dynamic port configuration: handshake, FRAME_CONFIG frames, and hot-reloadable listeners

This commit is contained in:
2026-02-18 18:20:53 +00:00
parent 072362a8e6
commit d869589663
8 changed files with 314 additions and 72 deletions

View File

@@ -37,6 +37,24 @@ impl Default for HubConfig {
pub struct AllowedEdge {
pub id: String,
pub secret: String,
#[serde(default)]
pub listen_ports: Vec<u16>,
pub stun_interval_secs: Option<u64>,
}
/// Handshake response sent to edge after authentication.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct HandshakeResponse {
listen_ports: Vec<u16>,
stun_interval_secs: u64,
}
/// Configuration update pushed to a connected edge at runtime.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EdgeConfigUpdate {
pub listen_ports: Vec<u16>,
}
/// Runtime status of a connected edge.
@@ -75,7 +93,7 @@ pub struct HubStatus {
/// The tunnel hub that accepts edge connections and demuxes streams to SmartProxy.
pub struct TunnelHub {
config: RwLock<HubConfig>,
allowed_edges: Arc<RwLock<HashMap<String, String>>>, // id -> secret
allowed_edges: Arc<RwLock<HashMap<String, AllowedEdge>>>,
connected_edges: Arc<Mutex<HashMap<String, ConnectedEdgeInfo>>>,
event_tx: mpsc::UnboundedSender<HubEvent>,
event_rx: Mutex<Option<mpsc::UnboundedReceiver<HubEvent>>>,
@@ -86,6 +104,7 @@ pub struct TunnelHub {
struct ConnectedEdgeInfo {
connected_at: u64,
active_streams: Arc<Mutex<HashMap<u32, mpsc::Sender<Vec<u8>>>>>,
config_tx: mpsc::Sender<EdgeConfigUpdate>,
}
impl TunnelHub {
@@ -108,12 +127,35 @@ impl TunnelHub {
}
/// Update the list of allowed edges.
/// For any currently-connected edge whose ports changed, push a config update.
pub async fn update_allowed_edges(&self, edges: Vec<AllowedEdge>) {
let mut map = self.allowed_edges.write().await;
map.clear();
for edge in edges {
map.insert(edge.id, edge.secret);
// Build new map
let mut new_map = HashMap::new();
for edge in &edges {
new_map.insert(edge.id.clone(), edge.clone());
}
// Push config updates to connected edges whose ports changed
let connected = self.connected_edges.lock().await;
for edge in &edges {
if let Some(info) = connected.get(&edge.id) {
// Check if ports changed compared to old config
let ports_changed = match map.get(&edge.id) {
Some(old) => old.listen_ports != edge.listen_ports,
None => true, // newly allowed edge that's already connected
};
if ports_changed {
let update = EdgeConfigUpdate {
listen_ports: edge.listen_ports.clone(),
};
let _ = info.config_tx.try_send(update);
}
}
}
*map = new_map;
}
/// Get the current hub status.
@@ -208,13 +250,13 @@ impl TunnelHub {
async fn handle_edge_connection(
stream: TcpStream,
acceptor: TlsAcceptor,
allowed: Arc<RwLock<HashMap<String, String>>>,
allowed: Arc<RwLock<HashMap<String, AllowedEdge>>>,
connected: Arc<Mutex<HashMap<String, ConnectedEdgeInfo>>>,
event_tx: mpsc::UnboundedSender<HubEvent>,
target_host: String,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let tls_stream = acceptor.accept(stream).await?;
let (read_half, write_half) = tokio::io::split(tls_stream);
let (read_half, mut write_half) = tokio::io::split(tls_stream);
let mut buf_reader = BufReader::new(read_half);
// Read auth line: "EDGE <edgeId> <secret>\n"
@@ -230,26 +272,36 @@ async fn handle_edge_connection(
let edge_id = parts[1].to_string();
let secret = parts[2];
// Verify credentials
{
// Verify credentials and extract edge config
let (listen_ports, stun_interval_secs) = {
let edges = allowed.read().await;
match edges.get(&edge_id) {
Some(expected) => {
if !constant_time_eq(secret.as_bytes(), expected.as_bytes()) {
Some(edge) => {
if !constant_time_eq(secret.as_bytes(), edge.secret.as_bytes()) {
return Err(format!("invalid secret for edge {}", edge_id).into());
}
(edge.listen_ports.clone(), edge.stun_interval_secs.unwrap_or(300))
}
None => {
return Err(format!("unknown edge {}", edge_id).into());
}
}
}
};
log::info!("Edge {} authenticated", edge_id);
let _ = event_tx.send(HubEvent::EdgeConnected {
edge_id: edge_id.clone(),
});
// Send handshake response with initial config before frame protocol begins
let handshake = HandshakeResponse {
listen_ports: listen_ports.clone(),
stun_interval_secs,
};
let mut handshake_json = serde_json::to_string(&handshake)?;
handshake_json.push('\n');
write_half.write_all(handshake_json.as_bytes()).await?;
// Track this edge
let streams: Arc<Mutex<HashMap<u32, mpsc::Sender<Vec<u8>>>>> =
Arc::new(Mutex::new(HashMap::new()));
@@ -258,6 +310,9 @@ async fn handle_edge_connection(
.unwrap_or_default()
.as_secs();
// Create config update channel
let (config_tx, mut config_rx) = mpsc::channel::<EdgeConfigUpdate>(16);
{
let mut edges = connected.lock().await;
edges.insert(
@@ -265,6 +320,7 @@ async fn handle_edge_connection(
ConnectedEdgeInfo {
connected_at: now,
active_streams: streams.clone(),
config_tx,
},
);
}
@@ -272,6 +328,23 @@ async fn handle_edge_connection(
// Shared writer for sending frames back to edge
let write_half = Arc::new(Mutex::new(write_half));
// Spawn task to forward config updates as FRAME_CONFIG frames
let config_writer = write_half.clone();
let config_edge_id = edge_id.clone();
let config_handle = tokio::spawn(async move {
while let Some(update) = config_rx.recv().await {
if let Ok(payload) = serde_json::to_vec(&update) {
let frame = encode_frame(0, FRAME_CONFIG, &payload);
let mut w = config_writer.lock().await;
if w.write_all(&frame).await.is_err() {
log::error!("Failed to send config update to edge {}", config_edge_id);
break;
}
log::info!("Sent config update to edge {}: ports {:?}", config_edge_id, update.listen_ports);
}
}
});
// Frame reading loop
let mut frame_reader = FrameReader::new(buf_reader);
@@ -398,6 +471,7 @@ async fn handle_edge_connection(
}
// Cleanup
config_handle.abort();
{
let mut edges = connected.lock().await;
edges.remove(&edge_id);