Compare commits

...

41 Commits

Author SHA1 Message Date
Istio Automation ac67367eb9
Automator: update common-files@master in istio/ztunnel@master (#1607) 2025-08-07 17:29:24 -04:00
Ian Rudie ec120597bd
chore - clippy cleanup (#1610)
* fix build.rs

Signed-off-by: Ian Rudie <ian.rudie@solo.io>

* clippy

Signed-off-by: Ian Rudie <ian.rudie@solo.io>

* remove swap

Signed-off-by: Ian Rudie <ian.rudie@solo.io>

---------

Signed-off-by: Ian Rudie <ian.rudie@solo.io>
2025-08-07 17:08:25 -04:00
Istio Automation 2837b0f410
Automator: update common-files@master in istio/ztunnel@master (#1604) 2025-08-01 07:17:06 -04:00
Steven Landow 6640774d9f
respect IPV6 setting for DNS server (#1601) 2025-07-24 16:45:47 -04:00
Ian Rudie 4f50f8403b
adopt rcgen 14 (#1599)
* adopt rcgen 14

Signed-off-by: Ian Rudie <ian.rudie@solo.io>

* fmt

Signed-off-by: Ian Rudie <ian.rudie@solo.io>

* fmt fuzz

Signed-off-by: Ian Rudie <ian.rudie@solo.io>

---------

Signed-off-by: Ian Rudie <ian.rudie@solo.io>
2025-07-18 17:01:43 -04:00
Istio Automation b102502cbd
Automator: update common-files@master in istio/ztunnel@master (#1598) 2025-07-17 12:57:43 -04:00
Istio Automation d58e82441f
Automator: update common-files@master in istio/ztunnel@master (#1597) 2025-07-15 11:26:38 -04:00
Istio Automation c555eaa812
Automator: update common-files@master in istio/ztunnel@master (#1596) 2025-07-15 06:07:38 -04:00
Istio Automation 85a94b6cc4
Automator: update common-files@master in istio/ztunnel@master (#1595) 2025-07-10 07:15:33 -04:00
Istio Automation 3fa6335035
Automator: update common-files@master in istio/ztunnel@master (#1589) 2025-07-08 05:44:30 -04:00
Steven Jin dfa3b58bbc
Buffer inner h2 streams (#1580)
* Buffer h2 streams

* Tests

* naming

* Review

simplify code
2025-07-07 17:52:29 -04:00
Gustavo Meira c2d2534edb
increasing limit for open files (#1586)
* increasing limit for open files

* suggestion from PR

* adding comment

* Update src/main.rs

Co-authored-by: Daniel Hawton <daniel@hawton.org>

---------

Co-authored-by: Daniel Hawton <daniel@hawton.org>
2025-07-07 15:37:29 -04:00
Krinkin, Mike 84f0e52e64
Multinetwork/Support remote networks for services with waypoints (#1565)
* Multinetwork/Support remote networks for services with waypoints

Currently `build_request` when it sees a service with a waypoint
resolves the waypoint backend and routes request there using regular
HBONE.

In multi network scenario though the waypoint may have workload on a
remote network and to reach it we have to go through E/W gateway and use
double HBONE.

This change enables handling of services with waypoint on a remote
network.

Some of the assumptions that were used when I prepared this change:

1. We assume uniformity of configuration (e.g., if service X in local
   cluster has a waypoint, then service X in remote network also has a
   waypoint, if waypoint is service addressable, then it's using service
   to address waypoint both locally and on remote network)
2  Split-horizon representation of waypoint workloads, just like with
   any regular workloads and services (e.g., in the local cluster
   instead of an actual waypoint workload pointing to a pod on another
   network we will have a "proxy" representation that just has network
   gateway).

Both of those can be in hanled by the controlplane (e.g., controlplane
can generate split-horizon workloads and when configuration is
non-uniform, just filter out remote configs for remote networks), though
we don't yet have a complete implementation.

Signed-off-by: Mikhail Krinkin <mkrinkin@microsoft.com>

* Return an error instead of panicking

Signed-off-by: Mikhail Krinkin <mkrinkin@microsoft.com>

* Update comments in src/proxy/outbound.rs

Co-authored-by: Ian Rudie <ilrudie@gmail.com>

* Update comments in src/proxy/outbound.rs

Signed-off-by: Mikhail Krinkin <mkrinkin@microsoft.com>

* Add a debug assert to provide a bit more context to the error in tests

Signed-off-by: Mikhail Krinkin <mkrinkin@microsoft.com>

* Fix formatting

Signed-off-by: Mikhail Krinkin <mkrinkin@microsoft.com>

* Added a few debug logs to be able to trace when a workload on a remote network is picked

Signed-off-by: Mikhail Krinkin <mkrinkin@microsoft.com>

---------

Signed-off-by: Mikhail Krinkin <mkrinkin@microsoft.com>
Co-authored-by: Ian Rudie <ilrudie@gmail.com>
2025-07-07 12:29:29 -04:00
Istio Automation f030073f2f
Automator: update common-files@master in istio/ztunnel@master (#1583) 2025-06-30 16:52:23 -04:00
Ian Rudie 3233bb1017
Improved Service Resolution (#1562)
* initial idea for improved resolution

Signed-off-by: ilrudie <ian.rudie@solo.io>

* handle preferred service namespace; unit testing

Signed-off-by: Ian Rudie <ian.rudie@solo.io>

---------

Signed-off-by: ilrudie <ian.rudie@solo.io>
Signed-off-by: Ian Rudie <ian.rudie@solo.io>
2025-06-30 11:23:22 -04:00
Istio Automation 7df8cf5d08
Automator: update common-files@master in istio/ztunnel@master (#1582) 2025-06-26 08:18:20 -04:00
Jacek Ewertowski 7cddb868e9
tls: add PQC compliance policy (#1561)
* tls: add PQC compliance policy

Signed-off-by: Jacek Ewertowski <jacek.ewertowski1@gmail.com>

* Add global lazy variable PQC_ENABLED

Signed-off-by: Jacek Ewertowski <jacek.ewertowski1@gmail.com>

* Add unused_imports and dead_code to PQC_ENABLED declaration

Signed-off-by: Jacek Ewertowski <jacek.ewertowski1@gmail.com>

---------

Signed-off-by: Jacek Ewertowski <jacek.ewertowski1@gmail.com>
2025-06-23 10:45:24 -04:00
Istio Automation ac477c15a8
Automator: update common-files@master in istio/ztunnel@master (#1578) 2025-06-19 09:56:10 -04:00
Istio Automation 5d0352588c
Automator: update common-files@master in istio/ztunnel@master (#1577) 2025-06-18 14:30:10 -04:00
Ian Rudie b86fd9989b
remove invalid test cases from parsing of ZTUNNEL_WORKER_THREADS (#1576)
Signed-off-by: Ian Rudie <ian.rudie@solo.io>
2025-06-18 13:15:00 -04:00
Istio Automation facd9a28a0
Automator: update common-files@master in istio/ztunnel@master (#1572) 2025-06-08 22:00:13 -04:00
Istio Automation 224b2c34ac
Automator: update common-files@master in istio/ztunnel@master (#1571) 2025-06-06 10:26:53 -04:00
Steven Landow c52e0bbdbf
don't send to empty address (#1570)
* don't send to empty address

* add test
2025-06-05 16:38:51 -04:00
John Howard 442923910b
Allow dynamic configuration of thread count (#1566)
* Allow dynamic configuration of thread count

* fix flakes
2025-06-04 14:31:52 -04:00
Istio Automation c616a29092
Automator: update common-files@master in istio/ztunnel@master (#1568) 2025-06-04 12:44:51 -04:00
Istio Automation d6d3b606ed
Automator: update common-files@master in istio/ztunnel@master (#1563) 2025-05-27 12:51:34 -04:00
Ian Rudie 8d9a56a416
update how io errors are being generated to fix clippy issues (#1564)
Signed-off-by: ilrudie <ian.rudie@solo.io>
2025-05-27 12:43:33 -04:00
zirain 615277a05a
remove git tag (#1559) 2025-05-21 23:47:28 -04:00
Harsh Pratap Singh 3d1223af09
Support TLS for metrics endpoint (#1507)
* add support for secure metrics endpoint

* nit

* no need to rewrite to localhost

* Implement tests for ztunnel identity and metrics routing

* nits

* Refactor to use environment variables for trust domain and service account
environments.

* refactoring

* nit

* Refactor Config and Identity Handling

* made identity and workload info optional and added unit test

* dont need to use env variables in test now

* Added logic to direct traffic to the ztunnel metrics endpoint on port 15020 via HBONE, ensuring proper request forwarding.

* Enhance Proxy to Support ztunnel Inbound Traffic

* Implement ztunnel inbound listener creation in shared proxy mode

* Refactor ztunnel inbound listener creation in shared proxy mode by removing unnecessary checks

* Refactor ztunnel inbound listener creation by encapsulating logic in ProxyFactory. =

* Enhance ProxyInputs to include disable_inbound_freebind flag for controlling source IP preservation.

* Add tests for ztunnel self-scrape and metrics scrape via HBONE

* refactored e2e test for ztunnel identity and inbound

* fmt nit

* clippy nits

* nits

* nit

* fmt nits

* Refactor e2e tests

* Refactor client in e2e test connection logic to perform a standard HTTP GET request to the metrics endpoint, ensuring proper verification of the response.

* fmt nit

* match nit

* remove unnecessary test and error

* Refactor ztunnel listener creation and configuration for shared proxy mode

* Refactor trust domain handling and improve string formatting in configuration
nagement.

* nits

* Revert timeout duration for TCP connection and

* fmt nit

* nits
2025-05-19 16:05:24 -04:00
Istio Automation 79dfd10249
Automator: update common-files@master in istio/ztunnel@master (#1558) 2025-05-15 14:26:14 -04:00
Istio Automation 9f6ae51005
Automator: update common-files@master in istio/ztunnel@master (#1557) 2025-05-13 18:03:14 -04:00
Istio Automation 46acf76463
Automator: update common-files@master in istio/ztunnel@master (#1554) 2025-05-09 10:01:46 -04:00
Istio Automation 58cf2a0f94
Automator: update common-files@master in istio/ztunnel@master (#1553) 2025-05-09 06:16:46 -04:00
Steven Jin 9c01d1276d
show crypto provider in ztunnel version (#1545)
* Version shows crypto provider

* All tls providers

* Move crypto consts
2025-05-08 16:42:45 -04:00
Yuval Kohavi d9ea32ce21
Make csr test stricter and more correct (previously #1432) (#1550)
* Make csr test stricter and more correct

Part of https://github.com/istio/ztunnel/issues/1431

Fails without https://github.com/rustls/rcgen/pull/311

* update rcgen

Signed-off-by: Yuval Kohavi <yuval.kohavi@gmail.com>

* fix merge issue

* format fix

Signed-off-by: Yuval Kohavi <yuval.kohavi@gmail.com>

---------

Signed-off-by: Yuval Kohavi <yuval.kohavi@gmail.com>
Co-authored-by: John Howard <john.howard@solo.io>
2025-05-07 16:01:44 -04:00
Istio Automation c96dd032da
Automator: update common-files@master in istio/ztunnel@master (#1549) 2025-05-07 14:25:48 -04:00
Istio Automation 903cf079de
Automator: update common-files@master in istio/ztunnel@master (#1544) 2025-04-25 04:03:03 -04:00
Istio Automation ad8bea43ef
Automator: update common-files@master in istio/ztunnel@master (#1543) 2025-04-24 16:28:02 -04:00
John Howard 6eaa32e8ac
http2: tune connection window size and add config (#1539)
Fixes https://github.com/istio/ztunnel/issues/1538

See comment for motivation as to why this change is needed.
2025-04-24 09:54:55 -04:00
zirain 3470f4bba2
fix istio_build metric (#1532)
* fix istio_build metric

* fmt
2025-04-18 09:54:24 -04:00
John Howard 93a0973175
Include error info in DNS access logs (#1529) 2025-04-17 15:32:23 -04:00
44 changed files with 1263 additions and 378 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "istio build-tools", "name": "istio build-tools",
"image": "gcr.io/istio-testing/build-tools:master-fcd42145fc132acd1e8f607e9e7aca15058e9fb9", "image": "gcr.io/istio-testing/build-tools:master-672e6089ff843019a2b28cf9e87754c7b74358ea",
"privileged": true, "privileged": true,
"remoteEnv": { "remoteEnv": {
"USE_GKE_GCLOUD_AUTH_PLUGIN": "True", "USE_GKE_GCLOUD_AUTH_PLUGIN": "True",

110
Cargo.lock generated
View File

@ -112,29 +112,13 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "asn1-rs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
dependencies = [
"asn1-rs-derive 0.5.1",
"asn1-rs-impl",
"displaydoc",
"nom",
"num-traits",
"rusticata-macros",
"thiserror 1.0.69",
"time",
]
[[package]] [[package]]
name = "asn1-rs" name = "asn1-rs"
version = "0.7.1" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
dependencies = [ dependencies = [
"asn1-rs-derive 0.6.0", "asn1-rs-derive",
"asn1-rs-impl", "asn1-rs-impl",
"displaydoc", "displaydoc",
"nom", "nom",
@ -144,18 +128,6 @@ dependencies = [
"time", "time",
] ]
[[package]]
name = "asn1-rs-derive"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
"synstructure",
]
[[package]] [[package]]
name = "asn1-rs-derive" name = "asn1-rs-derive"
version = "0.6.0" version = "0.6.0"
@ -761,27 +733,13 @@ dependencies = [
"const-oid", "const-oid",
] ]
[[package]]
name = "der-parser"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
dependencies = [
"asn1-rs 0.6.2",
"displaydoc",
"nom",
"num-bigint",
"num-traits",
"rusticata-macros",
]
[[package]] [[package]]
name = "der-parser" name = "der-parser"
version = "10.0.0" version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
dependencies = [ dependencies = [
"asn1-rs 0.7.1", "asn1-rs",
"displaydoc", "displaydoc",
"nom", "nom",
"num-bigint", "num-bigint",
@ -2226,22 +2184,13 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "oid-registry"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
dependencies = [
"asn1-rs 0.6.2",
]
[[package]] [[package]]
name = "oid-registry" name = "oid-registry"
version = "0.8.1" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
dependencies = [ dependencies = [
"asn1-rs 0.7.1", "asn1-rs",
] ]
[[package]] [[package]]
@ -2815,16 +2764,31 @@ dependencies = [
[[package]] [[package]]
name = "rcgen" name = "rcgen"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" checksum = "887a643fa081058097896d87764863994f6c32a1716e76adc479bd283974a825"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"pem", "pem",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
"time", "time",
"x509-parser 0.16.0", "x509-parser",
"yasna",
]
[[package]]
name = "rcgen"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49bc8ffa8a832eb1d7c8000337f8b0d2f4f2f5ec3cf4ddc26f125e3ad2451824"
dependencies = [
"aws-lc-rs",
"pem",
"ring",
"rustls-pki-types",
"time",
"x509-parser",
"yasna", "yasna",
] ]
@ -4305,36 +4269,18 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "x509-parser"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
dependencies = [
"asn1-rs 0.6.2",
"data-encoding",
"der-parser 9.0.0",
"lazy_static",
"nom",
"oid-registry 0.7.1",
"ring",
"rusticata-macros",
"thiserror 1.0.69",
"time",
]
[[package]] [[package]]
name = "x509-parser" name = "x509-parser"
version = "0.17.0" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460"
dependencies = [ dependencies = [
"asn1-rs 0.7.1", "asn1-rs",
"data-encoding", "data-encoding",
"der-parser 10.0.0", "der-parser",
"lazy_static", "lazy_static",
"nom", "nom",
"oid-registry 0.8.1", "oid-registry",
"ring", "ring",
"rusticata-macros", "rusticata-macros",
"thiserror 2.0.12", "thiserror 2.0.12",
@ -4510,7 +4456,8 @@ dependencies = [
"matches", "matches",
"netns-rs", "netns-rs",
"nix 0.29.0", "nix 0.29.0",
"oid-registry 0.8.1", "num_cpus",
"oid-registry",
"once_cell", "once_cell",
"openssl", "openssl",
"pin-project-lite", "pin-project-lite",
@ -4523,7 +4470,8 @@ dependencies = [
"prost-build", "prost-build",
"prost-types", "prost-types",
"rand 0.9.0", "rand 0.9.0",
"rcgen", "rcgen 0.13.3",
"rcgen 0.14.2",
"ring", "ring",
"rustc_version", "rustc_version",
"rustls", "rustls",
@ -4553,6 +4501,6 @@ dependencies = [
"tracing-log", "tracing-log",
"tracing-subscriber", "tracing-subscriber",
"url", "url",
"x509-parser 0.17.0", "x509-parser",
"ztunnel", "ztunnel",
] ]

View File

@ -71,15 +71,16 @@ itertools = "0.14"
keyed_priority_queue = "0.4" keyed_priority_queue = "0.4"
libc = "0.2" libc = "0.2"
log = "0.4" log = "0.4"
nix = { version = "0.29", features = ["socket", "sched", "uio", "fs", "ioctl", "user", "net", "mount"] } nix = { version = "0.29", features = ["socket", "sched", "uio", "fs", "ioctl", "user", "net", "mount", "resource" ] }
once_cell = "1.21" once_cell = "1.21"
num_cpus = "1.16"
ppp = "2.3" ppp = "2.3"
prometheus-client = { version = "0.23" } prometheus-client = { version = "0.23" }
prometheus-parse = "0.2" prometheus-parse = "0.2"
prost = "0.13" prost = "0.13"
prost-types = "0.13" prost-types = "0.13"
rand = { version = "0.9" , features = ["small_rng"]} rand = { version = "0.9" , features = ["small_rng"]}
rcgen = { version = "0.13", optional = true, features = ["pem"] } rcgen = { version = "0.14", optional = true, features = ["pem"] }
rustls = { version = "0.23", default-features = false } rustls = { version = "0.23", default-features = false }
rustls-native-certs = "0.8" rustls-native-certs = "0.8"
rustls-pemfile = "2.2" rustls-pemfile = "2.2"

View File

@ -458,10 +458,10 @@ fn hbone_connection_config() -> ztunnel::config::ConfigSource {
workload: Workload { workload: Workload {
workload_ips: vec![hbone_connection_ip(i)], workload_ips: vec![hbone_connection_ip(i)],
protocol: InboundProtocol::HBONE, protocol: InboundProtocol::HBONE,
uid: strng::format!("cluster1//v1/Pod/default/remote{}", i), uid: strng::format!("cluster1//v1/Pod/default/remote{i}"),
name: strng::format!("workload-{}", i), name: strng::format!("workload-{i}"),
namespace: strng::format!("namespace-{}", i), namespace: strng::format!("namespace-{i}"),
service_account: strng::format!("service-account-{}", i), service_account: strng::format!("service-account-{i}"),
..test_helpers::test_default_workload() ..test_helpers::test_default_workload()
}, },
services: Default::default(), services: Default::default(),

View File

@ -94,9 +94,6 @@ fn main() -> Result<(), anyhow::Error> {
"cargo:rustc-env=ZTUNNEL_BUILD_RUSTC_VERSION={}", "cargo:rustc-env=ZTUNNEL_BUILD_RUSTC_VERSION={}",
rustc_version::version().unwrap() rustc_version::version().unwrap()
); );
println!( println!("cargo:rustc-env=ZTUNNEL_BUILD_PROFILE_NAME={profile_name}");
"cargo:rustc-env=ZTUNNEL_BUILD_PROFILE_NAME={}",
profile_name
);
Ok(()) Ok(())
} }

View File

@ -1 +1 @@
a1d5c4198ab79a14c09c034f2d95245efa3e2bcb d235bc9f4a20f3c78c5aacbfa3f24d08a884a82e

View File

@ -184,6 +184,10 @@ linters:
- linters: - linters:
- staticcheck - staticcheck
text: 'S1007' text: 'S1007'
# TODO: remove once we have updated package names
- linters:
- revive
text: "var-naming: avoid meaningless package names"
paths: paths:
- .*\.pb\.go - .*\.pb\.go
- .*\.gen\.go - .*\.gen\.go

View File

@ -32,7 +32,7 @@ set -x
#################################################################### ####################################################################
# DEFAULT_KIND_IMAGE is used to set the Kubernetes version for KinD unless overridden in params to setup_kind_cluster(s) # DEFAULT_KIND_IMAGE is used to set the Kubernetes version for KinD unless overridden in params to setup_kind_cluster(s)
DEFAULT_KIND_IMAGE="gcr.io/istio-testing/kind-node:v1.32.0" DEFAULT_KIND_IMAGE="gcr.io/istio-testing/kind-node:v1.33.1"
# the default kind cluster should be ipv4 if not otherwise specified # the default kind cluster should be ipv4 if not otherwise specified
KIND_IP_FAMILY="${KIND_IP_FAMILY:-ipv4}" KIND_IP_FAMILY="${KIND_IP_FAMILY:-ipv4}"

View File

@ -75,7 +75,7 @@ fi
TOOLS_REGISTRY_PROVIDER=${TOOLS_REGISTRY_PROVIDER:-gcr.io} TOOLS_REGISTRY_PROVIDER=${TOOLS_REGISTRY_PROVIDER:-gcr.io}
PROJECT_ID=${PROJECT_ID:-istio-testing} PROJECT_ID=${PROJECT_ID:-istio-testing}
if [[ "${IMAGE_VERSION:-}" == "" ]]; then if [[ "${IMAGE_VERSION:-}" == "" ]]; then
IMAGE_VERSION=master-fcd42145fc132acd1e8f607e9e7aca15058e9fb9 IMAGE_VERSION=master-672e6089ff843019a2b28cf9e87754c7b74358ea
fi fi
if [[ "${IMAGE_NAME:-}" == "" ]]; then if [[ "${IMAGE_NAME:-}" == "" ]]; then
IMAGE_NAME=build-tools IMAGE_NAME=build-tools

5
fuzz/Cargo.lock generated
View File

@ -2359,9 +2359,9 @@ dependencies = [
[[package]] [[package]]
name = "rcgen" name = "rcgen"
version = "0.13.2" version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" checksum = "49bc8ffa8a832eb1d7c8000337f8b0d2f4f2f5ec3cf4ddc26f125e3ad2451824"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"pem", "pem",
@ -3843,6 +3843,7 @@ dependencies = [
"log", "log",
"netns-rs", "netns-rs",
"nix 0.29.0", "nix 0.29.0",
"num_cpus",
"once_cell", "once_cell",
"pin-project-lite", "pin-project-lite",
"pingora-pool", "pingora-pool",

View File

@ -390,7 +390,7 @@ fn change_log_level(reset: bool, level: &str) -> Response<Full<Bytes>> {
// Invalid level provided // Invalid level provided
return plaintext_response( return plaintext_response(
hyper::StatusCode::BAD_REQUEST, hyper::StatusCode::BAD_REQUEST,
format!("Invalid level provided: {}\n{}", level, HELP_STRING), format!("Invalid level provided: {level}\n{HELP_STRING}"),
); );
}; };
} }
@ -398,7 +398,7 @@ fn change_log_level(reset: bool, level: &str) -> Response<Full<Bytes>> {
Ok(_) => list_loggers(), Ok(_) => list_loggers(),
Err(e) => plaintext_response( Err(e) => plaintext_response(
hyper::StatusCode::BAD_REQUEST, hyper::StatusCode::BAD_REQUEST,
format!("Failed to set new level: {}\n{}", e, HELP_STRING), format!("Failed to set new level: {e}\n{HELP_STRING}"),
), ),
} }
} }

View File

@ -136,6 +136,25 @@ pub async fn build_with_cert(
if config.proxy_mode == config::ProxyMode::Shared { if config.proxy_mode == config::ProxyMode::Shared {
tracing::info!("shared proxy mode - in-pod mode enabled"); tracing::info!("shared proxy mode - in-pod mode enabled");
// Create ztunnel inbound listener only if its specific identity and workload info are configured.
if let Some(inbound) = proxy_gen.create_ztunnel_self_proxy_listener().await? {
// Run the inbound listener in the data plane worker pool
let mut xds_rx_for_inbound = xds_rx.clone();
data_plane_pool.send(DataPlaneTask {
block_shutdown: true,
fut: Box::pin(async move {
tracing::info!("Starting ztunnel inbound listener task");
let _ = xds_rx_for_inbound.changed().await;
tokio::task::spawn(async move {
inbound.run().in_current_span().await;
})
.await?;
Ok(())
}),
})?;
}
let run_future = init_inpod_proxy_mgr( let run_future = init_inpod_proxy_mgr(
&mut registry, &mut registry,
&mut admin_server, &mut admin_server,
@ -247,7 +266,8 @@ fn new_data_plane_pool(num_worker_threads: usize) -> mpsc::Sender<DataPlaneTask>
.thread_name_fn(|| { .thread_name_fn(|| {
static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0); static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst); let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
format!("ztunnel-proxy-{id}") // Thread name can only be 16 chars so keep it short
format!("ztunnel-{id}")
}) })
.enable_all() .enable_all()
.build() .build()

View File

@ -54,10 +54,12 @@ const LOCAL_XDS_PATH: &str = "LOCAL_XDS_PATH";
const LOCAL_XDS: &str = "LOCAL_XDS"; const LOCAL_XDS: &str = "LOCAL_XDS";
const XDS_ON_DEMAND: &str = "XDS_ON_DEMAND"; const XDS_ON_DEMAND: &str = "XDS_ON_DEMAND";
const XDS_ADDRESS: &str = "XDS_ADDRESS"; const XDS_ADDRESS: &str = "XDS_ADDRESS";
const PREFERED_SERVICE_NAMESPACE: &str = "PREFERED_SERVICE_NAMESPACE";
const CA_ADDRESS: &str = "CA_ADDRESS"; const CA_ADDRESS: &str = "CA_ADDRESS";
const SECRET_TTL: &str = "SECRET_TTL"; const SECRET_TTL: &str = "SECRET_TTL";
const FAKE_CA: &str = "FAKE_CA"; const FAKE_CA: &str = "FAKE_CA";
const ZTUNNEL_WORKER_THREADS: &str = "ZTUNNEL_WORKER_THREADS"; const ZTUNNEL_WORKER_THREADS: &str = "ZTUNNEL_WORKER_THREADS";
const ZTUNNEL_CPU_LIMIT: &str = "ZTUNNEL_CPU_LIMIT";
const POOL_MAX_STREAMS_PER_CONNECTION: &str = "POOL_MAX_STREAMS_PER_CONNECTION"; const POOL_MAX_STREAMS_PER_CONNECTION: &str = "POOL_MAX_STREAMS_PER_CONNECTION";
const POOL_UNUSED_RELEASE_TIMEOUT: &str = "POOL_UNUSED_RELEASE_TIMEOUT"; const POOL_UNUSED_RELEASE_TIMEOUT: &str = "POOL_UNUSED_RELEASE_TIMEOUT";
// CONNECTION_TERMINATION_DEADLINE configures an explicit deadline // CONNECTION_TERMINATION_DEADLINE configures an explicit deadline
@ -70,6 +72,10 @@ const ENABLE_ORIG_SRC: &str = "ENABLE_ORIG_SRC";
const PROXY_CONFIG: &str = "PROXY_CONFIG"; const PROXY_CONFIG: &str = "PROXY_CONFIG";
const IPV6_ENABLED: &str = "IPV6_ENABLED"; const IPV6_ENABLED: &str = "IPV6_ENABLED";
const HTTP2_STREAM_WINDOW_SIZE: &str = "HTTP2_STREAM_WINDOW_SIZE";
const HTTP2_CONNECTION_WINDOW_SIZE: &str = "HTTP2_CONNECTION_WINDOW_SIZE";
const HTTP2_FRAME_SIZE: &str = "HTTP2_FRAME_SIZE";
const UNSTABLE_ENABLE_SOCKS5: &str = "UNSTABLE_ENABLE_SOCKS5"; const UNSTABLE_ENABLE_SOCKS5: &str = "UNSTABLE_ENABLE_SOCKS5";
const DEFAULT_WORKER_THREADS: u16 = 2; const DEFAULT_WORKER_THREADS: u16 = 2;
@ -237,6 +243,12 @@ pub struct Config {
// Allow custom alternative XDS hostname verification // Allow custom alternative XDS hostname verification
pub alt_xds_hostname: Option<String>, pub alt_xds_hostname: Option<String>,
/// Prefered service namespace to use for service resolution.
/// If unset, local namespaces is preferred and other namespaces have equal priority.
/// If set, the local namespace is preferred, then the defined prefered_service_namespace
/// and finally other namespaces at an equal priority.
pub prefered_service_namespace: Option<String>,
/// TTL for CSR requests /// TTL for CSR requests
pub secret_ttl: Duration, pub secret_ttl: Duration,
/// YAML config for local XDS workloads /// YAML config for local XDS workloads
@ -293,6 +305,12 @@ pub struct Config {
// If true, when AppTunnel is set for // If true, when AppTunnel is set for
pub localhost_app_tunnel: bool, pub localhost_app_tunnel: bool,
pub ztunnel_identity: Option<identity::Identity>,
pub ztunnel_workload: Option<state::WorkloadInfo>,
pub ipv6_enabled: bool,
} }
#[derive(serde::Serialize, Clone, Copy, Debug)] #[derive(serde::Serialize, Clone, Copy, Debug)]
@ -399,6 +417,60 @@ fn parse_headers(prefix: &str) -> Result<MetadataVector, Error> {
Ok(metadata) Ok(metadata)
} }
fn get_cpu_count() -> Result<usize, Error> {
// Allow overriding the count with an env var. This can be used to pass the CPU limit on Kubernetes
// from the downward API.
// Note the downward API will return the total thread count ("logical cores") if no limit is set,
// so it is really the same as num_cpus.
// We allow num_cpus for cases its not set (not on Kubernetes, etc).
match parse::<usize>(ZTUNNEL_CPU_LIMIT)? {
Some(limit) => Ok(limit),
// This is *logical cores*
None => Ok(num_cpus::get()),
}
}
/// Parse worker threads configuration, supporting both fixed numbers and percentages
fn parse_worker_threads(default: usize) -> Result<usize, Error> {
match parse::<String>(ZTUNNEL_WORKER_THREADS)? {
Some(value) => {
if let Some(percent_str) = value.strip_suffix('%') {
// Parse as percentage
let percent: f64 = percent_str.parse().map_err(|e| {
Error::EnvVar(
ZTUNNEL_WORKER_THREADS.to_string(),
value.clone(),
format!("invalid percentage: {e}"),
)
})?;
if percent <= 0.0 || percent > 100.0 {
return Err(Error::EnvVar(
ZTUNNEL_WORKER_THREADS.to_string(),
value,
"percentage must be between 0 and 100".to_string(),
));
}
let cpu_count = get_cpu_count()?;
// Round up, minimum of 1
let threads = ((cpu_count as f64 * percent / 100.0).ceil() as usize).max(1);
Ok(threads)
} else {
// Parse as fixed number
value.parse::<usize>().map_err(|e| {
Error::EnvVar(
ZTUNNEL_WORKER_THREADS.to_string(),
value,
format!("invalid number: {e}"),
)
})
}
}
None => Ok(default),
}
}
pub fn parse_config() -> Result<Config, Error> { pub fn parse_config() -> Result<Config, Error> {
let pc = parse_proxy_config()?; let pc = parse_proxy_config()?;
construct_config(pc) construct_config(pc)
@ -438,6 +510,14 @@ pub fn construct_config(pc: ProxyConfig) -> Result<Config, Error> {
.or_else(|| Some(default_istiod_address.clone())), .or_else(|| Some(default_istiod_address.clone())),
))?; ))?;
let prefered_service_namespace = match parse::<String>(PREFERED_SERVICE_NAMESPACE) {
Ok(ns) => ns,
Err(e) => {
warn!(err=?e, "failed to parse {PREFERED_SERVICE_NAMESPACE}, continuing with default behavior");
None
}
};
let istio_meta_cluster_id = ISTIO_META_PREFIX.to_owned() + CLUSTER_ID; let istio_meta_cluster_id = ISTIO_META_PREFIX.to_owned() + CLUSTER_ID;
let cluster_id: String = match parse::<String>(&istio_meta_cluster_id)? { let cluster_id: String = match parse::<String>(&istio_meta_cluster_id)? {
Some(id) => id, Some(id) => id,
@ -519,7 +599,7 @@ pub fn construct_config(pc: ProxyConfig) -> Result<Config, Error> {
// on a pod-by-pod basis. // on a pod-by-pod basis.
let dns_proxy_addr: Address = match pc.proxy_metadata.get(DNS_PROXY_ADDR_METADATA) { let dns_proxy_addr: Address = match pc.proxy_metadata.get(DNS_PROXY_ADDR_METADATA) {
Some(dns_addr) => Address::new(ipv6_localhost_enabled, dns_addr) Some(dns_addr) => Address::new(ipv6_localhost_enabled, dns_addr)
.unwrap_or_else(|_| panic!("failed to parse DNS_PROXY_ADDR: {}", dns_addr)), .unwrap_or_else(|_| panic!("failed to parse DNS_PROXY_ADDR: {dns_addr}")),
None => Address::Localhost(ipv6_localhost_enabled, DEFAULT_DNS_PORT), None => Address::Localhost(ipv6_localhost_enabled, DEFAULT_DNS_PORT),
}; };
@ -600,6 +680,29 @@ pub fn construct_config(pc: ProxyConfig) -> Result<Config, Error> {
let socket_config_defaults = SocketConfig::default(); let socket_config_defaults = SocketConfig::default();
// Read ztunnel identity and workload info from Downward API if available
let (ztunnel_identity, ztunnel_workload) = match (
parse::<String>("POD_NAMESPACE")?,
parse::<String>("SERVICE_ACCOUNT")?,
parse::<String>("POD_NAME")?,
) {
(Some(namespace), Some(service_account), Some(pod_name)) => {
let trust_domain = std::env::var("TRUST_DOMAIN")
.unwrap_or_else(|_| crate::identity::manager::DEFAULT_TRUST_DOMAIN.to_string());
let identity = identity::Identity::from_parts(
trust_domain.into(),
namespace.clone().into(),
service_account.clone().into(),
);
let workload = state::WorkloadInfo::new(pod_name, namespace, service_account);
(Some(identity), Some(workload))
}
_ => (None, None),
};
validate_config(Config { validate_config(Config {
proxy: parse_default(ENABLE_PROXY, true)?, proxy: parse_default(ENABLE_PROXY, true)?,
// Enable by default; running the server is not an issue, clients still need to opt-in to sending their // Enable by default; running the server is not an issue, clients still need to opt-in to sending their
@ -619,9 +722,15 @@ pub fn construct_config(pc: ProxyConfig) -> Result<Config, Error> {
DEFAULT_POOL_UNUSED_RELEASE_TIMEOUT, DEFAULT_POOL_UNUSED_RELEASE_TIMEOUT,
)?, )?,
window_size: 4 * 1024 * 1024, // window size: per-stream limit
connection_window_size: 4 * 1024 * 1024, window_size: parse_default(HTTP2_STREAM_WINDOW_SIZE, 4 * 1024 * 1024)?,
frame_size: 1024 * 1024, // connection window size: per connection.
// Setting this to the same value as window_size can introduce deadlocks in some applications
// where clients do not read data on streamA until they receive data on streamB.
// If streamA consumes the entire connection window, we enter a deadlock.
// A 4x limit should be appropriate without introducing too much potential buffering.
connection_window_size: parse_default(HTTP2_CONNECTION_WINDOW_SIZE, 16 * 1024 * 1024)?,
frame_size: parse_default(HTTP2_FRAME_SIZE, 1024 * 1024)?,
self_termination_deadline: match parse_duration(CONNECTION_TERMINATION_DEADLINE)? { self_termination_deadline: match parse_duration(CONNECTION_TERMINATION_DEADLINE)? {
Some(period) => period, Some(period) => period,
@ -675,6 +784,7 @@ pub fn construct_config(pc: ProxyConfig) -> Result<Config, Error> {
xds_address, xds_address,
xds_root_cert, xds_root_cert,
prefered_service_namespace,
ca_address, ca_address,
ca_root_cert, ca_root_cert,
alt_xds_hostname: parse(ALT_XDS_HOSTNAME)?, alt_xds_hostname: parse(ALT_XDS_HOSTNAME)?,
@ -688,8 +798,7 @@ pub fn construct_config(pc: ProxyConfig) -> Result<Config, Error> {
fake_ca, fake_ca,
auth, auth,
num_worker_threads: parse_default( num_worker_threads: parse_worker_threads(
ZTUNNEL_WORKER_THREADS,
pc.concurrency.unwrap_or(DEFAULT_WORKER_THREADS).into(), pc.concurrency.unwrap_or(DEFAULT_WORKER_THREADS).into(),
)?, )?,
@ -753,6 +862,9 @@ pub fn construct_config(pc: ProxyConfig) -> Result<Config, Error> {
ca_headers: parse_headers(ISTIO_CA_HEADER_PREFIX)?, ca_headers: parse_headers(ISTIO_CA_HEADER_PREFIX)?,
localhost_app_tunnel: parse_default(LOCALHOST_APP_TUNNEL, true)?, localhost_app_tunnel: parse_default(LOCALHOST_APP_TUNNEL, true)?,
ztunnel_identity,
ztunnel_workload,
ipv6_enabled,
}) })
} }
@ -1055,4 +1167,45 @@ pub mod tests {
assert!(metadata.vec.contains(&(key, value))); assert!(metadata.vec.contains(&(key, value)));
} }
} }
#[test]
fn test_parse_worker_threads() {
unsafe {
// Test fixed number
env::set_var(ZTUNNEL_WORKER_THREADS, "4");
assert_eq!(parse_worker_threads(2).unwrap(), 4);
// Test percentage with CPU limit
env::set_var(ZTUNNEL_CPU_LIMIT, "8");
env::set_var(ZTUNNEL_WORKER_THREADS, "50%");
assert_eq!(parse_worker_threads(2).unwrap(), 4); // 50% of 8 CPUs = 4 threads
// Test percentage with CPU limit
env::set_var(ZTUNNEL_CPU_LIMIT, "16");
env::set_var(ZTUNNEL_WORKER_THREADS, "30%");
assert_eq!(parse_worker_threads(2).unwrap(), 5); // Round up to 5
// Test low percentage that rounds up to 1
env::set_var(ZTUNNEL_CPU_LIMIT, "4");
env::set_var(ZTUNNEL_WORKER_THREADS, "10%");
assert_eq!(parse_worker_threads(2).unwrap(), 1); // 10% of 4 CPUs = 0.4, rounds up to 1
// Test default when no env var is set
env::remove_var(ZTUNNEL_WORKER_THREADS);
assert_eq!(parse_worker_threads(2).unwrap(), 2);
// Test without CPU limit (should use system CPU count)
env::remove_var(ZTUNNEL_CPU_LIMIT);
let system_cpus = num_cpus::get();
assert_eq!(get_cpu_count().unwrap(), system_cpus);
// Test with CPU limit
env::set_var(ZTUNNEL_CPU_LIMIT, "12");
assert_eq!(get_cpu_count().unwrap(), 12);
// Clean up
env::remove_var(ZTUNNEL_WORKER_THREADS);
env::remove_var(ZTUNNEL_CPU_LIMIT);
}
}
} }

View File

@ -47,7 +47,7 @@ use crate::drain::{DrainMode, DrainWatcher};
use crate::metrics::{DeferRecorder, IncrementRecorder, Recorder}; use crate::metrics::{DeferRecorder, IncrementRecorder, Recorder};
use crate::proxy::Error; use crate::proxy::Error;
use crate::state::DemandProxyState; use crate::state::DemandProxyState;
use crate::state::service::IpFamily; use crate::state::service::{IpFamily, Service};
use crate::state::workload::Workload; use crate::state::workload::Workload;
use crate::state::workload::address::Address; use crate::state::workload::address::Address;
use crate::{config, dns}; use crate::{config, dns};
@ -85,6 +85,8 @@ impl Server {
drain: DrainWatcher, drain: DrainWatcher,
socket_factory: &(dyn SocketFactory + Send + Sync), socket_factory: &(dyn SocketFactory + Send + Sync),
local_workload_information: Arc<LocalWorkloadFetcher>, local_workload_information: Arc<LocalWorkloadFetcher>,
prefered_service_namespace: Option<String>,
ipv6_enabled: bool,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
// if the address we got from config is supposed to be v6-enabled, // if the address we got from config is supposed to be v6-enabled,
// actually check if the local pod context our socketfactory operates in supports V6. // actually check if the local pod context our socketfactory operates in supports V6.
@ -102,6 +104,8 @@ impl Server {
forwarder, forwarder,
metrics, metrics,
local_workload_information, local_workload_information,
prefered_service_namespace,
ipv6_enabled,
); );
let store = Arc::new(store); let store = Arc::new(store);
let handler = dns::handler::Handler::new(store.clone()); let handler = dns::handler::Handler::new(store.clone());
@ -191,6 +195,8 @@ struct Store {
svc_domain: Name, svc_domain: Name,
metrics: Arc<Metrics>, metrics: Arc<Metrics>,
local_workload: Arc<LocalWorkloadFetcher>, local_workload: Arc<LocalWorkloadFetcher>,
prefered_service_namespace: Option<String>,
ipv6_enabled: bool,
} }
impl Store { impl Store {
@ -200,6 +206,8 @@ impl Store {
forwarder: Arc<dyn Forwarder>, forwarder: Arc<dyn Forwarder>,
metrics: Arc<Metrics>, metrics: Arc<Metrics>,
local_workload_information: Arc<LocalWorkloadFetcher>, local_workload_information: Arc<LocalWorkloadFetcher>,
prefered_service_namespace: Option<String>,
ipv6_enabled: bool,
) -> Self { ) -> Self {
let domain = as_name(domain); let domain = as_name(domain);
let svc_domain = append_name(as_name("svc"), &domain); let svc_domain = append_name(as_name("svc"), &domain);
@ -211,6 +219,8 @@ impl Store {
svc_domain, svc_domain,
metrics, metrics,
local_workload: local_workload_information, local_workload: local_workload_information,
prefered_service_namespace,
ipv6_enabled,
} }
} }
@ -359,7 +369,7 @@ impl Store {
let search_name_str = search_name.to_string().into(); let search_name_str = search_name.to_string().into();
search_name.set_fqdn(true); search_name.set_fqdn(true);
let service = state let services: Vec<Arc<Service>> = state
.services .services
.get_by_host(&search_name_str) .get_by_host(&search_name_str)
.iter() .iter()
@ -382,13 +392,30 @@ impl Store {
}) })
// Get the service matching the client namespace. If no match exists, just // Get the service matching the client namespace. If no match exists, just
// return the first service. // return the first service.
.find_or_first(|service| service.namespace == client.namespace) // .find_or_first(|service| service.namespace == client.namespace)
.cloned(); .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
.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(),
},
};
// First, lookup the host as a service. // First, lookup the host as a service.
if let Some(service) = service { if let Some(service) = service {
return Some(ServerMatch { return Some(ServerMatch {
server: Address::Service(service), server: Address::Service(service.clone()),
name: search_name, name: search_name,
alias, alias,
}); });
@ -400,6 +427,13 @@ impl Store {
None None
} }
fn record_type_enabled(&self, addr: &IpAddr) -> bool {
match addr {
IpAddr::V4(_) => true, // IPv4 always
IpAddr::V6(_) => self.ipv6_enabled, // IPv6 must be not be disabled in config
}
}
/// Gets the list of addresses of the requested record type from the server. /// Gets the list of addresses of the requested record type from the server.
fn get_addresses( fn get_addresses(
&self, &self,
@ -412,7 +446,7 @@ impl Store {
.workload_ips .workload_ips
.iter() .iter()
.filter_map(|addr| { .filter_map(|addr| {
if is_record_type(addr, record_type) { if is_record_type(addr, record_type) && self.record_type_enabled(addr) {
Some(*addr) Some(*addr)
} else { } else {
None None
@ -431,10 +465,9 @@ impl Store {
debug!("failed to fetch workload for {}", ep.workload_uid); debug!("failed to fetch workload for {}", ep.workload_uid);
return None; return None;
}; };
wl.workload_ips wl.workload_ips.iter().copied().find(|addr| {
.iter() is_record_type(addr, record_type) && self.record_type_enabled(addr)
.copied() })
.find(|addr| is_record_type(addr, record_type))
}) })
.collect() .collect()
} else { } else {
@ -446,6 +479,7 @@ impl Store {
.filter_map(|vip| { .filter_map(|vip| {
if is_record_type(&vip.address, record_type) if is_record_type(&vip.address, record_type)
&& client.network == vip.network && client.network == vip.network
&& self.record_type_enabled(&vip.address)
{ {
Some(vip.address) Some(vip.address)
} else { } else {
@ -564,7 +598,12 @@ impl Resolver for Store {
} }
Err(e) => { Err(e) => {
// Forwarding failed. Just return the error. // Forwarding failed. Just return the error.
access_log(request, Some(&client), "forwarding failed", 0); access_log(
request,
Some(&client),
&format!("forwarding failed ({e})"),
0,
);
return Err(e); return Err(e);
} }
} }
@ -589,7 +628,12 @@ impl Resolver for Store {
} }
Err(e) => { Err(e) => {
// Forwarding failed. Just return the error. // Forwarding failed. Just return the error.
access_log(request, Some(&client), "forwarding failed", 0); access_log(
request,
Some(&client),
&format!("forwarding failed ({e})"),
0,
);
return Err(e); return Err(e);
} }
} }
@ -605,7 +649,7 @@ impl Resolver for Store {
// From this point on, we are the authority for the response. // From this point on, we are the authority for the response.
let is_authoritative = true; let is_authoritative = true;
if !service_family_allowed(&service_match.server, record_type) { if !service_family_allowed(&service_match.server, record_type, self.ipv6_enabled) {
access_log( access_log(
request, request,
Some(&client), Some(&client),
@ -674,7 +718,13 @@ impl Resolver for Store {
/// anyway, so would naturally work. /// anyway, so would naturally work.
/// Headless services, however, do not have VIPs, and the Pods behind them can have dual stack IPs even with /// Headless services, however, do not have VIPs, and the Pods behind them can have dual stack IPs even with
/// the Service being single-stack. In this case, we are NOT supposed to return both IPs. /// the Service being single-stack. In this case, we are NOT supposed to return both IPs.
fn service_family_allowed(server: &Address, record_type: RecordType) -> bool { /// If IPv6 is globally disabled, AAAA records are not allowed.
fn service_family_allowed(server: &Address, record_type: RecordType, ipv6_enabled: bool) -> bool {
// If IPv6 is globally disabled, don't allow AAAA records
if !ipv6_enabled && record_type == RecordType::AAAA {
return false;
}
match server { match server {
Address::Service(service) => match service.ip_families { Address::Service(service) => match service.ip_families {
Some(IpFamily::IPv4) if record_type == RecordType::AAAA => false, Some(IpFamily::IPv4) if record_type == RecordType::AAAA => false,
@ -946,6 +996,7 @@ mod tests {
const NS1: &str = "ns1"; const NS1: &str = "ns1";
const NS2: &str = "ns2"; const NS2: &str = "ns2";
const PREFERRED: &str = "preferred-ns";
const NW1: Strng = strng::literal!("nw1"); const NW1: Strng = strng::literal!("nw1");
const NW2: Strng = strng::literal!("nw2"); const NW2: Strng = strng::literal!("nw2");
@ -1053,6 +1104,8 @@ mod tests {
forwarder, forwarder,
metrics: test_metrics(), metrics: test_metrics(),
local_workload, local_workload,
prefered_service_namespace: None,
ipv6_enabled: true,
}; };
let namespaced_domain = n(format!("{}.svc.cluster.local", c.client_namespace)); let namespaced_domain = n(format!("{}.svc.cluster.local", c.client_namespace));
@ -1368,6 +1421,18 @@ mod tests {
expect_code: ResponseCode::NXDomain, expect_code: ResponseCode::NXDomain,
..Default::default() ..Default::default()
}, },
Case {
name: "success: preferred namespace is chosen if local namespace is not defined",
host: "preferred.io.",
expect_records: vec![a(n("preferred.io."), ipv4("10.10.10.211"))],
..Default::default()
},
Case {
name: "success: external service resolves to local namespace's address",
host: "everywhere.io.",
expect_records: vec![a(n("everywhere.io."), ipv4("10.10.10.112"))],
..Default::default()
},
]; ];
// Create and start the proxy. // Create and start the proxy.
@ -1385,6 +1450,8 @@ mod tests {
drain, drain,
&factory, &factory,
local_workload, local_workload,
Some(PREFERRED.to_string()),
true, // ipv6_enabled for tests
) )
.await .await
.unwrap(); .unwrap();
@ -1404,8 +1471,8 @@ mod tests {
tasks.push(async move { tasks.push(async move {
let name = format!("[{protocol}] {}", c.name); let name = format!("[{protocol}] {}", c.name);
let resp = send_request(&mut client, n(c.host), c.query_type).await; let resp = send_request(&mut client, n(c.host), c.query_type).await;
assert_eq!(c.expect_authoritative, resp.authoritative(), "{}", name); assert_eq!(c.expect_authoritative, resp.authoritative(), "{name}");
assert_eq!(c.expect_code, resp.response_code(), "{}", name); assert_eq!(c.expect_code, resp.response_code(), "{name}");
if c.expect_code == ResponseCode::NoError { if c.expect_code == ResponseCode::NoError {
let mut actual = resp.answers().to_vec(); let mut actual = resp.answers().to_vec();
@ -1416,7 +1483,7 @@ mod tests {
if c.expect_authoritative { if c.expect_authoritative {
sort_records(&mut actual); sort_records(&mut actual);
} }
assert_eq!(c.expect_records, actual, "{}", name); assert_eq!(c.expect_records, actual, "{name}");
} }
}); });
} }
@ -1471,6 +1538,8 @@ mod tests {
drain, drain,
&factory, &factory,
local_workload, local_workload,
None,
true, // ipv6_enabled for tests
) )
.await .await
.unwrap(); .unwrap();
@ -1485,7 +1554,7 @@ mod tests {
for (protocol, client) in [("tcp", &mut tcp_client), ("udp", &mut udp_client)] { for (protocol, client) in [("tcp", &mut tcp_client), ("udp", &mut udp_client)] {
let name = format!("[{protocol}] {}", c.name); let name = format!("[{protocol}] {}", c.name);
let resp = send_request(client, n(c.host), RecordType::A).await; let resp = send_request(client, n(c.host), RecordType::A).await;
assert_eq!(c.expect_code, resp.response_code(), "{}", name); assert_eq!(c.expect_code, resp.response_code(), "{name}");
if c.expect_code == ResponseCode::NoError { if c.expect_code == ResponseCode::NoError {
assert!(!resp.answers().is_empty()); assert!(!resp.answers().is_empty());
} }
@ -1520,6 +1589,8 @@ mod tests {
}), }),
state.clone(), state.clone(),
), ),
prefered_service_namespace: None,
ipv6_enabled: true,
}; };
let ip4n6_client_ip = ip("::ffff:202:202"); let ip4n6_client_ip = ip("::ffff:202:202");
@ -1527,7 +1598,7 @@ mod tests {
match store.lookup(&req).await { match store.lookup(&req).await {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
panic!("IPv6 encoded IPv4 should work! Error was {:?}", e) panic!("IPv6 encoded IPv4 should work! Error was {e:?}");
} }
} }
} }
@ -1553,6 +1624,8 @@ mod tests {
drain, drain,
&factory, &factory,
local_workload, local_workload,
None,
true, // ipv6_enabled for tests
) )
.await .await
.unwrap(); .unwrap();
@ -1669,6 +1742,16 @@ mod tests {
xds_external_service("www.google.com", &[na(NW1, "1.1.1.1")]), xds_external_service("www.google.com", &[na(NW1, "1.1.1.1")]),
xds_service("productpage", NS1, &[na(NW1, "9.9.9.9")]), xds_service("productpage", NS1, &[na(NW1, "9.9.9.9")]),
xds_service("example", NS2, &[na(NW1, "10.10.10.10")]), xds_service("example", NS2, &[na(NW1, "10.10.10.10")]),
// Service with the same name in another namespace
// This should not be used if the preferred service namespace is set
xds_namespaced_external_service("everywhere.io", NS2, &[na(NW1, "10.10.10.110")]),
xds_namespaced_external_service("preferred.io", NS2, &[na(NW1, "10.10.10.210")]),
// Preferred service namespace
xds_namespaced_external_service("everywhere.io", PREFERRED, &[na(NW1, "10.10.10.111")]),
xds_namespaced_external_service("preferred.io", PREFERRED, &[na(NW1, "10.10.10.211")]),
// 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")]),
with_fqdn( with_fqdn(
"details.ns2.svc.cluster.remote", "details.ns2.svc.cluster.remote",
xds_service( xds_service(
@ -1819,9 +1902,17 @@ mod tests {
} }
fn xds_external_service<S: AsRef<str>>(hostname: S, addrs: &[NetworkAddress]) -> XdsService { fn xds_external_service<S: AsRef<str>>(hostname: S, addrs: &[NetworkAddress]) -> XdsService {
xds_namespaced_external_service(hostname, NS1, addrs)
}
fn xds_namespaced_external_service<S1: AsRef<str>, S2: AsRef<str>>(
hostname: S1,
ns: S2,
vips: &[NetworkAddress],
) -> XdsService {
with_fqdn( with_fqdn(
hostname.as_ref(), hostname.as_ref(),
xds_service(hostname.as_ref(), NS1, addrs), xds_service(hostname.as_ref(), ns.as_ref(), vips),
) )
} }

View File

@ -58,10 +58,7 @@ async fn load_token(path: &PathBuf) -> io::Result<Vec<u8>> {
let t = tokio::fs::read(path).await?; let t = tokio::fs::read(path).await?;
if t.is_empty() { if t.is_empty() {
return Err(io::Error::new( return Err(io::Error::other("token file exists, but was empty"));
io::ErrorKind::Other,
"token file exists, but was empty",
));
} }
Ok(t) Ok(t)
} }

View File

@ -38,6 +38,9 @@ use keyed_priority_queue::KeyedPriorityQueue;
const CERT_REFRESH_FAILURE_RETRY_DELAY_MAX_INTERVAL: Duration = Duration::from_secs(150); const CERT_REFRESH_FAILURE_RETRY_DELAY_MAX_INTERVAL: Duration = Duration::from_secs(150);
/// Default trust domain to use if not otherwise specified.
pub const DEFAULT_TRUST_DOMAIN: &str = "cluster.local";
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
pub enum Identity { pub enum Identity {
Spiffe { Spiffe {
@ -130,11 +133,10 @@ impl Identity {
#[cfg(any(test, feature = "testing"))] #[cfg(any(test, feature = "testing"))]
impl Default for Identity { impl Default for Identity {
fn default() -> Self { fn default() -> Self {
const TRUST_DOMAIN: &str = "cluster.local";
const SERVICE_ACCOUNT: &str = "ztunnel"; const SERVICE_ACCOUNT: &str = "ztunnel";
const NAMESPACE: &str = "istio-system"; const NAMESPACE: &str = "istio-system";
Identity::Spiffe { Identity::Spiffe {
trust_domain: TRUST_DOMAIN.into(), trust_domain: DEFAULT_TRUST_DOMAIN.into(),
namespace: NAMESPACE.into(), namespace: NAMESPACE.into(),
service_account: SERVICE_ACCOUNT.into(), service_account: SERVICE_ACCOUNT.into(),
} }

View File

@ -148,7 +148,7 @@ impl crate::proxy::SocketFactory for InPodSocketPortReuseFactory {
})?; })?;
if let Err(e) = sock.set_reuseport(true) { if let Err(e) = sock.set_reuseport(true) {
tracing::warn!("setting set_reuseport failed: {} addr: {}", e, addr); tracing::warn!("setting set_reuseport failed: {e} addr: {addr}");
} }
sock.bind(addr)?; sock.bind(addr)?;

View File

@ -37,7 +37,7 @@ use std::os::fd::{AsRawFd, OwnedFd};
use tracing::debug; use tracing::debug;
pub fn uid(i: usize) -> crate::inpod::WorkloadUid { pub fn uid(i: usize) -> crate::inpod::WorkloadUid {
crate::inpod::WorkloadUid::new(format!("uid{}", i)) crate::inpod::WorkloadUid::new(format!("uid{i}"))
} }
pub struct Fixture { pub struct Fixture {
@ -138,7 +138,7 @@ pub async fn read_msg(s: &mut UnixStream) -> WorkloadResponse {
debug!("read {} bytes", read_amount); debug!("read {} bytes", read_amount);
let ret = WorkloadResponse::decode(&buf[..read_amount]) let ret = WorkloadResponse::decode(&buf[..read_amount])
.unwrap_or_else(|_| panic!("failed to decode. read amount: {}", read_amount)); .unwrap_or_else(|_| panic!("failed to decode. read amount: {read_amount}"));
debug!("decoded {:?}", ret); debug!("decoded {:?}", ret);
ret ret

View File

@ -401,7 +401,7 @@ pub(crate) mod tests {
assert!(e.contains("EOF")); assert!(e.contains("EOF"));
} }
Ok(()) => {} Ok(()) => {}
Err(e) => panic!("expected error due to EOF {:?}", e), Err(e) => panic!("expected error due to EOF {e:?}"),
} }
} }

View File

@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use once_cell::sync::Lazy;
use std::env;
pub mod admin; pub mod admin;
pub mod app; pub mod app;
pub mod assertions; pub mod assertions;
@ -42,3 +45,7 @@ pub mod xds;
#[cfg(any(test, feature = "testing"))] #[cfg(any(test, feature = "testing"))]
pub mod test_helpers; pub mod test_helpers;
#[allow(dead_code)]
static PQC_ENABLED: Lazy<bool> =
Lazy::new(|| env::var("COMPLIANCE_POLICY").unwrap_or_default() == "pqc");

View File

@ -14,8 +14,9 @@
extern crate core; extern crate core;
use nix::sys::resource::{Resource, getrlimit, setrlimit};
use std::sync::Arc; use std::sync::Arc;
use tracing::info; use tracing::{info, warn};
use ztunnel::*; use ztunnel::*;
#[cfg(feature = "jemalloc")] #[cfg(feature = "jemalloc")]
@ -28,6 +29,26 @@ static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
#[unsafe(export_name = "malloc_conf")] #[unsafe(export_name = "malloc_conf")]
pub static malloc_conf: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0"; pub static malloc_conf: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0";
// We use this on Unix systems to increase the number of open file descriptors
// if possible. This is useful for high-load scenarios where the default limit
// is too low, which can lead to droopped connections and other issues:
// see: https://github.com/istio/ztunnel/issues/1585
fn increase_open_files_limit() {
#[cfg(unix)]
if let Ok((soft_limit, hard_limit)) = getrlimit(Resource::RLIMIT_NOFILE) {
if let Err(e) = setrlimit(Resource::RLIMIT_NOFILE, hard_limit, hard_limit) {
warn!("failed to set file descriptor limits: {e}");
} else {
info!(
"set file descriptor limits from {} to {}",
soft_limit, hard_limit
);
}
} else {
warn!("failed to get file descriptor limits");
}
}
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let _log_flush = telemetry::setup_logging(); let _log_flush = telemetry::setup_logging();
@ -74,6 +95,7 @@ fn version() -> anyhow::Result<()> {
async fn proxy(cfg: Arc<config::Config>) -> anyhow::Result<()> { async fn proxy(cfg: Arc<config::Config>) -> anyhow::Result<()> {
info!("version: {}", version::BuildInfo::new()); info!("version: {}", version::BuildInfo::new());
increase_open_files_limit();
info!("running with config: {}", serde_yaml::to_string(&cfg)?); info!("running with config: {}", serde_yaml::to_string(&cfg)?);
app::build(cfg).await?.wait_termination().await app::build(cfg).await?.wait_termination().await
} }

View File

@ -48,8 +48,9 @@ use crate::state::{DemandProxyState, WorkloadInfo};
use crate::{config, identity, socket, tls}; use crate::{config, identity, socket, tls};
pub mod connection_manager; pub mod connection_manager;
pub mod inbound;
mod h2; mod h2;
mod inbound;
mod inbound_passthrough; mod inbound_passthrough;
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub mod metrics; pub mod metrics;
@ -259,6 +260,8 @@ pub(super) struct ProxyInputs {
socket_factory: Arc<dyn SocketFactory + Send + Sync>, socket_factory: Arc<dyn SocketFactory + Send + Sync>,
local_workload_information: Arc<LocalWorkloadInformation>, local_workload_information: Arc<LocalWorkloadInformation>,
resolver: Option<Arc<dyn Resolver + Send + Sync>>, resolver: Option<Arc<dyn Resolver + Send + Sync>>,
// If true, inbound connections created with these inputs will not attempt to preserve the original source IP.
pub disable_inbound_freebind: bool,
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -271,6 +274,7 @@ impl ProxyInputs {
socket_factory: Arc<dyn SocketFactory + Send + Sync>, socket_factory: Arc<dyn SocketFactory + Send + Sync>,
resolver: Option<Arc<dyn Resolver + Send + Sync>>, resolver: Option<Arc<dyn Resolver + Send + Sync>>,
local_workload_information: Arc<LocalWorkloadInformation>, local_workload_information: Arc<LocalWorkloadInformation>,
disable_inbound_freebind: bool,
) -> Arc<Self> { ) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
cfg, cfg,
@ -280,6 +284,7 @@ impl ProxyInputs {
socket_factory, socket_factory,
local_workload_information, local_workload_information,
resolver, resolver,
disable_inbound_freebind,
}) })
} }
} }
@ -301,7 +306,7 @@ impl Proxy {
old_cfg.inbound_addr = inbound.address(); old_cfg.inbound_addr = inbound.address();
let mut new_pi = (*pi).clone(); let mut new_pi = (*pi).clone();
new_pi.cfg = Arc::new(old_cfg); new_pi.cfg = Arc::new(old_cfg);
std::mem::swap(&mut pi, &mut Arc::new(new_pi)); pi = Arc::new(new_pi);
warn!("TEST FAKE: new address is {:?}", pi.cfg.inbound_addr); warn!("TEST FAKE: new address is {:?}", pi.cfg.inbound_addr);
} }
@ -368,7 +373,7 @@ impl fmt::Display for AuthorizationRejectionError {
match self { match self {
Self::NoWorkload => write!(fmt, "workload not found"), Self::NoWorkload => write!(fmt, "workload not found"),
Self::WorkloadMismatch => write!(fmt, "workload mismatch"), Self::WorkloadMismatch => write!(fmt, "workload mismatch"),
Self::ExplicitlyDenied(a, b) => write!(fmt, "explicitly denied by: {}/{}", a, b), Self::ExplicitlyDenied(a, b) => write!(fmt, "explicitly denied by: {a}/{b}"),
Self::NotAllowed => write!(fmt, "allow policies exist, but none allowed"), Self::NotAllowed => write!(fmt, "allow policies exist, but none allowed"),
} }
} }
@ -479,6 +484,9 @@ pub enum Error {
#[error("requested service {0} found, but has no IP addresses")] #[error("requested service {0} found, but has no IP addresses")]
NoIPForService(String), NoIPForService(String),
#[error("no service for target address: {0}")]
NoService(SocketAddr),
#[error( #[error(
"ip addresses were resolved for workload {0}, but valid dns response had no A/AAAA records" "ip addresses were resolved for workload {0}, but valid dns response had no A/AAAA records"
)] )]
@ -839,8 +847,8 @@ impl HboneAddress {
impl std::fmt::Display for HboneAddress { impl std::fmt::Display for HboneAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
HboneAddress::SocketAddr(addr) => write!(f, "{}", addr), HboneAddress::SocketAddr(addr) => write!(f, "{addr}"),
HboneAddress::SvcHostname(host, port) => write!(f, "{}:{}", host, port), HboneAddress::SvcHostname(host, port) => write!(f, "{host}:{port}"),
} }
} }
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
use crate::copy; use crate::copy;
use bytes::{BufMut, Bytes}; use bytes::Bytes;
use futures_core::ready; use futures_core::ready;
use h2::Reason; use h2::Reason;
use std::io::Error; use std::io::Error;
@ -85,7 +85,10 @@ pub struct H2StreamWriteHalf {
_dropped: Option<DropCounter>, _dropped: Option<DropCounter>,
} }
pub struct TokioH2Stream(H2Stream); pub struct TokioH2Stream {
stream: H2Stream,
buf: Bytes,
}
struct DropCounter { struct DropCounter {
// Whether the other end of this shared counter has already dropped. // Whether the other end of this shared counter has already dropped.
@ -144,7 +147,10 @@ impl Drop for DropCounter {
// then the specific implementation will conflict with the generic one. // then the specific implementation will conflict with the generic one.
impl TokioH2Stream { impl TokioH2Stream {
pub fn new(stream: H2Stream) -> Self { pub fn new(stream: H2Stream) -> Self {
Self(stream) Self {
stream,
buf: Bytes::new(),
}
} }
} }
@ -154,24 +160,21 @@ impl tokio::io::AsyncRead for TokioH2Stream {
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>, buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> { ) -> Poll<std::io::Result<()>> {
let pinned = std::pin::Pin::new(&mut self.0.read); // Just return the bytes we have left over and don't poll the stream because
copy::ResizeBufRead::poll_bytes(pinned, cx).map(|r| match r { // its unclear what to do if there are bytes left over from the previous read, and when we
Ok(bytes) => { // poll, we get an error.
if buf.remaining() < bytes.len() { if self.buf.is_empty() {
Err(Error::new( // If we have no unread bytes, we can poll the stream
std::io::ErrorKind::Other, // and fill self.buf with the bytes we read.
format!( let pinned = std::pin::Pin::new(&mut self.stream.read);
"kould overflow buffer of with {} remaining", let res = ready!(copy::ResizeBufRead::poll_bytes(pinned, cx))?;
buf.remaining() self.buf = res;
), }
)) // Copy as many bytes as we can from self.buf.
} else { let cnt = Ord::min(buf.remaining(), self.buf.len());
buf.put(bytes); buf.put_slice(&self.buf[..cnt]);
Ok(()) self.buf = self.buf.split_off(cnt);
} Poll::Ready(Ok(()))
}
Err(e) => Err(e),
})
} }
} }
@ -181,7 +184,7 @@ impl tokio::io::AsyncWrite for TokioH2Stream {
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &[u8], buf: &[u8],
) -> Poll<Result<usize, tokio::io::Error>> { ) -> Poll<Result<usize, tokio::io::Error>> {
let pinned = std::pin::Pin::new(&mut self.0.write); let pinned = std::pin::Pin::new(&mut self.stream.write);
let buf = Bytes::copy_from_slice(buf); let buf = Bytes::copy_from_slice(buf);
copy::AsyncWriteBuf::poll_write_buf(pinned, cx, buf) copy::AsyncWriteBuf::poll_write_buf(pinned, cx, buf)
} }
@ -190,7 +193,7 @@ impl tokio::io::AsyncWrite for TokioH2Stream {
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> { ) -> Poll<Result<(), std::io::Error>> {
let pinned = std::pin::Pin::new(&mut self.0.write); let pinned = std::pin::Pin::new(&mut self.stream.write);
copy::AsyncWriteBuf::poll_flush(pinned, cx) copy::AsyncWriteBuf::poll_flush(pinned, cx)
} }
@ -198,7 +201,7 @@ impl tokio::io::AsyncWrite for TokioH2Stream {
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> { ) -> Poll<Result<(), std::io::Error>> {
let pinned = std::pin::Pin::new(&mut self.0.write); let pinned = std::pin::Pin::new(&mut self.stream.write);
copy::AsyncWriteBuf::poll_shutdown(pinned, cx) copy::AsyncWriteBuf::poll_shutdown(pinned, cx)
} }
} }
@ -302,6 +305,6 @@ fn h2_to_io_error(e: h2::Error) -> std::io::Error {
if e.is_io() { if e.is_io() {
e.into_io().unwrap() e.into_io().unwrap()
} else { } else {
std::io::Error::new(std::io::ErrorKind::Other, e) std::io::Error::other(e)
} }
} }

View File

@ -45,7 +45,7 @@ use crate::state::{DemandProxyState, ProxyRbacContext};
use crate::strng::Strng; use crate::strng::Strng;
use crate::tls::TlsError; use crate::tls::TlsError;
pub(super) struct Inbound { pub struct Inbound {
listener: socket::Listener, listener: socket::Listener,
drain: DrainWatcher, drain: DrainWatcher,
pi: Arc<ProxyInputs>, pi: Arc<ProxyInputs>,
@ -53,7 +53,7 @@ pub(super) struct Inbound {
} }
impl Inbound { impl Inbound {
pub(super) async fn new(pi: Arc<ProxyInputs>, drain: DrainWatcher) -> Result<Inbound, Error> { pub(crate) async fn new(pi: Arc<ProxyInputs>, drain: DrainWatcher) -> Result<Inbound, Error> {
let listener = pi let listener = pi
.socket_factory .socket_factory
.tcp_bind(pi.cfg.inbound_addr) .tcp_bind(pi.cfg.inbound_addr)
@ -74,11 +74,12 @@ impl Inbound {
}) })
} }
pub(super) fn address(&self) -> SocketAddr { /// Returns the socket address this proxy is listening on.
pub fn address(&self) -> SocketAddr {
self.listener.local_addr() self.listener.local_addr()
} }
pub(super) async fn run(self) { pub async fn run(self) {
let pi = self.pi.clone(); let pi = self.pi.clone();
let acceptor = InboundCertProvider { let acceptor = InboundCertProvider {
local_workload: self.pi.local_workload_information.clone(), local_workload: self.pi.local_workload_information.clone(),
@ -122,7 +123,7 @@ impl Inbound {
let conn = Connection { let conn = Connection {
src_identity, src_identity,
src, src,
dst_network: strng::new(&network), // inbound request must be on our network dst_network: network.clone(), // inbound request must be on our network
dst, dst,
}; };
debug!(%conn, "accepted connection"); debug!(%conn, "accepted connection");
@ -244,10 +245,18 @@ impl Inbound {
SocketAddr::new(loopback, ri.upstream_addr.port()), SocketAddr::new(loopback, ri.upstream_addr.port()),
) )
} else { } else {
( // When ztunnel is proxying to its own internal endpoints (metrics server after HBONE termination),
enable_original_source.then_some(ri.rbac_ctx.conn.src.ip()), // we must not attempt to use the original external client's IP as the source for this internal connection.
ri.upstream_addr, // Setting `disable_inbound_freebind` to true for such self-proxy scenarios ensures `upstream_src_ip` is `None`,
) // causing `freebind_connect` to use a local IP for the connection to ztunnel's own service.
// For regular inbound traffic to other workloads, `disable_inbound_freebind` is false, and original source
// preservation depends on `enable_original_source`.
let upstream_src_ip = if pi.disable_inbound_freebind {
None
} else {
enable_original_source.then_some(ri.rbac_ctx.conn.src.ip())
};
(upstream_src_ip, ri.upstream_addr)
}; };
// Establish upstream connection between original source and destination // Establish upstream connection between original source and destination
@ -536,7 +545,7 @@ impl Inbound {
/// find_inbound_upstream determines the next hop for an inbound request. /// find_inbound_upstream determines the next hop for an inbound request.
#[expect(clippy::type_complexity)] #[expect(clippy::type_complexity)]
fn find_inbound_upstream( pub(super) fn find_inbound_upstream(
cfg: &Config, cfg: &Config,
state: &DemandProxyState, state: &DemandProxyState,
conn: &Connection, conn: &Connection,
@ -545,6 +554,7 @@ impl Inbound {
) -> Result<(SocketAddr, Option<TunnelRequest>, Vec<Arc<Service>>), Error> { ) -> Result<(SocketAddr, Option<TunnelRequest>, Vec<Arc<Service>>), Error> {
// We always target the local workload IP as the destination. But we need to determine the port to send to. // We always target the local workload IP as the destination. But we need to determine the port to send to.
let target_ip = conn.dst.ip(); let target_ip = conn.dst.ip();
// First, fetch the actual target SocketAddr as well as all possible services this could be for. // First, fetch the actual target SocketAddr as well as all possible services this could be for.
// Given they may request the pod directly, there may be multiple possible services; we will // Given they may request the pod directly, there may be multiple possible services; we will
// select a final one (if any) later. // select a final one (if any) later.
@ -640,7 +650,7 @@ impl Inbound {
} }
#[derive(Debug)] #[derive(Debug)]
struct TunnelRequest { pub(super) struct TunnelRequest {
tunnel_target: SocketAddr, tunnel_target: SocketAddr,
protocol: Protocol, protocol: Protocol,
} }
@ -702,37 +712,36 @@ fn build_response(status: StatusCode) -> Response<()> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::too_many_arguments)]
mod tests { mod tests {
use super::{Inbound, ProxyInputs}; use super::{Inbound, ProxyInputs};
use crate::{config, proxy::ConnectionManager, proxy::inbound::HboneAddress, strng};
use crate::{ use crate::{
config,
identity::manager::mock::new_secret_manager,
proxy::{
ConnectionManager, DefaultSocketFactory, LocalWorkloadInformation,
h2::server::RequestParts, inbound::HboneAddress,
},
rbac::Connection, rbac::Connection,
state::{ state::{
self, DemandProxyState, self, DemandProxyState, WorkloadInfo,
service::{Endpoint, EndpointSet, Service}, service::{Endpoint, EndpointSet, Service},
workload::{ workload::{
ApplicationTunnel, GatewayAddress, InboundProtocol, NetworkAddress, Workload, ApplicationTunnel, GatewayAddress, HealthStatus, InboundProtocol, NetworkAddress,
application_tunnel::Protocol as AppProtocol, gatewayaddress::Destination, NetworkMode, Workload, application_tunnel::Protocol as AppProtocol,
gatewayaddress::Destination,
}, },
}, },
test_helpers, strng, test_helpers,
}; };
use hickory_resolver::config::{ResolverConfig, ResolverOpts};
use http::{Method, Uri};
use prometheus_client::registry::Registry;
use std::{ use std::{
net::SocketAddr, net::SocketAddr,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
time::Duration, time::Duration,
}; };
use crate::identity::manager::mock::new_secret_manager;
use crate::proxy::DefaultSocketFactory;
use crate::proxy::LocalWorkloadInformation;
use crate::proxy::h2::server::RequestParts;
use crate::state::WorkloadInfo;
use crate::state::workload::HealthStatus;
use hickory_resolver::config::{ResolverConfig, ResolverOpts};
use http::{Method, Uri};
use prometheus_client::registry::Registry;
use test_case::test_case; use test_case::test_case;
const CLIENT_POD_IP: &str = "10.0.0.1"; const CLIENT_POD_IP: &str = "10.0.0.1";
@ -904,6 +913,7 @@ mod tests {
sf, sf,
None, None,
local_workload, local_workload,
false,
)); ));
let inbound_request = Inbound::build_inbound_request(&pi, conn, &request_parts).await; let inbound_request = Inbound::build_inbound_request(&pi, conn, &request_parts).await;
match want { match want {
@ -959,7 +969,6 @@ mod tests {
"waypoint", "waypoint",
WAYPOINT_POD_IP, WAYPOINT_POD_IP,
Waypoint::None, Waypoint::None,
// the waypoint's _workload_ gets the app tunnel field
server_waypoint.app_tunnel(), server_waypoint.app_tunnel(),
), ),
("client", CLIENT_POD_IP, Waypoint::None, None), ("client", CLIENT_POD_IP, Waypoint::None, None),
@ -975,6 +984,7 @@ mod tests {
namespace: "default".into(), namespace: "default".into(),
service_account: strng::format!("service-account-{name}"), service_account: strng::format!("service-account-{name}"),
application_tunnel: app_tunnel, application_tunnel: app_tunnel,
network_mode: NetworkMode::Standard,
..test_helpers::test_default_workload() ..test_helpers::test_default_workload()
}); });

View File

@ -391,7 +391,7 @@ pub fn log_early_deny<E: std::error::Error>(
"inbound" "inbound"
}, },
error = format!("{}", err), error = format!("{err}"),
"connection failed" "connection failed"
); );

View File

@ -35,10 +35,10 @@ use crate::proxy::{ConnectionOpen, ConnectionResult, DerivedWorkload, metrics};
use crate::drain::DrainWatcher; use crate::drain::DrainWatcher;
use crate::drain::run_with_drain; use crate::drain::run_with_drain;
use crate::proxy::h2::{H2Stream, client::WorkloadKey}; use crate::proxy::h2::{H2Stream, client::WorkloadKey};
use crate::state::ServiceResolutionMode; use crate::state::service::{Service, ServiceDescription};
use crate::state::service::ServiceDescription;
use crate::state::workload::OutboundProtocol; use crate::state::workload::OutboundProtocol;
use crate::state::workload::{InboundProtocol, NetworkAddress, Workload, address::Address}; use crate::state::workload::{InboundProtocol, NetworkAddress, Workload, address::Address};
use crate::state::{ServiceResolutionMode, Upstream};
use crate::{assertions, copy, proxy, socket}; use crate::{assertions, copy, proxy, socket};
use super::h2::TokioH2Stream; use super::h2::TokioH2Stream;
@ -369,6 +369,83 @@ impl OutboundConnection {
} }
} }
// This function is called when the select next hop is on a different network,
// so we expect the upstream workload to have a network gatewy configured.
//
// When we use a gateway to reach to a workload on a remote network we have to
// use double HBONE (HBONE incapsulated inside HBONE). The gateway will
// terminate the outer HBONE tunnel and forward the inner HBONE to the actual
// destination as a opaque stream of bytes and the actual destination will
// interpret it as an HBONE connection.
//
// If the upstream workload does not have an E/W gateway this function returns
// an error indicating that it could not find a valid destination.
//
// A note about double HBONE, in double HBONE both inner and outer HBONE use
// destination service name as HBONE target URI.
//
// Having target URI in the outer HBONE tunnel allows E/W gateway to figure out
// where to route the data next witout the need to terminate inner HBONE tunnel.
// In other words, it could forward inner HBONE as if it's an opaque stream of
// bytes without trying to interpret it.
//
// NOTE: when connecting through an E/W gateway, regardless of whether there is
// a waypoint or not, we always use service hostname and the service port. It's
// somewhat different from how regular HBONE works, so I'm calling it out here.
async fn build_request_through_gateway(
&self,
source: Arc<Workload>,
// next hop on the remote network that we picked as our destination.
// It may be a local view of a Waypoint workload on remote network or
// a local view of the service workload (when waypoint is not
// configured).
upstream: Upstream,
// This is a target service we wanted to reach in the first place.
//
// NOTE: Crossing network boundaries is only supported for services
// at the moment, so we should always have a service we could use.
service: &Service,
target: SocketAddr,
) -> Result<Request, Error> {
if let Some(gateway) = &upstream.workload.network_gateway {
let gateway_upstream = self
.pi
.state
.fetch_network_gateway(gateway, &source, target)
.await?;
let hbone_target_destination = Some(HboneAddress::SvcHostname(
service.hostname.clone(),
target.port(),
));
debug!("built request to a destination on another network through an E/W gateway");
Ok(Request {
protocol: OutboundProtocol::DOUBLEHBONE,
source,
hbone_target_destination,
actual_destination_workload: Some(gateway_upstream.workload.clone()),
intended_destination_service: Some(ServiceDescription::from(service)),
actual_destination: gateway_upstream.workload_socket_addr().ok_or(
Error::NoValidDestination(Box::new((*gateway_upstream.workload).clone())),
)?,
// The outer tunnel of double HBONE is terminated by the E/W
// gateway and so for the credentials of the next hop
// (upstream_sans) we use gateway credentials.
upstream_sans: gateway_upstream.workload_and_services_san(),
// The inner HBONE tunnel is terminated by either the server
// we want to reach or a Waypoint in front of it, depending on
// the configuration. So for the final destination credentials
// (final_sans) we use the upstream workload credentials.
final_sans: upstream.service_sans(),
})
} else {
// Do not try to send cross-network traffic without network gateway.
Err(Error::NoValidDestination(Box::new(
(*upstream.workload).clone(),
)))
}
}
// build_request computes all information about the request we should send // build_request computes all information about the request we should send
// TODO: Do we want a single lock for source and upstream...? // TODO: Do we want a single lock for source and upstream...?
async fn build_request( async fn build_request(
@ -381,7 +458,7 @@ impl OutboundConnection {
// If this is to-service traffic check for a service waypoint // If this is to-service traffic check for a service waypoint
// Capture result of whether this is svc addressed // Capture result of whether this is svc addressed
let svc_addressed = if let Some(Address::Service(target_service)) = state let service = if let Some(Address::Service(target_service)) = state
.fetch_address(&NetworkAddress { .fetch_address(&NetworkAddress {
network: self.pi.cfg.network.clone(), network: self.pi.cfg.network.clone(),
address: target.ip(), address: target.ip(),
@ -393,6 +470,18 @@ impl OutboundConnection {
.fetch_service_waypoint(&target_service, &source_workload, target) .fetch_service_waypoint(&target_service, &source_workload, target)
.await? .await?
{ {
if waypoint.workload.network != source_workload.network {
debug!("picked a waypoint on remote network");
return self
.build_request_through_gateway(
source_workload.clone(),
waypoint,
&target_service,
target,
)
.await;
}
let upstream_sans = waypoint.workload_and_services_san(); let upstream_sans = waypoint.workload_and_services_san();
let actual_destination = let actual_destination =
waypoint waypoint
@ -413,10 +502,10 @@ impl OutboundConnection {
}); });
} }
// this was service addressed but we did not find a waypoint // this was service addressed but we did not find a waypoint
true Some(target_service)
} else { } else {
// this wasn't service addressed // this wasn't service addressed
false None
}; };
let Some(us) = state let Some(us) = state
@ -428,7 +517,7 @@ impl OutboundConnection {
) )
.await? .await?
else { else {
if svc_addressed { if service.is_some() {
return Err(Error::NoHealthyUpstream(target)); return Err(Error::NoHealthyUpstream(target));
} }
debug!("built request as passthrough; no upstream found"); debug!("built request as passthrough; no upstream found");
@ -446,37 +535,26 @@ impl OutboundConnection {
// Check whether we are using an E/W gateway and sending cross network traffic // Check whether we are using an E/W gateway and sending cross network traffic
if us.workload.network != source_workload.network { if us.workload.network != source_workload.network {
if let Some(ew_gtw) = &us.workload.network_gateway { // Workloads on remote network must be service addressed, so if we got here
let gtw_us = { // and we don't have a service for the original target address then it's a
self.pi // bug either in ztunnel itself or in istiod.
.state //
.fetch_network_gateway(ew_gtw, &source_workload, target) // For a double HBONE protocol implementation we have to know the
.await? // destination service and if there is no service for the target it's a bug.
}; //
// This situation "should never happen" because for workloads fetch_upstream
let svc = us // above only checks the workloads on the same network as this ztunnel
.destination_service // instance and therefore it should not be able to find a workload on a
.as_ref() // different network.
.expect("Workloads with network gateways must be service addressed."); debug_assert!(
let hbone_target_destination = service.is_some(),
Some(HboneAddress::SvcHostname(svc.hostname.clone(), us.port)); "workload on remote network is not service addressed"
);
return Ok(Request { debug!("picked a workload on remote network");
protocol: OutboundProtocol::DOUBLEHBONE, let service = service.as_ref().ok_or(Error::NoService(target))?;
source: source_workload, return self
hbone_target_destination, .build_request_through_gateway(source_workload.clone(), us, service, target)
actual_destination_workload: Some(gtw_us.workload.clone()), .await;
intended_destination_service: us.destination_service.clone(),
actual_destination: gtw_us.workload_socket_addr().ok_or(
Error::NoValidDestination(Box::new((*gtw_us.workload).clone())),
)?,
upstream_sans: gtw_us.workload_and_services_san(),
final_sans: us.service_sans(),
});
} else {
// Do not try to send cross-network traffic without network gateway.
return Err(Error::NoValidDestination(Box::new((*us.workload).clone())));
}
} }
// We are not using a network gateway and there is no workload address. // We are not using a network gateway and there is no workload address.
@ -491,7 +569,7 @@ impl OutboundConnection {
// Check if we need to go through a workload addressed waypoint. // Check if we need to go through a workload addressed waypoint.
// Don't traverse waypoint twice if the source is sandwich-outbound. // Don't traverse waypoint twice if the source is sandwich-outbound.
// Don't traverse waypoint if traffic was addressed to a service (handled before) // Don't traverse waypoint if traffic was addressed to a service (handled before)
if !from_waypoint && !svc_addressed { if !from_waypoint && service.is_none() {
// For case upstream server has enabled waypoint // For case upstream server has enabled waypoint
let waypoint = state let waypoint = state
.fetch_workload_waypoint(&us.workload, &source_workload, target) .fetch_workload_waypoint(&us.workload, &source_workload, target)
@ -716,6 +794,7 @@ mod tests {
local_workload_information: local_workload_information.clone(), local_workload_information: local_workload_information.clone(),
connection_manager: ConnectionManager::default(), connection_manager: ConnectionManager::default(),
resolver: None, resolver: None,
disable_inbound_freebind: false,
}), }),
id: TraceParent::new(), id: TraceParent::new(),
pool: WorkloadHBONEPool::new( pool: WorkloadHBONEPool::new(
@ -815,6 +894,8 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn build_request_double_hbone() { async fn build_request_double_hbone() {
// example.com service has a workload on remote network.
// E/W gateway is addressed by an IP.
run_build_request_multi( run_build_request_multi(
"127.0.0.1", "127.0.0.1",
"127.0.0.3:80", "127.0.0.3:80",
@ -866,11 +947,13 @@ mod tests {
], ],
Some(ExpectedRequest { Some(ExpectedRequest {
protocol: OutboundProtocol::DOUBLEHBONE, protocol: OutboundProtocol::DOUBLEHBONE,
hbone_destination: "example.com:8080", hbone_destination: "example.com:80",
destination: "10.22.1.1:15009", destination: "10.22.1.1:15009",
}), }),
) )
.await; .await;
// example.com service has a workload on remote network.
// E/W gateway is addressed by a hostname.
run_build_request_multi( run_build_request_multi(
"127.0.0.1", "127.0.0.1",
"127.0.0.3:80", "127.0.0.3:80",
@ -943,11 +1026,218 @@ mod tests {
], ],
Some(ExpectedRequest { Some(ExpectedRequest {
protocol: OutboundProtocol::DOUBLEHBONE, protocol: OutboundProtocol::DOUBLEHBONE,
hbone_destination: "example.com:8080", hbone_destination: "example.com:80",
destination: "127.0.0.5:15008", destination: "127.0.0.5:15008",
}), }),
) )
.await; .await;
// example.com service has a waypoint and waypoint workload is on remote network.
// E/W gateway is addressed by an IP.
run_build_request_multi(
"127.0.0.1",
"127.0.0.3:80",
vec![
XdsAddressType::Service(XdsService {
hostname: "example.com".to_string(),
addresses: vec![XdsNetworkAddress {
network: "".to_string(),
address: vec![127, 0, 0, 3],
}],
ports: vec![Port {
service_port: 80,
target_port: 8080,
}],
waypoint: Some(xds::istio::workload::GatewayAddress {
destination: Some(
xds::istio::workload::gateway_address::Destination::Hostname(
XdsNamespacedHostname {
namespace: Default::default(),
hostname: "waypoint.com".into(),
},
),
),
hbone_mtls_port: 15008,
}),
..Default::default()
}),
XdsAddressType::Service(XdsService {
hostname: "waypoint.com".to_string(),
addresses: vec![XdsNetworkAddress {
network: "".to_string(),
address: vec![127, 0, 0, 4],
}],
ports: vec![Port {
service_port: 15008,
target_port: 15008,
}],
..Default::default()
}),
XdsAddressType::Workload(XdsWorkload {
uid: "Kubernetes//Pod/default/remote-waypoint-pod".to_string(),
addresses: vec![],
network: "remote".to_string(),
network_gateway: Some(xds::istio::workload::GatewayAddress {
destination: Some(
xds::istio::workload::gateway_address::Destination::Address(
XdsNetworkAddress {
network: "remote".to_string(),
address: vec![10, 22, 1, 1],
},
),
),
hbone_mtls_port: 15009,
}),
services: std::collections::HashMap::from([(
"/waypoint.com".to_string(),
PortList {
ports: vec![Port {
service_port: 15008,
target_port: 15008,
}],
},
)]),
..Default::default()
}),
XdsAddressType::Workload(XdsWorkload {
uid: "Kubernetes//Pod/default/remote-ew-gtw".to_string(),
addresses: vec![Bytes::copy_from_slice(&[10, 22, 1, 1])],
network: "remote".to_string(),
..Default::default()
}),
],
Some(ExpectedRequest {
protocol: OutboundProtocol::DOUBLEHBONE,
hbone_destination: "example.com:80",
destination: "10.22.1.1:15009",
}),
)
.await;
}
#[tokio::test]
async fn build_request_failover_to_remote() {
// Similar to the double HBONE test that we already have, but it sets up a scenario when
// load balancing logic will pick a workload on a remote cluster when local workloads are
// unhealthy, thus showing the expected failover behavior.
let service = XdsAddressType::Service(XdsService {
hostname: "example.com".to_string(),
addresses: vec![XdsNetworkAddress {
network: "".to_string(),
address: vec![127, 0, 0, 3],
}],
ports: vec![Port {
service_port: 80,
target_port: 8080,
}],
// Prefer routing to workloads on the same network, but when nothing is healthy locally
// allow failing over to remote networks.
load_balancing: Some(xds::istio::workload::LoadBalancing {
routing_preference: vec![
xds::istio::workload::load_balancing::Scope::Network.into(),
],
mode: xds::istio::workload::load_balancing::Mode::Failover.into(),
..Default::default()
}),
..Default::default()
});
let ew_gateway = XdsAddressType::Workload(XdsWorkload {
uid: "Kubernetes//Pod/default/remote-ew-gtw".to_string(),
addresses: vec![Bytes::copy_from_slice(&[10, 22, 1, 1])],
network: "remote".to_string(),
..Default::default()
});
let remote_workload = XdsAddressType::Workload(XdsWorkload {
uid: "Kubernetes//Pod/default/remote-example.com-pod".to_string(),
addresses: vec![],
network: "remote".to_string(),
network_gateway: Some(xds::istio::workload::GatewayAddress {
destination: Some(xds::istio::workload::gateway_address::Destination::Address(
XdsNetworkAddress {
network: "remote".to_string(),
address: vec![10, 22, 1, 1],
},
)),
hbone_mtls_port: 15009,
}),
services: std::collections::HashMap::from([(
"/example.com".to_string(),
PortList {
ports: vec![Port {
service_port: 80,
target_port: 8080,
}],
},
)]),
..Default::default()
});
let healthy_local_workload = XdsAddressType::Workload(XdsWorkload {
uid: "Kubernetes//Pod/default/local-example.com-pod".to_string(),
addresses: vec![Bytes::copy_from_slice(&[127, 0, 0, 2])],
network: "".to_string(),
tunnel_protocol: xds::istio::workload::TunnelProtocol::Hbone.into(),
services: std::collections::HashMap::from([(
"/example.com".to_string(),
PortList {
ports: vec![Port {
service_port: 80,
target_port: 8080,
}],
},
)]),
status: xds::istio::workload::WorkloadStatus::Healthy.into(),
..Default::default()
});
let unhealthy_local_workload = XdsAddressType::Workload(XdsWorkload {
uid: "Kubernetes//Pod/default/local-example.com-pod".to_string(),
addresses: vec![Bytes::copy_from_slice(&[127, 0, 0, 2])],
network: "".to_string(),
tunnel_protocol: xds::istio::workload::TunnelProtocol::Hbone.into(),
services: std::collections::HashMap::from([(
"/example.com".to_string(),
PortList {
ports: vec![Port {
service_port: 80,
target_port: 8080,
}],
},
)]),
status: xds::istio::workload::WorkloadStatus::Unhealthy.into(),
..Default::default()
});
run_build_request_multi(
"127.0.0.1",
"127.0.0.3:80",
vec![
service.clone(),
ew_gateway.clone(),
remote_workload.clone(),
healthy_local_workload.clone(),
],
Some(ExpectedRequest {
protocol: OutboundProtocol::HBONE,
hbone_destination: "127.0.0.2:8080",
destination: "127.0.0.2:15008",
}),
)
.await;
run_build_request_multi(
"127.0.0.1",
"127.0.0.3:80",
vec![
service.clone(),
ew_gateway.clone(),
remote_workload.clone(),
unhealthy_local_workload.clone(),
],
Some(ExpectedRequest {
protocol: OutboundProtocol::DOUBLEHBONE,
hbone_destination: "example.com:80",
destination: "10.22.1.1:15009",
}),
)
.await;
} }
#[tokio::test] #[tokio::test]

View File

@ -594,9 +594,10 @@ mod test {
} }
/// This is really a test for TokioH2Stream, but its nicer here because we have access to /// This is really a test for TokioH2Stream, but its nicer here because we have access to
/// streams /// streams.
/// Most important, we make sure there are no panics.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn small_reads() { async fn read_buffering() {
let (mut pool, srv) = setup_test(3).await; let (mut pool, srv) = setup_test(3).await;
let key = key(&srv, 2); let key = key(&srv, 2);
@ -612,13 +613,28 @@ mod test {
let c = pool.send_request_pooled(&key.clone(), req()).await.unwrap(); let c = pool.send_request_pooled(&key.clone(), req()).await.unwrap();
let mut c = TokioH2Stream::new(c); let mut c = TokioH2Stream::new(c);
c.write_all(b"abcde").await.unwrap(); c.write_all(b"abcde").await.unwrap();
let mut b = [0u8; 0]; let mut b = [0u8; 100];
// Crucially, this should error rather than panic. // Properly buffer reads and don't error
if let Err(e) = c.read(&mut b).await { assert_eq!(c.read(&mut b).await.unwrap(), 8);
assert_eq!(e.kind(), io::ErrorKind::Other); assert_eq!(&b[..8], b"poolsrv\n"); // this is added by itself
} else { assert_eq!(c.read(&mut b[..1]).await.unwrap(), 1);
panic!("Should have errored"); assert_eq!(&b[..1], b"a");
} assert_eq!(c.read(&mut b[..1]).await.unwrap(), 1);
assert_eq!(&b[..1], b"b");
assert_eq!(c.read(&mut b[..1]).await.unwrap(), 1);
assert_eq!(&b[..1], b"c");
assert_eq!(c.read(&mut b).await.unwrap(), 2); // there are only two bytes left
assert_eq!(&b[..2], b"de");
// Once we drop the pool, we should still retained the buffered data,
// but then we should error.
c.write_all(b"abcde").await.unwrap();
assert_eq!(c.read(&mut b[..3]).await.unwrap(), 3);
assert_eq!(&b[..3], b"abc");
drop(pool);
assert_eq!(c.read(&mut b[..2]).await.unwrap(), 2);
assert_eq!(&b[..2], b"de");
assert!(c.read(&mut b).await.is_err());
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@ -203,8 +203,7 @@ async fn negotiate_socks_connection(
if version != 0x05 { if version != 0x05 {
return Err(SocksError::invalid_protocol(format!( return Err(SocksError::invalid_protocol(format!(
"unsupported version {}", "unsupported version {version}",
version
))); )));
} }

View File

@ -22,10 +22,9 @@ use crate::dns;
use crate::drain::DrainWatcher; use crate::drain::DrainWatcher;
use crate::proxy::connection_manager::ConnectionManager; use crate::proxy::connection_manager::ConnectionManager;
use crate::proxy::{DefaultSocketFactory, Proxy, inbound::Inbound};
use crate::proxy::{Error, LocalWorkloadInformation, Metrics}; use crate::proxy::{Error, LocalWorkloadInformation, Metrics};
use crate::proxy::Proxy;
// Proxy factory creates ztunnel proxies using a socket factory. // Proxy factory creates ztunnel proxies using a socket factory.
// this allows us to create our proxies the same way in regular mode and in inpod mode. // this allows us to create our proxies the same way in regular mode and in inpod mode.
pub struct ProxyFactory { pub struct ProxyFactory {
@ -113,6 +112,8 @@ impl ProxyFactory {
drain.clone(), drain.clone(),
socket_factory.as_ref(), socket_factory.as_ref(),
local_workload_information.as_fetcher(), local_workload_information.as_fetcher(),
self.config.prefered_service_namespace.clone(),
self.config.ipv6_enabled,
) )
.await?; .await?;
resolver = Some(server.resolver()); resolver = Some(server.resolver());
@ -130,6 +131,7 @@ impl ProxyFactory {
socket_factory.clone(), socket_factory.clone(),
resolver, resolver,
local_workload_information, local_workload_information,
false,
); );
result.connection_manager = Some(cm); result.connection_manager = Some(cm);
result.proxy = Some(Proxy::from_inputs(pi, drain).await?); result.proxy = Some(Proxy::from_inputs(pi, drain).await?);
@ -137,6 +139,52 @@ impl ProxyFactory {
Ok(result) Ok(result)
} }
/// Creates an inbound listener specifically for ztunnel's own internal endpoints (metrics).
/// This allows ztunnel to act as its own workload, enforcing policies on traffic directed to itself.
/// This is distinct from the main inbound listener which handles traffic for other workloads proxied by ztunnel.
pub async fn create_ztunnel_self_proxy_listener(
&self,
) -> Result<Option<crate::proxy::inbound::Inbound>, Error> {
if self.config.proxy_mode != config::ProxyMode::Shared {
return Ok(None);
}
if let (Some(ztunnel_identity), Some(ztunnel_workload)) =
(&self.config.ztunnel_identity, &self.config.ztunnel_workload)
{
tracing::info!(
"creating ztunnel self-proxy listener with identity: {:?}",
ztunnel_identity
);
let local_workload_information = Arc::new(LocalWorkloadInformation::new(
Arc::new(ztunnel_workload.clone()),
self.state.clone(),
self.cert_manager.clone(),
));
let socket_factory = Arc::new(DefaultSocketFactory(self.config.socket_config));
let cm = ConnectionManager::default();
let pi = crate::proxy::ProxyInputs::new(
self.config.clone(),
cm.clone(),
self.state.clone(),
self.proxy_metrics.clone(),
socket_factory,
None,
local_workload_information,
true,
);
let inbound = Inbound::new(pi, self.drain.clone()).await?;
Ok(Some(inbound))
} else {
Ok(None)
}
}
} }
#[derive(Default)] #[derive(Default)]

View File

@ -366,6 +366,19 @@ impl ProxyState {
debug!("failed to fetch workload for {}", ep.workload_uid); debug!("failed to fetch workload for {}", ep.workload_uid);
return None; return None;
}; };
let in_network = wl.network == src.network;
let has_network_gateway = wl.network_gateway.is_some();
let has_address = !wl.workload_ips.is_empty() || !wl.hostname.is_empty();
if !has_address {
// Workload has no IP. We can only reach it via a network gateway
// WDS is client-agnostic, so we will get a network gateway for a workload
// even if it's in the same network; we should never use it.
if in_network || !has_network_gateway {
return None;
}
}
match resolution_mode { match resolution_mode {
ServiceResolutionMode::Standard => { ServiceResolutionMode::Standard => {
if target_port.unwrap_or_default() == 0 && !ep.port.contains_key(&svc_port) { if target_port.unwrap_or_default() == 0 && !ep.port.contains_key(&svc_port) {
@ -857,7 +870,7 @@ impl DemandProxyState {
self.finalize_upstream(source_workload, target_address, res) self.finalize_upstream(source_workload, target_address, res)
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
Error::UnknownNetworkGateway(format!("network gateway {:?} not found", gw_address)) Error::UnknownNetworkGateway(format!("network gateway {gw_address:?} not found"))
}) })
} }
@ -913,7 +926,7 @@ impl DemandProxyState {
}; };
self.finalize_upstream(source_workload, target_address, res) self.finalize_upstream(source_workload, target_address, res)
.await? .await?
.ok_or_else(|| Error::UnknownWaypoint(format!("waypoint {:?} not found", gw_address))) .ok_or_else(|| Error::UnknownWaypoint(format!("waypoint {gw_address:?} not found")))
} }
pub async fn fetch_service_waypoint( pub async fn fetch_service_waypoint(
@ -1365,17 +1378,17 @@ mod tests {
fn create_workload(dest_uid: u8) -> Workload { fn create_workload(dest_uid: u8) -> Workload {
Workload { Workload {
name: "test".into(), name: "test".into(),
namespace: format!("ns{}", dest_uid).into(), namespace: format!("ns{dest_uid}").into(),
trust_domain: "cluster.local".into(), trust_domain: "cluster.local".into(),
service_account: "defaultacct".into(), service_account: "defaultacct".into(),
workload_ips: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 0, dest_uid))], workload_ips: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 0, dest_uid))],
uid: format!("{}", dest_uid).into(), uid: format!("{dest_uid}").into(),
..test_helpers::test_default_workload() ..test_helpers::test_default_workload()
} }
} }
fn get_workload(state: &DemandProxyState, dest_uid: u8) -> Arc<Workload> { fn get_workload(state: &DemandProxyState, dest_uid: u8) -> Arc<Workload> {
let key: Strng = format!("{}", dest_uid).into(); let key: Strng = format!("{dest_uid}").into();
state.read().workloads.by_uid[&key].clone() state.read().workloads.by_uid[&key].clone()
} }
@ -1384,7 +1397,7 @@ mod tests {
dest_uid: u8, dest_uid: u8,
src_svc_acct: &str, src_svc_acct: &str,
) -> crate::state::ProxyRbacContext { ) -> crate::state::ProxyRbacContext {
let key: Strng = format!("{}", dest_uid).into(); let key: Strng = format!("{dest_uid}").into();
let workload = &state.read().workloads.by_uid[&key]; let workload = &state.read().workloads.by_uid[&key];
crate::state::ProxyRbacContext { crate::state::ProxyRbacContext {
conn: rbac::Connection { conn: rbac::Connection {
@ -1571,6 +1584,22 @@ mod tests {
}, },
..test_helpers::test_default_workload() ..test_helpers::test_default_workload()
}; };
let wl_empty_ip = Workload {
uid: "cluster1//v1/Pod/default/wl_empty_ip".into(),
name: "wl_empty_ip".into(),
namespace: "default".into(),
trust_domain: "cluster.local".into(),
service_account: "default".into(),
workload_ips: vec![], // none!
network: "network".into(),
locality: Locality {
region: "reg".into(),
zone: "zone".into(),
subzone: "".into(),
},
..test_helpers::test_default_workload()
};
let _ep_almost = Workload { let _ep_almost = Workload {
uid: "cluster1//v1/Pod/default/ep_almost".into(), uid: "cluster1//v1/Pod/default/ep_almost".into(),
name: "wl_almost".into(), name: "wl_almost".into(),
@ -1617,6 +1646,11 @@ mod tests {
port: HashMap::from([(80u16, 80u16)]), port: HashMap::from([(80u16, 80u16)]),
status: HealthStatus::Healthy, status: HealthStatus::Healthy,
}, },
Endpoint {
workload_uid: "cluster1//v1/Pod/default/wl_empty_ip".into(),
port: HashMap::from([(80u16, 80u16)]),
status: HealthStatus::Healthy,
},
]); ]);
let strict_svc = Service { let strict_svc = Service {
endpoints: endpoints.clone(), endpoints: endpoints.clone(),
@ -1649,6 +1683,7 @@ mod tests {
state.workloads.insert(Arc::new(wl_no_locality.clone())); state.workloads.insert(Arc::new(wl_no_locality.clone()));
state.workloads.insert(Arc::new(wl_match.clone())); state.workloads.insert(Arc::new(wl_match.clone()));
state.workloads.insert(Arc::new(wl_almost.clone())); state.workloads.insert(Arc::new(wl_almost.clone()));
state.workloads.insert(Arc::new(wl_empty_ip.clone()));
state.services.insert(strict_svc.clone()); state.services.insert(strict_svc.clone());
state.services.insert(failover_svc.clone()); state.services.insert(failover_svc.clone());
@ -1663,6 +1698,15 @@ mod tests {
assert!(want.contains(&got.unwrap()), "{}", desc); assert!(want.contains(&got.unwrap()), "{}", desc);
} }
}; };
let assert_not_endpoint =
|src: &Workload, svc: &Service, uid: &str, tries: usize, desc: &str| {
for _ in 0..tries {
let got = state
.load_balance(src, svc, 80, ServiceResolutionMode::Standard)
.map(|(ep, _)| ep.workload_uid.as_str());
assert!(got != Some(uid), "{}", desc);
}
};
assert_endpoint( assert_endpoint(
&wl_no_locality, &wl_no_locality,
@ -1708,5 +1752,12 @@ mod tests {
vec!["cluster1//v1/Pod/default/wl_match"], vec!["cluster1//v1/Pod/default/wl_match"],
"failover full match selects closest match", "failover full match selects closest match",
); );
assert_not_endpoint(
&wl_no_locality,
&failover_svc,
"cluster1//v1/Pod/default/wl_empty_ip",
10,
"failover no match can select any endpoint",
);
} }
} }

View File

@ -1028,8 +1028,8 @@ mod tests {
}, },
)]); )]);
let uid1 = format!("cluster1//v1/Pod/default/my-pod/{:?}", ip1); let uid1 = format!("cluster1//v1/Pod/default/my-pod/{ip1:?}");
let uid2 = format!("cluster1//v1/Pod/default/my-pod/{:?}", ip2); let uid2 = format!("cluster1//v1/Pod/default/my-pod/{ip2:?}");
updater updater
.insert_workload( .insert_workload(
@ -1734,7 +1734,7 @@ mod tests {
let xds_ip1 = Bytes::copy_from_slice(&[127, 0, 0, 1]); let xds_ip1 = Bytes::copy_from_slice(&[127, 0, 0, 1]);
let ip1 = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let ip1 = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let uid1 = format!("cluster1//v1/Pod/default/my-pod/{:?}", ip1); let uid1 = format!("cluster1//v1/Pod/default/my-pod/{ip1:?}");
let services = HashMap::from([( let services = HashMap::from([(
"ns/svc1.ns.svc.cluster.local".to_string(), "ns/svc1.ns.svc.cluster.local".to_string(),

View File

@ -170,7 +170,7 @@ impl Visitor<'_> {
} else { } else {
" " " "
}; };
write!(self.writer, "{}{:?}", padding, value) write!(self.writer, "{padding}{value:?}")
} }
} }
@ -188,9 +188,9 @@ impl field::Visit for Visitor<'_> {
// Skip fields that are actually log metadata that have already been handled // Skip fields that are actually log metadata that have already been handled
name if name.starts_with("log.") => Ok(()), name if name.starts_with("log.") => Ok(()),
// For the message, write out the message and a tab to separate the future fields // For the message, write out the message and a tab to separate the future fields
"message" => write!(self.writer, "{:?}\t", val), "message" => write!(self.writer, "{val:?}\t"),
// For the rest, k=v. // For the rest, k=v.
_ => self.write_padded(&format_args!("{}={:?}", field.name(), val)), _ => self.write_padded(&format_args!("{}={val:?}", field.name())),
} }
} }
} }
@ -234,7 +234,7 @@ where
let target = meta.target(); let target = meta.target();
// No need to prefix everything // No need to prefix everything
let target = target.strip_prefix("ztunnel::").unwrap_or(target); let target = target.strip_prefix("ztunnel::").unwrap_or(target);
write!(writer, "{}", target)?; write!(writer, "{target}")?;
// Write out span fields. Istio logging outside of Rust doesn't really have this concept // Write out span fields. Istio logging outside of Rust doesn't really have this concept
if let Some(scope) = ctx.event_scope() { if let Some(scope) = ctx.event_scope() {
@ -243,7 +243,7 @@ where
let ext = span.extensions(); let ext = span.extensions();
if let Some(fields) = &ext.get::<FormattedFields<N>>() { if let Some(fields) = &ext.get::<FormattedFields<N>>() {
if !fields.is_empty() { if !fields.is_empty() {
write!(writer, "{{{}}}", fields)?; write!(writer, "{{{fields}}}")?;
} }
} }
} }
@ -285,7 +285,7 @@ impl<S: SerializeMap> Visit for JsonVisitory<S> {
if self.state.is_ok() { if self.state.is_ok() {
self.state = self self.state = self
.serializer .serializer
.serialize_entry(field.name(), &format_args!("{:?}", value)) .serialize_entry(field.name(), &format_args!("{value:?}"))
} }
} }
@ -326,9 +326,7 @@ impl io::Write for WriteAdaptor<'_> {
let s = let s =
std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
self.fmt_write self.fmt_write.write_str(s).map_err(io::Error::other)?;
.write_str(s)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok(s.len()) Ok(s.len())
} }
@ -507,7 +505,7 @@ pub mod testing {
.map(|h| { .map(|h| {
h.iter() h.iter()
.sorted_by_key(|(k, _)| *k) .sorted_by_key(|(k, _)| *k)
.map(|(k, err)| format!("{}:{}", k, err)) .map(|(k, err)| format!("{k}:{err}"))
.join("\n") .join("\n")
}) })
.join("\n\n"); .join("\n\n");

View File

@ -169,10 +169,9 @@ pub fn localhost_error_message() -> String {
TEST_VIP, TEST_VIP,
]; ];
format!( format!(
"These tests use the following loopback addresses: {:?}. \ "These tests use the following loopback addresses: {addrs:?}. \
Your OS may require an explicit alias for each. If so, you'll need to manually \ Your OS may require an explicit alias for each. If so, you'll need to manually \
configure your system for each IP (e.g. `sudo ifconfig lo0 alias 127.0.0.2 up`).", configure your system for each IP (e.g. `sudo ifconfig lo0 alias 127.0.0.2 up`).",
addrs
) )
} }
@ -239,7 +238,7 @@ fn test_custom_workload(
hostname_only: bool, hostname_only: bool,
) -> anyhow::Result<LocalWorkload> { ) -> anyhow::Result<LocalWorkload> {
let host = match hostname_only { let host = match hostname_only {
true => format!("{}.reflect.internal.", ip_str), true => format!("{ip_str}.reflect.internal."),
false => "".to_string(), false => "".to_string(),
}; };
let wips = match hostname_only { let wips = match hostname_only {
@ -250,7 +249,7 @@ fn test_custom_workload(
workload_ips: wips, workload_ips: wips,
hostname: host.into(), hostname: host.into(),
protocol, protocol,
uid: format!("cluster1//v1/Pod/default/{}", name).into(), uid: format!("cluster1//v1/Pod/default/{name}").into(),
name: name.into(), name: name.into(),
namespace: "default".into(), namespace: "default".into(),
service_account: "default".into(), service_account: "default".into(),
@ -282,7 +281,7 @@ fn test_custom_svc(
}], }],
ports: HashMap::from([(80u16, echo_port)]), ports: HashMap::from([(80u16, echo_port)]),
endpoints: EndpointSet::from_list([Endpoint { endpoints: EndpointSet::from_list([Endpoint {
workload_uid: format!("cluster1//v1/Pod/default/{}", workload_name).into(), workload_uid: format!("cluster1//v1/Pod/default/{workload_name}").into(),
port: HashMap::from([(80u16, echo_port)]), port: HashMap::from([(80u16, echo_port)]),
status: HealthStatus::Healthy, status: HealthStatus::Healthy,
}]), }]),

View File

@ -52,6 +52,7 @@ pub struct TestApp {
pub namespace: Option<super::netns::Namespace>, pub namespace: Option<super::netns::Namespace>,
pub shutdown: ShutdownTrigger, pub shutdown: ShutdownTrigger,
pub ztunnel_identity: Option<identity::Identity>,
} }
impl From<(&Bound, Arc<SecretManager>)> for TestApp { impl From<(&Bound, Arc<SecretManager>)> for TestApp {
@ -66,6 +67,7 @@ impl From<(&Bound, Arc<SecretManager>)> for TestApp {
cert_manager, cert_manager,
namespace: None, namespace: None,
shutdown: app.shutdown.trigger(), shutdown: app.shutdown.trigger(),
ztunnel_identity: None,
} }
} }
} }
@ -103,7 +105,7 @@ impl TestApp {
let get_resp = move || async move { let get_resp = move || async move {
let req = Request::builder() let req = Request::builder()
.method(Method::GET) .method(Method::GET)
.uri(format!("http://localhost:{}/{path}", port)) .uri(format!("http://localhost:{port}/{path}"))
.header("content-type", "application/json") .header("content-type", "application/json")
.body(Empty::<Bytes>::new()) .body(Empty::<Bytes>::new())
.unwrap(); .unwrap();
@ -128,7 +130,7 @@ impl TestApp {
let get_resp = move || async move { let get_resp = move || async move {
let req = Request::builder() let req = Request::builder()
.method(Method::GET) .method(Method::GET)
.uri(format!("http://localhost:{}/{path}", port)) .uri(format!("http://localhost:{port}/{path}"))
.header("content-type", "application/json") .header("content-type", "application/json")
.body(Empty::<Bytes>::new()) .body(Empty::<Bytes>::new())
.unwrap(); .unwrap();

View File

@ -298,6 +298,8 @@ pub async fn run_dns(responses: HashMap<Name, Vec<IpAddr>>) -> anyhow::Result<Te
}), }),
state.clone(), state.clone(),
), ),
Some("prefered-namespace".to_string()),
true, // ipv6_enabled for tests
) )
.await?; .await?;

