diff --git a/options.md b/options.md index 2f82194..5fdaf57 100644 --- a/options.md +++ b/options.md @@ -12,6 +12,8 @@ | `cert-id=` | hexadecimal ID of PKCS11 certificate, bytes could be optionally separated with colon | | `search-domains=` | additional search domains for DNS resolver, comma-separated | | `ignore-search-domains=` | acquired search domains to ignore | +| `dns-servers=` | additional DNS servers, comma-separated | +| `ignore-dns-servers=` | acquired DNS servers to ignore, comma-separated | | `default-route=true\|false` | set default route through the VPN tunnel, default is false | | `no-routing=true\|false` | ignore all routes acquired from the VPN server, default is false | | `add-routes=` | additional static routes, comma-separated, in the format of x.x.x.x/x | diff --git a/snx-rs-gui/src/settings.rs b/snx-rs-gui/src/settings.rs index 4a733ca..f52c10f 100644 --- a/snx-rs-gui/src/settings.rs +++ b/snx-rs-gui/src/settings.rs @@ -1,5 +1,3 @@ -use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; - use anyhow::anyhow; use async_channel::Sender; use gtk::{ @@ -8,6 +6,8 @@ use gtk::{ Align, ButtonsType, DialogFlags, MessageType, Orientation, ResponseType, WindowPosition, }; use ipnet::Ipv4Net; +use std::net::Ipv4Addr; +use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; use tracing::warn; use crate::tray::TrayCommand; @@ -43,6 +43,8 @@ struct MyWidgets { no_dns: gtk::CheckButton, search_domains: gtk::Entry, ignored_domains: gtk::Entry, + dns_servers: gtk::Entry, + ignored_dns_servers: gtk::Entry, no_routing: gtk::CheckButton, default_routing: gtk::CheckButton, add_routes: gtk::Entry, @@ -101,6 +103,20 @@ impl MyWidgets { self.esp_lifetime.text().parse::()?; self.ike_port.text().parse::()?; + let dns_servers = self.dns_servers.text(); + if !dns_servers.is_empty() { + for r in dns_servers.split(',') { + r.parse::()?; + } + } + + let ignored_dns_servers = self.ignored_dns_servers.text(); + if !ignored_dns_servers.is_empty() { + for r in ignored_dns_servers.split(',') { + r.parse::()?; + } + } + let add_routes = self.add_routes.text(); if !add_routes.is_empty() { for r in add_routes.split(',') { @@ -155,6 +171,30 @@ impl SettingsDialog { .text(params.ignore_search_domains.join(",")) .build(); + let dns_servers = gtk::Entry::builder() + .placeholder_text("Comma-separated IP addresses") + .text( + params + .dns_servers + .iter() + .map(|r| r.to_string()) + .collect::>() + .join(","), + ) + .build(); + + let ignored_dns_servers = gtk::Entry::builder() + .placeholder_text("Comma-separated IP addresses") + .text( + params + .ignore_dns_servers + .iter() + .map(|r| r.to_string()) + .collect::>() + .join(","), + ) + .build(); + let no_routing = gtk::CheckButton::builder().active(params.no_routing).build(); let default_routing = gtk::CheckButton::builder().active(params.default_route).build(); @@ -336,6 +376,8 @@ impl SettingsDialog { no_dns, search_domains, ignored_domains, + dns_servers, + ignored_dns_servers, no_routing, default_routing, add_routes, @@ -418,6 +460,20 @@ impl SettingsDialog { .split(',') .map(|s| s.trim().to_owned()) .collect(); + params.dns_servers = self + .widgets + .dns_servers + .text() + .split(',') + .flat_map(|s| s.trim().parse().ok()) + .collect(); + params.ignore_dns_servers = self + .widgets + .ignored_dns_servers + .text() + .split(',') + .flat_map(|s| s.trim().parse().ok()) + .collect(); params.no_routing = self.widgets.no_routing.is_active(); params.default_route = self.widgets.default_routing.is_active(); params.add_routes = self @@ -580,6 +636,14 @@ impl SettingsDialog { no_dns.pack_start(&self.widgets.no_dns, false, true, 0); dns_box.pack_start(&no_dns, false, true, 6); + let dns_servers = self.form_box("Additional DNS servers"); + dns_servers.pack_start(&self.widgets.dns_servers, false, true, 0); + dns_box.pack_start(&dns_servers, false, true, 6); + + let ignored_dns_servers = self.form_box("Ignored DNS servers"); + ignored_dns_servers.pack_start(&self.widgets.ignored_dns_servers, false, true, 0); + dns_box.pack_start(&ignored_dns_servers, false, true, 6); + let search_domains = self.form_box("Additional search domains"); search_domains.pack_start(&self.widgets.search_domains, false, true, 0); dns_box.pack_start(&search_domains, false, true, 6); diff --git a/snx-rs/src/cmdline.rs b/snx-rs/src/cmdline.rs index 83c902d..97a7d26 100644 --- a/snx-rs/src/cmdline.rs +++ b/snx-rs/src/cmdline.rs @@ -1,7 +1,7 @@ -use std::{path::PathBuf, time::Duration}; - use clap::Parser; use ipnet::Ipv4Net; +use std::net::Ipv4Addr; +use std::{path::PathBuf, time::Duration}; use tracing::level_filters::LevelFilter; use snxcore::model::params::{CertType, OperationMode, TunnelParams, TunnelType}; @@ -52,6 +52,22 @@ pub struct CmdlineParams { )] pub ignore_search_domains: Vec, + #[clap( + long = "dns-servers", + short = 'D', + value_delimiter = ',', + help = "Additional DNS servers" + )] + pub dns_servers: Vec, + + #[clap( + long = "ignore-dns-servers", + short = 'G', + value_delimiter = ',', + help = "Ignore specified DNS servers from the acquired list" + )] + pub ignore_dns_servers: Vec, + #[clap( long = "default-route", short = 't', @@ -215,6 +231,14 @@ impl CmdlineParams { other.ignore_search_domains = self.ignore_search_domains; } + if !self.dns_servers.is_empty() { + other.dns_servers = self.dns_servers; + } + + if !self.ignore_dns_servers.is_empty() { + other.ignore_dns_servers = self.ignore_dns_servers; + } + if let Some(default_route) = self.default_route { other.default_route = default_route; } diff --git a/snxcore/src/model/params.rs b/snxcore/src/model/params.rs index 2313d3b..e5f0873 100644 --- a/snxcore/src/model/params.rs +++ b/snxcore/src/model/params.rs @@ -1,3 +1,9 @@ +use anyhow::anyhow; +use base64::Engine; +use directories_next::ProjectDirs; +use ipnet::Ipv4Net; +use serde::{Deserialize, Serialize}; +use std::net::Ipv4Addr; use std::{ fmt, fs, io::{Cursor, Write}, @@ -5,12 +11,6 @@ use std::{ str::FromStr, time::Duration, }; - -use anyhow::anyhow; -use base64::Engine; -use directories_next::ProjectDirs; -use ipnet::Ipv4Net; -use serde::{Deserialize, Serialize}; use tracing::warn; use crate::util; @@ -202,6 +202,8 @@ pub struct TunnelParams { pub log_level: String, pub search_domains: Vec, pub ignore_search_domains: Vec, + pub dns_servers: Vec, + pub ignore_dns_servers: Vec, pub default_route: bool, pub no_routing: bool, pub add_routes: Vec, @@ -239,6 +241,8 @@ impl Default for TunnelParams { log_level: "off".to_owned(), search_domains: Vec::new(), ignore_search_domains: Vec::new(), + dns_servers: Vec::new(), + ignore_dns_servers: Vec::new(), default_route: false, no_routing: false, add_routes: Vec::new(), @@ -289,6 +293,10 @@ impl TunnelParams { "ignore-search-domains" => { params.ignore_search_domains = v.split(',').map(|s| s.trim().to_owned()).collect(); } + "dns-servers" => params.dns_servers = v.split(',').flat_map(|s| s.trim().parse().ok()).collect(), + "ignore-dns-servers" => { + params.ignore_dns_servers = v.split(',').flat_map(|s| s.trim().parse().ok()).collect(); + } "default-route" => params.default_route = v.parse().unwrap_or_default(), "no-routing" => params.no_routing = v.parse().unwrap_or_default(), "add-routes" => params.add_routes = v.split(',').flat_map(|s| s.trim().parse().ok()).collect(), @@ -341,6 +349,24 @@ impl TunnelParams { )?; writeln!(buf, "search-domains={}", self.search_domains.join(","))?; writeln!(buf, "ignore-search-domains={}", self.ignore_search_domains.join(","))?; + writeln!( + buf, + "dns-servers={}", + self.dns_servers + .iter() + .map(|r| r.to_string()) + .collect::>() + .join(",") + )?; + writeln!( + buf, + "ignore-dns-servers={}", + self.ignore_dns_servers + .iter() + .map(|r| r.to_string()) + .collect::>() + .join(",") + )?; writeln!(buf, "default-route={}", self.default_route)?; writeln!(buf, "no-routing={}", self.no_routing)?; writeln!( diff --git a/snxcore/src/model/proto.rs b/snxcore/src/model/proto.rs index 65368f4..f86c3bc 100644 --- a/snxcore/src/model/proto.rs +++ b/snxcore/src/model/proto.rs @@ -92,7 +92,7 @@ impl Serialize for AuthenticationAlgorithm { pub struct OfficeMode { pub ipaddr: String, pub keep_address: Option, - pub dns_servers: Option>, + pub dns_servers: Option>, pub dns_suffix: Option, } diff --git a/snxcore/src/platform.rs b/snxcore/src/platform.rs index c47d4f6..cd34fa4 100644 --- a/snxcore/src/platform.rs +++ b/snxcore/src/platform.rs @@ -68,7 +68,7 @@ async fn udp_send_receive(socket: &UdpSocket, data: &[u8], timeout: Duration) -> #[derive(Debug, Clone, Default, PartialEq)] pub struct ResolverConfig { pub search_domains: Vec, - pub dns_servers: Vec, + pub dns_servers: Vec, } #[async_trait] diff --git a/snxcore/src/platform/linux/resolver.rs b/snxcore/src/platform/linux/resolver.rs index f6505e3..9fbbc29 100644 --- a/snxcore/src/platform/linux/resolver.rs +++ b/snxcore/src/platform/linux/resolver.rs @@ -29,9 +29,9 @@ impl ResolverConfigurator for SystemdResolvedConfigurator { crate::util::run_command("resolvectl", args).await?; crate::util::run_command("resolvectl", ["default-route", &self.device, "false"]).await?; - let mut args = vec!["dns", &self.device]; + let mut args = vec!["dns".to_owned(), self.device.clone()]; - let servers = config.dns_servers.iter().map(|s| s.trim()).collect::>(); + let servers = config.dns_servers.iter().map(|s| s.to_string()).collect::>(); args.extend(servers); @@ -118,7 +118,9 @@ impl ResolvConfConfigurator { let existing_nameservers = conf .lines() - .filter(|line| line.starts_with("nameserver") && !config.dns_servers.iter().any(|s| line.contains(s))) + .filter(|line| { + line.starts_with("nameserver") && !config.dns_servers.iter().any(|s| line.contains(&s.to_string())) + }) .collect::>(); let other_lines = conf @@ -339,7 +341,7 @@ mod tests { let config = ResolverConfig { search_domains: vec!["dom1.com".to_owned(), "dom2.net".to_owned()], - dns_servers: vec!["192.168.1.1".to_owned(), "192.168.1.2".to_owned()], + dns_servers: vec!["192.168.1.1".parse().unwrap(), "192.168.1.2".parse().unwrap()], }; cut.configure(&config).await.unwrap(); @@ -358,7 +360,7 @@ mod tests { let config = ResolverConfig { search_domains: vec!["dom1.com".to_owned(), "dom2.net".to_owned()], - dns_servers: vec!["192.168.1.1".to_owned(), "192.168.1.2".to_owned()], + dns_servers: vec!["192.168.1.1".parse().unwrap(), "192.168.1.2".parse().unwrap()], }; cut.cleanup(&config).await.unwrap(); diff --git a/snxcore/src/platform/linux/xfrm.rs b/snxcore/src/platform/linux/xfrm.rs index 1b8f19a..fce42cb 100644 --- a/snxcore/src/platform/linux/xfrm.rs +++ b/snxcore/src/platform/linux/xfrm.rs @@ -400,7 +400,9 @@ impl XfrmConfigurator { .ipsec_session .dns .iter() - .map(|server| server.to_string()) + .chain(self.tunnel_params.dns_servers.iter()) + .filter(|s| !self.tunnel_params.ignore_dns_servers.iter().any(|d| *d == **s)) + .cloned() .collect::>(); let resolver = new_resolver_configurator(&self.name)?; diff --git a/snxcore/src/tunnel/ssl/device.rs b/snxcore/src/tunnel/ssl/device.rs index 1d9df7a..9c3e802 100644 --- a/snxcore/src/tunnel/ssl/device.rs +++ b/snxcore/src/tunnel/ssl/device.rs @@ -90,11 +90,17 @@ impl TunDevice { Vec::new() }; - let dns_servers = if let Some(ref servers) = self.reply.office_mode.dns_servers { - servers.clone() - } else { - Vec::new() - }; + let dns_servers = self + .reply + .office_mode + .dns_servers + .clone() + .unwrap_or_default() + .iter() + .chain(params.dns_servers.iter()) + .filter(|s| !params.ignore_dns_servers.iter().any(|d| *d == **s)) + .cloned() + .collect::>(); let config = ResolverConfig { search_domains,