diff --git a/.github/workflows/fast.yml b/.github/workflows/fast.yml index a25f47cc07..5565010c07 100644 --- a/.github/workflows/fast.yml +++ b/.github/workflows/fast.yml @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 - run: rustup component add clippy - - run: cargo clippy --all + - run: cargo clippy --all --exclude=linkerd-meshtls-boring # Enforce automated formatting. check-fmt: @@ -48,6 +48,7 @@ jobs: - run: | cargo doc --all --no-deps \ --exclude=linkerd-meshtls \ + --exclude=linkerd-meshtls-boring \ --exclude=linkerd-meshtls-rustls # Test the meshtls backends. @@ -57,6 +58,7 @@ jobs: container: image: docker://rust:1.56.0-buster steps: + - run: apt update && apt install -y cmake clang golang # for boring - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 - working-directory: ./linkerd/meshtls run: cargo test --all-features --no-run @@ -65,16 +67,23 @@ jobs: - working-directory: ./linkerd/meshtls run: | cargo test --no-run \ + --package=linkerd-meshtls-boring \ --package=linkerd-meshtls-rustls - working-directory: ./linkerd/meshtls run: | cargo test \ + --package=linkerd-meshtls-boring \ --package=linkerd-meshtls-rustls - working-directory: linkerd/meshtls run: | cargo doc --all-features --no-deps \ --package=linkerd-meshtls \ + --package=linkerd-meshtls-boring \ --package=linkerd-meshtls-rustls + # Run clippy on the boring components while we have the dependencies installed. + - run: rustup component add clippy + - working-directory: linkerd/meshtls + run: cargo clippy --features=boring --all-targets # Run non-integration tests. This should be quick. test-unit: @@ -95,6 +104,7 @@ jobs: --exclude=linkerd-app-outbound \ --exclude=linkerd-app-test \ --exclude=linkerd-meshtls \ + --exclude=linkerd-meshtls-boring \ --exclude=linkerd-meshtls-rustls \ --exclude=linkerd2-proxy - run: | @@ -108,6 +118,7 @@ jobs: --exclude=linkerd-app-outbound \ --exclude=linkerd-app-test \ --exclude=linkerd-meshtls \ + --exclude=linkerd-meshtls-boring \ --exclude=linkerd-meshtls-rustls \ --exclude=linkerd2-proxy diff --git a/.github/workflows/slow.yml b/.github/workflows/slow.yml index 34d8c2c6a7..ed7e77b782 100644 --- a/.github/workflows/slow.yml +++ b/.github/workflows/slow.yml @@ -26,6 +26,7 @@ jobs: - run: | for toml in $(find . -mindepth 2 \ -not -path '*/fuzz/*' \ + -not -path './linkerd/meshtls/boring/*' \ -name Cargo.toml \ | sort -r) do diff --git a/Cargo.lock b/Cargo.lock index 83c9d1566f..c32a4ce8fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -79,6 +88,17 @@ dependencies = [ "syn", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -91,12 +111,58 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bindgen" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd4865004a46a0aafb2a0a5eb19d3c9fc46ee5f063a6cfc605c69ac9ecf5263d" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which 3.1.1", +] + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "boring" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de45976d3e185902843f8ac67fcdc3f10f47cb96e275a545218273bf09a592f6" +dependencies = [ + "bitflags", + "boring-sys", + "foreign-types", + "lazy_static", + "libc", +] + +[[package]] +name = "boring-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2416bce1bcabf0d7995ce0338ec2425b8766a4d5a39d758a3638008911642fc" +dependencies = [ + "bindgen", + "cmake", +] + [[package]] name = "bumpalo" version = "3.8.0" @@ -121,12 +187,56 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom 5.1.2", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clang-sys" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term 0.11.0", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cmake" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b858541263efe664aead4a5209a4ae5c5d2811167d4ed4ee0944503f8d2089" +dependencies = [ + "cc", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -192,6 +302,19 @@ dependencies = [ "syn", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "fixedbitset" version = "0.4.0" @@ -216,6 +339,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -321,6 +459,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "gzip-header" version = "0.3.0" @@ -434,6 +578,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.14" @@ -582,6 +732,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.106" @@ -599,6 +755,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "libloading" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "linked-hash-map" version = "0.5.4" @@ -1026,6 +1192,7 @@ dependencies = [ "linkerd-error", "linkerd-identity", "linkerd-io", + "linkerd-meshtls-boring", "linkerd-meshtls-rustls", "linkerd-proxy-transport", "linkerd-stack", @@ -1037,6 +1204,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "linkerd-meshtls-boring" +version = "0.1.0" +dependencies = [ + "boring", + "futures", + "hex", + "linkerd-dns-name", + "linkerd-error", + "linkerd-identity", + "linkerd-io", + "linkerd-stack", + "linkerd-tls", + "linkerd-tls-test-util", + "tokio", + "tokio-boring", + "tracing", +] + [[package]] name = "linkerd-meshtls-rustls" version = "0.1.0" @@ -1621,6 +1807,16 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff" +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + [[package]] name = "ntapi" version = "0.3.6" @@ -1691,6 +1887,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1762,7 +1964,7 @@ checksum = "6ab1427f3d2635891f842892dda177883dca0639e05fe66796a62c9d2f23b49c" dependencies = [ "byteorder", "libc", - "nom", + "nom 2.2.1", "rustc_version", ] @@ -1793,7 +1995,7 @@ dependencies = [ "prost-types", "regex", "tempfile", - "which", + "which 4.2.2", ] [[package]] @@ -1952,6 +2154,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.2.3" @@ -2037,6 +2245,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -2085,6 +2299,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "syn" version = "1.0.80" @@ -2110,6 +2330,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -2174,6 +2412,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "tokio-boring" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a691f1783bcff212705a7be3ce90428511f7012d085b948741b40a81acb8dd" +dependencies = [ + "boring", + "boring-sys", + "tokio", +] + [[package]] name = "tokio-io-timeout" version = "1.1.1" @@ -2405,7 +2654,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80a4ddde70311d8da398062ecf6fc2c309337de6b0f77d6c27aff8d53f6fca52" dependencies = [ - "ansi_term", + "ansi_term 0.12.1", "lazy_static", "matchers", "parking_lot", @@ -2493,6 +2742,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -2517,6 +2772,18 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + [[package]] name = "want" version = "0.3.0" @@ -2606,6 +2873,15 @@ dependencies = [ "untrusted", ] +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "which" version = "4.2.2" @@ -2639,6 +2915,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index eb20c52303..25ae5a878b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "linkerd/identity", "linkerd/io", "linkerd/meshtls", + "linkerd/meshtls/boring", "linkerd/meshtls/rustls", "linkerd/metrics", "linkerd/opencensus", diff --git a/Dockerfile b/Dockerfile index 898c7f395f..7627d42797 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,23 +32,29 @@ FROM $RUST_IMAGE as build ARG PROXY_UNOPTIMIZED # Controls what features are enabled in the proxy. -ARG PROXY_FEATURES +ARG PROXY_FEATURES="multicore,meshtls-rustls" RUN --mount=type=cache,target=/var/lib/apt/lists \ - --mount=type=cache,target=/var/tmp \ - apt update && apt install -y time cmake + --mount=type=cache,target=/var/tmp \ + apt update && apt install -y time + +RUN --mount=type=cache,target=/var/lib/apt/lists \ + --mount=type=cache,target=/var/tmp \ + if $(echo "$PROXY_FEATURES" | grep "meshtls-boring" >/dev/null); then \ + apt install -y cmake clang golang ; \ + fi WORKDIR /usr/src/linkerd2-proxy COPY . . RUN --mount=type=cache,target=target \ - --mount=type=cache,from=rust:1.56.0-buster,source=/usr/local/cargo,target=/usr/local/cargo \ + --mount=type=cache,from=rust:1.56.0-buster,source=/usr/local/cargo,target=/usr/local/cargo \ mkdir -p /out && \ if [ -n "$PROXY_UNOPTIMIZED" ]; then \ - (cd linkerd2-proxy && /usr/bin/time -v cargo build --locked --features="$PROXY_FEATURES") && \ - mv target/debug/linkerd2-proxy /out/linkerd2-proxy ; \ + (cd linkerd2-proxy && /usr/bin/time -v cargo build --locked --no-default-features --features="$PROXY_FEATURES") && \ + mv target/debug/linkerd2-proxy /out/linkerd2-proxy ; \ else \ - (cd linkerd2-proxy && /usr/bin/time -v cargo build --locked --release --features="$PROXY_FEATURES") && \ - mv target/release/linkerd2-proxy /out/linkerd2-proxy ; \ + (cd linkerd2-proxy && /usr/bin/time -v cargo build --locked --no-default-features --features="$PROXY_FEATURES" --release) && \ + mv target/release/linkerd2-proxy /out/linkerd2-proxy ; \ fi ## Install the proxy binary into the base runtime image. @@ -61,8 +67,9 @@ ARG SKIP_IDENTITY_WRAPPER WORKDIR /linkerd COPY --from=build /out/linkerd2-proxy /usr/lib/linkerd/linkerd2-proxy ENV LINKERD2_PROXY_LOG=warn,linkerd=info -RUN if [ -n "$SKIP_IDENTITY_WRAPPER" ] ; then \ - rm -f /usr/bin/linkerd2-proxy-run && \ - ln /usr/lib/linkerd/linkerd2-proxy /usr/bin/linkerd2-proxy-run ; \ +RUN \ + if [ -n "$SKIP_IDENTITY_WRAPPER" ] ; then \ + rm -f /usr/bin/linkerd2-proxy-run && \ + ln /usr/lib/linkerd/linkerd2-proxy /usr/bin/linkerd2-proxy-run ; \ fi # Inherits the ENTRYPOINT from the runtime image. diff --git a/deny.toml b/deny.toml index 516c9b5a18..4e92ca2400 100644 --- a/deny.toml +++ b/deny.toml @@ -47,8 +47,15 @@ highlight = "all" deny = [ { name = "rustls", wrappers = ["tokio-rustls"] } ] -skip = [] -skip-tree = [] +skip = [ + # boring-sys pulls in an old version via bindgen. See + # https://github.com/cloudflare/boring/pull/55. + { name = "ansi_term" }, +] +skip-tree = [ + # Hasn't seen a new release since 2017. Pulls in an older version of nom. + { name = "procinfo" } +] [sources] unknown-registry = "deny" diff --git a/linkerd/app/inbound/src/direct.rs b/linkerd/app/inbound/src/direct.rs index e0b8442c58..f55b11e5c9 100644 --- a/linkerd/app/inbound/src/direct.rs +++ b/linkerd/app/inbound/src/direct.rs @@ -97,7 +97,7 @@ impl Inbound { let identity = rt .identity .server() - .spawn_with_alpn(vec![transport_header::PROTOCOL.into()]) + .with_alpn(vec![transport_header::PROTOCOL.into()]) .expect("TLS credential store must be held"); inner diff --git a/linkerd/meshtls/Cargo.toml b/linkerd/meshtls/Cargo.toml index 5ba1f29acc..d8d3de19f1 100644 --- a/linkerd/meshtls/Cargo.toml +++ b/linkerd/meshtls/Cargo.toml @@ -8,6 +8,7 @@ publish = false [features] rustls = ["linkerd-meshtls-rustls", "__has_any_tls_impls"] +boring = ["linkerd-meshtls-boring", "__has_any_tls_impls"] # Enabled if *any* TLS impl is enabled. __has_any_tls_impls = [] @@ -16,6 +17,7 @@ futures = { version = "0.3", default-features = false } linkerd-error = { path = "../error" } linkerd-identity = { path = "../identity" } linkerd-io = { path = "../io" } +linkerd-meshtls-boring = { path = "boring", optional = true } linkerd-meshtls-rustls = { path = "rustls", optional = true } linkerd-stack = { path = "../stack" } linkerd-tls = { path = "../tls" } diff --git a/linkerd/meshtls/boring/Cargo.toml b/linkerd/meshtls/boring/Cargo.toml new file mode 100644 index 0000000000..fb1002d89c --- /dev/null +++ b/linkerd/meshtls/boring/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "linkerd-meshtls-boring" +version = "0.1.0" +authors = ["Linkerd Developers "] +license = "Apache-2.0" +edition = "2018" +publish = false + +[dependencies] +boring = "1" +futures = { version = "0.3", default-features = false } +hex = "0.4" # used for debug logging +linkerd-error = { path = "../../error" } +linkerd-dns-name = { path = "../../dns/name" } +linkerd-identity = { path = "../../identity" } +linkerd-io = { path = "../../io" } +linkerd-stack = { path = "../../stack" } +linkerd-tls = { path = "../../tls" } +tokio = { version = "1", features = ["macros", "sync"] } +tokio-boring = "2" +tracing = "0.1" + +[dev-dependencies] +linkerd-tls-test-util = { path = "../../tls/test-util" } diff --git a/linkerd/meshtls/boring/src/client.rs b/linkerd/meshtls/boring/src/client.rs new file mode 100644 index 0000000000..723836c4c8 --- /dev/null +++ b/linkerd/meshtls/boring/src/client.rs @@ -0,0 +1,166 @@ +use crate::creds::CredsRx; +use linkerd_identity::Name; +use linkerd_io as io; +use linkerd_stack::{NewService, Service}; +use linkerd_tls::{ + client::AlpnProtocols, ClientTls, HasNegotiatedProtocol, NegotiatedProtocolRef, ServerId, +}; +use std::{future::Future, pin::Pin, sync::Arc, task::Context}; +use tracing::debug; + +#[derive(Clone)] +pub struct NewClient(CredsRx); + +#[derive(Clone)] +pub struct Connect { + rx: CredsRx, + alpn: Option]>>, + server_id: Name, +} + +pub type ConnectFuture = Pin>> + Send>>; + +#[derive(Debug)] +pub struct ClientIo(tokio_boring::SslStream); + +// === impl NewClient === + +impl NewClient { + pub(crate) fn new(rx: CredsRx) -> Self { + Self(rx) + } +} + +impl NewService for NewClient { + type Service = Connect; + + fn new_service(&self, target: ClientTls) -> Self::Service { + Connect::new(target, self.0.clone()) + } +} + +impl std::fmt::Debug for NewClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NewClient").finish() + } +} + +// === impl Connect === + +impl Connect { + pub(crate) fn new(client_tls: ClientTls, rx: CredsRx) -> Self { + let ServerId(server_id) = client_tls.server_id; + let alpn = client_tls.alpn.map(|AlpnProtocols(ps)| ps.into()); + Self { + rx, + alpn, + server_id, + } + } +} + +impl Service for Connect +where + I: io::AsyncRead + io::AsyncWrite + Send + Unpin + 'static, +{ + type Response = ClientIo; + type Error = io::Error; + type Future = ConnectFuture; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> io::Poll<()> { + io::Poll::Ready(Ok(())) + } + + fn call(&mut self, io: I) -> Self::Future { + let id = self.server_id.clone(); + let connector = self + .rx + .borrow() + .connector(self.alpn.as_deref().unwrap_or(&[])); + Box::pin(async move { + let conn = connector.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let config = conn + .configure() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let io = tokio_boring::connect(config, id.as_str(), io) + .await + .map_err(|e| match e.as_io_error() { + // TODO(ver) boring should let us take ownership of the error directly. + Some(ioe) => io::Error::new(ioe.kind(), ioe.to_string()), + // XXX(ver) to use the boring error directly here we have to constraint the socket on Sync + + // std::fmt::Debug, which is a pain. + None => io::Error::new(io::ErrorKind::Other, "unexpected TLS handshake error"), + })?; + + debug!( + tls = io.ssl().version_str(), + client.cert = ?io.ssl().certificate().and_then(super::fingerprint), + peer.cert = ?io.ssl().peer_certificate().as_deref().and_then(super::fingerprint), + alpn = ?io.ssl().selected_alpn_protocol(), + "Initiated TLS connection" + ); + Ok(ClientIo(io)) + }) + } +} + +// === impl ClientIo === + +impl io::AsyncRead for ClientIo { + #[inline] + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut io::ReadBuf<'_>, + ) -> io::Poll<()> { + Pin::new(&mut self.0).poll_read(cx, buf) + } +} + +impl io::AsyncWrite for ClientIo { + #[inline] + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> io::Poll<()> { + Pin::new(&mut self.0).poll_flush(cx) + } + + #[inline] + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> io::Poll<()> { + Pin::new(&mut self.0).poll_shutdown(cx) + } + + #[inline] + fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> io::Poll { + Pin::new(&mut self.0).poll_write(cx, buf) + } + + #[inline] + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> io::Poll { + Pin::new(&mut self.0).poll_write_vectored(cx, bufs) + } + + #[inline] + fn is_write_vectored(&self) -> bool { + self.0.is_write_vectored() + } +} + +impl HasNegotiatedProtocol for ClientIo { + #[inline] + fn negotiated_protocol(&self) -> Option> { + self.0 + .ssl() + .selected_alpn_protocol() + .map(NegotiatedProtocolRef) + } +} + +impl io::PeerAddr for ClientIo { + #[inline] + fn peer_addr(&self) -> io::Result { + self.0.get_ref().peer_addr() + } +} diff --git a/linkerd/meshtls/boring/src/creds.rs b/linkerd/meshtls/boring/src/creds.rs new file mode 100644 index 0000000000..79f2651440 --- /dev/null +++ b/linkerd/meshtls/boring/src/creds.rs @@ -0,0 +1,210 @@ +mod receiver; +mod store; + +pub use self::{receiver::Receiver, store::Store}; +use boring::{ + pkey::{PKey, Private}, + ssl, + x509::{store::X509StoreBuilder, X509}, +}; +use linkerd_error::Result; +use linkerd_identity as id; +use std::sync::Arc; +use tokio::sync::watch; + +pub fn watch( + identity: id::Name, + roots_pem: &str, + key_pkcs8: &[u8], + csr: &[u8], +) -> Result<(Store, Receiver)> { + let creds = { + let roots = X509::stack_from_pem(roots_pem.as_bytes())?; + let key = PKey::private_key_from_pkcs8(key_pkcs8)?; + Arc::new(BaseCreds { roots, key }) + }; + + let (tx, rx) = watch::channel(Creds::from(creds.clone())); + let rx = Receiver::new(identity.clone(), rx); + let store = Store::new(creds, csr, identity, tx); + + Ok((store, rx)) +} + +pub(crate) struct Creds { + base: Arc, + certs: Option, +} + +struct BaseCreds { + roots: Vec, + key: PKey, +} + +struct Certs { + leaf: X509, + intermediates: Vec, +} + +pub(crate) type CredsRx = watch::Receiver; + +type CredsTx = watch::Sender; + +// === impl Creds === + +impl From> for Creds { + fn from(base: Arc) -> Self { + Self { base, certs: None } + } +} + +impl Creds { + // TODO(ver) Specify certificate types, signing algorithms, cipher suites.. + pub(crate) fn acceptor(&self, alpn_protocols: &[Vec]) -> Result { + // mozilla_intermediate_v5 is the only variant that enables TLSv1.3, so we use that. + let mut conn = ssl::SslAcceptor::mozilla_intermediate_v5(ssl::SslMethod::tls_server())?; + + // Force use of TLSv1.3. + conn.set_options(ssl::SslOptions::NO_TLSV1_2); + conn.clear_options(ssl::SslOptions::NO_TLSV1_3); + + let roots = self.root_store()?; + tracing::debug!( + roots = ?self + .base + .roots + .iter() + .filter_map(|c| super::fingerprint(&*c)) + .collect::>(), + "Configuring acceptor roots", + ); + conn.set_cert_store(roots); + + // Ensure that client certificates are validated when present. + conn.set_verify(ssl::SslVerifyMode::PEER); + + if let Some(certs) = &self.certs { + tracing::debug!( + cert = ?super::fingerprint(&*certs.leaf), + "Configuring acceptor certificate", + ); + conn.set_private_key(&self.base.key)?; + conn.set_certificate(&certs.leaf)?; + conn.check_private_key()?; + for c in &certs.intermediates { + conn.add_extra_chain_cert(c.to_owned())?; + } + } + + if !alpn_protocols.is_empty() { + let p = serialize_alpn(alpn_protocols)?; + conn.set_alpn_protos(&*p)?; + } + + Ok(conn.build()) + } + + // TODO(ver) Specify certificate types, signing algorithms, cipher suites.. + pub(crate) fn connector(&self, alpn_protocols: &[Vec]) -> Result { + // XXX(ver) This function reads from the environment and/or the filesystem. This likely is + // at best wasteful and at worst unsafe (if another thread were to mutate these environment + // variables simultaneously, for instance). Unfortunately, the boring APIs don't really give + // us an alternative AFAICT. + let mut conn = ssl::SslConnector::builder(ssl::SslMethod::tls_client())?; + + // Explicitly enable use of TLSv1.3 + conn.set_options(ssl::SslOptions::NO_TLSV1 | ssl::SslOptions::NO_TLSV1_1); + // XXX(ver) if we disable use of TLSv1.2, connections just hang. + //conn.set_options(ssl::SslOptions::NO_TLSV1_2); + conn.clear_options(ssl::SslOptions::NO_TLSV1_3); + + tracing::debug!( + roots = ?self + .base + .roots + .iter() + .filter_map(|c| super::fingerprint(&*c)) + .collect::>(), + "Configuring connector roots", + ); + let roots = self.root_store()?; + conn.set_cert_store(roots); + + if let Some(certs) = &self.certs { + tracing::debug!( + cert = ?super::fingerprint(&*certs.leaf), + intermediates = %certs.intermediates.len(), + "Configuring connector certificate", + ); + conn.set_private_key(&self.base.key)?; + conn.set_certificate(&certs.leaf)?; + conn.check_private_key()?; + for c in &certs.intermediates { + conn.add_extra_chain_cert(c.to_owned())?; + } + } + + if !alpn_protocols.is_empty() { + let p = serialize_alpn(alpn_protocols)?; + conn.set_alpn_protos(&*p)?; + } + + Ok(conn.build()) + } + + fn root_store(&self) -> Result { + let mut store = X509StoreBuilder::new()?; + for c in &self.base.roots { + store.add_cert(c.to_owned())?; + } + + Ok(store.build()) + } +} + +/// Encodes a list of ALPN protocols into a slice of bytes. +/// +/// `boring` requires that the list of protocols be encoded in the wire format. +fn serialize_alpn(protocols: &[Vec]) -> Result> { + // Allocate a buffer to hold the encoded protocols. + let mut bytes = { + // One additional byte for each protocol's length prefix. + let cap = protocols.len() + protocols.iter().map(Vec::len).sum::(); + Vec::with_capacity(cap) + }; + + // Encode each protocol as a length-prefixed string. + for p in protocols { + if p.is_empty() { + continue; + } + if p.len() > 255 { + return Err("ALPN protocols must be less than 256 bytes".into()); + } + bytes.push(p.len() as u8); + bytes.extend(p); + } + + Ok(bytes) +} + +#[cfg(test)] +#[test] +fn test_serialize_alpn() { + assert_eq!(serialize_alpn(&[b"h2".to_vec()]).unwrap(), b"\x02h2"); + assert_eq!( + serialize_alpn(&[b"h2".to_vec(), b"http/1.1".to_vec()]).unwrap(), + b"\x02h2\x08http/1.1" + ); + assert_eq!( + serialize_alpn(&[b"h2".to_vec(), b"http/1.1".to_vec()]).unwrap(), + b"\x02h2\x08http/1.1" + ); + assert_eq!( + serialize_alpn(&[b"h2".to_vec(), vec![], b"http/1.1".to_vec()]).unwrap(), + b"\x02h2\x08http/1.1" + ); + + assert!(serialize_alpn(&[(0..255).collect()]).is_ok()); + assert!(serialize_alpn(&[(0..=255).collect()]).is_err()); +} diff --git a/linkerd/meshtls/boring/src/creds/receiver.rs b/linkerd/meshtls/boring/src/creds/receiver.rs new file mode 100644 index 0000000000..9e78dea160 --- /dev/null +++ b/linkerd/meshtls/boring/src/creds/receiver.rs @@ -0,0 +1,38 @@ +use super::CredsRx; +use crate::{NewClient, Server}; +use linkerd_identity::Name; + +#[derive(Clone)] +pub struct Receiver { + name: Name, + rx: CredsRx, +} + +impl Receiver { + pub(crate) fn new(name: Name, rx: CredsRx) -> Self { + Self { name, rx } + } + + /// Returns the local identity. + pub fn name(&self) -> &Name { + &self.name + } + + /// Returns a `NewClient` that can be used to establish TLS on client connections. + pub fn new_client(&self) -> NewClient { + NewClient::new(self.rx.clone()) + } + + /// Returns a `Server` that can be used to terminate TLS on server connections. + pub fn server(&self) -> Server { + Server::new(self.name.clone(), self.rx.clone()) + } +} + +impl std::fmt::Debug for Receiver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Receiver") + .field("name", &self.name) + .finish() + } +} diff --git a/linkerd/meshtls/boring/src/creds/store.rs b/linkerd/meshtls/boring/src/creds/store.rs new file mode 100644 index 0000000000..bdabc7dbad --- /dev/null +++ b/linkerd/meshtls/boring/src/creds/store.rs @@ -0,0 +1,98 @@ +use super::{BaseCreds, Certs, Creds, CredsTx}; +use boring::x509::{X509StoreContext, X509}; +use linkerd_error::Result; +use linkerd_identity as id; +use std::sync::Arc; + +pub struct Store { + creds: Arc, + csr: Vec, + name: id::Name, + tx: CredsTx, +} + +// === impl Store === + +impl Store { + pub(super) fn new(creds: Arc, csr: &[u8], name: id::Name, tx: CredsTx) -> Self { + Self { + creds, + csr: csr.into(), + name, + tx, + } + } + + fn cert_matches_name(&self, cert: &X509) -> bool { + for san in cert.subject_alt_names().into_iter().flatten() { + if let Some(n) = san.dnsname() { + if let Ok(name) = n.parse::() { + if name == *self.name { + return true; + } + } + } + } + + false + } +} + +impl id::Credentials for Store { + /// Returns the proxy's identity. + fn dns_name(&self) -> &id::Name { + &self.name + } + + /// Returns the CSR that was configured at proxy startup. + fn gen_certificate_signing_request(&mut self) -> id::DerX509 { + id::DerX509(self.csr.to_vec()) + } + + /// Publishes TLS client and server configurations using + fn set_certificate( + &mut self, + id::DerX509(leaf): id::DerX509, + intermediates: Vec, + _expiry: std::time::SystemTime, + ) -> Result<()> { + let leaf = X509::from_der(&leaf)?; + if !self.cert_matches_name(&leaf) { + return Err("certificate does not have a DNS name SAN for the local identity".into()); + } + + let intermediates = intermediates + .into_iter() + .map(|id::DerX509(der)| X509::from_der(&der).map_err(Into::into)) + .collect::>>()?; + + let creds = Creds { + base: self.creds.clone(), + certs: Some(Certs { + leaf, + intermediates, + }), + }; + + let mut context = X509StoreContext::new()?; + let roots = creds.root_store()?; + + let mut chain = boring::stack::Stack::new()?; + for i in &creds.certs.as_ref().unwrap().intermediates { + chain.push(i.to_owned())?; + } + let init = { + let leaf = &creds.certs.as_ref().unwrap().leaf; + context.init(&roots, leaf, &chain, |c| c.verify_cert())? + }; + if !init { + return Err("certificate could not be validated against the trust chain".into()); + } + + // If receivers are dropped, we don't return an error (as this would likely cause the + // updater to retry more aggressively). It's fine to silently ignore these errors. + let _ = self.tx.send(creds); + + Ok(()) + } +} diff --git a/linkerd/meshtls/boring/src/lib.rs b/linkerd/meshtls/boring/src/lib.rs new file mode 100644 index 0000000000..d9f01312ac --- /dev/null +++ b/linkerd/meshtls/boring/src/lib.rs @@ -0,0 +1,40 @@ +#![deny(warnings, rust_2018_idioms)] +#![forbid(unsafe_code)] + +//! This crate provides an implementation of _meshtls_ backed by `boringssl` (as +//! provided by ). +//! +//! There are several caveats with the current implementation: +//! +//! In its current form, this crate is compatible with the `meshtls-rustls` +//! implementation, which requires of ECDSA-P256-SHA256 keys & signature +//! algorithms. This crate doesn't actually constrain the algorithms beyond the +//! Mozilla's 'intermediate' (v5) [defaults][defaults]. But, the goal for +//! supporting `boring` is to provide a FIPS 140-2 compliant mode. There's a +//! [PR][fips-pr] that implements this, but code changes will likely be required +//! to enable this once it's merged/released. +//! +//! A new SSL context is created for each connection. This is probably +//! unnecessary, but it's simpler for now. We can revisit this if needed. +//! +//! This module is not enabled by default. See the `linkerd-meshtls` and +//! `linkerd2-proxy` crates for more information. +//! +//! [defaults]: https://wiki.mozilla.org/Security/Server_Side_TLS +//! [fips-pr]: https://github.com/cloudflare/boring/pull/52 + +mod client; +pub mod creds; +mod server; +#[cfg(test)] +mod tests; + +pub use self::{ + client::{ClientIo, Connect, ConnectFuture, NewClient}, + server::{Server, ServerIo, TerminateFuture}, +}; + +fn fingerprint(c: &boring::x509::X509Ref) -> Option { + let digest = c.digest(boring::hash::MessageDigest::sha256()).ok()?; + Some(hex::encode(digest)[0..8].to_string()) +} diff --git a/linkerd/meshtls/boring/src/server.rs b/linkerd/meshtls/boring/src/server.rs new file mode 100644 index 0000000000..9d512471de --- /dev/null +++ b/linkerd/meshtls/boring/src/server.rs @@ -0,0 +1,178 @@ +use crate::creds::CredsRx; +use linkerd_identity::Name; +use linkerd_io as io; +use linkerd_stack::{Param, Service}; +use linkerd_tls::{ClientId, LocalId, NegotiatedProtocol, ServerTls}; +use std::{future::Future, pin::Pin, sync::Arc, task::Context}; +use tracing::debug; + +#[derive(Clone)] +pub struct Server { + name: Name, + rx: CredsRx, + alpn: Option]>>, +} + +pub type TerminateFuture = + Pin)>> + Send>>; + +#[derive(Debug)] +pub struct ServerIo(tokio_boring::SslStream); + +// === impl Server === + +impl Server { + pub(crate) fn new(name: Name, rx: CredsRx) -> Self { + Self { + name, + rx, + alpn: None, + } + } + + pub fn with_alpn(mut self, alpn_protocols: Vec>) -> Self { + self.alpn = if alpn_protocols.is_empty() { + None + } else { + Some(alpn_protocols.into()) + }; + + self + } +} + +impl Param for Server { + fn param(&self) -> LocalId { + LocalId(self.name.clone()) + } +} + +impl Service for Server +where + I: io::AsyncRead + io::AsyncWrite + Send + Unpin + 'static, +{ + type Response = (ServerTls, ServerIo); + type Error = std::io::Error; + type Future = TerminateFuture; + + #[inline] + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> io::Poll<()> { + io::Poll::Ready(Ok(())) + } + + fn call(&mut self, io: I) -> Self::Future { + // TODO(ver) we should avoid creating a new context for each connection. + let acceptor = self + .rx + .borrow() + .acceptor(self.alpn.as_deref().unwrap_or(&[])); + Box::pin(async move { + let acc = acceptor.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let io = tokio_boring::accept(&acc, io) + .await + .map(ServerIo) + .map_err(|e| match e.as_io_error() { + Some(ioe) => io::Error::new(ioe.kind(), ioe.to_string()), + // XXX(ver) to use the boring error directly here we have to constraint the + // socket on Sync + std::fmt::Debug, which is a pain. + None => io::Error::new(io::ErrorKind::Other, "unexpected TLS handshake error"), + })?; + + let client_id = io.client_identity(); + let negotiated_protocol = io.negotiated_protocol(); + + debug!( + tls = io.0.ssl().version_str(), + srv.cert = ?io.0.ssl().certificate().as_deref().and_then(super::fingerprint), + peer.cert = ?io.0.ssl().peer_certificate().as_deref().and_then(super::fingerprint), + client.id = ?client_id, + alpn = ?negotiated_protocol, + "Accepted TLS connection" + ); + let tls = ServerTls::Established { + client_id, + negotiated_protocol, + }; + Ok((tls, io)) + }) + } +} + +// === impl ServerIo === + +impl ServerIo { + #[inline] + fn negotiated_protocol(&self) -> Option { + self.0 + .ssl() + .selected_alpn_protocol() + .map(|p| NegotiatedProtocol(p.to_vec())) + } + + fn client_identity(&self) -> Option { + let cert = self.0.ssl().peer_certificate().or_else(|| { + debug!("Connection missing peer certificate"); + None + })?; + let sans = cert.subject_alt_names().or_else(|| { + debug!("Peer certificate missing SANs"); + None + })?; + sans.into_iter() + .filter_map(|san| san.dnsname()?.parse().ok()) + .next() + .or_else(|| { + debug!("Peer certificate missing DNS SANs"); + None + }) + } +} + +impl io::AsyncRead for ServerIo { + #[inline] + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut io::ReadBuf<'_>, + ) -> io::Poll<()> { + Pin::new(&mut self.0).poll_read(cx, buf) + } +} + +impl io::AsyncWrite for ServerIo { + #[inline] + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> io::Poll<()> { + Pin::new(&mut self.0).poll_flush(cx) + } + + #[inline] + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> io::Poll<()> { + Pin::new(&mut self.0).poll_shutdown(cx) + } + + #[inline] + fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> io::Poll { + Pin::new(&mut self.0).poll_write(cx, buf) + } + + #[inline] + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> io::Poll { + Pin::new(&mut self.0).poll_write_vectored(cx, bufs) + } + + #[inline] + fn is_write_vectored(&self) -> bool { + self.0.is_write_vectored() + } +} + +impl io::PeerAddr for ServerIo { + #[inline] + fn peer_addr(&self) -> io::Result { + self.0.get_ref().peer_addr() + } +} diff --git a/linkerd/meshtls/boring/src/tests.rs b/linkerd/meshtls/boring/src/tests.rs new file mode 100644 index 0000000000..1fd56856b3 --- /dev/null +++ b/linkerd/meshtls/boring/src/tests.rs @@ -0,0 +1,48 @@ +use linkerd_identity::{Credentials, DerX509}; +use linkerd_tls_test_util::*; +use std::time::Duration; + +fn load(ent: &Entity) -> crate::creds::Store { + let roots_pem = std::str::from_utf8(ent.trust_anchors).expect("valid PEM"); + let (store, _) = crate::creds::watch( + ent.name.parse().unwrap(), + roots_pem, + ent.key, + b"fake CSR data", + ) + .expect("credentials must be readable"); + store +} + +#[test] +fn can_construct_client_and_server_config_from_valid_settings() { + assert!(load(&FOO_NS1) + .set_certificate( + DerX509(FOO_NS1.crt.to_vec()), + vec![], + std::time::SystemTime::now() + Duration::from_secs(600) + ) + .is_ok()); +} + +#[test] +fn recognize_ca_did_not_issue_cert() { + assert!(load(&FOO_NS1_CA2) + .set_certificate( + DerX509(FOO_NS1.crt.to_vec()), + vec![], + std::time::SystemTime::now() + Duration::from_secs(600) + ) + .is_err()); +} + +#[test] +fn recognize_cert_is_not_valid_for_identity() { + assert!(load(&BAR_NS1) + .set_certificate( + DerX509(FOO_NS1.crt.to_vec()), + vec![], + std::time::SystemTime::now() + Duration::from_secs(600) + ) + .is_err()); +} diff --git a/linkerd/meshtls/src/client.rs b/linkerd/meshtls/src/client.rs index f12c758b81..a55bfe5d7d 100644 --- a/linkerd/meshtls/src/client.rs +++ b/linkerd/meshtls/src/client.rs @@ -7,14 +7,20 @@ use std::{ task::{Context, Poll}, }; -#[cfg(not(feature = "__has_any_tls_impls"))] -use std::marker::PhantomData; +#[cfg(feature = "boring")] +use crate::boring; #[cfg(feature = "rustls")] use crate::rustls; +#[cfg(not(feature = "__has_any_tls_impls"))] +use std::marker::PhantomData; + #[derive(Clone, Debug)] pub enum NewClient { + #[cfg(feature = "boring")] + Boring(boring::NewClient), + #[cfg(feature = "rustls")] Rustls(rustls::NewClient), @@ -24,6 +30,9 @@ pub enum NewClient { #[derive(Clone)] pub enum Connect { + #[cfg(feature = "boring")] + Boring(boring::Connect), + #[cfg(feature = "rustls")] Rustls(rustls::Connect), @@ -33,6 +42,9 @@ pub enum Connect { #[pin_project::pin_project(project = ConnectFutureProj)] pub enum ConnectFuture { + #[cfg(feature = "boring")] + Boring(#[pin] boring::ConnectFuture), + #[cfg(feature = "rustls")] Rustls(#[pin] rustls::ConnectFuture), @@ -43,6 +55,9 @@ pub enum ConnectFuture { #[pin_project::pin_project(project = ClientIoProj)] #[derive(Debug)] pub enum ClientIo { + #[cfg(feature = "boring")] + Boring(#[pin] boring::ClientIo), + #[cfg(feature = "rustls")] Rustls(#[pin] rustls::ClientIo), @@ -55,8 +70,12 @@ pub enum ClientIo { impl NewService for NewClient { type Service = Connect; + #[inline] fn new_service(&self, target: ClientTls) -> Self::Service { match self { + #[cfg(feature = "boring")] + Self::Boring(new_client) => Connect::Boring(new_client.new_service(target)), + #[cfg(feature = "rustls")] Self::Rustls(new_client) => Connect::Rustls(new_client.new_service(target)), @@ -76,8 +95,12 @@ where type Error = io::Error; type Future = ConnectFuture; + #[inline] fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { match self { + #[cfg(feature = "boring")] + Self::Boring(connect) => >::poll_ready(connect, cx), + #[cfg(feature = "rustls")] Self::Rustls(connect) => >::poll_ready(connect, cx), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -88,6 +111,9 @@ where #[inline] fn call(&mut self, io: I) -> Self::Future { match self { + #[cfg(feature = "boring")] + Self::Boring(connect) => ConnectFuture::Boring(connect.call(io)), + #[cfg(feature = "rustls")] Self::Rustls(connect) => ConnectFuture::Rustls(connect.call(io)), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -104,8 +130,15 @@ where { type Output = io::Result>; + #[inline] fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.project() { + #[cfg(feature = "boring")] + ConnectFutureProj::Boring(f) => { + let res = futures::ready!(f.poll(cx)); + Poll::Ready(res.map(ClientIo::Boring)) + } + #[cfg(feature = "rustls")] ConnectFutureProj::Rustls(f) => { let res = futures::ready!(f.poll(cx)); @@ -127,6 +160,9 @@ impl io::AsyncRead for ClientIo { buf: &mut io::ReadBuf<'_>, ) -> io::Poll<()> { match self.project() { + #[cfg(feature = "boring")] + ClientIoProj::Boring(io) => io.poll_read(cx, buf), + #[cfg(feature = "rustls")] ClientIoProj::Rustls(io) => io.poll_read(cx, buf), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -139,6 +175,9 @@ impl io::AsyncWrite for ClientIo { #[inline] fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> io::Poll<()> { match self.project() { + #[cfg(feature = "boring")] + ClientIoProj::Boring(io) => io.poll_flush(cx), + #[cfg(feature = "rustls")] ClientIoProj::Rustls(io) => io.poll_flush(cx), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -149,6 +188,9 @@ impl io::AsyncWrite for ClientIo { #[inline] fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> io::Poll<()> { match self.project() { + #[cfg(feature = "boring")] + ClientIoProj::Boring(io) => io.poll_shutdown(cx), + #[cfg(feature = "rustls")] ClientIoProj::Rustls(io) => io.poll_shutdown(cx), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -159,6 +201,9 @@ impl io::AsyncWrite for ClientIo { #[inline] fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> io::Poll { match self.project() { + #[cfg(feature = "boring")] + ClientIoProj::Boring(io) => io.poll_write(cx, buf), + #[cfg(feature = "rustls")] ClientIoProj::Rustls(io) => io.poll_write(cx, buf), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -173,6 +218,9 @@ impl io::AsyncWrite for ClientIo { bufs: &[io::IoSlice<'_>], ) -> Poll> { match self.project() { + #[cfg(feature = "boring")] + ClientIoProj::Boring(io) => io.poll_write_vectored(cx, bufs), + #[cfg(feature = "rustls")] ClientIoProj::Rustls(io) => io.poll_write_vectored(cx, bufs), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -183,6 +231,9 @@ impl io::AsyncWrite for ClientIo { #[inline] fn is_write_vectored(&self) -> bool { match self { + #[cfg(feature = "boring")] + Self::Boring(io) => io.is_write_vectored(), + #[cfg(feature = "rustls")] Self::Rustls(io) => io.is_write_vectored(), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -195,6 +246,9 @@ impl HasNegotiatedProtocol for ClientIo { #[inline] fn negotiated_protocol(&self) -> Option> { match self { + #[cfg(feature = "boring")] + Self::Boring(io) => io.negotiated_protocol(), + #[cfg(feature = "rustls")] Self::Rustls(io) => io.negotiated_protocol(), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -207,6 +261,9 @@ impl io::PeerAddr for ClientIo { #[inline] fn peer_addr(&self) -> io::Result { match self { + #[cfg(feature = "boring")] + Self::Boring(io) => io.peer_addr(), + #[cfg(feature = "rustls")] Self::Rustls(io) => io.peer_addr(), #[cfg(not(feature = "__has_any_tls_impls"))] diff --git a/linkerd/meshtls/src/creds.rs b/linkerd/meshtls/src/creds.rs index 494b38e01f..27305f52f7 100644 --- a/linkerd/meshtls/src/creds.rs +++ b/linkerd/meshtls/src/creds.rs @@ -2,10 +2,16 @@ use crate::{NewClient, Server}; use linkerd_error::Result; use linkerd_identity::{Credentials, DerX509, Name}; +#[cfg(feature = "boring")] +pub use crate::boring; + #[cfg(feature = "rustls")] pub use crate::rustls; pub enum Store { + #[cfg(feature = "boring")] + Boring(boring::creds::Store), + #[cfg(feature = "rustls")] Rustls(rustls::creds::Store), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -14,6 +20,9 @@ pub enum Store { #[derive(Clone, Debug)] pub enum Receiver { + #[cfg(feature = "boring")] + Boring(boring::creds::Receiver), + #[cfg(feature = "rustls")] Rustls(rustls::creds::Receiver), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -25,6 +34,9 @@ pub enum Receiver { impl Credentials for Store { fn dns_name(&self) -> &Name { match self { + #[cfg(feature = "boring")] + Self::Boring(store) => store.dns_name(), + #[cfg(feature = "rustls")] Self::Rustls(store) => store.dns_name(), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -34,6 +46,9 @@ impl Credentials for Store { fn gen_certificate_signing_request(&mut self) -> DerX509 { match self { + #[cfg(feature = "boring")] + Self::Boring(store) => store.gen_certificate_signing_request(), + #[cfg(feature = "rustls")] Self::Rustls(store) => store.gen_certificate_signing_request(), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -48,6 +63,9 @@ impl Credentials for Store { expiry: std::time::SystemTime, ) -> Result<()> { match self { + #[cfg(feature = "boring")] + Self::Boring(store) => store.set_certificate(leaf, chain, expiry), + #[cfg(feature = "rustls")] Self::Rustls(store) => store.set_certificate(leaf, chain, expiry), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -58,6 +76,13 @@ impl Credentials for Store { // === impl Receiver === +#[cfg(feature = "boring")] +impl From for Receiver { + fn from(rx: boring::creds::Receiver) -> Self { + Self::Boring(rx) + } +} + #[cfg(feature = "rustls")] impl From for Receiver { fn from(rx: rustls::creds::Receiver) -> Self { @@ -68,6 +93,9 @@ impl From for Receiver { impl Receiver { pub fn name(&self) -> &Name { match self { + #[cfg(feature = "boring")] + Self::Boring(receiver) => receiver.name(), + #[cfg(feature = "rustls")] Self::Rustls(receiver) => receiver.name(), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -77,6 +105,9 @@ impl Receiver { pub fn new_client(&self) -> NewClient { match self { + #[cfg(feature = "boring")] + Self::Boring(receiver) => NewClient::Boring(receiver.new_client()), + #[cfg(feature = "rustls")] Self::Rustls(receiver) => NewClient::Rustls(receiver.new_client()), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -86,6 +117,9 @@ impl Receiver { pub fn server(&self) -> Server { match self { + #[cfg(feature = "boring")] + Self::Boring(receiver) => Server::Boring(receiver.server()), + #[cfg(feature = "rustls")] Self::Rustls(receiver) => Server::Rustls(receiver.server()), #[cfg(not(feature = "__has_any_tls_impls"))] diff --git a/linkerd/meshtls/src/lib.rs b/linkerd/meshtls/src/lib.rs index fd317fdcaf..fbdc4b68ed 100644 --- a/linkerd/meshtls/src/lib.rs +++ b/linkerd/meshtls/src/lib.rs @@ -1,18 +1,15 @@ #![deny(warnings, rust_2018_idioms)] #![forbid(unsafe_code)] -#[cfg(not(feature = "__has_any_tls_impls"))] -#[macro_export] -macro_rules! no_tls { - ($($field:ident),*) => { - { - $( - let _ = $field; - )* - unreachable!("compiled without any TLS implementations enabled!"); - } - }; -} +//! This crate provides a static interface for the proxy's x509 certificate +//! provisioning and creation of client/server services. It supports the +//! `boring` and `rustls` TLS backends. +//! +//! This crate may be compiled without either implementation, in which case it +//! will fail at runtime. This enables an implementation to be chosen by the +//! proxy's frontend, so that other crates can depend on this crate without +//! having to pin a TLS implementation. Furthermore, this crate supports both +//! backends simultaneously so it can be compiled with `--all-features`. mod client; pub mod creds; @@ -22,14 +19,21 @@ pub use self::{ client::{ClientIo, Connect, ConnectFuture, NewClient}, server::{Server, ServerIo, TerminateFuture}, }; -use linkerd_error::Result; +use linkerd_error::{Error, Result}; use linkerd_identity::Name; +use std::str::FromStr; + +#[cfg(feature = "boring")] +pub use linkerd_meshtls_boring as boring; #[cfg(feature = "rustls")] pub use linkerd_meshtls_rustls as rustls; #[derive(Copy, Clone, Debug)] pub enum Mode { + #[cfg(feature = "boring")] + Boring, + #[cfg(feature = "rustls")] Rustls, @@ -37,15 +41,39 @@ pub enum Mode { NoTls, } +#[cfg(not(feature = "__has_any_tls_impls"))] +#[macro_export] +macro_rules! no_tls { + ($($field:ident),*) => { + { + $( + let _ = $field; + )* + unreachable!("compiled without any TLS implementations enabled!"); + } + }; +} + // === impl Mode === +#[cfg(feature = "rustls")] impl Default for Mode { fn default() -> Self { - #[cfg(feature = "rustls")] - return Self::Rustls; + Self::Rustls + } +} - // This may not be unreachable if no feature flags are enabled. - #[cfg(not(feature = "__has_any_tls_impls"))] +// FIXME(ver) We should have a way to opt into boring by configuration when both are enabled. +#[cfg(all(feature = "boring", not(feature = "rustls")))] +impl Default for Mode { + fn default() -> Self { + Self::Boring + } +} + +#[cfg(not(feature = "__has_any_tls_impls"))] +impl Default for Mode { + fn default() -> Self { Self::NoTls } } @@ -59,6 +87,15 @@ impl Mode { csr: &[u8], ) -> Result<(creds::Store, creds::Receiver)> { match self { + #[cfg(feature = "boring")] + Self::Boring => { + let (store, receiver) = boring::creds::watch(identity, roots_pem, key_pkcs8, csr)?; + Ok(( + creds::Store::Boring(store), + creds::Receiver::Boring(receiver), + )) + } + #[cfg(feature = "rustls")] Self::Rustls => { let (store, receiver) = rustls::creds::watch(identity, roots_pem, key_pkcs8, csr)?; @@ -73,3 +110,36 @@ impl Mode { } } } + +impl FromStr for Mode { + type Err = Error; + + fn from_str(s: &str) -> Result { + #[cfg(feature = "boring")] + if s.eq_ignore_ascii_case("boring") { + return Ok(Self::Boring); + } + + #[cfg(feature = "rustls")] + if s.eq_ignore_ascii_case("rustls") { + return Ok(Self::Rustls); + } + + Err(format!("unknown TLS backend: {}", s).into()) + } +} + +impl std::fmt::Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + #[cfg(feature = "boring")] + Self::Boring => "boring".fmt(f), + + #[cfg(feature = "rustls")] + Self::Rustls => "rustls".fmt(f), + + #[cfg(not(feature = "__has_any_tls_impls"))] + _ => no_tls!(f), + } + } +} diff --git a/linkerd/meshtls/src/server.rs b/linkerd/meshtls/src/server.rs index eabf7cf222..cceb9c570a 100644 --- a/linkerd/meshtls/src/server.rs +++ b/linkerd/meshtls/src/server.rs @@ -9,14 +9,20 @@ use std::{ task::{Context, Poll}, }; -#[cfg(not(feature = "__has_any_tls_impls"))] -use std::marker::PhantomData; +#[cfg(feature = "boring")] +use crate::boring; #[cfg(feature = "rustls")] use crate::rustls; +#[cfg(not(feature = "__has_any_tls_impls"))] +use std::marker::PhantomData; + #[derive(Clone)] pub enum Server { + #[cfg(feature = "boring")] + Boring(boring::Server), + #[cfg(feature = "rustls")] Rustls(rustls::Server), @@ -26,6 +32,9 @@ pub enum Server { #[pin_project::pin_project(project = TerminateFutureProj)] pub enum TerminateFuture { + #[cfg(feature = "boring")] + Boring(#[pin] boring::TerminateFuture), + #[cfg(feature = "rustls")] Rustls(#[pin] rustls::TerminateFuture), @@ -36,6 +45,9 @@ pub enum TerminateFuture { #[pin_project::pin_project(project = ServerIoProj)] #[derive(Debug)] pub enum ServerIo { + #[cfg(feature = "boring")] + Boring(#[pin] boring::ServerIo), + #[cfg(feature = "rustls")] Rustls(#[pin] rustls::ServerIo), @@ -46,8 +58,12 @@ pub enum ServerIo { // === impl Server === impl Param for Server { + #[inline] fn param(&self) -> LocalId { match self { + #[cfg(feature = "boring")] + Self::Boring(srv) => srv.param(), + #[cfg(feature = "rustls")] Self::Rustls(srv) => srv.param(), @@ -58,8 +74,11 @@ impl Param for Server { } impl Server { - pub fn spawn_with_alpn(self, alpn_protocols: Vec>) -> Result { + pub fn with_alpn(self, alpn_protocols: Vec>) -> Result { match self { + #[cfg(feature = "boring")] + Self::Boring(srv) => Ok(Self::Boring(srv.with_alpn(alpn_protocols))), + #[cfg(feature = "rustls")] Self::Rustls(srv) => srv .spawn_with_alpn(alpn_protocols) @@ -74,14 +93,18 @@ impl Server { impl Service for Server where - I: io::AsyncRead + io::AsyncWrite + Send + Sync + Unpin + 'static, + I: io::AsyncRead + io::AsyncWrite + Send + Unpin + 'static, { type Response = (ServerTls, ServerIo); type Error = io::Error; type Future = TerminateFuture; + #[inline] fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { match self { + #[cfg(feature = "boring")] + Self::Boring(svc) => >::poll_ready(svc, cx), + #[cfg(feature = "rustls")] Self::Rustls(svc) => >::poll_ready(svc, cx), @@ -93,6 +116,9 @@ where #[inline] fn call(&mut self, io: I) -> Self::Future { match self { + #[cfg(feature = "boring")] + Self::Boring(svc) => TerminateFuture::Boring(svc.call(io)), + #[cfg(feature = "rustls")] Self::Rustls(svc) => TerminateFuture::Rustls(svc.call(io)), @@ -110,8 +136,15 @@ where { type Output = io::Result<(ServerTls, ServerIo)>; + #[inline] fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.project() { + #[cfg(feature = "boring")] + TerminateFutureProj::Boring(f) => { + let res = futures::ready!(f.poll(cx)); + Poll::Ready(res.map(|(tls, io)| (tls, ServerIo::Boring(io)))) + } + #[cfg(feature = "rustls")] TerminateFutureProj::Rustls(f) => { let res = futures::ready!(f.poll(cx)); @@ -134,6 +167,9 @@ impl io::AsyncRead for ServerIo { buf: &mut io::ReadBuf<'_>, ) -> io::Poll<()> { match self.project() { + #[cfg(feature = "boring")] + ServerIoProj::Boring(io) => io.poll_read(cx, buf), + #[cfg(feature = "rustls")] ServerIoProj::Rustls(io) => io.poll_read(cx, buf), @@ -147,6 +183,9 @@ impl io::AsyncWrite for ServerIo { #[inline] fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> io::Poll<()> { match self.project() { + #[cfg(feature = "boring")] + ServerIoProj::Boring(io) => io.poll_flush(cx), + #[cfg(feature = "rustls")] ServerIoProj::Rustls(io) => io.poll_flush(cx), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -157,6 +196,9 @@ impl io::AsyncWrite for ServerIo { #[inline] fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> io::Poll<()> { match self.project() { + #[cfg(feature = "boring")] + ServerIoProj::Boring(io) => io.poll_shutdown(cx), + #[cfg(feature = "rustls")] ServerIoProj::Rustls(io) => io.poll_shutdown(cx), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -167,6 +209,9 @@ impl io::AsyncWrite for ServerIo { #[inline] fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> io::Poll { match self.project() { + #[cfg(feature = "boring")] + ServerIoProj::Boring(io) => io.poll_write(cx, buf), + #[cfg(feature = "rustls")] ServerIoProj::Rustls(io) => io.poll_write(cx, buf), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -181,6 +226,9 @@ impl io::AsyncWrite for ServerIo { bufs: &[io::IoSlice<'_>], ) -> Poll> { match self.project() { + #[cfg(feature = "boring")] + ServerIoProj::Boring(io) => io.poll_write_vectored(cx, bufs), + #[cfg(feature = "rustls")] ServerIoProj::Rustls(io) => io.poll_write_vectored(cx, bufs), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -191,6 +239,9 @@ impl io::AsyncWrite for ServerIo { #[inline] fn is_write_vectored(&self) -> bool { match self { + #[cfg(feature = "boring")] + Self::Boring(io) => io.is_write_vectored(), + #[cfg(feature = "rustls")] Self::Rustls(io) => io.is_write_vectored(), #[cfg(not(feature = "__has_any_tls_impls"))] @@ -203,6 +254,9 @@ impl io::PeerAddr for ServerIo { #[inline] fn peer_addr(&self) -> io::Result { match self { + #[cfg(feature = "boring")] + Self::Boring(io) => io.peer_addr(), + #[cfg(feature = "rustls")] Self::Rustls(io) => io.peer_addr(), #[cfg(not(feature = "__has_any_tls_impls"))] diff --git a/linkerd/meshtls/tests/boring.rs b/linkerd/meshtls/tests/boring.rs new file mode 100644 index 0000000000..17f3abda7d --- /dev/null +++ b/linkerd/meshtls/tests/boring.rs @@ -0,0 +1,22 @@ +#![cfg(feature = "boring")] +#![deny(warnings, rust_2018_idioms)] +#![forbid(unsafe_code)] + +mod util; + +use linkerd_meshtls::Mode; + +#[tokio::test(flavor = "current_thread")] +async fn plaintext() { + util::plaintext(Mode::Boring).await; +} + +#[tokio::test(flavor = "current_thread")] +async fn proxy_to_proxy_tls_works() { + util::proxy_to_proxy_tls_works(Mode::Boring).await; +} + +#[tokio::test(flavor = "current_thread")] +async fn proxy_to_proxy_tls_pass_through_when_identity_does_not_match() { + util::proxy_to_proxy_tls_pass_through_when_identity_does_not_match(Mode::Boring).await; +} diff --git a/linkerd2-proxy/Cargo.toml b/linkerd2-proxy/Cargo.toml index 541f2f736f..7c0729562f 100644 --- a/linkerd2-proxy/Cargo.toml +++ b/linkerd2-proxy/Cargo.toml @@ -10,6 +10,7 @@ description = "The main proxy executable" [features] default = ["multicore", "meshtls-rustls"] multicore = ["tokio/rt-multi-thread", "num_cpus"] +meshtls-boring = ["linkerd-meshtls/boring"] meshtls-rustls = ["linkerd-meshtls/rustls"] [dependencies] diff --git a/linkerd2-proxy/src/main.rs b/linkerd2-proxy/src/main.rs index 4683243139..7c8bde2d7d 100644 --- a/linkerd2-proxy/src/main.rs +++ b/linkerd2-proxy/src/main.rs @@ -6,9 +6,9 @@ // Emit a compile-time error if no TLS implementations are enabled. When adding // new implementations, add their feature flags here! -#[cfg(not(any(feature = "meshtls-rustls")))] +#[cfg(not(any(feature = "meshtls-boring", feature = "meshtls-rustls")))] compile_error!( - "at least one of the following TLS implementations must be enabled: 'meshtls-rustls'" + "at least one of the following TLS implementations must be enabled: 'meshtls-boring', 'meshtls-rustls'" ); use linkerd_app::{core::transport::BindTcp, trace, Config};