diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index a31becc0a0..8c3cd4987e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -193,6 +193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.3", "once_cell", "version_check", "zerocopy", @@ -655,6 +656,16 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "asynk-strim" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52697735bdaac441a29391a9e97102c74c6ef0f9b60a40cf109b1b404e29d2f6" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -721,7 +732,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", @@ -1784,6 +1795,7 @@ name = "codex-network-proxy" version = "0.0.0" dependencies = [ "anyhow", + "async-trait", "clap", "codex-app-server-protocol", "codex-core", @@ -3037,6 +3049,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +[[package]] +name = "endian-type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -3195,6 +3213,9 @@ name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +dependencies = [ + "getrandom 0.2.16", +] [[package]] name = "fax" @@ -3304,13 +3325,13 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" dependencies = [ + "fastrand", "futures-core", "futures-sink", - "nanorand", "spin", ] @@ -4727,6 +4748,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "matchit" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3eede3bdf92f3b4f9dc04072a9ce5ab557d5ec9038773bf9ffcd5588b3cc05b" + [[package]] name = "mcp-types" version = "0.0.0" @@ -4866,15 +4893,6 @@ dependencies = [ "serde", ] -[[package]] -name = "nanorand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -dependencies = [ - "getrandom 0.2.16", -] - [[package]] name = "native-tls" version = "0.2.14" @@ -5824,7 +5842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.104", @@ -5973,16 +5991,29 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" dependencies = [ - "endian-type", + "endian-type 0.1.2", + "nibble_vec", +] + +[[package]] +name = "radix_trie" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a" +dependencies = [ + "endian-type 0.2.0", "nibble_vec", ] [[package]] name = "rama" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9abba26d2a4a60eb94a17224755035ce03f06fd1f3448915cbd8bf02dd838d" +checksum = "66a8e02be6b50e4c35cbba44f15828fa716fa3048bb3b751b8dbda338983346f" dependencies = [ + "ahash", + "base64", + "opentelemetry-otlp", "pin-project-lite", "rama-core", "rama-crypto", @@ -5997,21 +6028,22 @@ dependencies = [ "rama-tcp", "rama-tls-boring", "rama-tls-rustls", - "rama-tower", "rama-ua", "rama-udp", "rama-unix", "rama-utils", "rama-ws", "rustversion", + "serde", "tokio", + "tracing-subscriber", ] [[package]] name = "rama-boring" -version = "0.4.0" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c52217ff947d630f3807cde9f8fe4cc69005b17a1e81fc99fa432cb3fb9c78" +checksum = "288926585d0b8ed1b1dd278a31ea1367007ad0bd4263ca84810e10939c2398a3" dependencies = [ "bitflags 2.10.0", "foreign-types 0.5.0", @@ -6022,11 +6054,10 @@ dependencies = [ [[package]] name = "rama-boring-sys" -version = "0.4.0" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7fd7057828ca9fa148c704f1bf0fff12f6367891212c9c0caad63ae17902a7" +checksum = "421ebb40444a6d740f867a5055a710a73e7cd74b9b389583b45e9b29d1330465" dependencies = [ - "autocfg", "bindgen", "cmake", "fs_extra", @@ -6035,9 +6066,9 @@ dependencies = [ [[package]] name = "rama-boring-tokio" -version = "0.4.0" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b07c71d66f9fa0b52b02055b94c779e98fc27d228e67105c6e528cfd1f19a7a4" +checksum = "913cf3d377b37ff903cd57c2f6133ba7da5b7e0fe94821dec83daa8a024bb6ce" dependencies = [ "rama-boring", "rama-boring-sys", @@ -6046,11 +6077,12 @@ dependencies = [ [[package]] name = "rama-core" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea03771324b3d27a4d5d450bdd288fdeac87638f7cdb5870f3d1da6503542483" +checksum = "0b93751ab27c9d151e84c1100057eab3f2a6a1378bc31b62abd416ecb1847658" dependencies = [ - "async-stream", + "ahash", + "asynk-strim", "bytes", "futures", "parking_lot", @@ -6058,16 +6090,19 @@ dependencies = [ "rama-error", "rama-macros", "rama-utils", + "serde", + "serde_json", "tokio", "tokio-graceful", + "tokio-util", "tracing", ] [[package]] name = "rama-crypto" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91965fc4cb4bc45bd2980f6388f97dbc955bcf164fdbd81fc0e9155c4e9abe2f" +checksum = "d4ea5a793b2fe86a32e11a672d68378a18073701cce9b3f2b477b80b304711c5" dependencies = [ "aws-lc-rs", "base64", @@ -6082,10 +6117,11 @@ dependencies = [ [[package]] name = "rama-dns" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7556de219395b9b170f9fdbff10ee6a1c022749207a2d013c0c89e197bbaae3c" +checksum = "e340fef2799277e204260b17af01bc23604712092eacd6defe40167f304baed8" dependencies = [ + "ahash", "hickory-resolver", "rama-core", "rama-net", @@ -6096,15 +6132,15 @@ dependencies = [ [[package]] name = "rama-error" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b3236ea868a50937223cbe56130b99725235c001ca8886cc01bf599736c9766" +checksum = "3c452aba1beb7e29b873ff32f304536164cffcc596e786921aea64e858ff8f40" [[package]] name = "rama-haproxy" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cf7f5fe1af97475f2e4c7dc6320caca1db2c65336c8ce6513bf5790ca0f75f" +checksum = "3ad683d7f8858bc04c6e62525f598e2289c1cfee6193b9d4e033805552fe3992" dependencies = [ "rama-core", "rama-net", @@ -6114,51 +6150,50 @@ dependencies = [ [[package]] name = "rama-http" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52279e9b6c364ac8db44209707544eac45a3b79c1953cb3cd3985272565d9818" +checksum = "453d60af031e23af2d48995e41b17023f6150044738680508b63671f8d7417dd" dependencies = [ + "ahash", "async-compression", + "base64", "bitflags 2.10.0", "chrono", + "compression-codecs", + "compression-core", "const_format", "csv", "flate2", + "http 1.3.1", "http-range-header", "httpdate", "iri-string", - "matchit", - "mime", - "mime_guess", + "matchit 0.9.1", + "parking_lot", "percent-encoding", "pin-project-lite", + "radix_trie 0.3.0", "rama-core", "rama-error", "rama-http-headers", "rama-http-types", "rama-net", - "rama-ua", "rama-utils", "rand 0.9.2", "rawzip", - "regex", "serde", "serde_html_form", "serde_json", - "smol_str", "tokio", - "tokio-util", "uuid", ] [[package]] name = "rama-http-backend" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f53a4fab6c60854a5d53dccdfdec7b86be697269d523adfff3ff13cca7455a9" +checksum = "f3ff6a3c8ae690be8167e43777ba0bf6b0c8c2f6de165c538666affe2a32fd81" dependencies = [ - "const_format", - "futures", "h2", "pin-project-lite", "rama-core", @@ -6175,38 +6210,40 @@ dependencies = [ [[package]] name = "rama-http-core" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d2a4dc0b90d0ff7a2b7eda9993b51c7ad234b343c700f480ff83f7dea46966" +checksum = "3822be6703e010afec0bcfeb5dbb6e5a3b23ca5689d9b1215b66ce6446653b77" dependencies = [ + "ahash", "atomic-waker", "futures-channel", "httparse", "httpdate", "indexmap 2.12.0", "itoa", + "parking_lot", "pin-project-lite", "rama-core", "rama-http", "rama-http-types", - "rama-net", + "rama-utils", "slab", - "smallvec", "tokio", "tokio-test", - "tokio-util", "want", ] [[package]] name = "rama-http-headers" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c49d5a5f7c95eaffab07609767ed60d52f6c56e7c2115565311545e7141ecd23" +checksum = "9d74fe0cd9bd4440827dc6dc0f504cf66065396532e798891dee2c1b740b2285" dependencies = [ + "ahash", "base64", + "chrono", + "const_format", "httpdate", - "mime", "rama-core", "rama-error", "rama-http-types", @@ -6220,10 +6257,12 @@ dependencies = [ [[package]] name = "rama-http-types" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020137823b0d45de8eb78d261a45dc92a6d2cf763a948fc01ff922cd23eeabd3" +checksum = "b6dae655a72da5f2b97cfacb67960d8b28c5025e62707b4c8c5f0c5c9843a444" dependencies = [ + "ahash", + "bytes", "const_format", "fnv", "http 1.3.1", @@ -6242,30 +6281,29 @@ dependencies = [ "rand 0.9.2", "serde", "serde_json", - "smallvec", - "smol_str", "sync_wrapper", "tokio", - "tokio-util", ] [[package]] name = "rama-macros" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8531184c892978544f6799b132e2ff7c00ceaf8b7d68c2d693ad441d351cbf5" +checksum = "ea18a110bcf21e35c5f194168e6914ccea45ffdd0fea51bc4b169fbeafef6428" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", + "syn 2.0.104", ] [[package]] name = "rama-net" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2ca3d7c32de50880b81fc7d923fa3d9c9cf8d89c6557abdbd7e58758087926" +checksum = "b28ee9e1e5d39264414b71f5c33e7fbb66b382c3fac456fe0daad39cf5509933" dependencies = [ + "ahash", "const_format", "flume", "hex", @@ -6276,14 +6314,13 @@ dependencies = [ "parking_lot", "pin-project-lite", "psl", - "radix_trie", + "radix_trie 0.3.0", "rama-core", "rama-http-types", "rama-macros", "rama-utils", "serde", "sha2", - "smol_str", "socket2 0.6.1", "tokio", "venndb", @@ -6291,9 +6328,9 @@ dependencies = [ [[package]] name = "rama-proxy" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ec0bb1a048cececb6d1a1c0e093ae7e3081d291511a6c2cbdcef8a5975a3c12" +checksum = "149eaf3134c30af80017182f3e659ad05d242e1ade8181ef8035b83859fafac7" dependencies = [ "arc-swap", "base64", @@ -6308,9 +6345,9 @@ dependencies = [ [[package]] name = "rama-socks5" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aeb01a3889de2d2a30e2a9143ffd26e1ed805f04fc477b02883dc0b907533b2" +checksum = "5468b263516daaf258de32542c1974b7cbe962363ad913dcb669f5d46db0ef3e" dependencies = [ "byteorder", "rama-core", @@ -6320,16 +6357,16 @@ dependencies = [ "rama-udp", "rama-utils", "rand 0.9.2", - "smallvec", "tokio", ] [[package]] name = "rama-tcp" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f6b443f656ccf6d4d3cf5e288f73d5484f92c17dbe9836a1cdf13b4c9a4f67" +checksum = "fe60cd604f91196b3659a1b28945add2e8b10bd0b4e6373c93d024fb3197704b" dependencies = [ + "pin-project-lite", "rama-core", "rama-dns", "rama-http-types", @@ -6341,10 +6378,11 @@ dependencies = [ [[package]] name = "rama-tls-boring" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed51b8ae893be304f578b94702f92d564e5d293928349e92d6151d9ab78bf90" +checksum = "def3d5d06d3ca3a2d2e4376cf93de0555cd9c7960f085bf77be9562f5c9ace8f" dependencies = [ + "ahash", "brotli", "flate2", "flume", @@ -6366,59 +6404,48 @@ dependencies = [ [[package]] name = "rama-tls-rustls" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b544da527a10d09ff03d08a120adb0e7ea239fabcbf871863bb7f134d1d70b" +checksum = "536d47f6b269fb20dffd45e4c04aa8b340698b3509326e3c36e444b4f33ce0d6" dependencies = [ "pin-project-lite", "rama-core", + "rama-http-types", "rama-net", "rama-utils", "rcgen", "rustls", "rustls-native-certs", - "rustls-pemfile", "rustls-pki-types", "tokio", "tokio-rustls", "webpki-roots", -] - -[[package]] -name = "rama-tower" -version = "0.3.0-alpha.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2236eed00294ddc68befaaf565e6924d1918c97eaf63d71e2448d6ab961d52c" -dependencies = [ - "rama-core", - "rama-http-types", - "tokio", - "tower-layer", - "tower-service", + "x509-parser", ] [[package]] name = "rama-ua" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496c2926de78dd36d3190fe852b11e39260079b05590c531b187c997078818c" +checksum = "d7abde8e7b428c80c5948885c1ee0492852a21d91844c8414a0de4a8a1d62262" dependencies = [ + "ahash", "itertools 0.14.0", "rama-core", - "rama-http-headers", - "rama-http-types", + "rama-http", "rama-net", "rama-utils", "rand 0.9.2", "serde", + "serde_html_form", "serde_json", ] [[package]] name = "rama-udp" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0460c8db3b802ffa1d1f91bd375ee431da3780d6625d2503891e881ab115339a" +checksum = "36ed05e0ecac73e084e92a3a8b1fbf16fdae8958c506f0f0eada180a2d99eef4" dependencies = [ "rama-core", "rama-net", @@ -6428,35 +6455,39 @@ dependencies = [ [[package]] name = "rama-unix" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa5e3efecbd7ba29667df34c9a949ed36da1d7b4f8f7b43b3b45e6234bbd932f" +checksum = "91acb16d571428ba4cece072dfab90d2667cdfa910a7b3cb4530c3f31542d708" dependencies = [ + "pin-project-lite", "rama-core", "rama-net", "tokio", - "tokio-util", ] [[package]] name = "rama-utils" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fd73071ddb078866fb7fca5def45bbfa12e7ea1cd819a0e3b701bf09ba2c72" +checksum = "bf28b18ba4a57f8334d7992d3f8020194ea359b246ae6f8f98b8df524c7a14ef" dependencies = [ + "const_format", "parking_lot", "pin-project-lite", "rama-macros", + "regex", "serde", + "smallvec", "smol_str", "tokio", + "wildcard", ] [[package]] name = "rama-ws" -version = "0.3.0-alpha.3" +version = "0.3.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6d2cc376163a79bdb2b49e8caad99b6b9c204424f8307c52b04c6627fd8e0b" +checksum = "300b2b6ba51381d1a6918d1142879a8314588f7cf24669f6ba8439d9d19ab486" dependencies = [ "flate2", "rama-core", @@ -6898,15 +6929,6 @@ dependencies = [ "security-framework 3.5.1", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -6950,7 +6972,7 @@ dependencies = [ "log", "memchr", "nix 0.28.0", - "radix_trie", + "radix_trie 0.2.1", "unicode-segmentation", "unicode-width 0.1.14", "utf8parse", @@ -7346,9 +7368,9 @@ dependencies = [ [[package]] name = "serde_html_form" -version = "0.2.8" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +checksum = "2acf96b1d9364968fce46ebb548f1c0e1d7eceae27bdff73865d42e6c7369d94" dependencies = [ "form_urlencoded", "indexmap 2.12.0", @@ -7618,6 +7640,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smawk" @@ -8580,22 +8605,36 @@ dependencies = [ "web-time", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ + "chrono", "matchers", "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -9268,6 +9307,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +[[package]] +name = "wildcard" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9b0540e91e49de3817c314da0dd3bc518093ceacc6ea5327cb0e1eb073e5189" +dependencies = [ + "thiserror 2.0.17", +] + [[package]] name = "wildmatch" version = "2.6.1" diff --git a/codex-rs/network-proxy/Cargo.toml b/codex-rs/network-proxy/Cargo.toml index 525cbf21e2..e998a72445 100644 --- a/codex-rs/network-proxy/Cargo.toml +++ b/codex-rs/network-proxy/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [dependencies] anyhow = { workspace = true } +async-trait = { workspace = true } clap = { workspace = true, features = ["derive"] } codex-app-server-protocol = { workspace = true } codex-core = { workspace = true } @@ -28,7 +29,7 @@ time = { workspace = true } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["fmt"] } -rama = { version = "=0.3.0-alpha.3", default-features = false, features = ["http-full", "proxy-full", "socks5", "rustls"] } +rama = { version = "=0.3.0-alpha.4", default-features = false, features = ["http-full", "proxy-full", "socks5", "rustls"] } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index 1654841690..a0f73d1a35 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -67,6 +67,13 @@ cargo run -p codex-network-proxy -- init cargo run -p codex-network-proxy -- ``` +Optional flags: + +```bash +# Enable SOCKS5 UDP associate support (off by default). +cargo run -p codex-network-proxy -- --enable-socks5-udp +``` + ### 4) Point a client at it For HTTP(S) traffic: @@ -97,6 +104,45 @@ In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed. In addition, H requires MITM to be enabled to allow read-only HTTPS; otherwise the proxy blocks CONNECT with reason `mitm_required`. +## Library API + +`codex-network-proxy` can be embedded as a library with a thin API: + +```rust +use codex_network_proxy::{NetworkProxy, NetworkDecision, NetworkPolicyRequest}; + +let proxy = NetworkProxy::builder() + .http_addr("127.0.0.1:8080".parse()?) + .socks_addr("127.0.0.1:1080".parse()?) + .admin_addr("127.0.0.1:9000".parse()?) + .policy_decider(|request: NetworkPolicyRequest| async move { + // Example: auto-allow when exec policy already approved a command prefix. + if let Some(command) = request.command.as_deref() { + if command.starts_with("curl ") { + return NetworkDecision::Allow; + } + } + NetworkDecision::Deny { + reason: "policy_denied".to_string(), + } + }) + .build() + .await?; + +let handle = proxy.run().await?; +handle.shutdown().await?; +``` + +### Policy hook (exec-policy mapping) + +The proxy exposes a policy hook (`NetworkPolicyDecider`) that can override allowlist-only blocks. +It receives `command` and `exec_policy_hint` fields when supplied by the embedding app. This lets +core map exec approvals to network access, e.g. if a user already approved `curl *` for a session, +the decider can auto-allow network requests originating from that command. + +**Important:** Explicit deny rules still win. The decider only gets a chance to override +`not_allowed` (allowlist misses), not `denied` or `not_allowed_local`. + ## Admin API The admin API is a small HTTP server intended for debugging and runtime adjustments. diff --git a/codex-rs/network-proxy/src/http_proxy.rs b/codex-rs/network-proxy/src/http_proxy.rs index 3f0779cb7a..ba9253d934 100644 --- a/codex-rs/network-proxy/src/http_proxy.rs +++ b/codex-rs/network-proxy/src/http_proxy.rs @@ -1,14 +1,20 @@ use crate::config::NetworkMode; use crate::mitm; +use crate::network_policy::NetworkDecision; +use crate::network_policy::NetworkPolicyDecider; +use crate::network_policy::NetworkPolicyRequest; +use crate::network_policy::NetworkProtocol; +use crate::network_policy::evaluate_host_policy; use crate::policy::normalize_host; use crate::responses::blocked_header_value; use crate::state::AppState; use crate::state::BlockedRequest; use anyhow::Context as _; use anyhow::Result; -use rama::Context; use rama::Layer; use rama::Service; +use rama::extensions::ExtensionsMut; +use rama::extensions::ExtensionsRef; use rama::http::Body; use rama::http::Request; use rama::http::Response; @@ -20,7 +26,7 @@ use rama::http::layer::upgrade::UpgradeLayer; use rama::http::layer::upgrade::Upgraded; use rama::http::matcher::MethodMatcher; use rama::http::server::HttpServer; -use rama::layer::AddExtensionLayer; +use rama::layer::AddInputExtensionLayer; use rama::net::http::RequestContext; use rama::net::proxy::ProxyTarget; use rama::net::stream::SocketInfo; @@ -35,7 +41,11 @@ use tracing::error; use tracing::info; use tracing::warn; -pub async fn run_http_proxy(state: Arc, addr: SocketAddr) -> Result<()> { +pub async fn run_http_proxy( + state: Arc, + addr: SocketAddr, + policy_decider: Option>, +) -> Result<()> { let listener = TcpListener::build() .bind(addr) .await @@ -51,39 +61,40 @@ pub async fn run_http_proxy(state: Arc, addr: SocketAddr) -> Result<() ( UpgradeLayer::new( MethodMatcher::CONNECT, - service_fn(http_connect_accept), + service_fn({ + let policy_decider = policy_decider.clone(); + move |req| http_connect_accept(policy_decider.clone(), req) + }), service_fn(http_connect_proxy), ), RemoveResponseHeaderLayer::hop_by_hop(), RemoveRequestHeaderLayer::hop_by_hop(), ) - .into_layer(service_fn(http_plain_proxy)), + .into_layer(service_fn({ + let policy_decider = policy_decider.clone(); + move |req| http_plain_proxy(policy_decider.clone(), req) + })), ); info!("HTTP proxy listening on {addr}"); listener - .serve(AddExtensionLayer::new(state).into_layer(http_service)) + .serve(AddInputExtensionLayer::new(state).into_layer(http_service)) .await; Ok(()) } -async fn http_connect_accept( - mut ctx: Context, - req: Request, -) -> Result<(Response, Context, Request), Response> -where - S: Clone + Send + Sync + 'static, -{ - let app_state = ctx +async fn http_connect_accept( + policy_decider: Option>, + mut req: Request, +) -> Result<(Response, Request), Response> { + let app_state = req + .extensions() .get::>() .cloned() .ok_or_else(|| text_response(StatusCode::INTERNAL_SERVER_ERROR, "missing state"))?; - let authority = match ctx - .get_or_try_insert_with_ctx::(|ctx| (ctx, &req).try_into()) - .map(|ctx| ctx.authority.clone()) - { + let authority = match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { Ok(authority) => authority, Err(err) => { warn!("CONNECT missing authority: {err}"); @@ -91,15 +102,25 @@ where } }; - let host = normalize_host(&authority.host().to_string()); + let host = normalize_host(&authority.host.to_string()); if host.is_empty() { return Err(text_response(StatusCode::BAD_REQUEST, "invalid host")); } - let client = client_addr(&ctx); + let client = client_addr(&req); - match app_state.host_blocked(&host, authority.port()).await { - Ok((true, reason)) => { + let request = NetworkPolicyRequest::new( + NetworkProtocol::HttpsConnect, + host.clone(), + authority.port, + client.clone(), + Some("CONNECT".to_string()), + None, + None, + ); + + match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { + Ok(NetworkDecision::Deny { reason }) => { let _ = app_state .record_blocked(BlockedRequest::new( host.clone(), @@ -114,7 +135,7 @@ where warn!("CONNECT blocked (client={client}, host={host}, reason={reason})"); return Err(blocked_text(&reason)); } - Ok((false, _)) => { + Ok(NetworkDecision::Allow) => { let client = client.as_deref().unwrap_or_default(); info!("CONNECT allowed (client={client}, host={host})"); } @@ -160,10 +181,10 @@ where return Err(blocked_text("mitm_required")); } - ctx.insert(ProxyTarget(authority)); - ctx.insert(mode); + req.extensions_mut().insert(ProxyTarget(authority)); + req.extensions_mut().insert(mode); if let Some(mitm_state) = mitm_state { - ctx.insert(mitm_state); + req.extensions_mut().insert(mitm_state); } Ok(( @@ -171,54 +192,59 @@ where .status(StatusCode::OK) .body(Body::empty()) .unwrap_or_else(|_| Response::new(Body::empty())), - ctx, req, )) } -async fn http_connect_proxy(ctx: Context, upgraded: Upgraded) -> Result<(), Infallible> -where - S: Clone + Send + Sync + 'static, -{ - let mode = ctx +async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> { + let mode = upgraded + .extensions() .get::() .copied() .unwrap_or(NetworkMode::Full); - let Some(target) = ctx.get::().map(|t| t.0.clone()) else { + let Some(target) = upgraded + .extensions() + .get::() + .map(|t| t.0.clone()) + else { warn!("CONNECT missing proxy target"); return Ok(()); }; - let host = normalize_host(&target.host().to_string()); + let host = normalize_host(&target.host.to_string()); - if ctx.get::>().is_some() { - let port = target.port(); + if upgraded + .extensions() + .get::>() + .is_some() + { + let port = target.port; info!("CONNECT MITM enabled (host={host}, port={port}, mode={mode:?})"); - if let Err(err) = mitm::mitm_tunnel(ctx, upgraded).await { + if let Err(err) = mitm::mitm_tunnel(upgraded).await { warn!("MITM tunnel error: {err}"); } return Ok(()); } let forwarder = Forwarder::ctx(); - if let Err(err) = forwarder.serve(ctx, upgraded).await { + if let Err(err) = forwarder.serve(upgraded).await { warn!("tunnel error: {err}"); } Ok(()) } -async fn http_plain_proxy(mut ctx: Context, req: Request) -> Result -where - S: Clone + Send + Sync + 'static, -{ - let app_state = match ctx.get::>().cloned() { +async fn http_plain_proxy( + policy_decider: Option>, + req: Request, +) -> Result { + let app_state = match req.extensions().get::>().cloned() { Some(state) => state, None => { error!("missing app state"); return Ok(text_response(StatusCode::INTERNAL_SERVER_ERROR, "error")); } }; - let client = client_addr(&ctx); + let client = client_addr(&req); let method_allowed = match app_state.method_allowed(req.method().as_str()).await { Ok(allowed) => allowed, @@ -263,7 +289,7 @@ where Ok(true) => { let client = client.as_deref().unwrap_or_default(); info!("unix socket allowed (client={client}, path={socket_path})"); - match proxy_via_unix_socket(ctx, req, &socket_path).await { + match proxy_via_unix_socket(req, &socket_path).await { Ok(resp) => return Ok(resp), Err(err) => { warn!("unix socket proxy failed: {err}"); @@ -286,21 +312,28 @@ where } } - let authority = match ctx - .get_or_try_insert_with_ctx::(|ctx| (ctx, &req).try_into()) - .map(|ctx| ctx.authority.clone()) - { + let authority = match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { Ok(authority) => authority, Err(err) => { warn!("missing host: {err}"); return Ok(text_response(StatusCode::BAD_REQUEST, "missing host")); } }; - let host = normalize_host(&authority.host().to_string()); - let port = authority.port(); + let host = normalize_host(&authority.host.to_string()); + let port = authority.port; - match app_state.host_blocked(&host, port).await { - Ok((true, reason)) => { + let request = NetworkPolicyRequest::new( + NetworkProtocol::Http, + host.clone(), + port, + client.clone(), + Some(req.method().as_str().to_string()), + None, + None, + ); + + match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { + Ok(NetworkDecision::Deny { reason }) => { let _ = app_state .record_blocked(BlockedRequest::new( host.clone(), @@ -315,7 +348,7 @@ where warn!("request blocked (client={client}, host={host}, reason={reason})"); return Ok(json_blocked(&host, &reason)); } - Ok((false, _)) => {} + Ok(NetworkDecision::Allow) => {} Err(err) => { error!("failed to evaluate host for {host}: {err}"); return Ok(text_response(StatusCode::INTERNAL_SERVER_ERROR, "error")); @@ -346,7 +379,7 @@ where info!("request allowed (client={client}, host={host}, method={method})"); let client = EasyHttpWebClient::default(); - match client.serve(ctx, req).await { + match client.serve(req).await { Ok(resp) => Ok(resp), Err(err) => { warn!("upstream request failed: {err}"); @@ -355,30 +388,24 @@ where } } -async fn proxy_via_unix_socket( - ctx: Context, - req: Request, - socket_path: &str, -) -> Result -where - S: Clone + Send + Sync + 'static, -{ +async fn proxy_via_unix_socket(req: Request, socket_path: &str) -> Result { #[cfg(target_os = "macos")] { use rama::unix::client::UnixConnector; - let client = EasyHttpWebClient::builder() + let client = EasyHttpWebClient::connector_builder() .with_custom_transport_connector(UnixConnector::fixed(socket_path)) .without_tls_proxy_support() .without_proxy_support() .without_tls_support() - .build(); + .with_default_http_connector() + .build_client(); let (mut parts, body) = req.into_parts(); let path = parts .uri .path_and_query() - .map(rama::http::dep::http::uri::PathAndQuery::as_str) + .map(rama::http::uri::PathAndQuery::as_str) .unwrap_or("/"); parts.uri = path .parse() @@ -386,19 +413,20 @@ where parts.headers.remove("x-unix-socket"); let req = Request::from_parts(parts, body); - Ok(client.serve(ctx, req).await?) + Ok(client.serve(req).await?) } #[cfg(not(target_os = "macos"))] { let _ = req; - let _ = ctx; let _ = socket_path; Err(anyhow::anyhow!("unix sockets not supported")) } } -fn client_addr(ctx: &Context) -> Option { - ctx.get::() +fn client_addr(input: &T) -> Option { + input + .extensions() + .get::() .map(|info| info.peer_addr().to_string()) } diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index db389e4717..6edfcee3d4 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -3,55 +3,31 @@ mod config; mod http_proxy; mod init; mod mitm; +mod network_policy; mod policy; +mod proxy; mod responses; mod socks5; mod state; -use crate::state::AppState; use anyhow::Result; -use clap::Parser; -use clap::Subcommand; -use std::net::SocketAddr; -use std::sync::Arc; -use tracing::warn; - -#[derive(Debug, Clone, Parser)] -#[command(name = "codex-network-proxy", about = "Codex network sandbox proxy")] -pub struct Args { - #[command(subcommand)] - pub command: Option, -} - -#[derive(Debug, Clone, Subcommand)] -pub enum Command { - /// Initialize the Codex network proxy directories (e.g. MITM cert paths). - Init, -} +pub use network_policy::NetworkDecision; +pub use network_policy::NetworkPolicyDecider; +pub use network_policy::NetworkPolicyRequest; +pub use network_policy::NetworkProtocol; +pub use proxy::Args; +pub use proxy::Command; +pub use proxy::NetworkProxy; +pub use proxy::NetworkProxyBuilder; +pub use proxy::NetworkProxyHandle; +pub use proxy::run_init; pub async fn run_main(args: Args) -> Result<()> { - tracing_subscriber::fmt::init(); - if let Some(Command::Init) = args.command { - init::run_init()?; + run_init()?; return Ok(()); } - if cfg!(not(target_os = "macos")) { - warn!("allowUnixSockets is macOS-only; requests will be rejected on this platform"); - } - - let state = Arc::new(AppState::new().await?); - let runtime = config::resolve_runtime(&state.current_cfg().await?); - - let http_addr: SocketAddr = runtime.http_addr; - let socks_addr: SocketAddr = runtime.socks_addr; - let admin_addr: SocketAddr = runtime.admin_addr; - - let http_task = http_proxy::run_http_proxy(state.clone(), http_addr); - let socks_task = socks5::run_socks5(state.clone(), socks_addr); - let admin_task = admin::run_admin_api(state.clone(), admin_addr); - - tokio::try_join!(http_task, socks_task, admin_task)?; - Ok(()) + let proxy = NetworkProxy::from_cli_args(args).await?; + proxy.run().await?.wait().await } diff --git a/codex-rs/network-proxy/src/main.rs b/codex-rs/network-proxy/src/main.rs index faea984c92..f76a3ac2b4 100644 --- a/codex-rs/network-proxy/src/main.rs +++ b/codex-rs/network-proxy/src/main.rs @@ -1,8 +1,20 @@ use anyhow::Result; use clap::Parser; use codex_network_proxy::Args; +use codex_network_proxy::Command; +use codex_network_proxy::NetworkProxy; +use codex_network_proxy::run_init; #[tokio::main] async fn main() -> Result<()> { - codex_network_proxy::run_main(Args::parse()).await + tracing_subscriber::fmt::init(); + + let args = Args::parse(); + if let Some(Command::Init) = args.command { + run_init()?; + return Ok(()); + } + + let proxy = NetworkProxy::from_cli_args(args).await?; + proxy.run().await?.wait().await } diff --git a/codex-rs/network-proxy/src/mitm.rs b/codex-rs/network-proxy/src/mitm.rs index db342c1beb..c5154231f4 100644 --- a/codex-rs/network-proxy/src/mitm.rs +++ b/codex-rs/network-proxy/src/mitm.rs @@ -8,12 +8,12 @@ use crate::state::BlockedRequest; use anyhow::Context as _; use anyhow::Result; use anyhow::anyhow; -use rama::Context; use rama::Layer; use rama::Service; use rama::bytes::Bytes; use rama::error::BoxError; use rama::error::OpaqueError; +use rama::extensions::ExtensionsRef; use rama::futures::stream::Stream; use rama::http::Body; use rama::http::HeaderValue; @@ -29,6 +29,7 @@ use rama::http::layer::upgrade::Upgraded; use rama::http::server::HttpServer; use rama::net::proxy::ProxyTarget; use rama::net::stream::SocketInfo; +use rama::rt::Executor; use rama::service::service_fn; use rama::tls::rustls::dep::pki_types::CertificateDer; use rama::tls::rustls::dep::pki_types::PrivateKeyDer; @@ -63,7 +64,7 @@ use rcgen_rama::SanType; pub struct MitmState { issuer: Issuer<'static, KeyPair>, - upstream: rama::service::BoxService<(), Request, Response, OpaqueError>, + upstream: rama::service::BoxService, inspect: bool, max_body_bytes: usize, } @@ -88,16 +89,17 @@ impl MitmState { let issuer: Issuer<'static, KeyPair> = Issuer::from_ca_cert_pem(&ca_cert_pem, ca_key).context("failed to parse CA cert")?; - let tls_config = rama::tls::rustls::client::TlsConnectorData::new_http_auto() + let tls_config = rama::tls::rustls::client::TlsConnectorData::try_new_http_auto() .context("create upstream TLS config")?; - let upstream: rama::service::BoxService<(), Request, Response, OpaqueError> = - EasyHttpWebClient::builder() + let upstream: rama::service::BoxService = + EasyHttpWebClient::connector_builder() // Use a direct transport connector (no upstream proxy) to avoid proxy loops. .with_default_transport_connector() .without_tls_proxy_support() .without_proxy_support() .with_tls_support_using_rustls(Some(tls_config)) - .build() + .with_default_http_connector() + .build_client() .boxed(); Ok(Self { @@ -135,23 +137,26 @@ impl MitmState { } } -pub async fn mitm_tunnel(ctx: Context, upgraded: Upgraded) -> Result<()> -where - S: Clone + Send + Sync + 'static, -{ - let state = ctx +pub async fn mitm_tunnel(upgraded: Upgraded) -> Result<()> { + let state = upgraded + .extensions() .get::>() .cloned() .context("missing MITM state")?; - let target = ctx + let target = upgraded + .extensions() .get::() .context("missing proxy target")? .0 .clone(); - let host = normalize_host(&target.host().to_string()); + let host = normalize_host(&target.host.to_string()); let acceptor_data = state.tls_acceptor_data_for_host(&host)?; - let executor = ctx.executor().clone(); + let executor = upgraded + .extensions() + .get::() + .cloned() + .unwrap_or_default(); let http_service = HttpServer::auto(executor).service( ( @@ -166,20 +171,14 @@ where .into_layer(http_service); https_service - .serve(ctx, upgraded) + .serve(upgraded) .await .map_err(|err| anyhow!("MITM serve error: {err}"))?; Ok(()) } -async fn handle_mitm_request( - ctx: Context, - req: Request, -) -> Result -where - S: Clone + Send + Sync + 'static, -{ - let response = match forward_request(ctx, req).await { +async fn handle_mitm_request(req: Request) -> Result { + let response = match forward_request(req).await { Ok(resp) => resp, Err(err) => { warn!("MITM upstream request failed: {err}"); @@ -189,27 +188,28 @@ where Ok(response) } -async fn forward_request(ctx: Context, req: Request) -> Result -where - S: Clone + Send + Sync + 'static, -{ - let target = ctx +async fn forward_request(req: Request) -> Result { + let target = req + .extensions() .get::() .context("missing proxy target")? .0 .clone(); - let target_host = normalize_host(&target.host().to_string()); - let target_port = target.port(); - let mode = ctx + let target_host = normalize_host(&target.host.to_string()); + let target_port = target.port; + let mode = req + .extensions() .get::() .copied() .unwrap_or(NetworkMode::Full); - let mitm = ctx + let mitm = req + .extensions() .get::>() .cloned() .context("missing MITM state")?; - let app_state = ctx + let app_state = req + .extensions() .get::>() .cloned() .context("missing app state")?; @@ -223,7 +223,8 @@ where let method = req.method().as_str().to_string(); let path = path_and_query(req.uri()); - let client = ctx + let client = req + .extensions() .get::() .map(|info| info.peer_addr().to_string()); @@ -276,10 +277,7 @@ where }; let upstream_req = Request::from_parts(parts, body); - let upstream_resp = mitm - .upstream - .serve(ctx.map_state(|_| ()), upstream_req) - .await?; + let upstream_resp = mitm.upstream.serve(upstream_req).await?; respond_with_inspection( upstream_resp, inspect, @@ -428,7 +426,7 @@ fn build_https_uri(authority: &str, path: &str) -> Result { fn path_and_query(uri: &Uri) -> String { uri.path_and_query() - .map(rama::http::dep::http::uri::PathAndQuery::as_str) + .map(rama::http::uri::PathAndQuery::as_str) .unwrap_or("/") .to_string() } diff --git a/codex-rs/network-proxy/src/network_policy.rs b/codex-rs/network-proxy/src/network_policy.rs new file mode 100644 index 0000000000..1684f7db31 --- /dev/null +++ b/codex-rs/network-proxy/src/network_policy.rs @@ -0,0 +1,113 @@ +use crate::state::AppState; +use anyhow::Result; +use async_trait::async_trait; +use std::future::Future; +use std::sync::Arc; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NetworkProtocol { + Http, + HttpsConnect, + Socks5Tcp, + Socks5Udp, +} + +#[derive(Clone, Debug)] +pub struct NetworkPolicyRequest { + pub protocol: NetworkProtocol, + pub host: String, + pub port: u16, + pub client_addr: Option, + pub method: Option, + pub command: Option, + pub exec_policy_hint: Option, +} + +impl NetworkPolicyRequest { + #[must_use] + pub fn new( + protocol: NetworkProtocol, + host: String, + port: u16, + client_addr: Option, + method: Option, + command: Option, + exec_policy_hint: Option, + ) -> Self { + Self { + protocol, + host, + port, + client_addr, + method, + command, + exec_policy_hint, + } + } +} + +#[derive(Clone, Debug)] +pub enum NetworkDecision { + Allow, + Deny { reason: String }, +} + +impl NetworkDecision { + #[must_use] + pub fn deny(reason: impl Into) -> Self { + let reason = reason.into(); + let reason = if reason.is_empty() { + "policy_denied".to_string() + } else { + reason + }; + Self::Deny { reason } + } +} + +/// Decide whether a network request should be allowed. +/// +/// If `command` or `exec_policy_hint` is provided, callers can map exec-policy +/// approvals to network access (e.g., allow all requests for commands matching +/// approved prefixes like `curl *`). +#[async_trait] +pub trait NetworkPolicyDecider: Send + Sync + 'static { + async fn decide(&self, req: NetworkPolicyRequest) -> NetworkDecision; +} + +#[async_trait] +impl NetworkPolicyDecider for Arc { + async fn decide(&self, req: NetworkPolicyRequest) -> NetworkDecision { + (**self).decide(req).await + } +} + +#[async_trait] +impl NetworkPolicyDecider for F +where + F: Fn(NetworkPolicyRequest) -> Fut + Send + Sync + 'static, + Fut: Future + Send, +{ + async fn decide(&self, req: NetworkPolicyRequest) -> NetworkDecision { + (self)(req).await + } +} + +pub(crate) async fn evaluate_host_policy( + state: &AppState, + decider: Option<&Arc>, + request: &NetworkPolicyRequest, +) -> Result { + let (blocked, reason) = state.host_blocked(&request.host, request.port).await?; + if !blocked { + return Ok(NetworkDecision::Allow); + } + + if reason == "not_allowed" + && let Some(decider) = decider + { + return Ok(decider.decide(request.clone()).await); + } + + Ok(NetworkDecision::deny(reason)) +} diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs new file mode 100644 index 0000000000..0e8a74e83f --- /dev/null +++ b/codex-rs/network-proxy/src/proxy.rs @@ -0,0 +1,181 @@ +use crate::admin; +use crate::config; +use crate::http_proxy; +use crate::init; +use crate::network_policy::NetworkPolicyDecider; +use crate::socks5; +use crate::state::AppState; +use anyhow::Result; +use clap::Parser; +use clap::Subcommand; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::task::JoinHandle; +use tracing::warn; + +#[derive(Debug, Clone, Parser)] +#[command(name = "codex-network-proxy", about = "Codex network sandbox proxy")] +pub struct Args { + #[command(subcommand)] + pub command: Option, + /// Enable SOCKS5 UDP associate support (default: disabled). + #[arg(long, default_value_t = false)] + pub enable_socks5_udp: bool, +} + +#[derive(Debug, Clone, Subcommand)] +pub enum Command { + /// Initialize the Codex network proxy directories (e.g. MITM cert paths). + Init, +} + +#[derive(Clone, Default)] +pub struct NetworkProxyBuilder { + state: Option>, + http_addr: Option, + socks_addr: Option, + admin_addr: Option, + policy_decider: Option>, + enable_socks5_udp: bool, +} + +impl NetworkProxyBuilder { + #[must_use] + pub fn state(mut self, state: Arc) -> Self { + self.state = Some(state); + self + } + + #[must_use] + pub fn http_addr(mut self, addr: SocketAddr) -> Self { + self.http_addr = Some(addr); + self + } + + #[must_use] + pub fn socks_addr(mut self, addr: SocketAddr) -> Self { + self.socks_addr = Some(addr); + self + } + + #[must_use] + pub fn admin_addr(mut self, addr: SocketAddr) -> Self { + self.admin_addr = Some(addr); + self + } + + #[must_use] + pub fn policy_decider(mut self, decider: D) -> Self + where + D: NetworkPolicyDecider, + { + self.policy_decider = Some(Arc::new(decider)); + self + } + + #[must_use] + pub fn policy_decider_arc(mut self, decider: Arc) -> Self { + self.policy_decider = Some(decider); + self + } + + #[must_use] + pub fn enable_socks5_udp(mut self, enabled: bool) -> Self { + self.enable_socks5_udp = enabled; + self + } + + pub async fn build(self) -> Result { + let state = match self.state { + Some(state) => state, + None => Arc::new(AppState::new().await?), + }; + let runtime = config::resolve_runtime(&state.current_cfg().await?); + + Ok(NetworkProxy { + state, + http_addr: self.http_addr.unwrap_or(runtime.http_addr), + socks_addr: self.socks_addr.unwrap_or(runtime.socks_addr), + admin_addr: self.admin_addr.unwrap_or(runtime.admin_addr), + policy_decider: self.policy_decider, + enable_socks5_udp: self.enable_socks5_udp, + }) + } +} + +#[derive(Clone)] +pub struct NetworkProxy { + state: Arc, + http_addr: SocketAddr, + socks_addr: SocketAddr, + admin_addr: SocketAddr, + policy_decider: Option>, + enable_socks5_udp: bool, +} + +impl NetworkProxy { + #[must_use] + pub fn builder() -> NetworkProxyBuilder { + NetworkProxyBuilder::default() + } + + pub async fn from_cli_args(args: Args) -> Result { + let mut builder = Self::builder(); + builder = builder.enable_socks5_udp(args.enable_socks5_udp); + builder.build().await + } + + pub async fn run(&self) -> Result { + if cfg!(not(target_os = "macos")) { + warn!("allowUnixSockets is macOS-only; requests will be rejected on this platform"); + } + + let http_task = tokio::spawn(http_proxy::run_http_proxy( + self.state.clone(), + self.http_addr, + self.policy_decider.clone(), + )); + let socks_task = tokio::spawn(socks5::run_socks5( + self.state.clone(), + self.socks_addr, + self.policy_decider.clone(), + self.enable_socks5_udp, + )); + let admin_task = tokio::spawn(admin::run_admin_api(self.state.clone(), self.admin_addr)); + + Ok(NetworkProxyHandle { + http_task, + socks_task, + admin_task, + }) + } +} + +pub struct NetworkProxyHandle { + http_task: JoinHandle>, + socks_task: JoinHandle>, + admin_task: JoinHandle>, +} + +impl NetworkProxyHandle { + pub async fn wait(self) -> Result<()> { + self.http_task.await??; + self.socks_task.await??; + self.admin_task.await??; + Ok(()) + } + + pub async fn shutdown(self) -> Result<()> { + self.http_task.abort(); + self.socks_task.abort(); + self.admin_task.abort(); + let _ = self.http_task.await; + let _ = self.socks_task.await; + let _ = self.admin_task.await; + Ok(()) + } +} + +pub fn run_init() -> Result<()> { + init::run_init() +} diff --git a/codex-rs/network-proxy/src/socks5.rs b/codex-rs/network-proxy/src/socks5.rs index 89eaf816ee..22fe84fce5 100644 --- a/codex-rs/network-proxy/src/socks5.rs +++ b/codex-rs/network-proxy/src/socks5.rs @@ -1,16 +1,24 @@ use crate::config::NetworkMode; +use crate::network_policy::NetworkDecision; +use crate::network_policy::NetworkPolicyDecider; +use crate::network_policy::NetworkPolicyRequest; +use crate::network_policy::NetworkProtocol; +use crate::network_policy::evaluate_host_policy; use crate::policy::normalize_host; use crate::state::AppState; use crate::state::BlockedRequest; use anyhow::Context as _; use anyhow::Result; -use rama::Context; use rama::Layer; use rama::Service; -use rama::layer::AddExtensionLayer; +use rama::extensions::ExtensionsRef; +use rama::layer::AddInputExtensionLayer; use rama::net::stream::SocketInfo; use rama::proxy::socks5::Socks5Acceptor; use rama::proxy::socks5::server::DefaultConnector; +use rama::proxy::socks5::server::DefaultUdpRelay; +use rama::proxy::socks5::server::udp::RelayRequest; +use rama::proxy::socks5::server::udp::RelayResponse; use rama::service::service_fn; use rama::tcp::client::Request as TcpRequest; use rama::tcp::client::service::TcpConnector; @@ -22,7 +30,12 @@ use tracing::error; use tracing::info; use tracing::warn; -pub async fn run_socks5(state: Arc, addr: SocketAddr) -> Result<()> { +pub async fn run_socks5( + state: Arc, + addr: SocketAddr, + policy_decider: Option>, + enable_socks5_udp: bool, +) -> Result<()> { let listener = TcpListener::build() .bind(addr) .await @@ -44,80 +57,193 @@ pub async fn run_socks5(state: Arc, addr: SocketAddr) -> Result<()> { } let tcp_connector = TcpConnector::default(); - let policy_tcp_connector = service_fn(move |ctx: Context<()>, req: TcpRequest| { - let tcp_connector = tcp_connector.clone(); - async move { - let app_state = ctx - .get::>() - .cloned() - .ok_or_else(|| io::Error::other("missing state"))?; + let policy_tcp_connector = service_fn({ + let policy_decider = policy_decider.clone(); + move |req: TcpRequest| { + let tcp_connector = tcp_connector.clone(); + let policy_decider = policy_decider.clone(); + async move { + let app_state = req + .extensions() + .get::>() + .cloned() + .ok_or_else(|| io::Error::other("missing state"))?; - let host = normalize_host(&req.authority().host().to_string()); - let port = req.authority().port(); - let client = ctx - .get::() - .map(|info| info.peer_addr().to_string()); + let host = normalize_host(&req.authority.host.to_string()); + let port = req.authority.port; + let client = req + .extensions() + .get::() + .map(|info| info.peer_addr().to_string()); + match app_state.network_mode().await { + Ok(NetworkMode::Limited) => { + let _ = app_state + .record_blocked(BlockedRequest::new( + host.clone(), + "method_not_allowed".to_string(), + client.clone(), + None, + Some(NetworkMode::Limited), + "socks5".to_string(), + )) + .await; + let client = client.as_deref().unwrap_or_default(); + warn!( + "SOCKS blocked by method policy (client={client}, host={host}, mode=limited, allowed_methods=GET, HEAD, OPTIONS)" + ); + return Err( + io::Error::new(io::ErrorKind::PermissionDenied, "blocked").into() + ); + } + Ok(NetworkMode::Full) => {} + Err(err) => { + error!("failed to evaluate method policy: {err}"); + return Err(io::Error::other("proxy error").into()); + } + } - match app_state.network_mode().await { - Ok(NetworkMode::Limited) => { - let _ = app_state - .record_blocked(BlockedRequest::new( - host.clone(), - "method_not_allowed".to_string(), - client.clone(), - None, - Some(NetworkMode::Limited), - "socks5".to_string(), - )) - .await; - let client = client.as_deref().unwrap_or_default(); - warn!( - "SOCKS blocked by method policy (client={client}, host={host}, mode=limited, allowed_methods=GET, HEAD, OPTIONS)" - ); - return Err(io::Error::new(io::ErrorKind::PermissionDenied, "blocked").into()); - } - Ok(NetworkMode::Full) => {} - Err(err) => { - error!("failed to evaluate method policy: {err}"); - return Err(io::Error::other("proxy error").into()); + let request = NetworkPolicyRequest::new( + NetworkProtocol::Socks5Tcp, + host.clone(), + port, + client.clone(), + None, + None, + None, + ); + + match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { + Ok(NetworkDecision::Deny { reason }) => { + let _ = app_state + .record_blocked(BlockedRequest::new( + host.clone(), + reason.clone(), + client.clone(), + None, + None, + "socks5".to_string(), + )) + .await; + let client = client.as_deref().unwrap_or_default(); + warn!("SOCKS blocked (client={client}, host={host}, reason={reason})"); + return Err( + io::Error::new(io::ErrorKind::PermissionDenied, "blocked").into() + ); + } + Ok(NetworkDecision::Allow) => { + let client = client.as_deref().unwrap_or_default(); + info!("SOCKS allowed (client={client}, host={host}, port={port})"); + } + Err(err) => { + error!("failed to evaluate host: {err}"); + return Err(io::Error::other("proxy error").into()); + } } + + tcp_connector.serve(req).await } - - match app_state.host_blocked(&host, port).await { - Ok((true, reason)) => { - let _ = app_state - .record_blocked(BlockedRequest::new( - host.clone(), - reason.clone(), - client.clone(), - None, - None, - "socks5".to_string(), - )) - .await; - let client = client.as_deref().unwrap_or_default(); - warn!("SOCKS blocked (client={client}, host={host}, reason={reason})"); - return Err(io::Error::new(io::ErrorKind::PermissionDenied, "blocked").into()); - } - Ok((false, _)) => { - let client = client.as_deref().unwrap_or_default(); - info!("SOCKS allowed (client={client}, host={host}, port={port})"); - } - Err(err) => { - error!("failed to evaluate host: {err}"); - return Err(io::Error::other("proxy error").into()); - } - } - - tcp_connector.serve(ctx, req).await } }); let socks_connector = DefaultConnector::default().with_connector(policy_tcp_connector); - let socks_acceptor = Socks5Acceptor::new().with_connector(socks_connector); + let base = Socks5Acceptor::new().with_connector(socks_connector); - listener - .serve(AddExtensionLayer::new(state).into_layer(socks_acceptor)) - .await; + if enable_socks5_udp { + let udp_state = state.clone(); + let udp_decider = policy_decider.clone(); + let udp_relay = DefaultUdpRelay::default().with_async_inspector(service_fn( + move |request: RelayRequest| { + let udp_state = udp_state.clone(); + let udp_decider = udp_decider.clone(); + async move { + let RelayRequest { + server_address, + payload, + extensions, + .. + } = request; + + let host = normalize_host(&server_address.ip_addr.to_string()); + let port = server_address.port; + let client = extensions + .get::() + .map(|info| info.peer_addr().to_string()); + + match udp_state.network_mode().await { + Ok(NetworkMode::Limited) => { + let _ = udp_state + .record_blocked(BlockedRequest::new( + host.clone(), + "method_not_allowed".to_string(), + client.clone(), + None, + Some(NetworkMode::Limited), + "socks5-udp".to_string(), + )) + .await; + return Ok(RelayResponse { + maybe_payload: None, + extensions, + }); + } + Ok(NetworkMode::Full) => {} + Err(err) => { + error!("failed to evaluate method policy: {err}"); + return Err(io::Error::other("proxy error")); + } + } + + let request = NetworkPolicyRequest::new( + NetworkProtocol::Socks5Udp, + host.clone(), + port, + client.clone(), + None, + None, + None, + ); + + match evaluate_host_policy(&udp_state, udp_decider.as_ref(), &request).await { + Ok(NetworkDecision::Deny { reason }) => { + let _ = udp_state + .record_blocked(BlockedRequest::new( + host.clone(), + reason.clone(), + client.clone(), + None, + None, + "socks5-udp".to_string(), + )) + .await; + let client = client.as_deref().unwrap_or_default(); + warn!( + "SOCKS UDP blocked (client={client}, host={host}, reason={reason})" + ); + Ok(RelayResponse { + maybe_payload: None, + extensions, + }) + } + Ok(NetworkDecision::Allow) => Ok(RelayResponse { + maybe_payload: Some(payload), + extensions, + }), + Err(err) => { + error!("failed to evaluate UDP host: {err}"); + Err(io::Error::other("proxy error")) + } + } + } + }, + )); + let socks_acceptor = base.with_udp_associator(udp_relay); + listener + .serve(AddInputExtensionLayer::new(state).into_layer(socks_acceptor)) + .await; + } else { + listener + .serve(AddInputExtensionLayer::new(state).into_layer(base)) + .await; + } Ok(()) }