Skip to content

Commit c555326

Browse files
committed
refactor: split into modules
1 parent 29f5727 commit c555326

File tree

7 files changed

+358
-333
lines changed

7 files changed

+358
-333
lines changed

.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,24 @@
22
"cSpell.words": [
33
"appender",
44
"binstall",
5+
"crypter",
56
"dalek",
67
"defguard",
78
"getrandom",
9+
"hmac",
810
"jemalloc",
911
"jemallocator",
1012
"keepalive",
1113
"multizone",
14+
"pkcs",
1215
"preshared",
1316
"prvkey",
1417
"reqwest",
1518
"rustup",
1619
"serde",
1720
"softprops",
1821
"swatinem",
22+
"symm",
1923
"thiserror",
2024
"tikv",
2125
"tlsv",

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ base64 = "0.22.1"
2121
hex = "^0.4.3"
2222
hickory-resolver = "0.25.2"
2323
openssl = { version = "0.10.75", features = ["vendored"] }
24-
reqwest = { version = "0.12.24", features = ["json"] }
24+
reqwest = { version = "0.12.25", features = ["json"] }
2525
serde = { version = "1.0.228", features = ["derive"] }
2626
serde_json = "1.0.145"
2727
thiserror = "2.0.17"
2828
tokio = { version = "1.48.0", features = ["full"] }
29-
tracing = "0.1.41"
30-
tracing-subscriber = "0.3.20"
31-
tracing-appender = "^0.2.3"
29+
tracing = "0.1.43"
30+
tracing-subscriber = "0.3.22"
31+
tracing-appender = "0.2.4"
3232
url = "2.5.7"
3333
x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] }
3434

src/config.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use std::{
2+
fs::{File, read_to_string},
3+
io::Write,
4+
path::Path,
5+
};
6+
7+
use serde::{Deserialize, Serialize};
8+
use x25519_dalek::{PublicKey, StaticSecret};
9+
10+
#[derive(Debug, Clone, Serialize, Deserialize)]
11+
pub struct ServerConfig {
12+
pub name: Option<String>,
13+
pub internal_cidr: String,
14+
pub ip: String,
15+
pub port: u32,
16+
pub pubkey: String,
17+
pub vpc_id: Option<String>,
18+
pub vpc_ip: Option<String>,
19+
pub persistent_keepalive_interval: Option<u16>,
20+
}
21+
22+
#[derive(Debug, Clone, Serialize, Deserialize)]
23+
pub struct NetworkConfig {
24+
pub mtu: u32,
25+
pub peers: Vec<ServerConfig>,
26+
}
27+
28+
#[derive(Debug, Clone, Serialize, Deserialize)]
29+
pub struct DaemonConfig {
30+
pub config_url: String,
31+
pub secret: Option<String>,
32+
pub fetch_interval: u64,
33+
pub interface_name: String,
34+
}
35+
36+
pub const PRIVATE_KEY_PATH: &str = "secret.key";
37+
pub const CONFIG_PATH: &str = "config.json";
38+
39+
pub fn load_key() -> (PublicKey, StaticSecret) {
40+
if !Path::new(PRIVATE_KEY_PATH).exists() {
41+
let secret_key = StaticSecret::random();
42+
let secret_key_bytes = secret_key.to_bytes();
43+
let secret_key_hex = hex::encode(secret_key_bytes);
44+
let mut file = File::create(PRIVATE_KEY_PATH).unwrap();
45+
file.write_all(secret_key_hex.as_bytes()).unwrap();
46+
}
47+
let secret_key = read_to_string(PRIVATE_KEY_PATH).unwrap();
48+
let secret_key = hex::decode(secret_key).unwrap();
49+
let secret_key: [u8; 32] = secret_key.try_into().unwrap();
50+
let secret_key = StaticSecret::from(secret_key);
51+
let pubkey = PublicKey::from(&secret_key);
52+
(pubkey, secret_key)
53+
}

