From da63ce6d1d97bdd49548cc1816c7126de9a90895 Mon Sep 17 00:00:00 2001 From: Ryo Kawaguchi Date: Thu, 28 Aug 2025 04:47:45 +0900 Subject: [PATCH 1/4] Add disable-cidr and enable-cidr commands --- client/src/data_store.rs | 1 + client/src/main.rs | 57 ++++++++- docker-tests/run-docker-tests.sh | 42 +++++++ server/src/api/admin/cidr.rs | 191 ++++++++++++++++++++++++++++++- server/src/api/user.rs | 4 + server/src/db/association.rs | 1 + server/src/db/cidr.rs | 36 ++++-- server/src/db/peer.rs | 14 +++ server/src/initialize.rs | 2 + server/src/lib.rs | 56 ++++++++- server/src/main.rs | 28 ++++- server/src/test.rs | 1 + shared/src/prompts.rs | 46 +++++++- shared/src/types.rs | 13 +++ 14 files changed, 465 insertions(+), 27 deletions(-) diff --git a/client/src/data_store.rs b/client/src/data_store.rs index a8f93879..f7e869b0 100644 --- a/client/src/data_store.rs +++ b/client/src/data_store.rs @@ -171,6 +171,7 @@ mod tests { name: "cidr".to_string(), cidr: "10.0.0.0/24".parse().unwrap(), parent: None, + is_disabled: false, }, }] }); diff --git a/client/src/main.rs b/client/src/main.rs index cf6ae9cb..e7a55cf9 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -9,10 +9,10 @@ use innernet_shared::{ prompts, update_hosts_file, wg::{DeviceExt, PeerInfoExt}, AddCidrOpts, AddDeleteAssociationOpts, AddPeerOpts, Association, AssociationContents, Cidr, - CidrTree, DeleteCidrOpts, EnableDisablePeerOpts, Endpoint, EndpointContents, HostsOpt, - InstallOpts, Interface, IoErrorContext, ListenPortOpts, NatOpts, NetworkOpts, - OverrideEndpointOpts, Peer, RedeemContents, RenameCidrOpts, RenamePeerOpts, ServerCapabilities, - State, WrappedIoError, REDEEM_TRANSITION_WAIT, + CidrTree, DeleteCidrOpts, EnableDisableCidrOpts, EnableDisablePeerOpts, Endpoint, + EndpointContents, HostsOpt, InstallOpts, Interface, IoErrorContext, ListenPortOpts, NatOpts, + NetworkOpts, OverrideEndpointOpts, Peer, RedeemContents, RenameCidrOpts, RenamePeerOpts, + ServerCapabilities, State, WrappedIoError, REDEEM_TRANSITION_WAIT, }; use std::{ io, @@ -229,6 +229,22 @@ enum Command { sub_opts: EnableDisablePeerOpts, }, + /// Disable an enabled CIDR + DisableCidr { + interface: Interface, + + #[clap(flatten)] + sub_opts: EnableDisableCidrOpts, + }, + + /// Enable a disabled CIDR + EnableCidr { + interface: Interface, + + #[clap(flatten)] + sub_opts: EnableDisableCidrOpts, + }, + /// Add an association between CIDRs AddAssociation { interface: Interface, @@ -935,6 +951,31 @@ fn delete_association( Ok(()) } +fn enable_or_disable_cidr( + interface: &InterfaceName, + opts: &Opts, + enable: bool, + sub_opts: EnableDisableCidrOpts, +) -> Result<(), Error> { + let InterfaceConfig { server, .. } = + InterfaceConfig::from_interface(&opts.config_dir, interface)?; + let api = Api::new(&server); + log::info!("Fetching CIDRs."); + let cidrs: Vec = api.http("GET", "/admin/cidrs")?; + if let Some(cidr) = prompts::enable_or_disable_cidr(&cidrs[..], &sub_opts, enable)? { + let endpoint = if enable { "enable" } else { "disable" }; + let _: () = api.http("PUT", &format!("/admin/cidrs/{}/{}", cidr.id, endpoint))?; + log::info!( + "CIDR '{}' has been {}.", + cidr.name, + if enable { "enabled" } else { "disabled" } + ); + } else { + log::info!("exiting without enabling or disabling CIDR."); + } + Ok(()) +} + fn list_associations(interface: &InterfaceName, opts: &Opts) -> Result<(), Error> { let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(&opts.config_dir, interface)?; @@ -1345,6 +1386,14 @@ fn run(opts: &Opts) -> Result<(), Error> { interface, sub_opts, } => enable_or_disable_peer(&interface, opts, sub_opts, true)?, + Command::DisableCidr { + interface, + sub_opts, + } => enable_or_disable_cidr(&interface, opts, false, sub_opts)?, + Command::EnableCidr { + interface, + sub_opts, + } => enable_or_disable_cidr(&interface, opts, true, sub_opts)?, Command::AddAssociation { interface, sub_opts, diff --git a/docker-tests/run-docker-tests.sh b/docker-tests/run-docker-tests.sh index e3c82ee4..a3c90611 100755 --- a/docker-tests/run-docker-tests.sh +++ b/docker-tests/run-docker-tests.sh @@ -179,6 +179,48 @@ test_short_lived_invitation() { --yes } +test_cidr_disable_enable() { + info "Testing CIDR disable/enable functionality." + + # First, disable all peers in the robots CIDR + info "Disabling peer2 in robots CIDR." + cmd docker exec "$PEER1_CONTAINER" innernet \ + disable-peer evilcorp \ + --name "peer2" \ + --yes + + # Now disable the robots CIDR + info "Disabling robots CIDR." + cmd docker exec "$PEER1_CONTAINER" innernet \ + disable-cidr evilcorp \ + --name "robots" \ + --yes + + # Try to enable peer2 (should fail) + info "Trying to enable peer2 while CIDR is disabled (should fail)." + if docker exec "$PEER1_CONTAINER" innernet \ + enable-peer evilcorp \ + --name "peer2" \ + --yes 2>/dev/null; then + echo -e "\033[0;31mERROR: Enabling peer in disabled CIDR should have failed!\033[0m" 1>&2 + exit 1 + fi + + # Re-enable the CIDR + info "Re-enabling robots CIDR." + cmd docker exec "$PEER1_CONTAINER" innernet \ + enable-cidr evilcorp \ + --name "robots" \ + --yes + + # Now enable peer2 (should succeed) + info "Enabling peer2 after CIDR is enabled." + cmd docker exec "$PEER1_CONTAINER" innernet \ + enable-peer evilcorp \ + --name "peer2" \ + --yes +} + test_simultaneous_redemption() { info "Creating invitation for fourth and fifth peer from first peer." cmd docker exec "$PEER1_CONTAINER" innernet \ diff --git a/server/src/api/admin/cidr.rs b/server/src/api/admin/cidr.rs index 13af7249..da584474 100644 --- a/server/src/api/admin/cidr.rs +++ b/server/src/api/admin/cidr.rs @@ -13,18 +13,30 @@ pub async fn routes( mut components: VecDeque, session: Session, ) -> Result, ServerError> { - match (req.method(), components.pop_front().as_deref()) { - (&Method::GET, None) => handlers::list(session).await, - (&Method::POST, None) => { + match ( + req.method(), + components.pop_front().as_deref(), + components.pop_front().as_deref(), + ) { + (&Method::GET, None, None) => handlers::list(session).await, + (&Method::POST, None, None) => { let form = form_body(req).await?; handlers::create(form, session).await }, - (&Method::PUT, Some(id)) => { + (&Method::PUT, Some(id), None) => { let id: i64 = id.parse().map_err(|_| ServerError::NotFound)?; let form = form_body(req).await?; handlers::update(id, form, session).await }, - (&Method::DELETE, Some(id)) => { + (&Method::PUT, Some(id), Some("enable")) => { + let id: i64 = id.parse().map_err(|_| ServerError::NotFound)?; + handlers::enable(id, session).await + }, + (&Method::PUT, Some(id), Some("disable")) => { + let id: i64 = id.parse().map_err(|_| ServerError::NotFound)?; + handlers::disable(id, session).await + }, + (&Method::DELETE, Some(id), None) => { let id: i64 = id.parse().map_err(|_| ServerError::NotFound)?; handlers::delete(id, session).await }, @@ -73,6 +85,49 @@ mod handlers { status_response(StatusCode::NO_CONTENT) } + + pub async fn enable(id: i64, session: Session) -> Result, ServerError> { + let conn = session.context.db.lock(); + let cidr = DatabaseCidr::get(&conn, id)?; + + DatabaseCidr::from(cidr.clone()).update( + &conn, + CidrContents { + is_disabled: false, + ..cidr.contents.clone() + }, + )?; + + status_response(StatusCode::NO_CONTENT) + } + + pub async fn disable(id: i64, session: Session) -> Result, ServerError> { + use crate::DatabasePeer; + + let conn = session.context.db.lock(); + let cidr = DatabaseCidr::get(&conn, id)?; + let peers = DatabasePeer::list(&conn)?; + + // Check if any peers in this CIDR are enabled + let enabled_peers: Vec<_> = peers + .iter() + .filter(|p| p.cidr_id == id && !p.is_disabled) + .collect(); + + if !enabled_peers.is_empty() { + return Err(ServerError::InvalidQuery); + } + + DatabaseCidr::from(cidr.clone()).update( + &conn, + CidrContents { + is_disabled: true, + ..cidr.contents.clone() + }, + )?; + + status_response(StatusCode::NO_CONTENT) + } } #[cfg(test)] @@ -93,6 +148,7 @@ mod tests { name: "experimental".to_string(), cidr: test::EXPERIMENTAL_CIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }; let res = server @@ -119,6 +175,7 @@ mod tests { name: "experimental".to_string(), cidr: test::EXPERIMENTAL_CIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }; let res = server @@ -132,6 +189,7 @@ mod tests { name: "experimental".to_string(), cidr: test::EXPERIMENTAL_SUBCIDR.parse()?, parent: Some(cidr_res.id), + is_disabled: false, }; let res = server .form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &contents) @@ -149,6 +207,7 @@ mod tests { name: "experimental".to_string(), cidr: test::EXPERIMENTAL_CIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }; let res = server @@ -167,6 +226,7 @@ mod tests { name: "experimental".to_string(), cidr: test::EXPERIMENTAL_CIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }; let res = server .form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &contents) @@ -177,6 +237,7 @@ mod tests { name: "experimental".to_string(), cidr: test::EXPERIMENTAL_SUBCIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }; let res = server @@ -195,6 +256,7 @@ mod tests { name: "experimental".to_string(), cidr: "10.80.1.0/21".parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }; let res = server .form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &contents) @@ -214,6 +276,7 @@ mod tests { name: "experimental".to_string(), cidr: test::EXPERIMENTAL_CIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }, )?; let experimental_subcidr = DatabaseCidr::create( @@ -222,6 +285,7 @@ mod tests { name: "experimental subcidr".to_string(), cidr: test::EXPERIMENTAL_SUBCIDR.parse()?, parent: Some(experimental_cidr.id), + is_disabled: false, }, )?; @@ -267,6 +331,7 @@ mod tests { name: "experimental".to_string(), cidr: test::EXPERIMENTAL_CIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }, )?; @@ -292,4 +357,120 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_cidr_disable_with_enabled_peers() -> Result<(), Error> { + let server = test::Server::new()?; + + // Create a test CIDR + let cidr = CidrContents { + name: "test-cidr".to_string(), + cidr: test::EXPERIMENTAL_CIDR.parse()?, + parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, + }; + + let res = server + .form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &cidr) + .await; + assert!(res.status().is_success()); + let whole_body = hyper::body::aggregate(res).await?; + let test_cidr: Cidr = serde_json::from_reader(whole_body.reader())?; + + // Create an enabled peer in the CIDR + let peer = test::peer_contents( + "test-peer", + test::EXPERIMENT_SUBCIDR_PEER_IP, + test_cidr.id, + false, + )?; + DatabasePeer::create(&server.db().lock(), peer)?; + + // Try to disable the CIDR (should fail) + let res = server + .request( + test::ADMIN_PEER_IP, + "PUT", + &format!("/v1/admin/cidrs/{}/disable", test_cidr.id), + ) + .await; + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + + Ok(()) + } + + #[tokio::test] + async fn test_cidr_disable_with_disabled_peers() -> Result<(), Error> { + let server = test::Server::new()?; + + // Create a test CIDR + let cidr = CidrContents { + name: "test-cidr2".to_string(), + cidr: "10.80.3.0/24".parse()?, + parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, + }; + + let res = server + .form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &cidr) + .await; + assert!(res.status().is_success()); + let whole_body = hyper::body::aggregate(res).await?; + let test_cidr: Cidr = serde_json::from_reader(whole_body.reader())?; + + // Create a disabled peer in the CIDR + let mut peer = test::peer_contents("test-peer2", "10.80.3.1", test_cidr.id, false)?; + peer.is_disabled = true; + DatabasePeer::create(&server.db().lock(), peer)?; + + // Try to disable the CIDR (should succeed) + let res = server + .request( + test::ADMIN_PEER_IP, + "PUT", + &format!("/v1/admin/cidrs/{}/disable", test_cidr.id), + ) + .await; + assert_eq!(res.status(), StatusCode::NO_CONTENT); + + // Verify CIDR is disabled + let disabled_cidr = DatabaseCidr::get(&server.db().lock(), test_cidr.id)?; + assert!(disabled_cidr.is_disabled); + + Ok(()) + } + + #[tokio::test] + async fn test_cidr_enable() -> Result<(), Error> { + let server = test::Server::new()?; + + // Create a disabled CIDR + let cidr = CidrContents { + name: "test-cidr3".to_string(), + cidr: "10.80.4.0/24".parse()?, + parent: Some(test::ROOT_CIDR_ID), + is_disabled: true, + }; + + let db = server.db(); + let conn = db.lock(); + let test_cidr = DatabaseCidr::create(&conn, cidr)?; + drop(conn); + + // Enable the CIDR + let res = server + .request( + test::ADMIN_PEER_IP, + "PUT", + &format!("/v1/admin/cidrs/{}/enable", test_cidr.id), + ) + .await; + assert_eq!(res.status(), StatusCode::NO_CONTENT); + + // Verify CIDR is enabled + let enabled_cidr = DatabaseCidr::get(&server.db().lock(), test_cidr.id)?; + assert!(!enabled_cidr.is_disabled); + + Ok(()) + } } diff --git a/server/src/api/user.rs b/server/src/api/user.rs index f6d9d454..ad396cc9 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -278,6 +278,7 @@ mod tests { name: "experiment cidr".to_string(), cidr: test::EXPERIMENTAL_CIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }, )?; let subcidr = DatabaseCidr::create( @@ -286,6 +287,7 @@ mod tests { name: "experiment subcidr".to_string(), cidr: test::EXPERIMENTAL_SUBCIDR.parse()?, parent: Some(cidr.id), + is_disabled: false, }, )?; DatabasePeer::create( @@ -347,6 +349,7 @@ mod tests { name: "experimental".to_string(), cidr: test::EXPERIMENTAL_CIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }, )?; @@ -409,6 +412,7 @@ mod tests { name: "experimental".to_string(), cidr: test::EXPERIMENTAL_CIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }, )?; diff --git a/server/src/db/association.rs b/server/src/db/association.rs index 15b5a7e9..7e7dac91 100644 --- a/server/src/db/association.rs +++ b/server/src/db/association.rs @@ -193,6 +193,7 @@ mod tests { name: "experimental".to_string(), cidr: test::EXPERIMENTAL_CIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), + is_disabled: false, }; let res = server diff --git a/server/src/db/cidr.rs b/server/src/db/cidr.rs index f0b37f4c..1b88bee1 100644 --- a/server/src/db/cidr.rs +++ b/server/src/db/cidr.rs @@ -10,6 +10,7 @@ pub static CREATE_TABLE_SQL: &str = "CREATE TABLE cidrs ( ip TEXT NOT NULL, prefix INTEGER NOT NULL, parent INTEGER REFERENCES cidrs, + is_disabled INTEGER DEFAULT 0 NOT NULL, UNIQUE(ip, prefix), FOREIGN KEY (parent) REFERENCES cidrs (id) @@ -43,7 +44,9 @@ impl DerefMut for DatabaseCidr { impl DatabaseCidr { pub fn create(conn: &Connection, contents: CidrContents) -> Result { - let CidrContents { name, cidr, parent } = &contents; + let CidrContents { + name, cidr, parent, .. + } = &contents; log::debug!("creating {:?}", contents); @@ -99,13 +102,14 @@ impl DatabaseCidr { } conn.execute( - "INSERT INTO cidrs (name, ip, prefix, parent) - VALUES (?1, ?2, ?3, ?4)", + "INSERT INTO cidrs (name, ip, prefix, parent, is_disabled) + VALUES (?1, ?2, ?3, ?4, ?5)", params![ name, cidr.addr().to_string(), cidr.prefix_len() as i32, - parent + parent, + contents.is_disabled as i32 ], )?; let id = conn.last_insert_rowid(); @@ -113,16 +117,21 @@ impl DatabaseCidr { } /// Update self with new contents, validating them and updating the backend in the process. - /// Currently this only supports updating the name and ignores changes to any other field. + /// Currently this supports updating the name and is_disabled fields. pub fn update(&mut self, conn: &Connection, contents: CidrContents) -> Result<(), ServerError> { let new_contents = CidrContents { name: contents.name, + is_disabled: contents.is_disabled, ..self.contents.clone() }; conn.execute( - "UPDATE cidrs SET name = ?2 WHERE id = ?1", - params![self.id, &*new_contents.name,], + "UPDATE cidrs SET name = ?2, is_disabled = ?3 WHERE id = ?1", + params![ + self.id, + &*new_contents.name, + new_contents.is_disabled as i32 + ], )?; self.contents = new_contents; @@ -144,22 +153,29 @@ impl DatabaseCidr { .map_err(|_| rusqlite::Error::ExecuteReturnedResults)?; let cidr = IpNet::new(ip, prefix).map_err(|_| rusqlite::Error::ExecuteReturnedResults)?; let parent = row.get(4)?; + let is_disabled: i32 = row.get(5).unwrap_or(0); Ok(Cidr { id, - contents: CidrContents { name, cidr, parent }, + contents: CidrContents { + name, + cidr, + parent, + is_disabled: is_disabled != 0, + }, }) } pub fn get(conn: &Connection, id: i64) -> Result { Ok(conn.query_row( - "SELECT id, name, ip, prefix, parent FROM cidrs WHERE id = ?1", + "SELECT id, name, ip, prefix, parent, is_disabled FROM cidrs WHERE id = ?1", params![id], Self::from_row, )?) } pub fn list(conn: &Connection) -> Result, ServerError> { - let mut stmt = conn.prepare_cached("SELECT id, name, ip, prefix, parent FROM cidrs")?; + let mut stmt = + conn.prepare_cached("SELECT id, name, ip, prefix, parent, is_disabled FROM cidrs")?; let cidr_iter = stmt.query_map(params![], Self::from_row)?; Ok(cidr_iter.collect::, rusqlite::Error>>()?) diff --git a/server/src/db/peer.rs b/server/src/db/peer.rs index 158a3b47..5a98343f 100644 --- a/server/src/db/peer.rs +++ b/server/src/db/peer.rs @@ -99,6 +99,11 @@ impl DatabasePeer { return Err(ServerError::InvalidQuery); } + if cidr.is_disabled { + log::warn!("tried to add peer to disabled CIDR."); + return Err(ServerError::InvalidQuery); + } + if !cidr.cidr.is_assignable(ip) { println!( "Peer IP {} is not unicast assignable in CIDR {}", @@ -147,6 +152,15 @@ impl DatabasePeer { return Err(ServerError::InvalidQuery); } + // If trying to enable a peer, check if the CIDR is disabled + if !contents.is_disabled && self.contents.is_disabled { + let cidr = DatabaseCidr::get(conn, self.cidr_id)?; + if cidr.is_disabled { + log::warn!("tried to enable peer in disabled CIDR."); + return Err(ServerError::InvalidQuery); + } + } + // We will only allow updates of certain fields at this point, disregarding any requests // for changes of IP address, public key, or parent CIDR, for security reasons. // diff --git a/server/src/initialize.rs b/server/src/initialize.rs index cde9525e..ab6cd611 100644 --- a/server/src/initialize.rs +++ b/server/src/initialize.rs @@ -71,6 +71,7 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(), name: db_init_data.network_name.clone(), cidr: db_init_data.network_cidr, parent: None, + is_disabled: false, }, ) .map_err(|_| anyhow!("failed to create root CIDR"))?; @@ -81,6 +82,7 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(), name: SERVER_NAME.into(), cidr: db_init_data.server_cidr, parent: Some(root_cidr.id), + is_disabled: false, }, ) .map_err(|_| anyhow!("failed to create innernet-server CIDR"))?; diff --git a/server/src/lib.rs b/server/src/lib.rs index a0a30446..6a064906 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -4,9 +4,9 @@ use dialoguer::Confirm; use hyper::{http, server::conn::AddrStream, Body, Request, Response}; use indoc::printdoc; use innernet_shared::{ - get_local_addrs, update_hosts_file, AddCidrOpts, AddPeerOpts, DeleteCidrOpts, - EnableDisablePeerOpts, Endpoint, IoErrorContext, NetworkOpts, PeerContents, RenameCidrOpts, - RenamePeerOpts, INNERNET_PUBKEY_HEADER, + get_local_addrs, update_hosts_file, AddCidrOpts, AddPeerOpts, CidrContents, DeleteCidrOpts, + EnableDisableCidrOpts, EnableDisablePeerOpts, Endpoint, IoErrorContext, NetworkOpts, + PeerContents, RenameCidrOpts, RenamePeerOpts, INNERNET_PUBKEY_HEADER, }; use ipnet::IpNet; use parking_lot::{Mutex, RwLock}; @@ -354,6 +354,56 @@ pub fn delete_cidr( Ok(()) } +pub fn enable_or_disable_cidr( + interface: &InterfaceName, + conf: &ServerConfig, + enable: bool, + opts: EnableDisableCidrOpts, +) -> Result<(), Error> { + let conn = open_database_connection(interface, conf)?; + let cidrs = DatabaseCidr::list(&conn)?; + + if let Some(cidr) = prompts::enable_or_disable_cidr(&cidrs[..], &opts, enable)? { + // If disabling, check that all peers in the CIDR are disabled + if !enable { + let peers = DatabasePeer::list(&conn)?; + let enabled_peers: Vec<_> = peers + .iter() + .filter(|p| p.cidr_id == cidr.id && !p.is_disabled) + .collect(); + + if !enabled_peers.is_empty() { + eprintln!( + "Cannot disable CIDR '{}': {} peer(s) are still enabled.", + cidr.name, + enabled_peers.len() + ); + eprintln!("Please disable all peers in this CIDR first."); + return Ok(()); + } + } + + let db_cidr = DatabaseCidr::get(&conn, cidr.id)?; + DatabaseCidr::from(db_cidr.clone()).update( + &conn, + CidrContents { + is_disabled: !enable, + ..cidr.contents.clone() + }, + )?; + + println!( + "CIDR '{}' has been {}.", + cidr.name, + if enable { "enabled" } else { "disabled" } + ); + } else { + log::info!("exiting without enabling or disabling CIDR."); + } + + Ok(()) +} + pub fn uninstall( interface: &InterfaceName, conf: &ServerConfig, diff --git a/server/src/main.rs b/server/src/main.rs index e7518a2d..9c249632 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,13 +1,13 @@ use clap::{Parser, Subcommand}; use colored::*; use innernet_shared::{ - AddCidrOpts, AddPeerOpts, DeleteCidrOpts, EnableDisablePeerOpts, HostsOpt, NetworkOpts, - RenameCidrOpts, RenamePeerOpts, + AddCidrOpts, AddPeerOpts, DeleteCidrOpts, EnableDisableCidrOpts, EnableDisablePeerOpts, + HostsOpt, NetworkOpts, RenameCidrOpts, RenamePeerOpts, }; use std::{env, path::PathBuf}; use innernet_server::{ - add_cidr, add_peer, delete_cidr, enable_or_disable_peer, + add_cidr, add_peer, delete_cidr, enable_or_disable_cidr, enable_or_disable_peer, initialize::{self, InitializeOpts}, rename_cidr, rename_peer, serve, uninstall, ServerConfig, }; @@ -121,6 +121,22 @@ enum Command { args: DeleteCidrOpts, }, + /// Disable an enabled CIDR + DisableCidr { + interface: Interface, + + #[clap(flatten)] + args: EnableDisableCidrOpts, + }, + + /// Enable a disabled CIDR + EnableCidr { + interface: Interface, + + #[clap(flatten)] + args: EnableDisableCidrOpts, + }, + /// Generate shell completion scripts Completions { #[clap(value_enum)] @@ -168,6 +184,12 @@ async fn main() -> Result<(), Box> { Command::AddCidr { interface, args } => add_cidr(&interface, &conf, args)?, Command::RenameCidr { interface, args } => rename_cidr(&interface, &conf, args)?, Command::DeleteCidr { interface, args } => delete_cidr(&interface, &conf, args)?, + Command::DisableCidr { interface, args } => { + enable_or_disable_cidr(&interface, &conf, false, args)? + }, + Command::EnableCidr { interface, args } => { + enable_or_disable_cidr(&interface, &conf, true, args)? + }, Command::Completions { shell } => { use clap::CommandFactory; let mut app = Opts::command(); diff --git a/server/src/test.rs b/server/src/test.rs index 32f69884..e1f3da51 100644 --- a/server/src/test.rs +++ b/server/src/test.rs @@ -243,6 +243,7 @@ pub fn create_cidr(db: &Connection, name: &str, cidr_str: &str) -> Result Result Result, Error> { + let eligible_cidrs: Vec<_> = cidrs + .iter() + .filter(|cidr| enable && cidr.is_disabled || !enable && !cidr.is_disabled) + .collect(); + let cidr = if let Some(ref name) = args.name { + eligible_cidrs + .into_iter() + .find(|c| &c.name == name) + .ok_or_else(|| anyhow!("CIDR '{}' does not exist", name))? + } else { + let cidr_selection: Vec<_> = eligible_cidrs + .iter() + .map(|cidr| format!("{} ({})", &cidr.name, &cidr.cidr)) + .collect(); + let (index, _) = select( + &format!("CIDR to {}able", if enable { "en" } else { "dis" }), + &cidr_selection, + )?; + eligible_cidrs[index] + }; + Ok( + if args.yes + || confirm(&format!( + "{}able CIDR {}?", + if enable { "En" } else { "Dis" }, + cidr.name + ))? + { + Some(cidr.clone()) + } else { + None + }, + ) +} + pub fn enable_or_disable_peer( peers: &[Peer], args: &EnableDisablePeerOpts, diff --git a/shared/src/types.rs b/shared/src/types.rs index 823c617a..d94662bc 100644 --- a/shared/src/types.rs +++ b/shared/src/types.rs @@ -213,6 +213,8 @@ pub struct CidrContents { pub name: String, pub cidr: IpNet, pub parent: Option, + #[serde(default)] + pub is_disabled: bool, } impl Deref for CidrContents { @@ -433,6 +435,17 @@ pub struct AddDeleteAssociationOpts { pub yes: bool, } +#[derive(Debug, Clone, PartialEq, Eq, Args)] +pub struct EnableDisableCidrOpts { + /// The CIDR name (eg. 'engineers') + #[clap(long)] + pub name: Option, + + /// Bypass confirmation + #[clap(long)] + pub yes: bool, +} + #[derive(Debug, Clone, PartialEq, Eq, Args)] pub struct ListenPortOpts { /// The listen port you'd like to set for the interface From 1e3662b5b0473479fedfbe1e02a101d279d663da Mon Sep 17 00:00:00 2001 From: Ryo Kawaguchi Date: Thu, 28 Aug 2025 05:05:11 +0900 Subject: [PATCH 2/4] Fix clippy warnings --- server/src/api/admin/cidr.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/api/admin/cidr.rs b/server/src/api/admin/cidr.rs index da584474..c36d230f 100644 --- a/server/src/api/admin/cidr.rs +++ b/server/src/api/admin/cidr.rs @@ -452,10 +452,7 @@ mod tests { is_disabled: true, }; - let db = server.db(); - let conn = db.lock(); - let test_cidr = DatabaseCidr::create(&conn, cidr)?; - drop(conn); + let test_cidr = DatabaseCidr::create(&server.db().lock(), cidr)?; // Enable the CIDR let res = server From 7a975b2856d9ad9b4139d0925d0c356cdafdf540 Mon Sep 17 00:00:00 2001 From: Ryo Kawaguchi Date: Thu, 28 Aug 2025 05:11:48 +0900 Subject: [PATCH 3/4] Add necessary migration steps --- server/src/db/mod.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/src/db/mod.rs b/server/src/db/mod.rs index 9caa309d..cb9a21ef 100644 --- a/server/src/db/mod.rs +++ b/server/src/db/mod.rs @@ -9,8 +9,9 @@ use rusqlite::params; const INVITE_EXPIRATION_VERSION: usize = 1; const ENDPOINT_CANDIDATES_VERSION: usize = 2; +const CIDR_DISABLED_VERSION: usize = 3; -pub const CURRENT_VERSION: usize = ENDPOINT_CANDIDATES_VERSION; +pub const CURRENT_VERSION: usize = CIDR_DISABLED_VERSION; pub fn auto_migrate(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> { let old_version: usize = conn.pragma_query_value(None, "user_version", |r| r.get(0))?; @@ -27,6 +28,13 @@ pub fn auto_migrate(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> conn.execute("ALTER TABLE peers ADD COLUMN candidates TEXT", params![])?; } + if old_version < CIDR_DISABLED_VERSION { + conn.execute( + "ALTER TABLE cidrs ADD COLUMN is_disabled INTEGER DEFAULT 0 NOT NULL", + params![], + )?; + } + if old_version != CURRENT_VERSION { conn.pragma_update(None, "user_version", CURRENT_VERSION)?; log::info!( From 545a60e09a6a201a31accbf5ad3750460ecb5b0c Mon Sep 17 00:00:00 2001 From: Ryo Kawaguchi Date: Thu, 28 Aug 2025 07:25:24 +0900 Subject: [PATCH 4/4] Fix failing tests with test-v6 feature --- server/src/api/admin/cidr.rs | 93 ++++++++++++++---------------------- 1 file changed, 36 insertions(+), 57 deletions(-) diff --git a/server/src/api/admin/cidr.rs b/server/src/api/admin/cidr.rs index c36d230f..c21c7432 100644 --- a/server/src/api/admin/cidr.rs +++ b/server/src/api/admin/cidr.rs @@ -362,36 +362,13 @@ mod tests { async fn test_cidr_disable_with_enabled_peers() -> Result<(), Error> { let server = test::Server::new()?; - // Create a test CIDR - let cidr = CidrContents { - name: "test-cidr".to_string(), - cidr: test::EXPERIMENTAL_CIDR.parse()?, - parent: Some(test::ROOT_CIDR_ID), - is_disabled: false, - }; - - let res = server - .form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &cidr) - .await; - assert!(res.status().is_success()); - let whole_body = hyper::body::aggregate(res).await?; - let test_cidr: Cidr = serde_json::from_reader(whole_body.reader())?; - - // Create an enabled peer in the CIDR - let peer = test::peer_contents( - "test-peer", - test::EXPERIMENT_SUBCIDR_PEER_IP, - test_cidr.id, - false, - )?; - DatabasePeer::create(&server.db().lock(), peer)?; - - // Try to disable the CIDR (should fail) + // USER_CIDR already has enabled peers (USER1 and USER2) + // Try to disable USER_CIDR (should fail because peers are enabled) let res = server .request( test::ADMIN_PEER_IP, "PUT", - &format!("/v1/admin/cidrs/{}/disable", test_cidr.id), + &format!("/v1/admin/cidrs/{}/disable", test::USER_CIDR_ID), ) .await; assert_eq!(res.status(), StatusCode::BAD_REQUEST); @@ -403,69 +380,71 @@ mod tests { async fn test_cidr_disable_with_disabled_peers() -> Result<(), Error> { let server = test::Server::new()?; - // Create a test CIDR - let cidr = CidrContents { - name: "test-cidr2".to_string(), - cidr: "10.80.3.0/24".parse()?, - parent: Some(test::ROOT_CIDR_ID), - is_disabled: false, - }; - - let res = server - .form_request(test::ADMIN_PEER_IP, "POST", "/v1/admin/cidrs", &cidr) - .await; - assert!(res.status().is_success()); - let whole_body = hyper::body::aggregate(res).await?; - let test_cidr: Cidr = serde_json::from_reader(whole_body.reader())?; - - // Create a disabled peer in the CIDR - let mut peer = test::peer_contents("test-peer2", "10.80.3.1", test_cidr.id, false)?; - peer.is_disabled = true; - DatabasePeer::create(&server.db().lock(), peer)?; + // First disable USER1 and USER2 peers + { + let db = server.db(); + let conn = db.lock(); + DatabasePeer::disable(&conn, test::USER1_PEER_ID)?; + DatabasePeer::disable(&conn, test::USER2_PEER_ID)?; + } - // Try to disable the CIDR (should succeed) + // Try to disable USER_CIDR (should succeed now that all peers are disabled) let res = server .request( test::ADMIN_PEER_IP, "PUT", - &format!("/v1/admin/cidrs/{}/disable", test_cidr.id), + &format!("/v1/admin/cidrs/{}/disable", test::USER_CIDR_ID), ) .await; assert_eq!(res.status(), StatusCode::NO_CONTENT); // Verify CIDR is disabled - let disabled_cidr = DatabaseCidr::get(&server.db().lock(), test_cidr.id)?; + let disabled_cidr = DatabaseCidr::get(&server.db().lock(), test::USER_CIDR_ID)?; assert!(disabled_cidr.is_disabled); Ok(()) } #[tokio::test] - async fn test_cidr_enable() -> Result<(), Error> { + async fn test_cidr_disable_enable() -> Result<(), Error> { let server = test::Server::new()?; - // Create a disabled CIDR + // Create EXPERIMENTAL_CIDR let cidr = CidrContents { - name: "test-cidr3".to_string(), - cidr: "10.80.4.0/24".parse()?, + name: "experimental".to_string(), + cidr: test::EXPERIMENTAL_CIDR.parse()?, parent: Some(test::ROOT_CIDR_ID), - is_disabled: true, + is_disabled: false, }; - let test_cidr = DatabaseCidr::create(&server.db().lock(), cidr)?; + let experimental_cidr = DatabaseCidr::create(&server.db().lock(), cidr)?; + + // Test disable operation (should succeed since no peers exist) + let res = server + .request( + test::ADMIN_PEER_IP, + "PUT", + &format!("/v1/admin/cidrs/{}/disable", experimental_cidr.id), + ) + .await; + assert_eq!(res.status(), StatusCode::NO_CONTENT); + + // Verify CIDR is disabled + let disabled_cidr = DatabaseCidr::get(&server.db().lock(), experimental_cidr.id)?; + assert!(disabled_cidr.is_disabled); - // Enable the CIDR + // Test enable operation let res = server .request( test::ADMIN_PEER_IP, "PUT", - &format!("/v1/admin/cidrs/{}/enable", test_cidr.id), + &format!("/v1/admin/cidrs/{}/enable", experimental_cidr.id), ) .await; assert_eq!(res.status(), StatusCode::NO_CONTENT); // Verify CIDR is enabled - let enabled_cidr = DatabaseCidr::get(&server.db().lock(), test_cidr.id)?; + let enabled_cidr = DatabaseCidr::get(&server.db().lock(), experimental_cidr.id)?; assert!(!enabled_cidr.is_disabled); Ok(())