- commit
- 26bc188bbc7c0375d2306cd3c595331e4e48d0d1
- parent
- e72a56d6dee0f61410ea9cb58be8f69a93d1bcb8
- author
- Dami <dami.etoile@gmail.com>
@@ -1,38 +1,32 @@
1-dev.site v0.5.0 improves the public documentation and homepage guidance for access, tickets,2-keys, and service transport.1+dev.site v0.5.1 moves production Iroh traffic to the managed shared relay tier.32 43 ## highlights54 6-- New concept guides explain tickets, keys and endpoint identities, and the ALPN wire7- protocol.8-- The homepage shows the delegated access workflow from request through service connection.9-- Command examples use colour to distinguish commands, subcommands, options, values, and10- ticket values.11-- Documentation now calls the approving human or automated process the granting party.5+- The CLI and daemon use four project relays through an Iroh custom relay map.6+- The control plane keeps the Iroh Services API key in its environment.7+- The control plane gives each client and daemon an endpoint-scoped relay token.8+- Public Iroh relays remain available only to transport integration tests.129 13-## tickets, keys, and protocol10+## deployment1411 15-The ticket guide explains each ticket type, its issuer, use count, lifetime, storage, and16-revocation rules.12+Set `IROH_SERVICES_API_SECRET` on the control plane before you release the new CLI.13+The server must return scoped relay tokens to version 0.5.1 clients.1714 18-The key guide explains endpoint identity, requester keys, control-plane signing keys, and19-private key storage.20-21-The ALPN guide describes `devsite/tcp/1`, its QUIC handshake, frame format, capability22-checks, and version boundary.15+The relay token grants relay use only. It does not grant access to a dev.site service.16+The existing signed capability checks still control each service connection.2317 2418 ## identity file migration2519 26-Version 0.5.0 continues to move legacy `identity.key` and `identity.pub` files to20+Version 0.5.1 continues to move legacy `identity.key` and `identity.pub` files to2721 `devsite-endpoint.key` and `devsite-endpoint.pub`.2822 2923 The migration checks only the dev.site config directory. It moves a legacy file only when3024 the destination file does not exist.3125 32-`devsite doctor` now warns that version 0.6.0 removes legacy file support.26+`devsite doctor` warns that version 0.6.0 removes legacy file support.3327 3428 Before you upgrade to version 0.6.0, run `devsite login` or `devsite daemon run` with35-version 0.5.0 or an earlier version.29+version 0.5.1 or an earlier version.3630 3731 ## upgrade notes3832 @@ -159,6 +159,45 @@ version = "0.7.8"
159159 source = "registry+https://github.com/rust-lang/crates.io-index"160160 checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"161161 162+[[package]]163+name = "asn1-rs"164+version = "0.7.2"165+source = "registry+https://github.com/rust-lang/crates.io-index"166+checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8"167+dependencies = [168+ "asn1-rs-derive",169+ "asn1-rs-impl",170+ "displaydoc",171+ "nom",172+ "num-traits",173+ "rusticata-macros",174+ "thiserror 2.0.20",175+ "time",176+]177+178+[[package]]179+name = "asn1-rs-derive"180+version = "0.6.0"181+source = "registry+https://github.com/rust-lang/crates.io-index"182+checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"183+dependencies = [184+ "proc-macro2",185+ "quote",186+ "syn 2.0.119",187+ "synstructure",188+]189+190+[[package]]191+name = "asn1-rs-impl"192+version = "0.2.0"193+source = "registry+https://github.com/rust-lang/crates.io-index"194+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"195+dependencies = [196+ "proc-macro2",197+ "quote",198+ "syn 2.0.119",199+]200+162201 [[package]]163202 name = "async-compression"164203 version = "0.4.43"@@ -193,6 +232,15 @@ dependencies = [
193232 "rustc_version",194233 ]195234 235+[[package]]236+name = "atomic-polyfill"237+version = "1.0.3"238+source = "registry+https://github.com/rust-lang/crates.io-index"239+checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"240+dependencies = [241+ "critical-section",242+]243+196244 [[package]]197245 name = "atomic-waker"198246 version = "1.1.2"@@ -310,6 +358,15 @@ version = "1.8.3"
310358 source = "registry+https://github.com/rust-lang/crates.io-index"311359 checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"312360 361+[[package]]362+name = "bit-vec"363+version = "0.9.1"364+source = "registry+https://github.com/rust-lang/crates.io-index"365+checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"366+dependencies = [367+ "serde",368+]369+313370 [[package]]314371 name = "bitflags"315372 version = "2.13.1"@@ -378,17 +435,47 @@ dependencies = [
378435 "alloc-stdlib",379436 ]380437 438+[[package]]439+name = "built"440+version = "0.8.1"441+source = "registry+https://github.com/rust-lang/crates.io-index"442+checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9"443+dependencies = [444+ "cargo-lock",445+]446+381447 [[package]]382448 name = "bumpalo"383449 version = "3.20.3"384450 source = "registry+https://github.com/rust-lang/crates.io-index"385451 checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"386452 453+[[package]]454+name = "byteorder"455+version = "1.5.0"456+source = "registry+https://github.com/rust-lang/crates.io-index"457+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"458+387459 [[package]]388460 name = "bytes"389461 version = "1.12.1"390462 source = "registry+https://github.com/rust-lang/crates.io-index"391463 checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"464+dependencies = [465+ "serde",466+]467+468+[[package]]469+name = "cargo-lock"470+version = "11.1.0"471+source = "registry+https://github.com/rust-lang/crates.io-index"472+checksum = "50524592e6bfbb1bf6f94e8184a786f637faeaf1cf37bbe65502b9a2c5c48939"473+dependencies = [474+ "semver",475+ "serde",476+ "toml",477+ "url",478+]392479 393480 [[package]]394481 name = "cc"@@ -790,6 +877,20 @@ dependencies = [
790877 "zeroize",791878 ]792879 880+[[package]]881+name = "der-parser"882+version = "10.0.0"883+source = "registry+https://github.com/rust-lang/crates.io-index"884+checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"885+dependencies = [886+ "asn1-rs",887+ "displaydoc",888+ "nom",889+ "num-bigint",890+ "num-traits",891+ "rusticata-macros",892+]893+793894 [[package]]794895 name = "deranged"795896 version = "0.5.8"@@ -821,7 +922,7 @@ dependencies = [
821922 822923 [[package]]823924 name = "devsite-cli"824-version = "0.5.0"925+version = "0.5.1"825926 dependencies = [826927 "anyhow",827928 "clap",@@ -842,9 +943,10 @@ dependencies = [
842943 843944 [[package]]844945 name = "devsite-client"845-version = "0.5.0"946+version = "0.5.1"846947 dependencies = [847948 "anyhow",949+ "devsite-iroh",848950 "devsite-proto",849951 "iroh",850952 "n0-future",@@ -854,11 +956,12 @@ dependencies = [
854956 855957 [[package]]856958 name = "devsite-daemon"857-version = "0.5.0"959+version = "0.5.1"858960 dependencies = [859961 "anyhow",860962 "data-encoding",861963 "devsite-client",964+ "devsite-iroh",862965 "devsite-proto",863966 "directories",864967 "ed25519-dalek 2.2.0",@@ -871,9 +974,19 @@ dependencies = [
871974 "tracing-subscriber",872975 ]873976 977+[[package]]978+name = "devsite-iroh"979+version = "0.5.1"980+dependencies = [981+ "anyhow",982+ "data-encoding",983+ "iroh",984+ "iroh-services",985+]986+874987 [[package]]875988 name = "devsite-proto"876-version = "0.5.0"989+version = "0.5.1"877990 dependencies = [878991 "data-encoding",879992 "ed25519-dalek 2.2.0",@@ -885,11 +998,12 @@ dependencies = [
885998 886999 [[package]]8871000 name = "devsite-server"888-version = "0.5.0"1001+version = "0.5.1"8891002 dependencies = [8901003 "anyhow",8911004 "axum",8921005 "data-encoding",1006+ "devsite-iroh",8931007 "devsite-proto",8941008 "ed25519-dalek 2.2.0",8951009 "jsonwebtoken",@@ -1025,9 +1139,9 @@ dependencies = [
10251139 10261140 [[package]]10271141 name = "ed25519-dalek"1028-version = "3.0.0"1142+version = "3.0.0-rc.0"10291143 source = "registry+https://github.com/rust-lang/crates.io-index"1030-checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de"1144+checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60"10311145 dependencies = [10321146 "curve25519-dalek 5.0.0",10331147 "ed25519 3.0.0",@@ -1182,7 +1296,7 @@ dependencies = [
11821296 "diatomic-waker",11831297 "futures-core",11841298 "pin-project-lite",1185- "spin",1299+ "spin 0.10.1",11861300 ]11871301 11881302 [[package]]@@ -1316,9 +1430,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
13161430 checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"13171431 dependencies = [13181432 "cfg-if",1433+ "js-sys",13191434 "libc",13201435 "r-efi 5.3.0",13211436 "wasip2",1437+ "wasm-bindgen",13221438 ]13231439 13241440 [[package]]@@ -1376,6 +1492,15 @@ dependencies = [
13761492 "tracing",13771493 ]13781494 1495+[[package]]1496+name = "hash32"1497+version = "0.2.1"1498+source = "registry+https://github.com/rust-lang/crates.io-index"1499+checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"1500+dependencies = [1501+ "byteorder",1502+]1503+13791504 [[package]]13801505 name = "hashbrown"13811506 version = "0.15.5"@@ -1405,12 +1530,32 @@ dependencies = [
14051530 "hashbrown 0.15.5",14061531 ]14071532 1533+[[package]]1534+name = "heapless"1535+version = "0.7.17"1536+source = "registry+https://github.com/rust-lang/crates.io-index"1537+checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"1538+dependencies = [1539+ "atomic-polyfill",1540+ "hash32",1541+ "rustc_version",1542+ "serde",1543+ "spin 0.9.9",1544+ "stable_deref_trait",1545+]1546+14081547 [[package]]14091548 name = "heck"14101549 version = "0.5.0"14111550 source = "registry+https://github.com/rust-lang/crates.io-index"14121551 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"14131552 1553+[[package]]1554+name = "hex"1555+version = "0.4.3"1556+source = "registry+https://github.com/rust-lang/crates.io-index"1557+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"1558+14141559 [[package]]14151560 name = "hickory-net"14161561 version = "0.26.1"@@ -1816,7 +1961,7 @@ dependencies = [
18161961 "ctutils",18171962 "data-encoding",18181963 "derive_more",1819- "ed25519-dalek 3.0.0",1964+ "ed25519-dalek 3.0.0-rc.0",18201965 "futures-util",18211966 "getrandom 0.4.3",18221967 "hickory-resolver",@@ -1864,7 +2009,7 @@ dependencies = [
18642009 "data-encoding",18652010 "data-encoding-macro",18662011 "derive_more",1867- "ed25519-dalek 3.0.0",2012+ "ed25519-dalek 3.0.0-rc.0",18682013 "getrandom 0.4.3",18692014 "n0-error",18702015 "rand 0.10.2",@@ -1969,6 +2114,107 @@ dependencies = [
19692114 "ws_stream_wasm",19702115 ]19712116 2117+[[package]]2118+name = "iroh-services"2119+version = "1.0.0"2120+source = "registry+https://github.com/rust-lang/crates.io-index"2121+checksum = "a1a88cd95fbd20abd9eadc4df91c722915dc943412b964e915f35b0b0a46a1fd"2122+dependencies = [2123+ "anyhow",2124+ "base64",2125+ "built",2126+ "bytes",2127+ "data-encoding",2128+ "derive_more",2129+ "ed25519-dalek 3.0.0-rc.0",2130+ "futures-buffered",2131+ "getrandom 0.4.3",2132+ "iroh",2133+ "iroh-metrics",2134+ "iroh-tickets",2135+ "irpc",2136+ "irpc-iroh",2137+ "n0-error",2138+ "n0-future",2139+ "portmapper",2140+ "postcard",2141+ "rand 0.10.2",2142+ "rcan",2143+ "serde",2144+ "serde_json",2145+ "strum",2146+ "thiserror 2.0.20",2147+ "tokio",2148+ "tracing",2149+ "tracing-subscriber",2150+ "uuid",2151+]2152+2153+[[package]]2154+name = "iroh-tickets"2155+version = "1.0.0"2156+source = "registry+https://github.com/rust-lang/crates.io-index"2157+checksum = "da53233419ca36bf521ed45683b7748366f9b233032891eefc2d70567a84ac54"2158+dependencies = [2159+ "data-encoding",2160+ "derive_more",2161+ "iroh-base",2162+ "n0-error",2163+ "postcard",2164+ "serde",2165+]2166+2167+[[package]]2168+name = "irpc"2169+version = "0.17.0"2170+source = "registry+https://github.com/rust-lang/crates.io-index"2171+checksum = "3623d6ff582b415904b29bbe6ebcb4a4f9a262ccdee05a45fdd003ef0950c386"2172+dependencies = [2173+ "futures-buffered",2174+ "futures-util",2175+ "irpc-derive",2176+ "n0-error",2177+ "n0-future",2178+ "noq",2179+ "postcard",2180+ "rcgen",2181+ "rustls",2182+ "serde",2183+ "smallvec",2184+ "tokio",2185+ "tokio-util",2186+ "tracing",2187+]2188+2189+[[package]]2190+name = "irpc-derive"2191+version = "0.17.0"2192+source = "registry+https://github.com/rust-lang/crates.io-index"2193+checksum = "35c254013736de16472140d26904e6ac98e8f3887284dcf4af40f88c77411b56"2194+dependencies = [2195+ "proc-macro2",2196+ "quote",2197+ "syn 2.0.119",2198+]2199+2200+[[package]]2201+name = "irpc-iroh"2202+version = "0.17.0"2203+source = "registry+https://github.com/rust-lang/crates.io-index"2204+checksum = "2342daed629b312f61e57e452b0750a59da162f261b97f260a6354de61d4fb0e"2205+dependencies = [2206+ "getrandom 0.3.4",2207+ "iroh",2208+ "iroh-base",2209+ "irpc",2210+ "n0-error",2211+ "n0-future",2212+ "postcard",2213+ "serde",2214+ "tokio",2215+ "tracing",2216+]2217+19722218 [[package]]19732219 name = "is_terminal_polyfill"19742220 version = "1.70.2"@@ -2205,6 +2451,12 @@ dependencies = [
22052451 "unicase",22062452 ]22072453 2454+[[package]]2455+name = "minimal-lexical"2456+version = "0.2.1"2457+source = "registry+https://github.com/rust-lang/crates.io-index"2458+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"2459+22082460 [[package]]22092461 name = "miniz_oxide"22102462 version = "0.8.9"@@ -2415,6 +2667,16 @@ dependencies = [
24152667 "wmi",24162668 ]24172669 2670+[[package]]2671+name = "nom"2672+version = "7.1.3"2673+source = "registry+https://github.com/rust-lang/crates.io-index"2674+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"2675+dependencies = [2676+ "memchr",2677+ "minimal-lexical",2678+]2679+24182680 [[package]]24192681 name = "noq"24202682 version = "1.1.1"@@ -2632,6 +2894,15 @@ dependencies = [
26322894 "objc2-security",26332895 ]26342896 2897+[[package]]2898+name = "oid-registry"2899+version = "0.8.1"2900+source = "registry+https://github.com/rust-lang/crates.io-index"2901+checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"2902+dependencies = [2903+ "asn1-rs",2904+]2905+26352906 [[package]]26362907 name = "once_cell"26372908 version = "1.21.4"@@ -2870,6 +3141,7 @@ dependencies = [
28703141 "cobs",28713142 "embedded-io 0.4.0",28723143 "embedded-io 0.6.1",3144+ "heapless",28733145 "postcard-derive",28743146 "serde",28753147 ]@@ -3080,6 +3352,36 @@ dependencies = [
30803352 "rand_core 0.10.1",30813353 ]30823354 3355+[[package]]3356+name = "rcan"3357+version = "0.4.0"3358+source = "registry+https://github.com/rust-lang/crates.io-index"3359+checksum = "12a624a4a4742f8c6e58fba99712e606cea0491b76f5b2345f06af5802101027"3360+dependencies = [3361+ "anyhow",3362+ "derive_more",3363+ "ed25519-dalek 3.0.0-rc.0",3364+ "hex",3365+ "n0-future",3366+ "postcard",3367+ "serde",3368+ "serdect",3369+]3370+3371+[[package]]3372+name = "rcgen"3373+version = "0.14.9"3374+source = "registry+https://github.com/rust-lang/crates.io-index"3375+checksum = "091e7a8e7d86e6feb87a27ce8e2cba29d49eff9507afeebefab7eeb2ca667fb4"3376+dependencies = [3377+ "pem",3378+ "ring",3379+ "rustls-pki-types",3380+ "time",3381+ "x509-parser",3382+ "yasna",3383+]3384+30833385 [[package]]30843386 name = "redox_syscall"30853387 version = "0.5.18"@@ -3241,6 +3543,15 @@ dependencies = [
32413543 "semver",32423544 ]32433545 3546+[[package]]3547+name = "rusticata-macros"3548+version = "4.1.0"3549+source = "registry+https://github.com/rust-lang/crates.io-index"3550+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"3551+dependencies = [3552+ "nom",3553+]3554+32443555 [[package]]32453556 name = "rustls"32463557 version = "0.23.43"@@ -3396,6 +3707,10 @@ name = "semver"
33963707 version = "1.0.28"33973708 source = "registry+https://github.com/rust-lang/crates.io-index"33983709 checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"3710+dependencies = [3711+ "serde",3712+ "serde_core",3713+]33993714 34003715 [[package]]34013716 name = "send_wrapper"@@ -3467,6 +3782,15 @@ dependencies = [
34673782 "serde_core",34683783 ]34693784 3785+[[package]]3786+name = "serde_spanned"3787+version = "1.1.1"3788+source = "registry+https://github.com/rust-lang/crates.io-index"3789+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"3790+dependencies = [3791+ "serde_core",3792+]3793+34703794 [[package]]34713795 name = "serde_urlencoded"34723796 version = "0.7.1"@@ -3556,9 +3880,6 @@ name = "signature"
35563880 version = "3.0.0"35573881 source = "registry+https://github.com/rust-lang/crates.io-index"35583882 checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5"3559-dependencies = [3560- "rand_core 0.10.1",3561-]35623883 35633884 [[package]]35643885 name = "simd-adler32"@@ -3642,6 +3963,15 @@ dependencies = [
36423963 "syn 2.0.119",36433964 ]36443965 3966+[[package]]3967+name = "spin"3968+version = "0.9.9"3969+source = "registry+https://github.com/rust-lang/crates.io-index"3970+checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"3971+dependencies = [3972+ "lock_api",3973+]3974+36453975 [[package]]36463976 name = "spin"36473977 version = "0.10.1"@@ -3968,6 +4298,21 @@ dependencies = [
39684298 "tokio-util",39694299 ]39704300 4301+[[package]]4302+name = "toml"4303+version = "1.1.4+spec-1.1.0"4304+source = "registry+https://github.com/rust-lang/crates.io-index"4305+checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"4306+dependencies = [4307+ "indexmap",4308+ "serde_core",4309+ "serde_spanned",4310+ "toml_datetime",4311+ "toml_parser",4312+ "toml_writer",4313+ "winnow",4314+]4315+39714316 [[package]]39724317 name = "toml_datetime"39734318 version = "1.1.1+spec-1.1.0"@@ -3998,6 +4343,12 @@ dependencies = [
39984343 "winnow",39994344 ]40004345 4346+[[package]]4347+name = "toml_writer"4348+version = "1.1.2+spec-1.1.0"4349+source = "registry+https://github.com/rust-lang/crates.io-index"4350+checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"4351+40014352 [[package]]40024353 name = "tower"40034354 version = "0.5.3"@@ -4099,6 +4450,16 @@ dependencies = [
40994450 "tracing-core",41004451 ]41014452 4453+[[package]]4454+name = "tracing-serde"4455+version = "0.2.0"4456+source = "registry+https://github.com/rust-lang/crates.io-index"4457+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"4458+dependencies = [4459+ "serde",4460+ "tracing-core",4461+]4462+41024463 [[package]]41034464 name = "tracing-subscriber"41044465 version = "0.3.23"@@ -4109,12 +4470,15 @@ dependencies = [
41094470 "nu-ansi-term",41104471 "once_cell",41114472 "regex-automata",4473+ "serde",4474+ "serde_json",41124475 "sharded-slab",41134476 "smallvec",41144477 "thread_local",41154478 "tracing",41164479 "tracing-core",41174480 "tracing-log",4481+ "tracing-serde",41184482 ]41194483 41204484 [[package]]@@ -4202,6 +4566,7 @@ checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
42024566 dependencies = [42034567 "getrandom 0.4.3",42044568 "js-sys",4569+ "serde_core",42054570 "wasm-bindgen",42064571 ]42074572 @@ -4724,6 +5089,24 @@ dependencies = [
47245089 "web-sys",47255090 ]47265091 5092+[[package]]5093+name = "x509-parser"5094+version = "0.18.1"5095+source = "registry+https://github.com/rust-lang/crates.io-index"5096+checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"5097+dependencies = [5098+ "asn1-rs",5099+ "data-encoding",5100+ "der-parser",5101+ "lazy_static",5102+ "nom",5103+ "oid-registry",5104+ "ring",5105+ "rusticata-macros",5106+ "thiserror 2.0.20",5107+ "time",5108+]5109+47275110 [[package]]47285111 name = "xml-rs"47295112 version = "0.8.29"@@ -4739,6 +5122,16 @@ dependencies = [
47395122 "xml-rs",47405123 ]47415124 5125+[[package]]5126+name = "yasna"5127+version = "0.6.0"5128+source = "registry+https://github.com/rust-lang/crates.io-index"5129+checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282"5130+dependencies = [5131+ "bit-vec",5132+ "time",5133+]5134+47425135 [[package]]47435136 name = "yoke"47445137 version = "0.8.3"@@ -2,6 +2,7 @@
22 resolver = "2"33 members = [44 "crates/devsite-proto",5+ "crates/devsite-iroh",56 "crates/devsite-client",67 "crates/devsite-daemon",78 "crates/devsite-cli",@@ -9,12 +10,13 @@ members = [
910 ]1011 1112 [workspace.package]12-version = "0.5.0"13+version = "0.5.1"1314 edition = "2021"1415 license = "Apache-2.0"1516 1617 [workspace.dependencies]1718 devsite-proto = { path = "crates/devsite-proto" }19+devsite-iroh = { path = "crates/devsite-iroh" }1820 devsite-client = { path = "crates/devsite-client" }1921 2022 anyhow = "1"@@ -244,6 +244,9 @@ control the theme, while `--devsite-folders`, `--devsite-open-folders`, and
244244 245245 Prerequisite: stable Rust.246246 247+Set `IROH_SERVICES_API_SECRET` before you start the control plane.248+The control plane uses it to make endpoint-scoped relay tokens.249+247250 ```bash248251 export DEVSITE_PUBLIC_ORIGIN=http://127.0.0.1:4000249252 cargo run -p devsite-server@@ -294,6 +297,7 @@ the configured issuer, client, and subjects.
294297 | Path | Responsibility |295298 | --- | --- |296299 | `crates/devsite-proto` | Opaque ids, signed capabilities, and the TCP stream handshake. |300+| `crates/devsite-iroh` | Shared Iroh Services relay configuration. |297301 | `crates/devsite-client` | Native Iroh client endpoint and authorized service streams. |298302 | `crates/devsite-daemon` | Capability verification and fixed-target TCP forwarding. |299303 | `crates/devsite-cli` | Login, links, service hosting/connect, themes, and daemon lifecycle. |@@ -310,6 +314,7 @@ the configured issuer, client, and subjects.
310314 memory, while the control plane stores its hash until expiry or disconnect.311315 - Capabilities are bound to the authenticated Iroh client endpoint and cannot be replayed312316 by another endpoint.317+- The control plane keeps the Iroh Services API key and gives each endpoint a scoped relay token.313318 - Each capability opens one stream and its nonce is consumed once.314319 - Unknown resources, invalid signatures, wrong audiences, wrong clients, expiry, and replay315320 all produce the same denial.@@ -83,20 +83,6 @@ impl ControlPlane {
8383 }8484 Ok(())8585 }86-87- /// PUT where a 2xx with no body is the expected success.88- pub async fn put_empty<B: Serialize>(&self, path: &str, body: &B) -> Result<()> {89- let response = self90- .request(reqwest::Method::PUT, path)91- .json(body)92- .send()93- .await94- .with_context(|| format!("PUT {path}"))?;95- if !response.status().is_success() {96- bail!("{path} failed: {}", error_text(response).await);97- }98- Ok(())99- }10086 }10187 10288 async fn decode<T: DeserializeOwned>(response: reqwest::Response, path: &str) -> Result<T> {@@ -388,6 +388,7 @@ struct CapabilityResponse {
388388 389389 struct RedeemTicketResponse {390390 session_token: String,391+ relay_token: String,391392 resource_id: String,392393 name: String,393394 expires_at: u64,@@ -441,10 +442,16 @@ struct TunnelSessionInfo {
441442 resource_id: String,442443 name: String,443444 requester_endpoint_id: String,445+ relay_token: String,444446 expires_at: u64,445447 brokered: bool,446448 }447449 450+451+struct RegisterDaemonResponse {452+ relay_token: String,453+}454+448455 449456 struct AuthorizationListing {450457 authorizations: Vec<ServiceAuthorization>,@@ -1163,18 +1170,20 @@ async fn connect(server: &str, ticket: &str, listen: SocketAddr, output: Output)
11631170 if !listen.ip().is_loopback() {11641171 bail!("--listen must be a loopback address; refusing to publish the tunnel on the LAN");11651172 }1166- let client = Arc::new(ClientEndpoint::create().await?);1173+ let secret_key = iroh::SecretKey::generate();11671174 let bootstrap = ControlPlane::new(server, None);11681175 let redeemed: RedeemTicketResponse = bootstrap11691176 .post(11701177 "/api/tickets/redeem",11711178 &serde_json::json!({11721179 "ticket": ticket,1173- "client_endpoint_id": client.endpoint_id().to_string(),1180+ "client_endpoint_id": secret_key.public().to_string(),11741181 }),11751182 )11761183 .await11771184 .context("redeeming connection ticket")?;1185+ let client =1186+ Arc::new(ClientEndpoint::create_with_relay(secret_key, redeemed.relay_token).await?);11781187 let api = Arc::new(ControlPlane::new(server, Some(redeemed.session_token)));11791188 run_tunnel(11801189 "connect",@@ -1460,16 +1469,17 @@ async fn connect_grant(
14601469 .as_slice()14611470 .try_into()14621471 .map_err(|_| anyhow::anyhow!("{} is not a 32 byte endpoint key", key_path.display()))?;1463- let client =1464- Arc::new(ClientEndpoint::create_with_secret(iroh::SecretKey::from_bytes(&bytes)).await?);1472+ let secret_key = iroh::SecretKey::from_bytes(&bytes);14651473 let api = Arc::new(ControlPlane::new(server, Some(grant.to_string())));14661474 let session: TunnelSessionInfo = api14671475 .get("/api/tunnel/session")14681476 .await14691477 .context("validating delegated access grant")?;1470- if session.requester_endpoint_id != client.endpoint_id().to_string() || !session.brokered {1478+ if session.requester_endpoint_id != secret_key.public().to_string() || !session.brokered {14711479 bail!("the grant is not bound to the supplied requester endpoint key");14721480 }1481+ let client =1482+ Arc::new(ClientEndpoint::create_with_relay(secret_key, session.relay_token).await?);14731483 run_tunnel(14741484 "access.connect",14751485 server,@@ -2835,9 +2845,19 @@ async fn run_daemon(paths: &Paths, server: &str, output: Output) -> Result<()> {
28352845 28362846 let secret_key = paths.load_or_create_identity()?;28372847 let proof = endpoint_proof(&secret_key);2838- let daemon = Arc::new(Daemon::bind(secret_key, config).await?);2848+ let endpoint_id = secret_key.public().to_string();2849+2850+ // The endpoint id is the public half of the key in DEVSITE_HOME/identity, so2851+ // it is the same on every run. Register it before the endpoint binds so the2852+ // control plane can make its scoped relay token.2853+ let api = ControlPlane::new(&server_url, token);2854+ let registration: RegisterDaemonResponse = api2855+ .put("/api/daemon", &serde_json::json!({ "proof": proof }))2856+ .await2857+ .context("registering this daemon with the control plane")?;2858+ let daemon =2859+ Arc::new(Daemon::bind_with_relay(secret_key, config, registration.relay_token).await?);28392860 2840- let endpoint_id = daemon.endpoint().id().to_string();28412861 let relay = daemon28422862 .endpoint()28432863 .addr()@@ -2846,19 +2866,6 @@ async fn run_daemon(paths: &Paths, server: &str, output: Output) -> Result<()> {
28462866 .map(|u| u.to_string())28472867 .context("no relay was assigned; check network connectivity")?;28482868 2849- // Told to the control plane once, not on a timer.2850- //2851- // The endpoint id is the public half of the key in DEVSITE_HOME/identity, so2852- // it is the same on every run and there is nothing to refresh. The relay2853- // above is printed for the operator and deliberately not uploaded: the2854- // endpoint publishes its own address through iroh's address lookup, and2855- // clients resolve it from there. The control plane holds permissions; it is2856- // not a directory service.2857- let api = ControlPlane::new(&server_url, token);2858- api.put_empty("/api/daemon", &serde_json::json!({ "proof": proof }))2859- .await2860- .context("registering this daemon with the control plane")?;2861-28622869 if output.json {28632870 output.event(28642871 "daemon.run",@@ -94,6 +94,22 @@ impl ClientEndpoint {
9494 })9595 }9696 97+ /// Bind an endpoint to the dev.site relay network.98+ pub async fn create_with_relay(99+ secret_key: iroh::SecretKey,100+ relay_token: impl Into<String>,101+ ) -> Result<Self> {102+ let endpoint = Endpoint::builder(devsite_iroh::preset(secret_key, relay_token)?)103+ .bind()104+ .await105+ .context("binding client endpoint")?;106+ endpoint.online().await;107+ Ok(Self {108+ endpoint,109+ connections: Mutex::new(HashMap::new()),110+ })111+ }112+97113 /// This client's public key. The control plane binds capabilities to it, and the98114 /// daemon checks that binding against the authenticated peer of the connection.99115 pub fn endpoint_id(&self) -> EndpointId {@@ -6,6 +6,7 @@ license.workspace = true
66 77 [dependencies]88 devsite-proto.workspace = true9+devsite-iroh.workspace = true910 iroh = "1"1011 anyhow.workspace = true1112 tracing.workspace = true@@ -54,10 +54,30 @@ impl Daemon {
5454 /// Fails if no control-plane key has been pinned: a daemon that cannot verify5555 /// capabilities must refuse to start rather than run in some permissive mode.5656 pub async fn bind(secret_key: SecretKey, config: DaemonConfig) -> Result<Self> {57+ Self::bind_builder(58+ Endpoint::builder(iroh::endpoint::presets::N0).secret_key(secret_key),59+ config,60+ )61+ .await62+ }63+64+ /// Bind the endpoint to the dev.site relay network.65+ pub async fn bind_with_relay(66+ secret_key: SecretKey,67+ config: DaemonConfig,68+ relay_token: impl Into<String>,69+ ) -> Result<Self> {70+ Self::bind_builder(71+ Endpoint::builder(devsite_iroh::preset(secret_key, relay_token)?),72+ config,73+ )74+ .await75+ }76+77+ async fn bind_builder(builder: iroh::endpoint::Builder, config: DaemonConfig) -> Result<Self> {5778 let control_plane_key = config.verifying_key()?;5879 59- let endpoint = Endpoint::builder(iroh::endpoint::presets::N0)60- .secret_key(secret_key)80+ let endpoint = builder6181 .alpns(vec![ALPN.to_vec()])6282 .bind()6383 .await@@ -0,0 +1,94 @@
1+use std::time::Duration;2+3+use anyhow::{Context, Result};4+use iroh::endpoint::presets::Preset;5+use iroh::{RelayMap, RelayMode, RelayUrl, SecretKey};6+use iroh_services::caps::{Cap, Caps, RelayCap};7+use iroh_services::{ApiSecret, API_SECRET_ENV_VAR_NAME};8+9+const RELAYS: [&str; 4] = [10+ "https://e67jyfby7vj7ak9h9.euc1.relay.iroh-svc.com/",11+ "https://e67jyfby7vj7ak9h9.use1.relay.iroh-svc.com/",12+ "https://e67jyfby7vj7ak9h9.usw1.relay.iroh-svc.com/",13+ "https://e67jyfby7vj7ak9h9.aps1.relay.iroh-svc.com/",14+];15+16+const RELAY_TOKEN_LIFETIME: Duration = Duration::from_secs(30 * 24 * 60 * 60);17+18+/// The server-side key that grants relay access to one endpoint.19+pub struct RelayIssuer {20+ api_secret: ApiSecret,21+}22+23+impl RelayIssuer {24+ /// Load the Iroh Services API secret from its standard environment variable.25+ pub fn from_env() -> Result<Self> {26+ let api_secret = ApiSecret::from_env_var(API_SECRET_ENV_VAR_NAME)27+ .context("reading IROH_SERVICES_API_SECRET")?;28+ Ok(Self { api_secret })29+ }30+31+ /// Make a relay-only token for one endpoint.32+ pub fn token(&self, endpoint_key: &[u8; 32]) -> Result<String> {33+ let endpoint_id = iroh::EndpointId::from_bytes(endpoint_key)34+ .context("the relay endpoint key is invalid")?;35+ let token = iroh_services::caps::create_api_token_from_secret_key(36+ self.api_secret.secret.clone(),37+ endpoint_id,38+ RELAY_TOKEN_LIFETIME,39+ Caps::new([Cap::Relay(RelayCap::Use)]),40+ )?;41+ let mut encoded = data_encoding::BASE32_NOPAD.encode(&token.encode());42+ encoded.make_ascii_lowercase();43+ Ok(encoded)44+ }45+}46+47+/// The endpoint preset for the dev.site relay network.48+pub struct DevsitePreset {49+ secret_key: SecretKey,50+ relays: RelayMap,51+}52+53+impl Preset for DevsitePreset {54+ fn apply(self, builder: iroh::endpoint::Builder) -> iroh::endpoint::Builder {55+ let builder = iroh::endpoint::presets::N0.apply(builder);56+ builder57+ .relay_mode(RelayMode::Custom(self.relays))58+ .secret_key(self.secret_key)59+ }60+}61+62+/// Make the Iroh preset for the dev.site relay network.63+pub fn preset(secret_key: SecretKey, relay_token: impl Into<String>) -> Result<DevsitePreset> {64+ let relays = RELAYS65+ .into_iter()66+ .map(|url| {67+ url.parse::<RelayUrl>()68+ .with_context(|| format!("invalid dev.site relay URL {url}"))69+ })70+ .collect::<Result<Vec<_>>>()?;71+ let relays = RelayMap::from_iter(relays).with_auth_token(relay_token);72+ Ok(DevsitePreset { secret_key, relays })73+}74+75+76+mod tests {77+ use super::*;78+79+ 80+ fn relay_tokens_are_scoped_to_endpoint_keys() {81+ let api_secret = ApiSecret::new(SecretKey::generate(), SecretKey::generate().public());82+ let issuer = RelayIssuer { api_secret };83+ let first = SecretKey::generate().public();84+ let second = SecretKey::generate().public();85+86+ let first_token = issuer.token(first.as_bytes()).unwrap();87+ let second_token = issuer.token(second.as_bytes()).unwrap();88+89+ assert_ne!(first_token, second_token);90+ assert!(first_token91+ .bytes()92+ .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit()));93+ }94+}@@ -28,6 +28,7 @@ pub struct AppState {
2828 pub issuer: Issuer,2929 pub identity_namespace: String,3030 pub public_origin: String,31+ pub relay_issuer: devsite_iroh::RelayIssuer,3132 }3233 3334 @@ -401,6 +402,7 @@ pub struct RedeemTicketRequest {
401402 402403 pub struct RedeemTicketResponse {403404 pub session_token: String,405+ pub relay_token: String,404406 pub resource_id: String,405407 pub name: String,406408 pub expires_at: u64,@@ -1144,7 +1146,15 @@ async fn register_daemon(
11441146 let db = state.db.lock().unwrap();11451147 db.register_daemon(machine.account.id, &machine.endpoint_id)11461148 .map_err(ApiError::internal)?;1147- Ok(StatusCode::NO_CONTENT)1149+ drop(db);1150+1151+ let endpoint_key = parse_endpoint_id(&machine.endpoint_id)1152+ .ok_or_else(|| ApiError::internal(anyhow::anyhow!("stored endpoint id is invalid")))?;1153+ let relay_token = state1154+ .relay_issuer1155+ .token(&endpoint_key)1156+ .map_err(ApiError::internal)?;1157+ Ok(Json(serde_json::json!({ "relay_token": relay_token })))11481158 }11491159 11501160 /// Report endpoint identities without implying daemon liveness. Registration is@@ -1913,6 +1923,10 @@ async fn redeem_connection_ticket(
19131923 19141924 Ok(Json(RedeemTicketResponse {19151925 session_token,1926+ relay_token: state1927+ .relay_issuer1928+ .token(&client_key)1929+ .map_err(ApiError::internal)?,19161930 resource_id: resource.id.to_string(),19171931 name: resource.name,19181932 expires_at,@@ -2036,6 +2050,11 @@ async fn tunnel_session_info(
20362050 "resource_id": resource.id.to_string(),20372051 "name": resource.name,20382052 "requester_endpoint_id": session.client_endpoint_id,2053+ "relay_token": state.relay_issuer.token(2054+ &parse_endpoint_id(&session.client_endpoint_id).ok_or_else(||2055+ ApiError::internal(anyhow::anyhow!("stored client endpoint id is invalid"))2056+ )?2057+ ).map_err(ApiError::internal)?,20392058 "expires_at": session.expires_at,20402059 "brokered": session.issuer_credential_id.is_some(),20412060 })))@@ -103,6 +103,7 @@ async fn main() -> Result<()> {
103103 104104 let db = Db::open(&config.database)?;105105 let issuer = config.issuer()?;106+ let relay_issuer = devsite_iroh::RelayIssuer::from_env()?;106107 let oidc = OidcConfig::from_env(&config.public_origin)?;107108 108109 tracing::info!(@@ -119,6 +120,7 @@ async fn main() -> Result<()> {
119120 issuer,120121 identity_namespace: oidc.issuer().to_string(),121122 public_origin: config.public_origin.clone(),123+ relay_issuer,122124 });123125 124126 let index = config.web_root.join("index.html");@@ -112,6 +112,7 @@ non-OIDC adapter.
112112 | `DEVSITE_OIDC_SCOPES` | optional | Space-separated; defaults to `openid` and must include it. |113113 | `DEVSITE_OIDC_ALGORITHMS` | optional | Allowed asymmetric JWT algorithms; defaults to Shoo's `ES256`. |114114 | `DEVSITE_SIGNING_KEY` | `fly secrets` | Hex. Never in the repository. |115+| `IROH_SERVICES_API_SECRET` | `fly secrets` | Iroh Services API key. Never in the repository. |115116 | `DEVSITE_BIND` | `Dockerfile` | `0.0.0.0:8080`, matching `internal_port`. |116117 | `DEVSITE_DB` | `Dockerfile` | `/data/devsite.db`, on the volume. |117118 | `DEVSITE_STATE_DIR` | `Dockerfile` | Unused while the secret is set. |