src/crypto.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use std::error::Error;
2+
3+
use base64::Engine;
4+
use openssl::symm::{Cipher, Crypter, Mode};
5+
6+
pub fn decrypt_config(encrypted: &str, secret: &str) -> Result<String, Box<dyn Error>> {
7+
// Step 1: Base64 decode
8+
let data = base64::engine::general_purpose::STANDARD
9+
.decode(encrypted.replace("\n", "").replace(" ", ""))
10+
.unwrap();
11+
12+
// Step 2: Verify prefix
13+
assert!(&data[0..8] == b"Salted__");
14+
let salt = &data[8..16];
15+
let encrypted = &data[16..];
16+
17+
// Step 3: Derive key and iv using PBKDF2-HMAC-SHA256
18+
let mut key = [0u8; 32]; // AES-256 => 32-byte key
19+
let mut iv = [0u8; 16]; // CBC IV
20+
let mut derived = [0u8; 48]; // key + iv = 48 bytes total
21+
22+
openssl::pkcs5::pbkdf2_hmac(
23+
secret.as_bytes(),
24+
salt,
25+
10_000,
26+
openssl::hash::MessageDigest::sha256(),
27+
&mut derived,
28+
)
29+
.unwrap();
30+
key.copy_from_slice(&derived[..32]);
31+
iv.copy_from_slice(&derived[32..48]);
32+
33+
// Step 4: Decrypt
34+
let cipher = Cipher::aes_256_cbc();
35+
let mut crypter = Crypter::new(cipher, Mode::Decrypt, &key, Some(&iv)).unwrap();
36+
crypter.pad(true);
37+
38+
let mut plaintext = vec![0; encrypted.len() + cipher.block_size()];
39+
let mut count = crypter.update(encrypted, &mut plaintext).unwrap();
40+
count += crypter.finalize(&mut plaintext[count..]).unwrap();
41+
plaintext.truncate(count);
42+
43+
Ok(String::from_utf8(plaintext)?)
44+
}
45+
46+
#[cfg(test)]
47+
mod test {
48+
use super::*;
49+
use crate::config::NetworkConfig;
50+
51+
#[tokio::test]
52+
async fn test_decrypt_config() {
53+
// cat servers.json | openssl enc -aes-256-cbc -a -salt -pbkdf2 -pass pass:wg-multizone
54+
let mut r = r#"
55+
U2FsdGVkX1/yCvhJaU9iPq+ei2yy3AebTbqBByZH+o5Q75eyWhbOkelF1ON2elCq
56+
RN07JlKorWX1O5FJeoeNud4ciwT/KUcnDnTIZWVSmni3gMpDhOcvl4otQqS48pwt
57+
hVUQkbuhtqiThOFBOmRPLEc7h0A6z+rkKngPWoyDuRak04DH18XpHFUdwCoGc75F
58+
fOerWTS5sLBHcaQzxf2qeeBr0jkcQqVWPkjMyHYDhR8YV4Kk8XOSXw6cfmICbeJl
59+
7P9+qwzZSJz4n2PNqjYz8A=="#
60+
.replace("\n", "")
61+
.replace(" ", "");
62+
r = decrypt_config(&r, "wg-multizone").unwrap();
63+
println!("{}", r);
64+
let r: NetworkConfig = serde_json::from_str(&r).unwrap();
65+
println!("{:?}", r);
66+
}
67+
}

src/dns.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use std::{net::IpAddr, time::Duration};
2+
3+
use hickory_resolver::Resolver;
4+
5+
pub async fn resolve_to_ip(host: &str) -> Option<String> {
6+
// Try to parse as IP address first
7+
if let Ok(_) = host.parse::<IpAddr>() {
8+
return Some(host.to_string());
9+
}
10+
11+
// If not an IP, try to resolve as domain name
12+
let resolver = match Resolver::builder_tokio() {
13+
Ok(mut builder) => {
14+
builder.options_mut().timeout = Duration::from_secs(5);
15+
builder.build()
16+
}
17+
Err(e) => {
18+
println!("Failed to create DNS resolver: {}", e);
19+
return None;
20+
}
21+
};
22+
23+
match resolver.lookup_ip(host).await {
24+
Ok(response) => {
25+
// Get the first IP address from the response
26+
response.iter().next().map(|ip| ip.to_string())
27+
}
28+
Err(e) => {
29+
println!("Failed to resolve domain {}: {}", host, e);
30+
None
31+
}
32+
}
33+
}

