diff --git a/Cargo.lock b/Cargo.lock index 6ec363e..549a0be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "addr2line" @@ -239,7 +239,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", ] @@ -264,6 +264,25 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-server" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56bac90848f6a9393ac03c63c640925c4b7c8ca21654de40d53f55964667c7d8" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower 0.4.13", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -362,9 +381,9 @@ checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" -version = "1.1.37" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "jobserver", "libc", @@ -410,9 +429,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -420,9 +439,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -444,9 +463,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "color_quant" @@ -504,25 +523,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -631,9 +631,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -652,12 +652,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "fontdb" -version = "0.22.0" +name = "fontconfig-parser" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a6f9af55fb97ad673fb7a69533eb2f967648a06fa21f8c9bb2cd6d33975716" +checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7" dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", "log", + "memmap2", "slotmap", "tinyvec", "ttf-parser", @@ -777,8 +788,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1139,26 +1152,15 @@ dependencies = [ "byteorder-lite", "color_quant", "gif", - "image-webp 0.2.0", + "image-webp", "num-traits", "png", "ravif", - "rayon", "rgb", "zune-core", "zune-jpeg", ] -[[package]] -name = "image-webp" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" -dependencies = [ - "byteorder-lite", - "quick-error", -] - [[package]] name = "image-webp" version = "0.2.0" @@ -1329,7 +1331,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", - "rayon", ] [[package]] @@ -1338,6 +1339,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" @@ -1669,9 +1679,9 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" dependencies = [ "cc", ] @@ -1782,7 +1792,6 @@ dependencies = [ "loop9", "quick-error", "rav1e", - "rayon", "rgb", ] @@ -1795,26 +1804,6 @@ dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "redox_syscall" version = "0.5.7" @@ -1902,17 +1891,17 @@ dependencies = [ [[package]] name = "resvg" version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958" +source = "git+https://github.com/linebender/resvg?rev=ac767218d791fa42bb6da3a8cc85f0a4454685f1#ac767218d791fa42bb6da3a8cc85f0a4454685f1" dependencies = [ "gif", - "image-webp 0.1.3", + "image-webp", "log", "pico-args", "rgb", "svgtypes", "tiny-skia", "usvg", + "zune-jpeg", ] [[package]] @@ -2011,9 +2000,9 @@ checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "rustybuzz" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -2073,9 +2062,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] @@ -2104,9 +2093,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -2162,6 +2151,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2483,6 +2481,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -2567,6 +2566,21 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.1" @@ -2600,6 +2614,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-core", ] @@ -2621,9 +2636,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ttf-parser" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" +checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e" dependencies = [ "core_maths", ] @@ -2636,15 +2651,15 @@ checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-bidi-mirroring" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" [[package]] name = "unicode-ccc" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" @@ -2690,8 +2705,7 @@ dependencies = [ [[package]] name = "usvg" version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" +source = "git+https://github.com/linebender/resvg?rev=ac767218d791fa42bb6da3a8cc85f0a4454685f1#ac767218d791fa42bb6da3a8cc85f0a4454685f1" dependencies = [ "base64", "data-url", @@ -3137,16 +3151,19 @@ name = "yumechi-no-kuni-proxy-worker" version = "0.1.0" dependencies = [ "axum", + "axum-server", "chumsky", "clap", "console_error_panic_hook", "env_logger", + "fontdb", "futures", + "getrandom", "governor", "image", + "libc", "log", "quote", - "rand_core", "reqwest", "resvg", "serde", diff --git a/Cargo.toml b/Cargo.toml index 09dd893..4a84dda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,8 @@ [package] name = "yumechi-no-kuni-proxy-worker" +description = "A Misskey proxy worker for ゆめちのくに (Yumechi-no-kuni) instance." +repository = "https://forge.yumechi.jp/yume/yumechi-no-kuni-proxy-worker" +license = "Apache-2.0" version = "0.1.0" edition = "2021" authors = [ "eternal-flame-AD " ] @@ -9,7 +12,6 @@ release = false [lib] crate-type = ["cdylib", "rlib"] -build = "build.rs" [profile.release] lto = true @@ -26,15 +28,26 @@ panic = "unwind" [features] default = [] -env-local = ["axum/http1", "axum/http2", "reqwest", "tokio", "env_logger", "governor", "clap", "toml", "image/rayon"] +env-local = ["axum/http1", "axum/http2", + "axum-server", + "reqwest", "tokio", + "env_logger", + "governor", + "clap", "toml", + "image/ico", + "svg-text", "resvg/system-fonts", "resvg/raster-images", "fontdb/fontconfig" + ] +reuse-port = [] cf-worker = ["dep:worker", "dep:worker-macros"] +cf-worker-paid = ["cf-worker", "resvg/raster-images", "resvg/text", "image/ico", "panic-console-error"] panic-console-error = ["dep:console_error_panic_hook"] -apparmor = ["dep:rand_core", "dep:siphasher"] +apparmor = ["dep:siphasher", "dep:libc"] reqwest = ["dep:reqwest"] -svg-text = ["resvg/text"] +svg-text = ["resvg/text", "dep:fontdb"] tokio = ["dep:tokio", "axum/tokio"] env_logger = ["dep:env_logger"] governor = ["dep:governor"] +axum-server = ["dep:axum-server"] [dependencies] worker = { version="0.4.2", features=['http', 'axum'], optional = true } @@ -46,9 +59,9 @@ serde = { version = "1", features = ["derive"] } futures = { version = "0.3.31", default-features = false, features = ["std"] } image = { version = "0.25.5", default-features = false, features = ["avif", "bmp", "gif", "jpeg", "png", "webp"] } reqwest = { version = "0.12.9", features = ["brotli", "gzip", "stream", "zstd"], optional = true } -rand_core = { version = "0.6.4", features = ["getrandom"], optional = true } +getrandom = { version = "0.2", features = ["js"] } siphasher = { version = "1.0.1", optional = true } -tokio = { version = "1.41.1", features = ["rt", "rt-multi-thread", "macros"], optional = true } +tokio = { version = "1.41.1", features = ["rt", "rt-multi-thread", "macros", "sync", "signal"], optional = true } clap = { version = "4.5.20", features = ["derive"], optional = true } toml = { version = "0.8", optional = true } log = "0.4" @@ -58,6 +71,13 @@ resvg = { version = "0.44.0", default-features = false, features = ["gif", "imag thiserror = "2.0" serde_json = "1" wasm-bindgen = { version = "0.2" } +libc = { version = "0.2.162", optional = true } +axum-server = { version = "0.7.1", optional = true } +fontdb = { version = "0.23", optional = true } + +[patch.crates-io] +# licensing and webp dependencies +resvg = { git = "https://github.com/linebender/resvg", rev = "ac767218d791fa42bb6da3a8cc85f0a4454685f1" } [build-dependencies] chumsky = "0.9.3" @@ -67,5 +87,4 @@ serde_json = "1.0.132" [[bin]] name = "yumechi-no-kuni-proxy-worker" -path = "src/main.rs" required-features = ["env-local"] diff --git a/README.md b/README.md index 2f44132..377d10c 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ Work in progress! Currently to do: - [X] Rate-limiting on local deployment (untested) - [X] Read config from Cloudflare - [X] Timing and Rate-limiting headers (some not available on Cloudflare Workers) +- [ ] Cache Results on Cloudflare KV. - [ ] Handle all possible panics reported by Clippy -- [ ] Sandboxing the image rendering +- [X] Sandboxing the image rendering ## Demo @@ -44,28 +45,28 @@ Image: 1. Clone this repository. Load the submodules with `git submodule update --init`. 2. Install Rust and Cargo, using [rustup](https://rustup.rs/) is recommended. - If you do not plan on deploying to Cloudflare Workers, you can remove the `rust-toolchain` file intended to get around [cloudflare/worker-rs#668](https://github.com/cloudflare/workers-rs/issues/668). Otherwise you may need to install that specific version of Rust by `rustup install install $(cat rust-toolchain)`. + If you do not plan on deploying to Cloudflare Workers, you can remove the `rust-toolchain` file intended to get around [cloudflare/worker-rs#668](https://github.com/cloudflare/workers-rs/issues/668). Otherwise you may need to install that specific version of Rust by `rustup install $(cat rust-toolchain)`. 3. IF deploying locally: 1. Edit `local.toml` to your liking. The documentations can be opened with `cargo doc --open`. - 1. Test run with `cargo run --features env-local -- -c local.toml`. + 2. Test run with `cargo run --features env-local -- -c local.toml`. Additional features `apparmor` and `reuse-port` are available for Linux users. - 1. Build with `cargo build --features env-local --profile release-local`. The built binary will be in `target/release-local/yumechi-no-kuni-proxy-worker`. + 3. Build with `cargo build --features env-local --profile release-local`. The built binary will be in `target/release-local/yumechi-no-kuni-proxy-worker`. - 1. The only flag understood is `-c` for the configuration file. The configuration file is in TOML format. However, the `RUST_LOG` environment variable will change the log level. The log level is `info` by default if the environment variable is not set. + 4. The only flag understood is `-c` for the configuration file. The configuration file is in TOML format. However, the `RUST_LOG` environment variable will change the log level. The log level is `info` by default if the environment variable is not set. IF deploying to Cloudflare Workers: - 1. Add the wasm target with `rustup +$(cat rust-toolchain) target add wasm32-unknown-unknown`. + 5. Add the wasm target with `rustup +$(cat rust-toolchain) target add wasm32-unknown-unknown`. - 1. Have a working JS environment. + 6. Have a working JS environment. - 1. Install `wrangler` with you JS package manager of choice. See https://developers.cloudflare.com/workers/wrangler/install-and-update/. `npx` also works. + 7. Install `wrangler` with you JS package manager of choice. See https://developers.cloudflare.com/workers/wrangler/install-and-update/. `npx` also works. - 1. Edit `wrangler.toml` to your liking. Everything in the `[vars]` section maps directly into the `config` section of the TOML configuration file. + 8. Edit `wrangler.toml` to your liking. Everything in the `[vars]` section maps directly into the `config` section of the TOML configuration file. There is a `cf-worker-paid` feature set which enable some additional features that will never fit in the free plan, mainly SVG font rendering and some debugging features. - 1. Test locally with `wrangler dev`. + 9. Test locally with `wrangler dev`. - 1. Deploy with `wrangler deploy --outdir bundled/`. + 10. Deploy with `wrangler deploy --outdir bundled/`. diff --git a/deny.toml b/deny.toml index 02d0c00..e698995 100644 --- a/deny.toml +++ b/deny.toml @@ -97,7 +97,6 @@ allow = [ "Unicode-3.0", "BSD-3-Clause", "BSD-2-Clause", - "MPL-2.0", "Zlib", ] # The confidence threshold for detecting a license from license text. @@ -235,7 +234,7 @@ allow-git = [] [sources.allow-org] # 1 or more github.com organizations to allow git sources for -github = ["eternal-flame-AD"] +github = ["linebender"] # 1 or more gitlab.com organizations to allow git sources for gitlab = [] # 1 or more bitbucket.org organizations to allow git sources for diff --git a/local.toml b/local.toml index c455e02..c3c8b67 100644 --- a/local.toml +++ b/local.toml @@ -3,6 +3,11 @@ enable_cache = false index_redirect = { permanent = false, url = "https://mi.yumechi.jp/" } allow_unknown = false +# you need AppArmor and the policy loaded to use this +[sandbox.apparmor] +serve = "yumechi-no-kuni-proxy-worker//serve" +image_hat = "image" + [fetch] addr_family = "both" allow_http = false diff --git a/mac/apparmor/yumechi-no-kuni-proxy-worker b/mac/apparmor/yumechi-no-kuni-proxy-worker new file mode 100644 index 0000000..8df7f0c --- /dev/null +++ b/mac/apparmor/yumechi-no-kuni-proxy-worker @@ -0,0 +1,88 @@ +abi , + +include + +@{prog} = yumechi-no-kuni-proxy-worker +@{prog_path} = /{,usr/}{,local/}{,s}bin/@{prog} /var/lib/@{prog}/{,bin}/@{prog} + +profile yumechi-no-kuni-proxy-worker @{prog_path} { + include + include + include + include + include + include + + deny capability, + + /{,usr/}lib/**.so.* mr, + + /{,usr/}{,local/}{,s}bin/@{prog} ixr, + owner /var/lib/@{prog}/{,bin}/@{prog} ixr, + + # Configuration file + owner /var/lib/@{prog}/config.toml r, + /etc/@{prog}/config.toml r, + + network tcp, + network udp, + network raw, + deny network (bind) udp, + + change_profile -> yumechi-no-kuni-proxy-worker//serve, + + profile serve { + include + include + include + include + include + include + + deny capability, + + # DNS related + @{etc_ro}/default/nss r, + @{etc_ro}/protocols r, + @{etc_ro}/resolv.conf r, + @{etc_ro}/services r, + @{etc_ro}/host.conf r, + @{etc_ro}/hosts r, + /var/lib/nscd/group r, + /var/lib/nscd/passwd r, + @{run}/nscd/db* r, + @{run}/resolvconf/resolv.conf r, + @{run}/systemd/resolve/resolv.conf r, + @{run}/systemd/resolve/stub-resolv.conf r, + + @{run}/resolvconf/resolv.conf r, + @{run}/systemd/resolve/resolv.conf r, + @{run}/systemd/resolve/stub-resolv.conf r, + + # cgroup + owner @{PROC}/@{pid}/cgroup r, + @{sys}/fs/cgroup/user.slice/cpu.max r, + @{sys}/fs/cgroup/user.slice/user-@{uid}.slice/cpu.max r, + @{sys}/fs/cgroup/user.slice/user-@{uid}.slice/session-*.scope/cpu.max r, + + network tcp, + network udp, + deny network (bind) tcp, + deny network (bind) udp, + + /{,usr/}{,local/}{,s}bin/@{prog} ixr, + owner /var/lib/@{prog}/{,bin}/@{prog} ixr, + + signal (send) peer=yume-proxy-workers//serve//image, + + + ^image { + include + include + include + + signal (receive) peer=yume-proxy-worker//serve, + } + } + +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index ff499a4..fbf162a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,6 +18,10 @@ pub struct Config { /// The listen address pub listen: Option, + /// Sandbox configuration + #[serde(default)] + pub sandbox: SandboxConfig, + /// Send Cache-Control headers pub enable_cache: bool, @@ -38,6 +42,35 @@ pub struct Config { pub rate_limit: RateLimitConfig, } +/// Sandbox configuration +#[derive(Debug, Clone, serde::Deserialize)] +#[non_exhaustive] +pub enum SandboxConfig { + /// No sandboxing + #[serde(rename = "none")] + NoSandbox, + /// AppArmor sandboxing + #[cfg(feature = "apparmor")] + #[serde(rename = "apparmor")] + AppArmor(AppArmorConfig), +} + +impl Default for SandboxConfig { + fn default() -> Self { + Self::NoSandbox + } +} + +/// AppArmor configuration +#[cfg(feature = "apparmor")] +#[derive(Debug, Clone, serde::Deserialize)] +pub struct AppArmorConfig { + /// The profile to transition to after initialization is complete + pub serve: String, + /// The AppArmor hat to use when processing media + pub image_hat: String, +} + /// Governor configuration #[cfg(feature = "governor")] #[derive(Debug, Clone, serde::Deserialize)] @@ -76,6 +109,7 @@ impl Default for Config { Config { listen: Some("127.0.0.1:3000".to_string()), enable_cache: false, + sandbox: SandboxConfig::default(), fetch: FetchConfig { #[cfg(not(feature = "cf-worker"))] addr_family: AddrFamilyConfig::Both, diff --git a/src/lib.rs b/src/lib.rs index 12fa34c..65a8ce1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ use governor::{ RateLimiter, }; use post_process::MediaResponse; -use sandbox::{NoSandbox, Sandboxing}; +use sandbox::Sandboxing; use serde::Deserialize; #[cfg(feature = "cf-worker")] @@ -69,6 +69,7 @@ async fn fetch( _ctx: Context, ) -> WorkerResult> { use fetch::cf_worker::CfWorkerClient; + use sandbox::NoSandbox; use tower_service::Service; let config = match Config::load_from_cf_env(env) { @@ -88,7 +89,9 @@ async fn fetch( #[cfg(any(feature = "cf-worker", feature = "reqwest"))] /// Application Router -pub fn router(config: Config) -> Router +pub fn router( + config: Config, +) -> Router where <::Response as HTTPResponse>::BodyStream: Unpin, { @@ -110,7 +113,7 @@ where ) .with_middleware::(), client: Upstream::new(&config.fetch), - sandbox: NoSandbox, + sandbox: S::new(&config.sandbox), config, }; @@ -151,6 +154,7 @@ where )) .fallback(|| async { ErrorResponse::method_not_allowed() }), ) + .layer(middleware::from_fn(common_security_headers)) .with_state(Arc::clone(&state)); #[cfg(feature = "governor")] @@ -161,8 +165,8 @@ where /// Set the Cache-Control header #[cfg_attr(feature = "cf-worker", worker::send)] -pub async fn set_cache_control( - State(state): State>>, +pub async fn set_cache_control( + State(state): State>>, request: axum::extract::Request, next: axum::middleware::Next, ) -> Response { @@ -185,11 +189,33 @@ pub async fn set_cache_control( resp } +/// Middleware for common security headers +pub async fn common_security_headers( + request: axum::extract::Request, + next: axum::middleware::Next, +) -> Response { + let mut resp = next.run(request).await; + let hdr = resp.headers_mut(); + + if !hdr.contains_key("Content-Security-Policy") { + hdr.insert( + "Content-Security-Policy", + "default-src 'self'".parse().unwrap(), + ); + } + hdr.insert("X-Content-Type-Options", "nosniff".parse().unwrap()); + hdr.insert("X-Frame-Options", "DENY".parse().unwrap()); + hdr.insert("X-XSS-Protection", "1; mode=block".parse().unwrap()); + hdr.insert("Permissions-Policy", "interest-cohort=()".parse().unwrap()); + + resp +} + /// Middleware for rate limiting #[cfg(feature = "governor")] #[cfg_attr(feature = "cf-worker", worker::send)] -pub async fn rate_limit_middleware( - State(state): State>>, +pub async fn rate_limit_middleware( + State(state): State>>, ConnectInfo(addr): ConnectInfo, request: axum::extract::Request, next: axum::middleware::Next, @@ -289,6 +315,7 @@ pub struct ImageOptions { pub avatar: Option, /// See upstream specification #[serde(default, deserialize_with = "deserialize_query_bool")] + #[serde(rename = "static")] pub static_: Option, /// See upstream specification #[serde(default, deserialize_with = "deserialize_query_bool")] @@ -366,6 +393,14 @@ impl Display for ErrorResponse { impl std::error::Error for ErrorResponse {} impl ErrorResponse { + /// Entropy source exhausted + #[must_use] + pub const fn entropy_exhausted() -> Self { + Self { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: Cow::Borrowed("Entropy source exhausted"), + } + } /// Worker is configured improperly #[cfg(feature = "cf-worker")] #[must_use] @@ -563,10 +598,10 @@ pub struct App { #[cfg(any(feature = "cf-worker", feature = "reqwest"))] #[allow(clippy::unused_async)] -impl App { +impl App { /// Root endpoint #[cfg_attr(feature = "cf-worker", worker::send)] - pub async fn index(State(state): State>>) -> Response { + pub async fn index(State(state): State>>) -> Response { match &state.clone().config.index_redirect { IndexConfig::Redirect { permanent, ref url } => { if *permanent { @@ -583,7 +618,7 @@ impl App { async fn proxy_impl<'a>( method: http::Method, filename: Option<&str>, - State(state): State>>, + State(state): State>>, Query(query): Query, info: IncomingInfo, ) -> Result @@ -624,15 +659,23 @@ impl App { .request_upstream(&info, &query.url, false, true, DEFAULT_MAX_REDIRECTS) .await?; - let media = Box::pin(MediaResponse::from_upstream_response( + let media = Box::pin(MediaResponse::from_upstream_response::( resp, state.config.allow_unknown, &state.config.post_process, options, + &state.sandbox, )) .await?; - Ok(media.into_response()) + // Just in case we proxied something we shouldn't + let mut resp = media.into_response(); + resp.headers_mut().insert( + "Content-Security-Policy", + "default-src 'none'; img-src 'self'".parse().unwrap(), + ); + + Ok(resp) } /// Proxy endpoint without filename @@ -640,7 +683,7 @@ impl App { pub async fn proxy_without_filename( method: http::Method, Query(query): Query, - State(state): State>>, + State(state): State>>, info: IncomingInfo, ) -> Result where @@ -666,7 +709,7 @@ impl App { pub async fn proxy_with_filename( method: http::Method, Path(filename): Path, - State(state): State>>, + State(state): State>>, Query(query): Query, info: IncomingInfo, ) -> Result diff --git a/src/main.rs b/src/main.rs index b3c5dd0..cbbb820 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,36 +1,194 @@ -use std::net::SocketAddr; +use std::{ffi::c_int, net::SocketAddr, os::fd::AsRawFd, time::Duration}; use clap::Parser; -use yumechi_no_kuni_proxy_worker::{config::Config, router, sandbox::NoSandbox, Upstream}; +use tokio::sync::mpsc; +use yumechi_no_kuni_proxy_worker::{ + config::{Config, SandboxConfig}, + router, + sandbox::NoSandbox, + Upstream, +}; + +#[cfg(feature = "apparmor")] +use yumechi_no_kuni_proxy_worker::sandbox::apparmor; #[derive(Parser)] struct Cli { - #[clap(short, long)] + #[clap(short, long, default_value = concat!("/etc/", env!("CARGO_PKG_NAME"), "/config.toml"))] config: String, } -#[tokio::main] -async fn main() { +fn interrupt() -> mpsc::Receiver<()> { + let (tx, rx) = mpsc::channel(8); + + #[cfg(target_family = "unix")] + { + let tx_clone = tx.clone(); + tokio::spawn(async move { + let mut sigterm = + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("Failed to listen for SIGTERM"); + while sigterm.recv().await.is_some() { + sigterm.recv().await; + tx_clone.send(()).await.expect("Failed to send interrupt"); + } + }); + } + + tokio::spawn(async move { + loop { + tokio::signal::ctrl_c() + .await + .expect("Failed to listen for Ctrl-C"); + tx.send(()).await.expect("Failed to send interrupt"); + } + }); + + rx +} + +fn main() { if std::env::var("RUST_LOG").is_err() { - std::env::set_var("RUST_LOG", "info"); + std::env::set_var("RUST_LOG", "info,usvg=error"); } env_logger::init(); let cli = Cli::parse(); - let config_bytes = tokio::fs::read_to_string(&cli.config) - .await - .expect("Failed to read config file"); + let config_bytes = std::fs::read_to_string(&cli.config).expect("Failed to read config file"); let config: Config = toml::from_str(&config_bytes).expect("Failed to parse config file"); + log::info!("Config: {:?}", config); let listen = config.listen.clone().expect("No listen address provided"); - let router = router::(config); + #[cfg(feature = "apparmor")] + let mut serve_profile = None; + + let router = match config.sandbox { + #[cfg(feature = "apparmor")] + SandboxConfig::AppArmor(ref prof) => { + serve_profile = Some(prof.serve.clone()); + let (label, mode) = apparmor::get_context(); + log::info!("Detected AppArmor profile: {} ({})", label, mode); + if label.is_empty() || label == "unconfined" { + panic!("Refusing to start in unconfined AppArmor profile when AppArmor is enabled"); + } + router::(config) + } + SandboxConfig::NoSandbox => router::(config), + _ => panic!("Unsupported sandbox configuration, did you forget to enable the feature?"), + }; let ms = router.into_make_service_with_connect_info::(); - let listener = tokio::net::TcpListener::bind(listen) - .await - .expect("Failed to bind listener"); + let listener = std::net::TcpListener::bind(&listen).expect("Failed to bind listener"); + listener + .set_nonblocking(true) + .expect("Failed to set nonblocking socket"); - axum::serve(listener, ms).await.expect("Failed to serve"); + #[cfg(target_os = "linux")] + { + let optval: c_int = 1; + #[cfg(feature = "reuse-port")] + let opts = [libc::SO_REUSEPORT, libc::SO_REUSEADDR]; + #[cfg(not(feature = "reuse-port"))] + let opts = [libc::SO_REUSEADDR]; + + opts.into_iter().for_each(|name| { + if 0 != unsafe { + libc::setsockopt( + listener.as_raw_fd(), + libc::SOL_SOCKET, + name, + &optval as *const _ as *const libc::c_void, + std::mem::size_of_val(&optval) as libc::socklen_t, + ) + } { + panic!("Failed setting sock options.") + } + }); + } + log::info!("Created listener on {}", listen); + + #[cfg(feature = "apparmor")] + if let Some(prof) = serve_profile { + apparmor::change_profile(&prof).expect("Failed to drop privileges."); + + log::info!("Successfully dropped into profile {}", prof); + + let (_, mode) = apparmor::get_context(); + + if !(mode.contains("kill")) + && std::fs::OpenOptions::new() + .read(true) + .open(cli.config) + .is_ok() + { + log::warn!("Config file is readable by the dropped privileges, this is indicative of a misconfiguration."); + } + } + + let runtime = tokio::runtime::Builder::new_multi_thread() + .max_blocking_threads(512) + .enable_all() + .thread_name("serve") + .build() + .expect("Failed to build Tokio Runtime."); + + log::info!("Spawned Tokio reactor."); + + runtime + .block_on(async move { + #[cfg(not(feature = "axum-server"))] + { + let listener = tokio::net::TcpListener::from_std(listener) + .expect("Failed to pass socket to Tokio"); + + log::warn!( + "Built without axum-server feature, using hyper without graceful shutdown" + ); + log::info!("Ready for connections."); + axum::serve(listener, ms).await.expect("Failed to serve"); + return; + } + + #[cfg(feature = "axum-server")] + { + let handle = axum_server::Handle::new(); + let handle_clone = handle.clone(); + + tokio::spawn(async move { + let mut interrupt_level = 0; + let mut signal = interrupt(); + + while let Some(_) = signal.recv().await { + interrupt_level += 1; + match interrupt_level { + 1 => { + log::info!( + "Received interrupt, sending graceful shutdown. (60 sec)" + ); + handle.graceful_shutdown(Some(Duration::from_secs(60))); + } + 2 => { + log::warn!("Received second interrupt, shutting down immediately."); + handle.shutdown(); + } + _ => { + log::warn!("Received third interrupt, forcing exit."); + std::process::exit(1); + } + } + } + }); + + log::info!("Ready for connections."); + axum_server::from_tcp(listener) + .handle(handle_clone) + .serve(ms) + .await + } + }) + .expect("Failed to run Tokio runtime."); + + log::info!("Exiting."); } diff --git a/src/post_process/image_processing.rs b/src/post_process/image_processing.rs index c570bef..5482911 100644 --- a/src/post_process/image_processing.rs +++ b/src/post_process/image_processing.rs @@ -1,4 +1,4 @@ -use std::io::Cursor; +use std::{io::Cursor, sync::Arc}; use image::{ codecs::{png::PngDecoder, webp::WebPDecoder}, @@ -90,6 +90,8 @@ pub enum SvgPostprocessError { /// Preprocesses an SVG image using the given options. By specs SVG must be rasterized as opposed to passing the SVG data directly for security reasons. pub fn postprocess_svg_image( data: &[u8], + #[cfg(feature = "svg-text")] font_data: Option>, + #[cfg(not(feature = "svg-text"))] _font_data: Option<()>, opt: &ImageOptions, ) -> Result { use resvg::{ @@ -101,6 +103,20 @@ pub fn postprocess_svg_image( data, &usvg::Options { default_size: usvg::Size::from_wh(256., 256.).unwrap(), + text_rendering: usvg::TextRendering::OptimizeLegibility, + image_rendering: usvg::ImageRendering::OptimizeSpeed, + #[cfg(feature = "svg-text")] + fontdb: { + let mut db = usvg::fontdb::Database::new(); + #[cfg(not(target_arch = "wasm32"))] + db.load_system_fonts(); + + if let Some(font_data) = font_data { + db.load_font_data(font_data); + } + + Arc::new(db) + }, ..Default::default() }, )?; diff --git a/src/post_process/mod.rs b/src/post_process/mod.rs index 5c9488b..1e5a846 100644 --- a/src/post_process/mod.rs +++ b/src/post_process/mod.rs @@ -3,7 +3,10 @@ use std::{ pin::Pin, }; -use crate::timing::{IntoResponseExt, WithMaybeTimingInfo, WithTimingInfo}; +use crate::{ + sandbox::Sandboxing, + timing::{IntoResponseExt, WithMaybeTimingInfo, WithTimingInfo}, +}; use axum::{ body::{Body, Bytes}, http::HeaderValue, @@ -51,18 +54,30 @@ where } #[cfg(feature = "tokio")] -macro_rules! run_blocking { - ($($tt:tt)*) => { +macro_rules! sandboxed { + ($sandbox:expr => $($tt:tt)*) => { tokio::task::block_in_place(|| { - $($tt)* + let mut key = [0u8; 8]; + getrandom::getrandom(&mut key).map_err(|_| ErrorResponse::entropy_exhausted())?; + let guard = $sandbox.setup(&key); + let ret = $($tt)*; + drop(guard); + ret }) }; } #[cfg(not(feature = "tokio"))] -macro_rules! run_blocking { - ($($tt:tt)*) => { - $($tt)* +macro_rules! sandboxed { + ($sandbox:expr => $($tt:tt)*) => { + { + let mut key = [0u8; 8]; + getrandom::getrandom(&mut key).map_err(|_| ErrorResponse::entropy_exhausted())?; + let guard = $sandbox.setup(&key); + let ret = $($tt)*; + drop(guard); + ret + } } } @@ -71,11 +86,12 @@ where ::BodyStream: Unpin, { /// Create a new media response from a redirect - pub async fn from_upstream_response( + pub async fn from_upstream_response( response: R, allow_unknown: bool, config: &PostProcessConfig, options: ImageOptions, + sandbox: &S, ) -> Result>>, ErrorResponse> { let begin = crate::timing::Instant::now(); const TIME_TO_FIRST_BYTE_KEY: &str = "fetch-first-byte"; @@ -158,8 +174,8 @@ where buf.extend_from_slice(bytes.as_ref()); } - let img = run_blocking!(image_processing::postprocess_svg_image(&buf, &options)) - .map_err(|e| ErrorResponse::postprocess_failed(e.to_string().into()))?; + let img = sandboxed!(sandbox => image_processing::postprocess_svg_image(&buf, None, &options) + .map_err(|e| ErrorResponse::postprocess_failed(e.to_string().into())))?; Ok(MediaResponse::ProcessedStaticImage(StaticImage { data: img, @@ -258,12 +274,11 @@ where if options.format.is_none() { if mime.starts_with("image/png") || mime.starts_with("image/apng") { - let result = run_blocking!( + let result = sandboxed!(sandbox => image_processing::postprocess_png_image(&buf, &options) - ) .map_err(|e| { ErrorResponse::postprocess_failed(e.to_string().into()) - })?; + }))?; return match result { Some(img) => { @@ -286,12 +301,11 @@ where }; } if mime.starts_with("image/webp") { - let result = run_blocking!( + let result = sandboxed!(sandbox => image_processing::postprocess_webp_image(&buf, &options) - ) .map_err(|e| { ErrorResponse::postprocess_failed(e.to_string().into()) - })?; + }))?; return match result { Some(img) => { @@ -315,10 +329,10 @@ where } } - let result = run_blocking!(image_processing::postprocess_static_image( + let result = sandboxed!(sandbox => image_processing::postprocess_static_image( &buf, &options - )) - .map_err(|e| ErrorResponse::postprocess_failed(e.to_string().into()))?; + ) + .map_err(|e| ErrorResponse::postprocess_failed(e.to_string().into())))?; Ok(MediaResponse::ProcessedStaticImage(StaticImage { data: result, diff --git a/src/sandbox.rs b/src/sandbox.rs index da9e79d..2843247 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -1,8 +1,13 @@ +use crate::SandboxConfig; + /// A trait for setting up a thread sandboxing environment pub trait Sandboxing { /// The type of the guard that is returned by the setup function type Guard; + /// Create a new sandboxing environment + fn new(config: &SandboxConfig) -> Self; + /// Set up the sandboxing environment fn setup(&self, key: &[u8]) -> Self::Guard; } @@ -14,6 +19,14 @@ pub struct NoSandbox; impl Sandboxing for NoSandbox { type Guard = (); + fn new(config: &SandboxConfig) -> Self { + #[allow(unreachable_patterns)] + match config { + SandboxConfig::NoSandbox => Self, + _ => panic!("NoSandbox does not support this configuration"), + } + } + fn setup(&self, _key: &[u8]) -> Self::Guard {} } @@ -21,18 +34,68 @@ impl Sandboxing for NoSandbox { #[cfg(feature = "apparmor")] pub mod apparmor { use std::{ - ffi::{c_int, c_ulong, CString}, + ffi::{c_char, c_int, c_ulong, CStr, CString}, hash::Hasher, }; - use rand_core::RngCore; use siphasher::sip::SipHasher; use super::Sandboxing; #[link(name = "apparmor")] extern "C" { - fn aa_change_hat(profile: *const i8, token: c_ulong) -> c_int; + fn aa_is_enabled() -> c_int; + fn aa_change_hat(profile: *const c_char, token: c_ulong) -> c_int; + fn aa_getcon(label: *mut *mut c_char, mode: *mut *mut c_char) -> c_int; + fn aa_change_profile(profile: *const c_char) -> c_int; + } + + /// Check if `AppArmor` is enabled + #[allow(unsafe_code)] + pub fn is_enabled() -> std::io::Result { + let enabled = unsafe { aa_is_enabled() == 1 }; + if enabled { + Ok(true) + } else { + Err(std::io::Error::last_os_error()) + } + } + + /// Change profile now + pub fn change_profile(profile: &str) -> std::io::Result<()> { + let cstr = CString::new(profile).expect("Failed to allocate C String"); + + #[allow(unsafe_code)] + if unsafe { aa_change_profile(cstr.as_ptr()) } != 0 { + return Err(std::io::Error::last_os_error()); + } + + Ok(()) + } + + /// Check current `AppArmor` context + #[allow(unsafe_code)] + pub fn get_context() -> (String, String) { + let mut label = std::ptr::null_mut(); + let mut mode = std::ptr::null_mut(); + let ret = unsafe { aa_getcon(&mut label, &mut mode) }; + assert!( + ret >= 0, + "AppArmor get context failed: {:?}", + std::io::Error::last_os_error() + ); + if label.is_null() { + return (String::new(), String::new()); + } + let label_str = unsafe { CStr::from_ptr(label) } + .to_string_lossy() + .into_owned(); + let mode_bytes = unsafe { + std::slice::from_raw_parts(mode as *const u8, ret as usize - 1 - label_str.len()) + }; + let mode_str = String::from_utf8_lossy(mode_bytes).to_string(); + unsafe { libc::free(label as *mut libc::c_void) }; + (label_str, mode_str) } /// An `AppArmor` hat environment @@ -50,7 +113,7 @@ pub mod apparmor { pub fn new(profile: &str) -> Self { let cstr = std::ffi::CString::new(profile).expect("Invalid profile name"); let mut buf = [0; 16]; - rand_core::OsRng.fill_bytes(&mut buf); + getrandom::getrandom(&mut buf).expect("Failed to get random bytes"); Self { profile: cstr, hasher: SipHasher::new_with_key(&buf), @@ -58,7 +121,7 @@ pub mod apparmor { } } - /// A 'challange' to the less secure task to prove it can still function + /// A 'challenge' to the less secure task to prove it can still function /// The key here is hide the token reasonably well against ROP attacks pub struct AppArmorHandle { hasher: SipHasher, @@ -66,15 +129,16 @@ pub mod apparmor { } impl Drop for AppArmorHandle { - #[allow(clippy::inline_always, reason = "Intentional")] + #[allow(clippy::inline_always)] #[inline(always)] fn drop(&mut self) { self.hasher.write_u64(*self.hash_1x); + let hash_2x = self.hasher.finish(); + log::trace!("Recovering from hat"); #[allow(unsafe_code)] let ret = unsafe { aa_change_hat(std::ptr::null(), hash_2x as c_ulong) }; - // This should never happen as aa_change_hat in return mode will kill on any failed call // This should never happen as aa_change_hat in return mode will kill on any failed call assert!(ret == 0, "AppArmor hat return failed: {ret}"); } @@ -82,20 +146,37 @@ pub mod apparmor { impl Sandboxing for AppArmorHat { type Guard = AppArmorHandle; + + fn new(config: &crate::SandboxConfig) -> Self { + match is_enabled() { + Ok(true) => {} + Ok(false) => panic!("AppArmor is not enabled on this system, did you forget to load the kernel module or enable introspection on the profile?"), + Err(e) => panic!("Failed to check if AppArmor is enabled: {:?}, did you forget to load the kernel module or enable introspection on the profile?", e) + } + match config { + crate::SandboxConfig::AppArmor(aa) => Self::new(&aa.image_hat), + _ => panic!("AppArmorHat does not support this configuration"), + } + } + + #[allow(clippy::inline_always)] + #[inline(always)] fn setup(&self, key: &[u8]) -> Self::Guard { let mut hash = self.hasher; hash.write(key); let hash_1x = hash.finish(); let mut hash_2 = self.hasher; + hash_2.write_u64(hash_1x); let hash_2x = hash_2.finish(); + log::trace!("Entering hat."); #[allow(unsafe_code)] let ret = unsafe { aa_change_hat(self.profile.as_ptr(), hash_2x as c_ulong) }; assert!(ret == 0, "AppArmor hat change failed: {ret}"); AppArmorHandle { - hasher: hash, + hasher: self.hasher, hash_1x: Box::new(hash_1x), } }