Skip to content

Commit 76c4ed2

Browse files
committed
feat(openconnect): add optional integration crate
Add a focused aetheris-openconnect crate that isolates libopenconnect FFI behind safe Rust session, config, event, and error types. Expose the crate behind an optional aetheris-app openconnect feature, wire it into the workspace, and document the architecture and local build requirements for the system-libopenconnect feature.
1 parent 939385d commit 76c4ed2

12 files changed

Lines changed: 513 additions & 3 deletions

File tree

ARCHITECTURE.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ flowchart TD
1010
UI --> Relm[Relm4 App Component]
1111
Relm --> Commands[Async Commands and Streams]
1212
Commands --> KubeCrate[aetheris-kube]
13+
Commands -. optional VPN .-> OpenConnectCrate[aetheris-openconnect]
1314
KubeCrate --> Kubeconfig[Kubeconfig]
1415
KubeCrate --> Api[Kubernetes / OpenShift API]
1516
Relm --> Store[ProjectStore]
@@ -18,7 +19,7 @@ flowchart TD
1819
VTE --> Commands
1920
```
2021

21-
`aetheris-app` owns windows, widgets, user state, and persistence of Aetheris projects. `aetheris-kube` owns kubeconfig parsing, Kubernetes clients, discovery, list/watch, mutations, logs, exec, port-forwarding, metrics, and resource details.
22+
`aetheris-app` owns windows, widgets, user state, and persistence of Aetheris projects. `aetheris-kube` owns kubeconfig parsing, Kubernetes clients, discovery, list/watch, mutations, logs, exec, port-forwarding, metrics, and resource details. `aetheris-openconnect` is an optional native integration crate for libopenconnect; it isolates C FFI and exposes a Rust API for future VPN workflows.
2223

2324
## Crate Boundaries
2425

@@ -40,10 +41,17 @@ flowchart LR
4041
Ops[Logs exec port-forward mutations]
4142
end
4243
44+
subgraph OpenConnect["aetheris-openconnect"]
45+
SafeApi[Safe Rust API]
46+
Ffi[libopenconnect FFI]
47+
end
48+
4349
Widgets --> State
4450
State --> Commands
4551
State --> Streams
4652
Commands --> Manager
53+
Commands -. optional .-> SafeApi
54+
SafeApi --> Ffi
4755
Streams --> Manager
4856
Manager --> Session
4957
Session --> Resources
@@ -52,7 +60,7 @@ flowchart LR
5260
Projects --> State
5361
```
5462

55-
The backend crate must not import GTK, Adwaita, Relm4, VTE, or application widgets. Shared data crosses the boundary through DTOs exported from `aetheris-kube::types`.
63+
The backend crate must not import GTK, Adwaita, Relm4, VTE, or application widgets. Shared data crosses the boundary through DTOs exported from `aetheris-kube::types`. Native C integrations are kept out of both UI widgets and `aetheris-kube`; each integration gets a focused crate such as `aetheris-openconnect`, with `unsafe` declarations confined to its FFI module.
5664

5765
## Application Lifecycle
5866

CONTRIBUTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ sudo dnf install -y \
1313
openssl-devel
1414
```
1515

16+
OpenConnect integration is optional while the VPN UI is under development. To
17+
build it locally, install the libopenconnect development files and enable the
18+
feature:
19+
20+
```sh
21+
sudo dnf install -y openconnect-devel
22+
cargo check -p aetheris-openconnect --features system-libopenconnect
23+
cargo check -p aetheris-app --features openconnect
24+
```
25+
1626
Run the app:
1727

1828
```sh
@@ -56,6 +66,7 @@ Aetheris has two Rust crates:
5666

5767
- `aetheris-kube` — pure Kubernetes backend. It must not depend on GTK, Relm4, Libadwaita, or VTE.
5868
- `aetheris-app` — Relm4/GTK4 application. It owns UI state, widgets, project persistence, and command wiring.
69+
- `aetheris-openconnect` — optional libopenconnect integration. It owns C FFI and exposes only safe Rust types to the app.
5970

6071
See [ARCHITECTURE.md](ARCHITECTURE.md) for diagrams and module responsibilities.
6172

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
[workspace]
22
resolver = "2"
3-
members = ["crates/aetheris-app", "crates/aetheris-kube"]
3+
members = [
4+
"crates/aetheris-app",
5+
"crates/aetheris-kube",
6+
"crates/aetheris-openconnect",
7+
]
48

59
[workspace.package]
610
version = "1.3.0"

crates/aetheris-app/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,16 @@ resources = [
5353
"../../po/pt_BR/LC_MESSAGES/org.luminusos.Aetheris.mo",
5454
]
5555