src/loader.rs

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
use crate::{
2+
config::{DaemonConfig, NetworkConfig, ServerConfig},
3+
crypto::decrypt_config,
4+
dns::resolve_to_ip,
5+
};
6+
use defguard_wireguard_rs::{
7+
InterfaceConfiguration, WGApi, WireguardInterfaceApi, host::Peer, key::Key, net::IpAddrMask,
8+
};
9+
use std::{error::Error, fs::read_to_string, net::IpAddr, str::FromStr, time::Duration};
10+
use url::Url;
11+
12+
pub async fn fetch_config_content(config_url: &str) -> Result<String, Box<dyn Error>> {
13+
// Try to parse as URL first
14+
if let Ok(url) = Url::parse(config_url) {
15+
match url.scheme() {
16+
"file" => {
17+
// Handle file:// URLs
18+
let path = url
19+
.to_file_path()
20+
.map_err(|_| format!("Invalid file URL: {}", config_url))?;
21+
Ok(read_to_string(path)?)
22+
}
23+
"http" | "https" => {
24+
// Handle HTTP(S) URLs with 30-second timeout
25+
let client = reqwest::Client::builder()
26+
.timeout(Duration::from_secs(30))
27+
.build()?;
28+
Ok(client.get(config_url).send().await?.text().await?)
29+
}
30+
scheme => Err(format!("Unsupported URL scheme: {}", scheme).into()),
31+
}
32+
} else {
33+
// If URL parsing fails, treat it as a plain file path
34+
Ok(read_to_string(config_url)?)
35+
}
36+
}
37+
38+
pub(crate) async fn fetch_and_apply_config(
39+
wgapi: &WGApi,
40+
interface_config: &mut InterfaceConfiguration,
41+
pubkey: &str,
42+
config: &DaemonConfig,
43+
) -> Result<(), Box<dyn Error>> {
44+
let mut r = fetch_config_content(&config.config_url).await?;
45+
if config.secret.is_some() {
46+
r = decrypt_config(&r, &config.secret.as_ref().unwrap())?;
47+
}
48+
let r: NetworkConfig = serde_json::from_str(&r)?;
49+
let mut my_config: Option<ServerConfig> = None;
50+
for peer in r.peers.iter() {
51+
if peer.pubkey == pubkey {
52+
my_config = Some(peer.clone());
53+
}
54+
}
55+
if my_config.is_none() {
56+
println!("Current server is not configured in the network");
57+
return Ok(());
58+
}
59+
let my_config = my_config.unwrap();
60+
let current_address = if interface_config.addresses.len() > 0 {
61+
interface_config.addresses[0].to_string()
62+
} else {
63+
"".to_string()
64+
};
65+
if current_address != my_config.internal_cidr
66+
|| my_config.port != interface_config.port
67+
|| Some(r.mtu) != interface_config.mtu
68+
{
69+
if interface_config.port == 0 {
70+
wgapi.create_interface()?;
71+
}
72+
interface_config.addresses = vec![IpAddrMask::from_str(&my_config.internal_cidr)?];
73+
interface_config.port = my_config.port;
74+
interface_config.mtu = Some(r.mtu);
75+
wgapi.configure_interface(&interface_config)?;
76+
}
77+
let current_peers = wgapi.read_interface_data()?;
78+
let mut next_peers = std::collections::HashSet::new();
79+
for peer in r.peers.iter() {
80+
if peer.pubkey == pubkey {
81+
continue;
82+
}
83+
let pubkey = Key::from_str(&peer.pubkey)?;
84+
next_peers.insert(pubkey.clone());
85+
let current_peer = current_peers.peers.get(&pubkey);
86+
let mut should_reconfigure = false;
87+
let mut peer_cidr = IpAddrMask::from_str(&peer.internal_cidr)?;
88+
peer_cidr.cidr = 32;
89+
let peer_endpoint_host = if my_config.vpc_id.is_some()
90+
&& my_config.vpc_id == peer.vpc_id
91+
&& peer.vpc_ip.is_some()
92+
{
93+
peer.vpc_ip.as_ref().unwrap().clone()
94+
} else {
95+
peer.ip.clone()
96+
};
97+
98+
// Resolve domain name to IP address
99+
let peer_endpoint_ip = resolve_to_ip(&peer_endpoint_host).await;
100+
101+
// Calculate keepalive interval as minimum of both endpoints
102+
let keepalive_interval = match (
103+
my_config.persistent_keepalive_interval,
104+
peer.persistent_keepalive_interval,
105+
) {
106+
(Some(a), Some(b)) => Some(a.min(b)),
107+
(Some(a), None) => Some(a),
108+
(None, Some(b)) => Some(b),
109+
(None, None) => None,
110+
};
111+
112+
if let Some(p) = current_peer {
113+
if let Some(endpoint) = p.endpoint {
114+
if let Some(resolved_ip) = &peer_endpoint_ip {
115+
if endpoint.ip().to_string() != *resolved_ip
116+
|| endpoint.port() as u32 != peer.port
117+
{
118+
should_reconfigure = true;
119+
}
120+
} else {
121+
// Resolution failed, need to reconfigure to clear endpoint
122+
should_reconfigure = true;
123+
}
124+
} else {
125+
should_reconfigure = true;
126+
}
127+
if p.allowed_ips.len() != 1 {
128+
should_reconfigure = true;
129+
} else if p.allowed_ips[0] != peer_cidr {
130+
should_reconfigure = true;
131+
}
132+
if p.persistent_keepalive_interval != keepalive_interval {
133+
should_reconfigure = true;
134+
}
135+
} else {
136+
should_reconfigure = true;
137+
}
138+
if should_reconfigure {
139+
let endpoint = match &peer_endpoint_ip {
140+
Some(ip) => {
141+
// Check if IP is IPv6 and needs brackets for SocketAddr parsing
142+
let endpoint_str = match ip.parse::<IpAddr>() {
143+
Ok(IpAddr::V6(_)) => format!("[{}]:{}", ip, peer.port),
144+
_ => format!("{}:{}", ip, peer.port),
145+
};
146+
match endpoint_str.parse() {
147+
Ok(addr) => Some(addr),
148+
Err(e) => {
149+
println!("Failed to parse endpoint {}: {}", endpoint_str, e);
150+
None
151+
}
152+
}
153+
}
154+
None => None,
155+
};
156+
let wg_peer = Peer {
157+
public_key: pubkey,
158+
preshared_key: None,
159+
protocol_version: None,
160+
endpoint,
161+
last_handshake: None,
162+
tx_bytes: 0,
163+
rx_bytes: 0,
164+
persistent_keepalive_interval: keepalive_interval,
165+
allowed_ips: vec![peer_cidr],
166+
};
167+
let endpoint_display = peer_endpoint_ip
168+
.as_ref()
169+
.map(|ip| format!("{}:{}", ip, peer.port))
170+
.unwrap_or_else(|| "(no endpoint - resolution failed)".to_string());
171+
println!(
172+
"Configuring peer: {} {} {}",
173+
&peer.pubkey, endpoint_display, &peer.internal_cidr
174+
);
175+
wgapi.configure_peer(&wg_peer)?;
176+
}
177+
}
178+
for peer in current_peers.peers.iter() {
179+
if next_peers.contains(&peer.0) {
180+
continue;
181+
}
182+
wgapi.remove_peer(&peer.0)?;
183+
}
184+
Ok(())
185+
}

0 commit comments

Comments
 (0)