Skip to content
Merged
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
2 changes: 2 additions & 0 deletions examples/localhost.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ services:
80: 8080
subjectAltNames:
- spiffe://cluster.local/ns/default/sa/local
canonical: true
- name: remote
namespace: default
hostname: example2.com
vips:
- remote/127.10.0.2
ports:
80: 8080
canonical: true
4 changes: 4 additions & 0 deletions proto/workload.proto
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ message Service {

// Extension provides a mechanism to attach arbitrary additional configuration to an object.
repeated Extension extensions = 10;

// canonical marks this Service as taking priority during hostname lookups,
// when there is not a match in the namespace of the client.
bool canonical = 11;
Comment thread
ilrudie marked this conversation as resolved.
}

enum IPFamilies {
Expand Down
1 change: 1 addition & 0 deletions src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,7 @@ mod tests {
}), // ..Default::default() // intentionally don't default. we want all fields populated
ip_families: 0,
extensions: Default::default(),
canonical: true,
};

let auth = XdsAuthorization {
Expand Down
106 changes: 92 additions & 14 deletions src/dns/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,26 @@ impl Server {
}
}

enum MatchReason<'a> {
Canonical(&'a Arc<Service>),
First(&'a Arc<Service>),
Namespace(&'a Arc<Service>),
PreferredNamespace(&'a Arc<Service>),
None,
}
Comment on lines +190 to +196
Copy link
Copy Markdown
Contributor

@Stevenjin8 Stevenjin8 Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely not a blocker, but more of a neat idea I had. If you implemement Ord (and PartialOrd) for this:

impl<'a> Ord for MatchReason<'a> {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        use MatchReason::*;
        let self_rank: i32 = match self {
            Canonical(_) => 3,
            PreferredNamespace(_) => 2,
            Namespace(_) => 1,
            First(_) => 0,
        };
        let other_rank: i32 = match other {
            Canonical(_) => 3,
            PreferredNamespace(_) => 2,
            Namespace(_) => 1,
            First(_) => 0,
        };
        self_rank.cmp(&other_rank)
    }
}

then you can do

let service: Option<&Arc<Service>> = services Vec<Arc<Service>>
    .iter() Iter<'_, Arc<Service>>
    .map(|s: &Arc<Service>| {
        if s.namespace == client.namespace {
            MatchReason::Namespace(s)
        } else if s.canonical {
            MatchReason::Canonical(s)
        } else if let Some(preferred_ns: &String) = &self.prefered_service_namespace
            && s.namespace == *preferred_ns
        {
            MatchReason::PreferredNamespace(s)
        } else {
            MatchReason::First(s)
        }
    }) impl Iterator<Item = MatchReason<'_>>
    .max() Option<MatchReason<'_>>
    .map(|mr: MatchReason<'_>| mr.into());

It also lets you get rid of the None variant in the enum. The downside is I'm not sure if max will short circuit.


impl<'a> From<MatchReason<'a>> for Option<&'a Arc<Service>> {
fn from(value: MatchReason<'a>) -> Option<&'a Arc<Service>> {
match value {
MatchReason::Canonical(s)
| MatchReason::First(s)
| MatchReason::Namespace(s)
| MatchReason::PreferredNamespace(s) => Some(s),
MatchReason::None => None,
}
}
}

/// A DNS [Resolver] backed by the ztunnel [DemandProxyState].
struct Store {
state: DemandProxyState,
Expand Down Expand Up @@ -390,21 +410,35 @@ impl Store {
.cloned()
.collect();

// TODO: ideally we'd sort these by creation time so that the oldest would be used if there are no namespace matches
// presently service doesn't have creation time in WDS, but we could add it
// TODO: if the local namespace doesn't define a service, kube service should be prioritized over se
let service = match services
let service: Option<&Arc<Service>> = services
.iter()
.find(|service| service.namespace == client.namespace)
{
Some(service) => Some(service),
None => match self.prefered_service_namespace.as_ref() {
Some(prefered_namespace) => services.iter().find_or_first(|service| {
service.namespace == prefered_namespace.as_str()
}),
None => services.first(),
},
};
.fold_while(MatchReason::None, |r, s| {
if s.namespace == client.namespace {
Comment thread
Stevenjin8 marked this conversation as resolved.
itertools::FoldWhile::Done(MatchReason::Namespace(s))
} else if s.canonical {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having multiple marked canonical is UB right? do we have sorting or anything on the list so it would be consistent?

A good control plane would probably not send multiple marked canonical of course.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, UB and a good control plane should not send this.

We don't have consistent sorting so IF you have two services marked as canonical (control plane bug) and IF you have two ztunnels who's wds input ordering is differed enough that their data structures are ordered differently you could have inconsistent behaviors node-to-node. This isn't actually any worse than how it was when we just did "match namespace or first" since "first" was essentially the same UB. I think it's probably ok but maybe we can add a debug_assertion? Would our integ test builds run debug assertions or are they optimized builds already?

itertools::FoldWhile::Continue(MatchReason::Canonical(s))
} else {
// TODO: deprecate preferred_service_namespace
// https://github.com/istio/ztunnel/issues/1709
if let Some(preferred_namespace) =
self.prefered_service_namespace.as_ref()
&& preferred_namespace.as_str() == s.namespace
&& !matches!(r, MatchReason::Canonical(_))
{
return itertools::FoldWhile::Continue(
MatchReason::PreferredNamespace(s),
);
}
match r {
MatchReason::None => {
itertools::FoldWhile::Continue(MatchReason::First(s))
}
_ => itertools::FoldWhile::Continue(r),
}
}
})
.into_inner()
.into();

// First, lookup the host as a service.
if let Some(service) = service {
Expand Down Expand Up @@ -963,6 +997,7 @@ mod tests {

const NS1: &str = "ns1";
const NS2: &str = "ns2";
const NS3: &str = "ns3";
const PREFERRED: &str = "preferred-ns";
const NW1: Strng = strng::literal!("nw1");
const NW2: Strng = strng::literal!("nw2");
Expand Down Expand Up @@ -1394,6 +1429,18 @@ mod tests {
expect_records: vec![a(n("everywhere.io."), ipv4("10.10.10.112"))],
..Default::default()
},
Case {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should have a test case that NS local can win out over one marked canonical

name: "success: canonical services are preferred when no ns-local hostname is present",
host: "canonical.svc",
expect_records: vec![a(n("canonical.svc."), ipv4("10.10.10.141"))],
..Default::default()
},
Case {
name: "success: namespace-local service should be preferred over canonical",
host: "canonical.with.local",
expect_records: vec![a(n("canonical.with.local."), ipv4("10.10.10.150"))],
..Default::default()
},
];

// Create and start the proxy.
Expand Down Expand Up @@ -1713,6 +1760,24 @@ mod tests {
// Service with the same name in the same namespace
// Client in NS1 should use this service
xds_namespaced_external_service("everywhere.io", NS1, &[na(NW1, "10.10.10.112")]),
// Service that is canonical should be preferrred when no ns-local definition
xds_namespaced_external_service("canonical.svc", NS2, &[na(NW1, "10.10.10.140")]),
xds_namespaced_external_canonical_service(
"canonical.svc",
NS3,
&[na(NW1, "10.10.10.141")],
),
// Client in NS1 should prefer local over canonical
xds_namespaced_external_service(
"canonical.with.local",
NS1,
&[na(NW1, "10.10.10.150")],
),
xds_namespaced_external_canonical_service(
"canonical.with.local",
NS2,
&[na(NW1, "10.10.10.151")],
),
with_fqdn(
"details.ns2.svc.cluster.remote",
xds_service(
Expand Down Expand Up @@ -1838,6 +1903,11 @@ mod tests {
svc
}

fn with_canonical(canonical: bool, mut svc: XdsService) -> XdsService {
svc.canonical = canonical;
svc
}

fn xds_service<S1: AsRef<str>, S2: AsRef<str>>(
name: S1,
ns: S2,
Expand Down Expand Up @@ -1877,6 +1947,14 @@ mod tests {
)
}

fn xds_namespaced_external_canonical_service<S1: AsRef<str>, S2: AsRef<str>>(
hostname: S1,
ns: S2,
vips: &[NetworkAddress],
) -> XdsService {
with_canonical(true, xds_namespaced_external_service(hostname, ns, vips))
}

fn xds_workload(
name: &str,
ns: &str,
Expand Down
1 change: 1 addition & 0 deletions src/proxy/inbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,7 @@ mod tests {
waypoint: waypoint.service_attached(),
load_balancer: None,
ip_families: None,
canonical: true,
});

let workloads = vec![
Expand Down
4 changes: 4 additions & 0 deletions src/state/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ pub struct Service {

#[serde(default, skip_serializing_if = "is_default")]
pub ip_families: Option<IpFamily>,

#[serde(default)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this also have skip serializing on default?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my mind it would be nice to serialize canonical: false here instead of it being omitted. I can change if we feel strongly.

pub canonical: bool,
}

/// EndpointSet is an abstraction over a set of endpoints.
Expand Down Expand Up @@ -328,6 +331,7 @@ impl TryFrom<&XdsService> for Service {
waypoint,
load_balancer: lb,
ip_families,
canonical: s.canonical,
};
Ok(svc)
}
Expand Down
7 changes: 7 additions & 0 deletions src/state/workload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,7 @@ mod tests {
load_balancing: None,
ip_families: 0,
extensions: Default::default(),
canonical: true,
},
)
.unwrap();
Expand Down Expand Up @@ -1157,6 +1158,7 @@ mod tests {
load_balancing: None,
ip_families: 0,
extensions: Default::default(),
canonical: true,
},
)
.unwrap();
Expand Down Expand Up @@ -1214,6 +1216,7 @@ mod tests {
load_balancing: None,
ip_families: 0,
extensions: Default::default(),
canonical: true,
},
)
.unwrap();
Expand Down Expand Up @@ -1525,6 +1528,7 @@ mod tests {
load_balancing: None,
ip_families: 0,
extensions: Default::default(),
canonical: true,
},
)
.unwrap();
Expand Down Expand Up @@ -1552,6 +1556,7 @@ mod tests {
}),
ip_families: 0,
extensions: Default::default(),
canonical: true,
},
)
.unwrap();
Expand Down Expand Up @@ -1603,6 +1608,7 @@ mod tests {
}),
ip_families: 0,
extensions: Default::default(),
canonical: true,
};
updater
.insert_service(
Expand All @@ -1624,6 +1630,7 @@ mod tests {
load_balancing: None,
ip_families: 0,
extensions: Default::default(),
canonical: true,
},
)
.unwrap();
Expand Down
2 changes: 2 additions & 0 deletions src/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ pub fn mock_default_service() -> Service {
waypoint: None,
load_balancer: None,
ip_families: None,
canonical: true,
}
}

Expand Down Expand Up @@ -289,6 +290,7 @@ fn test_custom_svc(
waypoint: None,
load_balancer: None,
ip_families: None,
canonical: true,
})
}

Expand Down
1 change: 1 addition & 0 deletions src/test_helpers/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ impl<'a> TestServiceBuilder<'a> {
waypoint: None,
load_balancer: None,
ip_families: None,
canonical: true,
},
manager,
}
Expand Down