Skip to content

Commit 9be4e77

Browse files
committed
feat: support domain name & keep alive setting
1 parent 2247514 commit 9be4e77

File tree

2 files changed

+109
-11
lines changed

2 files changed

+109
-11
lines changed

Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "wg-multizone"
3-
version = "1.0.2"
3+
version = "1.0.3"
44
edition = "2024"
55

66
[[bin]]
@@ -17,10 +17,10 @@ opt-level = 3
1717
incremental = true
1818

1919
[dependencies]
20-
anyhow = "1.0.100"
2120
base64 = "0.22.1"
2221
hex = "^0.4.3"
23-
openssl = { version = "0.10.74", features = ["vendored"] }
22+
hickory-resolver = "0.25.2"
23+
openssl = { version = "0.10.75", features = ["vendored"] }
2424
reqwest = { version = "0.12.24", features = ["json"] }
2525
serde = { version = "1.0.228", features = ["derive"] }
2626
serde_json = "1.0.145"
@@ -29,6 +29,7 @@ tokio = { version = "1.48.0", features = ["full"] }
2929
tracing = "0.1.41"
3030
tracing-subscriber = "0.3.20"
3131
tracing-appender = "^0.2.3"
32+
url = "2.5.7"
3233
x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] }
3334

3435
[dependencies.defguard_wireguard_rs]