View File

@ -124,38 +124,104 @@ impl WorkloadManager {
wli: Option<state::WorkloadInfo>, wli: Option<state::WorkloadInfo>,
) -> anyhow::Result<TestApp> { ) -> anyhow::Result<TestApp> {
let mut inpod_uds: PathBuf = "/dev/null".into(); let mut inpod_uds: PathBuf = "/dev/null".into();
let ztunnel_server = if self.mode == Shared { let current_mode = self.mode;
inpod_uds = self.tmp_dir.join(node); let proxy_mode = match current_mode {
Some(start_ztunnel_server(inpod_uds.clone()).await) Shared => ProxyMode::Shared,
Dedicated => ProxyMode::Dedicated,
};
let ztunnel_name = format!("ztunnel-{node}");
// Define ztunnel's own identity and workload info if it's a Shared proxy.
// These are used for registering ztunnel as a workload and for cfg.ztunnel_identity/workload.
let ztunnel_shared_identity: Option<identity::Identity> = if proxy_mode == ProxyMode::Shared
{
Some(identity::Identity::Spiffe {
trust_domain: "cluster.local".into(),
namespace: "default".into(),
service_account: ztunnel_name.clone().into(),
})
} else { } else {
None None
}; };
let ns = TestWorkloadBuilder::new(&format!("ztunnel-{node}"), self)
.on_node(node) let ztunnel_shared_workload_info: Option<state::WorkloadInfo> =
.uncaptured() if proxy_mode == ProxyMode::Shared {
.register() Some(state::WorkloadInfo::new(
.await?; ztunnel_name.clone(),
"default".to_string(),
ztunnel_name.clone(),
))
} else {
None
};
let ztunnel_server = match current_mode {
Shared => {
inpod_uds = self.tmp_dir.join(node);
Some(start_ztunnel_server(inpod_uds.clone()).await)
}
Dedicated => None,
};
let ns = match current_mode {
Shared => {
// Shared mode: Ztunnel has its own identity, registered as HBONE
TestWorkloadBuilder::new(&ztunnel_name, self)
.on_node(node)
.identity(
ztunnel_shared_identity
.clone()
.expect("Shared mode must have an identity for ztunnel registration"),
)
.hbone() // Shared ztunnel uses HBONE protocol
.register()
.await?
}
Dedicated => {
TestWorkloadBuilder::new(&ztunnel_name, self)
.on_node(node)
.uncaptured() // Dedicated ztunnel is treated as uncaptured TCP
.register()
.await?
}
};
let _ztunnel_local_workload = self
.workloads
.last()
.cloned()
.expect("ztunnel workload should be registered");
let ip = ns.ip(); let ip = ns.ip();
let initial_config = LocalConfig { let initial_config = LocalConfig {
workloads: self.workloads.clone(), workloads: self.workloads.clone(),
policies: self.policies.clone(), policies: self.policies.clone(),
services: self.services.values().cloned().collect_vec(), services: self.services.values().cloned().collect_vec(),
}; };
let proxy_mode = if ztunnel_server.is_some() {
ProxyMode::Shared
} else {
ProxyMode::Dedicated
};
let (mut tx_cfg, rx_cfg) = mpsc_ack(1); let (mut tx_cfg, rx_cfg) = mpsc_ack(1);
tx_cfg.send(initial_config).await?; tx_cfg.send(initial_config).await?;
let local_xds_config = Some(ConfigSource::Dynamic(Arc::new(Mutex::new(rx_cfg)))); let local_xds_config = Some(ConfigSource::Dynamic(Arc::new(Mutex::new(rx_cfg))));
// Config for ztunnel's own identity and workload, primarily for when it acts as a server (metrics endpoint).
let cfg_ztunnel_identity = ztunnel_shared_identity.clone();
let cfg_ztunnel_workload_info = ztunnel_shared_workload_info.clone();
// Config for the workload this ztunnel instance is proxying for :
// If Shared, ztunnel is effectively proxying for itself
// If Dedicated, it's for the application workload `wli`
let cfg_proxy_workload_information = match proxy_mode {
// Ztunnel's own info for shared mode proxy
ProxyMode::Shared => ztunnel_shared_workload_info.clone(),
// Application's workload info for dedicated mode
ProxyMode::Dedicated => wli,
};
let cfg = config::Config { let cfg = config::Config {
xds_address: None, xds_address: None,
dns_proxy: true, dns_proxy: true,
fake_ca: true, fake_ca: true,
local_xds_config, local_xds_config,
local_node: Some(node.to_string()), local_node: Some(node.to_string()),
proxy_workload_information: wli, proxy_workload_information: cfg_proxy_workload_information,
inpod_uds, inpod_uds,
proxy_mode, proxy_mode,
// We use packet mark even in dedicated to distinguish proxy from application // We use packet mark even in dedicated to distinguish proxy from application
@ -166,12 +232,16 @@ impl WorkloadManager {
Some(true) Some(true)
}, },
localhost_app_tunnel: true, localhost_app_tunnel: true,
ztunnel_identity: cfg_ztunnel_identity,
ztunnel_workload: cfg_ztunnel_workload_info,
..config::parse_config().unwrap() ..config::parse_config().unwrap()
}; };
let (tx, rx) = std::sync::mpsc::sync_channel(0); let (tx, rx) = std::sync::mpsc::sync_channel(0);
// Setup the ztunnel... // Setup the ztunnel...
let cloned_ns = ns.clone(); let cloned_ns = ns.clone();
let cloned_ns2 = ns.clone(); let cloned_ns2 = ns.clone();
let ztunnel_identity = ztunnel_shared_identity.clone();
// run_ready will spawn a thread and block on it. Run with spawn_blocking so it doesn't block the runtime. // run_ready will spawn a thread and block on it. Run with spawn_blocking so it doesn't block the runtime.
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
ns.run_ready(move |ready| async move { ns.run_ready(move |ready| async move {
@ -210,9 +280,9 @@ impl WorkloadManager {
ip, ip,
)), )),
cert_manager, cert_manager,
namespace: Some(cloned_ns), namespace: Some(cloned_ns),
shutdown, shutdown,
ztunnel_identity: ztunnel_identity.clone(),
}; };
ta.ready().await; ta.ready().await;
info!("ready"); info!("ready");
@ -512,16 +582,18 @@ impl<'a> TestWorkloadBuilder<'a> {
pub async fn register(mut self) -> anyhow::Result<Namespace> { pub async fn register(mut self) -> anyhow::Result<Namespace> {
let zt = self.manager.ztunnels.get(self.w.workload.node.as_str()); let zt = self.manager.ztunnels.get(self.w.workload.node.as_str());
let node = self.w.workload.node.clone(); let node = self.w.workload.node.clone();
let network_namespace = if self.manager.mode == Dedicated && zt.is_some() { let network_namespace = match (self.manager.mode, zt.is_some()) {
// This is a bit of hack. For dedicated mode, we run the app and ztunnel in the same namespace (Dedicated, true) => {
// We probably should express this more natively in the framework, but for now we just detect it // This is a bit of hack. For dedicated mode, we run the app and ztunnel in the same namespace
// and re-use the namespace. // We probably should express this more natively in the framework, but for now we just detect it
tracing::info!("node already has ztunnel and dedicate mode, sharing"); // and re-use the namespace.
zt.as_ref().unwrap().namespace.clone() tracing::info!("node already has ztunnel and dedicate mode, sharing");
} else { zt.as_ref().unwrap().namespace.clone()
self.manager }
_ => self
.manager
.namespaces .namespaces
.child(&self.w.workload.node, &self.w.workload.name)? .child(&self.w.workload.node, &self.w.workload.name)?,
}; };
if self.w.workload.network_gateway.is_some() { if self.w.workload.network_gateway.is_some() {
// This is a little inefficient, because we create the // This is a little inefficient, because we create the
@ -569,7 +641,7 @@ impl<'a> TestWorkloadBuilder<'a> {
let fd = network_namespace.netns().file().as_raw_fd(); let fd = network_namespace.netns().file().as_raw_fd();
let msg = inpod::Message::Start(inpod::StartZtunnelMessage { let msg = inpod::Message::Start(inpod::StartZtunnelMessage {
uid: uid.to_string(), uid: uid.to_string(),
workload_info: Some(wli), workload_info: Some(wli.clone()),
fd, fd,
}); });
zt_info zt_info

View File

@ -428,7 +428,6 @@ mod test {
SystemTime::now() + Duration::from_secs(60), SystemTime::now() + Duration::from_secs(60),
None, None,
TEST_ROOT_KEY, TEST_ROOT_KEY,
TEST_ROOT,
); );
let cert1 = let cert1 =
WorkloadCertificate::new(key.as_bytes(), cert.as_bytes(), vec![&joined]).unwrap(); WorkloadCertificate::new(key.as_bytes(), cert.as_bytes(), vec![&joined]).unwrap();
@ -440,7 +439,6 @@ mod test {
SystemTime::now() + Duration::from_secs(60), SystemTime::now() + Duration::from_secs(60),
None, None,
TEST_ROOT2_KEY, TEST_ROOT2_KEY,
TEST_ROOT2,
); );
let cert2 = let cert2 =
WorkloadCertificate::new(key.as_bytes(), cert.as_bytes(), vec![&joined]).unwrap(); WorkloadCertificate::new(key.as_bytes(), cert.as_bytes(), vec![&joined]).unwrap();

View File

@ -124,28 +124,44 @@ impl CsrOptions {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tls; use crate::tls;
use itertools::Itertools;
#[test] #[test]
fn test_csr() { fn test_csr() {
use x509_parser::prelude::FromDer; use x509_parser::prelude::*;
let csr = tls::csr::CsrOptions { let csr = tls::csr::CsrOptions {
san: "spiffe://td/ns/ns1/sa/sa1".to_string(), san: "spiffe://td/ns/ns1/sa/sa1".to_string(),
} }
.generate() .generate()
.unwrap(); .unwrap();
let (_, der) = x509_parser::pem::parse_x509_pem(csr.csr.as_bytes()).unwrap(); let (_, der) = x509_parser::pem::parse_x509_pem(csr.csr.as_bytes()).unwrap();
let (_, cert) = let (_, cert) =
x509_parser::certification_request::X509CertificationRequest::from_der(&der.contents) x509_parser::certification_request::X509CertificationRequest::from_der(&der.contents)
.unwrap(); .unwrap();
cert.verify_signature().unwrap(); cert.verify_signature().unwrap();
let subject = cert.certification_request_info.subject.iter().collect_vec();
assert_eq!(subject.len(), 0);
let attr = cert let attr = cert
.certification_request_info .certification_request_info
.iter_attributes() .iter_attributes()
.next() .next()
.unwrap(); .unwrap();
// SAN is encoded in some format I don't understand how to parse; this could be improved.
// but make sure it's there in a hacky manner let ParsedCriAttribute::ExtensionRequest(parsed) = attr.parsed_attribute() else {
assert!(attr.value.ends_with(b"spiffe://td/ns/ns1/sa/sa1")); panic!("not a ExtensionRequest")
};
let ext = parsed.clone().extensions;
assert_eq!(ext.len(), 1);
let ext = ext.into_iter().next().unwrap();
assert!(ext.critical);
let ParsedExtension::SubjectAlternativeName(san) = ext.parsed_extension() else {
panic!("not a SubjectAlternativeName")
};
assert_eq!(
&format!("{san:?}"),
"SubjectAlternativeName { general_names: [URI(\"spiffe://td/ns/ns1/sa/sa1\")] }"
)
} }
} }

View File

@ -14,6 +14,8 @@
use super::Error; use super::Error;
#[allow(unused_imports)]
use crate::PQC_ENABLED;
use crate::identity::{self, Identity}; use crate::identity::{self, Identity};
use std::fmt::Debug; use std::fmt::Debug;
@ -40,6 +42,15 @@ pub trait ServerCertProvider: Send + Sync + Clone {
pub(super) static TLS_VERSIONS: &[&rustls::SupportedProtocolVersion] = &[&rustls::version::TLS13]; pub(super) static TLS_VERSIONS: &[&rustls::SupportedProtocolVersion] = &[&rustls::version::TLS13];
#[cfg(feature = "tls-aws-lc")]
pub static CRYPTO_PROVIDER: &str = "tls-aws-lc";
#[cfg(feature = "tls-ring")]
pub static CRYPTO_PROVIDER: &str = "tls-ring";
#[cfg(feature = "tls-boring")]
pub static CRYPTO_PROVIDER: &str = "tls-boring";
#[cfg(feature = "tls-openssl")]
pub static CRYPTO_PROVIDER: &str = "tls-openssl";
// Ztunnel use `rustls` with pluggable crypto modules. // Ztunnel use `rustls` with pluggable crypto modules.
// All crypto MUST be done via the below providers. // All crypto MUST be done via the below providers.
// //
@ -68,14 +79,20 @@ pub(super) fn provider() -> Arc<CryptoProvider> {
#[cfg(feature = "tls-aws-lc")] #[cfg(feature = "tls-aws-lc")]
pub(super) fn provider() -> Arc<CryptoProvider> { pub(super) fn provider() -> Arc<CryptoProvider> {
Arc::new(CryptoProvider { let mut provider = CryptoProvider {
// Limit to only the subset of ciphers that are FIPS compatible // Limit to only the subset of ciphers that are FIPS compatible
cipher_suites: vec![ cipher_suites: vec![
rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_256_GCM_SHA384, rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_256_GCM_SHA384,
rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_128_GCM_SHA256, rustls::crypto::aws_lc_rs::cipher_suite::TLS13_AES_128_GCM_SHA256,
], ],
..rustls::crypto::aws_lc_rs::default_provider() ..rustls::crypto::aws_lc_rs::default_provider()
}) };
if *PQC_ENABLED {
provider.kx_groups = vec![rustls::crypto::aws_lc_rs::kx_group::X25519MLKEM768]
}
Arc::new(provider)
} }
#[cfg(feature = "tls-openssl")] #[cfg(feature = "tls-openssl")]

View File

@ -18,7 +18,6 @@ use std::fmt::{Display, Formatter};
use rand::RngCore; use rand::RngCore;
use rand::SeedableRng; use rand::SeedableRng;
use rand::rngs::SmallRng; use rand::rngs::SmallRng;
use rcgen::{Certificate, CertificateParams, KeyPair};
use std::net::IpAddr; use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
@ -105,8 +104,7 @@ pub fn generate_test_certs_at(
not_after: SystemTime, not_after: SystemTime,
rng: Option<&mut dyn rand::RngCore>, rng: Option<&mut dyn rand::RngCore>,
) -> WorkloadCertificate { ) -> WorkloadCertificate {
let (key, cert) = let (key, cert) = generate_test_certs_with_root(id, not_before, not_after, rng, TEST_ROOT_KEY);
generate_test_certs_with_root(id, not_before, not_after, rng, TEST_ROOT_KEY, TEST_ROOT);
let mut workload = let mut workload =
WorkloadCertificate::new(key.as_bytes(), cert.as_bytes(), vec![TEST_ROOT]).unwrap(); WorkloadCertificate::new(key.as_bytes(), cert.as_bytes(), vec![TEST_ROOT]).unwrap();
// Certificates do not allow sub-millisecond, but we need this for tests. // Certificates do not allow sub-millisecond, but we need this for tests.
@ -121,7 +119,6 @@ pub fn generate_test_certs_with_root(
not_after: SystemTime, not_after: SystemTime,
rng: Option<&mut dyn rand::RngCore>, rng: Option<&mut dyn rand::RngCore>,
ca_key: &[u8], ca_key: &[u8],
ca_cert: &[u8],
) -> (String, String) { ) -> (String, String) {
use rcgen::*; use rcgen::*;
let serial_number = { let serial_number = {
@ -150,15 +147,17 @@ pub fn generate_test_certs_with_root(
ExtendedKeyUsagePurpose::ClientAuth, ExtendedKeyUsagePurpose::ClientAuth,
]; ];
p.subject_alt_names = vec![match id { p.subject_alt_names = vec![match id {
TestIdentity::Identity(i) => SanType::URI(Ia5String::try_from(i.to_string()).unwrap()), TestIdentity::Identity(i) => {
SanType::URI(string::Ia5String::try_from(i.to_string()).unwrap())
}
TestIdentity::Ip(i) => SanType::IpAddress(*i), TestIdentity::Ip(i) => SanType::IpAddress(*i),
}]; }];
let kp = KeyPair::from_pem(std::str::from_utf8(TEST_PKEY).unwrap()).unwrap(); let kp = KeyPair::from_pem(std::str::from_utf8(TEST_PKEY).unwrap()).unwrap();
let ca_kp = KeyPair::from_pem(std::str::from_utf8(ca_key).unwrap()).unwrap(); let ca_kp = KeyPair::from_pem(std::str::from_utf8(ca_key).unwrap()).unwrap();
let key = kp.serialize_pem(); let key = kp.serialize_pem();
let ca = test_ca(ca_key, ca_cert); let issuer = Issuer::from_params(&p, &ca_kp);
let cert = p.signed_by(&kp, &ca, &ca_kp).unwrap(); let cert = p.signed_by(&kp, &issuer).unwrap();
let cert = cert.pem(); let cert = cert.pem();
(key, cert) (key, cert)
} }
@ -172,12 +171,6 @@ pub fn generate_test_certs(
generate_test_certs_at(id, not_before, not_before + duration_until_expiry, None) generate_test_certs_at(id, not_before, not_before + duration_until_expiry, None)
} }
fn test_ca(key: &[u8], cert: &[u8]) -> Certificate {
let key = KeyPair::from_pem(std::str::from_utf8(key).unwrap()).unwrap();
let ca_param = CertificateParams::from_ca_cert_pem(std::str::from_utf8(cert).unwrap()).unwrap();
ca_param.self_signed(&key).unwrap()
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MockServerCertProvider(Arc<WorkloadCertificate>); pub struct MockServerCertProvider(Arc<WorkloadCertificate>);

View File

@ -17,10 +17,11 @@ use std::fmt;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::string::String; use std::string::String;
use crate::tls::CRYPTO_PROVIDER;
const BUILD_VERSION: &str = env!("ZTUNNEL_BUILD_buildVersion"); const BUILD_VERSION: &str = env!("ZTUNNEL_BUILD_buildVersion");
const BUILD_GIT_REVISION: &str = env!("ZTUNNEL_BUILD_buildGitRevision"); const BUILD_GIT_REVISION: &str = env!("ZTUNNEL_BUILD_buildGitRevision");
const BUILD_STATUS: &str = env!("ZTUNNEL_BUILD_buildStatus"); const BUILD_STATUS: &str = env!("ZTUNNEL_BUILD_buildStatus");
const BUILD_TAG: &str = env!("ZTUNNEL_BUILD_buildTag");
const BUILD_RUST_VERSION: &str = env!("ZTUNNEL_BUILD_RUSTC_VERSION"); const BUILD_RUST_VERSION: &str = env!("ZTUNNEL_BUILD_RUSTC_VERSION");
const BUILD_RUST_PROFILE: &str = env!("ZTUNNEL_BUILD_PROFILE_NAME"); const BUILD_RUST_PROFILE: &str = env!("ZTUNNEL_BUILD_PROFILE_NAME");
@ -32,8 +33,8 @@ pub struct BuildInfo {
rust_version: String, rust_version: String,
build_profile: String, build_profile: String,
build_status: String, build_status: String,
git_tag: String,
pub istio_version: String, pub istio_version: String,
crypto_provider: String,
} }
impl BuildInfo { impl BuildInfo {
@ -44,8 +45,9 @@ impl BuildInfo {
rust_version: BUILD_RUST_VERSION.to_string(), rust_version: BUILD_RUST_VERSION.to_string(),
build_profile: BUILD_RUST_PROFILE.to_string(), build_profile: BUILD_RUST_PROFILE.to_string(),
build_status: BUILD_STATUS.to_string(), build_status: BUILD_STATUS.to_string(),
git_tag: BUILD_TAG.to_string(), istio_version: env::var("ISTIO_META_ISTIO_VERSION")
istio_version: env::var("ISTIO_VERSION").unwrap_or_else(|_| "unknown".to_string()), .unwrap_or_else(|_| "unknown".to_string()),
crypto_provider: CRYPTO_PROVIDER.to_string(),
} }
} }
} }
@ -54,14 +56,14 @@ impl Display for BuildInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!( write!(
f, f,
"version.BuildInfo{{Version:\"{}\", GitRevision:\"{}\", RustVersion:\"{}\", BuildProfile:\"{}\", BuildStatus:\"{}\", GitTag:\"{}\", IstioVersion:\"{}\"}}", "version.BuildInfo{{Version:\"{}\", GitRevision:\"{}\", RustVersion:\"{}\", BuildProfile:\"{}\", BuildStatus:\"{}\", IstioVersion:\"{}\", CryptoProvider:\"{}\"}}",
self.version, self.version,
self.git_revision, self.git_revision,
self.rust_version, self.rust_version,
self.build_profile, self.build_profile,
self.build_status, self.build_status,
self.git_tag, self.istio_version,
self.istio_version self.crypto_provider,
) )
} }
} }

