connlib: JNI bridge (#1848)

The biggest internal change is that all the methods on `Callbacks` (on
the Rust side!) return a `Result` now, so errors from the bridge or even
the client callbacks will be handled.

@roop there's nothing for you to review here, but note:

- the `bool` return values you've asked about in the past are gone now
- the route string for `onAddRoute`/`onRemoveRoute` no longer has the
extra quotes (it's no longer JSON)

---------

Signed-off-by: Francesca Lovebloom <franlovebloom@gmail.com>
Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
Francesca Lovebloom
2023-08-07 17:03:28 -07:00
committed by GitHub
parent b9c11007a4
commit bc3a5d9e54
23 changed files with 618 additions and 212 deletions

15
rust/Cargo.lock generated
View File

@@ -752,8 +752,11 @@ version = "0.1.6"
dependencies = [
"android_logger",
"firezone-client-connlib",
"ip_network",
"jni 0.21.1",
"log",
"serde_json",
"thiserror",
]
[[package]]
@@ -763,6 +766,7 @@ dependencies = [
"anyhow",
"diva",
"firezone-client-connlib",
"ip_network",
"libc",
"serde_json",
"swift-bridge",
@@ -1161,7 +1165,6 @@ dependencies = [
"rand_core",
"rtnetlink",
"serde",
"serde_json",
"thiserror",
"tokio",
"tracing",
@@ -1279,6 +1282,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"firezone-gateway-connlib",
"ip_network",
"tracing",
"tracing-subscriber",
"url",
@@ -1345,6 +1349,7 @@ dependencies = [
"anyhow",
"clap",
"firezone-client-connlib",
"ip_network",
"tracing",
"tracing-subscriber",
"url",
@@ -3150,18 +3155,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.40"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.40"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -3,16 +3,19 @@ name = "connlib-android"
version = "0.1.6"
edition = "2021"
[features]
mock = ["firezone-client-connlib/mock"]
[dependencies]
jni = { version = "0.21.1", features = ["invocation"] }
firezone-client-connlib = { path = "../../libs/client" }
log = "0.4"
android_logger = "0.13"
[lib]
name = "connlib"
crate-type = ["cdylib"]
doc = false
[features]
mock = ["firezone-client-connlib/mock"]
[dependencies]
android_logger = "0.13"
firezone-client-connlib = { path = "../../libs/client" }
jni = { version = "0.21.1", features = ["invocation"] }
ip_network = "0.4"
log = "0.4"
serde_json = "1"
thiserror = "1"

View File

@@ -4,11 +4,14 @@
// ecosystem, so it's used here for consistency.
use firezone_client_connlib::{Callbacks, Error, ResourceDescription, Session};
use ip_network::IpNetwork;
use jni::{
objects::{JClass, JObject, JString, JValue},
JNIEnv,
strings::JNIString,
JNIEnv, JavaVM,
};
use std::net::{Ipv4Addr, Ipv6Addr};
use thiserror::Error;
/// This should be called once after the library is loaded by the system.
#[allow(non_snake_case)]
@@ -25,76 +28,297 @@ pub extern "system" fn Java_dev_firezone_connlib_Logger_init(_: JNIEnv, _: JClas
)
}
#[derive(Clone)]
pub struct CallbackHandler;
pub struct CallbackHandler {
vm: JavaVM,
callback_handler: JObject<'static>,
}
impl Clone for CallbackHandler {
fn clone(&self) -> Self {
// This is essentially a `memcpy` to bypass redundant checks from
// doing `as_raw` -> `from_raw`/etc; both of these fields are just
// dumb pointers but the wrappers don't implement `Clone`.
//
// SAFETY: `self` is guaranteed to be valid and `Self` is POD.
unsafe { std::ptr::read(self) }
}
}
#[derive(Debug, Error)]
pub enum CallbackError {
#[error("Failed to attach current thread as daemon: {0}")]
AttachCurrentThreadFailed(#[source] jni::errors::Error),
#[error("Failed to serialize JSON: {0}")]
SerializeFailed(#[from] serde_json::Error),
#[error("Failed to create string `{name}`: {source}")]
NewStringFailed {
name: &'static str,
source: jni::errors::Error,
},
#[error("Failed to call method `{name}`: {source}")]
CallMethodFailed {
name: &'static str,
source: jni::errors::Error,
},
}
impl CallbackHandler {
fn env<T>(
&self,
f: impl FnOnce(JNIEnv) -> Result<T, CallbackError>,
) -> Result<T, CallbackError> {
self.vm
.attach_current_thread_as_daemon()
.map_err(CallbackError::AttachCurrentThreadFailed)
.and_then(f)
}
}
fn call_method(
env: &mut JNIEnv,
this: &JObject,
name: &'static str,
sig: &str,
args: &[JValue],
) -> Result<(), CallbackError> {
env.call_method(this, name, sig, args)
.map(|val| log::trace!("`{name}` returned `{val:?}`"))
.map_err(|source| CallbackError::CallMethodFailed { name, source })
}
impl Callbacks for CallbackHandler {
type Error = CallbackError;
fn on_set_interface_config(
&self,
_tunnel_address_v4: Ipv4Addr,
_tunnel_address_v6: Ipv6Addr,
_dns_address: Ipv4Addr,
) {
todo!()
tunnel_address_v4: Ipv4Addr,
tunnel_address_v6: Ipv6Addr,
dns_address: Ipv4Addr,
) -> Result<(), Self::Error> {
self.env(|mut env| {
let tunnel_address_v4 =
env.new_string(tunnel_address_v4.to_string())
.map_err(|source| CallbackError::NewStringFailed {
name: "tunnel_address_v4",
source,
})?;
let tunnel_address_v6 =
env.new_string(tunnel_address_v6.to_string())
.map_err(|source| CallbackError::NewStringFailed {
name: "tunnel_address_v6",
source,
})?;
let dns_address = env.new_string(dns_address.to_string()).map_err(|source| {
CallbackError::NewStringFailed {
name: "dns_address",
source,
}
})?;
// TODO: Don't hardcode this string here!
let dns_fallback_strategy = env.new_string("upstream_resolver").map_err(|source| {
CallbackError::NewStringFailed {
name: "dns_fallback_strategy",
source,
}
})?;
call_method(
&mut env,
&self.callback_handler,
"onSetInterfaceConfig",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)",
&[
JValue::from(&tunnel_address_v4),
JValue::from(&tunnel_address_v6),
JValue::from(&dns_address),
JValue::from(&dns_fallback_strategy),
],
)
})
}
fn on_tunnel_ready(&self) {
todo!()
fn on_tunnel_ready(&self) -> Result<(), Self::Error> {
self.env(|mut env| {
call_method(&mut env, &self.callback_handler, "onTunnelReady", "()", &[])
})
}
fn on_add_route(&self, _route: String) {
todo!()
fn on_add_route(&self, route: IpNetwork) -> Result<(), Self::Error> {
self.env(|mut env| {
let route = env.new_string(route.to_string()).map_err(|source| {
CallbackError::NewStringFailed {
name: "route",
source,
}
})?;
call_method(
&mut env,
&self.callback_handler,
"onAddRoute",
"(Ljava/lang/String;)",
&[JValue::from(&route)],
)
})
}
fn on_remove_route(&self, _route: String) {
todo!()
fn on_remove_route(&self, route: IpNetwork) -> Result<(), Self::Error> {
self.env(|mut env| {
let route = env.new_string(route.to_string()).map_err(|source| {
CallbackError::NewStringFailed {
name: "route",
source,
}
})?;
call_method(
&mut env,
&self.callback_handler,
"onRemoveRoute",
"(Ljava/lang/String;)",
&[JValue::from(&route)],
)
})
}
fn on_update_resources(&self, _resource_list: Vec<ResourceDescription>) {
todo!()
fn on_update_resources(
&self,
resource_list: Vec<ResourceDescription>,
) -> Result<(), Self::Error> {
self.env(|mut env| {
let resource_list = env
.new_string(serde_json::to_string(&resource_list)?)
.map_err(|source| CallbackError::NewStringFailed {
name: "resource_list",
source,
})?;
call_method(
&mut env,
&self.callback_handler,
"onUpdateResources",
"(Ljava/lang/String;)",
&[JValue::from(&resource_list)],
)
})
}
fn on_disconnect(&self, _error: Option<&Error>) {
todo!()
fn on_disconnect(&self, error: Option<&Error>) -> Result<(), Self::Error> {
self.env(|mut env| {
let error = env
.new_string(serde_json::to_string(&error.map(ToString::to_string))?)
.map_err(|source| CallbackError::NewStringFailed {
name: "error",
source,
})?;
call_method(
&mut env,
&self.callback_handler,
"onDisconnect",
"(Ljava/lang/String;)",
&[JValue::from(&error)],
)
})
}
fn on_error(&self, _error: &Error) {
todo!()
fn on_error(&self, error: &Error) -> Result<(), Self::Error> {
self.env(|mut env| {
let error = env.new_string(error.to_string()).map_err(|source| {
CallbackError::NewStringFailed {
name: "error",
source,
}
})?;
call_method(
&mut env,
&self.callback_handler,
"onError",
"(Ljava/lang/String;)",
&[JValue::from(&error)],
)
})
}
}
fn throw(env: &mut JNIEnv, class: &str, msg: impl Into<JNIString>) {
if let Err(err) = env.throw_new(class, msg) {
// We can't panic, since unwinding across the FFI boundary is UB...
log::error!("failed to throw Java exception: {err}");
}
}
fn catch_and_throw<F: FnOnce(&mut JNIEnv) -> R, R>(env: &mut JNIEnv, f: F) -> Option<R> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(env)))
.map_err(|info| {
log::error!("catching Rust panic");
throw(
env,
"java/lang/Exception",
match info.downcast_ref::<&str>() {
Some(msg) => format!("Rust panicked: {msg}"),
None => "Rust panicked with no message".to_owned(),
},
);
})
.ok()
}
#[derive(Debug, Error)]
enum ConnectError {
#[error("Failed to access {name:?}: {source}")]
StringInvalid {
name: &'static str,
source: jni::errors::Error,
},
#[error("Failed to get Java VM: {0}")]
GetJavaVmFailed(#[source] jni::errors::Error),
#[error(transparent)]
ConnectFailed(#[from] Error),
}
fn connect(
env: &mut JNIEnv,
portal_url: JString,
portal_token: JString,
callback_handler: JObject<'static>,
) -> Result<Session<CallbackHandler>, ConnectError> {
let portal_url = String::from(env.get_string(&portal_url).map_err(|source| {
ConnectError::StringInvalid {
name: "portal_url",
source,
}
})?);
let portal_token = env
.get_string(&portal_token)
.map_err(|source| ConnectError::StringInvalid {
name: "portal_token",
source,
})?
.into();
let callback_handler = CallbackHandler {
vm: env.get_java_vm().map_err(ConnectError::GetJavaVmFailed)?,
callback_handler,
};
Session::connect(portal_url.as_str(), portal_token, callback_handler.clone())
.map_err(Into::into)
}
/// # Safety
/// Pointers must be valid
#[allow(non_snake_case)]
#[no_mangle]
pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_connect(
mut env: JNIEnv,
mut env: JNIEnv<'static>,
_class: JClass,
portal_url: JString,
portal_token: JString,
callback: JObject,
callback_handler: JObject<'static>,
) -> *const Session<CallbackHandler> {
let portal_url: String = env.get_string(&portal_url).unwrap().into();
let portal_token: String = env.get_string(&portal_token).unwrap().into();
let session = Box::new(
Session::connect(portal_url.as_str(), portal_token, CallbackHandler).expect("TODO!"),
);
// TODO: Get actual IPs returned from portal based on this device
let tunnelAddressesJSON = "[{\"tunnel_ipv4\": \"100.100.1.1\", \"tunnel_ipv6\": \"fd00:0222:2011:1111:6def:1001:fe67:0012\"}]";
let tunnel_addresses = env.new_string(tunnelAddressesJSON).unwrap();
match env.call_method(
callback,
"onTunnelReady",
"(Ljava/lang/String;)Z",
&[JValue::from(&tunnel_addresses)],
) {
Ok(res) => log::trace!("`onTunnelReady` returned `{res:?}`"),
Err(err) => log::error!("Failed to call `onTunnelReady`: {err}"),
if let Some(result) = catch_and_throw(&mut env, |env| {
connect(env, portal_url, portal_token, callback_handler)
}) {
match result {
Ok(session) => return Box::into_raw(Box::new(session)),
Err(err) => throw(&mut env, "java/lang/Exception", err.to_string()),
}
}
Box::into_raw(session)
std::ptr::null()
}
/// # Safety
@@ -102,14 +326,19 @@ pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_connect(
#[allow(non_snake_case)]
#[no_mangle]
pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_disconnect(
_env: JNIEnv,
mut env: JNIEnv,
_: JClass,
session_ptr: *mut Session<CallbackHandler>,
) -> bool {
if session_ptr.is_null() {
return false;
session: *mut Session<CallbackHandler>,
) {
if let Some(session) = session.as_mut() {
catch_and_throw(&mut env, |_| {
session.disconnect(None);
});
} else {
throw(
&mut env,
"java/lang/NullPointerException",
"Cannot disconnect because \"session\" is null",
);
}
let session = unsafe { &mut *session_ptr };
session.disconnect(None)
}

View File

@@ -13,6 +13,7 @@ swift-bridge-build = "0.1.52"
walkdir = "2.3.3"
[dependencies]
ip_network = "0.4"
libc = "0.2"
swift-bridge = { workspace = true }
firezone-client-connlib = { path = "../../libs/client" }

View File

@@ -3,6 +3,7 @@
#![allow(improper_ctypes, non_camel_case_types)]
use firezone_client_connlib::{Callbacks, Error, ResourceDescription, Session};
use ip_network::IpNetwork;
use std::{
net::{Ipv4Addr, Ipv6Addr},
sync::Arc,
@@ -21,12 +22,12 @@ mod ffi {
) -> Result<WrappedSession, String>;
#[swift_bridge(swift_name = "bumpSockets")]
fn bump_sockets(&self) -> bool;
fn bump_sockets(&self);
#[swift_bridge(swift_name = "disableSomeRoamingForBrokenMobileSemantics")]
fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool;
fn disable_some_roaming_for_broken_mobile_semantics(&self);
fn disconnect(&mut self) -> bool;
fn disconnect(&mut self);
}
extern "Swift" {
@@ -78,45 +79,57 @@ unsafe impl Sync for ffi::CallbackHandler {}
pub struct CallbackHandler(Arc<ffi::CallbackHandler>);
impl Callbacks for CallbackHandler {
type Error = std::convert::Infallible;
fn on_set_interface_config(
&self,
tunnel_address_v4: Ipv4Addr,
tunnel_address_v6: Ipv6Addr,
dns_address: Ipv4Addr,
) {
) -> Result<(), Self::Error> {
self.0.on_set_interface_config(
tunnel_address_v4.to_string(),
tunnel_address_v6.to_string(),
dns_address.to_string(),
)
);
Ok(())
}
fn on_tunnel_ready(&self) {
self.0.on_tunnel_ready()
fn on_tunnel_ready(&self) -> Result<(), Self::Error> {
self.0.on_tunnel_ready();
Ok(())
}
fn on_add_route(&self, route: String) {
self.0.on_add_route(route)
fn on_add_route(&self, route: IpNetwork) -> Result<(), Self::Error> {
self.0.on_add_route(route.to_string());
Ok(())
}
fn on_remove_route(&self, route: String) {
self.0.on_remove_route(route)
fn on_remove_route(&self, route: IpNetwork) -> Result<(), Self::Error> {
self.0.on_remove_route(route.to_string());
Ok(())
}
fn on_update_resources(&self, resource_list: Vec<ResourceDescription>) {
fn on_update_resources(
&self,
resource_list: Vec<ResourceDescription>,
) -> Result<(), Self::Error> {
self.0.on_update_resources(
serde_json::to_string(&resource_list)
.expect("developer error: failed to serialize resource list"),
)
);
Ok(())
}
fn on_disconnect(&self, error: Option<&Error>) {
fn on_disconnect(&self, error: Option<&Error>) -> Result<(), Self::Error> {
self.0
.on_disconnect(error.map(ToString::to_string).unwrap_or_default())
.on_disconnect(error.map(ToString::to_string).unwrap_or_default());
Ok(())
}
fn on_error(&self, error: &Error) {
self.0.on_error(error.to_string())
fn on_error(&self, error: &Error) -> Result<(), Self::Error> {
self.0.on_error(error.to_string());
Ok(())
}
}
@@ -148,17 +161,16 @@ impl WrappedSession {
.map_err(|err| err.to_string())
}
fn bump_sockets(&self) -> bool {
// TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#L177
todo!()
fn bump_sockets(&self) {
self.session.bump_sockets()
}
fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool {
// TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#LL197C6-L197C50
todo!()
fn disable_some_roaming_for_broken_mobile_semantics(&self) {
self.session
.disable_some_roaming_for_broken_mobile_semantics()
}
fn disconnect(&mut self) -> bool {
fn disconnect(&mut self) {
self.session.disconnect(None)
}
}

View File

@@ -7,6 +7,7 @@ edition = "2021"
[dependencies]
firezone-client-connlib = { path = "../../libs/client" }
ip_network = "0.4"
url = { version = "2.3.1", default-features = false }
tracing-subscriber = { version = "0.3" }
tracing = { version = "0.1" }

View File

@@ -1,5 +1,6 @@
use anyhow::{Context, Result};
use clap::Parser;
use ip_network::IpNetwork;
use std::{
net::{Ipv4Addr, Ipv6Addr},
str::FromStr,
@@ -12,32 +13,46 @@ use url::Url;
pub struct CallbackHandler;
impl Callbacks for CallbackHandler {
type Error = std::convert::Infallible;
fn on_set_interface_config(
&self,
_tunnel_address_v4: Ipv4Addr,
_tunnel_address_v6: Ipv6Addr,
_dns_address: Ipv4Addr,
) {
) -> Result<(), Self::Error> {
Ok(())
}
fn on_tunnel_ready(&self) {
fn on_tunnel_ready(&self) -> Result<(), Self::Error> {
tracing::trace!("Tunnel connected");
Ok(())
}
fn on_add_route(&self, _route: String) {}
fn on_add_route(&self, _route: IpNetwork) -> Result<(), Self::Error> {
Ok(())
}
fn on_remove_route(&self, _route: String) {}
fn on_remove_route(&self, _route: IpNetwork) -> Result<(), Self::Error> {
Ok(())
}
fn on_update_resources(&self, resource_list: Vec<ResourceDescription>) {
fn on_update_resources(
&self,
resource_list: Vec<ResourceDescription>,
) -> Result<(), Self::Error> {
tracing::trace!("Resources updated, current list: {resource_list:?}");
Ok(())
}
fn on_disconnect(&self, error: Option<&Error>) {
fn on_disconnect(&self, error: Option<&Error>) -> Result<(), Self::Error> {
tracing::trace!("Tunnel disconnected: {error:?}");
Ok(())
}
fn on_error(&self, error: &Error) {
fn on_error(&self, error: &Error) -> Result<(), Self::Error> {
tracing::warn!("Encountered recoverable error: {error}");
Ok(())
}
}

View File

@@ -7,6 +7,7 @@ edition = "2021"
[dependencies]
firezone-gateway-connlib = { path = "../libs/gateway" }
ip_network = "0.4"
url = { version = "2.3.1", default-features = false }
tracing-subscriber = { version = "0.3" }
tracing = { version = "0.1" }

View File

@@ -1,4 +1,5 @@
use anyhow::{Context, Result};
use ip_network::IpNetwork;
use std::{
net::{Ipv4Addr, Ipv6Addr},
str::FromStr,
@@ -11,32 +12,46 @@ use url::Url;
pub struct CallbackHandler;
impl Callbacks for CallbackHandler {
type Error = std::convert::Infallible;
fn on_set_interface_config(
&self,
_tunnel_address_v4: Ipv4Addr,
_tunnel_address_v6: Ipv6Addr,
_dns_address: Ipv4Addr,
) {
) -> Result<(), Self::Error> {
Ok(())
}
fn on_tunnel_ready(&self) {
fn on_tunnel_ready(&self) -> Result<(), Self::Error> {
tracing::trace!("Tunnel connected with address");
Ok(())
}
fn on_add_route(&self, _route: String) {}
fn on_add_route(&self, _route: IpNetwork) -> Result<(), Self::Error> {
Ok(())
}
fn on_remove_route(&self, _route: String) {}
fn on_remove_route(&self, _route: IpNetwork) -> Result<(), Self::Error> {
Ok(())
}
fn on_update_resources(&self, resource_list: Vec<ResourceDescription>) {
fn on_update_resources(
&self,
resource_list: Vec<ResourceDescription>,
) -> Result<(), Self::Error> {
tracing::trace!("Resources updated, current list: {resource_list:?}");
Ok(())
}
fn on_disconnect(&self, error: Option<&Error>) {
fn on_disconnect(&self, error: Option<&Error>) -> Result<(), Self::Error> {
tracing::trace!("Tunnel disconnected: {error:?}");
Ok(())
}
fn on_error(&self, error: &Error) {
fn on_error(&self, error: &Error) -> Result<(), Self::Error> {
tracing::warn!("Encountered recoverable error: {error}");
Ok(())
}
}

View File

@@ -100,7 +100,7 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
)
.await
{
self.tunnel.callbacks().on_error(&e);
let _ = self.tunnel.callbacks().on_error(&e);
}
}
@@ -142,12 +142,12 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
.await
{
tunnel.cleanup_connection(resource_id);
tunnel.callbacks().on_error(&err);
let _ = tunnel.callbacks().on_error(&err);
}
}
Err(err) => {
tunnel.cleanup_connection(resource_id);
tunnel.callbacks().on_error(&err);
let _ = tunnel.callbacks().on_error(&err);
}
}
});
@@ -175,7 +175,7 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
tracing::error!(
"An offline error came back with a reference to a non-valid resource id"
);
self.tunnel.callbacks().on_error(&Error::ControlProtocolError);
let _ = self.tunnel.callbacks().on_error(&Error::ControlProtocolError);
return;
};
// TODO: Rate limit the number of attempts of getting the relays before just trying a local network connection
@@ -183,9 +183,10 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
}
None => {
tracing::error!(
"An offline portal error came without a reference that originated the error"
);
self.tunnel
"An offline portal error came without a reference that originated the error"
);
let _ = self
.tunnel
.callbacks()
.on_error(&Error::ControlProtocolError);
}

View File

@@ -60,6 +60,16 @@ pub enum ConnlibError {
/// Error when reading system's interface
#[error("Error while reading system's interface")]
IfaceRead(std::io::Error),
#[error("`on_set_interface_config` failed: {0}")]
OnSetInterfaceConfigFailed(String),
#[error("`on_tunnel_ready` failed: {0}")]
OnTunnelReadyFailed(String),
#[error("`on_add_route` failed: {0}")]
OnAddRouteFailed(String),
#[error("`on_remove_route` failed: {0}")]
OnRemoveRouteFailed(String),
#[error("`on_update_resources` failed: {0}")]
OnUpdateResourcesFailed(String),
/// Glob for errors without a type.
#[error("Other error: {0}")]
Other(&'static str),

View File

@@ -13,7 +13,7 @@ pub mod messages;
pub use error::ConnlibError as Error;
pub use error::Result;
pub use session::{Callbacks, ControlSession, Session, DNS_SENTINEL};
pub use session::{CallbackErrorFacade, Callbacks, ControlSession, Session, DNS_SENTINEL};
const VERSION: &str = env!("CARGO_PKG_VERSION");
const LIB_NAME: &str = "connlib";

View File

@@ -1,12 +1,16 @@
use async_trait::async_trait;
use backoff::{backoff::Backoff, ExponentialBackoffBuilder};
use boringtun::x25519::{PublicKey, StaticSecret};
use ip_network::IpNetwork;
use parking_lot::Mutex;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rand_core::OsRng;
use std::{
error::Error as StdError,
fmt::{Debug, Display},
marker::PhantomData,
net::{Ipv4Addr, Ipv6Addr},
result::Result as StdResult,
sync::Arc,
time::Duration,
};
@@ -46,34 +50,123 @@ pub trait ControlSession<T, CB: Callbacks> {
/// A session is created using [Session::connect], then to stop a session we use [Session::disconnect].
pub struct Session<T, U, V, R, M, CB: Callbacks> {
runtime: Arc<Mutex<Option<Runtime>>>,
callbacks: CB,
callbacks: CallbackErrorFacade<CB>,
_phantom: PhantomData<(T, U, V, R, M)>,
}
/// Traits that will be used by connlib to callback the client upper layers.
pub trait Callbacks: Clone + Send + Sync {
/// Error returned when a callback fails.
type Error: Debug + Display + StdError;
/// Called when the tunnel address is set.
fn on_set_interface_config(
&self,
tunnel_address_v4: Ipv4Addr,
tunnel_address_v6: Ipv6Addr,
dns_address: Ipv4Addr,
);
) -> StdResult<(), Self::Error>;
/// Called when the tunnel is connected.
fn on_tunnel_ready(&self);
fn on_tunnel_ready(&self) -> StdResult<(), Self::Error>;
/// Called when when a route is added.
fn on_add_route(&self, route: String);
fn on_add_route(&self, route: IpNetwork) -> StdResult<(), Self::Error>;
/// Called when when a route is removed.
fn on_remove_route(&self, route: String);
fn on_remove_route(&self, route: IpNetwork) -> StdResult<(), Self::Error>;
/// Called when the resource list changes.
fn on_update_resources(&self, resource_list: Vec<ResourceDescription>);
fn on_update_resources(
&self,
resource_list: Vec<ResourceDescription>,
) -> StdResult<(), Self::Error>;
/// Called when the tunnel is disconnected.
///
/// If the tunnel disconnected due to a fatal error, `error` is the error
/// that caused the disconnect.
fn on_disconnect(&self, error: Option<&Error>);
fn on_disconnect(&self, error: Option<&Error>) -> StdResult<(), Self::Error>;
/// Called when there's a recoverable error.
fn on_error(&self, error: &Error);
fn on_error(&self, error: &Error) -> StdResult<(), Self::Error>;
}
#[derive(Clone)]
pub struct CallbackErrorFacade<CB: Callbacks>(pub CB);
impl<CB: Callbacks> Callbacks for CallbackErrorFacade<CB> {
type Error = Error;
fn on_set_interface_config(
&self,
tunnel_address_v4: Ipv4Addr,
tunnel_address_v6: Ipv6Addr,
dns_address: Ipv4Addr,
) -> Result<()> {
let result = self
.0
.on_set_interface_config(tunnel_address_v4, tunnel_address_v6, dns_address)
.map_err(|err| Error::OnSetInterfaceConfigFailed(err.to_string()));
if let Err(err) = result.as_ref() {
tracing::error!("{err}");
}
result
}
fn on_tunnel_ready(&self) -> Result<()> {
let result = self
.0
.on_tunnel_ready()
.map_err(|err| Error::OnTunnelReadyFailed(err.to_string()));
if let Err(err) = result.as_ref() {
tracing::error!("{err}");
}
result
}
fn on_add_route(&self, route: IpNetwork) -> Result<()> {
let result = self
.0
.on_add_route(route)
.map_err(|err| Error::OnAddRouteFailed(err.to_string()));
if let Err(err) = result.as_ref() {
tracing::error!("{err}");
}
result
}
fn on_remove_route(&self, route: IpNetwork) -> Result<()> {
let result = self
.0
.on_remove_route(route)
.map_err(|err| Error::OnRemoveRouteFailed(err.to_string()));
if let Err(err) = result.as_ref() {
tracing::error!("{err}");
}
result
}
fn on_update_resources(&self, resource_list: Vec<ResourceDescription>) -> Result<()> {
let result = self
.0
.on_update_resources(resource_list)
.map_err(|err| Error::OnUpdateResourcesFailed(err.to_string()));
if let Err(err) = result.as_ref() {
tracing::error!("{err}");
}
result
}
fn on_disconnect(&self, error: Option<&Error>) -> Result<()> {
if let Err(err) = self.0.on_disconnect(error) {
tracing::error!("`on_disconnect` failed: {err}");
}
// There's nothing we can really do if `on_disconnect` fails.
Ok(())
}
fn on_error(&self, error: &Error) -> Result<()> {
if let Err(err) = self.0.on_error(error) {
tracing::error!("`on_error` failed: {err}");
}
// There's nothing we really want to do if `on_error` fails.
Ok(())
}
}
macro_rules! fatal_error {
@@ -128,6 +221,7 @@ where
// Big question here however is how do we get the result? We could block here await the result and spawn a new task.
// but then platforms should know that this function is blocking.
let callbacks = CallbackErrorFacade(callbacks);
let this = Self {
runtime: Mutex::new(Some(
tokio::runtime::Builder::new_multi_thread()
@@ -155,7 +249,7 @@ where
}
if cfg!(feature = "mock") {
Self::connect_mock(this.callbacks.clone());
Self::connect_mock(Arc::clone(&this.runtime), this.callbacks.clone());
} else {
Self::connect_inner(
Arc::clone(&this.runtime),
@@ -172,7 +266,7 @@ where
runtime: Arc<Mutex<Option<Runtime>>>,
portal_url: Url,
token: String,
callbacks: CB,
callbacks: CallbackErrorFacade<CB>,
) {
let runtime_disconnector = Arc::clone(&runtime);
runtime.lock().as_ref().unwrap().spawn(async move {
@@ -206,7 +300,7 @@ where
let topic = T::socket_path().to_string();
let internal_sender = connection.sender_with_topic(topic.clone());
fatal_error!(
T::start(private_key, control_plane_receiver, internal_sender, callbacks.clone()).await,
T::start(private_key, control_plane_receiver, internal_sender, callbacks.0.clone()).await,
&runtime_disconnector,
&callbacks
);
@@ -218,7 +312,7 @@ where
let result = connection.start(vec![topic.clone()], || exponential_backoff.reset()).await;
if let Some(t) = exponential_backoff.next_backoff() {
tracing::warn!("Error during connection to the portal, retrying in {} seconds", t.as_secs());
callbacks.on_error(&result.err().unwrap_or(Error::PortalConnectionError(tokio_tungstenite::tungstenite::Error::ConnectionClosed)));
let _ = callbacks.on_error(&result.err().unwrap_or(Error::PortalConnectionError(tokio_tungstenite::tungstenite::Error::ConnectionClosed)));
tokio::time::sleep(t).await;
} else {
tracing::error!("Connection to the portal error, check your internet or the status of the portal.\nDisconnecting interface.");
@@ -235,41 +329,57 @@ where
});
}
fn connect_mock(callbacks: CB) {
fn connect_mock(runtime: Arc<Mutex<Option<Runtime>>>, callbacks: CallbackErrorFacade<CB>) {
std::thread::sleep(Duration::from_secs(1));
callbacks.on_set_interface_config(
"100.100.111.2".parse().unwrap(),
"fd00:0222:2021:1111::2".parse().unwrap(),
DNS_SENTINEL,
fatal_error!(
callbacks.on_set_interface_config(
"100.100.111.2".parse().unwrap(),
"fd00:0222:2021:1111::2".parse().unwrap(),
DNS_SENTINEL,
),
&runtime,
&callbacks
);
fatal_error!(callbacks.on_tunnel_ready(), &runtime, &callbacks);
let handle = {
let callbacks = callbacks.clone();
std::thread::spawn(move || -> Result<()> {
std::thread::sleep(Duration::from_secs(3));
let resources = vec![
ResourceDescriptionCidr {
id: Uuid::new_v4(),
address: "8.8.4.4".parse::<Ipv4Addr>().unwrap().into(),
name: "Google Public DNS IPv4".to_string(),
},
ResourceDescriptionCidr {
id: Uuid::new_v4(),
address: "2001:4860:4860::8844".parse::<Ipv6Addr>().unwrap().into(),
name: "Google Public DNS IPv6".to_string(),
},
];
for resource in &resources {
callbacks.on_add_route(resource.address)?;
}
callbacks.on_update_resources(
resources
.into_iter()
.map(ResourceDescription::Cidr)
.collect(),
)
})
};
fatal_error!(
handle.join().expect("mock thread panicked"),
&runtime,
&callbacks
);
callbacks.on_tunnel_ready();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(3));
let resources = vec![
ResourceDescriptionCidr {
id: Uuid::new_v4(),
address: "8.8.4.4".parse::<Ipv4Addr>().unwrap().into(),
name: "Google Public DNS IPv4".to_string(),
},
ResourceDescriptionCidr {
id: Uuid::new_v4(),
address: "2001:4860:4860::8844".parse::<Ipv6Addr>().unwrap().into(),
name: "Google Public DNS IPv6".to_string(),
},
];
for resource in &resources {
callbacks.on_add_route(serde_json::to_string(&resource.address).unwrap());
}
callbacks.on_update_resources(
resources
.into_iter()
.map(ResourceDescription::Cidr)
.collect(),
);
});
}
fn disconnect_inner(runtime: &Mutex<Option<Runtime>>, callbacks: &CB, error: Option<Error>) {
fn disconnect_inner(
runtime: &Mutex<Option<Runtime>>,
callbacks: &CallbackErrorFacade<CB>,
error: Option<Error>,
) {
// 1. Close the websocket connection
// 2. Free the device handle (UNIX)
// 3. Close the file descriptor (UNIX)
@@ -283,26 +393,25 @@ where
// implement them :)
*runtime.lock() = None;
callbacks.on_disconnect(error.as_ref());
let _ = callbacks.on_disconnect(error.as_ref());
}
/// Cleanup a [Session].
///
/// For now this just drops the runtime, which should drop all pending tasks.
/// Further cleanup should be done here. (Otherwise we can just drop [Session]).
pub fn disconnect(&mut self, error: Option<Error>) -> bool {
Self::disconnect_inner(&self.runtime, &self.callbacks, error);
true
pub fn disconnect(&mut self, error: Option<Error>) {
Self::disconnect_inner(&self.runtime, &self.callbacks, error)
}
/// TODO
pub fn bump_sockets(&self) -> bool {
true
// TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#L177
pub fn bump_sockets(&self) {
tracing::error!("`bump_sockets` is unimplemented");
}
/// TODO
pub fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool {
true
// TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#LL197C6-L197C50
pub fn disable_some_roaming_for_broken_mobile_semantics(&self) {
tracing::error!("`disable_some_roaming_for_broken_mobile_semantics` is unimplemented");
}
}

View File

@@ -90,12 +90,12 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
.await
{
tunnel.cleanup_connection(connection_request.device.id);
tunnel.callbacks().on_error(&err);
let _ = tunnel.callbacks().on_error(&err);
}
}
Err(err) => {
tunnel.cleanup_connection(connection_request.device.id);
tunnel.callbacks().on_error(&err);
let _ = tunnel.callbacks().on_error(&err);
}
}
});

View File

@@ -9,7 +9,6 @@ tokio = { version = "1.27", default-features = false, features = ["rt", "rt-mult
thiserror = { version = "1.0", default-features = false }
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
serde = { version = "1.0", default-features = false, features = ["derive", "std"] }
serde_json = { version = "1.0", default-features = false, features = ["std"] }
futures = { version = "0.3", default-features = false, features = ["std", "async-await", "executor"] }
futures-util = { version = "0.3", default-features = false, features = ["std", "async-await", "async-await-macro"] }
tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] }

View File

@@ -174,7 +174,7 @@ where
let Some(gateway_public_key) = tunnel.gateway_public_keys.lock().remove(&resource_id) else {
tunnel.cleanup_connection(resource_id);
tracing::warn!("Opened ICE channel with gateway without ever receiving public key");
tunnel.callbacks.on_error(&Error::ControlProtocolError);
let _ = tunnel.callbacks.on_error(&Error::ControlProtocolError);
return;
};
let peer_config = PeerConfig {
@@ -186,7 +186,7 @@ where
if let Err(e) = tunnel.handle_channel_open(d, index, peer_config, None, resource_id, None).await {
tracing::error!("Couldn't establish wireguard link after channel was opened: {e}");
tunnel.callbacks.on_error(&e);
let _ = tunnel.callbacks.on_error(&e);
tunnel.cleanup_connection(resource_id);
}
tunnel.awaiting_connection.lock().remove(&resource_id);
@@ -302,10 +302,10 @@ where
Box::pin(async move {
{
let mut iface_config = tunnel.iface_config.lock().await;
for ip in &peer.ips {
for &ip in &peer.ips {
if let Err(e) = iface_config.add_route(ip, tunnel.callbacks()).await
{
tunnel.callbacks.on_error(&e);
let _ = tunnel.callbacks.on_error(&e);
}
}
}
@@ -321,7 +321,7 @@ where
)
.await
{
tunnel.callbacks.on_error(&e);
let _ = tunnel.callbacks.on_error(&e);
tracing::error!(
"Couldn't establish wireguard link after opening channel: {e}"
);
@@ -331,7 +331,7 @@ where
if let Some(conn) = conn {
if let Err(e) = conn.close().await {
tracing::error!("Problem while trying to close channel: {e:?}");
tunnel.callbacks().on_error(&e.into());
let _ = tunnel.callbacks().on_error(&e.into());
}
}
}

View File

@@ -38,7 +38,7 @@ use std::{
use libs_common::{
messages::{Id, Interface as InterfaceConfig, ResourceDescription},
Result,
CallbackErrorFacade, Result,
};
use device_channel::{create_iface, DeviceChannel};
@@ -146,7 +146,7 @@ pub struct Tunnel<C: ControlSignal, CB: Callbacks> {
resources: RwLock<ResourceTable>,
control_signaler: C,
gateway_public_keys: Mutex<HashMap<Id, PublicKey>>,
callbacks: CB,
callbacks: CallbackErrorFacade<CB>,
}
impl<C, CB> Tunnel<C, CB>
@@ -208,7 +208,7 @@ where
resources,
awaiting_connection,
control_signaler,
callbacks,
callbacks: CallbackErrorFacade(callbacks),
})
}
@@ -221,7 +221,7 @@ where
{
let mut iface_config = self.iface_config.lock().await;
for ip in resource_description.ips() {
iface_config.add_route(&ip, self.callbacks()).await?;
iface_config.add_route(ip, self.callbacks()).await?;
}
}
let resource_list = {
@@ -229,7 +229,7 @@ where
resources.insert(resource_description);
resources.resource_list()
};
self.callbacks.on_update_resources(resource_list);
self.callbacks.on_update_resources(resource_list)?;
Ok(())
}
@@ -247,14 +247,14 @@ where
.await
.expect("Couldn't initiate interface");
iface_config
.add_route(&DNS_SENTINEL.into(), self.callbacks())
.add_route(DNS_SENTINEL.into(), self.callbacks())
.await?;
}
self.start_timers();
self.start_iface_handler();
self.callbacks.on_tunnel_ready();
self.callbacks.on_tunnel_ready()?;
tracing::trace!("Started background loops");
@@ -389,7 +389,7 @@ where
}
Err(TunnResult::Err(e)) => {
tracing::error!("Wireguard error: {e:?}");
tunnel.callbacks().on_error(&e.into());
let _ = tunnel.callbacks().on_error(&e.into());
continue;
}
Err(_) => {
@@ -419,7 +419,7 @@ where
}
TunnResult::Err(err) => {
tracing::error!("Error decapsulating packet: {err:?}");
tunnel.callbacks().on_error(&err.into());
let _ = tunnel.callbacks().on_error(&err.into());
continue;
}
TunnResult::WriteToNetwork(packet) => {
@@ -449,13 +449,13 @@ where
async fn write4_device_infallible(&self, packet: &[u8]) {
if let Err(e) = self.device_channel.write4(packet).await {
self.callbacks.on_error(&e.into());
let _ = self.callbacks.on_error(&e.into());
}
}
async fn write6_device_infallible(&self, packet: &[u8]) {
if let Err(e) = self.device_channel.write6(packet).await {
self.callbacks.on_error(&e.into());
let _ = self.callbacks.on_error(&e.into());
}
}
@@ -485,13 +485,13 @@ where
Ok(res) => res,
Err(err) => {
tracing::error!("Couldn't read packet from interface: {err}");
dev.callbacks.on_error(&err.into());
let _ = dev.callbacks.on_error(&err.into());
continue;
}
},
Err(err) => {
tracing::error!("Couldn't obtain iface mtu: {err}");
dev.callbacks.on_error(&err);
let _ = dev.callbacks.on_error(&err);
continue;
}
}
@@ -572,7 +572,7 @@ where
// Not a deadlock because this is a different task
dev.awaiting_connection.lock().remove(&id);
tracing::error!("couldn't start protocol for new connection to resource: {e}");
dev.callbacks.on_error(&e);
let _ = dev.callbacks.on_error(&e);
}
});
}
@@ -591,7 +591,7 @@ where
}
TunnResult::Err(e) => {
tracing::error!(message = "Encapsulate error for resource corresponding to {dst_addr}", error = ?e);
dev.callbacks.on_error(&e.into());
let _ = dev.callbacks.on_error(&e.into());
}
TunnResult::WriteToNetwork(packet) => {
tracing::trace!("writing iface packet to peer: {dst_addr}");
@@ -612,11 +612,11 @@ where
tracing::error!(
"Problem while trying to close channel: {e:?}"
);
dev.callbacks().on_error(&e.into());
let _ = dev.callbacks().on_error(&e.into());
}
}
}
dev.callbacks.on_error(&e.into());
let _ = dev.callbacks.on_error(&e.into());
}
}
_ => panic!("Unexpected result from encapsulate"),
@@ -629,7 +629,7 @@ where
self.next_index.lock().next()
}
pub fn callbacks(&self) -> &CB {
pub fn callbacks(&self) -> &CallbackErrorFacade<CB> {
&self.callbacks
}
}

View File

@@ -40,7 +40,7 @@ impl Peer {
pub(crate) async fn send_infallible<CB: Callbacks>(&self, data: &[u8], callbacks: &CB) {
if let Err(e) = self.channel.write(&Bytes::copy_from_slice(data)).await {
tracing::error!("Couldn't send packet to connected peer: {e}");
callbacks.on_error(&e.into());
let _ = callbacks.on_error(&e.into());
}
}

View File

@@ -60,7 +60,7 @@ where
let mut address = r.address.split(':');
let Some(dst_addr) = address.next() else {
tracing::error!("invalid DNS name for resource: {}", r.address);
self.callbacks().on_error(&Error::InvalidResource(r.address.clone()));
let _ = self.callbacks().on_error(&Error::InvalidResource(r.address.clone()));
return;
};
let Ok(mut dst_addr) = format!("{dst_addr}:0").to_socket_addrs() else {

View File

@@ -1,7 +1,7 @@
use super::InterfaceConfig;
use ip_network::IpNetwork;
use libc::{close, open, O_RDWR};
use libs_common::{Callbacks, Error, Result, DNS_SENTINEL};
use libs_common::{CallbackErrorFacade, Callbacks, Error, Result, DNS_SENTINEL};
use std::{
os::fd::{AsRawFd, RawFd},
sync::Arc,
@@ -73,15 +73,18 @@ impl IfaceConfig {
pub async fn set_iface_config(
&mut self,
config: &InterfaceConfig,
callbacks: &impl Callbacks,
callbacks: &CallbackErrorFacade<impl Callbacks>,
) -> Result<()> {
callbacks.on_set_interface_config(config.ipv4, config.ipv6, DNS_SENTINEL);
Ok(())
}
pub async fn add_route(&mut self, route: &IpNetwork, callbacks: &impl Callbacks) -> Result<()> {
callbacks.on_add_route(serde_json::to_string(route)?);
Ok(())
pub async fn add_route(
&mut self,
route: IpNetwork,
callbacks: &CallbackErrorFacade<impl Callbacks>,
) -> Result<()> {
callbacks.on_add_route(route)
}
pub async fn up(&mut self) -> Result<()> {

View File

@@ -5,7 +5,7 @@ use libc::{
CTLIOCGINFO, F_GETFL, F_SETFL, IF_NAMESIZE, IPPROTO_IP, O_NONBLOCK, PF_SYSTEM, SOCK_DGRAM,
SOCK_STREAM, SYSPROTO_CONTROL, UTUN_OPT_IFNAME,
};
use libs_common::{Callbacks, Error, Result, DNS_SENTINEL};
use libs_common::{CallbackErrorFacade, Callbacks, Error, Result, DNS_SENTINEL};
use std::{
ffi::{c_int, c_short, c_uchar},
io,
@@ -266,15 +266,17 @@ impl IfaceConfig {
pub async fn set_iface_config(
&mut self,
config: &InterfaceConfig,
callbacks: &impl Callbacks,
callbacks: &CallbackErrorFacade<impl Callbacks>,
) -> Result<()> {
callbacks.on_set_interface_config(config.ipv4, config.ipv6, DNS_SENTINEL);
Ok(())
callbacks.on_set_interface_config(config.ipv4, config.ipv6, DNS_SENTINEL)
}
pub async fn add_route(&mut self, route: &IpNetwork, callbacks: &impl Callbacks) -> Result<()> {
callbacks.on_add_route(serde_json::to_string(route)?);
Ok(())
pub async fn add_route(
&mut self,
route: IpNetwork,
callbacks: &CallbackErrorFacade<impl Callbacks>,
) -> Result<()> {
callbacks.on_add_route(route)
}
pub async fn up(&mut self) -> Result<()> {

View File

@@ -4,7 +4,7 @@ use libc::{
close, fcntl, ioctl, open, read, sockaddr, sockaddr_in, write, F_GETFL, F_SETFL,
IFF_MULTI_QUEUE, IFF_NO_PI, IFF_TUN, IFNAMSIZ, O_NONBLOCK, O_RDWR,
};
use libs_common::{Callbacks, Error, Result};
use libs_common::{CallbackErrorFacade, Callbacks, Error, Result};
use netlink_packet_route::{rtnl::link::nlas::Nla, RT_SCOPE_UNIVERSE};
use rtnetlink::{new_connection, Handle};
use std::{
@@ -178,8 +178,8 @@ fn get_last_error() -> Error {
impl IfaceConfig {
pub async fn add_route(
&mut self,
route: &IpNetwork,
_callbacks: &impl Callbacks,
route: IpNetwork,
_callbacks: &CallbackErrorFacade<impl Callbacks>,
) -> Result<()> {
let req = self
.0
@@ -224,7 +224,7 @@ impl IfaceConfig {
pub async fn set_iface_config(
&mut self,
config: &InterfaceConfig,
_callbacks: &impl Callbacks,
_callbacks: &CallbackErrorFacade<impl Callbacks>,
) -> Result<()> {
let ips = self
.0

View File

@@ -1,6 +1,6 @@
use super::InterfaceConfig;
use ip_network::IpNetwork;
use libs_common::{Callbacks, Result};
use libs_common::{CallbackErrorFacade, Callbacks, Result};
#[derive(Debug)]
pub(crate) struct IfaceConfig;
@@ -10,15 +10,15 @@ impl IfaceConfig {
pub async fn set_iface_config(
&mut self,
_config: &InterfaceConfig,
_callbacks: &impl Callbacks,
_callbacks: &CallbackErrorFacade<impl Callbacks>,
) -> Result<()> {
todo!()
}
pub async fn add_route(
&mut self,
_route: &IpNetwork,
_callbacks: &impl Callbacks,
_route: IpNetwork,
_callbacks: &CallbackErrorFacade<impl Callbacks>,
) -> Result<()> {
todo!()
}