src/main.rs

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::{
22
error::Error,
33
fs::{File, read_to_string},
44
io::Write,
5+
net::IpAddr,
56
path::Path,
67
str::FromStr,
78
time::Duration,
@@ -11,9 +12,11 @@ use base64::Engine;
1112
use defguard_wireguard_rs::{
1213
InterfaceConfiguration, WGApi, WireguardInterfaceApi, host::Peer, key::Key, net::IpAddrMask,
1314
};
15+
use hickory_resolver::Resolver;
1416
use openssl::symm::{Cipher, Crypter, Mode};
1517
use serde::{Deserialize, Serialize};
1618
use tokio::time::sleep;
19+
use url::Url;
1720
use x25519_dalek::{PublicKey, StaticSecret};
1821

1922
#[cfg(not(any(target_os = "macos", target_os = "windows", target_arch = "arm")))]
@@ -32,6 +35,7 @@ struct ServerConfig {
3235
pub pubkey: String,
3336
pub vpc_id: Option<String>,
3437
pub vpc_ip: Option<String>,
38+
pub persistent_keepalive_interval: Option<u16>,
3539
}
3640

3741
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -67,6 +71,31 @@ fn load_key() -> (PublicKey, StaticSecret) {
6771
(pubkey, secret_key)
6872
}
6973

74+
async fn fetch_config_content(config_url: &str) -> Result<String, Box<dyn Error>> {
75+
// Try to parse as URL first
76+
if let Ok(url) = Url::parse(config_url) {
77+
match url.scheme() {
78+
"file" => {
79+
// Handle file:// URLs
80+
let path = url.to_file_path().map_err(|_| {
81+
format!("Invalid file URL: {}", config_url)
82+
})?;
83+
Ok(read_to_string(path)?)
84+
}
85+
"http" | "https" => {
86+
// Handle HTTP(S) URLs
87+
Ok(reqwest::get(config_url).await?.text().await?)
88+
}
89+
scheme => {
90+
Err(format!("Unsupported URL scheme: {}", scheme).into())
91+
}
92+
}
93+
} else {
94+
// If URL parsing fails, treat it as a plain file path
95+
Ok(read_to_string(config_url)?)
96+
}
97+
}
98+
7099
fn decrypt_config(encrypted: &str, secret: &str) -> Result<String, Box<dyn Error>> {
71100
// Step 1: Base64 decode
72101
let data = base64::engine::general_purpose::STANDARD
@@ -107,13 +136,40 @@ fn decrypt_config(encrypted: &str, secret: &str) -> Result<String, Box<dyn Error
107136
Ok(String::from_utf8(plaintext)?)
108137
}
109138

139+
async fn resolve_to_ip(host: &str) -> Option<String> {
140+
// Try to parse as IP address first
141+
if let Ok(_) = host.parse::<IpAddr>() {
142+
return Some(host.to_string());
143+
}
144+
145+
// If not an IP, try to resolve as domain name
146+
let resolver = match Resolver::builder_tokio() {
147+
Ok(builder) => builder.build(),
148+
Err(e) => {
149+
println!("Failed to create DNS resolver: {}", e);
150+
return None;
151+
}
152+
};
153+
154+
match resolver.lookup_ip(host).await {
155+
Ok(response) => {
156+
// Get the first IP address from the response
157+
response.iter().next().map(|ip| ip.to_string())
158+
}
159+
Err(e) => {
160+
println!("Failed to resolve domain {}: {}", host, e);
161+
None
162+
}
163+
}
164+
}
165+
110166
async fn fetch_and_apply_config(
111167
wgapi: &WGApi,
112168
interface_config: &mut InterfaceConfiguration,
113169
pubkey: &str,
114170
config: &DaemonConfig,
115171
) -> Result<(), Box<dyn Error>> {
116-
let mut r = reqwest::get(&config.config_url).await?.text().await?;
172+
let mut r = fetch_config_content(&config.config_url).await?;
117173
if config.secret.is_some() {
118174
r = decrypt_config(
119175
&r.replace("\n", "").replace(" ", ""),
@@ -161,19 +217,36 @@ async fn fetch_and_apply_config(
161217
let mut should_reconfigure = false;
162218
let mut peer_cidr = IpAddrMask::from_str(&peer.internal_cidr)?;
163219
peer_cidr.cidr = 32;
164-
let peer_endpoint_ip = if my_config.vpc_id.is_some()
220+
let peer_endpoint_host = if my_config.vpc_id.is_some()
165221
&& my_config.vpc_id == peer.vpc_id
166222
&& peer.vpc_ip.is_some()
167223
{
168224
peer.vpc_ip.as_ref().unwrap().clone()
169225
} else {
170226
peer.ip.clone()
171227
};
228+
229+
// Resolve domain name to IP address
230+
let peer_endpoint_ip = resolve_to_ip(&peer_endpoint_host).await;
231+
232+
// Calculate keepalive interval as minimum of both endpoints
233+
let keepalive_interval = match (my_config.persistent_keepalive_interval, peer.persistent_keepalive_interval) {
234+
(Some(a), Some(b)) => Some(a.min(b)),
235+
(Some(a), None) => Some(a),
236+
(None, Some(b)) => Some(b),
237+
(None, None) => None,
238+
};
239+
172240
if let Some(p) = current_peer {
173241
if let Some(endpoint) = p.endpoint {
174-
if endpoint.ip().to_string() != peer_endpoint_ip
175-
|| endpoint.port() as u32 != peer.port
176-
{
242+
if let Some(resolved_ip) = &peer_endpoint_ip {
243+
if endpoint.ip().to_string() != *resolved_ip
244+
|| endpoint.port() as u32 != peer.port
245+
{
246+
should_reconfigure = true;
247+
}
248+
} else {
249+
// Resolution failed, need to reconfigure to clear endpoint
177250
should_reconfigure = true;
178251
}
179252
} else {
@@ -184,24 +257,48 @@ async fn fetch_and_apply_config(
184257
} else if p.allowed_ips[0] != peer_cidr {
185258
should_reconfigure = true;
186259
}
260+
if p.persistent_keepalive_interval != keepalive_interval {
261+
should_reconfigure = true;
262+
}
187263
} else {
188264
should_reconfigure = true;
189265
}
190266
if should_reconfigure {
267+
let endpoint = match &peer_endpoint_ip {
268+
Some(ip) => {
269+
// Check if IP is IPv6 and needs brackets for SocketAddr parsing
270+
let endpoint_str = match ip.parse::<IpAddr>() {
271+
Ok(IpAddr::V6(_)) => format!("[{}]:{}", ip, peer.port),
272+
_ => format!("{}:{}", ip, peer.port),
273+
};
274+
match endpoint_str.parse() {
275+
Ok(addr) => Some(addr),
276+
Err(e) => {
277+
println!("Failed to parse endpoint {}: {}", endpoint_str, e);
278+
None
279+
}
280+
}
281+
}
282+
None => None,
283+
};
191284
let wg_peer = Peer {
192285
public_key: pubkey,
193286
preshared_key: None,
194287
protocol_version: None,
195-
endpoint: Some(format!("{}:{}", peer_endpoint_ip, peer.port).parse()?),
288+
endpoint,
196289
last_handshake: None,
197290
tx_bytes: 0,
198291
rx_bytes: 0,
199-
persistent_keepalive_interval: None,
292+
persistent_keepalive_interval: keepalive_interval,
200293
allowed_ips: vec![peer_cidr],
201294
};
295+
let endpoint_display = peer_endpoint_ip
296+
.as_ref()
297+
.map(|ip| format!("{}:{}", ip, peer.port))
298+
.unwrap_or_else(|| "(no endpoint - resolution failed)".to_string());
202299
println!(
203300
"Configuring peer: {} {} {}",
204-
&peer.pubkey, &peer_endpoint_ip, &peer.internal_cidr
301+
&peer.pubkey, endpoint_display, &peer.internal_cidr
205302
);
206303
wgapi.configure_peer(&wg_peer)?;
207304
}

0 commit comments

Comments
 (0)