Repository: tonarino/innernet (language: rust)

Issue:
Feature Request - Ability to rename a peer
Is there a current way to rename a peer? Or if not, that would be a feature we'd (where we're testing `innernet` for use at work) find useful. 

We understand the security implications of having immutable publickey/private IP tuples, which is fine. But we've run into a few instances were we need to move a peer and end up with cidrs full of peers like `foo`, `foo_take2`, `foo_take3`, etc. It'd be nice to be able to disable `foo`, then rename to `diasabled` (or whatever), and then create a new `foo` peer. This way we don't have cidrs full of confusing names (since we could later do something like filter by disabled, or even just `grep -v`, etc.)

I could see how some could say a DNS hostname suddenly changing private IP/public key could be an issue, however I'd argue that since the new public key/private IP tuple is valid on the wireguard network, and has a valid association you should be assuming it's safe anyways.


Discussion / hints:
You could always modify the sqlite database to rename them and then have all peers run `inn fetch` as a temporary workaround.
For sure, and that workaround is fine for us at the moment. I just wanted to make sure I wasn't missing some official method of doing that, and if not to request (as a low priority) that idea be added to the CLI.
As a matter of fact, `PUT /v1/admin/peers/<id>` does already support peer renaming (you can see all the allowed fields to edit on that endpoint at https://github.com/tonarino/innernet/blob/main/server/src/db/peer.rs#L133), so no server changes should be required to add that command to the client.

In the mean time you can either construct your own HTTP request to the server or edit the SQLite database on the server side directly. They should be roughly the same, with a slight preference for the HTTP API since it does validation of the hostname string that the SQLite checks won't do.
I don't mind putting in a PR for this if there is appetite to enable this feature through the CLI.
@kbknapp that would be great, I think it's a useful feature to have.

Fix patch:
diff --git a/client/src/main.rs b/client/src/main.rs
index d79cfd58..e59deb51 100644
--- a/client/src/main.rs
+++ b/client/src/main.rs
@@ -5,8 +5,8 @@ use indoc::eprintdoc;
 use shared::{
     interface_config::InterfaceConfig, prompts, AddAssociationOpts, AddCidrOpts, AddPeerOpts,
     Association, AssociationContents, Cidr, CidrTree, DeleteCidrOpts, EndpointContents,
-    InstallOpts, Interface, IoErrorContext, NetworkOpt, Peer, RedeemContents, State,
-    CLIENT_CONFIG_DIR, REDEEM_TRANSITION_WAIT,
+    InstallOpts, Interface, IoErrorContext, NetworkOpt, Peer, RedeemContents, RenamePeerOpts,
+    State, CLIENT_CONFIG_DIR, REDEEM_TRANSITION_WAIT,
 };
 use std::{
     fmt,
@@ -140,6 +140,19 @@ enum Command {
         opts: AddPeerOpts,
     },
 
+    /// Rename a peer.
+    ///
+    /// By default, you'll be prompted interactively to select a peer, but you can
+    /// also specify all the options in the command, eg:
+    ///
+    /// --name "person" --new-name "human"
+    RenamePeer {
+        interface: Interface,
+
+        #[structopt(flatten)]
+        opts: RenamePeerOpts,
+    },
+
     /// Add a new CIDR.
     AddCidr {
         interface: Interface,
@@ -612,6 +625,32 @@ fn add_peer(interface: &InterfaceName, opts: AddPeerOpts) -> Result<(), Error> {
     Ok(())
 }
 
+fn rename_peer(interface: &InterfaceName, opts: RenamePeerOpts) -> Result<(), Error> {
+    let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
+    let api = Api::new(&server);
+
+    log::info!("Fetching peers");
+    let peers: Vec<Peer> = api.http("GET", "/admin/peers")?;
+
+    if let Some((peer_request, old_name)) = prompts::rename_peer(&peers, &opts)? {
+        log::info!("Renaming peer...");
+
+        let id = peers
+            .iter()
+            .filter(|p| p.name == old_name)
+            .map(|p| p.id)
+            .next()
+            .ok_or_else(|| "Peer not found.")?;
+
+        let _ = api.http_form("PUT", &format!("/admin/peers/{}", id), peer_request)?;
+        log::info!("Peer renamed.");
+    } else {
+        log::info!("exited without renaming peer.");
+    }
+
+    Ok(())
+}
+
 fn enable_or_disable_peer(interface: &InterfaceName, enable: bool) -> Result<(), Error> {
     let InterfaceConfig { server, .. } = InterfaceConfig::from_interface(interface)?;
     let api = Api::new(&server);
@@ -982,6 +1021,7 @@ fn run(opt: Opts) -> Result<(), Error> {
         Command::Down { interface } => wg::down(&interface, opt.network.backend)?,
         Command::Uninstall { interface } => uninstall(&interface, opt.network)?,
         Command::AddPeer { interface, opts } => add_peer(&interface, opts)?,
+        Command::RenamePeer { interface, opts } => rename_peer(&interface, opts)?,
         Command::AddCidr { interface, opts } => add_cidr(&interface, opts)?,
         Command::DeleteCidr { interface, opts } => delete_cidr(&interface, opts)?,
         Command::DisablePeer { interface } => enable_or_disable_peer(&interface, false)?,
diff --git a/server/src/main.rs b/server/src/main.rs
index acdf0287..68fff7f3 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -7,7 +7,8 @@ use parking_lot::{Mutex, RwLock};
 use rusqlite::Connection;
 use serde::{Deserialize, Serialize};
 use shared::{
-    AddCidrOpts, AddPeerOpts, DeleteCidrOpts, IoErrorContext, NetworkOpt, INNERNET_PUBKEY_HEADER,
+    AddCidrOpts, AddPeerOpts, DeleteCidrOpts, IoErrorContext, NetworkOpt, RenamePeerOpts,
+    INNERNET_PUBKEY_HEADER,
 };
 use std::{
     collections::{HashMap, VecDeque},
@@ -80,6 +81,14 @@ enum Command {
         args: AddPeerOpts,
     },
 
+    /// Rename an existing peer.
+    RenamePeer {
+        interface: Interface,
+
+        #[structopt(flatten)]
+        args: RenamePeerOpts,
+    },
+
     /// Add a new CIDR to an existing network.
     AddCidr {
         interface: Interface,
@@ -230,6 +239,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
             network: routing,
         } => serve(*interface, &conf, routing).await?,
         Command::AddPeer { interface, args } => add_peer(&interface, &conf, args, opt.network)?,
+        Command::RenamePeer { interface, args } => rename_peer(&interface, &conf, args)?,
         Command::AddCidr { interface, args } => add_cidr(&interface, &conf, args)?,
         Command::DeleteCidr { interface, args } => delete_cidr(&interface, &conf, args)?,
     }
@@ -301,6 +311,30 @@ fn add_peer(
     Ok(())
 }
 
+fn rename_peer(
+    interface: &InterfaceName,
+    conf: &ServerConfig,
+    opts: RenamePeerOpts,
+) -> Result<(), Error> {
+    let conn = open_database_connection(interface, conf)?;
+    let peers = DatabasePeer::list(&conn)?
+        .into_iter()
+        .map(|dp| dp.inner)
+        .collect::<Vec<_>>();
+
+    if let Some((peer_request, old_name)) = shared::prompts::rename_peer(&peers, &opts)? {
+        let mut db_peer = DatabasePeer::list(&conn)?
+            .into_iter()
+            .find(|p| p.name == old_name)
+            .ok_or_else(|| "Peer not found.")?;
+        let _peer = db_peer.update(&conn, peer_request)?;
+    } else {
+        println!("exited without creating peer.");
+    }
+
+    Ok(())
+}
+
 fn add_cidr(
     interface: &InterfaceName,
     conf: &ServerConfig,
diff --git a/shared/src/prompts.rs b/shared/src/prompts.rs
index 1e8a3200..7120d451 100644
--- a/shared/src/prompts.rs
+++ b/shared/src/prompts.rs
@@ -1,7 +1,7 @@
 use crate::{
     interface_config::{InterfaceConfig, InterfaceInfo, ServerInfo},
     AddCidrOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, DeleteCidrOpts, Endpoint,
-    Error, Peer, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
+    Error, Hostname, Peer, PeerContents, RenamePeerOpts, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
 };
 use colored::*;
 use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
@@ -274,6 +274,63 @@ pub fn add_peer(
     )
 }
 
+/// Bring up a prompt to create a new peer. Returns the peer request.
+pub fn rename_peer(
+    peers: &[Peer],
+    args: &RenamePeerOpts,
+) -> Result<Option<(PeerContents, Hostname)>, Error> {
+    let eligible_peers = peers
+        .iter()
+        .filter(|p| &*p.name != "innernet-server")
+        .collect::<Vec<_>>();
+    let old_peer = if let Some(ref name) = args.name {
+        eligible_peers
+            .into_iter()
+            .find(|p| &p.name == name)
+            .ok_or_else(|| format!("Peer '{}' does not exist", name))?
+            .clone()
+    } else {
+        let peer_index = Select::with_theme(&*THEME)
+            .with_prompt("Peer to rename")
+            .items(
+                &eligible_peers
+                    .iter()
+                    .map(|ep| ep.name.clone())
+                    .collect::<Vec<_>>(),
+            )
+            .interact()?;
+        eligible_peers[peer_index].clone()
+    };
+    let old_name = old_peer.name.clone();
+    let new_name = if let Some(ref name) = args.new_name {
+        name.clone()
+    } else {
+        Input::with_theme(&*THEME)
+            .with_prompt("New Name")
+            .interact()?
+    };
+
+    let mut new_peer = old_peer;
+    new_peer.contents.name = new_name.clone();
+
+    Ok(
+        if args.yes
+            || Confirm::with_theme(&*THEME)
+                .with_prompt(&format!(
+                    "Rename peer {} to {}?",
+                    old_name.yellow(),
+                    new_name.yellow()
+                ))
+                .default(false)
+                .interact()?
+        {
+            Some((new_peer.contents, old_name))
+        } else {
+            None
+        },
+    )
+}
+
 /// Presents a selection and confirmation of eligible peers for either disabling or enabling,
 /// and returns back the ID of the selected peer.
 pub fn enable_or_disable_peer(peers: &[Peer], enable: bool) -> Result<Option<Peer>, Error> {
diff --git a/shared/src/types.rs b/shared/src/types.rs
index 19c111a6..4c199095 100644
--- a/shared/src/types.rs
+++ b/shared/src/types.rs
@@ -318,6 +318,21 @@ pub struct AddPeerOpts {
     pub invite_expires: Option<Timestring>,
 }
 
+#[derive(Debug, Clone, PartialEq, StructOpt)]
+pub struct RenamePeerOpts {
+    /// Name of peer to rename
+    #[structopt(long)]
+    pub name: Option<Hostname>,
+
+    /// The new name of the peer
+    #[structopt(long)]
+    pub new_name: Option<Hostname>,
+
+    /// Bypass confirmation
+    #[structopt(long)]
+    pub yes: bool,
+}
+
 #[derive(Debug, Clone, PartialEq, StructOpt)]
 pub struct AddCidrOpts {
     /// The CIDR name (eg. "engineers")


Test patch: