1use crate::{Error, IoErrorContext, NetworkOpts, Peer, PeerDiff};
2use ipnet::IpNet;
3use std::{
4 io,
5 net::{IpAddr, SocketAddr},
6 time::Duration,
7};
8use wireguard_control::{
9 Backend, Device, DeviceUpdate, InterfaceName, Key, PeerConfigBuilder, PeerInfo,
10};
11
12#[cfg(any(target_os = "macos", target_os = "openbsd"))]
13fn cmd(bin: &str, args: &[&str]) -> Result<std::process::Output, io::Error> {
14 let output = std::process::Command::new(bin).args(args).output()?;
15 log::debug!("cmd: {} {}", bin, args.join(" "));
16 log::debug!("status: {:?}", output.status.code());
17 log::trace!("stdout: {}", String::from_utf8_lossy(&output.stdout));
18 log::trace!("stderr: {}", String::from_utf8_lossy(&output.stderr));
19 if output.status.success() {
20 Ok(output)
21 } else {
22 Err(io::Error::other(format!(
23 "failed to run {} {} command: {}",
24 bin,
25 args.join(" "),
26 String::from_utf8_lossy(&output.stderr)
27 )))
28 }
29}
30
31#[cfg(target_os = "macos")]
32pub fn set_addr(interface: &InterfaceName, addr: IpNet) -> Result<(), io::Error> {
33 let real_interface = wireguard_control::backends::userspace::resolve_tun(interface)?;
34
35 if matches!(addr, IpNet::V4(_)) {
36 cmd(
37 "ifconfig",
38 &[
39 &real_interface,
40 "inet",
41 &addr.to_string(),
42 &addr.addr().to_string(),
43 "alias",
44 ],
45 )
46 .map(|_output| ())
47 } else {
48 cmd(
49 "ifconfig",
50 &[&real_interface, "inet6", &addr.to_string(), "alias"],
51 )
52 .map(|_output| ())
53 }
54}
55
56#[cfg(target_os = "macos")]
57pub fn set_up(interface: &InterfaceName, mtu: u32) -> Result<(), io::Error> {
58 let real_interface = wireguard_control::backends::userspace::resolve_tun(interface)?;
59 cmd("ifconfig", &[&real_interface, "mtu", &mtu.to_string()])?;
60 Ok(())
61}
62
63#[cfg(target_os = "openbsd")]
64pub fn set_addr(interface: &InterfaceName, addr: IpNet) -> Result<(), io::Error> {
65 let af = match &addr {
66 IpNet::V4(_) => "inet",
67 IpNet::V6(_) => "inet6",
68 };
69 cmd("ifconfig", &[&interface.to_string(), af, &addr.to_string()]).map(|_output| ())
70}
71
72#[cfg(target_os = "openbsd")]
73pub fn set_up(interface: &InterfaceName, mtu: u32) -> Result<(), io::Error> {
74 cmd(
75 "ifconfig",
76 &[&interface.to_string(), "mtu", &mtu.to_string()],
77 )?;
78 Ok(())
79}
80
81#[cfg(target_os = "linux")]
82pub use super::netlink::set_addr;
83
84#[cfg(target_os = "linux")]
85pub use super::netlink::set_up;
86
87pub fn up(
88 interface: &InterfaceName,
89 private_key: &str,
90 address: IpNet,
91 listen_port: Option<u16>,
92 peer: Option<(&str, IpAddr, SocketAddr)>,
93 network: NetworkOpts,
94) -> Result<(), io::Error> {
95 let mut device = DeviceUpdate::new();
96 if let Some((public_key, address, endpoint)) = peer {
97 let prefix = if address.is_ipv4() { 32 } else { 128 };
98 let peer_config = PeerConfigBuilder::new(
99 &wireguard_control::Key::from_base64(public_key).map_err(|_| {
100 io::Error::new(
101 io::ErrorKind::InvalidInput,
102 "failed to parse base64 public key",
103 )
104 })?,
105 )
106 .add_allowed_ip(address, prefix)
107 .set_persistent_keepalive_interval(25)
108 .set_endpoint(endpoint);
109 device = device.add_peer(peer_config);
110 }
111 if let Some(listen_port) = listen_port {
112 device = device.set_listen_port(listen_port);
113 }
114 device
115 .set_private_key(wireguard_control::Key::from_base64(private_key).unwrap())
116 .apply(interface, network.backend)?;
117 set_addr(interface, address)?;
118 set_up(interface, network.mtu.unwrap_or(1280))?;
119 if !network.no_routing {
120 #[cfg(not(target_os = "openbsd"))]
122 add_route(interface, address)?;
123 }
124 Ok(())
125}
126
127pub fn set_listen_port(
128 interface: &InterfaceName,
129 listen_port: Option<u16>,
130 backend: Backend,
131) -> Result<(), Error> {
132 let mut device = DeviceUpdate::new();
133 if let Some(listen_port) = listen_port {
134 device = device.set_listen_port(listen_port);
135 } else {
136 device = device.randomize_listen_port();
137 }
138 device.apply(interface, backend)?;
139
140 Ok(())
141}
142
143pub fn down(interface: &InterfaceName, backend: Backend) -> Result<(), Error> {
144 Ok(Device::get(interface, backend)
145 .with_str(interface.as_str_lossy())?
146 .delete()
147 .with_str(interface.as_str_lossy())?)
148}
149
150#[cfg(target_os = "macos")]
154pub fn add_route(interface: &InterfaceName, cidr: IpNet) -> Result<bool, io::Error> {
155 let real_interface = wireguard_control::backends::userspace::resolve_tun(interface)?;
156 let output = cmd(
157 "route",
158 &[
159 "-n",
160 "add",
161 if matches!(cidr, IpNet::V4(_)) {
162 "-inet"
163 } else {
164 "-inet6"
165 },
166 &cidr.to_string(),
167 "-interface",
168 &real_interface,
169 ],
170 )?;
171 let stderr = String::from_utf8_lossy(&output.stderr);
172 if !output.status.success() {
173 Err(io::Error::other(format!(
174 "failed to add route for device {} ({}): {}",
175 &interface, real_interface, stderr
176 )))
177 } else {
178 Ok(!stderr.contains("File exists"))
179 }
180}
181
182#[cfg(target_os = "linux")]
183pub use super::netlink::add_route;
184
185pub trait DeviceExt {
186 fn diff<'a>(&'a self, peers: &'a [Peer]) -> Vec<PeerDiff<'a>>;
188
189 fn get_peer(&self, public_key: &str) -> Option<&PeerInfo>;
191}
192
193impl DeviceExt for Device {
194 fn diff<'a>(&'a self, peers: &'a [Peer]) -> Vec<PeerDiff<'a>> {
195 let interface_public_key = self
196 .public_key
197 .as_ref()
198 .map(|k| k.to_base64())
199 .unwrap_or_default();
200 let existing_peers = &self.peers;
201
202 let modifications = peers.iter().filter_map(|peer| {
204 if peer.is_disabled || peer.public_key == interface_public_key {
205 None
206 } else {
207 let existing_peer = existing_peers
208 .iter()
209 .find(|p| p.config.public_key.to_base64() == peer.public_key);
210 PeerDiff::new(existing_peer, Some(peer)).unwrap()
211 }
212 });
213
214 let removals = existing_peers.iter().filter_map(|existing| {
216 let public_key = existing.config.public_key.to_base64();
217 if peers.iter().any(|p| p.public_key == public_key) {
218 None
219 } else {
220 PeerDiff::new(Some(existing), None).unwrap()
221 }
222 });
223
224 modifications.chain(removals).collect::<Vec<_>>()
225 }
226
227 fn get_peer(&self, public_key: &str) -> Option<&PeerInfo> {
228 Key::from_base64(public_key)
229 .ok()
230 .and_then(|key| self.peers.iter().find(|peer| peer.config.public_key == key))
231 }
232}
233
234pub trait PeerInfoExt {
235 fn is_recently_connected(&self) -> bool;
238}
239impl PeerInfoExt for PeerInfo {
240 fn is_recently_connected(&self) -> bool {
241 const REJECT_AFTER_TIME: Duration = Duration::from_secs(180);
242
243 let last_handshake = self
244 .stats
245 .last_handshake_time
246 .and_then(|t| t.elapsed().ok())
247 .unwrap_or(Duration::MAX);
248
249 last_handshake <= REJECT_AFTER_TIME
250 }
251}