1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! The server config
use crate::error;
use crate::error::Error;
use base64ct::{Base64, Encoding};
use std::borrow::Cow;
use std::env::{self, VarError};
use std::fmt::{self, Display, Formatter};
use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
/// The server config
#[derive(Debug, Clone)]
#[allow(non_snake_case, reason = "We want to map the exact naming of the environment variables")]
pub struct Config {
/// The server address to forward the traffic to
///
/// # Example
/// An `address:port` combination
pub WGPROXY_SERVER: String,
/// The server public key for handshake validation
///
/// # Note
/// The public key is used for handshake verfication and quick rejection when a new proxy connection is created.
/// This is a security feature to ensure that the relay will not forward arbitrary rogue packets.
/// **If the handshake does not match the configured public key, the packet will be dropped.**
pub WGPROXY_PUBKEY: [u8; 32],
/// The address to listen on and to use for relaying
///
/// # Example
/// An inclusive range of ports, defaults to [`Self::WGPROXY_LISTEN_DEFAULT`]
pub WGPROXY_LISTEN: SocketAddr,
/// The timeout duration for NAT mappings to expire
///
/// # Example
/// A duration in seconds, defaults to [`Self::WGPROXY_TIMEOUT_DEFAULT`]
pub WGPROXY_TIMEOUT: Duration,
/// The log level
///
/// # Possible Values
/// Possible values are:
/// - `0`: Logs **errors** only
/// - `1`: Logs **warnings** and **errors**
/// - `2`: Logs **informational** messages, **warnings**, and **errors**
/// - `3`: Logs **debug** and **informational** messages, **warnings**, and **errors**
///
/// # Example
/// A positive integer value, defaults to [`Self::WGPROXY_LOGLEVEL_DEFAULT`]
pub WGPROXY_LOGLEVEL: u8,
}
impl Config {
/// The default listening address if [`Self::WGPROXY_LISTEN`] is not specified
pub const WGPROXY_LISTEN_DEFAULT: &str = "[::]:51820";
/// The default timeout in seconds if [`Self::WGPROXY_TIMEOUT`] is not specified
pub const WGPROXY_TIMEOUT_DEFAULT: &str = "60";
/// The default loglevel if [`Self::WGPROXY_LOGLEVEL`] is not specified
pub const WGPROXY_LOGLEVEL_DEFAULT: &str = "1";
/// Gets the config from the environment
pub fn from_env() -> Result<Self, Error> {
Ok(Config {
WGPROXY_SERVER: Self::wgproxy_server()?,
WGPROXY_PUBKEY: Self::wgproxy_pubkey()?,
WGPROXY_LISTEN: Self::wgproxy_listen()?,
WGPROXY_TIMEOUT: Self::wgproxy_timeout()?,
WGPROXY_LOGLEVEL: Self::wgproxy_loglevel()?,
})
}
/// Parses the `WGPROXY_SERVER` environment variable
fn wgproxy_server() -> Result<String, Error> {
let address = Self::env("WGPROXY_SERVER", "<unspecified>")?;
let Some(_) = address.to_socket_addrs()?.next() else {
// The address cannot be resolved; fail fast
return Err(error!(r#"Failed to resolve server address {address}"#));
};
// Retain the address as string so we can periodically re-resolve DNS names for to catch e.g. dynDNS or load
// balancing
Ok(address.to_string())
}
/// Parses the `WGPROXY_PUBKEY` environment variable
fn wgproxy_pubkey() -> Result<[u8; 32], Error> {
// Decode pubkey
let pubkey = Self::env("WGPROXY_PUBKEY", "<unspecified>")?;
let binary = Base64::decode_vec(&pubkey)
.map_err(|e| error!(with: e, r#"Failed to base64-decode public key "{pubkey}""#))?;
// Ensure the decoded public key is exactly 32 bytes
let maybe_binary = <[u8; 32]>::try_from(binary).ok();
maybe_binary.ok_or(error!(r#"Invalid public key "{pubkey}""#))
}
/// Parses the `WGPROXY_LISTEN` environment variable, or falls back to [`Self::WGPROXY_LISTEN_DEFAULT`]
fn wgproxy_listen() -> Result<SocketAddr, Error> {
let address = Self::env("WGPROXY_LISTEN", Self::WGPROXY_LISTEN_DEFAULT)?;
let maybe_address: Result<SocketAddr, _> = address.parse();
maybe_address.map_err(|e| error!(with: e, r#"Invalid listening address "{address}""#))
}
/// Parses the `WGPROXY_TIMEOUT` environment variable, or falls back to [`Self::WGPROXY_TIMEOUT_DEFAULT`]
fn wgproxy_timeout() -> Result<Duration, Error> {
let seconds = Self::env("WGPROXY_TIMEOUT", Self::WGPROXY_TIMEOUT_DEFAULT)?;
let seconds = seconds.parse()?;
Ok(Duration::from_secs(seconds))
}
/// Parses the `WGPROXY_LOGLEVEL` environment variable, or falls back to [`Self::WGPROXY_LOGLEVEL_DEFAULT`]
pub fn wgproxy_loglevel() -> Result<u8, Error> {
let loglevel = Self::env("WGPROXY_LOGLEVEL", Self::WGPROXY_LOGLEVEL_DEFAULT)?;
Ok(loglevel.parse()?)
}
/// Gets the environment variable with the given name or returns the default value
fn env(name: &str, default: &'static str) -> Result<Cow<'static, str>, Error> {
match env::var(name) {
Ok(value) => Ok(Cow::Owned(value)),
Err(VarError::NotPresent) => Ok(Cow::Borrowed(default)),
Err(e) => Err(error!(with: e, r#"Invalid environment variable "{name}""#)),
}
}
}
impl Display for Config {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
// Re-encode the public key to display them
let pubkey = Base64::encode_string(&self.WGPROXY_PUBKEY);
// Format struct
f.debug_struct("Config")
.field("WGPROXY_SERVER", &self.WGPROXY_SERVER)
.field("WGPROXY_PUBKEY", &pubkey)
.field("WGPROXY_LISTEN", &self.WGPROXY_LISTEN)
.field("WGPROXY_TIMEOUT", &self.WGPROXY_TIMEOUT)
.field("WGPROXY_LOGLEVEL", &self.WGPROXY_LOGLEVEL)
.finish()
}
}