View File

@ -876,7 +876,7 @@ mod tests {
fn get_auth(i: usize) -> ProtoResource { fn get_auth(i: usize) -> ProtoResource {
let addr = XdsAuthorization { let addr = XdsAuthorization {
name: format!("foo{}", i), name: format!("foo{i}"),
namespace: "default".to_string(), namespace: "default".to_string(),
scope: crate::xds::istio::security::Scope::Global as i32, scope: crate::xds::istio::security::Scope::Global as i32,
action: crate::xds::istio::security::Action::Deny as i32, action: crate::xds::istio::security::Action::Deny as i32,
@ -890,7 +890,7 @@ mod tests {
}], }],
}; };
ProtoResource { ProtoResource {
name: format!("foo{}", i), name: format!("foo{i}"),
aliases: vec![], aliases: vec![],
version: "0.0.1".to_string(), version: "0.0.1".to_string(),
resource: Some(Any { resource: Some(Any {
@ -908,8 +908,8 @@ mod tests {
}; };
let addr = XdsAddress { let addr = XdsAddress {
r#type: Some(XdsType::Workload(XdsWorkload { r#type: Some(XdsType::Workload(XdsWorkload {
name: format!("foo{}", i), name: format!("foo{i}"),
uid: format!("default/foo{}", i), uid: format!("default/foo{i}"),
namespace: "default".to_string(), namespace: "default".to_string(),
addresses: vec![octets.into()], addresses: vec![octets.into()],
tunnel_protocol: 0, tunnel_protocol: 0,
@ -924,7 +924,7 @@ mod tests {
}; };
ProtoResource { ProtoResource {
name: format!("foo{}", i), name: format!("foo{i}"),
aliases: vec![], aliases: vec![],
version: "0.0.1".to_string(), version: "0.0.1".to_string(),
resource: Some(Any { resource: Some(Any {

View File

@ -282,8 +282,7 @@ fn on_demand_dns_assertions(metrics: ParsedMetrics) {
}; };
assert!( assert!(
value == expected, value == expected,
"expected metric {metric} to be 1, was {:?}", "expected metric {metric} to be 1, was {value:?}",
value
); );
} }
} }
@ -361,9 +360,9 @@ async fn test_stats_exist() {
{ {
for (name, doc) in metric_info { for (name, doc) in metric_info {
if stable_metrics.contains(&*name) { if stable_metrics.contains(&*name) {
assert!(!doc.contains("unstable"), "{}: {}", name, doc); assert!(!doc.contains("unstable"), "{name}: {doc}");
} else { } else {
assert!(doc.contains("unstable"), "{}: {}", name, doc); assert!(doc.contains("unstable"), "{name}: {doc}");
} }
} }
} }

View File

@ -18,14 +18,16 @@ mod namespaced {
use futures::future::poll_fn; use futures::future::poll_fn;
use http_body_util::Empty; use http_body_util::Empty;
use std::collections::HashMap; use std::collections::HashMap;
use ztunnel::state::workload::ApplicationTunnel;
use ztunnel::state::workload::application_tunnel::Protocol;
use ztunnel::state::workload::gatewayaddress::Destination; use ztunnel::state::workload::gatewayaddress::Destination;
use ztunnel::state::workload::{GatewayAddress, NamespacedHostname}; use ztunnel::state::workload::{GatewayAddress, NamespacedHostname};
use ztunnel::test_helpers::linux::TestMode;
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use anyhow::Context; use anyhow::Context;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle; use std::thread::JoinHandle;
use std::time::Duration; use std::time::Duration;
use ztunnel::rbac::{Authorization, RbacMatch, StringMatch}; use ztunnel::rbac::{Authorization, RbacMatch, StringMatch};
@ -39,18 +41,17 @@ mod namespaced {
use tokio::time::timeout; use tokio::time::timeout;
use tracing::{error, info}; use tracing::{error, info};
use ztunnel::state::workload::{ApplicationTunnel, NetworkAddress}; use ztunnel::state::workload::NetworkAddress;
use ztunnel::test_helpers::app::ParsedMetrics; use ztunnel::test_helpers::app::{ParsedMetrics, TestApp};
use ztunnel::test_helpers::app::TestApp; use ztunnel::test_helpers::linux::TestMode::{Dedicated, Shared};
use ztunnel::test_helpers::linux::WorkloadManager;
use ztunnel::test_helpers::netns::{Namespace, Resolver};
use ztunnel::test_helpers::*;
use ztunnel::{identity, strng, telemetry}; use ztunnel::{identity, strng, telemetry};
use crate::namespaced::WorkloadMode::Captured; use crate::namespaced::WorkloadMode::Captured;
use ztunnel::setup_netns_test; use ztunnel::setup_netns_test;
use ztunnel::test_helpers::linux::TestMode::{Dedicated, Shared};
use ztunnel::test_helpers::linux::WorkloadManager;
use ztunnel::test_helpers::netns::{Namespace, Resolver};
use ztunnel::test_helpers::*;
const WAYPOINT_MESSAGE: &[u8] = b"waypoint\n"; const WAYPOINT_MESSAGE: &[u8] = b"waypoint\n";
@ -926,27 +927,12 @@ mod namespaced {
// Now shutdown the server. In real world, the server app would shutdown, then ztunnel would remove itself. // Now shutdown the server. In real world, the server app would shutdown, then ztunnel would remove itself.
// In this test, we will leave the server app running, but shutdown ztunnel. // In this test, we will leave the server app running, but shutdown ztunnel.
manager.delete_workload("server").await.unwrap(); manager.delete_workload("server").await.unwrap();
// Request should fail now
let tx = Arc::new(Mutex::new(tx));
#[allow(clippy::await_holding_lock)]
assert_eventually(
Duration::from_secs(2),
|| async { tx.lock().unwrap().send_and_wait(()).await.is_err() },
true,
)
.await;
// Close the connection
drop(tx);
// Should fail as the last request fails // In shared mode, verify that new connections succeed but data transfer fails
assert!(cjh.join().unwrap().is_err());
// Now try to connect and make sure it fails
client client
.run_and_wait(move || async move { .run_and_wait(move || async move {
let mut stream = TcpStream::connect(srv).await.unwrap(); let mut stream = TcpStream::connect(srv).await.unwrap();
// We should be able to connect (since client is running), but not send a request // We should be able to connect (since client is running), but not send a request
const BODY: &[u8] = b"hello world"; const BODY: &[u8] = b"hello world";
stream.write_all(BODY).await.unwrap(); stream.write_all(BODY).await.unwrap();
let mut buf = [0; BODY.len() * 2]; let mut buf = [0; BODY.len() * 2];
@ -955,6 +941,16 @@ mod namespaced {
Ok(()) Ok(())
}) })
.unwrap(); .unwrap();
// The long running connection should also fail on next attempt
let tx_send_result = tx.send_and_wait(()).await;
assert!(
tx_send_result.is_err(),
"long running connection should fail after workload deletion"
);
drop(tx);
assert!(cjh.join().unwrap().is_err());
Ok(()) Ok(())
} }
@ -1220,7 +1216,7 @@ mod namespaced {
vec![ vec![
(zt, 15001, Request), // Outbound: should be blocked due to recursive call (zt, 15001, Request), // Outbound: should be blocked due to recursive call
(zt, 15006, Request), // Inbound: should be blocked due to recursive call (zt, 15006, Request), // Inbound: should be blocked due to recursive call
(zt, 15008, Request), // HBONE: expected TLS, reject (zt, 15008, Request), // HBONE: Connection succeeds (ztunnel listens) but request fails due to TLS
// Localhost still get connection established, as ztunnel accepts anything. But they are dropped immediately. // Localhost still get connection established, as ztunnel accepts anything. But they are dropped immediately.
(zt, 15080, Request), // socks5: localhost (zt, 15080, Request), // socks5: localhost
(zt, 15000, Request), // admin: localhost (zt, 15000, Request), // admin: localhost
@ -1252,7 +1248,7 @@ mod namespaced {
// Ztunnel doesn't listen on these ports... // Ztunnel doesn't listen on these ports...
(zt, 15001, Connection), // Outbound: should be blocked due to recursive call (zt, 15001, Connection), // Outbound: should be blocked due to recursive call
(zt, 15006, Connection), // Inbound: should be blocked due to recursive call (zt, 15006, Connection), // Inbound: should be blocked due to recursive call
(zt, 15008, Connection), // HBONE: expected TLS, reject (zt, 15008, Request), // HBONE: Connection succeeds (ztunnel listens) but request fails due to TLS
// Localhost is not accessible // Localhost is not accessible
(zt, 15080, Connection), // socks5: localhost (zt, 15080, Connection), // socks5: localhost
(zt, 15000, Connection), // admin: localhost (zt, 15000, Connection), // admin: localhost
@ -1329,66 +1325,190 @@ mod namespaced {
let id1s = id1.to_string(); let id1s = id1.to_string();
let ta = manager.deploy_ztunnel(DEFAULT_NODE).await?; let ta = manager.deploy_ztunnel(DEFAULT_NODE).await?;
let ztunnel_identity_obj = ta.ztunnel_identity.as_ref().unwrap().clone();
ta.cert_manager
.fetch_certificate(&ztunnel_identity_obj)
.await?;
let ztunnel_identity_str = ztunnel_identity_obj.to_string();
let check = |want: Vec<String>, help: &str| { let check = |want: Vec<String>, help: &str| {
let cm = ta.cert_manager.clone(); let cm = ta.cert_manager.clone();
let help = help.to_string(); let help = help.to_string();
let mut sorted_want = want.clone();
sorted_want.sort();
async move { async move {
// Cert manager is async, so we need to wait
let res = check_eventually( let res = check_eventually(
Duration::from_secs(2), Duration::from_secs(2),
|| cm.collect_certs(|a, _b| a.to_string()), || async {
want, let mut certs = cm.collect_certs(|a, _b| a.to_string()).await;
certs.sort();
certs
},
sorted_want,
) )
.await; .await;
assert!(res.is_ok(), "{}: got {:?}", help, res.err().unwrap()); assert!(res.is_ok(), "{}: got {:?}", help, res.err().unwrap());
} }
}; };
check(vec![], "initially empty").await; check(
vec![ztunnel_identity_str.clone()],
"initially only ztunnel cert",
)
.await;
manager manager
.workload_builder("id1-a-remote-node", REMOTE_NODE) .workload_builder("id1-a-remote-node", REMOTE_NODE)
.identity(id1.clone()) .identity(id1.clone())
.register() .register()
.await?; .await?;
check(vec![], "we should not prefetch remote nodes").await; check(
vec![ztunnel_identity_str.clone()],
"we should not prefetch remote nodes",
)
.await;
manager manager
.workload_builder("id1-a-same-node", DEFAULT_NODE) .workload_builder("id1-a-same-node", DEFAULT_NODE)
.identity(id1.clone()) .identity(id1.clone())
.register() .register()
.await?; .await?;
check(vec![id1s.clone()], "we should prefetch our nodes").await; check(
vec![ztunnel_identity_str.clone(), id1s.clone()],
"we should prefetch our nodes",
)
.await;
manager manager
.workload_builder("id1-b-same-node", DEFAULT_NODE) .workload_builder("id1-b-same-node", DEFAULT_NODE)
.identity(id1.clone()) .identity(id1.clone())
.register() .register()
.await?; .await?;
check( check(
vec![id1s.clone()], vec![ztunnel_identity_str.clone(), id1s.clone()],
"multiple of same identity shouldn't do anything", "multiple of same identity shouldn't do anything",
) )
.await; .await;
manager.delete_workload("id1-a-remote-node").await?; manager.delete_workload("id1-a-remote-node").await?;
// Deleting remote node should not affect local certs if local workloads still exist
check( check(
vec![id1s.clone()], vec![ztunnel_identity_str.clone(), id1s.clone()],
"removing remote node shouldn't impact anything", "removing remote node shouldn't impact anything",
) )
.await; .await;
manager.delete_workload("id1-b-same-node").await?; manager.delete_workload("id1-b-same-node").await?;
// Deleting one local node shouldn't impact certs if another local workload still exists
check( check(
vec![id1s.clone()], vec![ztunnel_identity_str.clone(), id1s.clone()],
"removing local node shouldn't impact anything if I still have some running", "removing local node shouldn't impact anything if I still have some running",
) )
.await; .await;
manager.delete_workload("id1-a-same-node").await?; manager.delete_workload("id1-a-same-node").await?;
// TODO: this should be vec![], but our testing setup doesn't exercise the real codepath // After deleting all workloads using sa1, give cert manager time to clean up
tokio::time::sleep(Duration::from_millis(100)).await;
// In shared mode, certificates may be kept alive by the inbound listener
// for handling inbound connections, even after workload deletion
let expected_certs = match manager.mode() {
TestMode::Shared => vec![ztunnel_identity_str.clone(), id1s.clone()],
TestMode::Dedicated => vec![ztunnel_identity_str.clone()],
};
check( check(
vec![id1s.clone()], expected_certs,
"removing final workload should clear things out", "removing final workload should clear certs except those needed by inbound listener",
) )
.await; .await;
Ok(()) Ok(())
} }
#[tokio::test]
async fn test_hbone_metrics_access() -> Result<(), anyhow::Error> {
let mut manager = setup_netns_test!(Shared);
// Deploy ztunnel for the node
let zt = manager.deploy_ztunnel(DEFAULT_NODE).await?;
let ztunnel_node_ip = manager.resolve("ztunnel-node")?;
// Use the actual metrics address ztunnel is listening on (e.g., [::]:15020)
// but combine it with the node IP for the client to target.
let target_metrics_addr = SocketAddr::new(ztunnel_node_ip, zt.metrics_address.port());
let target_metrics_url = format!("http://{target_metrics_addr}/metrics");
// Deploy a client workload (simulating Prometheus)
let client = manager
.workload_builder("client", DEFAULT_NODE)
.register()
.await?;
let zt_identity_str = zt.ztunnel_identity.as_ref().unwrap().to_string();
// Client makes a standard HTTP GET request to ztunnel's metrics endpoint
// Ztunnel's outbound capture should intercept this, initiate HBONE to its own inbound,
// which then proxies to the internal metrics server.
client
.run(move || async move {
info!(target=%target_metrics_url, "Client attempting standard HTTP GET to metrics endpoint");
let client = hyper_util::client::legacy::Client::builder(
ztunnel::hyper_util::TokioExecutor,
)
.build_http();
let req = hyper::Request::builder()
.method(Method::GET)
.uri(&target_metrics_url)
.body(Empty::<Bytes>::new())?;
let response = client.request(req).await?;
info!("Received response status: {:?}", response.status());
assert_eq!(response.status(), StatusCode::OK, "GET request failed");
let body_bytes = http_body_util::BodyExt::collect(response.into_body())
.await?
.to_bytes();
let response_str = String::from_utf8_lossy(&body_bytes);
assert!(
response_str.contains("# TYPE"),
"Expected Prometheus metrics (# TYPE) in response, got:\n{response_str}",
);
info!("Successfully verified metrics response body");
Ok(())
})?
.join()
.unwrap()?;
// Verify metrics from the DESTINATION perspective (ztunnel handling its own inbound)
let metrics = [
(CONNECTIONS_OPENED, 1), // One connection opened (client -> zt inbound via HBONE)
(CONNECTIONS_CLOSED, 1), // One connection closed
];
verify_metrics(&zt, &metrics, &destination_labels()).await;
// Verify INBOUND telemetry log for the metrics connection
let dst_addr_log = format!("{ztunnel_node_ip}:15008");
let dst_hbone_addr_log = format!("{target_metrics_addr}");
// We don't know exact byte counts, so omit them from the check for now
let want = HashMap::from([
("scope", "access"),
("src.workload", "client"),
("dst.workload", "ztunnel-node"), // ztunnel's workload name
("dst.addr", dst_addr_log.as_str()), // Connected to HBONE port
("dst.hbone_addr", dst_hbone_addr_log.as_str()), // Original target
("direction", "inbound"),
("message", "connection complete"), // Assuming success
(
"src.identity",
"spiffe://cluster.local/ns/default/sa/client",
), // Client identity
("dst.identity", zt_identity_str.as_str()), // Ztunnel identity
]);
telemetry::testing::assert_contains(want);
Ok(())
}
const TEST_VIP: &str = "10.10.0.1"; const TEST_VIP: &str = "10.10.0.1";
const TEST_VIP2: &str = "10.10.0.2"; const TEST_VIP2: &str = "10.10.0.2";
const TEST_VIP3: &str = "10.10.0.3"; const TEST_VIP3: &str = "10.10.0.3";
@ -1759,7 +1879,6 @@ mod namespaced {
} }
use Failure::*; use Failure::*;
use ztunnel::state::WorkloadInfo; use ztunnel::state::WorkloadInfo;
use ztunnel::state::workload::application_tunnel::Protocol;
async fn malicious_calls_test( async fn malicious_calls_test(
client: Namespace, client: Namespace,
@ -1786,14 +1905,14 @@ mod namespaced {
let stream = timeout(Duration::from_secs(1), TcpStream::connect(tgt)).await?; let stream = timeout(Duration::from_secs(1), TcpStream::connect(tgt)).await?;
error!("stream {stream:?}"); error!("stream {stream:?}");
if failure == Connection { if failure == Connection {
assert!(stream.is_err()); assert!(stream.is_err(), "expected connection to fail for {tgt}");
continue; continue;
} }
let mut stream = stream.unwrap(); let mut stream = stream.unwrap();
let res = timeout(Duration::from_secs(1), send_traffic(&mut stream)).await?; let res = timeout(Duration::from_secs(1), send_traffic(&mut stream)).await?;
if failure == Request { if failure == Request {
assert!(res.is_err()); assert!(res.is_err(), "expected request to fail for {tgt}");
continue; continue;
} }
res.unwrap(); res.unwrap();