feat(connlib): allow glob patterns for matching domain names (#5901)

Currently, `connlib` can only handle "simple" DNS wildcards where `*`
matches any number of subdomains, including zero and `?` matches a
single subdomain.

With this PR, we expand `connlib'`s capabilities to allow for a much
more complex matching of domains that more closely resembles glob
patterns:

- `**` matches any number of subdomains. This supersedes the previous
`*` operator.
- `*` matches a single subdomain. This supersedes the previous `?`
operator.
- `?` matches a single character. This wasn't possible before.
- Additionally, any of these can be combined. Previously, only `*` or
`?` was allowed and they were only accepted at the front of the domain
name pattern.

Resolves: #5056.

---------

Signed-off-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
Thomas Eizinger
2024-08-15 02:30:53 +01:00
committed by GitHub
parent b1b9b552c2
commit 7c70850217
25 changed files with 353 additions and 202 deletions

View File

@@ -156,9 +156,9 @@ jobs:
artifact: firezone-client-headless-linux
image_name: client
# mark:next-headless-version
release_name: headless-client-1.1.8
release_name: headless-client-1.2.0
# mark:next-headless-version
version: 1.1.8
version: 1.2.0
- package: firezone-relay
artifact: firezone-relay
image_name: relay
@@ -166,9 +166,9 @@ jobs:
artifact: firezone-gateway
image_name: gateway
# mark:next-gateway-version
release_name: gateway-1.1.6
release_name: gateway-1.2.0
# mark:next-gateway-version
version: 1.1.6
version: 1.2.0
- package: snownet-tests
artifact: snownet-tests
image_name: snownet-tests
@@ -352,7 +352,7 @@ jobs:
- name: relay
- name: gateway
# mark:next-gateway-version
version: 1.1.6
version: 1.2.0
- name: client
# mark:next-client-version
version: 1.0.6

View File

@@ -26,31 +26,31 @@ jobs:
include:
- runs-on: ubuntu-20.04
# mark:next-gui-version
binary-dest-path: firezone-client-gui-linux_1.1.13_x86_64
binary-dest-path: firezone-client-gui-linux_1.2.0_x86_64
rename-script: ../../scripts/build/tauri-rename-ubuntu.sh
upload-script: ../../scripts/build/tauri-upload-ubuntu.sh
# mark:next-gui-version
syms-artifact: rust/gui-client/firezone-client-gui-linux_1.1.13_x86_64.dwp
syms-artifact: rust/gui-client/firezone-client-gui-linux_1.2.0_x86_64.dwp
# mark:next-gui-version
pkg-artifact: rust/gui-client/firezone-client-gui-linux_1.1.13_x86_64.deb
pkg-artifact: rust/gui-client/firezone-client-gui-linux_1.2.0_x86_64.deb
- runs-on: ubuntu-20.04-arm
# mark:next-gui-version
binary-dest-path: firezone-client-gui-linux_1.1.13_aarch64
binary-dest-path: firezone-client-gui-linux_1.2.0_aarch64
rename-script: ../../scripts/build/tauri-rename-ubuntu.sh
upload-script: ../../scripts/build/tauri-upload-ubuntu.sh
# mark:next-gui-version
syms-artifact: rust/gui-client/firezone-client-gui-linux_1.1.13_aarch64.dwp
syms-artifact: rust/gui-client/firezone-client-gui-linux_1.2.0_aarch64.dwp
# mark:next-gui-version
pkg-artifact: rust/gui-client/firezone-client-gui-linux_1.1.13_aarch64.deb
pkg-artifact: rust/gui-client/firezone-client-gui-linux_1.2.0_aarch64.deb
- runs-on: windows-2019
# mark:next-gui-version
binary-dest-path: firezone-client-gui-windows_1.1.13_x86_64
binary-dest-path: firezone-client-gui-windows_1.2.0_x86_64
rename-script: ../../scripts/build/tauri-rename-windows.sh
upload-script: ../../scripts/build/tauri-upload-windows.sh
# mark:next-gui-version
syms-artifact: rust/gui-client/firezone-client-gui-windows_1.1.13_x86_64.pdb
syms-artifact: rust/gui-client/firezone-client-gui-windows_1.2.0_x86_64.pdb
# mark:next-gui-version
pkg-artifact: rust/gui-client/firezone-client-gui-windows_1.1.13_x86_64.msi
pkg-artifact: rust/gui-client/firezone-client-gui-windows_1.2.0_x86_64.msi
env:
BINARY_DEST_PATH: ${{ matrix.binary-dest-path }}
AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
@@ -96,7 +96,7 @@ jobs:
if: ${{ runner.os == 'Windows' }}
shell: bash
# mark:next-gui-version
run: ../../scripts/build/sign.sh ../target/release/bundle/msi/Firezone_1.1.13_x64_en-US.msi
run: ../../scripts/build/sign.sh ../target/release/bundle/msi/Firezone_1.2.0_x64_en-US.msi
- name: Rename artifacts and compute SHA256
shell: bash
run: ${{ matrix.rename-script }}
@@ -121,6 +121,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
# mark:next-gui-version
TAG_NAME: gui-client-1.1.13
TAG_NAME: gui-client-1.2.0
shell: bash
run: ${{ matrix.upload-script }}

View File

@@ -46,13 +46,13 @@ jobs:
matrix:
include:
# mark:next-gateway-version
- release_name: gateway-1.1.6
- release_name: gateway-1.2.0
config_name: release-drafter-gateway.yml
# mark:next-headless-version
- release_name: headless-client-1.1.8
- release_name: headless-client-1.2.0
config_name: release-drafter-headless-client.yml
# mark:next-gui-version
- release_name: gui-client-1.1.13
- release_name: gui-client-1.2.0
config_name: release-drafter-gui-client.yml
steps:
- uses: release-drafter/release-drafter@v6

View File

@@ -40,11 +40,11 @@ jobs:
if [[ "${{ github.event.release.name }}" =~ gateway* ]]; then
ARTIFACT=gateway
# mark:next-gateway-version
VERSION="1.1.6"
VERSION="1.2.0"
elif [[ "${{ github.event.release.name }}" =~ headless* ]]; then
ARTIFACT=client
# mark:next-headless-version
VERSION="1.1.8"
VERSION="1.2.0"
else
echo "Release doesn't require publishing Docker images"
exit 0

View File

@@ -56,7 +56,7 @@ android {
targetSdk = 35
versionCode = (System.currentTimeMillis() / 1000 / 10).toInt()
// mark:next-android-version
versionName = "1.1.7"
versionName = "1.2.0"
multiDexEnabled = true
testInstrumentationRunner = "dev.firezone.android.core.HiltTestRunner"
}

94
rust/Cargo.lock generated
View File

@@ -878,6 +878,7 @@ dependencies = [
"anstyle",
"clap_lex",
"strsim 0.11.0",
"terminal_size",
]
[[package]]
@@ -988,9 +989,15 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "condtype"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af"
[[package]]
name = "connlib-client-android"
version = "1.1.7"
version = "1.2.0"
dependencies = [
"android_log-sys",
"backoff",
@@ -1016,7 +1023,7 @@ dependencies = [
[[package]]
name = "connlib-client-apple"
version = "1.1.6"
version = "1.2.0"
dependencies = [
"anyhow",
"backoff",
@@ -1516,6 +1523,31 @@ dependencies = [
"syn 2.0.72",
]
[[package]]
name = "divan"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d567df2c9c2870a43f3f2bd65aaeb18dbce1c18f217c3e564b4fbaeb3ee56c"
dependencies = [
"cfg-if",
"clap",
"condtype",
"divan-macros",
"libc",
"regex-lite",
]
[[package]]
name = "divan-macros"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27540baf49be0d484d8f0130d7d8da3011c32a44d4fc873368154f1510e574a2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.72",
]
[[package]]
name = "dns-lookup"
version = "2.0.4"
@@ -1796,7 +1828,7 @@ dependencies = [
[[package]]
name = "firezone-gateway"
version = "1.1.6"
version = "1.2.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1832,7 +1864,7 @@ dependencies = [
[[package]]
name = "firezone-gui-client"
version = "1.1.13"
version = "1.2.0"
dependencies = [
"anyhow",
"arboard",
@@ -1884,7 +1916,7 @@ dependencies = [
[[package]]
name = "firezone-headless-client"
version = "1.1.8"
version = "1.2.0"
dependencies = [
"anyhow",
"atomicwrites",
@@ -1991,11 +2023,13 @@ dependencies = [
"chrono",
"connlib-shared",
"derivative",
"divan",
"domain",
"firezone-logging",
"firezone-relay",
"futures",
"futures-util",
"glob",
"hex",
"ip-packet",
"ip_network",
@@ -2011,6 +2045,7 @@ dependencies = [
"snownet",
"socket-factory",
"socket2",
"test-case",
"test-strategy",
"thiserror",
"tokio",
@@ -4893,6 +4928,12 @@ dependencies = [
"regex-syntax 0.8.3",
]
[[package]]
name = "regex-lite"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "regex-syntax"
version = "0.6.29"
@@ -6157,6 +6198,49 @@ dependencies = [
"utf-8",
]
[[package]]
name = "terminal_size"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "test-case"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8"
dependencies = [
"test-case-macros",
]
[[package]]
name = "test-case-core"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.72",
]
[[package]]
name = "test-case-macros"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.72",
"test-case-core",
]
[[package]]
name = "test-strategy"
version = "0.3.1"

View File

@@ -1,7 +1,7 @@
[package]
name = "connlib-client-android"
# mark:next-android-version
version = "1.1.7"
version = "1.2.0"
edition = "2021"
[lib]

View File

@@ -1,7 +1,7 @@
[package]
name = "connlib-client-apple"
# mark:next-apple-version
version = "1.1.6"
version = "1.2.0"
edition = "2021"
[features]

View File

@@ -10,10 +10,12 @@ boringtun = { workspace = true }
bytes = { version = "1.7", default-features = false, features = ["std"] }
chrono = { workspace = true }
connlib-shared = { workspace = true }
divan = { version = "0.1.14", optional = true }
domain = { workspace = true }
firezone-logging = { workspace = true }
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"] }
glob = "0.3.1"
hex = "0.4.3"
ip-packet = { workspace = true }
ip_network = { version = "0.4", default-features = false }
@@ -41,12 +43,19 @@ ip-packet = { workspace = true, features = ["proptest"] }
proptest-state-machine = "0.3"
rand = "0.8"
serde_json = "1.0"
test-case = "3.3.1"
test-strategy = "0.3.1"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[[bench]]
name = "divan"
harness = false
required-features = ["divan"]
[features]
proptest = ["dep:proptest"]
divan = ["dep:divan"]
[lints]
workspace = true

View File

@@ -0,0 +1,5 @@
extern crate firezone_tunnel; // Ensure benchmarks aren't optimised out.
fn main() {
divan::main()
}

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,6 @@
use crate::client::IpProvider;
use connlib_shared::messages::{DnsServer, ResourceId};
use connlib_shared::DomainName;
use domain::base::RelativeName;
use domain::base::{
iana::{Class, Rcode, Rtype},
Message, MessageBuilder, ToName,
@@ -10,6 +9,7 @@ use domain::rdata::AllRecordData;
use ip_packet::IpPacket;
use ip_packet::Packet as _;
use itertools::Itertools;
use pattern::{Candidate, Pattern};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
@@ -23,8 +23,8 @@ pub struct StubResolver {
fqdn_to_ips: HashMap<DomainName, Vec<IpAddr>>,
ips_to_fqdn: HashMap<IpAddr, (DomainName, ResourceId)>,
ip_provider: IpProvider,
/// All DNS resources we know about, indexed by their domain (could be wildcard domain like `*.mycompany.com`).
dns_resources: HashMap<String, ResourceId>,
/// All DNS resources we know about, indexed by the glob pattern they match against.
dns_resources: HashMap<Pattern, ResourceId>,
/// Fixed dns name that will be resolved to fixed ip addrs, similar to /etc/hosts
known_hosts: KnownHosts,
}
@@ -112,8 +112,16 @@ impl StubResolver {
Some((fqdn, self.fqdn_to_ips.get(fqdn).unwrap()))
}
pub(crate) fn add_resource(&mut self, id: ResourceId, address: String) -> bool {
let existing = self.dns_resources.insert(address, id);
pub(crate) fn add_resource(&mut self, id: ResourceId, pattern: String) -> bool {
let parsed_pattern = match Pattern::new(&pattern) {
Ok(p) => p,
Err(e) => {
tracing::warn!(%pattern, "Domain pattern is not valid: {e}");
return false;
}
};
let existing = self.dns_resources.insert(parsed_pattern, id);
existing.is_none()
}
@@ -157,8 +165,26 @@ impl StubResolver {
ips
}
fn match_resource(&self, domain_name: &DomainName) -> Option<ResourceId> {
match_domain(domain_name, &self.dns_resources)
/// Attempts to match the given domain against our list of possible patterns.
///
/// This performs a linear search and is thus O(N) and **must not** be called in the hot-path of packet routing.
#[tracing::instrument(level = "trace", skip_all, fields(domain))]
fn match_resource_linear(&self, domain_name: &DomainName) -> Option<ResourceId> {
let name = Candidate::from_domain(domain_name);
for (pattern, id) in &self.dns_resources {
if pattern.matches(&name) {
tracing::trace!(%id, %pattern, "Matched domain");
return Some(*id);
}
tracing::trace!(%pattern, %id, "No match");
}
tracing::debug!("No resources matched");
None
}
fn resource_address_name_by_reservse_dns(
@@ -225,7 +251,8 @@ impl StubResolver {
return Some(ResolveStrategy::LocalResponse(packet));
}
let maybe_resource = self.match_resource(&domain);
// `match_resource` is `O(N)` which we deem fine for DNS queries.
let maybe_resource = self.match_resource_linear(&domain);
let resource_records = match (qtype, maybe_resource) {
(_, Some(resource)) if !self.knows_resource(&resource) => {
@@ -304,68 +331,17 @@ fn build_dns_with_answer(
}
pub fn is_subdomain(name: &DomainName, resource: &str) -> bool {
let question_mark = RelativeName::<Vec<_>>::from_octets(b"\x01?".as_ref().into()).unwrap();
let Ok(resource) = DomainName::vec_from_str(resource) else {
return false;
let pattern = match Pattern::new(resource) {
Ok(p) => p,
Err(e) => {
tracing::warn!(%resource, "Unable to parse pattern: {e}");
return false;
}
};
if resource.starts_with(&question_mark) {
return resource
.parent()
.is_some_and(|r| r == name || name.parent().is_some_and(|n| r == n));
}
let candidate = Candidate::from_domain(name);
if resource.starts_with(&RelativeName::wildcard_vec()) {
let Some(resource) = resource.parent() else {
return false;
};
return name.iter_suffixes().any(|n| n == resource);
}
name == &resource
}
fn match_domain<T>(name: &DomainName, resources: &HashMap<String, T>) -> Option<T>
where
T: Copy,
{
// Safety: `?` is less than 254 bytes long.
const QUESTION_MARK: RelativeName<&'static [u8]> =
unsafe { RelativeName::from_octets_unchecked(b"\x01?") };
// Safety: `*` is less than 254 bytes long.
const WILDCARD: RelativeName<&'static [u8]> =
unsafe { RelativeName::from_octets_unchecked(b"\x01*") };
// First, check for full match.
if let Some(resource) = resources.get(&name.to_string()) {
return Some(*resource);
}
// Second, check for `?` matching this domain exactly.
let qm_dot_domain = QUESTION_MARK.chain(name).ok()?.to_string();
if let Some(resource) = resources.get(&qm_dot_domain) {
return Some(*resource);
}
// Third, check for `?` matching up to 1 parent.
if let Some(parent) = name.parent() {
let qm_dot_parent = QUESTION_MARK.chain(parent).ok()?.to_string();
if let Some(resource) = resources.get(&qm_dot_parent) {
return Some(*resource);
}
}
// Last, check for any wildcard domains, starting with the most specific one.
for suffix in name.iter_suffixes() {
let wildcard_dot_suffix = WILDCARD.chain(suffix).ok()?.to_string();
if let Some(resource) = resources.get(&wildcard_dot_suffix) {
return Some(*resource);
}
}
None
pattern.matches(&candidate)
}
fn reverse_dns_addr(name: &str) -> Option<IpAddr> {
@@ -438,9 +414,78 @@ fn ips_to_fqdn_for_known_hosts(
.collect()
}
mod pattern {
use super::*;
use std::{convert::Infallible, fmt, str::FromStr};
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Pattern {
inner: glob::Pattern,
original: String,
}
impl fmt::Display for Pattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.original.fmt(f)
}
}
impl Pattern {
pub fn new(p: &str) -> Result<Self, glob::PatternError> {
Ok(Self {
inner: glob::Pattern::new(&p.replace('.', "/"))?,
original: p.to_string(),
})
}
/// Matches a [`Candidate`] against this [`Pattern`].
///
/// Matching only requires a reference, thus allowing users to test a [`Candidate`] against multiple [`Pattern`]s.
pub fn matches(&self, domain: &Candidate) -> bool {
let domain = domain.0.as_str();
if let Some(rem) = self.inner.as_str().strip_prefix("*/") {
if domain == rem {
return true;
}
}
self.inner.matches_with(
domain,
glob::MatchOptions {
case_sensitive: false,
require_literal_separator: true,
require_literal_leading_dot: false,
},
)
}
}
/// A candidate for matching against a domain [`Pattern`].
///
/// Creates a type-safe contract that replaces `.` with `/` in the domain which is requires for pattern matching.
pub struct Candidate(String);
impl Candidate {
pub fn from_domain(domain: &DomainName) -> Self {
Self(domain.to_string().replace('.', "/"))
}
}
impl FromStr for Candidate {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.replace('.', "/")))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr as _;
use test_case::test_case;
#[test]
fn reverse_dns_addr_works_v4() {
@@ -496,66 +541,82 @@ mod tests {
}
#[test]
fn wildcard_matching() {
let resources = HashMap::from([("*.foo.com".to_string(), 0), ("*.com".to_string(), 1)]);
fn pattern_displays_without_slash() {
let pattern = Pattern::new("**.example.com").unwrap();
assert_eq!(match_domain(&domain("a.foo.com"), &resources), Some(0));
assert_eq!(match_domain(&domain("foo.com"), &resources), Some(0));
assert_eq!(match_domain(&domain("a.b.foo.com"), &resources), Some(0));
assert_eq!(match_domain(&domain("oo.com"), &resources), Some(1));
assert_eq!(match_domain(&domain("oo.xyz"), &resources), None);
assert_eq!(pattern.to_string(), "**.example.com")
}
#[test]
fn question_mark_matching() {
let resources = HashMap::from([("?.bar.com".to_string(), 1)]);
#[test_case("**.example.com", "example.com"; "double star matches root domain")]
#[test_case("app.**.example.com", "app.bar.foo.example.com"; "double star matches multiple levels within domain")]
#[test_case("**.example.com", "foo.example.com"; "double star matches one level")]
#[test_case("**.example.com", "foo.bar.example.com"; "double star matches two levels")]
#[test_case("*.example.com", "foo.example.com"; "single star matches one level")]
#[test_case("*.example.com", "example.com"; "single star matches root domain")]
#[test_case("foo.*.example.com", "foo.bar.example.com"; "single star matches one domain within domain")]
#[test_case("app.*.*.example.com", "app.foo.bar.example.com"; "single star can appear on multiple levels")]
#[test_case("app.f??.example.com", "app.foo.example.com"; "question mark matches one letter")]
#[test_case("app.example.com", "app.example.com"; "matches literal domain")]
#[test_case("*?*.example.com", "app.example.com"; "mix of * and ?")]
#[test_case("app.**.web.**.example.com", "app.web.example.com"; "multiple double star within domain")]
assert_eq!(match_domain(&domain("a.bar.com"), &resources), Some(1));
assert_eq!(match_domain(&domain("bar.com"), &resources), Some(1));
assert_eq!(match_domain(&domain("a.b.bar.com"), &resources), None);
fn domain_pattern_matches(pattern: &str, domain: &str) {
let pattern = Pattern::new(pattern).unwrap();
let candidate = Candidate::from_str(domain).unwrap();
let matches = pattern.matches(&candidate);
assert!(matches);
}
#[test]
fn exact_matching() {
let resources = HashMap::from([("baz.com".to_string(), 2)]);
#[test_case("app.*.example.com", "app.foo.bar.example.com"; "single star does not match two level")]
#[test_case("app.*com", "app.foo.com"; "single star does not match dot")]
#[test_case("app?com", "app.com"; "question mark does not match dot")]
fn domain_pattern_does_not_match(pattern: &str, domain: &str) {
let pattern = Pattern::new(pattern).unwrap();
let candidate = Candidate::from_str(domain).unwrap();
assert_eq!(match_domain(&domain("baz.com"), &resources), Some(2));
assert_eq!(match_domain(&domain("a.baz.com"), &resources), None);
assert_eq!(match_domain(&domain("a.b.baz.com"), &resources), None);
}
let matches = pattern.matches(&candidate);
#[test]
fn exact_subdomain_match() {
assert!(is_subdomain(&domain("foo.com"), "foo.com"));
assert!(!is_subdomain(&domain("a.foo.com"), "foo.com"));
assert!(!is_subdomain(&domain("a.b.foo.com"), "foo.com"));
assert!(!is_subdomain(&domain("foo.com"), "a.foo.com"));
}
#[test]
fn wildcard_subdomain_match() {
assert!(is_subdomain(&domain("foo.com"), "*.foo.com"));
assert!(is_subdomain(&domain("a.foo.com"), "*.foo.com"));
assert!(is_subdomain(&domain("a.foo.com"), "*.a.foo.com"));
assert!(is_subdomain(&domain("b.a.foo.com"), "*.a.foo.com"));
assert!(is_subdomain(&domain("a.b.foo.com"), "*.foo.com"));
assert!(!is_subdomain(&domain("afoo.com"), "*.foo.com"));
assert!(!is_subdomain(&domain("b.afoo.com"), "*.foo.com"));
assert!(!is_subdomain(&domain("bar.com"), "*.foo.com"));
assert!(!is_subdomain(&domain("foo.com"), "*.a.foo.com"));
}
#[test]
fn question_mark_subdomain_match() {
assert!(is_subdomain(&domain("foo.com"), "?.foo.com"));
assert!(is_subdomain(&domain("a.foo.com"), "?.foo.com"));
assert!(!is_subdomain(&domain("a.b.foo.com"), "?.foo.com"));
assert!(!is_subdomain(&domain("bar.com"), "?.foo.com"));
assert!(!is_subdomain(&domain("foo.com"), "?.a.foo.com"));
assert!(!is_subdomain(&domain("afoo.com"), "?.foo.com"));
}
fn domain(name: &str) -> DomainName {
DomainName::vec_from_str(name).unwrap()
assert!(!matches);
}
}
#[cfg(feature = "divan")]
mod benches {
use super::*;
use rand::{distributions::DistString, seq::IteratorRandom, Rng};
#[divan::bench(
consts = [10, 100, 1_000, 10_000, 100_000]
)]
fn match_domain_linear<const NUM_RES: u128>(bencher: divan::Bencher) {
bencher
.with_inputs(|| {
let mut resolver = StubResolver::new(HashMap::default());
let mut rng = rand::thread_rng();
for n in 0..NUM_RES {
resolver.add_resource(ResourceId::from_u128(n), make_domain(&mut rng));
}
let needle = resolver
.dns_resources
.keys()
.choose(&mut rng)
.unwrap()
.to_string();
let needle = DomainName::vec_from_str(&needle).unwrap();
(resolver, needle)
})
.bench_refs(|(resolver, needle)| resolver.match_resource_linear(needle).unwrap());
}
fn make_domain(rng: &mut impl Rng) -> String {
(0..rng.gen_range(2..5))
.map(|_| rand::distributions::Alphanumeric.sample_string(rng, 3))
.join(".")
}
}

View File

@@ -674,13 +674,7 @@ fn is_subdomain(name: &str, record: &str) -> bool {
return false;
};
match first {
"*" => name.ends_with(end) && name.strip_suffix(end).is_some_and(|n| n.ends_with('.')),
"?" => {
name.ends_with(end)
&& name
.strip_suffix(end)
.is_some_and(|n| n.ends_with('.') && n.matches('.').count() == 1)
}
"**" => name.ends_with(end) && name.strip_suffix(end).is_some_and(|n| n.ends_with('.')),
_ => false,
}
}

View File

@@ -66,7 +66,7 @@ pub(crate) fn stub_portal() -> impl Strategy<Value = StubPortal> {
prop_oneof![
non_wildcard_dns_resource(any_site(sites.clone())),
star_wildcard_dns_resource(any_site(sites.clone())),
question_mark_wildcard_dns_resource(any_site(sites.clone())),
double_star_wildcard_dns_resource(any_site(sites.clone())),
],
1..5,
);
@@ -188,11 +188,11 @@ fn star_wildcard_dns_resource(
})
}
fn question_mark_wildcard_dns_resource(
fn double_star_wildcard_dns_resource(
site: impl Strategy<Value = Site>,
) -> impl Strategy<Value = ResourceDescriptionDns> {
dns_resource(site.prop_map(|s| vec![s])).prop_map(|r| ResourceDescriptionDns {
address: format!("?.{}", r.address),
address: format!("**.{}", r.address),
..r
})
}

View File

@@ -211,17 +211,14 @@ impl StubPortal {
.map(|resource| {
let address = resource.address.clone();
match address.chars().next().unwrap() {
'*' => subdomain_records(
address.trim_start_matches("*.").to_owned(),
domain_name(1..3),
)
.boxed(),
'?' => subdomain_records(
address.trim_start_matches("?.").to_owned(),
domain_label(),
)
.boxed(),
// Only generate simple wildcard domains for these tests.
// The matching logic is extensively unit-tested so we don't need to cover all cases here.
// What we do want to cover is multiple domains pointing to the same resource.
// For example, `*.example.com` and `app.example.com`.
match address.split_once('.') {
Some(("*" | "**", base)) => {
subdomain_records(base.to_owned(), domain_label()).boxed()
}
_ => resolved_ips()
.prop_map(move |resolved_ips| {
HashMap::from([(address.parse().unwrap(), resolved_ips)])

View File

@@ -1,7 +1,7 @@
[package]
name = "firezone-gateway"
# mark:next-gateway-version
version = "1.1.6"
version = "1.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,7 +1,7 @@
[package]
name = "firezone-gui-client"
# mark:next-gui-version
version = "1.1.13"
version = "1.2.0"
description = "Firezone"
edition = "2021"
default-run = "firezone-gui-client"

View File

@@ -1,7 +1,7 @@
[package]
name = "firezone-headless-client"
# mark:next-headless-version
version = "1.1.8"
version = "1.2.0"
edition = "2021"
authors = ["Firezone, Inc."]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -20,11 +20,11 @@ current-gui-version = 1.1.12
current-headless-version = 1.1.7
# Tracks the next version to release for each platform
next-apple-version = 1.1.6
next-android-version = 1.1.7
next-gateway-version = 1.1.6
next-gui-version = 1.1.13
next-headless-version = 1.1.8
next-apple-version = 1.2.0
next-android-version = 1.2.0
next-gateway-version = 1.2.0
next-gui-version = 1.2.0
next-headless-version = 1.2.0
# macOS uses a slightly different sed syntax
ifeq ($(shell uname),Darwin)

View File

@@ -585,7 +585,7 @@
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(CONNLIB_TARGET_DIR)/aarch64-apple-ios/debug";
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
MARKETING_VERSION = 1.1.6;
MARKETING_VERSION = 1.2.0;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = "-lconnlib";
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).debug.network-extension";
@@ -627,7 +627,7 @@
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(CONNLIB_TARGET_DIR)/aarch64-apple-ios/release";
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
MARKETING_VERSION = 1.1.6;
MARKETING_VERSION = 1.2.0;
OTHER_LDFLAGS = "-lconnlib";
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).network-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -671,7 +671,7 @@
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(CONNLIB_TARGET_DIR)/aarch64-apple-darwin/debug";
"LIBRARY_SEARCH_PATHS[arch=arm64e]" = "$(CONNLIB_TARGET_DIR)/aarch64-apple-darwin/debug";
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(CONNLIB_TARGET_DIR)/x86_64-apple-darwin/debug";
MARKETING_VERSION = 1.1.6;
MARKETING_VERSION = 1.2.0;
OTHER_LDFLAGS = "-lconnlib";
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).debug.network-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -714,7 +714,7 @@
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(CONNLIB_TARGET_DIR)/aarch64-apple-darwin/release";
"LIBRARY_SEARCH_PATHS[arch=arm64e]" = "$(CONNLIB_TARGET_DIR)/aarch64-apple-darwin/release";
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(CONNLIB_TARGET_DIR)/x86_64-apple-darwin/release";
MARKETING_VERSION = 1.1.6;
MARKETING_VERSION = 1.2.0;
OTHER_LDFLAGS = "-lconnlib";
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).network-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -908,7 +908,7 @@
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MARKETING_VERSION = 1.1.6;
MARKETING_VERSION = 1.2.0;
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited)";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -959,7 +959,7 @@
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MARKETING_VERSION = 1.1.6;
MARKETING_VERSION = 1.2.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited)";
PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)";
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -10,8 +10,11 @@ export default function Android() {
title="Android"
>
{/*
<Entry version="1.1.7" date={new Date(todo)}>
<Entry version="1.2.0" date={new Date(todo)}>
<ul className="list-disc space-y-2 pl-4 mb-4">
<ChangeItem pull="5901">
Implements glob-like matching of domains for DNS resources.
</ChangeItem>
</ul>
</Entry>
*/}

View File

@@ -9,8 +9,11 @@ export default function Apple() {
href="https://apps.apple.com/us/app/firezone/id6443661826"
title="macOS / iOS"
>
{/* <Entry version="1.1.5" date={new Date(TODO)}>
{/* <Entry version="1.2.0" date={new Date(TODO)}>
<ul className="list-disc space-y-2 pl-4 mb-4">
<ChangeItem pull="5901">
Implements glob-like matching of domains for DNS resources.
</ChangeItem>
</ul>
</Entry> */}
<Entry version="1.1.5" date={new Date("2024-08-13")}>

View File

@@ -14,16 +14,10 @@ export default function GUI({ title }: { title: string }) {
<Entries href={href} arches={arches} title={title}>
{/* When you cut a release, remove any solved issues from the "known issues" lists over in `client-apps`. This cannot be done when the issue's PR merges. */}
{/*
<Entry version="1.1.12" date={new Date("Invalid date")}>
<Entry version="1.2.0" date={new Date("Invalid date")}>
<ul className="list-disc space-y-2 pl-4 mb-4">
<ChangeItem pull="6226">
Fixes a bug where clearing the log files would delete the current files, preventing logs from being written.
</ChangeItem>
<ChangeItem pull="6276">
Fixes a bug where relayed connections failed to establish after an idle period.
</ChangeItem>
<ChangeItem pull="6277">
Fixes a bug where restrictive NATs caused connectivity problems.
<ChangeItem pull="5901">
Implements glob-like matching of domains for DNS resources.
</ChangeItem>
<ChangeItem enable={title === "Windows"} pull="6280">
Fixes a bug where the "Clear Logs" button did not clear the IPC service logs.

View File

@@ -10,13 +10,10 @@ export default function Gateway() {
return (
<Entries href={href} arches={arches} title="Gateway">
{/*
<Entry version="1.1.5" date={new Date("2024-XX-XX")}>
<Entry version="1.2.0" date={new Date("2024-XX-XX")}>
<ul className="list-disc space-y-2 pl-4 mb-4">
<ChangeItem pull="6276">
Fixes a bug where relayed connections failed to establish after an idle period.
</ChangeItem>
<ChangeItem pull="6277">
Fixes a bug where restrictive NATs caused connectivity problems.
<ChangeItem pull="5901">
Implements glob-like matching of domains for DNS resources.
</ChangeItem>
</ul>
</Entry>

View File

@@ -10,8 +10,11 @@ export default function Headless() {
return (
<Entries href={href} arches={arches} title="Linux headless">
{/*
<Entry version="1.1.8" date={new Date(todo)}>
<Entry version="1.2.0" date={new Date(todo)}>
<ul className="list-disc space-y-2 pl-4 mb-4">
<ChangeItem pull="5901">
Implements glob-like matching of domains for DNS resources.
</ChangeItem>
</ul>
</Entry>
*/}