56+
[features]
57+
default = []
58+
openconnect = [
59+
"dep:aetheris-openconnect",
60+
"aetheris-openconnect/system-libopenconnect",
61+
]
62+
5663
[dependencies]
5764
aetheris-kube = { path = "../aetheris-kube" }
65+
aetheris-openconnect = { path = "../aetheris-openconnect", optional = true }
5866
anyhow = "1"
5967
dirs = "6"
6068
futures = "0.3"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "aetheris-openconnect"
3+
description = "OpenConnect integration layer for Aetheris"
4+
version.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
authors.workspace = true
8+
links = "openconnect"
9+
10+
[features]
11+
default = []
12+
system-libopenconnect = []
13+
14+
[dependencies]
15+
thiserror.workspace = true
16+
17+
[build-dependencies]
18+
pkg-config = "0.3"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
fn main() {
2+
println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");
3+
println!("cargo:rerun-if-env-changed=OPENCONNECT_NO_PKG_CONFIG");
4+
5+
if std::env::var_os("CARGO_FEATURE_SYSTEM_LIBOPENCONNECT").is_none() {
6+
return;
7+
}
8+
9+
if std::env::var_os("OPENCONNECT_NO_PKG_CONFIG").is_some() {
10+
println!("cargo:rustc-link-lib=openconnect");
11+
return;
12+
}
13+
14+
pkg_config::Config::new()
15+
.atleast_version("8.20")
16+
.probe("openconnect")
17+
.expect("system-libopenconnect requires libopenconnect development files");
18+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use std::ffi::NulError;
2+
3+
#[derive(Debug, thiserror::Error)]
4+
pub enum OpenConnectError {
5+
#[error("OpenConnect support was built without libopenconnect")]
6+
NotLinked,
7+
#[error("{field} contains an interior NUL byte")]
8+
InteriorNul {
9+
field: &'static str,
10+
#[source]
11+
source: NulError,
12+
},
13+
#[error("libopenconnect initialization failed with code {0}")]
14+
InitFailed(i32),
15+
#[error("unable to allocate OpenConnect session")]
16+
AllocationFailed,
17+
#[error("{operation} failed with code {code}")]
18+
OperationFailed { operation: &'static str, code: i32 },
19+
}
20+
21+
pub type Result<T> = std::result::Result<T, OpenConnectError>;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#[derive(Debug, Clone, PartialEq, Eq)]
2+
pub enum OpenConnectEvent {
3+
Progress {
4+
level: ProgressLevel,
5+
message: String,
6+
},
7+
CertificateValidationRequired {
8+
reason: String,
9+
},
10+
AuthenticationFormRequired,
11+
Connected,
12+
Disconnected,
13+
Error(String),
14+
}
15+
16+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17+
pub enum ProgressLevel {
18+
Error,
19+
Info,
20+
Debug,
21+
Trace,
22+
Unknown(i32),
23+
}
24+
25+
impl From<i32> for ProgressLevel {
26+
fn from(value: i32) -> Self {
27+
match value {
28+
0 => Self::Error,
29+
1 => Self::Info,
30+
2 => Self::Debug,
31+
3 => Self::Trace,
32+
other => Self::Unknown(other),
33+
}
34+
}
35+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#![cfg(feature = "system-libopenconnect")]
2+
3+
use std::ffi::{c_char, c_int, c_void};
4+
5+
#[repr(C)]
6+
pub struct OpenconnectInfo {
7+
_private: [u8; 0],
8+
}
9+
10+
pub type ValidatePeerCertCallback =
11+
unsafe extern "C" fn(privdata: *mut c_void, reason: *const c_char) -> c_int;
12+
pub type WriteNewConfigCallback =
13+
unsafe extern "C" fn(privdata: *mut c_void, buf: *const c_char, buflen: c_int) -> c_int;
14+
pub type ProcessAuthFormCallback =
15+
unsafe extern "C" fn(privdata: *mut c_void, form: *mut c_void) -> c_int;
16+
17+
unsafe extern "C" {
18+
pub fn openconnect_init_ssl() -> c_int;
19+
pub fn openconnect_get_version() -> *const c_char;
20+
21+
pub fn openconnect_vpninfo_new(
22+
useragent: *const c_char,
23+
validate_peer_cert: Option<ValidatePeerCertCallback>,
24+
write_new_config: Option<WriteNewConfigCallback>,
25+
process_auth_form: Option<ProcessAuthFormCallback>,
26+
progress: *mut c_void,
27+
privdata: *mut c_void,
28+
) -> *mut OpenconnectInfo;
29+
30+
pub fn openconnect_vpninfo_free(vpninfo: *mut OpenconnectInfo);
31+
pub fn openconnect_parse_url(vpninfo: *mut OpenconnectInfo, url: *const c_char) -> c_int;
32+
pub fn openconnect_set_protocol(
33+
vpninfo: *mut OpenconnectInfo,
34+
protocol: *const c_char,
35+
) -> c_int;
36+
pub fn openconnect_set_cafile(vpninfo: *mut OpenconnectInfo, cafile: *const c_char) -> c_int;
37+
pub fn openconnect_set_loglevel(vpninfo: *mut OpenconnectInfo, level: c_int);
38+
pub fn openconnect_set_system_trust(vpninfo: *mut OpenconnectInfo, val: u32);
39+
pub fn openconnect_disable_ipv6(vpninfo: *mut OpenconnectInfo) -> c_int;
40+
pub fn openconnect_disable_dtls(vpninfo: *mut OpenconnectInfo) -> c_int;
41+
pub fn openconnect_get_protocol(vpninfo: *mut OpenconnectInfo) -> *const c_char;
42+
pub fn openconnect_get_connect_url(vpninfo: *mut OpenconnectInfo) -> *const c_char;
43+
}

0 commit comments

Comments
 (0)