aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Garrelou <simon@sixfoisneuf.fr>2023-04-25 20:50:44 +0200
committerSimon Garrelou <simon@sixfoisneuf.fr>2023-04-27 16:00:08 +0200
commit9de54b89828bf40edcae28ae3477eeab19ca3169 (patch)
tree778383b89ab4e837258dd9f6a8d8aadc614d0304
parenta1b23498ddb6f6d0129528bd295dfb490be723da (diff)
downloadwgmgr-9de54b89828bf40edcae28ae3477eeab19ca3169.tar.gz
wgmgr-9de54b89828bf40edcae28ae3477eeab19ca3169.zip
Use anyhow, +add +config
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml1
-rw-r--r--src/main.rs161
-rw-r--r--src/wg.rs3
-rw-r--r--src/wg/config.rs56
-rw-r--r--src/wg/error.rs31
-rw-r--r--src/wg/peer.rs104
8 files changed, 297 insertions, 68 deletions
diff --git a/.gitignore b/.gitignore
index ea8c4bf..55e5a90 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
1/target 1/target
2private_keys
3*.conf \ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 7e505bb..6c862da 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -52,6 +52,12 @@ dependencies = [
52] 52]
53 53
54[[package]] 54[[package]]
55name = "anyhow"
56version = "1.0.70"
57source = "registry+https://github.com/rust-lang/crates.io-index"
58checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
59
60[[package]]
55name = "bitflags" 61name = "bitflags"
56version = "1.3.2" 62version = "1.3.2"
57source = "registry+https://github.com/rust-lang/crates.io-index" 63source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -211,6 +217,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
211name = "wgmgr" 217name = "wgmgr"
212version = "0.1.0" 218version = "0.1.0"
213dependencies = [ 219dependencies = [
220 "anyhow",
214 "clap", 221 "clap",
215 "ipnetwork", 222 "ipnetwork",
216] 223]
diff --git a/Cargo.toml b/Cargo.toml
index 48f40cd..8b25ae0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,5 +6,6 @@ edition = "2021"
6# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 6# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 7
8[dependencies] 8[dependencies]
9anyhow = "1.0.70"
9clap = { version = "4.2.2", features = ["cargo"] } 10clap = { version = "4.2.2", features = ["cargo"] }
10ipnetwork = "0.20.0" 11ipnetwork = "0.20.0"
diff --git a/src/main.rs b/src/main.rs
index 6c4787e..4fdfddd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,19 +1,35 @@
1use std::ffi::OsStr;
2use std::fs::{read_to_string, self, File};
3use std::net::Ipv4Addr;
4use std::str::FromStr;
1use std::{env, process::exit}; 5use std::{env, process::exit};
2use std::error::Error; 6use std::io::Write;
3 7
4use clap::command; 8use anyhow::{Result, anyhow};
9use clap::{command, ArgMatches};
5use clap::Command; 10use clap::Command;
6use clap::arg; 11use clap::arg;
12use wg::config::WireguardConfig;
7 13
8mod wg; 14mod wg;
9 15
10fn main() { 16fn main() {
11 let matches = command!() 17 let matches = command!()
18 .arg(
19 arg!(
20 -c --config <CONFIG> "Configuration file name"
21 )
22 .required(false)
23 )
12 .subcommand_required(true) 24 .subcommand_required(true)
25
26 // "ls"
13 .subcommand( 27 .subcommand(
14 Command::new("ls") 28 Command::new("ls")
15 .about("List known clients") 29 .about("List known clients")
16 ) 30 )
31
32 // "config"
17 .subcommand( 33 .subcommand(
18 Command::new("config") 34 Command::new("config")
19 .about("Generate the configuration file for a client") 35 .about("Generate the configuration file for a client")
@@ -25,14 +41,35 @@ fn main() {
25 ) 41 )
26 .arg(arg!(<PEER> "Name of the peer")) 42 .arg(arg!(<PEER> "Name of the peer"))
27 ) 43 )
44
45 // "add"
46 .subcommand(
47 Command::new("add")
48 .about("Add a new client to your VPN")
49 .arg(
50 arg!(
51 <NAME> "Name of the new client"
52 )
53 .required(true)
54 )
55 .arg(
56 arg!(
57 -i --ip <IP> "IP address (auto-assigned)"
58 )
59 .required(false)
60 )
61 )
28 .get_matches(); 62 .get_matches();
29 63
30 let conf_path = match env::var("WG_CONF") { 64 let conf_path = match find_config_file(&matches) {
31 Ok(s) => s, 65 Ok(s) => s,
32 Err(_) => String::from("/etc/wireguard/wg0.conf") 66 Err(e) => {
67 eprintln!("Configuration file error: {}", e);
68 exit(1);
69 }
33 }; 70 };
34 71
35 let conf = match wg::config::WireguardConfig::new(&conf_path) { 72 let mut conf = match wg::config::WireguardConfig::new(&conf_path) {
36 Ok(c) => c, 73 Ok(c) => c,
37 Err(e) => { 74 Err(e) => {
38 eprintln!("Error loading the configuration file '{}'", conf_path); 75 eprintln!("Error loading the configuration file '{}'", conf_path);
@@ -67,6 +104,17 @@ fn main() {
67 }, 104 },
68 None => {} 105 None => {}
69 } 106 }
107 },
108
109 Some(("add", args)) => {
110 let new_name = args.get_one::<String>("NAME").unwrap().to_string();
111 if let Err(e) = do_add(&mut conf, conf_path, &new_name, args.get_one::<String>("ip")) {
112 eprintln!("Error adding peer: {}", e);
113 exit(1);
114 }
115 else {
116 println!("Peer '{}' added successfully. Don't forget to restart your WireGuard server.", new_name);
117 }
70 } 118 }
71 119
72 _ => { 120 _ => {
@@ -76,6 +124,63 @@ fn main() {
76 124
77} 125}
78 126
127fn find_config_file(matches: &ArgMatches) -> Result<String> {
128 // Top priority goes to the command-line argument
129 match matches.get_one::<String>("config") {
130 Some(s) => {
131 if s.starts_with("/") {
132 return Ok(s.to_string())
133 }
134 else {
135 return Ok(format!("/etc/wireguard/{}", s))
136 }
137 },
138 None => {}
139 }
140
141 // Then, if the environment variable exists, we take it
142 match env::var("WG_CONF") {
143 Ok(s) => return Ok(s),
144 Err(_) => {}
145 };
146
147 // Otherwise, we will first try a simple "wg0.conf"
148 match read_to_string("/etc/wireguard/wg0.conf") {
149 Ok(_) => return Ok(String::from_str("/etc/wireguard/wg0.conf").unwrap()),
150 Err(_) => {}
151 }
152
153 // Finally, we can try to see if there is a single ".conf" file in /etc/wireguard
154 match fs::read_dir("/etc/wireguard/") {
155 Ok(d) => {
156 let conf_files: Vec<fs::DirEntry> = d.filter(|e| {
157 if let Ok(e) = e {
158 if e.file_type().unwrap().is_dir() {
159 return false;
160 }
161
162 match e.path().extension().and_then(OsStr::to_str) {
163 Some("conf") => { return true} ,
164 _ => {return false},
165 }
166 }
167
168 return false;
169 }).map(|e| {
170 return e.unwrap();
171 }).collect();
172
173 if conf_files.len() == 1 {
174 return Ok(String::from_str(conf_files[0].path().to_str().unwrap()).unwrap());
175 }
176 else {
177 return Err(anyhow!("Could not determine the path to your WireGuard configuration file. Set the WG_CONF environment variable, or pass the '--config' parameter."))
178 }
179 },
180 Err(e) => return Err(anyhow!("Error listing /etc/wireguard/: {}", e))
181 }
182}
183
79fn do_list(conf: &wg::config::WireguardConfig) { 184fn do_list(conf: &wg::config::WireguardConfig) {
80 let mut max_length = 0; 185 let mut max_length = 0;
81 for p in conf.peers.iter() { 186 for p in conf.peers.iter() {
@@ -89,30 +194,44 @@ fn do_list(conf: &wg::config::WireguardConfig) {
89 } 194 }
90} 195}
91 196
92fn do_config(conf: &wg::config::WireguardConfig, peer_name: String, is_full: bool) -> Result<(), Box<dyn Error>> { 197fn do_config(conf: &wg::config::WireguardConfig, peer_name: String, is_full: bool) -> Result<()> {
93 let peer = match conf.peers.iter().filter(|p| { p.name == peer_name }).nth(0) { 198 let peer = match conf.get_peer(peer_name.as_str()) {
94 Some(p) => p, 199 Some(p) => p,
95 None => { 200 None => {
96 eprintln!("No such peer: {}", peer_name); 201 return Err(anyhow!("No such peer: {}", peer_name));
97 exit(1);
98 } 202 }
99 }; 203 };
100 204
101 println!("[Interface]"); 205 println!("{}", peer.gen_config(conf, is_full)?);
102 println!("PrivateKey = {}", peer.private_key()?);
103 println!("Address = {}/32", peer.ip);
104 println!("DNS = TODO\n");
105 206
106 println!("[Peer]"); 207 Ok(())
107 println!("PublicKey = TODO"); 208}
108 209
109 let allowed_ips = match is_full { 210fn do_add(conf: &mut WireguardConfig, conf_path: String, peer_name: &String, ip: Option<&String>) -> Result<()> {
110 true => String::from("0.0.0.0/0"), 211 let ip = match ip {
111 false => conf.network.to_string() 212 Some(s) => {
213 Ipv4Addr::from_str(s.as_str())?
214 },
215 None => {
216 match conf.next_free_ip() {
217 Ok(i) => i,
218 Err(e) => {
219 return Err(e);
220 }
221 }
222 }
112 }; 223 };
113 println!("AllowedIPs = {}", allowed_ips); 224
114 println!("Endpoint = TODO"); 225 match conf.get_peer(peer_name.as_str()) {
115 println!("PersistentKeepAlive = 25"); 226 Some(_) => { return Err(anyhow!("There is already a peer named {}", peer_name)); },
227 None => {}
228 }
229
230 let p = wg::peer::Peer::new(peer_name.clone(), ip);
231 conf.peers.push(p);
232
233 let mut f = File::create(conf_path)?;
234 write!(f, "{}", conf.gen_config()?)?;
116 235
117 Ok(()) 236 Ok(())
118} \ No newline at end of file 237} \ No newline at end of file
diff --git a/src/wg.rs b/src/wg.rs
index 39cfc0c..a7d9b85 100644
--- a/src/wg.rs
+++ b/src/wg.rs
@@ -1,3 +1,2 @@
1mod error;
2pub mod config; 1pub mod config;
3mod peer; \ No newline at end of file 2pub mod peer; \ No newline at end of file
diff --git a/src/wg/config.rs b/src/wg/config.rs
index 03d2ce0..4a07287 100644
--- a/src/wg/config.rs
+++ b/src/wg/config.rs
@@ -1,9 +1,10 @@
1use std::str::FromStr; 1use std::str::FromStr;
2use std::{net::Ipv4Addr, fs, error::Error}; 2use std::{net::Ipv4Addr, fs};
3use std::fmt::Write;
3 4
5use anyhow::{Result, anyhow};
4use ipnetwork::Ipv4Network; 6use ipnetwork::Ipv4Network;
5 7
6use crate::wg::error::WgMgrError;
7use crate::wg::peer::Peer; 8use crate::wg::peer::Peer;
8 9
9#[derive(Debug)] 10#[derive(Debug)]
@@ -19,7 +20,7 @@ pub struct WireguardConfig {
19 20
20 21
21impl WireguardConfig { 22impl WireguardConfig {
22 pub fn new(config_path: &str) -> Result<WireguardConfig, Box<dyn Error>> { 23 pub fn new(config_path: &str) -> Result<WireguardConfig> {
23 let f = fs::read_to_string(config_path)?; 24 let f = fs::read_to_string(config_path)?;
24 25
25 let mut conf = WireguardConfig{ 26 let mut conf = WireguardConfig{
@@ -96,8 +97,7 @@ impl WireguardConfig {
96 let ip = Ipv4Network::from_str(addr)?; 97 let ip = Ipv4Network::from_str(addr)?;
97 98
98 if ip.prefix() != 32 { 99 if ip.prefix() != 32 {
99 let msg = format!("Peer '{}' has invalid net range {}", current_peer.name, ip.prefix()); 100 return Err(anyhow!("Peer '{}' has invalid net range {}", current_peer.name, ip.prefix()));
100 return Err(Box::new(WgMgrError::new(msg)));
101 } 101 }
102 102
103 current_peer.ip = ip.ip(); 103 current_peer.ip = ip.ip();
@@ -118,7 +118,7 @@ impl WireguardConfig {
118 } 118 }
119 119
120 120
121 pub fn next_free_ip(&self) -> Result<Ipv4Addr, WgMgrError> { 121 pub fn next_free_ip(&self) -> Result<Ipv4Addr> {
122 let mut iter = self.network.iter(); 122 let mut iter = self.network.iter();
123 iter.next(); // Skip the first IP (identification) 123 iter.next(); // Skip the first IP (identification)
124 124
@@ -140,19 +140,57 @@ impl WireguardConfig {
140 } 140 }
141 } 141 }
142 142
143 Err(WgMgrError::new(String::from("No more free IP addresses"))) 143 Err(anyhow!("No more free IP addresses"))
144 }
145
146 pub fn get_peer(&self, name: &str) -> Option<&Peer> {
147 self.peers.iter().filter(|p| { p.name.as_str() == name }).nth(0)
148 }
149
150 pub fn gen_config(&self) -> Result<String, std::fmt::Error> {
151 let mut res = String::new();
152
153 writeln!(res, "[Interface]")?;
154 writeln!(res, "PrivateKey = {}", self.private_key)?;
155
156 if self.listen_port != 0 {
157 writeln!(res, "ListenPort = {}", self.listen_port)?;
158 }
159
160 writeln!(res, "Address = {}", self.network)?;
161
162 for pre_up in self.pre_ups.iter() {
163 writeln!(res, "PreUp = {}", pre_up)?;
164 }
165
166 for post_down in self.post_downs.iter() {
167 writeln!(res, "PostDown = {}", post_down)?;
168 }
169
170 writeln!(res, "")?;
171
172 // Now, add each peer
173 for peer in self.peers.iter() {
174 writeln!(res, "# {}", peer.name)?;
175 writeln!(res, "[Peer]")?;
176 writeln!(res, "PublicKey = {}", peer.public_key)?;
177 writeln!(res, "AllowedIPs = {}/32", peer.ip)?;
178 writeln!(res, "")?;
179 }
180
181 Ok(res)
144 } 182 }
145} 183}
146 184
147 185
148 186
149fn config_value(line: &str) -> Result<&str, &str> { 187fn config_value(line: &str) -> Result<&str> {
150 match line.find("=") { 188 match line.find("=") {
151 Some(i) => { 189 Some(i) => {
152 Ok(line[i+1..].trim()) 190 Ok(line[i+1..].trim())
153 }, 191 },
154 None => { 192 None => {
155 Err("line does not seem to contain a key-value pair") 193 Err(anyhow!("line does not seem to contain a key-value pair"))
156 } 194 }
157 } 195 }
158} \ No newline at end of file 196} \ No newline at end of file
diff --git a/src/wg/error.rs b/src/wg/error.rs
deleted file mode 100644
index 0167b22..0000000
--- a/src/wg/error.rs
+++ /dev/null
@@ -1,31 +0,0 @@
1use std::fmt::{Formatter, Display};
2use std::error::Error;
3
4#[derive(Debug)]
5pub struct WgMgrError {
6 msg: String
7}
8
9impl WgMgrError {
10 pub fn new(msg: String) -> WgMgrError {
11 WgMgrError {
12 msg
13 }
14 }
15}
16
17impl Error for WgMgrError {
18 fn description(&self) -> &str {
19 return self.msg.as_str()
20 }
21
22 fn cause(&self) -> Option<&dyn Error> {
23 None
24 }
25}
26
27impl Display for WgMgrError {
28 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
29 write!(f, "{}", self.msg)
30 }
31} \ No newline at end of file
diff --git a/src/wg/peer.rs b/src/wg/peer.rs
index 0a25841..17722e5 100644
--- a/src/wg/peer.rs
+++ b/src/wg/peer.rs
@@ -1,7 +1,14 @@
1use std::error::Error;
2use std::net::Ipv4Addr; 1use std::net::Ipv4Addr;
3use std::env::join_paths; 2use std::env::join_paths;
4use std::fs::read_to_string; 3use std::fs::{read_to_string, self, File};
4use std::fmt::Write;
5use std::path::{Path};
6use std::process::{Command, Stdio};
7use std::io::Write as ioWrite;
8
9use anyhow::Result;
10
11use super::config::WireguardConfig;
5 12
6#[derive(Debug)] 13#[derive(Debug)]
7pub struct Peer { 14pub struct Peer {
@@ -11,16 +18,103 @@ pub struct Peer {
11} 18}
12 19
13impl Peer { 20impl Peer {
14 pub fn private_key(&self) -> Result<String, Box<dyn Error>> { 21 pub fn new(name: String, ip: Ipv4Addr) -> Peer {
15 // TODO: do not hardcode where private keys are stored 22 let private = Command::new("wg")
23 .arg("genkey")
24 .output()
25 .expect("Could not generate a private key. Is 'wg' installed?");
26
27 let mut public = Command::new("wg")
28 .arg("pubkey")
29 .stdin(Stdio::piped())
30 .stdout(Stdio::piped())
31 .spawn()
32 .expect("Could not run 'wg'");
33
34 let private_clone = private.stdout.clone();
35
36 let mut stdin = public.stdin.take().expect("could not open wg stdin");
37 std::thread::spawn(move || {
38 stdin.write_all(&private.stdout).unwrap();
39 });
40
41 let output = public.wait_with_output().expect("could not read from 'wg pubkey'");
42
43 let private_key = String::from_utf8(private_clone).expect("could not decode private key");
44 let public_key = String::from_utf8(output.stdout).expect("could not decode public key");
45
46 let private_key = private_key.trim();
47 let public_key = public_key.trim();
16 48
49 let p = Peer{
50 name,
51 ip,
52 public_key: String::from(public_key)
53 };
54
55 let mut f = match File::create(p.private_key_path()) {
56 Ok(f) => f,
57 Err(e) => {
58 panic!("Error creating private key file: {}", e);
59 }
60 };
61 write!(f, "{}", private_key).expect("could not write private key to file");
62
63 p
64 }
65
66 fn private_key_folder(&self) -> String {
17 let pk_folder = String::from("./private_keys/"); 67 let pk_folder = String::from("./private_keys/");
18 let pk_path = join_paths(&[pk_folder, self.name.clone()])?;
19 68
69 let pk_path = Path::new(pk_folder.as_str());
70 if !pk_path.exists() {
71 fs::create_dir(pk_path).expect("Could not create private keys folder");
72 }
73
74 if !pk_path.is_dir() {
75 panic!("Error: the private key folder exists but is not a directory")
76 }
77
78 pk_folder
79 }
80
81 fn private_key_path(&self) -> String {
82 let folder_name = self.private_key_folder();
83 let pk_folder = Path::new(folder_name.as_str());
84 let pk_path = pk_folder.join(self.name.clone());
85
86 String::from(pk_path.to_str().unwrap())
87 }
88
89 pub fn private_key(&self) -> Result<String> {
90 let pk_folder = self.private_key_folder();
91 let pk_path = join_paths(&[pk_folder, self.name.clone()])?;
20 let pk = read_to_string(pk_path)?; 92 let pk = read_to_string(pk_path)?;
21 93
22 Ok(pk) 94 Ok(pk)
23 } 95 }
96
97 pub fn gen_config(&self, conf: &WireguardConfig, is_full: bool) -> Result<String>{
98 let mut res = String::new();
99
100 writeln!(res, "[Interface]")?;
101 writeln!(res, "PrivateKey = {}", self.private_key()?)?;
102 writeln!(res, "Address = {}/32", self.ip)?;
103 writeln!(res, "DNS = TODO\n")?;
104
105 writeln!(res, "[Peer]")?;
106 writeln!(res, "PublicKey = TODO")?;
107
108 let allowed_ips = match is_full {
109 true => String::from("0.0.0.0/0"),
110 false => conf.network.to_string()
111 };
112 writeln!(res, "AllowedIPs = {}", allowed_ips)?;
113 writeln!(res, "Endpoint = TODO")?;
114 writeln!(res, "PersistentKeepAlive = 25")?;
115
116 Ok(res)
117 }
24} 118}
25 119
26impl PartialOrd for Peer { 120impl PartialOrd for Peer {