Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
491 changes: 491 additions & 0 deletions src-tauri/Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
webbrowser = "1.0"
tempfile = "3"
futures-util = "0.3"
http = "1"
image = "0.25"
qrcode = { version = "0.14", features = ["image"] }
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
vergen-git2 = { version = "9.1", features = ["build"] }

[workspace.package]
Expand Down
7 changes: 7 additions & 0 deletions src-tauri/client-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ common = { package = "defguard-client-common", path = "../common" }
defguard_core = { package = "defguard-client-core", path = "../core" }
defguard_client_posture = { package = "defguard-client-posture", path = "../enterprise/posture" }
defguard_client_proto = { package = "defguard-client-proto", path = "../client-proto" }
base64.workspace = true
reqwest.workspace = true
secrecy.workspace = true
serde = { workspace = true, features = ["derive"] }
Expand All @@ -29,12 +30,18 @@ tokio.workspace = true
tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
webbrowser.workspace = true
http.workspace = true
qrcode.workspace = true
image.workspace = true
tokio-tungstenite.workspace = true
futures-util.workspace = true

# Dummy feature to let tauri build the release.
[features]
custom-protocol = []

[dev-dependencies]
sha1 = "0.10"
tempfile.workspace = true
tokio-stream = "0.1"
tonic.workspace = true
5 changes: 5 additions & 0 deletions src-tauri/client-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ pub enum Commands {
#[arg(long)]
mfa_method: Option<String>,

/// Save the mobile-approve MFA QR code as a PNG image to this path.
/// Required when stderr is not a terminal.
#[arg(long)]
qr_file: Option<String>,

/// Override route-all-traffic for this connection only.
#[arg(long, overrides_with = "predefined_traffic")]
all_traffic: bool,
Expand Down
28 changes: 25 additions & 3 deletions src-tauri/client-cli/src/commands/connect.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::io::{stdin, IsTerminal};
use std::io::{stderr, stdin, IsTerminal};

use defguard_client_posture::{authorize_posture_session, get_posture_data};
use defguard_client_proto::defguard::client_types::MfaMethod;
Expand Down Expand Up @@ -30,6 +30,7 @@ pub async fn handle(
code: Option<&str>,
code_command: Option<&str>,
mfa_method: Option<&str>,
qr_file: Option<&str>,
all_traffic: bool,
predefined_traffic: bool,
json: bool,
Expand Down Expand Up @@ -81,8 +82,10 @@ pub async fn handle(
ResolvedTarget::Location(location) => {
if location.mfa_enabled() {
// Resolve the effective MFA method.
// Also rejects --code / --code-command when the method is OIDC.
let method = mfa::resolve_method(location, mfa_method, code, code_command)?;
let method = mfa::resolve_method(location, mfa_method)?;

// Reject flags that are incompatible with the resolved method.
mfa::validate_mfa_flags(method, &location.name, code, code_command, qr_file)?;

let instance = Instance::find_by_id(&state.pool, location.instance_id)
.await
Expand All @@ -106,6 +109,25 @@ pub async fn handle(
let psk = if method == MfaMethod::Oidc {
mfa::authorize_oidc(location, &instance, posture_data, &state.pool, json)
.await?
} else if method == MfaMethod::MobileApprove {
// Fail-fast: if neither stderr is a TTY nor --qr-file is set,
// the user cannot scan the QR. Do not call /start.
if !stderr().is_terminal() && qr_file.is_none() {
return Err(CliError::InvalidInput(
"No QR display available (stderr is not a TTY). \
Use --qr-file <path> to save the QR as a PNG image."
.into(),
));
}
mfa::authorize_mobile_approve(
location,
&instance,
posture_data,
qr_file,
&state.pool,
json,
)
.await?
} else {
// Determine the MFA code source from CLI flags.
let code_source = code
Expand Down
7 changes: 5 additions & 2 deletions src-tauri/client-cli/src/commands/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub struct InstanceListResult {
impl CommandOutput for InstanceListResult {
fn human(&self) -> String {
if self.instances.is_empty() {
"No instances configured.".to_string()
"No instances configured. Use the desktop app to enroll first.".to_string()
} else {
format_instance_list_table(&self.instances)
}
Expand Down Expand Up @@ -149,7 +149,10 @@ mod tests {
let result = InstanceListResult {
instances: Vec::new(),
};
assert_eq!(result.human(), "No instances configured.");
assert_eq!(
result.human(),
"No instances configured. Use the desktop app to enroll first."
);
}

#[test]
Expand Down
7 changes: 5 additions & 2 deletions src-tauri/client-cli/src/commands/location.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ pub struct LocationListResult {
impl CommandOutput for LocationListResult {
fn human(&self) -> String {
if self.locations.is_empty() {
"No locations configured.".to_string()
"No locations configured. Use the desktop app to enroll an instance first.".to_string()
} else {
format_location_list_table(&self.locations, &self.instance_names)
}
Expand Down Expand Up @@ -328,7 +328,10 @@ mod tests {
locations: Vec::new(),
instance_names: HashMap::new(),
};
assert_eq!(result.human(), "No locations configured.");
assert_eq!(
result.human(),
"No locations configured. Use the desktop app to enroll an instance first."
);
}

#[test]
Expand Down
7 changes: 5 additions & 2 deletions src-tauri/client-cli/src/commands/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub struct TunnelListResult {
impl CommandOutput for TunnelListResult {
fn human(&self) -> String {
if self.tunnels.is_empty() {
"No tunnels configured.".to_string()
"No tunnels configured. Import tunnels via the desktop app.".to_string()
} else {
format_tunnel_list_table(&self.tunnels)
}
Expand Down Expand Up @@ -199,7 +199,10 @@ mod tests {
let result = TunnelListResult {
tunnels: Vec::new(),
};
assert_eq!(result.human(), "No tunnels configured.");
assert_eq!(
result.human(),
"No tunnels configured. Import tunnels via the desktop app."
);
}

#[test]
Expand Down
5 changes: 4 additions & 1 deletion src-tauri/client-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ mod exit;
mod logging;
mod mfa;
mod mfa_code;
mod mfa_qr;
mod output;
mod resolve;
mod state;
#[cfg(test)]
#[cfg(all(test, target_os = "linux"))]
mod tests_daemon;
#[cfg(test)]
mod tests_proxy;
Expand Down Expand Up @@ -62,6 +63,7 @@ async fn main() -> ExitCode {
code,
code_command,
mfa_method,
qr_file,
all_traffic,
predefined_traffic,
} => output::finish(
Expand All @@ -74,6 +76,7 @@ async fn main() -> ExitCode {
code.as_deref(),
code_command.as_deref(),
mfa_method.as_deref(),
qr_file.as_deref(),
all_traffic,
predefined_traffic,
cli.json,
Expand Down
Loading
Loading