diff --git a/components/mycelium/.github/dependabot.yml b/components/mycelium/.github/dependabot.yml new file mode 100644 index 0000000..d23b411 --- /dev/null +++ b/components/mycelium/.github/dependabot.yml @@ -0,0 +1,31 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "cargo" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + groups: + mycelium: + patterns: + - "*" + - package-ecosystem: "cargo" # See documentation for possible values + directory: "/myceliumd" # Location of package manifests + schedule: + interval: "weekly" + groups: + myceliumd: + patterns: + - "*" + - package-ecosystem: "cargo" # See documentation for possible values + directory: "/myceliumd-private" # Location of package manifests + schedule: + interval: "weekly" + groups: + myceliumd-private: + patterns: + - "*" diff --git a/components/mycelium/.github/workflows/ci.yaml b/components/mycelium/.github/workflows/ci.yaml new file mode 100644 index 0000000..95cf8ed --- /dev/null +++ b/components/mycelium/.github/workflows/ci.yaml @@ -0,0 +1,133 @@ +name: ci + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + check_fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + - uses: clechasseur/rs-fmt-check@v2 + + clippy: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Set windows VCPKG_ROOT env variable + run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append + if: runner.os == 'Windows' + - name: Install windows openssl + run: vcpkg install openssl:x64-windows-static-md + if: runner.os == 'Windows' + - uses: actions/checkout@v4 + - name: Run Clippy + run: cargo clippy --all-features -- -Dwarnings + + check_library: + strategy: + matrix: + os: [ubuntu-latest, macos-latest,windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Set windows VCPKG_ROOT env variable + run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append + if: runner.os == 'Windows' + - name: Install windows openssl + run: vcpkg install openssl:x64-windows-static-md + if: runner.os == 'Windows' + - uses: actions/checkout@v4 + - name: Build + run: cargo build -p mycelium --all-features --verbose + - name: Run tests + run: cargo test -p mycelium --all-features --verbose + + check_ios_library: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: install ios target + run: rustup target add aarch64-apple-ios + - name: Cache cargo + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Build + run: cargo build --target aarch64-apple-ios + working-directory: mobile + + check_android_library: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: install android target + run: rustup target add aarch64-linux-android + - name: Setup Java + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: '17' + - name: Set up Android NDK + uses: android-actions/setup-android@v3 + - name: Accept Android Licenses + run: yes | sdkmanager --licenses || true + - name: Cache cargo + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: install cargo NDK + run: cargo install cargo-ndk + - name: Build + run: cargo ndk -t arm64-v8a build + working-directory: mobile + + check_binaries: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + binary: [myceliumd, myceliumd-private] + runs-on: ${{ matrix.os }} + steps: + - name: Set windows VCPKG_ROOT env variable + run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append + if: runner.os == 'Windows' + - name: Install windows openssl + run: vcpkg install openssl:x64-windows-static-md + if: runner.os == 'Windows' + - uses: actions/checkout@v4 + - name: Change directory to binary + run: cd ${{ matrix.binary }} + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose + - name: Run Clippy + run: cargo clippy --all-features -- -Dwarnings + + check_flake: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + permissions: + id-token: "write" + contents: "read" + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/flake-checker-action@main + - name: Run `nix build` + run: nix build . diff --git a/components/mycelium/.github/workflows/release.yaml b/components/mycelium/.github/workflows/release.yaml new file mode 100644 index 0000000..40597bc --- /dev/null +++ b/components/mycelium/.github/workflows/release.yaml @@ -0,0 +1,121 @@ +name: Release + +permissions: + contents: write + +on: + push: + tags: + - v[0-9]+.* + +jobs: + create-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: taiki-e/create-gh-release-action@v1 + with: + changelog: CHANGELOG.md + # (required) GitHub token for creating GitHub Releases. + token: ${{ secrets.GITHUB_TOKEN }} + + upload-assets-mycelium: + needs: create-release + strategy: + matrix: + include: + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: aarch64-unknown-linux-musl + os: ubuntu-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: taiki-e/upload-rust-binary-action@v1 + with: + # Name of the compiled binary, also name of the non-extension part of the produced file + bin: mycelium + # --target flag value, default is host + target: ${{ matrix.target }} + # Name of the archive when uploaded + archive: $bin-$target + # (required) GitHub token for uploading assets to GitHub Releases. + token: ${{ secrets.GITHUB_TOKEN }} + # Specify manifest since we are in a subdirectory + manifest-path: myceliumd/Cargo.toml + + # TODO: Figure out the correct matrix setup to have this in a single action + upload-assets-myceliumd-private: + needs: create-release + strategy: + matrix: + include: + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: aarch64-unknown-linux-musl + os: ubuntu-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: taiki-e/upload-rust-binary-action@v1 + with: + # Name of the compiled binary, also name of the non-extension part of the produced file + bin: mycelium-private + # Set the vendored-openssl flag for provided release builds + features: vendored-openssl + # --target flag value, default is host + target: ${{ matrix.target }} + # Name of the archive when uploaded + archive: $bin-$target + # (required) GitHub token for uploading assets to GitHub Releases. + token: ${{ secrets.GITHUB_TOKEN }} + # Specify manifest since we are in a subdirectory + manifest-path: myceliumd-private/Cargo.toml + + build-msi: + needs: create-release + runs-on: windows-latest + steps: + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Create .exe file + shell: bash + run: cd myceliumd && RUSTFLAGS="-C target-feature=+crt-static" cargo build --release && cd .. + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v4.0.0 + + - name: Install WiX Toolset + run: dotnet tool install --global wix + + - name: Add WixToolset.UI.wixext extension + run: wix extension add WixToolset.UI.wixext + + - name: Download Wintun zip file + run: curl -o wintun.zip https://www.wintun.net/builds/wintun-0.14.1.zip + + - name: Unzip Wintun + run: unzip wintun.zip + + - name: Move .dll file to myceliumd directory + run: move wintun\bin\amd64\wintun.dll myceliumd + + - name: Build MSI package + run: wix build -loc installers\windows\wix\mycelium.en-us.wxl installers\windows\wix\mycelium.wxs -ext WixToolset.UI.wixext -arch x64 -dcl high -out mycelium_installer.msi + + - name: Upload MSI artifact + uses: alexellis/upload-assets@0.4.0 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + asset_paths: '["mycelium_installer.msi"]' diff --git a/components/mycelium/.gitignore b/components/mycelium/.gitignore new file mode 100644 index 0000000..f828e85 --- /dev/null +++ b/components/mycelium/.gitignore @@ -0,0 +1,21 @@ +/target +nodeconfig.toml +keys.txt +priv_key.bin + +# Profile output +*.profraw +*.profdata +profile.json + +# vscode settings, keep these locally +.vscode +# visual studio project stuff +.vs + +# wintun.dll, windows tun driver in repo root for windows development +wintun.dll + +result/ + +.idea diff --git a/components/mycelium/CHANGELOG.md b/components/mycelium/CHANGELOG.md new file mode 100644 index 0000000..ba50296 --- /dev/null +++ b/components/mycelium/CHANGELOG.md @@ -0,0 +1,614 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- New log format option `plain`, this option is the same as logfmt, but with colors + always disabled. + +## [0.6.1] - 2025-05-14 + +### Added + +- When a route is used which is about to expire, we now send a route request to + try and refresh its duration before it expires. +- We now track when a peer was fist discovered and when we last connected to it. + This info is displayed in the CLI when listing peers. +- We now maintain a cache of recently sent route requests, so we can avoid spamming + peers with duplicate requests. + +### Changed + +- Only keep a record of retracted routes for 6 seconds instead of 60. We'll track + how this affects the route propagation before removing this altogether. + +### Fixed + +- Fixed an unsoundness issue in the routing table clone implementation. +- Clear dead peer buffer once peers have been removed from the routing table. +- Properly reply with an address unreachable ICMP when pinging an IP in the local + subnet which does not exist. +- Verify a packet has sufficient TTL to be routed before injecting it, and reply + with a TTL exceeded otherwise. This fixes an issue where packets with a TTL of + 1 and 0 originating locally would not result in a proper ICMP reply. This happens + for instance when using `traceroute`. +- Check the local seqno request cache before sending a seqno request to a peer, + to avoid spamming in certain occasions. +- Don't accept packet for a destination if we only have fallback routes for said + destination. + +## [0.6.0] - 2025-04-25 + +This is a breaking change, check the main README file for update info. + +### Added + +- Json-rpc based API, see the docs for more info. +- Message forwarding to unix sockets if configured. +- Config file support to handle messages, if this is enabled. + +### Changed + +- Routing has been reworked. We no longer advertise selected subnets (which aren't + our own). Now if a subnet is needed, we perform a route request for that subnet, + memorizing state and responses. The current imlementation expires routes every 5 + minutes but does not yet refresh active routes before they expire. +- Before we process a seqno request for a subnet, check the seqno cache to see if + we recently forwarded an entry for it. +- Discard Update TLV's if there are too many in the queue already. This binds memory + usage but can cause nodes with a lot of load to not pick up routes immediately. + +## [0.5.7] - 2024-11-31 + +### Fixed + +- Properly set the interface ipv6 MTU on windows. + +## [0.5.6] - 2024-10-03 + +### Fixed + +- Fix a panic in the route cleanup task when a peer dies who is the last stored + announcer of a subnet. + +## [0.5.5] - 2024-09-27 + +### Added + +- Mycelium-ui, a standalone GUI which exposes (part of) the mycelium API. This + does **not** have a bundled mycelium node, so that needs to be run separately. + +### Changed + +- Default TUN name on Linux and Windows is now `mycelium`. On MacOS it is now `utun0`. +- TUN interface name validation on MacOS. If the user supplies an invalid or already + taken interface name, an available interface name will be automatically assigned. + +### Fixed + +- Release flow to create the Windows installer now properly extracts wintun +- Guard against a race condition when a route is deleted which could rarely + trigger a panic and subsequent memory leak. + +## [0.5.4] - 2024-08-20 + +### Added + +- Quic protocol can now be disabled using the `--disable-quic` flag +- Mycelium can now be started with a configuration file using `--config-file`. + If no configuration file is supplied, Mycelium will look in a default location + based on the OS. For more information see [README](/README.md#configuration) +- Windows installer for Mycelium. The `.msi` file can be downloaded from the release + assets. +- Added flag to specify how many update workers should be started, which governs + the amount of parallelism used for processing updates. +- Send a seqno request if we receive an unfeasible update for a subnet with no + routes, or if there is no selected route for the subnet. +- New public peers in US, India, and Singapore. + +### Changed + +- Increased the starting metric of a peer from 50 to 1000. +- Reworked the internals of the routing table, which should reduce memory consumption. + Additionally, it is now possible to apply updates in parallel +- Periodically reduce the allocated size of the seqno cache to avoid wasting some + memory which is not currently used by the cache but still allocated. +- Demote seqno cache warnings about duplicate seqno requests go debug lvl, as it + is valid to send duplicate requests if sufficient time passed. +- Skip route selection after an unfeasible update to a fallback route, as the (now + unfeasible) route won't be selected anyway. +- No longer refresh route timer after an unfeasbile update. This allows routes + which have become unfeasible to gracefully be removed from the routing table + over time. +- Expired routes which aren't selected are now immediately removed from the routing + table. +- Changed how updates are sent to be more performant. +- A triggered update is no longer sent just because a route sequence number got + increased. We do still send the update to peer in the seqno request cache. +- Reduced log level when a route changes next-hop to debug from info. + +### Fixed + +- When running `mycelium` with a command, a keyfile was loaded (or created, if not + yet present). This was not necessary in that context. +- Limit the amount of time allowed for inbound quic connections to be set up, and + process multiple of them in parallel. This fixes a DOS vector against the quic + listener. +- We now update the source table even if we don't send an update because we are + sure the receiver won't select us as a next-hop anyway. + +## [0.5.3] - 2024-06-07 + +### Added + +- On Linux and macOS, a more descriptive error is printed when setting up the tun + device fails because a device with the same name already exists. +- Seqno request cache, to avoid spamming peers with duplicate seqno requests and + to make sure seqno's are forwarded to different peers. +- Added myceliumd-private binary, which contains private network functionality. +- Added API endpoint to retrieve the public key associated with an IP. +- The CLI can now be used to list, remove or add peers (see `mycelium peers --help`) +- The CLI can now be used to list selected and fallback routes (see + `mycelium routes --help`) + +### Changed + +- We now send seqno requests to all peers who advertised a subnet if the selected + route to it is lost as a result of the next-hop dying, or and update coming in + which causes no routes to be feasible anymore. +- Switched from the log to the tracing ecosystem. +- Only do the periodic route announcement every 5 minutes instead of every minute. +- Mycelium binary is no longer part of the workspace, and no longer contains private + network functionality. +- If a packet received from a peer can't be forwarded to the router, terminate the + connection to the peer. + +### Fixed + +- Manually implement Hash for Subnet, previously we could potentially have multiple + distinct entries in the source table for the same source key. + +## [0.5.2] - 2024-05-03 + +### Added + +- New CI workflow to build and test the mycelium library separately from the full + provided binary build. + +### Changed + +- Disabled the protobuf feature on prometheus, this removes protobuf related code + and significantly reduces the release binary size. +- Changed log level when sending a protocol message to a peer which is no longer + alive from error to trace in most instances. +- Improved performance of sending protocol messages to peers by queueing up multiple + packets at once (if multiple are ready). +- Before trying to send an update we now check if it makes sense to do so. +- If a peer died, fallback routes using it are no longer retained with an infinite + metric but removed immediately. +- No longer run route selection for subnets if a peer died and the route is not + selected. +- If routes are removed, shrink the capacity of the route list in the route table + if it is larger than required. +- Check if the originator of a TLV is still available before processing said TLV. +- The router now uses a dedicated task per TLV type to handle received TLV's from + peers. +- Statically linking openssl is now a feature flag when building yourself. + +### Fixed + +- If a peer died, unselect the selected routes which have it as next-hop if there + is no other feasible route. +- Properly unselect a route if a retraction update comes in and there is no other + feasible route. +- If the router bumps it's seqno it now properly announces the local route to it's + peers instead of the selected routes +- Seqno bump requests for advertised local routes now properly bump the router + seqno. + +## [0.5.1] - 2024-04-19 + +### Added + +- The repo is now a workspace, and pure library code is separated out. This is mainly + done to make it easier to develop implementations on different platforms. +- Link local discovery will now send discovery beacons on every interface the process + listens on for remote beacons. +- Experimental private network support. See [the private network docs](/docs/private_network.md) + for more info. +- You can now optionally expose Prometheus compatible metrics about the system by + setting the --metrics-api-address flag. +- On Linux, you can now set an optional firewall mark by setting the --firewall-mark + flag. +- Added a nix flake to the repo. + +### Changed + +- We no longer create an outbound connection to a link local discovered IP if that + IP is already known (usually as inbound address) with potentially a different + port. + +## [0.5.0] - 2024-04-04 + +### Changed + +- Connection identifier is now included in the error log if we can't forward a + seqno request. +- Garbage collection time for source entries has been increased from 5 to 30 minutes + for now. +- The router implementation has been changed to use regular locks instead of an + always readable concurrency primitive for all but the actual routing table. This + should reduce the memory consumption a bit. +- Public key and shared secret for a destination are now saved on the router, instead + of maintaining a separate mapping for them. This slightly reduces memory consumption + of the router, and ensures stale data is properly cleaned up when all routes to + a subnet are removed. +- Hello packets now set the interval in which the next Hello will be sent properly + in centiseconds. +- IHU packets now set the interval properly in centiseconds. +- IHU packets now set an RX cost. For now this is the link cost, in the future + this will be set properly. +- Route expiration time is now calculated from the interval received in updates. +- Ip address derivation from public keys now uses the blake3 hash algorithm. + +### Fixed + +- Don't try to forward seqno requests to a peer if we know its connection is dead. + +## [0.4.5] - 2024-03-26 + +### Changed + +- Size of data packets is limited to 65535 bytes. +- Update interval is now expressed as centiseconds, in accordance with the babel + RFC. +- Update filters now allow retractions for a route from any router-id. + +### Fixed + +- The feasibility distance of an existing source key is no longer incorrectly updated + when the metric increases. +- Source key garbage collection timers are properly reset on update even if the + source key itself is not updated. +- Nodes now properly reply to route requests for a static route. +- A retraction is now sent as reply to a route request if the route is not known. + +## [0.4.4] - 2024-03-22 + +### Changed + +- The amount of bytes read and written to a peer are now no longer reset after + a reconnect (for outgoing connection). +- Renamed `connectionTxBytes` and `connectionRxBytes` on the peer stats struct + to `txBytes` and `rxBytes` to better express that they are no longer tied to + a single connection to the peer. + +### Fixed + +- When joining a link local multicast group on an interface returns a + `Address already in use` error, the error is now ignored and the interface is + considered to be joined. +- When sending an update to a peer, the source table is now correctly updated before + the update is sent, instead of doing a batched source table update afterward. + +## [0.4.3] - 2024-03-15 + +### Added + +- Feature flag for message subsystem. It is enabled by default, but a user can + make a custom build with `--default-features-false` which completely leaves out + the message related code, should he desire this and have no need for it. +- Link local discovery now periodically checks for new IPv6 enabled interfaces + and also joins the discovery multicast group on them. +- Trace logs are removed from release binaries at compile time, slightly reducing + binary size. +- New `--silent` flag which disables all logging except error logs. + +### Changed + +- Update GitHub CI action to use latest version of the checkout action. +- Update GitHub CI action to stop using deprecated actions-rs actions. +- Failing to join the link local discovery multicast group now logs as warning + instead of error. +- Failing to join any IPv6 multicast group for link local peer discovery will no + longer disable local peer discovery entirely. + +### Fixed + +- Add proper validation when receiving an OOB ICMP packet. + +## [0.4.2] - 2024-02-28 + +### Fixed + +- Make sure the HTTP API doesn't shut down immediately after startup. + +## [0.4.1] - 2024-02-27 + +### Added + +- Admin API + - Ability to see current peers and related info + - Ability to add a new peer + - Ability to remove an existing peer + - List current selected routes + - List current fallback routes + - General node info (for now just the node subnet) + +### Changed + +- The tokio_unstable config flag is no longer used when building. +- The key file is now created without read permissions for the group/world. + +### Removed + +- .cargo/config.toml aarch64-linux target specific entries. Cross compilation for + these platforms can use `cross` or entries in the global .cargo/config.toml of + the developer instead. +- Sending SIGUSR1 to the process on unix based systems no longer dumps internal + state, this can be accessed with the admin API instead. + +## [0.4.0] - 2024-02-22 + +### Added + +- Support for windows tunnels. While this works, there are no windows + packages yet, so this is still a "developer experience". +- Validation on source IP when sending packets over TUN interface. + +### Changed + +- Overlay network is now hosted in 400::/7 instead of 200::/7. +- Key file is no longer created if it does not exist when the + inspect command is run. +- Packets with destination outside the global subnet now return + a proper ICMP instead of being silently dropped. + +### Fixed + +- Log the endpoint when a Quic connection can't be established. + +## [0.3.2] - 2024-01-31 + +### Added + +- If the router notices a Peer is dead, the connection is now forcibly terminated. +- Example Systemd file. + +### Changed + +- The multicast discovery group is now joined from all available + interfaces. This should increase the resilience of local peer + discovery. +- Setup of the node is now done completely in the library. +- Route selection now accounts for the link cost of peers when + considering if it should switch to the new route. +- Hop count of data packets is now decremented on the first + hop as well. As a result the sending node will show up in + traceroute results. + +### Fixed + +- Inbound peers now replace existing peers in the peer manager. This should fix + an issue where Quic could leave zombie connections. + +## [0.3.1] - 2024-01-23 + +### Added + +- You can now check the version of the current binary with the --version flag. +- Bandwidth usage is now tracked per peer. + +### Changed + +- Prefix decoding is now more resilient to bad prefix lengths. +- The `-k/--key-file` flag is now global, allowing you to specify it for (inspect) + sub commands. +- Log the actual endpoint when we can't connect to it + +## [0.3.0] - 2024-01-17 + +### Added + +- Nodes can now explicitly request selected route(s) from connected peers by + sending a Route Request Tlv. +- The router can now inform a Peer that the connection is seemingly + dead. This should improve the reconnect speed on connection types + which can't tell themselves if they died. + +### Changed + +- Locally discovered peers are now forgotten if we fail to connect to them 3 + times. +- Duration between periodic events has been increased, this should + reduce bandwidth when idle to maintain the system. +- Address encoding in update packets is now in-line with address + encoding as described by the babel RFC. + +### Fixed + +- TLV bodies of unknown type are now properly skipped. Previously, the + calculation of the body size was off by one, causing the connection to + the peer to always die. Now, these packets should be properly ignored. +- We are a bit more active now and no longer sleep for a second when we + need to remove an expired route entry. + +## [0.2.3] - 2024-01-04 + +### Added + +- Added automatic release builds for aarch64-linux. + +### Changed + +- Reduce the Quic keep-alive timeout. + +## [0.2.2] - 2024-01-03 + +### Changed + +- Changed default multicast peer discovery port to 9650. + +## [0.2.1] - 2024-01-03 + +### Added + +- Experimental Quic transport. The same socket is used for sending and + receiving. This transport is experimental and breaking changes are + possible which won't be covered by semver guarantees for now. + +## [0.2.0] - 2023-12-29 + +### Added + +- Link local peer discovery over IPv6. The system will automatically detect peers on the same LAN and try to connect to them. + This makes sure peers on the same network don't needlessly use bandwidth on external "hop" peers. +- Data packets now carry a Hop Limit field as part of the header. Every node decrements this value, and if it is decremented + to zero, the packet is discarded +- Intermediate nodes can now send ICMP packets back to the source in + reply to a dropped packet. This is useful if a hop does not have route + to forward a packet, or the hop count for a packet reaches 0. +- Local node now returns an ICMP Destination unreachable - no route if a + packet is sent on the TUN interface and there is no key for the remote + address (so the user data can't be encrypted). +- Peers connected over IPv4 now incur a higher processing cost, causing + IPv6 connections to be preferred for forwarding data. +- Peer addresses now include a protocol specifier, so multiple underlay + connections can be specified in the future. + +### Changed + +- The peer manager now tracks sufficient info of each connected peer to + avoid going through the router every time to see if it needs to + reconnect to said peers. +- We don't send the receiver nodes IP address in IHU packets anymore, as the packet is sent over a unicast link. + This is valid per the babel rfc. +- Setting the peers on the CLI now requires specifying the protocol to use. + For now only TCP is supported. +- Change `--port` flag to `--tcp-listen-port` to more accurately reflect + what it controls, as well as the fact that it is only for TCP. + +### Removed + +- Handshake when connecting to a new peer has been removed. + +## [0.1.3] - 2023-11-22 + +### Added + +- Add info log when next hop of a peer changes. +- Add windows builds to CI. + +### Changed + +- When printing the connected peers, print the underlay IP instead of the overlay IP. +- The link cost of a peer is now the smoothed average. This makes sure a single short latency spike doesn't disrupt routing. +- On Linux, set the TUN ip as /7 and avoid setting a /64 route. This brings it in line with OSX. +- When selecting the best route for a subnet, consider the currently + installed route and only switch if it is significantly better, or + directly connected. +- Increase the static link cost component of a peer. This will increase + the value of a hop in the metric of a route, in turn increasing the + impact of multiple hops on route selection. The route selection will + thus be more inclined to find a path with fewer hops toward a + destination. Ultimately, if multiple paths to a destination exist with + roughly the same latency, they one with fewer hops should be + preferred, since this avoids putting unnecessary pressure on multiple + nodes in the network. +- IHU packets now include the underlay IP instead of the overlay IP. +- When a new peer connects the underlay IP is logged instead of the + overlay IP. + +### Fixed + +- Ignore retraction updates if the route table has an existing but retracted route already. This fixes + an issue where retracted routes would not be flushed from the routing table. + +### Removed + +- All uses of the exchanged overlay IP in the peer handshake are fully + removed. Handshake is still performed to stay backwards compatible + until the next breaking release. + +## [0.1.2] - 2023-11-21 + +### Changed + +- Allow routes with infinite metric for a subnet to be selected. They will only be selected if no feasible route + with finite metric exists. They are also still ignored when looking up a route to a subnet. + +### Fixed + +- Don't trigger an update when a route retraction comes in for a subnet where the route is already retracted. + This fixes a potential storm of retraction requests in the network. + +## [0.1.1] - 2023-11-21 + +### Added + +- CHANGELOG.md file to keep track of notable additions, changes, fixes, deprecations, and removals. +- A peer can now detect if it is no longer useable in most cases, allowing it to notify the router if it died. This + allows near instant retraction of routes if a connection dies, decreasing the amount of time needed to find a + suitable alternative route. +- Add CHANGELOG.md entries when creating a release. +- When sending SIGUSR1 to the process, the routing table dump will now include a list of the public key derived IP + for every currently known subnet. +- You can now set the name of the TUN interface on the command line with the --tun-name flag. +- Added support for TUN devices on OSX. + +### Changed + +- When a peer is found to be dead, routes which use it as next-hop now have their metric set to infinity. + If the route is selected, route selection for the subnet is run again and if needed a triggered update is sent. + This will allow downstream peers to receive a timely update informing them of a potentially retracted route, + instead of having to wait for route expiration. +- Account for the link with the peer of a route when performing route selection. This was not the case previously, + and could theoretically lead to a case where a route was selected with a non-optimal path, because the lower metric + was offset by a high link cost of the peer. + +### Fixed + +- Remove trailing 'e' character from release archive names + +## [0.1.0] - 2023-11-15 + +### Added + +- Initial working setup with end-to-end encryption of traffic. +- Ability to specify peers to connect to +- Message subsystem, including API to use it. +- CLI options to send and receive messages with the API. + +[0.1.1]: https://github.com/threefoldtech/mycelium/compare/v0.1.0...v0.1.1 +[0.1.2]: https://github.com/threefoldtech/mycelium/compare/v0.1.1...v0.1.2 +[0.1.3]: https://github.com/threefoldtech/mycelium/compare/v0.1.2...v0.1.3 +[0.2.0]: https://github.com/threefoldtech/mycelium/compare/v0.1.3...v0.2.0 +[0.2.1]: https://github.com/threefoldtech/mycelium/compare/v0.2.0...v0.2.1 +[0.2.2]: https://github.com/threefoldtech/mycelium/compare/v0.2.1...v0.2.2 +[0.2.3]: https://github.com/threefoldtech/mycelium/compare/v0.2.2...v0.2.3 +[0.3.0]: https://github.com/threefoldtech/mycelium/compare/v0.2.3...v0.3.0 +[0.3.1]: https://github.com/threefoldtech/mycelium/compare/v0.3.0...v0.3.1 +[0.3.2]: https://github.com/threefoldtech/mycelium/compare/v0.3.1...v0.3.2 +[0.4.0]: https://github.com/threefoldtech/mycelium/compare/v0.3.2...v0.4.0 +[0.4.1]: https://github.com/threefoldtech/mycelium/compare/v0.4.0...v0.4.1 +[0.4.2]: https://github.com/threefoldtech/mycelium/compare/v0.4.1...v0.4.2 +[0.4.3]: https://github.com/threefoldtech/mycelium/compare/v0.4.2...v0.4.3 +[0.4.4]: https://github.com/threefoldtech/mycelium/compare/v0.4.3...v0.4.4 +[0.4.5]: https://github.com/threefoldtech/mycelium/compare/v0.4.4...v0.4.5 +[0.5.0]: https://github.com/threefoldtech/mycelium/compare/v0.4.5...v0.5.0 +[0.5.1]: https://github.com/threefoldtech/mycelium/compare/v0.5.0...v0.5.1 +[0.5.2]: https://github.com/threefoldtech/mycelium/compare/v0.5.1...v0.5.2 +[0.5.3]: https://github.com/threefoldtech/mycelium/compare/v0.5.2...v0.5.3 +[0.5.4]: https://github.com/threefoldtech/mycelium/compare/v0.5.3...v0.5.4 +[0.5.5]: https://github.com/threefoldtech/mycelium/compare/v0.5.4...v0.5.5 +[0.5.6]: https://github.com/threefoldtech/mycelium/compare/v0.5.5...v0.5.6 +[0.5.7]: https://github.com/threefoldtech/mycelium/compare/v0.5.6...v0.5.7 +[0.6.0]: https://github.com/threefoldtech/mycelium/compare/v0.5.7...v0.6.0 +[0.6.1]: https://github.com/threefoldtech/mycelium/compare/v0.6.0...v0.6.1 +[unreleased]: https://github.com/threefoldtech/mycelium/compare/v0.6.1...HEAD diff --git a/components/mycelium/CODEOWNERS b/components/mycelium/CODEOWNERS new file mode 100644 index 0000000..9658d49 --- /dev/null +++ b/components/mycelium/CODEOWNERS @@ -0,0 +1,2 @@ +# global code owners +* @leesmet diff --git a/components/mycelium/Cargo.lock b/components/mycelium/Cargo.lock new file mode 100644 index 0000000..89ecb66 --- /dev/null +++ b/components/mycelium/Cargo.lock @@ -0,0 +1,3967 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byte-unit" +version = "5.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +dependencies = [ + "rust_decimal", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "c2rust-bitfields" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b43c3f07ab0ef604fa6f595aa46ec2f8a22172c975e186f6f5bf9829a3b72c41" +dependencies = [ + "c2rust-bitfields-derive", +] + +[[package]] +name = "c2rust-bitfields-derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3cbc102e2597c9744c8bd8c15915d554300601c91a079430d309816b0912545" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] + +[[package]] +name = "cdn-meta" +version = "0.1.0" +source = "git+https://github.com/threefoldtech/mycelium-cdn-registry#2fac04710b60502a5165f1b1d90671730346e977" +dependencies = [ + "aes-gcm", + "bincode", + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "etherparse" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff83a5facf1a7cbfef93cfb48d6d4fb6a1f42d8ac2341a96b3255acb4d4f860" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.10", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", +] + +[[package]] +name = "ioctl-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd11f3a29434026f5ff98c730b668ba74b1033637b8817940b54d040696133c" + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpsee" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fba77a59c4c644fd48732367624d1bcf6f409f9c9a286fbc71d2f1fc0b2ea16" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-proc-macros", + "jsonrpsee-server", + "jsonrpsee-types", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693c93cbb7db25f4108ed121304b671a36002c2db67dff2ee4391a688c738547" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "parking_lot 0.12.3", + "pin-project", + "rand 0.9.1", + "rustc-hash", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fa4f5daed39f982a1bb9d15449a28347490ad42b212f8eaa2a2a344a0dce9e9" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38b0bcf407ac68d241f90e2d46041e6a06988f97fe1721fb80b91c42584fae6" +dependencies = [ + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66df7256371c45621b3b7d2fb23aea923d577616b9c0e9c0b950a6ea5c2be0ca" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "left-right" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabfddf3ad712b726484562039aa6fc2014bc1b5c088bb211b208052cf0439e6" +dependencies = [ + "loom", + "slab", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "mycelium" +version = "0.6.1" +dependencies = [ + "aes-gcm", + "ahash 0.8.11", + "arc-swap", + "axum", + "axum-extra", + "blake3", + "bytes", + "cdn-meta", + "dashmap", + "etherparse", + "faster-hex", + "futures", + "ip_network_table-deps-treebitmap", + "ipnet", + "left-right", + "libc", + "netdev", + "nix 0.29.0", + "nix 0.30.1", + "openssl", + "quinn", + "rand 0.9.1", + "rcgen", + "redis", + "reed-solomon-erasure", + "reqwest", + "rtnetlink", + "rustls", + "serde", + "tokio", + "tokio-openssl", + "tokio-stream", + "tokio-tun", + "tokio-util", + "tracing", + "tracing-logfmt", + "tracing-subscriber", + "tun", + "wintun 0.5.1", + "x25519-dalek", +] + +[[package]] +name = "mycelium-api" +version = "0.6.1" +dependencies = [ + "async-trait", + "axum", + "base64", + "jsonrpsee", + "mycelium", + "mycelium-metrics", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "mycelium-cli" +version = "0.6.1" +dependencies = [ + "base64", + "byte-unit", + "mycelium", + "mycelium-api", + "prettytable-rs", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "urlencoding", +] + +[[package]] +name = "mycelium-metrics" +version = "0.6.1" +dependencies = [ + "axum", + "mycelium", + "prometheus", + "tokio", + "tracing", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "netdev" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862209dce034f82a44c95ce2b5183730d616f2a68746b9c1959aa2572e77c0a1" +dependencies = [ + "dlopen2", + "ipnet", + "libc", + "netlink-packet-core", + "netlink-packet-route 0.22.0", + "netlink-sys", + "once_cell", + "system-configuration", + "windows-sys 0.59.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d83370a96813d7c977f8b63054f1162df6e5784f1c598d689236564fb5a6f2" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-proto" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.12", +] + +[[package]] +name = "netlink-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +dependencies = [ + "bytes", + "futures", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-src" +version = "300.4.2+3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.10", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.23", +] + +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" +dependencies = [ + "bitflags 2.9.0", + "hex", + "procfs-core", + "rustix 0.38.44", +] + +[[package]] +name = "procfs-core" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +dependencies = [ + "bitflags 2.9.0", + "hex", +] + +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "libc", + "memchr", + "parking_lot 0.12.3", + "procfs", + "thiserror 2.0.12", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.1", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.1", +] + +[[package]] +name = "rcgen" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49bc8ffa8a832eb1d7c8000337f8b0d2f4f2f5ec3cf4ddc26f125e3ad2451824" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redis" +version = "0.32.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f66bf4cac9733a23bcdf1e0e01effbaaad208567beba68be8f67e5f4af3ee1" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.6.0", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "reed-solomon-erasure" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7263373d500d4d4f505d43a2a662d475a894aa94503a1ee28e9188b5f3960d4f" +dependencies = [ + "libm", + "lru", + "parking_lot 0.11.2", + "smallvec", + "spin", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rtnetlink" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbe0a03f6b9b483c67d4b328fc5d66c8db0b6aa274e0fa2def71b5e442a69acf" +dependencies = [ + "futures", + "log", + "netlink-packet-core", + "netlink-packet-route 0.24.0", + "netlink-packet-utils", + "netlink-proto", + "netlink-sys", + "nix 0.29.0", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.59.0", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" + +[[package]] +name = "time-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2 0.5.10", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-openssl" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59df6849caa43bb7567f9a36f863c447d95a11d5903c9cc334ba32576a27eadd" +dependencies = [ + "openssl", + "openssl-sys", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tun" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be25a316a2d10d0ddd46de9ddd73cdb4262678e1e52f4a32a31421f1e2b816b4" +dependencies = [ + "libc", + "nix 0.29.0", + "thiserror 2.0.12", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-logfmt" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1f47d22deb79c3f59fcf2a1f00f60cbdc05462bf17d1cd356c1fefa3f444bd" +dependencies = [ + "nu-ansi-term 0.50.1", + "time", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term 0.46.0", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tun" +version = "0.6.1" +source = "git+https://github.com/LeeSmet/rust-tun#ae4c222e7a7aba5752cb08d2dbaf67bbc669c26a" +dependencies = [ + "byteorder", + "bytes", + "futures-core", + "ioctl-sys", + "libc", + "log", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "wintun 0.3.2", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +dependencies = [ + "memchr", +] + +[[package]] +name = "wintun" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b83b0eca06dd125dbcd48a45327c708a6da8aada3d95a3f06db0ce4b17e0d4" +dependencies = [ + "c2rust-bitfields", + "libloading", + "log", + "thiserror 1.0.69", + "windows 0.51.1", +] + +[[package]] +name = "wintun" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da99be64b5aa3de869c16977994314d0759a698d9a73ab0a5b1d52e2282033ae" +dependencies = [ + "c2rust-bitfields", + "libloading", + "log", + "thiserror 1.0.69", + "windows-sys 0.52.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] diff --git a/components/mycelium/Cargo.toml b/components/mycelium/Cargo.toml new file mode 100644 index 0000000..77523cf --- /dev/null +++ b/components/mycelium/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] +members = ["mycelium", "mycelium-metrics", "mycelium-api", "mycelium-cli"] +exclude = ["myceliumd", "myceliumd-private", "mycelium-ui", "mobile"] +resolver = "2" + + +[profile.release] +lto = "fat" +codegen-units = 1 diff --git a/components/mycelium/LICENSE b/components/mycelium/LICENSE new file mode 100644 index 0000000..b6418e9 --- /dev/null +++ b/components/mycelium/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright TF Tech NV + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/components/mycelium/README.md b/components/mycelium/README.md new file mode 100644 index 0000000..57f5966 --- /dev/null +++ b/components/mycelium/README.md @@ -0,0 +1,229 @@ +# Mycelium + +Mycelium is an IPv6 overlay network written in Rust. Each node that joins the overlay +network will receive an overlay network IP in the 400::/7 range. + +## Features + +- Mycelium, is locality aware, it will look for the shortest path between nodes +- All traffic between the nodes is end-2-end encrypted +- Traffic can be routed over nodes of friends, location aware +- If a physical link goes down Mycelium will automatically reroute your traffic +- The IP address is IPV6 and linked to private key +- A simple reliable messagebus is implemented on top of Mycelium +- Mycelium has multiple ways how to communicate quic, tcp, ... and we are working on holepunching for Quick which means P2P traffic without middlemen for NATted networks e.g. most homes +- Scalability is very important for us, we tried many overlay networks before and got stuck on all of them, we are trying to design a network which scales to a planetary level +- You can run mycelium without TUN and only use it as reliable message bus. + +> We are looking for lots of testers to push the system + +> [see here for docs](https://github.com/threefoldtech/mycelium/tree/master/docs) + +## Running + +> Currently, Linux, macOS and Windows are supported. + +### Linux and macOS + +Get an useable binary, either by downloading [an artifact from a release](https://github.com/threefoldtech/mycelium/releases), +or by [checking out and building the code yourself](#developing). + +### Windows + +Download the [mycelium_installer.msi](https://github.com/threefoldtech/mycelium/releases/latest/download/mycelium_installer.msi) and run the installer. + +### Run Mycelium + +Once you have an useable binary, simply start it. If you want to connect to other +nodes, you can specify their listening address as part of the command (combined +with the protocol they are listening on, usually TCP). Check the next section if +you want to connect to hosted public nodes. + +```sh +mycelium --peers tcp://188.40.132.242:9651 quic://185.69.166.8:9651 + +#other example with other tun interface if utun3 (the default) would already be used +#also here we use sudo e.g. on OSX +sudo mycelium --peers tcp://188.40.132.242:9651 quic://185.69.166.8:9651 --tun-name utun9 + +``` + +By default, the node will listen on port `9651`, though this can be overwritten +with the `-p` flag. + +To check your own info + +```bash +mycelium inspect --json +{ + "publicKey": "abd16194646defe7ad2318a0f0a69eb2e3fe939c3b0b51cf0bb88bb8028ecd1d", + "address": "5c4:c176:bf44:b2ab:5e7e:f6a:b7e2:11ca" +} +# test that network works, ping to anyone in the network +ping6 54b:83ab:6cb5:7b38:44ae:cd14:53f3:a907 +``` + +The node uses a `x25519` key pair from which its identity is derived. The private key of this key pair +is saved in a local file (32 bytes in binary format). You can specify the path to this file with the +`-k` flag. By default, the file is saved in the current working directory as `priv_key.bin`. + +### Running without TUN interface + +It is possible to run the system without creating a TUN interface, by starting with the `--no-tun` flag. +Obviously, this means that your node won't be able to send or receive L3 traffic. There is no interface +to send packets on, and consequently no interface to send received packets out of. From the point of +other nodes, your node will simply drop all incoming L3 traffic destined for it. The node **will still +route traffic** as normal. It takes part in routing, exchanges route info, and forwards packets not +intended for itself. + +The node also still allows access to the [message subsystem](#message-system). + +## Configuration + +Mycelium can be started with an **optional** configuration file using the `--config-file` +option, which offers the same capabilities as the command line arguments. + +If no configuration file is specified with `--config-file`, Mycelium will search for one +in a default location based on the operating system: + +- Linux: $HOME/.config/mycelium.toml +- Windows: %APPDATA%/ThreeFold Tech/Mycelium/mycelium.toml +- Mac OS: $HOME/Library/Application Support/ThreeFold Tech/Mycelium/mycelium.toml + +Command line arguments will override any settings found in the configuration file. + +## Hosted public nodes v0.6.x + +A couple of public nodes are provided, which can be freely connected to. This allows +anyone to join the global network. These are hosted in multiple geographic regions, +on both IPv4 and IPv6, and supporting both the Tcp and Quic protocols. The nodes +are the following: + +| Node ID | Region | IPv4 | IPv6 | Tcp port | Quic port | Mycelium IP | +| ------- | ------- | -------------- | --------------------------------- | -------- | --------- | -------------------------------------- | +| 01 | DE | 188.40.132.242 | 2a01:4f8:221:1e0b::2 | 9651 | 9651 | 54b:83ab:6cb5:7b38:44ae:cd14:53f3:a907 | +| 02 | DE | 136.243.47.186 | 2a01:4f8:212:fa6::2 | 9651 | 9651 | 40a:152c:b85b:9646:5b71:d03a:eb27:2462 | +| 03 | BE | 185.69.166.7 | 2a02:1802:5e:0:ec4:7aff:fe51:e80d | 9651 | 9651 | 597:a4ef:806:b09:6650:cbbf:1b68:cc94 | +| 04 | BE | 185.69.166.8 | 2a02:1802:5e:0:ec4:7aff:fe51:e36b | 9651 | 9651 | 549:8bce:fa45:e001:cbf8:f2e2:2da6:a67c | +| 05 | FI | 65.21.231.58 | 2a01:4f9:6a:1dc5::2 | 9651 | 9651 | 410:2778:53bf:6f41:af28:1b60:d7c0:707a | +| 06 | FI | 65.109.18.113 | 2a01:4f9:5a:1042::2 | 9651 | 9651 | 488:74ac:8a31:277b:9683:c8e:e14f:79a7 | +| 07 | US-EAST | 209.159.146.190 | 2604:a00:50:17b:9e6b:ff:fe1f:e054 | 9651 | 9651 | 4ab:a385:5a4e:ef8f:92e0:1605:7cb6:24b2 | +| 08 | US-WEST | 5.78.122.16 | 2a01:4ff:1f0:8859::1 | 9651 | 9651 | 4de:b695:3859:8234:d04c:5de6:8097:c27c | +| 09 | SG | 5.223.43.251 | 2a01:4ff:2f0:3621::1 | 9651 | 9651 | 5eb:c711:f9ab:eb24:ff26:e392:a115:1c0e | +| 10 | IND | 142.93.217.194 | 2400:6180:100:d0::841:2001 | 9651 | 9651 | 445:465:fe81:1e2b:5420:a029:6b0:9f61 | + +These nodes are all interconnected, so 2 peers who each connect to a different node +(or set of disjoint nodes) will still be able to reach each other. For optimal performance, +it is recommended to connect to all of the above at once however. An example connection +string could be: + +`--peers tcp://188.40.132.242:9651 "quic://[2a01:4f8:212:fa6::2]:9651" tcp://185.69.166.7:9651 "quic://[2a02:1802:5e:0:ec4:7aff:fe51:e36b]:9651" tcp://65.21.231.58:9651 "quic://[2a01:4f9:5a:1042::2]:9651" "tcp://[2604:a00:50:17b:9e6b:ff:fe1f:e054]:9651" quic://5.78.122.16:9651 "tcp://[2a01:4ff:2f0:3621::1]:9651" quic://142.93.217.194:9651` + +It is up to the user to decide which peers he wants to use, over which protocol. +Note that quotation may or may not be required, depending on which shell is being +used. IPv6 addresses should of course only be used if your ISP provides you with +IPv6 connectivity. + +### Private network + +Mycelium supports running a private network, in which you must know the network name +and a PSK (pre shared key) to connect to nodes in the network. For more info, check +out [the relevant docs](/docs/private_network.md). + +## API + +The node starts an HTTP API, which by default listens on `localhost:8989`. A different +listening address can be specified on the CLI when starting the system through the +`--api-addr` flag. The API allows access to [send and receive messages](#message-system), +and will later be expanded to allow admin functionality on the system. Note that +message are sent using the identity of the node, and a future admin API can be +used to change the system behavior. As such, care should be taken that this API +is not accessible to unauthorized users. + +## Message system + +A message system is provided which allows users to send a message, which is essentially just "some data" +to a remote. Since the system is end-to-end encrypted, a receiver of a message is sure of the authenticity +and confidentiality of the content. The system does not interpret the data in any way and handles it +as an opaque block of bytes. Messages are sent with a deadline. This means the system continuously +tries to send (part of) the message, until it either succeeds, or the deadline expires. This happens +similar to the way TCP handles data. Messages are transmitted in chunks, which are embedded in the +same data stream used by L3 packets. As such, intermediate nodes can't distinguish between regular L3 +and message data. +The primary way to interact with the message system is through [the API](#api). The message API is +documented in [an OpenAPI spec in the docs folder](docs/api.yaml). For some more info about how to +use the message system, see [the message docs](/docs/message.md). + +Messages can be categorized by topics, which can be configured with whitelisted subnets and socket forwarding paths. +For detailed information on how to configure topics, see the [Topic Configuration Guide](/docs/topic_configuration.md). +use the message system, see [the message docs](/docs/message.md). + +## Inspecting node keys + +Using the `inspect` subcommand, you can view the address associated with a public key. If no public key is provided, the node will show +its own public key. In either case, the derived address is also printed. You can specify the path to the private key with the `-k` flag. +If the file does not exist, a new private key will be generated. The optional `--json` flag can be used to print the information in json +format. + +```sh +mycelium inspect a47c1d6f2a15b2c670d3a88fbe0aeb301ced12f7bcb4c8e3aa877b20f8559c02 +Public key: a47c1d6f2a15b2c670d3a88fbe0aeb301ced12f7bcb4c8e3aa877b20f8559c02 +Address: 47f:b2c5:a944:4dad:9cb1:da4:8bf7:7e65 +``` + +```sh +mycelium inspect --json +{ + "publicKey": "955bf6bea5e1150fd8e270c12e5b2fc08f08f7c5f3799d10550096cc137d671b", + "address": "54f:b680:ba6e:7ced:355f:346f:d97b:eecb" +} +``` + +## Developing + +This project is built in Rust, and you must have a rust compiler to build the code +yourself. Please refer to [the official rust documentation](https://www.rust-lang.org/) +for information on how to install `rustc` and `cargo`. This project is a workspace, +however the binaries (`myceliumd` and `myceliumd-private`) are explicitly _not_ +part of this workspace. The reason for this is the way the cargo resolver unifies +features. Making both binaries part of the workspace would make the library build +for the regular binary include the code for the private network, and since that +is internal code it won't be removed at link time. + +First make sure you have cloned the repo + +```sh +git clone https://github.com/threefoldtech/mycelium.git +cd mycelium +``` + +If you only want to build the library, you can do so from the root of the repo + +```sh +cargo build +``` + +If you instead want to build a binary, that must be done from the appropriate subdirectory + +```sh +cd myceliumd +cargo build +``` + +Refer to the README files in those directories for more info. + +In case a release build is required, the `--release` flag can be added to the cargo +command (`cargo build --release`). + +## Cross compilation + +For cross compilation, it is advised to use the [`cross`](https://github.com/cross-rs/cross) +project. Alternatively, the standard way of cross compiling in rust can be used +(by specifying the `--target` flag in the `cargo build` command). This might require +setting some environment variables or local cargo config. On top of this, you should +also provide the `vendored-openssl` feature flag to build and statically link a copy +of openssl. + +## Remarks + +- The overlay network uses some of the core principles of the Babel routing protocol (). diff --git a/components/mycelium/config_example.toml b/components/mycelium/config_example.toml new file mode 100644 index 0000000..4e79779 --- /dev/null +++ b/components/mycelium/config_example.toml @@ -0,0 +1,22 @@ +# REMOVE/ADD COMMENT TO ENABLE/DISABLE LINE + +peers = [ + "tcp://188.40.132.242:9651", + "quic://[2a01:4f8:212:fa6::2]:9651", + "quic://185.69.166.7:9651", + "tcp://[2a02:1802:5e:0:8c9e:7dff:fec9:f0d2]:9651", + "quic://65.21.231.58:9651", + "tcp://[2a01:4f9:5a:1042::2]:9651", +] +api_addr = "127.0.0.1:8989" +tcp_listen_port = 9651 +quic_listen_port = 9651 +tun_name = "mycelium" +disable_peer_discovery = false +no_tun = false +#metrics_api_address = 0.0.0.0:9999 +#firewall_mark = 30 + +## Options below only apply when myceliumd-private is used +#network_name = "private network name" +#network_key_file = "path_to_key_file" diff --git a/components/mycelium/default.nix b/components/mycelium/default.nix new file mode 100644 index 0000000..2b62571 --- /dev/null +++ b/components/mycelium/default.nix @@ -0,0 +1,14 @@ +( + import + ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + {src = ./.;} +) +.defaultNix diff --git a/components/mycelium/docs/api.yaml b/components/mycelium/docs/api.yaml new file mode 100644 index 0000000..d1005ed --- /dev/null +++ b/components/mycelium/docs/api.yaml @@ -0,0 +1,990 @@ +openapi: 3.0.2 +info: + version: "1.0.0" + + title: Mycelium management + contact: + url: "https://github.com/threefoldtech/mycelium" + license: + name: Apache 2.0 + url: "https://github.com/threefoldtech/mycelium/blob/master/LICENSE" + + description: | + This is the specification of the **mycelium** management API. It is used to perform admin tasks on the system, and + to perform administrative duties. + +externalDocs: + description: For full documentation, check out the mycelium github repo. + url: "https://github.com/threefoldtech/mycelium" + +tags: + - name: Admin + description: Administrative operations + - name: Peer + description: Operations related to peer management + - name: Route + description: Operations related to network routes + - name: Message + description: Operations on the embedded message subsystem + - name: Topic + description: Operations related to message topic configuration + +servers: + - url: "http://localhost:8989" + +paths: + "/api/v1/admin": + get: + tags: + - Admin + summary: Get general info about the node + description: | + Get general info about the node, which is not related to other more specific functionality + operationId: getInfo + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/Info" + + "/api/v1/admin/peers": + get: + tags: + - Admin + - Peer + summary: List known peers + description: | + List all peers known in the system, and info about their connection. + This includes the endpoint, how we know about the peer, the connection state, and if the connection is alive the amount + of bytes we've sent to and received from the peer. + operationId: getPeers + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PeerStats" + post: + tags: + - Admin + - Peer + summary: Add a new peer + description: | + Add a new peer identified by the provided endpoint. + The peer is added to the list of known peers. It will eventually be connected + to by the standard connection loop of the peer manager. This means that a peer + which can't be connected to will stay in the system, as it might be reachable + later on. + operationId: addPeer + responses: + "204": + description: Peer added + "400": + description: Malformed endpoint + content: + text/plain: + schema: + type: string + description: Details about why the endpoint is not valid + "409": + description: Peer already exists + content: + text/plain: + schema: + type: string + description: message saying we already know this peer + + "/api/v1/admin/peers/{endpoint}": + delete: + tags: + - Admin + - Peer + summary: Remove an existing peer + description: | + Remove an existing peer identified by the provided endpoint. + The peer is removed from the list of known peers. If a connection to it + is currently active, it will be closed. + operationId: deletePeer + responses: + "204": + description: Peer removed + "400": + description: Malformed endpoint + content: + text/plain: + schema: + type: string + description: Details about why the endpoint is not valid + "404": + description: Peer doesn't exist + content: + text/plain: + schema: + type: string + description: message saying we don't know this peer + + "/api/v1/admin/routes/selected": + get: + tags: + - Admin + - Route + summary: List all selected routes + description: | + List all selected routes in the system, and their next hop identifier, metric and sequence number. + It is possible for a route to be selected and have an infinite metric. This route will however not forward packets. + operationId: getSelectedRoutes + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Route" + + "/api/v1/admin/routes/fallback": + get: + tags: + - Admin + - Route + summary: List all active fallback routes + description: | + List all fallback routes in the system, and their next hop identifier, metric and sequence number. + These routes are available to be selected in case the selected route for a destination suddenly fails, or gets retracted. + operationId: getFallbackRoutes + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Route" + + "/api/v1/admin/routes/queried": + get: + tags: + - Admin + - Route + summary: List all currently queried subnets + description: | + List all currently queried subnets in the system, and the amount of seconds until the query expires. + These subnets are actively being probed in the network. If no route to them is discovered before the query expires, + they will be marked as not reachable temporarily. + operationId: getQueriedSubnets + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/QueriedSubnet" + + "/api/v1/admin/routes/no_route": + get: + tags: + - Admin + - Route + summary: List all subnets which are explicitly marked as no route + description: | + List all subnets in the system which are marked no route, and the amount of seconds until the query expires. + These subnets have recently been probed in the network, and no route for them was discovered in time. No more + route requests will be send for these subnets until the entry expires. + operationId: getNoRouteEntries + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/NoRouteSubnet" + + "/api/v1/messages": + get: + tags: + - Message + summary: Get a message from the inbound message queue + description: | + Get a message from the inbound message queue. By default, the message is removed from the queue and won't be shown again. + If the peek query parameter is set to true, the message will be peeked, and the next call to this endpoint will show the same message. + This method returns immediately by default: a message is returned if one is ready, and if there isn't nothing is returned. If the timeout + query parameter is set, this call won't return for the given amount of seconds, unless a message is received + operationId: popMessage + parameters: + - in: query + name: peek + required: false + schema: + type: boolean + description: Whether to peek the message or not. If this is true, the message won't be removed from the inbound queue when it is read + example: true + - in: query + name: timeout + required: false + schema: + type: integer + format: int64 + minimum: 0 + description: | + Amount of seconds to wait for a message to arrive if one is not available. Setting this to 0 is valid and will return + a message if present, or return immediately if there isn't + example: 60 + - in: query + name: topic + required: false + schema: + type: string + format: byte + minLength: 0 + maxLength: 340 + description: | + Optional filter for loading messages. If set, the system checks if the message has the given string at the start. This way + a topic can be encoded. + example: example.topic + responses: + "200": + description: Message retrieved + content: + application/json: + schema: + $ref: "#/components/schemas/InboundMessage" + "204": + description: No message ready + post: + tags: + - Message + summary: Submit a new message to the system. + description: | + Push a new message to the systems outbound message queue. The system will continuously attempt to send the message until + it is either fully transmitted, or the send deadline is expired. + operationId: pushMessage + parameters: + - in: query + name: reply_timeout + required: false + schema: + type: integer + format: int64 + minimum: 0 + description: | + Amount of seconds to wait for a reply to this message to come in. If not set, the system won't wait for a reply and return + the ID of the message, which can be used later. If set, the system will wait for at most the given amount of seconds for a reply + to come in. If a reply arrives, it is returned to the client. If not, the message ID is returned for later use. + example: 120 + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PushMessageBody" + responses: + "200": + description: We received a reply within the specified timeout + content: + application/json: + schema: + $ref: "#/components/schemas/InboundMessage" + + "201": + description: Message pushed successfully, and not waiting for a reply + content: + application/json: + schema: + $ref: "#/components/schemas/PushMessageResponseId" + "408": + description: The system timed out waiting for a reply to the message + content: + application/json: + schema: + $ref: "#/components/schemas/PushMessageResponseId" + + "/api/v1/messages/reply/{id}": + post: + tags: + - Message + summary: Reply to a message with the given ID + description: | + Submits a reply message to the system, where ID is an id of a previously received message. If the sender is waiting + for a reply, it will bypass the queue of open messages. + operationId: pushMessageReply + parameters: + - in: path + name: id + required: true + schema: + type: string + format: hex + minLength: 16 + maxLength: 16 + example: abcdef0123456789 + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PushMessageBody" + responses: + "204": + description: successfully submitted the reply + + "/api/v1/messages/status/{id}": + get: + tags: + - Message + summary: Get the status of an outbound message + description: | + Get information about the current state of an outbound message. This can be used to check the transmission + state, size and destination of the message. + operationId: getMessageInfo + parameters: + - in: path + name: id + required: true + schema: + type: string + format: hex + minLength: 16 + maxLength: 16 + example: abcdef0123456789 + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/MessageStatusResponse" + "404": + description: Message not found + + "/api/v1/messages/topics/default": + get: + tags: + - Message + - Topic + summary: Get the default topic action + description: | + Get the default action for topics that are not explicitly configured (accept or reject). + operationId: getDefaultTopicAction + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultTopicActionResponse" + put: + tags: + - Message + - Topic + summary: Set the default topic action + description: | + Set the default action for topics that are not explicitly configured (accept or reject). + operationId: setDefaultTopicAction + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultTopicActionRequest" + responses: + "204": + description: Default topic action set successfully + + "/api/v1/messages/topics": + get: + tags: + - Message + - Topic + summary: Get all configured topics + description: | + Get all topics that are explicitly configured in the system. + operationId: getTopics + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + type: string + format: byte + description: Base64 encoded topic identifier + post: + tags: + - Message + - Topic + summary: Add a new topic + description: | + Add a new topic to the system's whitelist. + operationId: addTopic + requestBody: + content: + application/json: + schema: + type: string + format: byte + description: The topic to add + responses: + "201": + description: Topic added successfully + + "/api/v1/messages/topics/{topic}": + delete: + tags: + - Message + - Topic + summary: Remove a topic + description: | + Remove a topic from the system's whitelist. + operationId: removeTopic + parameters: + - in: path + name: topic + required: true + schema: + type: string + format: byte + description: The topic to remove (base64 encoded) + responses: + "204": + description: Topic removed successfully + "400": + description: Invalid topic format + + "/api/v1/messages/topics/{topic}/sources": + get: + tags: + - Message + - Topic + summary: Get sources for a topic + description: | + Get all sources (subnets) that are allowed to send messages for a specific topic. + operationId: getTopicSources + parameters: + - in: path + name: topic + required: true + schema: + type: string + format: byte + description: The topic to get sources for (base64 encoded) + responses: + "200": + description: Success + content: + application/json: + schema: + type: array + items: + type: string + description: Subnet in CIDR notation + "400": + description: Invalid topic format + post: + tags: + - Message + - Topic + summary: Add a source to a topic + description: | + Add a source (subnet) that is allowed to send messages for a specific topic. + operationId: addTopicSource + parameters: + - in: path + name: topic + required: true + schema: + type: string + format: byte + description: The topic to add a source to (base64 encoded) + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TopicSourceRequest" + responses: + "204": + description: Source added successfully + "400": + description: Invalid topic format or subnet + + "/api/v1/messages/topics/{topic}/sources/{subnet}": + delete: + tags: + - Message + - Topic + summary: Remove a source from a topic + description: | + Remove a source (subnet) that is allowed to send messages for a specific topic. + operationId: removeTopicSource + parameters: + - in: path + name: topic + required: true + schema: + type: string + format: byte + description: The topic to remove a source from (base64 encoded) + - in: path + name: subnet + required: true + schema: + type: string + description: The subnet to remove as a source + responses: + "204": + description: Source removed successfully + "400": + description: Invalid topic format or subnet + + "/api/v1/messages/topics/{topic}/forward": + get: + tags: + - Message + - Topic + summary: Get the forward socket for a topic + description: | + Get the socket path where messages for a specific topic are forwarded to. + operationId: getTopicForwardSocket + parameters: + - in: path + name: topic + required: true + schema: + type: string + format: byte + description: The topic to get the forward socket for (base64 encoded) + responses: + "200": + description: Success + content: + application/json: + schema: + type: string + nullable: true + description: The socket path where messages are forwarded to + "400": + description: Invalid topic format + put: + tags: + - Message + - Topic + summary: Set the forward socket for a topic + description: | + Set the socket path where messages for a specific topic should be forwarded to. + operationId: setTopicForwardSocket + parameters: + - in: path + name: topic + required: true + schema: + type: string + format: byte + description: The topic to set the forward socket for (base64 encoded) + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TopicForwardSocketRequest" + responses: + "204": + description: Forward socket set successfully + "400": + description: Invalid topic format + delete: + tags: + - Message + - Topic + summary: Remove the forward socket for a topic + description: | + Remove the socket path where messages for a specific topic are forwarded to. + operationId: removeTopicForwardSocket + parameters: + - in: path + name: topic + required: true + schema: + type: string + format: byte + description: The topic to remove the forward socket for (base64 encoded) + responses: + "204": + description: Forward socket removed successfully + "400": + description: Invalid topic format + "/api/v1/pubkey/{mycelium_ip}": + get: + summary: Get the pubkey from node ip + description: | + Get the node's public key from it's IP address. + operationId: getPublicKeyFromIp + parameters: + - in: path + name: mycelium_ip + required: true + schema: + type: string + format: ipv6 + example: 5fd:7636:b80:9ad0::1 + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/PublicKeyResponse" + "404": + description: Public key not found + +components: + schemas: + Info: + description: General information about a node + type: object + properties: + nodeSubnet: + description: The subnet owned by the node and advertised to peers + type: string + example: 54f:b680:ba6e:7ced::/64 + nodePubkey: + description: The public key of the node + type: string + format: hex + minLength: 64 + maxLength: 64 + example: 02468ace13579bdf02468ace13579bdf02468ace13579bdf02468ace13579bdf + + Endpoint: + description: Identification to connect to a peer + type: object + properties: + proto: + description: Protocol used + type: string + enum: + - "tcp" + - "quic" + example: tcp + socketAddr: + description: The socket address used + type: string + example: 192.0.2.6:9651 + + PeerStats: + description: Info about a peer + type: object + properties: + endpoint: + $ref: "#/components/schemas/Endpoint" + type: + description: How we know about this peer + type: string + enum: + - "static" + - "inbound" + - "linkLocalDiscovery" + example: static + connectionState: + description: The current state of the connection to the peer + type: string + enum: + - "alive" + - "connecting" + - "dead" + example: alive + txBytes: + description: The amount of bytes transmitted to this peer + type: integer + format: int64 + minimum: 0 + example: 464531564 + rxBytes: + description: The amount of bytes received from this peer + type: integer + format: int64 + minimum: 0 + example: 64645089 + + Route: + description: Information about a route + type: object + properties: + subnet: + description: The overlay subnet for which this is the route + type: string + example: 469:1348:ab0c:a1d8::/64 + nextHop: + description: A way to identify the next hop of the route, where forwarded packets will be sent + type: string + example: TCP 203.0.113.2:60128 <-> 198.51.100.27:9651 + metric: + description: The metric of the route, an estimation of how long the packet will take to arrive at its final destination + oneOf: + - description: A finite metric value + type: integer + format: int32 + minimum: 0 + maximum: 65534 + example: 13 + - description: An infinite (unreachable) metric. This is always `infinite` + type: string + example: infinite + seqno: + description: the sequence number advertised with this route by the source + type: integer + format: int32 + minimum: 0 + maximum: 65535 + example: 1 + + QueriedSubnet: + description: Information about a subnet currently being queried + type: object + properties: + subnet: + description: The overlay subnet which we are currently querying + type: string + example: 503:5478:df06:d79a::/64 + expiration: + description: The amount of seconds until the query expires + type: string + example: "37" + + NoRouteSubnet: + description: Information about a subnet which is marked as no route + type: object + properties: + subnet: + description: The overlay subnet which is marked + type: string + example: 503:5478:df06:d79a::/64 + expiration: + description: The amount of seconds until the entry expires + type: string + example: "37" + + InboundMessage: + description: A message received by the system + type: object + properties: + id: + description: Id of the message, hex encoded + type: string + format: hex + minLength: 16 + maxLength: 16 + example: 0123456789abcdef + srcIp: + description: Sender overlay IP address + type: string + format: ipv6 + example: 449:abcd:0123:defa::1 + srcPk: + description: Sender public key, hex encoded + type: string + format: hex + minLength: 64 + maxLength: 64 + example: fedbca9876543210fedbca9876543210fedbca9876543210fedbca9876543210 + dstIp: + description: Receiver overlay IP address + type: string + format: ipv6 + example: 34f:b680:ba6e:7ced:355f:346f:d97b:eecb + dstPk: + description: Receiver public key, hex encoded. This is the public key of the system + type: string + format: hex + minLength: 64 + maxLength: 64 + example: 02468ace13579bdf02468ace13579bdf02468ace13579bdf02468ace13579bdf + topic: + description: An optional message topic + type: string + format: byte + minLength: 0 + maxLength: 340 + example: hpV+ + payload: + description: The message payload, encoded in standard alphabet base64 + type: string + format: byte + example: xuV+ + + PushMessageBody: + description: A message to send to a given receiver + type: object + properties: + dst: + $ref: "#/components/schemas/MessageDestination" + topic: + description: An optional message topic + type: string + format: byte + minLength: 0 + maxLength: 340 + example: hpV+ + payload: + description: The message to send, base64 encoded + type: string + format: byte + example: xuV+ + + MessageDestination: + oneOf: + - description: An IP in the subnet of the receiver node + type: object + properties: + ip: + description: The target IP of the message + format: ipv6 + example: 449:abcd:0123:defa::1 + - description: The hex encoded public key of the receiver node + type: object + properties: + pk: + description: The hex encoded public key of the target node + type: string + minLength: 64 + maxLength: 64 + example: bb39b4a3a4efd70f3e05e37887677e02efbda14681d0acd3882bc0f754792c32 + + PushMessageResponseId: + description: The ID generated for a message after pushing it to the system + type: object + properties: + id: + description: Id of the message, hex encoded + type: string + format: hex + minLength: 16 + maxLength: 16 + example: 0123456789abcdef + + MessageStatusResponse: + description: Information about an outbound message + type: object + properties: + dst: + description: IP address of the receiving node + type: string + format: ipv6 + example: 449:abcd:0123:defa::1 + state: + $ref: "#/components/schemas/TransmissionState" + created: + description: Unix timestamp of when this message was created + type: integer + format: int64 + example: 1649512789 + deadline: + description: Unix timestamp of when this message will expire. If the message is not received before this, the system will give up + type: integer + format: int64 + example: 1649513089 + msgLen: + description: Length of the message in bytes + type: integer + minimum: 0 + example: 27 + + TransmissionState: + description: The state of an outbound message in it's lifetime + oneOf: + - type: string + enum: ["pending", "received", "read", "aborted"] + example: "received" + - type: object + properties: + sending: + type: object + properties: + pending: + type: integer + minimum: 0 + example: 5 + sent: + type: integer + minimum: 0 + example: 17 + acked: + type: integer + minimum: 0 + example: 3 + example: "received" + + PublicKeyResponse: + description: Public key requested based on a node's IP + type: object + properties: + NodePubKey: + type: string + format: hex + minLength: 64 + maxLength: 64 + example: 02468ace13579bdf02468ace13579bdf02468ace13579bdf02468ace13579bdf + + DefaultTopicActionResponse: + description: Response for the default topic action + type: object + properties: + accept: + description: Whether unconfigured topics are accepted by default + type: boolean + example: true + + DefaultTopicActionRequest: + description: Request to set the default topic action + type: object + properties: + accept: + description: Whether to accept unconfigured topics by default + type: boolean + example: true + + TopicInfo: + description: Information about a configured topic + type: object + properties: + topic: + description: The topic identifier (base64 encoded) + type: string + format: byte + example: "ZXhhbXBsZS50b3BpYw==" + sources: + description: List of subnets that are allowed to send messages for this topic + type: array + items: + type: string + example: "503:5478:df06:d79a::/64" + forward_socket: + description: Optional socket path where messages for this topic are forwarded to + type: string + nullable: true + example: "/var/run/mycelium/topic_socket" + + TopicSourceRequest: + description: Request to add a source to a topic whitelist + type: object + properties: + subnet: + description: The subnet to add as a source in CIDR notation + type: string + example: "503:5478:df06:d79a::/64" + + TopicForwardSocketRequest: + description: Request to set a forward socket for a topic + type: object + properties: + socket_path: + description: The socket path where messages should be forwarded to + type: string + example: "/var/run/mycelium/topic_socket" diff --git a/components/mycelium/docs/data_packet.md b/components/mycelium/docs/data_packet.md new file mode 100644 index 0000000..c648e1c --- /dev/null +++ b/components/mycelium/docs/data_packet.md @@ -0,0 +1,54 @@ +# Data packet + +A `data packet` contains user specified data. This can be any data, as long as the sender and receiver +both understand what it is, without further help. Intermediate hops, which route the data have sufficient +information with the header to know where to forward the packet. In practice, the data will be encrypted +to avoid eavesdropping by intermediate hops. + +## Packet header + +The packet header has a fixed size of 36 bytes, with the following layout: + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Reserved | Length | Hop Limit | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | ++ + +| | ++ Source IP + +| | ++ + +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | ++ + +| | ++ Destination IP + +| | ++ + +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +The first 8 bits are reserved and must be set to 0. + +The next 16 bits are used to specify the length of the body. It is expected that +the actual length of a packet does not exceed 65K right now, and overhead related +to encryption should be handled by the client before sending the packet. + +The next byte is the hop-limit. Every node decrements this value by 1 before sending +the packet. If a node decrements this value to 0, the packet is discarded. + +The next 16 bytes contain the sender IP address. + +The final 16 bytes contain the destination IP address. + +## Body + +Following the header is a variable length body. The protocol does not have any requirements for the +body, and the only requirement imposed is that the body is as long as specified in the header length +field. It is technically legal according to the protocol to transmit a data packet without a body, +i.e. a body length of 0. This is useless however, as there will not be any data to interpret. diff --git a/components/mycelium/docs/message.md b/components/mycelium/docs/message.md new file mode 100644 index 0000000..44c00ca --- /dev/null +++ b/components/mycelium/docs/message.md @@ -0,0 +1,161 @@ +# Message subsystem + +The message subsystem can be used to send arbitrary length messages to receivers. A receiver is any +other node in the network. It can be identified both by its public key, or an IP address in its announced +range. The message subsystem can be interacted with both via the HTTP API, which is +[documented here](./api.yaml), or via the `mycelium` binary. By default, the messages do not interpret +the data in any way. When using the binary, the message is slightly modified to include an optional +topic at the start of the message. Note that in the HTTP API, all messages are encoded in base64. This +might make it difficult to consume these messages without additional tooling. + +Messages can be categorized by topics, which can be configured with whitelisted subnets and socket forwarding paths. +For detailed information on how to configure topics, see the [Topic Configuration Guide](./topic_configuration.md). + +## JSON-RPC API Examples + +These examples assume you have at least 2 nodes running, and that they are both part of the same network. + +Send a message on node1, waiting up to 2 minutes for a possible reply: + +```json +{ + "jsonrpc": "2.0", + "method": "pushMessage", + "params": [ + { + "dst": {"pk": "bb39b4a3a4efd70f3e05e37887677e02efbda14681d0acd3882bc0f754792c32"}, + "payload": "xuV+" + }, + 120 + ], + "id": 1 +} +``` + +Using curl: + +```bash +curl -X POST http://localhost:8990/rpc \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "method": "pushMessage", + "params": [ + { + "dst": {"pk": "bb39b4a3a4efd70f3e05e37887677e02efbda14681d0acd3882bc0f754792c32"}, + "payload": "xuV+" + }, + 120 + ], + "id": 1 + }' +``` + +Listen for a message on node2. Note that messages received while nothing is listening are added to +a queue for later consumption. Wait for up to 1 minute. + +```json +{ + "jsonrpc": "2.0", + "method": "popMessage", + "params": [false, 60, null], + "id": 1 +} +``` + +Using curl: + +```bash +curl -X POST http://localhost:8990/rpc \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "method": "popMessage", + "params": [false, 60, null], + "id": 1 + }' +``` + +The system will (immediately) receive our previously sent message: + +```json +{"id":"e47b25063912f4a9","srcIp":"34f:b680:ba6e:7ced:355f:346f:d97b:eecb","srcPk":"955bf6bea5e1150fd8e270c12e5b2fc08f08f7c5f3799d10550096cc137d671b","dstIp":"2e4:9ace:9252:630:beee:e405:74c0:d876","dstPk":"bb39b4a3a4efd70f3e05e37887677e02efbda14681d0acd3882bc0f754792c32","payload":"xuV+"} +``` + +To send a reply, we can post a message on the reply path, with the received message `id` (still on +node2): + +```json +{ + "jsonrpc": "2.0", + "method": "pushMessageReply", + "params": [ + "e47b25063912f4a9", + { + "dst": {"pk":"955bf6bea5e1150fd8e270c12e5b2fc08f08f7c5f3799d10550096cc137d671b"}, + "payload": "xuC+" + } + ], + "id": 1 +} +``` + +Using curl: + +```bash +curl -X POST http://localhost:8990/rpc \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "method": "pushMessageReply", + "params": [ + "e47b25063912f4a9", + { + "dst": {"pk":"955bf6bea5e1150fd8e270c12e5b2fc08f08f7c5f3799d10550096cc137d671b"}, + "payload": "xuC+" + } + ], + "id": 1 + }' +``` + +If you did this fast enough, the initial sender (node1) will now receive the reply. + +## Mycelium binary examples + +As explained above, while using the binary the message is slightly modified to insert the optional +topic. As such, when using the binary to send messages, it is suggested to make sure the receiver is +also using the binary to listen for messages. The options discussed here are not covering all possibilities, +use the `--help` flag (`mycelium message send --help` and `mycelium message receive --help`) for a +full overview. + +Once again, send a message. This time using a topic (example.topic). Note that there are no constraints +on what a valid topic is, other than that it is valid UTF-8, and at most 255 bytes in size. The `--wait` +flag can be used to indicate that we are waiting for a reply. If it is set, we can also use an additional +`--timeout` flag to govern exactly how long (in seconds) to wait for. The default is to wait forever. + +```bash +mycelium message send 2e4:9ace:9252:630:beee:e405:74c0:d876 'this is a message' -t example.topic --wait +``` + +On the second node, listen for messages with this topic. If a different topic is used, the previous +message won't be received. If no topic is set, all messages are received. An optional timeout flag +can be specified, which indicates how long to wait for. Absence of this flag will cause the binary +to wait forever. + +```bash +mycelium message receive -t example.topic +``` + +Again, if the previous command was executed a message will be received immediately: + +```json +{"id":"4a6c956e8d36381f","topic":"example.topic","srcIp":"34f:b680:ba6e:7ced:355f:346f:d97b:eecb","srcPk":"955bf6bea5e1150fd8e270c12e5b2fc08f08f7c5f3799d10550096cc137d671b","dstIp":"2e4:9ace:9252:630:beee:e405:74c0:d876","dstPk":"bb39b4a3a4efd70f3e05e37887677e02efbda14681d0acd3882bc0f754792c32","payload":"this is a message"} +``` + +And once again, we can use the ID from this message to reply to the original sender, who might be waiting +for this reply (notice we used the hex encoded public key to identify the receiver here, rather than an IP): + +```bash +mycelium message send 955bf6bea5e1150fd8e270c12e5b2fc08f08f7c5f3799d10550096cc137d671b "this is a reply" --reply-to 4a6c956e8d36381f +``` diff --git a/components/mycelium/docs/openrpc.json b/components/mycelium/docs/openrpc.json new file mode 100644 index 0000000..762512b --- /dev/null +++ b/components/mycelium/docs/openrpc.json @@ -0,0 +1,1190 @@ +{ + "openrpc": "1.2.6", + "info": { + "version": "1.0.0", + "title": "Mycelium JSON-RPC API", + "description": "This is the specification of the Mycelium JSON-RPC API. It is used to perform admin tasks on the system, and to perform administrative duties.", + "contact": { + "url": "https://github.com/threefoldtech/mycelium" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/threefoldtech/mycelium/blob/master/LICENSE" + } + }, + "servers": [ + { + "url": "http://localhost:8990", + "name": "Mycelium JSON-RPC API" + } + ], + "methods": [ + { + "name": "getInfo", + "summary": "Get general info about the node", + "description": "Get general info about the node, which is not related to other more specific functionality", + "tags": [ + { + "name": "Admin" + } + ], + "params": [], + "result": { + "name": "info", + "description": "General information about the node", + "schema": { + "$ref": "#/components/schemas/Info" + } + } + }, + { + "name": "getPeers", + "summary": "List known peers", + "description": "List all peers known in the system, and info about their connection. This includes the endpoint, how we know about the peer, the connection state, and if the connection is alive the amount of bytes we've sent to and received from the peer.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Peer" + } + ], + "params": [], + "result": { + "name": "peers", + "description": "List of peers", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PeerStats" + } + } + } + }, + { + "name": "addPeer", + "summary": "Add a new peer", + "description": "Add a new peer identified by the provided endpoint. The peer is added to the list of known peers. It will eventually be connected to by the standard connection loop of the peer manager. This means that a peer which can't be connected to will stay in the system, as it might be reachable later on.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Peer" + } + ], + "params": [ + { + "name": "endpoint", + "description": "The endpoint of the peer to add", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the peer was added successfully", + "schema": { + "type": "boolean" + } + }, + "errors": [ + { + "code": 400, + "message": "Malformed endpoint" + }, + { + "code": 409, + "message": "Peer already exists" + } + ] + }, + { + "name": "deletePeer", + "summary": "Remove an existing peer", + "description": "Remove an existing peer identified by the provided endpoint. The peer is removed from the list of known peers. If a connection to it is currently active, it will be closed.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Peer" + } + ], + "params": [ + { + "name": "endpoint", + "description": "The endpoint of the peer to remove", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the peer was removed successfully", + "schema": { + "type": "boolean" + } + }, + "errors": [ + { + "code": 400, + "message": "Malformed endpoint" + }, + { + "code": 404, + "message": "Peer doesn't exist" + } + ] + }, + { + "name": "getSelectedRoutes", + "summary": "List all selected routes", + "description": "List all selected routes in the system, and their next hop identifier, metric and sequence number. It is possible for a route to be selected and have an infinite metric. This route will however not forward packets.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Route" + } + ], + "params": [], + "result": { + "name": "routes", + "description": "List of selected routes", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Route" + } + } + } + }, + { + "name": "getFallbackRoutes", + "summary": "List all active fallback routes", + "description": "List all fallback routes in the system, and their next hop identifier, metric and sequence number. These routes are available to be selected in case the selected route for a destination suddenly fails, or gets retracted.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Route" + } + ], + "params": [], + "result": { + "name": "routes", + "description": "List of fallback routes", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Route" + } + } + } + }, + { + "name": "getQueriedSubnets", + "summary": "List all currently queried subnets", + "description": "List all currently queried subnets in the system, and the amount of seconds until the query expires. These subnets are actively being probed in the network. If no route to them is discovered before the query expires, they will be marked as not reachable temporarily.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Route" + } + ], + "params": [], + "result": { + "name": "subnets", + "description": "List of queried subnets", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueriedSubnet" + } + } + } + }, + { + "name": "getNoRouteEntries", + "summary": "List all subnets which are explicitly marked as no route", + "description": "List all subnets in the system which are marked no route, and the amount of seconds until the query expires. These subnets have recently been probed in the network, and no route for them was discovered in time. No more route requests will be send for these subnets until the entry expires.", + "tags": [ + { + "name": "Admin" + }, + { + "name": "Route" + } + ], + "params": [], + "result": { + "name": "subnets", + "description": "List of no route subnets", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NoRouteSubnet" + } + } + } + }, + { + "name": "popMessage", + "summary": "Get a message from the inbound message queue", + "description": "Get a message from the inbound message queue. By default, the message is removed from the queue and won't be shown again. If the peek parameter is set to true, the message will be peeked, and the next call to this method will show the same message. This method returns immediately by default: a message is returned if one is ready, and if there isn't nothing is returned. If the timeout parameter is set, this call won't return for the given amount of seconds, unless a message is received.", + "tags": [ + { + "name": "Message" + } + ], + "params": [ + { + "name": "peek", + "description": "Whether to peek the message or not. If this is true, the message won't be removed from the inbound queue when it is read", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "timeout", + "description": "Amount of seconds to wait for a message to arrive if one is not available. Setting this to 0 is valid and will return a message if present, or return immediately if there isn't", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "name": "topic", + "description": "Optional filter for loading messages. If set, the system checks if the message has the given string at the start. This way a topic can be encoded.", + "required": false, + "schema": { + "type": "string", + "format": "byte", + "minLength": 0, + "maxLength": 340 + } + } + ], + "result": { + "name": "message", + "description": "The retrieved message", + "schema": { + "$ref": "#/components/schemas/InboundMessage" + } + }, + "errors": [ + { + "code": 204, + "message": "No message ready" + } + ] + }, + { + "name": "pushMessage", + "summary": "Submit a new message to the system", + "description": "Push a new message to the systems outbound message queue. The system will continuously attempt to send the message until it is either fully transmitted, or the send deadline is expired.", + "tags": [ + { + "name": "Message" + } + ], + "params": [ + { + "name": "message", + "description": "The message to send", + "required": true, + "schema": { + "$ref": "#/components/schemas/PushMessageBody" + } + }, + { + "name": "reply_timeout", + "description": "Amount of seconds to wait for a reply to this message to come in. If not set, the system won't wait for a reply and return the ID of the message, which can be used later. If set, the system will wait for at most the given amount of seconds for a reply to come in. If a reply arrives, it is returned to the client. If not, the message ID is returned for later use.", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + } + ], + "result": { + "name": "response", + "description": "The response to the message push", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InboundMessage" + }, + { + "$ref": "#/components/schemas/PushMessageResponseId" + } + ] + } + }, + "errors": [ + { + "code": 408, + "message": "The system timed out waiting for a reply to the message" + } + ] + }, + { + "name": "pushMessageReply", + "summary": "Reply to a message with the given ID", + "description": "Submits a reply message to the system, where ID is an id of a previously received message. If the sender is waiting for a reply, it will bypass the queue of open messages.", + "tags": [ + { + "name": "Message" + } + ], + "params": [ + { + "name": "id", + "description": "The ID of the message to reply to", + "required": true, + "schema": { + "type": "string", + "format": "hex", + "minLength": 16, + "maxLength": 16 + } + }, + { + "name": "message", + "description": "The reply message", + "required": true, + "schema": { + "$ref": "#/components/schemas/PushMessageBody" + } + } + ], + "result": { + "name": "success", + "description": "Whether the reply was submitted successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "getMessageInfo", + "summary": "Get the status of an outbound message", + "description": "Get information about the current state of an outbound message. This can be used to check the transmission state, size and destination of the message.", + "tags": [ + { + "name": "Message" + } + ], + "params": [ + { + "name": "id", + "description": "The ID of the message to get info for", + "required": true, + "schema": { + "type": "string", + "format": "hex", + "minLength": 16, + "maxLength": 16 + } + } + ], + "result": { + "name": "info", + "description": "Information about the message", + "schema": { + "$ref": "#/components/schemas/MessageStatusResponse" + } + }, + "errors": [ + { + "code": 404, + "message": "Message not found" + } + ] + }, + { + "name": "getPublicKeyFromIp", + "summary": "Get the pubkey from node ip", + "description": "Get the node's public key from it's IP address.", + "tags": [ + { + "name": "Admin" + } + ], + "params": [ + { + "name": "mycelium_ip", + "description": "The IP address to get the public key for", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + } + ], + "result": { + "name": "pubkey", + "description": "The public key of the node", + "schema": { + "$ref": "#/components/schemas/PublicKeyResponse" + } + }, + "errors": [ + { + "code": 404, + "message": "Public key not found" + } + ] + }, + { + "name": "getDefaultTopicAction", + "summary": "Get the default topic action", + "description": "Get the default action for topics that are not explicitly configured (accept or reject).", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [], + "result": { + "name": "accept", + "description": "Whether unconfigured topics are accepted by default", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "setDefaultTopicAction", + "summary": "Set the default topic action", + "description": "Set the default action for topics that are not explicitly configured (accept or reject).", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "accept", + "description": "Whether to accept unconfigured topics by default", + "required": true, + "schema": { + "type": "boolean" + } + } + ], + "result": { + "name": "success", + "description": "Whether the default topic action was set successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "getTopics", + "summary": "Get all configured topics", + "description": "Get all topics that are explicitly configured in the system.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [], + "result": { + "name": "topics", + "description": "List of configured topics", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "byte", + "description": "Base64 encoded topic identifier" + } + } + } + }, + { + "name": "addTopic", + "summary": "Add a new topic", + "description": "Add a new topic to the system's whitelist.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to add", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the topic was added successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "removeTopic", + "summary": "Remove a topic", + "description": "Remove a topic from the system's whitelist.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to remove", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the topic was removed successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "getTopicSources", + "summary": "Get sources for a topic", + "description": "Get all sources (subnets) that are allowed to send messages for a specific topic.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to get sources for", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "sources", + "description": "List of sources (subnets) for the topic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + { + "name": "addTopicSource", + "summary": "Add a source to a topic", + "description": "Add a source (subnet) that is allowed to send messages for a specific topic.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to add a source to", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "subnet", + "description": "The subnet to add as a source", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the source was added successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "removeTopicSource", + "summary": "Remove a source from a topic", + "description": "Remove a source (subnet) that is allowed to send messages for a specific topic.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to remove a source from", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "subnet", + "description": "The subnet to remove as a source", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the source was removed successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "getTopicForwardSocket", + "summary": "Get the forward socket for a topic", + "description": "Get the socket path where messages for a specific topic are forwarded to.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to get the forward socket for", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "socket_path", + "description": "The socket path where messages are forwarded to", + "schema": { + "type": ["string", "null"] + } + } + }, + { + "name": "setTopicForwardSocket", + "summary": "Set the forward socket for a topic", + "description": "Set the socket path where messages for a specific topic should be forwarded to.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to set the forward socket for", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "socket_path", + "description": "The socket path where messages should be forwarded to", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the forward socket was set successfully", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "removeTopicForwardSocket", + "summary": "Remove the forward socket for a topic", + "description": "Remove the socket path where messages for a specific topic are forwarded to.", + "tags": [ + { + "name": "Message" + }, + { + "name": "Topic" + } + ], + "params": [ + { + "name": "topic", + "description": "The topic to remove the forward socket for", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "success", + "description": "Whether the forward socket was removed successfully", + "schema": { + "type": "boolean" + } + } + } + ], + "components": { + "schemas": { + "Info": { + "description": "General information about a node", + "type": "object", + "properties": { + "nodeSubnet": { + "description": "The subnet owned by the node and advertised to peers", + "type": "string", + "example": "54f:b680:ba6e:7ced::/64" + }, + "nodePubkey": { + "description": "The public key of the node", + "type": "string", + "format": "hex", + "minLength": 64, + "maxLength": 64, + "example": "02468ace13579bdf02468ace13579bdf02468ace13579bdf02468ace13579bdf" + } + } + }, + "Endpoint": { + "description": "Identification to connect to a peer", + "type": "object", + "properties": { + "proto": { + "description": "Protocol used", + "type": "string", + "enum": [ + "tcp", + "quic" + ], + "example": "tcp" + }, + "socketAddr": { + "description": "The socket address used", + "type": "string", + "example": "192.0.2.6:9651" + } + } + }, + "TopicInfo": { + "description": "Information about a configured topic", + "type": "object", + "properties": { + "topic": { + "description": "The topic identifier", + "type": "string", + "format": "byte", + "example": "example.topic" + }, + "sources": { + "description": "List of subnets that are allowed to send messages for this topic", + "type": "array", + "items": { + "type": "string", + "example": "503:5478:df06:d79a::/64" + } + }, + "forward_socket": { + "description": "Optional socket path where messages for this topic are forwarded to", + "type": ["string", "null"], + "example": "/var/run/mycelium/topic_socket" + } + } + }, + "PeerStats": { + "description": "Info about a peer", + "type": "object", + "properties": { + "endpoint": { + "$ref": "#/components/schemas/Endpoint" + }, + "type": { + "description": "How we know about this peer", + "type": "string", + "enum": [ + "static", + "inbound", + "linkLocalDiscovery" + ], + "example": "static" + }, + "connectionState": { + "description": "The current state of the connection to the peer", + "type": "string", + "enum": [ + "alive", + "connecting", + "dead" + ], + "example": "alive" + }, + "txBytes": { + "description": "The amount of bytes transmitted to this peer", + "type": "integer", + "format": "int64", + "minimum": 0, + "example": 464531564 + }, + "rxBytes": { + "description": "The amount of bytes received from this peer", + "type": "integer", + "format": "int64", + "minimum": 0, + "example": 64645089 + } + } + }, + "Route": { + "description": "Information about a route", + "type": "object", + "properties": { + "subnet": { + "description": "The overlay subnet for which this is the route", + "type": "string", + "example": "469:1348:ab0c:a1d8::/64" + }, + "nextHop": { + "description": "A way to identify the next hop of the route, where forwarded packets will be sent", + "type": "string", + "example": "TCP 203.0.113.2:60128 <-> 198.51.100.27:9651" + }, + "metric": { + "description": "The metric of the route, an estimation of how long the packet will take to arrive at its final destination", + "oneOf": [ + { + "description": "A finite metric value", + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 65534, + "example": 13 + }, + { + "description": "An infinite (unreachable) metric. This is always `infinite`", + "type": "string", + "example": "infinite" + } + ] + }, + "seqno": { + "description": "the sequence number advertised with this route by the source", + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 65535, + "example": 1 + } + } + }, + "QueriedSubnet": { + "description": "Information about a subnet currently being queried", + "type": "object", + "properties": { + "subnet": { + "description": "The overlay subnet which we are currently querying", + "type": "string", + "example": "503:5478:df06:d79a::/64" + }, + "expiration": { + "description": "The amount of seconds until the query expires", + "type": "string", + "example": "37" + } + } + }, + "NoRouteSubnet": { + "description": "Information about a subnet which is marked as no route", + "type": "object", + "properties": { + "subnet": { + "description": "The overlay subnet which is marked", + "type": "string", + "example": "503:5478:df06:d79a::/64" + }, + "expiration": { + "description": "The amount of seconds until the entry expires", + "type": "string", + "example": "37" + } + } + }, + "InboundMessage": { + "description": "A message received by the system", + "type": "object", + "properties": { + "id": { + "description": "Id of the message, hex encoded", + "type": "string", + "format": "hex", + "minLength": 16, + "maxLength": 16, + "example": "0123456789abcdef" + }, + "srcIp": { + "description": "Sender overlay IP address", + "type": "string", + "format": "ipv6", + "example": "449:abcd:0123:defa::1" + }, + "srcPk": { + "description": "Sender public key, hex encoded", + "type": "string", + "format": "hex", + "minLength": 64, + "maxLength": 64, + "example": "fedbca9876543210fedbca9876543210fedbca9876543210fedbca9876543210" + }, + "dstIp": { + "description": "Receiver overlay IP address", + "type": "string", + "format": "ipv6", + "example": "34f:b680:ba6e:7ced:355f:346f:d97b:eecb" + }, + "dstPk": { + "description": "Receiver public key, hex encoded. This is the public key of the system", + "type": "string", + "format": "hex", + "minLength": 64, + "maxLength": 64, + "example": "02468ace13579bdf02468ace13579bdf02468ace13579bdf02468ace13579bdf" + }, + "topic": { + "description": "An optional message topic", + "type": "string", + "format": "byte", + "minLength": 0, + "maxLength": 340, + "example": "hpV+" + }, + "payload": { + "description": "The message payload, encoded in standard alphabet base64", + "type": "string", + "format": "byte", + "example": "xuV+" + } + } + }, + "PushMessageBody": { + "description": "A message to send to a given receiver", + "type": "object", + "properties": { + "dst": { + "$ref": "#/components/schemas/MessageDestination" + }, + "topic": { + "description": "An optional message topic", + "type": "string", + "format": "byte", + "minLength": 0, + "maxLength": 340, + "example": "hpV+" + }, + "payload": { + "description": "The message to send, base64 encoded", + "type": "string", + "format": "byte", + "example": "xuV+" + } + } + }, + "MessageDestination": { + "oneOf": [ + { + "description": "An IP in the subnet of the receiver node", + "type": "object", + "properties": { + "ip": { + "description": "The target IP of the message", + "type": "string", + "format": "ipv6", + "example": "449:abcd:0123:defa::1" + } + } + }, + { + "description": "The hex encoded public key of the receiver node", + "type": "object", + "properties": { + "pk": { + "description": "The hex encoded public key of the target node", + "type": "string", + "minLength": 64, + "maxLength": 64, + "example": "bb39b4a3a4efd70f3e05e37887677e02efbda14681d0acd3882bc0f754792c32" + } + } + } + ] + }, + "PushMessageResponseId": { + "description": "The ID generated for a message after pushing it to the system", + "type": "object", + "properties": { + "id": { + "description": "Id of the message, hex encoded", + "type": "string", + "format": "hex", + "minLength": 16, + "maxLength": 16, + "example": "0123456789abcdef" + } + } + }, + "MessageStatusResponse": { + "description": "Information about an outbound message", + "type": "object", + "properties": { + "dst": { + "description": "IP address of the receiving node", + "type": "string", + "format": "ipv6", + "example": "449:abcd:0123:defa::1" + }, + "state": { + "$ref": "#/components/schemas/TransmissionState" + }, + "created": { + "description": "Unix timestamp of when this message was created", + "type": "integer", + "format": "int64", + "example": 1649512789 + }, + "deadline": { + "description": "Unix timestamp of when this message will expire. If the message is not received before this, the system will give up", + "type": "integer", + "format": "int64", + "example": 1649513089 + }, + "msgLen": { + "description": "Length of the message in bytes", + "type": "integer", + "minimum": 0, + "example": 27 + } + } + }, + "TransmissionState": { + "description": "The state of an outbound message in it's lifetime", + "oneOf": [ + { + "type": "string", + "enum": [ + "pending", + "received", + "read", + "aborted" + ], + "example": "received" + }, + { + "type": "object", + "properties": { + "sending": { + "type": "object", + "properties": { + "pending": { + "type": "integer", + "minimum": 0, + "example": 5 + }, + "sent": { + "type": "integer", + "minimum": 0, + "example": 17 + }, + "acked": { + "type": "integer", + "minimum": 0, + "example": 3 + } + } + } + } + } + ] + }, + "PublicKeyResponse": { + "description": "Public key requested based on a node's IP", + "type": "object", + "properties": { + "NodePubKey": { + "type": "string", + "format": "hex", + "minLength": 64, + "maxLength": 64, + "example": "02468ace13579bdf02468ace13579bdf02468ace13579bdf02468ace13579bdf" + } + } + } + } + } +} diff --git a/components/mycelium/docs/packet.md b/components/mycelium/docs/packet.md new file mode 100644 index 0000000..50d5987 --- /dev/null +++ b/components/mycelium/docs/packet.md @@ -0,0 +1,22 @@ +# Packet + +A `Packet` is the largest communication object between established `peers`. All communication is done +via these `packets`. The `packet` itself consists of a fixed size header, and a variable size body. +The body contains a more specific type of data. + +## Packet header + +The packet header has a fixed size of 4 bytes, with the following layout: + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Version | Type | Reserved | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +The first byte is used to indicate the version of the protocol. Currently, only version 1 is supported +(0x01). The next byte is used to indicate the type of the body. `0x00` indicates a data packet, while +`0x01` indicates a control packet. The remaining 16 bits are currently reserved, and should be set to +all 0. diff --git a/components/mycelium/docs/private_network.md b/components/mycelium/docs/private_network.md new file mode 100644 index 0000000..5f44a54 --- /dev/null +++ b/components/mycelium/docs/private_network.md @@ -0,0 +1,35 @@ +# Private network + +> Private network functionality is currently in an experimental stage + +While traffic is end-to-end encrypted in mycelium, any node in the network learns +every available connected subnet (and can derive the associated default address +in that subnet). As a result, running a mycelium node adds what is effectively +a public interface to your computer, so everyone can send traffic to it. On top +of this, the routing table consumes memory in relation to the amount of nodes in +the network. To remedy this, people can opt to run a "private network". By configuring +a pre shared key (and network name), only nodes which know the key associated to +the name can connect to your network. + +## Implementation + +Private networks are implemented entirely in the connection layer (no specific +protocol logic is implemented to support this). This relies on the pre shared key +functionality of TLS 1.3. As such, you need both a `network name` (an `identity`), +and the `PSK` itself. Next to the limitations in the protocol, we currently further +limit the network name and PSK as follows: + +- Network name must be a UTF-8 encoded string of 2 to 64 bytes. +- PSK must be exactly 32 bytes. + +Not all cipher suites supported in TLS1.3 are supported. At present, _at least_ +`TLS_AES_128_GCM_SHA256` and `TLS_CHACHA20_POLY1305_SHA256` are supported. + +## Enable private network + +In order to use the private network implementation of `mycelium`, a separate `mycelium-private` +binary is available. Private network functionality can be enabled by setting both +the `network-name` and `network-key-file` flags on the command line. All nodes who +wish to join the network must use the same values for both flags. + +> ⚠️ Network name is public, do not put any confidential data here. diff --git a/components/mycelium/docs/topic_configuration.md b/components/mycelium/docs/topic_configuration.md new file mode 100644 index 0000000..dc61090 --- /dev/null +++ b/components/mycelium/docs/topic_configuration.md @@ -0,0 +1,211 @@ +# Topic Configuration Guide + +This document explains how to configure message topics in Mycelium, including how to add new topics, configure socket forwarding paths, and manage whitelisted subnets. + +## Overview + +Mycelium's messaging system uses topics to categorize and route messages. Each topic can be configured with: + +- **Whitelisted subnets**: IP subnets that are allowed to send messages to this topic +- **Forward socket**: A Unix domain socket path where messages for this topic will be forwarded + +When a message is received with a topic that has a configured socket path, the content of the message is pushed to the socket, and the system waits for a reply from the socket, which is then sent back to the original sender. + +## Configuration Using JSON-RPC API + +The JSON-RPC API provides a comprehensive set of methods for managing topics, socket forwarding paths, and whitelisted subnets. + +## Adding a New Topic + +### Using the JSON-RPC API + +```json +{ + "jsonrpc": "2.0", + "method": "addTopic", + "params": ["dGVzdC10b3BpYw=="], // base64 encoding of "test-topic" + "id": 1 +} +``` + +Example using curl: + +```bash +curl -X POST http://localhost:8990/rpc \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "method": "addTopic", + "params": ["dGVzdC10b3BpYw=="], + "id": 1 + }' +``` + + +## Configuring a Socket Forwarding Path + +When a topic is configured with a socket forwarding path, messages for that topic will be forwarded to the specified Unix domain socket instead of being pushed to the message queue. + +### Using the JSON-RPC API + +```json +{ + "jsonrpc": "2.0", + "method": "setTopicForwardSocket", + "params": ["dGVzdC10b3BpYw==", "/path/to/socket"], + "id": 1 +} +``` + +Example using curl: + +```bash +curl -X POST http://localhost:8990/rpc \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "method": "setTopicForwardSocket", + "params": ["dGVzdC10b3BpYw==", "/path/to/socket"], + "id": 1 + }' +``` +``` + +## Adding a Whitelisted Subnet + +Whitelisted subnets control which IP addresses are allowed to send messages to a specific topic. If a message is received from an IP that is not in the whitelist, it will be dropped. + +### Using the JSON-RPC API + +```json +{ + "jsonrpc": "2.0", + "method": "addTopicSource", + "params": ["dGVzdC10b3BpYw==", "192.168.1.0/24"], + "id": 1 +} +``` + +Example using curl: + +```bash +curl -X POST http://localhost:8990/rpc \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "method": "addTopicSource", + "params": ["dGVzdC10b3BpYw==", "192.168.1.0/24"], + "id": 1 + }' +``` +``` + +## Setting the Default Topic Action + +You can configure the default action to take for topics that don't have explicit whitelist configurations: + +### Using the JSON-RPC API + +```json +{ + "jsonrpc": "2.0", + "method": "setDefaultTopicAction", + "params": [true], + "id": 1 +} +``` + +Example using curl: + +```bash +curl -X POST http://localhost:8990/rpc \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "method": "setDefaultTopicAction", + "params": [true], + "id": 1 + }' +``` +``` + +## Socket Protocol + +When a message is forwarded to a socket, the raw message data is sent to the socket. The socket is expected to process the message and send a reply, which will be forwarded back to the original sender. + +The socket protocol is simple: +1. The message data is written to the socket +2. The system waits for a reply from the socket (with a configurable timeout) +3. The reply data is read from the socket and sent back to the original sender + +## Example: Creating a Socket Server + +Here's an example of a simple socket server that echoes back the received data: + +```rust +use std::{ + io::{Read, Write}, + os::unix::net::UnixListener, + path::Path, + thread, +}; + +fn main() { + let socket_path = "/tmp/mycelium-socket"; + + // Remove the socket file if it already exists + if Path::new(socket_path).exists() { + std::fs::remove_file(socket_path).unwrap(); + } + + // Create the Unix domain socket + let listener = UnixListener::bind(socket_path).unwrap(); + println!("Socket server listening on {}", socket_path); + + // Accept connections in a loop + for stream in listener.incoming() { + match stream { + Ok(mut stream) => { + // Spawn a thread to handle the connection + thread::spawn(move || { + // Read the data + let mut buffer = Vec::new(); + stream.read_to_end(&mut buffer).unwrap(); + println!("Received {} bytes", buffer.len()); + + // Process the data (in this case, just echo it back) + // In a real application, you would parse and process the message here + + // Send the reply + stream.write_all(&buffer).unwrap(); + println!("Sent reply"); + }); + } + Err(e) => { + eprintln!("Error accepting connection: {}", e); + } + } + } +} +``` + +## Troubleshooting + +### Message Not Being Forwarded to Socket + +1. Check that the topic is correctly configured with a socket path +2. Verify that the socket server is running and the socket file exists +3. Ensure that the sender's IP is in the whitelisted subnets for the topic +4. Check the logs for any socket connection or timeout errors + +### Socket Server Not Receiving Messages + +1. Verify that the socket path is correct and accessible +2. Check that the socket server has the necessary permissions to read/write to the socket +3. Ensure that the socket server is properly handling the connection + +### Reply Not Being Sent Back + +1. Verify that the socket server is sending a reply +2. Check for any timeout errors in the logs +3. Ensure that the original sender is still connected and able to receive the reply diff --git a/components/mycelium/flake.lock b/components/mycelium/flake.lock new file mode 100644 index 0000000..03f3fe3 --- /dev/null +++ b/components/mycelium/flake.lock @@ -0,0 +1,107 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1742317686, + "narHash": "sha256-ScJYnUykEDhYeCepoAWBbZWx2fpQ8ottyvOyGry7HqE=", + "owner": "ipetkov", + "repo": "crane", + "rev": "66cb0013f9a99d710b167ad13cbd8cc4e64f2ddb", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-compat": { + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "revCount": 69, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "id": "flake-utils", + "type": "indirect" + } + }, + "nix-filter": { + "locked": { + "lastModified": 1731533336, + "narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=", + "owner": "numtide", + "repo": "nix-filter", + "rev": "f7653272fd234696ae94229839a99b73c9ab7de0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "nix-filter", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1742288794, + "narHash": "sha256-Txwa5uO+qpQXrNG4eumPSD+hHzzYi/CdaM80M9XRLCo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b6eaf97c6960d97350c584de1b6dcff03c9daf42", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "nix-filter": "nix-filter", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/components/mycelium/flake.nix b/components/mycelium/flake.nix new file mode 100644 index 0000000..d514c81 --- /dev/null +++ b/components/mycelium/flake.nix @@ -0,0 +1,156 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + crane.url = "github:ipetkov/crane"; + + flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; + + nix-filter.url = "github:numtide/nix-filter"; + }; + + outputs = + { self + , crane + , flake-utils + , nix-filter + , ... + }@inputs: + { + overlays.default = final: prev: + let + inherit (final) lib stdenv darwin; + craneLib = crane.mkLib final; + in + { + myceliumd = + let + cargoToml = ./myceliumd/Cargo.toml; + cargoLock = ./myceliumd/Cargo.lock; + manifest = craneLib.crateNameFromCargoToml { inherit cargoToml; }; + in + lib.makeOverridable craneLib.buildPackage { + src = nix-filter { + root = ./.; + + # If no include is passed, it will include all the paths. + include = [ + ./Cargo.toml + ./Cargo.lock + ./mycelium + ./mycelium-api + ./mycelium-cli + ./mycelium-metrics + ./myceliumd + ./myceliumd-private + ./mobile + ./docs + ]; + }; + + inherit (manifest) pname version; + inherit cargoToml cargoLock; + sourceRoot = "source/myceliumd"; + + doCheck = false; + + nativeBuildInputs = [ + final.pkg-config + # openssl base library + final.openssl + # required by openssl-sys + final.perl + ]; + + buildInputs = lib.optionals stdenv.isDarwin [ + darwin.apple_sdk.frameworks.Security + darwin.apple_sdk.frameworks.SystemConfiguration + final.libiconv + ]; + + meta = { + mainProgram = "mycelium"; + }; + }; + myceliumd-private = + let + cargoToml = ./myceliumd-private/Cargo.toml; + cargoLock = ./myceliumd-private/Cargo.lock; + manifest = craneLib.crateNameFromCargoToml { inherit cargoToml; }; + in + lib.makeOverridable craneLib.buildPackage { + src = nix-filter { + root = ./.; + + include = [ + ./Cargo.toml + ./Cargo.lock + ./mycelium + ./mycelium-api + ./mycelium-cli + ./mycelium-metrics + ./myceliumd + ./myceliumd-private + ./mobile + ./docs + ]; + }; + + inherit (manifest) pname version; + inherit cargoToml cargoLock; + sourceRoot = "source/myceliumd-private"; + + doCheck = false; + + nativeBuildInputs = [ + final.pkg-config + # openssl base library + final.openssl + # required by openssl-sys + final.perl + ]; + + buildInputs = lib.optionals stdenv.isDarwin [ + darwin.apple_sdk.frameworks.Security + darwin.apple_sdk.frameworks.SystemConfiguration + final.libiconv + ]; + + meta = { + mainProgram = "mycelium-private"; + }; + }; + }; + } // + flake-utils.lib.eachSystem + [ + flake-utils.lib.system.x86_64-linux + flake-utils.lib.system.aarch64-linux + flake-utils.lib.system.x86_64-darwin + flake-utils.lib.system.aarch64-darwin + ] + (system: + let + craneLib = crane.mkLib pkgs; + + pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + }; + in + { + devShells.default = craneLib.devShell { + packages = [ + pkgs.rust-analyzer + ]; + + RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; + }; + + packages = { + default = self.packages.${system}.myceliumd; + + inherit (pkgs) myceliumd myceliumd-private; + }; + }); +} diff --git a/components/mycelium/installers/windows/wix/LICENSE.rtf b/components/mycelium/installers/windows/wix/LICENSE.rtf new file mode 100644 index 0000000..79d56b3 --- /dev/null +++ b/components/mycelium/installers/windows/wix/LICENSE.rtf @@ -0,0 +1,206 @@ +{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{\fonttbl{\f0\fnil\fcharset0 Calibri;}} +{\colortbl ;\red0\green0\blue255;} +{\*\generator Riched20 10.0.22621}\viewkind4\uc1 +\pard\sa200\sl276\slmult1\f0\fs22\lang9 Apache License\par + Version 2.0, January 2004\par + {{\field{\*\fldinst{HYPERLINK http://www.apache.org/licenses/ }}{\fldrslt{http://www.apache.org/licenses/\ul0\cf0}}}}\f0\fs22\par +\par + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\par +\par + 1. Definitions.\par +\par + "License" shall mean the terms and conditions for use, reproduction,\par + and distribution as defined by Sections 1 through 9 of this document.\par +\par + "Licensor" shall mean the copyright owner or entity authorized by\par + the copyright owner that is granting the License.\par +\par + "Legal Entity" shall mean the union of the acting entity and all\par + other entities that control, are controlled by, or are under common\par + control with that entity. For the purposes of this definition,\par + "control" means (i) the power, direct or indirect, to cause the\par + direction or management of such entity, whether by contract or\par + otherwise, or (ii) ownership of fifty percent (50%) or more of the\par + outstanding shares, or (iii) beneficial ownership of such entity.\par +\par + "You" (or "Your") shall mean an individual or Legal Entity\par + exercising permissions granted by this License.\par +\par + "Source" form shall mean the preferred form for making modifications,\par + including but not limited to software source code, documentation\par + source, and configuration files.\par +\par + "Object" form shall mean any form resulting from mechanical\par + transformation or translation of a Source form, including but\par + not limited to compiled object code, generated documentation,\par + and conversions to other media types.\par +\par + "Work" shall mean the work of authorship, whether in Source or\par + Object form, made available under the License, as indicated by a\par + copyright notice that is included in or attached to the work\par + (an example is provided in the Appendix below).\par +\par + "Derivative Works" shall mean any work, whether in Source or Object\par + form, that is based on (or derived from) the Work and for which the\par + editorial revisions, annotations, elaborations, or other modifications\par + represent, as a whole, an original work of authorship. For the purposes\par + of this License, Derivative Works shall not include works that remain\par + separable from, or merely link (or bind by name) to the interfaces of,\par + the Work and Derivative Works thereof.\par +\par + "Contribution" shall mean any work of authorship, including\par + the original version of the Work and any modifications or additions\par + to that Work or Derivative Works thereof, that is intentionally\par + submitted to Licensor for inclusion in the Work by the copyright owner\par + or by an individual or Legal Entity authorized to submit on behalf of\par + the copyright owner. For the purposes of this definition, "submitted"\par + means any form of electronic, verbal, or written communication sent\par + to the Licensor or its representatives, including but not limited to\par + communication on electronic mailing lists, source code control systems,\par + and issue tracking systems that are managed by, or on behalf of, the\par + Licensor for the purpose of discussing and improving the Work, but\par + excluding communication that is conspicuously marked or otherwise\par + designated in writing by the copyright owner as "Not a Contribution."\par +\par + "Contributor" shall mean Licensor and any individual or Legal Entity\par + on behalf of whom a Contribution has been received by Licensor and\par + subsequently incorporated within the Work.\par +\par + 2. Grant of Copyright License. Subject to the terms and conditions of\par + this License, each Contributor hereby grants to You a perpetual,\par + worldwide, non-exclusive, no-charge, royalty-free, irrevocable\par + copyright license to reproduce, prepare Derivative Works of,\par + publicly display, publicly perform, sublicense, and distribute the\par + Work and such Derivative Works in Source or Object form.\par +\par + 3. Grant of Patent License. Subject to the terms and conditions of\par + this License, each Contributor hereby grants to You a perpetual,\par + worldwide, non-exclusive, no-charge, royalty-free, irrevocable\par + (except as stated in this section) patent license to make, have made,\par + use, offer to sell, sell, import, and otherwise transfer the Work,\par + where such license applies only to those patent claims licensable\par + by such Contributor that are necessarily infringed by their\par + Contribution(s) alone or by combination of their Contribution(s)\par + with the Work to which such Contribution(s) was submitted. If You\par + institute patent litigation against any entity (including a\par + cross-claim or counterclaim in a lawsuit) alleging that the Work\par + or a Contribution incorporated within the Work constitutes direct\par + or contributory patent infringement, then any patent licenses\par + granted to You under this License for that Work shall terminate\par + as of the date such litigation is filed.\par +\par + 4. Redistribution. You may reproduce and distribute copies of the\par + Work or Derivative Works thereof in any medium, with or without\par + modifications, and in Source or Object form, provided that You\par + meet the following conditions:\par +\par + (a) You must give any other recipients of the Work or\par + Derivative Works a copy of this License; and\par +\par + (b) You must cause any modified files to carry prominent notices\par + stating that You changed the files; and\par +\par + (c) You must retain, in the Source form of any Derivative Works\par + that You distribute, all copyright, patent, trademark, and\par + attribution notices from the Source form of the Work,\par + excluding those notices that do not pertain to any part of\par + the Derivative Works; and\par +\par + (d) If the Work includes a "NOTICE" text file as part of its\par + distribution, then any Derivative Works that You distribute must\par + include a readable copy of the attribution notices contained\par + within such NOTICE file, excluding those notices that do not\par + pertain to any part of the Derivative Works, in at least one\par + of the following places: within a NOTICE text file distributed\par + as part of the Derivative Works; within the Source form or\par + documentation, if provided along with the Derivative Works; or,\par + within a display generated by the Derivative Works, if and\par + wherever such third-party notices normally appear. The contents\par + of the NOTICE file are for informational purposes only and\par + do not modify the License. You may add Your own attribution\par + notices within Derivative Works that You distribute, alongside\par + or as an addendum to the NOTICE text from the Work, provided\par + that such additional attribution notices cannot be construed\par + as modifying the License.\par +\par + You may add Your own copyright statement to Your modifications and\par + may provide additional or different license terms and conditions\par + for use, reproduction, or distribution of Your modifications, or\par + for any such Derivative Works as a whole, provided Your use,\par + reproduction, and distribution of the Work otherwise complies with\par + the conditions stated in this License.\par +\par + 5. Submission of Contributions. Unless You explicitly state otherwise,\par + any Contribution intentionally submitted for inclusion in the Work\par + by You to the Licensor shall be under the terms and conditions of\par + this License, without any additional terms or conditions.\par + Notwithstanding the above, nothing herein shall supersede or modify\par + the terms of any separate license agreement you may have executed\par + with Licensor regarding such Contributions.\par +\par + 6. Trademarks. This License does not grant permission to use the trade\par + names, trademarks, service marks, or product names of the Licensor,\par + except as required for reasonable and customary use in describing the\par + origin of the Work and reproducing the content of the NOTICE file.\par +\par + 7. Disclaimer of Warranty. Unless required by applicable law or\par + agreed to in writing, Licensor provides the Work (and each\par + Contributor provides its Contributions) on an "AS IS" BASIS,\par + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\par + implied, including, without limitation, any warranties or conditions\par + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\par + PARTICULAR PURPOSE. You are solely responsible for determining the\par + appropriateness of using or redistributing the Work and assume any\par + risks associated with Your exercise of permissions under this License.\par +\par + 8. Limitation of Liability. In no event and under no legal theory,\par + whether in tort (including negligence), contract, or otherwise,\par + unless required by applicable law (such as deliberate and grossly\par + negligent acts) or agreed to in writing, shall any Contributor be\par + liable to You for damages, including any direct, indirect, special,\par + incidental, or consequential damages of any character arising as a\par + result of this License or out of the use or inability to use the\par + Work (including but not limited to damages for loss of goodwill,\par + work stoppage, computer failure or malfunction, or any and all\par + other commercial damages or losses), even if such Contributor\par + has been advised of the possibility of such damages.\par +\par + 9. Accepting Warranty or Additional Liability. While redistributing\par + the Work or Derivative Works thereof, You may choose to offer,\par + and charge a fee for, acceptance of support, warranty, indemnity,\par + or other liability obligations and/or rights consistent with this\par + License. However, in accepting such obligations, You may act only\par + on Your own behalf and on Your sole responsibility, not on behalf\par + of any other Contributor, and only if You agree to indemnify,\par + defend, and hold each Contributor harmless for any liability\par + incurred by, or claims asserted against, such Contributor by reason\par + of your accepting any such warranty or additional liability.\par +\par + END OF TERMS AND CONDITIONS\par +\par + APPENDIX: How to apply the Apache License to your work.\par +\par + To apply the Apache License to your work, attach the following\par + boilerplate notice, with the fields enclosed by brackets "[]"\par + replaced with your own identifying information. (Don't include\par + the brackets!) The text should be enclosed in the appropriate\par + comment syntax for the file format. We also recommend that a\par + file or class name and description of purpose be included on the\par + same "printed page" as the copyright notice for easier\par + identification within third-party archives.\par +\par + Copyright TF Tech NV\par +\par + Licensed under the Apache License, Version 2.0 (the "License");\par + you may not use this file except in compliance with the License.\par + You may obtain a copy of the License at\par +\par + {{\field{\*\fldinst{HYPERLINK http://www.apache.org/licenses/LICENSE-2.0 }}{\fldrslt{http://www.apache.org/licenses/LICENSE-2.0\ul0\cf0}}}}\f0\fs22\par +\par + Unless required by applicable law or agreed to in writing, software\par + distributed under the License is distributed on an "AS IS" BASIS,\par + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\par + See the License for the specific language governing permissions and\par + limitations under the License.\par +} + \ No newline at end of file diff --git a/components/mycelium/installers/windows/wix/mycelium.en-us.wxl b/components/mycelium/installers/windows/wix/mycelium.en-us.wxl new file mode 100644 index 0000000..3f49151 --- /dev/null +++ b/components/mycelium/installers/windows/wix/mycelium.en-us.wxl @@ -0,0 +1,8 @@ + + + + + + diff --git a/components/mycelium/installers/windows/wix/mycelium.wxs b/components/mycelium/installers/windows/wix/mycelium.wxs new file mode 100644 index 0000000..73d3327 --- /dev/null +++ b/components/mycelium/installers/windows/wix/mycelium.wxs @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/mycelium/mobile/.gitignore b/components/mycelium/mobile/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/components/mycelium/mobile/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/components/mycelium/mobile/Cargo.lock b/components/mycelium/mobile/Cargo.lock new file mode 100644 index 0000000..a664559 --- /dev/null +++ b/components/mycelium/mobile/Cargo.lock @@ -0,0 +1,3426 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_log-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.100", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "c2rust-bitfields" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b43c3f07ab0ef604fa6f595aa46ec2f8a22172c975e186f6f5bf9829a3b72c41" +dependencies = [ + "c2rust-bitfields-derive", +] + +[[package]] +name = "c2rust-bitfields-derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3cbc102e2597c9744c8bd8c15915d554300601c91a079430d309816b0912545" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cc" +version = "1.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +dependencies = [ + "shlex", +] + +[[package]] +name = "cdn-meta" +version = "0.1.0" +source = "git+https://github.com/threefoldtech/mycelium-cdn-registry#2fac04710b60502a5165f1b1d90671730346e977" +dependencies = [ + "aes-gcm", + "bincode", + "serde", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "etherparse" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff83a5facf1a7cbfef93cfb48d6d4fb6a1f42d8ac2341a96b3255acb4d4f860" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.9", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", +] + +[[package]] +name = "ioctl-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd11f3a29434026f5ff98c730b668ba74b1033637b8817940b54d040696133c" + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "left-right" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabfddf3ad712b726484562039aa6fc2014bc1b5c088bb211b208052cf0439e6" +dependencies = [ + "loom", + "slab", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "mobile" +version = "0.1.0" +dependencies = [ + "mycelium", + "once_cell", + "thiserror 2.0.12", + "tokio", + "tracing", + "tracing-android", + "tracing-oslog", + "tracing-subscriber", +] + +[[package]] +name = "mycelium" +version = "0.6.1" +dependencies = [ + "aes-gcm", + "ahash 0.8.11", + "arc-swap", + "axum", + "axum-extra", + "blake3", + "bytes", + "cdn-meta", + "dashmap", + "etherparse", + "faster-hex", + "futures", + "ip_network_table-deps-treebitmap", + "ipnet", + "left-right", + "libc", + "netdev", + "nix 0.29.0", + "nix 0.30.1", + "openssl", + "quinn", + "rand", + "rcgen", + "redis", + "reed-solomon-erasure", + "reqwest", + "rtnetlink", + "rustls", + "serde", + "tokio", + "tokio-stream", + "tokio-tun", + "tokio-util", + "tracing", + "tracing-logfmt", + "tracing-subscriber", + "tun", + "wintun 0.5.1", + "x25519-dalek", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "netdev" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862209dce034f82a44c95ce2b5183730d616f2a68746b9c1959aa2572e77c0a1" +dependencies = [ + "dlopen2", + "ipnet", + "libc", + "netlink-packet-core", + "netlink-packet-route 0.22.0", + "netlink-sys", + "once_cell", + "system-configuration", + "windows-sys 0.59.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d83370a96813d7c977f8b63054f1162df6e5784f1c598d689236564fb5a6f2" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-proto" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.12", +] + +[[package]] +name = "netlink-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +dependencies = [ + "bytes", + "futures", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-src" +version = "300.5.0+3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.11", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.24", +] + +[[package]] +name = "prettyplease" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +dependencies = [ + "proc-macro2", + "syn 2.0.100", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.5.9", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.2", + "lru-slab", + "rand", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.9", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rcgen" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49bc8ffa8a832eb1d7c8000337f8b0d2f4f2f5ec3cf4ddc26f125e3ad2451824" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redis" +version = "0.32.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f66bf4cac9733a23bcdf1e0e01effbaaad208567beba68be8f67e5f4af3ee1" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.6.0", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "reed-solomon-erasure" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7263373d500d4d4f505d43a2a662d475a894aa94503a1ee28e9188b5f3960d4f" +dependencies = [ + "libm", + "lru", + "parking_lot 0.11.2", + "smallvec", + "spin", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rtnetlink" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbe0a03f6b9b483c67d4b328fc5d66c8db0b6aa274e0fa2def71b5e442a69acf" +dependencies = [ + "futures", + "log", + "netlink-packet-core", + "netlink-packet-route 0.24.0", + "netlink-packet-utils", + "netlink-proto", + "netlink-sys", + "nix 0.29.0", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.5.9", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tun" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be25a316a2d10d0ddd46de9ddd73cdb4262678e1e52f4a32a31421f1e2b816b4" +dependencies = [ + "libc", + "nix 0.29.0", + "thiserror 2.0.12", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12612be8f868a09c0ceae7113ff26afe79d81a24473a393cb9120ece162e86c0" +dependencies = [ + "android_log-sys", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-logfmt" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1f47d22deb79c3f59fcf2a1f00f60cbdc05462bf17d1cd356c1fefa3f444bd" +dependencies = [ + "nu-ansi-term 0.50.1", + "time", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-oslog" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528bdd1f0e27b5dd9a4ededf154e824b0532731e4af73bb531de46276e0aab1e" +dependencies = [ + "bindgen", + "cc", + "cfg-if", + "once_cell", + "parking_lot 0.12.3", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term 0.46.0", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tun" +version = "0.6.1" +source = "git+https://github.com/LeeSmet/rust-tun#ae4c222e7a7aba5752cb08d2dbaf67bbc669c26a" +dependencies = [ + "byteorder", + "bytes", + "futures-core", + "ioctl-sys", + "libc", + "log", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "wintun 0.3.2", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wintun" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b83b0eca06dd125dbcd48a45327c708a6da8aada3d95a3f06db0ce4b17e0d4" +dependencies = [ + "c2rust-bitfields", + "libloading", + "log", + "thiserror 1.0.69", + "windows 0.51.1", +] + +[[package]] +name = "wintun" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da99be64b5aa3de869c16977994314d0759a698d9a73ab0a5b1d52e2282033ae" +dependencies = [ + "c2rust-bitfields", + "libloading", + "log", + "thiserror 1.0.69", + "windows-sys 0.52.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] diff --git a/components/mycelium/mobile/Cargo.toml b/components/mycelium/mobile/Cargo.toml new file mode 100644 index 0000000..feda14b --- /dev/null +++ b/components/mycelium/mobile/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "mobile" +version = "0.1.0" +edition = "2021" + +[features] +mactunfd = ["mycelium/mactunfd"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +mycelium = { path = "../mycelium", features = ["vendored-openssl"] } +tokio = { version = "1.44.0", features = ["signal", "rt-multi-thread"] } +thiserror = "2.0.12" +tracing = { version = "0.1.41", features = ["release_max_level_debug"] } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +once_cell = "1.21.1" + +[target.'cfg(target_os = "android")'.dependencies] +tracing-android = "0.2.0" + +[target.'cfg(target_os = "ios")'.dependencies] +tracing-oslog = "0.2.0" + +[target.'cfg(target_os = "macos")'.dependencies] +tracing-oslog = "0.2.0" diff --git a/components/mycelium/mobile/README.md b/components/mycelium/mobile/README.md new file mode 100644 index 0000000..e9f8f9e --- /dev/null +++ b/components/mycelium/mobile/README.md @@ -0,0 +1,3 @@ +# mobile crate + +This crate will be called from Dart/Flutter, Kotlin(Android), or Swift(iOS) \ No newline at end of file diff --git a/components/mycelium/mobile/src/lib.rs b/components/mycelium/mobile/src/lib.rs new file mode 100644 index 0000000..bc9b705 --- /dev/null +++ b/components/mycelium/mobile/src/lib.rs @@ -0,0 +1,284 @@ +use std::convert::TryFrom; +use std::io; + +use tracing::{error, info}; + +use metrics::Metrics; +use mycelium::endpoint::Endpoint; +use mycelium::{crypto, metrics, Config, Node}; +use once_cell::sync::Lazy; +use tokio::sync::{mpsc, Mutex}; +use tokio::time::{sleep, timeout, Duration}; + +const CHANNEL_MSG_OK: &str = "ok"; +const CHANNEL_TIMEOUT: u64 = 2; + +#[cfg(target_os = "android")] +fn setup_logging() { + use tracing::level_filters::LevelFilter; + use tracing_subscriber::filter::Targets; + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + let targets = Targets::new() + .with_default(LevelFilter::INFO) + .with_target("mycelium::router", LevelFilter::WARN); + tracing_subscriber::registry() + .with(tracing_android::layer("mycelium").expect("failed to setup logger")) + .with(targets) + .init(); +} + +#[cfg(any(target_os = "ios", target_os = "macos"))] +fn setup_logging() { + use tracing::level_filters::LevelFilter; + use tracing_oslog::OsLogger; + use tracing_subscriber::filter::Targets; + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + let targets = Targets::new() + .with_default(LevelFilter::INFO) + .with_target("mycelium::router", LevelFilter::WARN); + tracing_subscriber::registry() + .with(OsLogger::new("mycelium", "default")) + .with(targets) + .init(); +} + +#[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] +static INIT_LOG: Lazy<()> = Lazy::new(|| { + setup_logging(); +}); + +#[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] +fn setup_logging_once() { + // Accessing the Lazy value will ensure setup_logging is called exactly once + let _ = &*INIT_LOG; +} + +// Declare the channel globally so we can use it on the start & stop mycelium functions +type CommandChannelType = (Mutex>, Mutex>); +static COMMAND_CHANNEL: Lazy = Lazy::new(|| { + let (tx_cmd, rx_cmd) = mpsc::channel::(1); + (Mutex::new(tx_cmd), Mutex::new(rx_cmd)) +}); + +type ResponseChannelType = ( + Mutex>, + Mutex>, +); +static RESPONSE_CHANNEL: Lazy = Lazy::new(|| { + let (tx_resp, rx_resp) = mpsc::channel::(1); + (Mutex::new(tx_resp), Mutex::new(rx_resp)) +}); + +#[tokio::main] +#[allow(unused_variables)] // because tun_fd is only used in android and ios +pub async fn start_mycelium(peers: Vec, tun_fd: i32, priv_key: Vec) { + #[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] + setup_logging_once(); + + info!("starting mycelium"); + let endpoints: Vec = peers + .into_iter() + .filter_map(|peer| peer.parse().ok()) + .collect(); + + let secret_key = build_secret_key(priv_key).await.unwrap(); + + let config = Config { + node_key: secret_key, + peers: endpoints, + no_tun: false, + tcp_listen_port: DEFAULT_TCP_LISTEN_PORT, + quic_listen_port: None, + peer_discovery_port: None, // disable multicast discovery + #[cfg(any( + target_os = "linux", + all(target_os = "macos", not(feature = "mactunfd")), + target_os = "windows" + ))] + tun_name: "tun0".to_string(), + + metrics: NoMetrics, + private_network_config: None, + firewall_mark: None, + #[cfg(any( + target_os = "android", + target_os = "ios", + all(target_os = "macos", feature = "mactunfd"), + ))] + tun_fd: Some(tun_fd), + update_workers: 1, + cdn_cache: None, + }; + let _node = match Node::new(config).await { + Ok(node) => { + info!("node successfully created"); + node + } + Err(err) => { + error!("failed to create mycelium node: {err}"); + return; + } + }; + + let mut rx = COMMAND_CHANNEL.1.lock().await; + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + info!("Received SIGINT, stopping mycelium node"); + break; + } + cmd = rx.recv() => { + match cmd.unwrap().cmd { + CmdType::Stop => { + info!("Received stop command, stopping mycelium node"); + send_response(vec![CHANNEL_MSG_OK.to_string()]).await; + break; + } + CmdType::Status => { + let mut vec: Vec = Vec::new(); + for info in _node.peer_info() { + vec.push(info.endpoint.proto().to_string() + ","+ info.endpoint.address().to_string().as_str()+","+ &info.connection_state.to_string()); + } + send_response(vec).await; + } + } + } + } + } + info!("mycelium stopped"); +} + +struct Cmd { + cmd: CmdType, +} + +enum CmdType { + Stop, + Status, +} + +struct Response { + response: Vec, +} + +// stop_mycelium returns string with the status of the command +#[tokio::main] +pub async fn stop_mycelium() -> String { + if let Err(e) = send_command(CmdType::Stop).await { + return e.to_string(); + } + + match recv_response().await { + Ok(_) => CHANNEL_MSG_OK.to_string(), + Err(e) => e.to_string(), + } +} + +// get_peer_status returns vector of string +// first element is always the status of the command (ok or error) +// next elements are the peer status +#[tokio::main] +pub async fn get_peer_status() -> Vec { + if let Err(e) = send_command(CmdType::Status).await { + return vec![e.to_string()]; + } + + match recv_response().await { + Ok(mut resp) => { + resp.insert(0, CHANNEL_MSG_OK.to_string()); + resp + } + Err(e) => vec![e.to_string()], + } +} + +#[tokio::main] +pub async fn get_status() -> Result { + Err(NodeError::NodeDead) +} + +use thiserror::Error; +#[derive(Error, Debug)] +pub enum NodeError { + #[error("err_node_dead")] + NodeDead, + + #[error("err_node_timeout")] + NodeTimeout, +} + +async fn send_command(cmd_type: CmdType) -> Result<(), NodeError> { + let tx = COMMAND_CHANNEL.0.lock().await; + tokio::select! { + _ = sleep(Duration::from_secs(CHANNEL_TIMEOUT)) => { + Err(NodeError::NodeTimeout) + } + result = tx.send(Cmd { cmd: cmd_type }) => { + match result { + Ok(_) => Ok(()), + Err(_) => Err(NodeError::NodeDead) + } + } + } +} + +async fn send_response(resp: Vec) { + let tx = RESPONSE_CHANNEL.0.lock().await; + + tokio::select! { + _ = sleep(Duration::from_secs(CHANNEL_TIMEOUT)) => { + error!("send_response timeout"); + } + result = tx.send(Response { response: resp }) => { + match result { + Ok(_) => {}, + Err(_) =>{error!("send_response failed");}, + } + } + } +} + +async fn recv_response() -> Result, NodeError> { + let mut rx = RESPONSE_CHANNEL.1.lock().await; + let duration = Duration::from_secs(CHANNEL_TIMEOUT); + match timeout(duration, rx.recv()).await { + Ok(result) => match result { + Some(resp) => Ok(resp.response), + None => Err(NodeError::NodeDead), + }, + Err(_) => Err(NodeError::NodeTimeout), + } +} + +#[derive(Clone)] +pub struct NoMetrics; +impl Metrics for NoMetrics {} + +/// The default port on the underlay to listen on for incoming TCP connections. +const DEFAULT_TCP_LISTEN_PORT: u16 = 9651; + +fn convert_slice_to_array32(slice: &[u8]) -> Result<[u8; 32], std::array::TryFromSliceError> { + <[u8; 32]>::try_from(slice) +} + +async fn build_secret_key(bin: Vec) -> Result +where + T: From<[u8; 32]>, +{ + Ok(T::from(convert_slice_to_array32(bin.as_slice()).unwrap())) +} + +/// generate secret key +/// it is used by android & ios app +pub fn generate_secret_key() -> Vec { + crypto::SecretKey::new().as_bytes().into() +} + +/// generate node_address from secret key +pub fn address_from_secret_key(data: Vec) -> String { + let data = <[u8; 32]>::try_from(data.as_slice()).unwrap(); + let secret_key = crypto::SecretKey::from(data); + crypto::PublicKey::from(&secret_key).address().to_string() +} diff --git a/components/mycelium/mycelium-api/Cargo.toml b/components/mycelium/mycelium-api/Cargo.toml new file mode 100644 index 0000000..2067221 --- /dev/null +++ b/components/mycelium/mycelium-api/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "mycelium-api" +version = "0.6.1" +edition = "2021" +license-file = "../LICENSE" +readme = "../README.md" + +[features] +message = ["mycelium/message"] + +[dependencies] +axum = { version = "0.8.4", default-features = false, features = [ + "http1", + "http2", + "json", + "query", + "tokio", +] } +base64 = "0.22.1" +jsonrpsee = { version = "0.25.1", features = [ + "server", + "macros", + "jsonrpsee-types", +] } +serde_json = "1.0.140" +tracing = "0.1.41" +tokio = { version = "1.46.1", default-features = false, features = [ + "net", + "rt", +] } +mycelium = { path = "../mycelium" } +mycelium-metrics = { path = "../mycelium-metrics", features = ["prometheus"] } +serde = { version = "1.0.219", features = ["derive"] } +async-trait = "0.1.88" + +[dev-dependencies] +serde_json = "1.0.140" diff --git a/components/mycelium/mycelium-api/src/lib.rs b/components/mycelium/mycelium-api/src/lib.rs new file mode 100644 index 0000000..ca1c46e --- /dev/null +++ b/components/mycelium/mycelium-api/src/lib.rs @@ -0,0 +1,492 @@ +use core::fmt; +use std::{net::IpAddr, net::SocketAddr, str::FromStr, sync::Arc}; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + routing::{delete, get}, + Json, Router, +}; +use serde::{de, Deserialize, Deserializer, Serialize}; +use tokio::{sync::Mutex, time::Instant}; +use tracing::{debug, error}; + +use mycelium::{ + crypto::PublicKey, + endpoint::Endpoint, + metrics::Metrics, + peer_manager::{PeerExists, PeerNotFound, PeerStats}, +}; + +const INFINITE_STR: &str = "infinite"; + +#[cfg(feature = "message")] +mod message; +#[cfg(feature = "message")] +pub use message::{MessageDestination, MessageReceiveInfo, MessageSendInfo, PushMessageResponse}; + +pub use rpc::JsonRpc; + +// JSON-RPC API implementation +pub mod rpc; + +/// Http API server handle. The server is spawned in a background task. If this handle is dropped, +/// the server is terminated. +pub struct Http { + /// Channel to send cancellation to the http api server. We just keep a reference to it since + /// dropping it will also cancel the receiver and thus the server. + _cancel_tx: tokio::sync::oneshot::Sender<()>, +} + +#[derive(Clone)] +/// Shared state accessible in HTTP endpoint handlers. +pub struct ServerState { + /// Access to the (`node`)(mycelium::Node) state. + pub node: Arc>>, +} + +impl Http { + /// Spawns a new HTTP API server on the provided listening address. + pub fn spawn(node: Arc>>, listen_addr: SocketAddr) -> Self + where + M: Metrics + Clone + Send + Sync + 'static, + { + let server_state = ServerState { node }; + let admin_routes = Router::new() + .route("/admin", get(get_info)) + .route("/admin/peers", get(get_peers).post(add_peer)) + .route("/admin/peers/{endpoint}", delete(delete_peer)) + .route("/admin/routes/selected", get(get_selected_routes)) + .route("/admin/routes/fallback", get(get_fallback_routes)) + .route("/admin/routes/queried", get(get_queried_routes)) + .route("/admin/routes/no_route", get(get_no_route_entries)) + .route("/pubkey/{ip}", get(get_pubk_from_ip)) + .with_state(server_state.clone()); + let app = Router::new().nest("/api/v1", admin_routes); + #[cfg(feature = "message")] + let app = app.nest("/api/v1", message::message_router_v1(server_state)); + + let (_cancel_tx, cancel_rx) = tokio::sync::oneshot::channel(); + + tokio::spawn(async move { + let listener = match tokio::net::TcpListener::bind(listen_addr).await { + Ok(listener) => listener, + Err(e) => { + error!(err=%e, "Failed to bind listener for Http Api server"); + error!("API disabled"); + return; + } + }; + + let server = + axum::serve(listener, app.into_make_service()).with_graceful_shutdown(async { + cancel_rx.await.ok(); + }); + + if let Err(e) = server.await { + error!(err=%e, "Http API server error"); + } + }); + Http { _cancel_tx } + } +} + +/// Get the stats of the current known peers +async fn get_peers(State(state): State>) -> Json> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Fetching peer stats"); + Json(state.node.lock().await.peer_info()) +} + +/// Payload of an add_peer request +#[derive(Deserialize, Serialize)] +pub struct AddPeer { + /// The endpoint used to connect to the peer + pub endpoint: String, +} + +/// Add a new peer to the system +async fn add_peer( + State(state): State>, + Json(payload): Json, +) -> Result +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!( + peer.endpoint = payload.endpoint, + "Attempting to add peer to the system" + ); + let endpoint = match Endpoint::from_str(&payload.endpoint) { + Ok(endpoint) => endpoint, + Err(e) => return Err((StatusCode::BAD_REQUEST, e.to_string())), + }; + + match state.node.lock().await.add_peer(endpoint) { + Ok(()) => Ok(StatusCode::NO_CONTENT), + Err(PeerExists) => Err(( + StatusCode::CONFLICT, + "A peer identified by that endpoint already exists".to_string(), + )), + } +} + +/// remove an existing peer from the system +async fn delete_peer( + State(state): State>, + Path(endpoint): Path, +) -> Result +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!(peer.endpoint=%endpoint, "Attempting to remove peer from the system"); + let endpoint = match Endpoint::from_str(&endpoint) { + Ok(endpoint) => endpoint, + Err(e) => return Err((StatusCode::BAD_REQUEST, e.to_string())), + }; + + match state.node.lock().await.remove_peer(endpoint) { + Ok(()) => Ok(StatusCode::NO_CONTENT), + Err(PeerNotFound) => Err(( + StatusCode::NOT_FOUND, + "A peer identified by that endpoint does not exist".to_string(), + )), + } +} + +/// Alias to a [`Metric`](crate::metric::Metric) for serialization in the API. +#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] +pub enum Metric { + /// Finite metric + Value(u16), + /// Infinite metric + Infinite, +} + +/// Info about a route. This uses base types only to avoid having to introduce too many Serialize +/// bounds in the core types. +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord)] +#[serde(rename_all = "camelCase")] +pub struct Route { + /// We convert the [`subnet`](Subnet) to a string to avoid introducing a bound on the actual + /// type. + pub subnet: String, + /// Next hop of the route, in the underlay. + pub next_hop: String, + /// Computed metric of the route. + pub metric: Metric, + /// Sequence number of the route. + pub seqno: u16, +} + +/// List all currently selected routes. +async fn get_selected_routes(State(state): State>) -> Json> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Loading selected routes"); + let routes = state + .node + .lock() + .await + .selected_routes() + .into_iter() + .map(|sr| Route { + subnet: sr.source().subnet().to_string(), + next_hop: sr.neighbour().connection_identifier().clone(), + metric: if sr.metric().is_infinite() { + Metric::Infinite + } else { + Metric::Value(sr.metric().into()) + }, + seqno: sr.seqno().into(), + }) + .collect(); + + Json(routes) +} + +/// List all active fallback routes. +async fn get_fallback_routes(State(state): State>) -> Json> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Loading fallback routes"); + let routes = state + .node + .lock() + .await + .fallback_routes() + .into_iter() + .map(|sr| Route { + subnet: sr.source().subnet().to_string(), + next_hop: sr.neighbour().connection_identifier().clone(), + metric: if sr.metric().is_infinite() { + Metric::Infinite + } else { + Metric::Value(sr.metric().into()) + }, + seqno: sr.seqno().into(), + }) + .collect(); + + Json(routes) +} + +/// Info about a queried subnet. This uses base types only to avoid having to introduce too +/// many Serialize bounds in the core types. +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord)] +#[serde(rename_all = "camelCase")] +pub struct QueriedSubnet { + /// We convert the [`subnet`](Subnet) to a string to avoid introducing a bound on the actual + /// type. + pub subnet: String, + /// The amount of time left before the query expires. + pub expiration: String, +} + +async fn get_queried_routes(State(state): State>) -> Json> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Loading queried subnets"); + let queries = state + .node + .lock() + .await + .queried_subnets() + .into_iter() + .map(|qs| QueriedSubnet { + subnet: qs.subnet().to_string(), + expiration: qs + .query_expires() + .duration_since(Instant::now()) + .as_secs() + .to_string(), + }) + .collect(); + + Json(queries) +} + +/// Info about a subnet with no route. This uses base types only to avoid having to introduce too +/// many Serialize bounds in the core types. +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord)] +#[serde(rename_all = "camelCase")] +pub struct NoRouteSubnet { + /// We convert the [`subnet`](Subnet) to a string to avoid introducing a bound on the actual + /// type. + pub subnet: String, + /// The amount of time left before the query expires. + pub expiration: String, +} + +async fn get_no_route_entries(State(state): State>) -> Json> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Loading queried subnets"); + let queries = state + .node + .lock() + .await + .no_route_entries() + .into_iter() + .map(|nrs| NoRouteSubnet { + subnet: nrs.subnet().to_string(), + expiration: nrs + .entry_expires() + .duration_since(Instant::now()) + .as_secs() + .to_string(), + }) + .collect(); + + Json(queries) +} + +/// General info about a node. +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Info { + /// The overlay subnet in use by the node. + pub node_subnet: String, + /// The public key of the node + pub node_pubkey: PublicKey, +} + +/// Get general info about the node. +async fn get_info(State(state): State>) -> Json +where + M: Metrics + Clone + Send + Sync + 'static, +{ + let info = state.node.lock().await.info(); + Json(Info { + node_subnet: info.node_subnet.to_string(), + node_pubkey: info.node_pubkey, + }) +} + +/// Public key from a node. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PubKey { + /// The public key from the node + pub public_key: PublicKey, +} + +/// Get public key from IP. +async fn get_pubk_from_ip( + State(state): State>, + Path(ip): Path, +) -> Result, StatusCode> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + match state.node.lock().await.get_pubkey_from_ip(ip) { + Some(pubkey) => Ok(Json(PubKey { public_key: pubkey })), + None => Err(StatusCode::NOT_FOUND), + } +} + +impl Serialize for Metric { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Infinite => serializer.serialize_str(INFINITE_STR), + Self::Value(v) => serializer.serialize_u16(*v), + } + } +} + +impl<'de> Deserialize<'de> for Metric { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct MetricVisitor; + + impl serde::de::Visitor<'_> for MetricVisitor { + type Value = Metric; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string or a u16") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + match value { + INFINITE_STR => Ok(Metric::Infinite), + _ => Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(value), + &format!("expected '{INFINITE_STR}'").as_str(), + )), + } + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + if value <= u16::MAX as u64 { + Ok(Metric::Value(value as u16)) + } else { + Err(E::invalid_value( + de::Unexpected::Unsigned(value), + &"expected a non-negative integer within the range of u16", + )) + } + } + } + deserializer.deserialize_any(MetricVisitor) + } +} + +impl fmt::Display for Metric { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Value(val) => write!(f, "{val}"), + Self::Infinite => write!(f, "{INFINITE_STR}"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn finite_metric_serialization() { + let metric = super::Metric::Value(10); + let s = serde_json::to_string(&metric).expect("can encode finite metric"); + + assert_eq!("10", s); + } + + #[test] + fn infinite_metric_serialization() { + let metric = super::Metric::Infinite; + let s = serde_json::to_string(&metric).expect("can encode infinite metric"); + + assert_eq!(format!("\"{INFINITE_STR}\""), s); + } + + #[test] + fn test_deserialize_metric() { + // Test deserialization of a Metric::Value + let json_value = json!(20); + let metric: Metric = serde_json::from_value(json_value).unwrap(); + assert_eq!(metric, Metric::Value(20)); + + // Test deserialization of a Metric::Infinite + let json_infinite = json!(INFINITE_STR); + let metric: Metric = serde_json::from_value(json_infinite).unwrap(); + assert_eq!(metric, Metric::Infinite); + + // Test deserialization of an invalid metric + let json_invalid = json!("invalid"); + let result: Result = serde_json::from_value(json_invalid); + assert!(result.is_err()); + } + + #[test] + fn test_deserialize_route() { + let json_data = r#" + [ + {"subnet":"406:1d77:2438:aa7c::/64","nextHop":"TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651","metric":20,"seqno":0}, + {"subnet":"407:8458:dbf5:4ed7::/64","nextHop":"TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651","metric":174,"seqno":0}, + {"subnet":"408:7ba3:3a4d:808a::/64","nextHop":"TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651","metric":"infinite","seqno":0} + ] + "#; + + let routes: Vec = serde_json::from_str(json_data).unwrap(); + + assert_eq!(routes[0], Route { + subnet: "406:1d77:2438:aa7c::/64".to_string(), + next_hop: "TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651".to_string(), + metric: Metric::Value(20), + seqno: 0 + }); + + assert_eq!(routes[1], Route { + subnet: "407:8458:dbf5:4ed7::/64".to_string(), + next_hop: "TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651".to_string(), + metric: Metric::Value(174), + seqno: 0 + }); + + assert_eq!(routes[2], Route { + subnet: "408:7ba3:3a4d:808a::/64".to_string(), + next_hop: "TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651".to_string(), + metric: Metric::Infinite, + seqno: 0 + }); + } +} diff --git a/components/mycelium/mycelium-api/src/message.rs b/components/mycelium/mycelium-api/src/message.rs new file mode 100644 index 0000000..888b95b --- /dev/null +++ b/components/mycelium/mycelium-api/src/message.rs @@ -0,0 +1,652 @@ +use std::{net::IpAddr, ops::Deref, time::Duration}; + +use axum::{ + extract::{Path, Query, State}, + http::StatusCode, + routing::{delete, get, post}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; +use tracing::debug; + +use mycelium::{ + crypto::PublicKey, + message::{MessageId, MessageInfo}, + metrics::Metrics, + subnet::Subnet, +}; +use std::path::PathBuf; + +use super::ServerState; + +/// Default amount of time to try and send a message if it is not explicitly specified. +const DEFAULT_MESSAGE_TRY_DURATION: Duration = Duration::from_secs(60 * 5); + +/// Return a router which has message endpoints and their handlers mounted. +pub fn message_router_v1(server_state: ServerState) -> Router +where + M: Metrics + Clone + Send + Sync + 'static, +{ + Router::new() + .route("/messages", get(get_message).post(push_message)) + .route("/messages/status/{id}", get(message_status)) + .route("/messages/reply/{id}", post(reply_message)) + // Topic configuration endpoints + .route( + "/messages/topics/default", + get(get_default_topic_action).put(set_default_topic_action), + ) + .route("/messages/topics", get(get_topics).post(add_topic)) + .route("/messages/topics/{topic}", delete(remove_topic)) + .route( + "/messages/topics/{topic}/sources", + get(get_topic_sources).post(add_topic_source), + ) + .route( + "/messages/topics/{topic}/sources/{subnet}", + delete(remove_topic_source), + ) + .route( + "/messages/topics/{topic}/forward", + get(get_topic_forward_socket) + .put(set_topic_forward_socket) + .delete(remove_topic_forward_socket), + ) + .with_state(server_state) +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageSendInfo { + pub dst: MessageDestination, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "base64::optional_binary")] + pub topic: Option>, + #[serde(with = "base64::binary")] + pub payload: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum MessageDestination { + Ip(IpAddr), + Pk(PublicKey), +} + +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageReceiveInfo { + pub id: MessageId, + pub src_ip: IpAddr, + pub src_pk: PublicKey, + pub dst_ip: IpAddr, + pub dst_pk: PublicKey, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "base64::optional_binary")] + pub topic: Option>, + #[serde(with = "base64::binary")] + pub payload: Vec, +} + +impl MessageDestination { + /// Get the IP address of the destination. + fn ip(self) -> IpAddr { + match self { + MessageDestination::Ip(ip) => ip, + MessageDestination::Pk(pk) => IpAddr::V6(pk.address()), + } + } +} + +#[derive(Deserialize)] +struct GetMessageQuery { + peek: Option, + timeout: Option, + /// Optional filter for start of the message, base64 encoded. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "base64::optional_binary")] + topic: Option>, +} + +impl GetMessageQuery { + /// Did the query indicate we should peek the message instead of pop? + fn peek(&self) -> bool { + matches!(self.peek, Some(true)) + } + + /// Amount of seconds to hold and try and get values. + fn timeout_secs(&self) -> u64 { + self.timeout.unwrap_or(0) + } +} + +async fn get_message( + State(state): State>, + Query(query): Query, +) -> Result, StatusCode> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!( + "Attempt to get message, peek {}, timeout {} seconds", + query.peek(), + query.timeout_secs() + ); + + // A timeout of 0 seconds essentially means get a message if there is one, and return + // immediatly if there isn't. This is the result of the implementation of Timeout, which does a + // poll of the internal future first, before polling the delay. + tokio::time::timeout( + Duration::from_secs(query.timeout_secs()), + state + .node + .lock() + .await + .get_message(!query.peek(), query.topic), + ) + .await + .or(Err(StatusCode::NO_CONTENT)) + .map(|m| { + Json(MessageReceiveInfo { + id: m.id, + src_ip: m.src_ip, + src_pk: m.src_pk, + dst_ip: m.dst_ip, + dst_pk: m.dst_pk, + topic: if m.topic.is_empty() { + None + } else { + Some(m.topic) + }, + payload: m.data, + }) + }) +} + +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageIdReply { + pub id: MessageId, +} + +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum PushMessageResponse { + Reply(MessageReceiveInfo), + Id(MessageIdReply), +} + +#[derive(Clone, Deserialize)] +struct PushMessageQuery { + reply_timeout: Option, +} + +impl PushMessageQuery { + /// The user requested to wait for the reply or not. + fn await_reply(&self) -> bool { + self.reply_timeout.is_some() + } + + /// Amount of seconds to wait for the reply. + fn timeout(&self) -> u64 { + self.reply_timeout.unwrap_or(0) + } +} + +async fn push_message( + State(state): State>, + Query(query): Query, + Json(message_info): Json, +) -> Result<(StatusCode, Json), StatusCode> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + let dst = message_info.dst.ip(); + debug!( + message.dst=%dst, + message.len=message_info.payload.len(), + "Pushing new message to stack", + ); + + let (id, sub) = match state.node.lock().await.push_message( + dst, + message_info.payload, + message_info.topic, + DEFAULT_MESSAGE_TRY_DURATION, + query.await_reply(), + ) { + Ok((id, sub)) => (id, sub), + Err(_) => { + return Err(StatusCode::BAD_REQUEST); + } + }; + + if !query.await_reply() { + // If we don't wait for the reply just return here. + return Ok(( + StatusCode::CREATED, + Json(PushMessageResponse::Id(MessageIdReply { id })), + )); + } + + let mut sub = sub.unwrap(); + tokio::select! { + sub_res = sub.changed() => { + match sub_res { + Ok(_) => { + if let Some(m) = sub.borrow().deref() { + Ok((StatusCode::OK, Json(PushMessageResponse::Reply(MessageReceiveInfo { + id: m.id, + src_ip: m.src_ip, + src_pk: m.src_pk, + dst_ip: m.dst_ip, + dst_pk: m.dst_pk, + topic: if m.topic.is_empty() { None } else { Some(m.topic.clone()) }, + payload: m.data.clone(), + })))) + } else { + // This happens if a none value is send, which should not happen. + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } + Err(_) => { + // This happens if the sender drops, which should not happen. + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } + }, + _ = tokio::time::sleep(Duration::from_secs(query.timeout())) => { + // Timeout expired while waiting for reply + Ok((StatusCode::REQUEST_TIMEOUT, Json(PushMessageResponse::Id(MessageIdReply { id })))) + } + } +} + +async fn reply_message( + State(state): State>, + Path(id): Path, + Json(message_info): Json, +) -> StatusCode +where + M: Metrics + Clone + Send + Sync + 'static, +{ + let dst = message_info.dst.ip(); + debug!( + message.id=id.as_hex(), + message.dst=%dst, + message.len=message_info.payload.len(), + "Pushing new reply to message stack", + ); + + state.node.lock().await.reply_message( + id, + dst, + message_info.payload, + DEFAULT_MESSAGE_TRY_DURATION, + ); + + StatusCode::NO_CONTENT +} + +async fn message_status( + State(state): State>, + Path(id): Path, +) -> Result, StatusCode> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!(message.id=%id.as_hex(), "Fetching message status"); + + state + .node + .lock() + .await + .message_status(id) + .ok_or(StatusCode::NOT_FOUND) + .map(Json) +} + +/// Module to implement base64 decoding and encoding +/// Sourced from https://users.rust-lang.org/t/serialize-a-vec-u8-to-json-as-base64/57781, with some +/// addaptions to work with the new version of the base64 crate +mod base64 { + use base64::engine::{GeneralPurpose, GeneralPurposeConfig}; + use base64::{alphabet, Engine}; + + const B64ENGINE: GeneralPurpose = base64::engine::general_purpose::GeneralPurpose::new( + &alphabet::STANDARD, + GeneralPurposeConfig::new(), + ); + + pub fn encode(input: &[u8]) -> String { + B64ENGINE.encode(input) + } + + pub fn decode(input: &[u8]) -> Result, base64::DecodeError> { + B64ENGINE.decode(input) + } + + pub mod binary { + use super::B64ENGINE; + use base64::Engine; + use serde::{Deserialize, Serialize}; + use serde::{Deserializer, Serializer}; + + pub fn serialize(v: &Vec, s: S) -> Result { + let base64 = B64ENGINE.encode(v); + String::serialize(&base64, s) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + let base64 = String::deserialize(d)?; + B64ENGINE + .decode(base64.as_bytes()) + .map_err(serde::de::Error::custom) + } + } + + pub mod optional_binary { + use super::B64ENGINE; + use base64::Engine; + use serde::{Deserialize, Serialize}; + use serde::{Deserializer, Serializer}; + + pub fn serialize(v: &Option>, s: S) -> Result { + if let Some(v) = v { + let base64 = B64ENGINE.encode(v); + String::serialize(&base64, s) + } else { + >::serialize(&None, s) + } + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result>, D::Error> { + if let Some(base64) = >::deserialize(d)? { + B64ENGINE + .decode(base64.as_bytes()) + .map_err(serde::de::Error::custom) + .map(Option::Some) + } else { + Ok(None) + } + } + } +} + +// Topic configuration API + +/// Response for the default topic action +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct DefaultTopicActionResponse { + accept: bool, +} + +/// Request to set the default topic action +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct DefaultTopicActionRequest { + accept: bool, +} + +/// Request to add a source to a topic whitelist +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TopicSourceRequest { + subnet: String, +} + +/// Request to set a forward socket for a topic +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TopicForwardSocketRequest { + socket_path: String, +} + +/// Get the default topic action (accept or reject) +async fn get_default_topic_action( + State(state): State>, +) -> Json +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Getting default topic action"); + let accept = state.node.lock().await.unconfigure_topic_action(); + Json(DefaultTopicActionResponse { accept }) +} + +/// Set the default topic action (accept or reject) +async fn set_default_topic_action( + State(state): State>, + Json(request): Json, +) -> StatusCode +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!(accept=%request.accept, "Setting default topic action"); + state + .node + .lock() + .await + .accept_unconfigured_topic(request.accept); + StatusCode::NO_CONTENT +} + +/// Get all whitelisted topics +async fn get_topics(State(state): State>) -> Json> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Getting all whitelisted topics"); + let node = state.node.lock().await; + + // Get the whitelist from the node + let topics = node.topics(); + + // Convert to TopicInfo structs + let topics: Vec = topics.iter().map(|topic| base64::encode(topic)).collect(); + + Json(topics) +} + +/// Add a topic to the whitelist +async fn add_topic( + State(state): State>, + Json(topic_info): Json>, +) -> StatusCode +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Adding topic to whitelist"); + state.node.lock().await.add_topic_whitelist(topic_info); + StatusCode::CREATED +} + +/// Remove a topic from the whitelist +async fn remove_topic( + State(state): State>, + Path(topic): Path, +) -> Result +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Removing topic from whitelist"); + + // Decode the base64 topic + let topic_bytes = match base64::decode(topic.as_bytes()) { + Ok(bytes) => bytes, + Err(_) => return Err(StatusCode::BAD_REQUEST), + }; + + state.node.lock().await.remove_topic_whitelist(topic_bytes); + Ok(StatusCode::NO_CONTENT) +} + +/// Get all sources for a topic +async fn get_topic_sources( + State(state): State>, + Path(topic): Path, +) -> Result>, StatusCode> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Getting sources for topic"); + + // Decode the base64 topic + let topic_bytes = match base64::decode(topic.as_bytes()) { + Ok(bytes) => bytes, + Err(_) => return Err(StatusCode::BAD_REQUEST), + }; + + let node = state.node.lock().await; + + // Get the whitelist from the node + let sources = node.topic_allowed_sources(&topic_bytes); + + // Find the topic in the whitelist + if let Some(sources) = sources { + let sources = sources.into_iter().map(|s| s.to_string()).collect(); + Ok(Json(sources)) + } else { + Err(StatusCode::NOT_FOUND) + } +} + +/// Add a source to a topic whitelist +async fn add_topic_source( + State(state): State>, + Path(topic): Path, + Json(request): Json, +) -> Result +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Adding source to topic whitelist"); + + // Decode the base64 topic + let topic_bytes = match base64::decode(topic.as_bytes()) { + Ok(bytes) => bytes, + Err(_) => return Err(StatusCode::BAD_REQUEST), + }; + + // Parse the subnet + let subnet = match request.subnet.parse::() { + Ok(subnet) => subnet, + Err(_) => return Err(StatusCode::BAD_REQUEST), + }; + + state + .node + .lock() + .await + .add_topic_whitelist_src(topic_bytes, subnet); + Ok(StatusCode::CREATED) +} + +/// Remove a source from a topic whitelist +async fn remove_topic_source( + State(state): State>, + Path((topic, subnet_str)): Path<(String, String)>, +) -> Result +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Removing source from topic whitelist"); + + // Decode the base64 topic + let topic_bytes = match base64::decode(topic.as_bytes()) { + Ok(bytes) => bytes, + Err(_) => return Err(StatusCode::BAD_REQUEST), + }; + + // Parse the subnet + let subnet = match subnet_str.parse::() { + Ok(subnet) => subnet, + Err(_) => return Err(StatusCode::BAD_REQUEST), + }; + + state + .node + .lock() + .await + .remove_topic_whitelist_src(topic_bytes, subnet); + Ok(StatusCode::NO_CONTENT) +} + +/// Get the forward socket for a topic +async fn get_topic_forward_socket( + State(state): State>, + Path(topic): Path, +) -> Result>, StatusCode> +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Getting forward socket for topic"); + + // Decode the base64 topic + let topic_bytes = match base64::decode(topic.as_bytes()) { + Ok(bytes) => bytes, + Err(_) => return Err(StatusCode::BAD_REQUEST), + }; + + let node = state.node.lock().await; + let socket_path = node + .get_topic_forward_socket(&topic_bytes) + .map(|p| p.to_string_lossy().to_string()); + + Ok(Json(socket_path)) +} + +/// Set the forward socket for a topic +async fn set_topic_forward_socket( + State(state): State>, + Path(topic): Path, + Json(request): Json, +) -> Result +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Setting forward socket for topic"); + + // Decode the base64 topic + let topic_bytes = match base64::decode(topic.as_bytes()) { + Ok(bytes) => bytes, + Err(_) => return Err(StatusCode::BAD_REQUEST), + }; + + let socket_path = PathBuf::from(request.socket_path); + state + .node + .lock() + .await + .set_topic_forward_socket(topic_bytes, socket_path); + Ok(StatusCode::NO_CONTENT) +} + +/// Remove the forward socket for a topic +async fn remove_topic_forward_socket( + State(state): State>, + Path(topic): Path, +) -> Result +where + M: Metrics + Clone + Send + Sync + 'static, +{ + debug!("Removing forward socket for topic"); + + // Decode the base64 topic + let topic_bytes = match base64::decode(topic.as_bytes()) { + Ok(bytes) => bytes, + Err(_) => return Err(StatusCode::BAD_REQUEST), + }; + + state + .node + .lock() + .await + .delete_topic_forward_socket(topic_bytes); + Ok(StatusCode::NO_CONTENT) +} diff --git a/components/mycelium/mycelium-api/src/rpc.rs b/components/mycelium/mycelium-api/src/rpc.rs new file mode 100644 index 0000000..38bf473 --- /dev/null +++ b/components/mycelium/mycelium-api/src/rpc.rs @@ -0,0 +1,752 @@ +//! JSON-RPC API implementation for Mycelium + +mod spec; + +use std::net::SocketAddr; +#[cfg(feature = "message")] +use std::ops::Deref; +use std::str::FromStr; +use std::sync::Arc; + +#[cfg(feature = "message")] +use base64::Engine; +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; +use jsonrpsee::server::{ServerBuilder, ServerHandle}; +use jsonrpsee::types::{ErrorCode, ErrorObject}; +#[cfg(feature = "message")] +use mycelium::subnet::Subnet; +#[cfg(feature = "message")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "message")] +use std::path::PathBuf; +use tokio::sync::Mutex; +#[cfg(feature = "message")] +use tokio::time::Duration; +use tracing::debug; + +use crate::{Info, Metric, NoRouteSubnet, QueriedSubnet, Route, ServerState}; +use mycelium::crypto::PublicKey; +use mycelium::endpoint::Endpoint; +use mycelium::metrics::Metrics; +use mycelium::peer_manager::{PeerExists, PeerNotFound, PeerStats}; + +use self::spec::OPENRPC_SPEC; + +// Topic configuration struct for RPC API +#[cfg(feature = "message")] +#[derive(Clone, Serialize, Deserialize)] +struct TopicInfo { + topic: String, + sources: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + forward_socket: Option, +} + +// Define the base RPC API trait using jsonrpsee macros +#[rpc(server)] +pub trait MyceliumApi { + // Admin methods + #[method(name = "getInfo")] + async fn get_info(&self) -> RpcResult; + + #[method(name = "getPublicKeyFromIp")] + async fn get_pubkey_from_ip(&self, ip: String) -> RpcResult; + + // Peer methods + #[method(name = "getPeers")] + async fn get_peers(&self) -> RpcResult>; + + #[method(name = "addPeer")] + async fn add_peer(&self, endpoint: String) -> RpcResult; + + #[method(name = "deletePeer")] + async fn delete_peer(&self, endpoint: String) -> RpcResult; + + // Route methods + #[method(name = "getSelectedRoutes")] + async fn get_selected_routes(&self) -> RpcResult>; + + #[method(name = "getFallbackRoutes")] + async fn get_fallback_routes(&self) -> RpcResult>; + + #[method(name = "getQueriedSubnets")] + async fn get_queried_subnets(&self) -> RpcResult>; + + #[method(name = "getNoRouteEntries")] + async fn get_no_route_entries(&self) -> RpcResult>; + + // OpenRPC discovery + #[method(name = "rpc.discover")] + async fn discover(&self) -> RpcResult; +} + +// Define a separate message API trait that is only compiled when the message feature is enabled +#[cfg(feature = "message")] +#[rpc(server)] +pub trait MyceliumMessageApi { + // Message methods + #[method(name = "popMessage")] + async fn pop_message( + &self, + peek: Option, + timeout: Option, + topic: Option, + ) -> RpcResult; + + #[method(name = "pushMessage")] + async fn push_message( + &self, + message: crate::message::MessageSendInfo, + reply_timeout: Option, + ) -> RpcResult; + + #[method(name = "pushMessageReply")] + async fn push_message_reply( + &self, + id: String, + message: crate::message::MessageSendInfo, + ) -> RpcResult; + + #[method(name = "getMessageInfo")] + async fn get_message_info(&self, id: String) -> RpcResult; + + // Topic configuration methods + #[method(name = "getDefaultTopicAction")] + async fn get_default_topic_action(&self) -> RpcResult; + + #[method(name = "setDefaultTopicAction")] + async fn set_default_topic_action(&self, accept: bool) -> RpcResult; + + #[method(name = "getTopics")] + async fn get_topics(&self) -> RpcResult>; + + #[method(name = "addTopic")] + async fn add_topic(&self, topic: String) -> RpcResult; + + #[method(name = "removeTopic")] + async fn remove_topic(&self, topic: String) -> RpcResult; + + #[method(name = "getTopicSources")] + async fn get_topic_sources(&self, topic: String) -> RpcResult>; + + #[method(name = "addTopicSource")] + async fn add_topic_source(&self, topic: String, subnet: String) -> RpcResult; + + #[method(name = "removeTopicSource")] + async fn remove_topic_source(&self, topic: String, subnet: String) -> RpcResult; + + #[method(name = "getTopicForwardSocket")] + async fn get_topic_forward_socket(&self, topic: String) -> RpcResult>; + + #[method(name = "setTopicForwardSocket")] + async fn set_topic_forward_socket(&self, topic: String, socket_path: String) + -> RpcResult; + + #[method(name = "removeTopicForwardSocket")] + async fn remove_topic_forward_socket(&self, topic: String) -> RpcResult; +} + +// Implement the API trait +#[derive(Clone)] +struct RPCApi { + state: Arc>, +} + +// Implement the base API trait +#[async_trait::async_trait] +impl MyceliumApiServer for RPCApi +where + M: Metrics + Clone + Send + Sync + 'static, +{ + async fn get_info(&self) -> RpcResult { + debug!("Getting node info via RPC"); + let node_info = self.state.node.lock().await.info(); + Ok(Info { + node_subnet: node_info.node_subnet.to_string(), + node_pubkey: node_info.node_pubkey, + }) + } + + async fn get_pubkey_from_ip(&self, ip_str: String) -> RpcResult { + debug!(ip = %ip_str, "Getting public key from IP via RPC"); + let ip = std::net::IpAddr::from_str(&ip_str) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32007)))?; + + let pubkey = self.state.node.lock().await.get_pubkey_from_ip(ip); + + match pubkey { + Some(pk) => Ok(pk), + None => Err(ErrorObject::from(ErrorCode::from(-32008))), + } + } + + async fn get_peers(&self) -> RpcResult> { + debug!("Fetching peer stats via RPC"); + let peers = self.state.node.lock().await.peer_info(); + Ok(peers) + } + + async fn add_peer(&self, endpoint_str: String) -> RpcResult { + debug!( + peer.endpoint = endpoint_str, + "Attempting to add peer to the system via RPC" + ); + + let endpoint = Endpoint::from_str(&endpoint_str) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32009)))?; + + match self.state.node.lock().await.add_peer(endpoint) { + Ok(()) => Ok(true), + Err(PeerExists) => Err(ErrorObject::from(ErrorCode::from(-32010))), + } + } + + async fn delete_peer(&self, endpoint_str: String) -> RpcResult { + debug!( + peer.endpoint = endpoint_str, + "Attempting to remove peer from the system via RPC" + ); + + let endpoint = Endpoint::from_str(&endpoint_str) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32012)))?; + + match self.state.node.lock().await.remove_peer(endpoint) { + Ok(()) => Ok(true), + Err(PeerNotFound) => Err(ErrorObject::from(ErrorCode::from(-32011))), + } + } + + async fn get_selected_routes(&self) -> RpcResult> { + debug!("Loading selected routes via RPC"); + let routes = self + .state + .node + .lock() + .await + .selected_routes() + .into_iter() + .map(|sr| Route { + subnet: sr.source().subnet().to_string(), + next_hop: sr.neighbour().connection_identifier().clone(), + metric: if sr.metric().is_infinite() { + Metric::Infinite + } else { + Metric::Value(sr.metric().into()) + }, + seqno: sr.seqno().into(), + }) + .collect(); + + Ok(routes) + } + + async fn get_fallback_routes(&self) -> RpcResult> { + debug!("Loading fallback routes via RPC"); + let routes = self + .state + .node + .lock() + .await + .fallback_routes() + .into_iter() + .map(|sr| Route { + subnet: sr.source().subnet().to_string(), + next_hop: sr.neighbour().connection_identifier().clone(), + metric: if sr.metric().is_infinite() { + Metric::Infinite + } else { + Metric::Value(sr.metric().into()) + }, + seqno: sr.seqno().into(), + }) + .collect(); + + Ok(routes) + } + + async fn get_queried_subnets(&self) -> RpcResult> { + debug!("Loading queried subnets via RPC"); + let queries = self + .state + .node + .lock() + .await + .queried_subnets() + .into_iter() + .map(|qs| QueriedSubnet { + subnet: qs.subnet().to_string(), + expiration: qs + .query_expires() + .duration_since(tokio::time::Instant::now()) + .as_secs() + .to_string(), + }) + .collect(); + + Ok(queries) + } + + async fn get_no_route_entries(&self) -> RpcResult> { + debug!("Loading no route entries via RPC"); + let entries = self + .state + .node + .lock() + .await + .no_route_entries() + .into_iter() + .map(|nrs| NoRouteSubnet { + subnet: nrs.subnet().to_string(), + expiration: nrs + .entry_expires() + .duration_since(tokio::time::Instant::now()) + .as_secs() + .to_string(), + }) + .collect(); + + Ok(entries) + } + + async fn discover(&self) -> RpcResult { + let spec = serde_json::from_str::(OPENRPC_SPEC) + .expect("Failed to parse OpenRPC spec"); + Ok(spec) + } +} + +// Implement the message API trait only when the message feature is enabled +#[cfg(feature = "message")] +#[async_trait::async_trait] +impl MyceliumMessageApiServer for RPCApi +where + M: Metrics + Clone + Send + Sync + 'static, +{ + async fn pop_message( + &self, + peek: Option, + timeout: Option, + topic: Option, + ) -> RpcResult { + debug!( + "Attempt to get message via RPC, peek {}, timeout {} seconds", + peek.unwrap_or(false), + timeout.unwrap_or(0) + ); + + let topic_bytes = if let Some(topic_str) = topic { + Some( + base64::engine::general_purpose::STANDARD + .decode(topic_str.as_bytes()) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32013)))?, + ) + } else { + None + }; + + // A timeout of 0 seconds essentially means get a message if there is one, and return + // immediately if there isn't. + let result = tokio::time::timeout( + Duration::from_secs(timeout.unwrap_or(0)), + self.state + .node + .lock() + .await + .get_message(!peek.unwrap_or(false), topic_bytes), + ) + .await; + + match result { + Ok(m) => Ok(crate::message::MessageReceiveInfo { + id: m.id, + src_ip: m.src_ip, + src_pk: m.src_pk, + dst_ip: m.dst_ip, + dst_pk: m.dst_pk, + topic: if m.topic.is_empty() { + None + } else { + Some(m.topic) + }, + payload: m.data, + }), + _ => Err(ErrorObject::from(ErrorCode::from(-32014))), + } + } + + async fn push_message( + &self, + message: crate::message::MessageSendInfo, + reply_timeout: Option, + ) -> RpcResult { + let dst = match message.dst { + crate::message::MessageDestination::Ip(ip) => ip, + crate::message::MessageDestination::Pk(pk) => pk.address().into(), + }; + + debug!( + message.dst=%dst, + message.len=message.payload.len(), + "Pushing new message via RPC", + ); + + // Default message try duration + const DEFAULT_MESSAGE_TRY_DURATION: Duration = Duration::from_secs(60 * 5); + + let result = self.state.node.lock().await.push_message( + dst, + message.payload, + message.topic, + DEFAULT_MESSAGE_TRY_DURATION, + reply_timeout.is_some(), + ); + + let (id, sub) = match result { + Ok((id, sub)) => (id, sub), + Err(_) => { + return Err(ErrorObject::from(ErrorCode::from(-32015))); + } + }; + + if reply_timeout.is_none() { + // If we don't wait for the reply just return here. + return Ok(crate::message::PushMessageResponse::Id( + crate::message::MessageIdReply { id }, + )); + } + + let mut sub = sub.unwrap(); + + // Wait for reply with timeout + tokio::select! { + sub_res = sub.changed() => { + match sub_res { + Ok(_) => { + if let Some(m) = sub.borrow().deref() { + Ok(crate::message::PushMessageResponse::Reply(crate::message::MessageReceiveInfo { + id: m.id, + src_ip: m.src_ip, + src_pk: m.src_pk, + dst_ip: m.dst_ip, + dst_pk: m.dst_pk, + topic: if m.topic.is_empty() { None } else { Some(m.topic.clone()) }, + payload: m.data.clone(), + })) + } else { + // This happens if a none value is send, which should not happen. + Err(ErrorObject::from(ErrorCode::from(-32016))) + } + } + Err(_) => { + // This happens if the sender drops, which should not happen. + Err(ErrorObject::from(ErrorCode::from(-32017))) + } + } + }, + _ = tokio::time::sleep(Duration::from_secs(reply_timeout.unwrap_or(0))) => { + // Timeout expired while waiting for reply + Ok(crate::message::PushMessageResponse::Id(crate::message::MessageIdReply { id })) + } + } + } + + async fn push_message_reply( + &self, + id: String, + message: crate::message::MessageSendInfo, + ) -> RpcResult { + let message_id = match mycelium::message::MessageId::from_hex(id.as_bytes()) { + Ok(id) => id, + Err(_) => { + return Err(ErrorObject::from(ErrorCode::from(-32018))); + } + }; + + let dst = match message.dst { + crate::message::MessageDestination::Ip(ip) => ip, + crate::message::MessageDestination::Pk(pk) => pk.address().into(), + }; + + debug!( + message.id=id, + message.dst=%dst, + message.len=message.payload.len(), + "Pushing new reply to message via RPC", + ); + + // Default message try duration + const DEFAULT_MESSAGE_TRY_DURATION: Duration = Duration::from_secs(60 * 5); + + self.state.node.lock().await.reply_message( + message_id, + dst, + message.payload, + DEFAULT_MESSAGE_TRY_DURATION, + ); + + Ok(true) + } + + async fn get_message_info(&self, id: String) -> RpcResult { + let message_id = match mycelium::message::MessageId::from_hex(id.as_bytes()) { + Ok(id) => id, + Err(_) => { + return Err(ErrorObject::from(ErrorCode::from(-32020))); + } + }; + + debug!(message.id=%id, "Fetching message status via RPC"); + + let result = self.state.node.lock().await.message_status(message_id); + + match result { + Some(info) => Ok(info), + None => Err(ErrorObject::from(ErrorCode::from(-32019))), + } + } + + // Topic configuration methods implementation + async fn get_default_topic_action(&self) -> RpcResult { + debug!("Getting default topic action via RPC"); + let accept = self.state.node.lock().await.unconfigure_topic_action(); + Ok(accept) + } + + async fn set_default_topic_action(&self, accept: bool) -> RpcResult { + debug!(accept=%accept, "Setting default topic action via RPC"); + self.state + .node + .lock() + .await + .accept_unconfigured_topic(accept); + Ok(true) + } + + async fn get_topics(&self) -> RpcResult> { + debug!("Getting all whitelisted topics via RPC"); + + let topics = self + .state + .node + .lock() + .await + .topics() + .into_iter() + .map(|topic| base64::engine::general_purpose::STANDARD.encode(&topic)) + .collect(); + + // For now, we'll return an empty list + Ok(topics) + } + + async fn add_topic(&self, topic: String) -> RpcResult { + debug!("Adding topic to whitelist via RPC"); + + // Decode the base64 topic + let topic_bytes = base64::engine::general_purpose::STANDARD + .decode(topic.as_bytes()) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?; + + self.state + .node + .lock() + .await + .add_topic_whitelist(topic_bytes); + Ok(true) + } + + async fn remove_topic(&self, topic: String) -> RpcResult { + debug!("Removing topic from whitelist via RPC"); + + // Decode the base64 topic + let topic_bytes = base64::engine::general_purpose::STANDARD + .decode(topic.as_bytes()) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?; + + self.state + .node + .lock() + .await + .remove_topic_whitelist(topic_bytes); + Ok(true) + } + + async fn get_topic_sources(&self, topic: String) -> RpcResult> { + debug!("Getting sources for topic via RPC"); + + // Decode the base64 topic + let topic_bytes = base64::engine::general_purpose::STANDARD + .decode(topic.as_bytes()) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?; + + let subnets = self + .state + .node + .lock() + .await + .topic_allowed_sources(&topic_bytes) + .ok_or(ErrorObject::from(ErrorCode::from(-32030)))? + .into_iter() + .map(|subnet| subnet.to_string()) + .collect(); + + Ok(subnets) + } + + async fn add_topic_source(&self, topic: String, subnet: String) -> RpcResult { + debug!("Adding source to topic whitelist via RPC"); + + // Decode the base64 topic + let topic_bytes = base64::engine::general_purpose::STANDARD + .decode(topic.as_bytes()) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?; + + // Parse the subnet + let subnet_obj = subnet + .parse::() + .map_err(|_| ErrorObject::from(ErrorCode::from(-32023)))?; + + self.state + .node + .lock() + .await + .add_topic_whitelist_src(topic_bytes, subnet_obj); + Ok(true) + } + + async fn remove_topic_source(&self, topic: String, subnet: String) -> RpcResult { + debug!("Removing source from topic whitelist via RPC"); + + // Decode the base64 topic + let topic_bytes = base64::engine::general_purpose::STANDARD + .decode(topic.as_bytes()) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?; + + // Parse the subnet + let subnet_obj = subnet + .parse::() + .map_err(|_| ErrorObject::from(ErrorCode::from(-32023)))?; + + self.state + .node + .lock() + .await + .remove_topic_whitelist_src(topic_bytes, subnet_obj); + Ok(true) + } + + async fn get_topic_forward_socket(&self, topic: String) -> RpcResult> { + debug!("Getting forward socket for topic via RPC"); + + // Decode the base64 topic + let topic_bytes = base64::engine::general_purpose::STANDARD + .decode(topic.as_bytes()) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?; + + let node = self.state.node.lock().await; + let socket_path = node + .get_topic_forward_socket(&topic_bytes) + .map(|p| p.to_string_lossy().to_string()); + + Ok(socket_path) + } + + async fn set_topic_forward_socket( + &self, + topic: String, + socket_path: String, + ) -> RpcResult { + debug!("Setting forward socket for topic via RPC"); + + // Decode the base64 topic + let topic_bytes = base64::engine::general_purpose::STANDARD + .decode(topic.as_bytes()) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?; + + let path = PathBuf::from(socket_path); + self.state + .node + .lock() + .await + .set_topic_forward_socket(topic_bytes, path); + Ok(true) + } + + async fn remove_topic_forward_socket(&self, topic: String) -> RpcResult { + debug!("Removing forward socket for topic via RPC"); + + // Decode the base64 topic + let topic_bytes = base64::engine::general_purpose::STANDARD + .decode(topic.as_bytes()) + .map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?; + + self.state + .node + .lock() + .await + .delete_topic_forward_socket(topic_bytes); + Ok(true) + } +} + +/// JSON-RPC API server handle. The server is spawned in a background task. If this handle is dropped, +/// the server is terminated. +pub struct JsonRpc { + /// JSON-RPC server handle + _server: ServerHandle, +} + +impl JsonRpc { + /// Spawns a new JSON-RPC API server on the provided listening address. + /// + /// # Arguments + /// + /// * `node` - The Mycelium node to use for the JSON-RPC API + /// * `listen_addr` - The address to listen on for JSON-RPC requests + /// + /// # Returns + /// + /// A `JsonRpc` instance that will be dropped when the server is terminated + pub async fn spawn(node: Arc>>, listen_addr: SocketAddr) -> Self + where + M: Metrics + Clone + Send + Sync + 'static, + { + debug!(%listen_addr, "Starting JSON-RPC server"); + + let server_state = Arc::new(ServerState { node }); + + // Create the server builder + let server = ServerBuilder::default() + .build(listen_addr) + .await + .expect("Failed to build JSON-RPC server"); + + // Create the API implementation + let api = RPCApi { + state: server_state, + }; + + // Register the API implementation + + // Create the RPC module + #[allow(unused_mut)] + let mut methods = MyceliumApiServer::into_rpc(api.clone()); + + // When the message feature is enabled, merge the message RPC module + #[cfg(feature = "message")] + { + let message_methods = MyceliumMessageApiServer::into_rpc(api); + methods + .merge(message_methods) + .expect("Can merge message API into base API"); + } + + // Start the server with the appropriate module(s) + let handle = server.start(methods); + + debug!(%listen_addr, "JSON-RPC server started successfully"); + + JsonRpc { _server: handle } + } +} diff --git a/components/mycelium/mycelium-api/src/rpc/admin.rs b/components/mycelium/mycelium-api/src/rpc/admin.rs new file mode 100644 index 0000000..d039241 --- /dev/null +++ b/components/mycelium/mycelium-api/src/rpc/admin.rs @@ -0,0 +1,64 @@ +//! Admin-related JSON-RPC methods for the Mycelium API + +use jsonrpc_core::{Error, ErrorCode, Result as RpcResult}; +use std::net::IpAddr; +use std::str::FromStr; +use tracing::debug; + +use mycelium::crypto::PublicKey; +use mycelium::metrics::Metrics; + +use crate::HttpServerState; +use crate::Info; +use crate::rpc::models::error_codes; +use crate::rpc::traits::AdminApi; + +/// Implementation of Admin-related JSON-RPC methods +pub struct AdminRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + state: HttpServerState, +} + +impl AdminRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + /// Create a new AdminRpc instance + pub fn new(state: HttpServerState) -> Self { + Self { state } + } +} + +impl AdminApi for AdminRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + fn get_info(&self) -> RpcResult { + debug!("Getting node info via RPC"); + let info = self.state.node.blocking_lock().info(); + Ok(Info { + node_subnet: info.node_subnet.to_string(), + node_pubkey: info.node_pubkey, + }) + } + + fn get_pubkey_from_ip(&self, mycelium_ip: String) -> RpcResult { + debug!(ip = %mycelium_ip, "Getting public key from IP via RPC"); + let ip = IpAddr::from_str(&mycelium_ip).map_err(|e| Error { + code: ErrorCode::InvalidParams, + message: format!("Invalid IP address: {}", e), + data: None, + })?; + + match self.state.node.blocking_lock().get_pubkey_from_ip(ip) { + Some(pubkey) => Ok(pubkey), + None => Err(Error { + code: ErrorCode::ServerError(error_codes::PUBKEY_NOT_FOUND), + message: "Public key not found".to_string(), + data: None, + }), + } + } +} \ No newline at end of file diff --git a/components/mycelium/mycelium-api/src/rpc/message.rs b/components/mycelium/mycelium-api/src/rpc/message.rs new file mode 100644 index 0000000..ecdad93 --- /dev/null +++ b/components/mycelium/mycelium-api/src/rpc/message.rs @@ -0,0 +1,263 @@ +//! Message-related JSON-RPC methods for the Mycelium API + +use jsonrpc_core::{Error, ErrorCode, Result as RpcResult}; +use std::time::Duration; +use tracing::debug; + +use mycelium::metrics::Metrics; +use mycelium::message::{MessageId, MessageInfo}; + +use crate::HttpServerState; +use crate::message::{MessageReceiveInfo, MessageSendInfo, PushMessageResponse}; +use crate::rpc::models::error_codes; +use crate::rpc::traits::MessageApi; + +/// Implementation of Message-related JSON-RPC methods +pub struct MessageRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + state: HttpServerState, +} + +impl MessageRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + /// Create a new MessageRpc instance + pub fn new(state: HttpServerState) -> Self { + Self { state } + } + + /// Convert a base64 string to bytes + fn decode_base64(&self, s: &str) -> Result, Error> { + base64::engine::general_purpose::STANDARD.decode(s.as_bytes()) + .map_err(|e| Error { + code: ErrorCode::InvalidParams, + message: format!("Invalid base64 encoding: {}", e), + data: None, + }) + } +} + +impl MessageApi for MessageRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + fn pop_message(&self, peek: Option, timeout: Option, topic: Option) -> RpcResult { + debug!( + "Attempt to get message via RPC, peek {}, timeout {} seconds", + peek.unwrap_or(false), + timeout.unwrap_or(0) + ); + + let topic_bytes = if let Some(topic_str) = topic { + Some(self.decode_base64(&topic_str)?) + } else { + None + }; + + // A timeout of 0 seconds essentially means get a message if there is one, and return + // immediately if there isn't. + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + tokio::time::timeout( + Duration::from_secs(timeout.unwrap_or(0)), + self.state + .node + .lock() + .await + .get_message(!peek.unwrap_or(false), topic_bytes), + ) + .await + }) + }); + + match result { + Ok(Ok(m)) => Ok(MessageReceiveInfo { + id: m.id, + src_ip: m.src_ip, + src_pk: m.src_pk, + dst_ip: m.dst_ip, + dst_pk: m.dst_pk, + topic: if m.topic.is_empty() { + None + } else { + Some(m.topic) + }, + payload: m.data, + }), + _ => Err(Error { + code: ErrorCode::ServerError(error_codes::NO_MESSAGE_READY), + message: "No message ready".to_string(), + data: None, + }), + } + } + + fn push_message(&self, message: MessageSendInfo, reply_timeout: Option) -> RpcResult { + let dst = match message.dst { + crate::message::MessageDestination::Ip(ip) => ip, + crate::message::MessageDestination::Pk(pk) => pk.address().into(), + }; + + debug!( + message.dst=%dst, + message.len=message.payload.len(), + "Pushing new message via RPC", + ); + + // Default message try duration + const DEFAULT_MESSAGE_TRY_DURATION: Duration = Duration::from_secs(60 * 5); + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + self.state.node.lock().await.push_message( + dst, + message.payload, + message.topic, + DEFAULT_MESSAGE_TRY_DURATION, + reply_timeout.is_some(), + ) + }) + }); + + let (id, sub) = match result { + Ok((id, sub)) => (id, sub), + Err(_) => { + return Err(Error { + code: ErrorCode::InvalidParams, + message: "Failed to push message".to_string(), + data: None, + }); + } + }; + + if reply_timeout.is_none() { + // If we don't wait for the reply just return here. + return Ok(PushMessageResponse::Id(crate::message::MessageIdReply { id })); + } + + let mut sub = sub.unwrap(); + + // Wait for reply with timeout + let reply_result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + tokio::select! { + sub_res = sub.changed() => { + match sub_res { + Ok(_) => { + if let Some(m) = sub.borrow().deref() { + Ok(PushMessageResponse::Reply(MessageReceiveInfo { + id: m.id, + src_ip: m.src_ip, + src_pk: m.src_pk, + dst_ip: m.dst_ip, + dst_pk: m.dst_pk, + topic: if m.topic.is_empty() { None } else { Some(m.topic.clone()) }, + payload: m.data.clone(), + })) + } else { + // This happens if a none value is send, which should not happen. + Err(Error { + code: ErrorCode::InternalError, + message: "Internal error while waiting for reply".to_string(), + data: None, + }) + } + } + Err(_) => { + // This happens if the sender drops, which should not happen. + Err(Error { + code: ErrorCode::InternalError, + message: "Internal error while waiting for reply".to_string(), + data: None, + }) + } + } + }, + _ = tokio::time::sleep(Duration::from_secs(reply_timeout.unwrap_or(0))) => { + // Timeout expired while waiting for reply + Ok(PushMessageResponse::Id(crate::message::MessageIdReply { id })) + } + } + }) + }); + + match reply_result { + Ok(response) => Ok(response), + Err(e) => Err(e), + } + } + + fn push_message_reply(&self, id: String, message: MessageSendInfo) -> RpcResult { + let message_id = match MessageId::from_hex(&id) { + Ok(id) => id, + Err(_) => { + return Err(Error { + code: ErrorCode::InvalidParams, + message: "Invalid message ID".to_string(), + data: None, + }); + } + }; + + let dst = match message.dst { + crate::message::MessageDestination::Ip(ip) => ip, + crate::message::MessageDestination::Pk(pk) => pk.address().into(), + }; + + debug!( + message.id=id, + message.dst=%dst, + message.len=message.payload.len(), + "Pushing new reply to message via RPC", + ); + + // Default message try duration + const DEFAULT_MESSAGE_TRY_DURATION: Duration = Duration::from_secs(60 * 5); + + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + self.state.node.lock().await.reply_message( + message_id, + dst, + message.payload, + DEFAULT_MESSAGE_TRY_DURATION, + ); + }) + }); + + Ok(true) + } + + fn get_message_info(&self, id: String) -> RpcResult { + let message_id = match MessageId::from_hex(&id) { + Ok(id) => id, + Err(_) => { + return Err(Error { + code: ErrorCode::InvalidParams, + message: "Invalid message ID".to_string(), + data: None, + }); + } + }; + + debug!(message.id=%id, "Fetching message status via RPC"); + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + self.state.node.lock().await.message_status(message_id) + }) + }); + + match result { + Some(info) => Ok(info), + None => Err(Error { + code: ErrorCode::ServerError(error_codes::MESSAGE_NOT_FOUND), + message: "Message not found".to_string(), + data: None, + }), + } + } +} \ No newline at end of file diff --git a/components/mycelium/mycelium-api/src/rpc/models.rs b/components/mycelium/mycelium-api/src/rpc/models.rs new file mode 100644 index 0000000..01da021 --- /dev/null +++ b/components/mycelium/mycelium-api/src/rpc/models.rs @@ -0,0 +1,30 @@ +//! Models for the Mycelium JSON-RPC API + +use serde::{Deserialize, Serialize}; + +// Define any additional models needed for the JSON-RPC API +// Most models can be reused from the existing REST API + +/// Error codes for the JSON-RPC API +pub mod error_codes { + /// Invalid parameters error code + pub const INVALID_PARAMS: i64 = -32602; + + /// Peer already exists error code + pub const PEER_EXISTS: i64 = 409; + + /// Peer not found error code + pub const PEER_NOT_FOUND: i64 = 404; + + /// Message not found error code + pub const MESSAGE_NOT_FOUND: i64 = 404; + + /// Public key not found error code + pub const PUBKEY_NOT_FOUND: i64 = 404; + + /// No message ready error code + pub const NO_MESSAGE_READY: i64 = 204; + + /// Timeout waiting for reply error code + pub const TIMEOUT_WAITING_FOR_REPLY: i64 = 408; +} \ No newline at end of file diff --git a/components/mycelium/mycelium-api/src/rpc/peer.rs b/components/mycelium/mycelium-api/src/rpc/peer.rs new file mode 100644 index 0000000..747f828 --- /dev/null +++ b/components/mycelium/mycelium-api/src/rpc/peer.rs @@ -0,0 +1,86 @@ +//! Peer-related JSON-RPC methods for the Mycelium API + +use jsonrpc_core::{Error, ErrorCode, Result as RpcResult}; +use std::str::FromStr; +use tracing::debug; + +use mycelium::endpoint::Endpoint; +use mycelium::metrics::Metrics; +use mycelium::peer_manager::{PeerExists, PeerNotFound, PeerStats}; + +use crate::rpc::models::error_codes; +use crate::rpc::traits::PeerApi; +use crate::HttpServerState; + +/// Implementation of Peer-related JSON-RPC methods +pub struct PeerRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + state: HttpServerState, +} + +impl PeerRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + /// Create a new PeerRpc instance + pub fn new(state: HttpServerState) -> Self { + Self { state } + } +} + +impl PeerApi for PeerRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + fn get_peers(&self) -> RpcResult> { + debug!("Fetching peer stats via RPC"); + Ok(self.state.node.blocking_lock().peer_info()) + } + + fn add_peer(&self, endpoint: String) -> RpcResult { + debug!( + peer.endpoint = endpoint, + "Attempting to add peer to the system via RPC" + ); + + let endpoint = Endpoint::from_str(&endpoint).map_err(|e| Error { + code: ErrorCode::InvalidParams, + message: e.to_string(), + data: None, + })?; + + match self.state.node.blocking_lock().add_peer(endpoint) { + Ok(()) => Ok(true), + Err(PeerExists) => Err(Error { + code: ErrorCode::ServerError(error_codes::PEER_EXISTS), + message: "A peer identified by that endpoint already exists".to_string(), + data: None, + }), + } + } + + fn delete_peer(&self, endpoint: String) -> RpcResult { + debug!( + peer.endpoint = endpoint, + "Attempting to remove peer from the system via RPC" + ); + + let endpoint = Endpoint::from_str(&endpoint).map_err(|e| Error { + code: ErrorCode::InvalidParams, + message: e.to_string(), + data: None, + })?; + + match self.state.node.blocking_lock().remove_peer(endpoint) { + Ok(()) => Ok(true), + Err(PeerNotFound) => Err(Error { + code: ErrorCode::ServerError(error_codes::PEER_NOT_FOUND), + message: "A peer identified by that endpoint does not exist".to_string(), + data: None, + }), + } + } +} + diff --git a/components/mycelium/mycelium-api/src/rpc/route.rs b/components/mycelium/mycelium-api/src/rpc/route.rs new file mode 100644 index 0000000..f379a2b --- /dev/null +++ b/components/mycelium/mycelium-api/src/rpc/route.rs @@ -0,0 +1,120 @@ +//! Route-related JSON-RPC methods for the Mycelium API + +use jsonrpc_core::Result as RpcResult; +use tracing::debug; + +use mycelium::metrics::Metrics; + +use crate::HttpServerState; +use crate::Route; +use crate::QueriedSubnet; +use crate::NoRouteSubnet; +use crate::Metric; +use crate::rpc::traits::RouteApi; + +/// Implementation of Route-related JSON-RPC methods +pub struct RouteRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + state: HttpServerState, +} + +impl RouteRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + /// Create a new RouteRpc instance + pub fn new(state: HttpServerState) -> Self { + Self { state } + } +} + +impl RouteApi for RouteRpc +where + M: Metrics + Clone + Send + Sync + 'static, +{ + fn get_selected_routes(&self) -> RpcResult> { + debug!("Loading selected routes via RPC"); + let routes = self.state + .node + .blocking_lock() + .selected_routes() + .into_iter() + .map(|sr| Route { + subnet: sr.source().subnet().to_string(), + next_hop: sr.neighbour().connection_identifier().clone(), + metric: if sr.metric().is_infinite() { + Metric::Infinite + } else { + Metric::Value(sr.metric().into()) + }, + seqno: sr.seqno().into(), + }) + .collect(); + + Ok(routes) + } + + fn get_fallback_routes(&self) -> RpcResult> { + debug!("Loading fallback routes via RPC"); + let routes = self.state + .node + .blocking_lock() + .fallback_routes() + .into_iter() + .map(|sr| Route { + subnet: sr.source().subnet().to_string(), + next_hop: sr.neighbour().connection_identifier().clone(), + metric: if sr.metric().is_infinite() { + Metric::Infinite + } else { + Metric::Value(sr.metric().into()) + }, + seqno: sr.seqno().into(), + }) + .collect(); + + Ok(routes) + } + + fn get_queried_subnets(&self) -> RpcResult> { + debug!("Loading queried subnets via RPC"); + let queries = self.state + .node + .blocking_lock() + .queried_subnets() + .into_iter() + .map(|qs| QueriedSubnet { + subnet: qs.subnet().to_string(), + expiration: qs + .query_expires() + .duration_since(tokio::time::Instant::now()) + .as_secs() + .to_string(), + }) + .collect(); + + Ok(queries) + } + + fn get_no_route_entries(&self) -> RpcResult> { + debug!("Loading no route entries via RPC"); + let entries = self.state + .node + .blocking_lock() + .no_route_entries() + .into_iter() + .map(|nrs| NoRouteSubnet { + subnet: nrs.subnet().to_string(), + expiration: nrs + .entry_expires() + .duration_since(tokio::time::Instant::now()) + .as_secs() + .to_string(), + }) + .collect(); + + Ok(entries) + } +} \ No newline at end of file diff --git a/components/mycelium/mycelium-api/src/rpc/spec.rs b/components/mycelium/mycelium-api/src/rpc/spec.rs new file mode 100644 index 0000000..94e686a --- /dev/null +++ b/components/mycelium/mycelium-api/src/rpc/spec.rs @@ -0,0 +1,4 @@ +//! OpenRPC specification for the Mycelium JSON-RPC API + +/// The OpenRPC specification for the Mycelium JSON-RPC API +pub const OPENRPC_SPEC: &str = include_str!("../../../docs/openrpc.json"); diff --git a/components/mycelium/mycelium-api/src/rpc/traits.rs b/components/mycelium/mycelium-api/src/rpc/traits.rs new file mode 100644 index 0000000..3e28c9b --- /dev/null +++ b/components/mycelium/mycelium-api/src/rpc/traits.rs @@ -0,0 +1,80 @@ +//! RPC trait definitions for the Mycelium JSON-RPC API + +use jsonrpc_core::Result as RpcResult; +use jsonrpc_derive::rpc; + +use crate::Info; +use crate::Route; +use crate::QueriedSubnet; +use crate::NoRouteSubnet; +use mycelium::crypto::PublicKey; +use mycelium::peer_manager::PeerStats; +use mycelium::message::{MessageId, MessageInfo}; + +// Admin-related RPC methods +#[rpc] +pub trait AdminApi { + /// Get general info about the node + #[rpc(name = "getInfo")] + fn get_info(&self) -> RpcResult; + + /// Get the pubkey from node ip + #[rpc(name = "getPublicKeyFromIp")] + fn get_pubkey_from_ip(&self, mycelium_ip: String) -> RpcResult; +} + +// Peer-related RPC methods +#[rpc] +pub trait PeerApi { + /// List known peers + #[rpc(name = "getPeers")] + fn get_peers(&self) -> RpcResult>; + + /// Add a new peer + #[rpc(name = "addPeer")] + fn add_peer(&self, endpoint: String) -> RpcResult; + + /// Remove an existing peer + #[rpc(name = "deletePeer")] + fn delete_peer(&self, endpoint: String) -> RpcResult; +} + +// Route-related RPC methods +#[rpc] +pub trait RouteApi { + /// List all selected routes + #[rpc(name = "getSelectedRoutes")] + fn get_selected_routes(&self) -> RpcResult>; + + /// List all active fallback routes + #[rpc(name = "getFallbackRoutes")] + fn get_fallback_routes(&self) -> RpcResult>; + + /// List all currently queried subnets + #[rpc(name = "getQueriedSubnets")] + fn get_queried_subnets(&self) -> RpcResult>; + + /// List all subnets which are explicitly marked as no route + #[rpc(name = "getNoRouteEntries")] + fn get_no_route_entries(&self) -> RpcResult>; +} + +// Message-related RPC methods +#[rpc] +pub trait MessageApi { + /// Get a message from the inbound message queue + #[rpc(name = "popMessage")] + fn pop_message(&self, peek: Option, timeout: Option, topic: Option) -> RpcResult; + + /// Submit a new message to the system + #[rpc(name = "pushMessage")] + fn push_message(&self, message: crate::message::MessageSendInfo, reply_timeout: Option) -> RpcResult; + + /// Reply to a message with the given ID + #[rpc(name = "pushMessageReply")] + fn push_message_reply(&self, id: String, message: crate::message::MessageSendInfo) -> RpcResult; + + /// Get the status of an outbound message + #[rpc(name = "getMessageInfo")] + fn get_message_info(&self, id: String) -> RpcResult; +} \ No newline at end of file diff --git a/components/mycelium/mycelium-cli/Cargo.toml b/components/mycelium/mycelium-cli/Cargo.toml new file mode 100644 index 0000000..00139e7 --- /dev/null +++ b/components/mycelium/mycelium-cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "mycelium-cli" +version = "0.6.1" +edition = "2021" +license-file = "../LICENSE" +readme = "./README.md" + +[features] +message = ["mycelium/message", "mycelium-api/message"] + +[dependencies] +mycelium = { path = "../mycelium" } +mycelium-api = { path = "../mycelium-api" } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +base64 = "0.22.1" +prettytable-rs = "0.10.0" +tracing = "0.1.41" +tokio = { version = "1.46.1", default-features = false, features = [ + "net", + "rt", + "fs", +] } +reqwest = { version = "0.12.22", default-features = false, features = ["json"] } +byte-unit = "5.1.6" +urlencoding = "2.1.3" diff --git a/components/mycelium/mycelium-cli/src/inspect.rs b/components/mycelium/mycelium-cli/src/inspect.rs new file mode 100644 index 0000000..d9849f3 --- /dev/null +++ b/components/mycelium/mycelium-cli/src/inspect.rs @@ -0,0 +1,30 @@ +use std::net::IpAddr; + +use mycelium::crypto::PublicKey; +use serde::Serialize; + +#[derive(Debug, Serialize)] +struct InspectOutput { + #[serde(rename = "publicKey")] + public_key: PublicKey, + address: IpAddr, +} + +/// Inspect the given pubkey, or the local key if no pubkey is given +pub fn inspect(pubkey: PublicKey, json: bool) -> Result<(), Box> { + let address = pubkey.address().into(); + if json { + let out = InspectOutput { + public_key: pubkey, + address, + }; + + let out_string = serde_json::to_string_pretty(&out)?; + println!("{out_string}"); + } else { + println!("Public key: {pubkey}"); + println!("Address: {address}"); + } + + Ok(()) +} diff --git a/components/mycelium/mycelium-cli/src/lib.rs b/components/mycelium/mycelium-cli/src/lib.rs new file mode 100644 index 0000000..983ef30 --- /dev/null +++ b/components/mycelium/mycelium-cli/src/lib.rs @@ -0,0 +1,13 @@ +mod inspect; +#[cfg(feature = "message")] +mod message; +mod peer; +mod routes; + +pub use inspect::inspect; +#[cfg(feature = "message")] +pub use message::{recv_msg, send_msg}; +pub use peer::{add_peers, list_peers, remove_peers}; +pub use routes::{ + list_fallback_routes, list_no_route_entries, list_queried_subnets, list_selected_routes, +}; diff --git a/components/mycelium/mycelium-cli/src/message.rs b/components/mycelium/mycelium-cli/src/message.rs new file mode 100644 index 0000000..7bea75a --- /dev/null +++ b/components/mycelium/mycelium-cli/src/message.rs @@ -0,0 +1,306 @@ +use std::{ + io::Write, + mem, + net::{IpAddr, SocketAddr}, + path::PathBuf, +}; + +use base64::{ + alphabet, + engine::{GeneralPurpose, GeneralPurposeConfig}, + Engine, +}; +use mycelium::{crypto::PublicKey, message::MessageId, subnet::Subnet}; +use serde::{Serialize, Serializer}; +use tracing::{debug, error}; + +use mycelium_api::{MessageDestination, MessageReceiveInfo, MessageSendInfo, PushMessageResponse}; + +enum Payload { + Readable(String), + NotReadable(Vec), +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CliMessage { + id: MessageId, + src_ip: IpAddr, + src_pk: PublicKey, + dst_ip: IpAddr, + dst_pk: PublicKey, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(serialize_with = "serialize_payload")] + topic: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(serialize_with = "serialize_payload")] + payload: Option, +} + +const B64ENGINE: GeneralPurpose = base64::engine::general_purpose::GeneralPurpose::new( + &alphabet::STANDARD, + GeneralPurposeConfig::new(), +); +fn serialize_payload(p: &Option, s: S) -> Result { + let base64 = match p { + None => None, + Some(Payload::Readable(data)) => Some(data.clone()), + Some(Payload::NotReadable(data)) => Some(B64ENGINE.encode(data)), + }; + >::serialize(&base64, s) +} + +/// Encode arbitrary data in standard base64. +pub fn encode_base64(input: &[u8]) -> String { + B64ENGINE.encode(input) +} + +/// Send a message to a receiver. +#[allow(clippy::too_many_arguments)] +pub async fn send_msg( + destination: String, + msg: Option, + wait: bool, + timeout: Option, + reply_to: Option, + topic: Option, + msg_path: Option, + server_addr: SocketAddr, +) -> Result<(), Box> { + if reply_to.is_some() && wait { + error!("Can't wait on a reply for a reply, either use --reply-to or --wait"); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Only one of --reply-to or --wait is allowed", + ) + .into()); + } + let destination = if destination.len() == 64 { + // Public key in hex format + match PublicKey::try_from(&*destination) { + Err(_) => { + error!("{destination} is not a valid hex encoded public key"); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Invalid hex encoded public key", + ) + .into()); + } + Ok(pk) => MessageDestination::Pk(pk), + } + } else { + match destination.parse() { + Err(e) => { + error!("{destination} is not a valid IPv6 address: {e}"); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Invalid IPv6 address", + ) + .into()); + } + Ok(ip) => { + let global_subnet = Subnet::new( + mycelium::GLOBAL_SUBNET_ADDRESS, + mycelium::GLOBAL_SUBNET_PREFIX_LEN, + ) + .unwrap(); + if !global_subnet.contains_ip(ip) { + error!("{destination} is not a part of {global_subnet}"); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "IPv6 address is not part of the mycelium subnet", + ) + .into()); + } + MessageDestination::Ip(ip) + } + } + }; + + // Load msg, files have prio. + let msg = if let Some(path) = msg_path { + match tokio::fs::read(&path).await { + Err(e) => { + error!("Could not read file at {:?}: {e}", path); + return Err(e.into()); + } + Ok(data) => data, + } + } else if let Some(msg) = msg { + msg.into_bytes() + } else { + error!("Message is a required argument if `--msg-path` is not provided"); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Message is a required argument if `--msg-path` is not provided", + ) + .into()); + }; + + let mut url = format!("http://{server_addr}/api/v1/messages"); + if let Some(reply_to) = reply_to { + url.push_str(&format!("/reply/{reply_to}")); + } + if wait { + // A year should be sufficient to wait + let reply_timeout = timeout.unwrap_or(60 * 60 * 24 * 365); + url.push_str(&format!("?reply_timeout={reply_timeout}")); + } + + match reqwest::Client::new() + .post(url) + .json(&MessageSendInfo { + dst: destination, + topic: topic.map(String::into_bytes), + payload: msg, + }) + .send() + .await + { + Err(e) => { + error!("Failed to send request: {e}"); + return Err(e.into()); + } + Ok(res) => { + if res.status() == STATUSCODE_NO_CONTENT { + return Ok(()); + } + match res.json::().await { + Err(e) => { + error!("Failed to load response body {e}"); + return Err(e.into()); + } + Ok(resp) => { + match resp { + PushMessageResponse::Id(id) => { + let _ = serde_json::to_writer(std::io::stdout(), &id); + } + PushMessageResponse::Reply(mri) => { + let cm = CliMessage { + id: mri.id, + + topic: mri.topic.map(|topic| { + if let Ok(s) = String::from_utf8(topic.clone()) { + Payload::Readable(s) + } else { + Payload::NotReadable(topic) + } + }), + src_ip: mri.src_ip, + src_pk: mri.src_pk, + dst_ip: mri.dst_ip, + dst_pk: mri.dst_pk, + payload: Some({ + if let Ok(s) = String::from_utf8(mri.payload.clone()) { + Payload::Readable(s) + } else { + Payload::NotReadable(mri.payload) + } + }), + }; + let _ = serde_json::to_writer(std::io::stdout(), &cm); + } + } + println!(); + } + } + } + } + + Ok(()) +} + +const STATUSCODE_NO_CONTENT: u16 = 204; + +pub async fn recv_msg( + timeout: Option, + topic: Option, + msg_path: Option, + raw: bool, + server_addr: SocketAddr, +) -> Result<(), Box> { + // One year timeout should be sufficient + let timeout = timeout.unwrap_or(60 * 60 * 24 * 365); + let mut url = format!("http://{server_addr}/api/v1/messages?timeout={timeout}"); + if let Some(ref topic) = topic { + if topic.len() > 255 { + error!("{topic} is longer than the maximum allowed topic length of 255"); + return Err( + std::io::Error::new(std::io::ErrorKind::InvalidInput, "Topic too long").into(), + ); + } + url.push_str(&format!("&topic={}", encode_base64(topic.as_bytes()))); + } + let mut cm = match reqwest::get(url).await { + Err(e) => { + error!("Failed to wait for message: {e}"); + return Err(e.into()); + } + Ok(resp) => { + if resp.status() == STATUSCODE_NO_CONTENT { + debug!("No message ready yet"); + return Ok(()); + } + + debug!("Received message response"); + match resp.json::().await { + Err(e) => { + error!("Failed to load response json: {e}"); + return Err(e.into()); + } + Ok(mri) => CliMessage { + id: mri.id, + topic: mri.topic.map(|topic| { + if let Ok(s) = String::from_utf8(topic.clone()) { + Payload::Readable(s) + } else { + Payload::NotReadable(topic) + } + }), + src_ip: mri.src_ip, + src_pk: mri.src_pk, + dst_ip: mri.dst_ip, + dst_pk: mri.dst_pk, + payload: Some({ + if let Ok(s) = String::from_utf8(mri.payload.clone()) { + Payload::Readable(s) + } else { + Payload::NotReadable(mri.payload) + } + }), + }, + } + } + }; + + if let Some(ref file_path) = msg_path { + if let Err(e) = tokio::fs::write( + &file_path, + match mem::take(&mut cm.payload).unwrap() { + Payload::Readable(ref s) => s as &dyn AsRef<[u8]>, + Payload::NotReadable(ref v) => v, + }, + ) + .await + { + error!("Failed to write response payload to file: {e}"); + return Err(e.into()); + } + } + + if raw { + // only print payload if not already written + if msg_path.is_none() { + let _ = std::io::stdout().write_all(match cm.payload.unwrap() { + Payload::Readable(ref s) => s.as_bytes(), + Payload::NotReadable(ref v) => v, + }); + println!(); + } + } else { + let _ = serde_json::to_writer(std::io::stdout(), &cm); + println!(); + } + + Ok(()) +} diff --git a/components/mycelium/mycelium-cli/src/peer.rs b/components/mycelium/mycelium-cli/src/peer.rs new file mode 100644 index 0000000..a981bcc --- /dev/null +++ b/components/mycelium/mycelium-cli/src/peer.rs @@ -0,0 +1,141 @@ +use mycelium::peer_manager::PeerStats; +use mycelium_api::AddPeer; +use prettytable::{row, Table}; +use std::net::SocketAddr; +use tracing::{debug, error}; + +/// List the peers the current node is connected to +pub async fn list_peers( + server_addr: SocketAddr, + json_print: bool, +) -> Result<(), Box> { + // Make API call + let request_url = format!("http://{server_addr}/api/v1/admin/peers"); + match reqwest::get(&request_url).await { + Err(e) => { + error!("Failed to retrieve peers"); + return Err(e.into()); + } + Ok(resp) => { + debug!("Listing connected peers"); + match resp.json::>().await { + Err(e) => { + error!("Failed to load response json: {e}"); + return Err(e.into()); + } + Ok(peers) => { + if json_print { + // Print peers in JSON format + let json_output = serde_json::to_string_pretty(&peers)?; + println!("{json_output}"); + } else { + // Print peers in table format + let mut table = Table::new(); + table.add_row(row![ + "Protocol", + "Socket", + "Type", + "Connection", + "Rx total", + "Tx total", + "Discovered", + "Last connection" + ]); + for peer in peers.iter() { + table.add_row(row![ + peer.endpoint.proto(), + peer.endpoint.address(), + peer.pt, + peer.connection_state, + format_bytes(peer.rx_bytes), + format_bytes(peer.tx_bytes), + format_seconds(peer.discovered), + peer.last_connected + .map(format_seconds) + .unwrap_or("Never connected".to_string()), + ]); + } + table.printstd(); + } + } + } + } + }; + + Ok(()) +} + +fn format_bytes(bytes: u64) -> String { + let byte = byte_unit::Byte::from_u64(bytes); + let adjusted_byte = byte.get_appropriate_unit(byte_unit::UnitType::Binary); + format!( + "{:.2} {}", + adjusted_byte.get_value(), + adjusted_byte.get_unit() + ) +} + +/// Convert an amount of seconds into a human readable string. +fn format_seconds(total_seconds: u64) -> String { + let seconds = total_seconds % 60; + let minutes = (total_seconds / 60) % 60; + let hours = (total_seconds / 3600) % 60; + let days = (total_seconds / 86400) % 60; + + if days > 0 { + format!("{days}d {hours}h {minutes}m {seconds}s") + } else if hours > 0 { + format!("{hours}h {minutes}m {seconds}s") + } else if minutes > 0 { + format!("{minutes}m {seconds}s") + } else { + format!("{seconds}s") + } +} + +/// Remove peer(s) by (underlay) IP +pub async fn remove_peers( + server_addr: SocketAddr, + peers: Vec, +) -> Result<(), Box> { + let client = reqwest::Client::new(); + for peer in peers.iter() { + // encode to pass in URL + let peer_encoded = urlencoding::encode(peer); + let request_url = format!("http://{server_addr}/api/v1/admin/peers/{peer_encoded}"); + if let Err(e) = client + .delete(&request_url) + .send() + .await + .and_then(|res| res.error_for_status()) + { + error!("Failed to delete peer: {e}"); + return Err(e.into()); + } + } + + Ok(()) +} + +/// Add peer(s) by (underlay) IP +pub async fn add_peers( + server_addr: SocketAddr, + peers: Vec, +) -> Result<(), Box> { + let client = reqwest::Client::new(); + for peer in peers.into_iter() { + let request_url = format!("http://{server_addr}/api/v1/admin/peers"); + if let Err(e) = client + .post(&request_url) + .json(&AddPeer { endpoint: peer }) + .send() + .await + .and_then(|res| res.error_for_status()) + { + error!("Failed to add peer: {e}"); + return Err(e.into()); + } + } + + Ok(()) +} diff --git a/components/mycelium/mycelium-cli/src/routes.rs b/components/mycelium/mycelium-cli/src/routes.rs new file mode 100644 index 0000000..37c8d48 --- /dev/null +++ b/components/mycelium/mycelium-cli/src/routes.rs @@ -0,0 +1,152 @@ +use mycelium_api::{NoRouteSubnet, QueriedSubnet, Route}; +use prettytable::{row, Table}; +use std::net::SocketAddr; + +use tracing::{debug, error}; + +pub async fn list_selected_routes( + server_addr: SocketAddr, + json_print: bool, +) -> Result<(), Box> { + let request_url = format!("http://{server_addr}/api/v1/admin/routes/selected"); + match reqwest::get(&request_url).await { + Err(e) => { + error!("Failed to retrieve selected routes"); + return Err(e.into()); + } + Ok(resp) => { + debug!("Listing selected routes"); + + if json_print { + // API call returns routes in JSON format by default + let selected_routes = resp.text().await?; + println!("{selected_routes}"); + } else { + // Print routes in table format + let routes: Vec = resp.json().await?; + let mut table = Table::new(); + table.add_row(row!["Subnet", "Next Hop", "Metric", "Seq No"]); + + for route in routes.iter() { + table.add_row(row![ + &route.subnet, + &route.next_hop, + route.metric, + route.seqno, + ]); + } + + table.printstd(); + } + } + } + + Ok(()) +} + +pub async fn list_fallback_routes( + server_addr: SocketAddr, + json_print: bool, +) -> Result<(), Box> { + let request_url = format!("http://{server_addr}/api/v1/admin/routes/fallback"); + match reqwest::get(&request_url).await { + Err(e) => { + error!("Failed to retrieve fallback routes"); + return Err(e.into()); + } + Ok(resp) => { + debug!("Listing fallback routes"); + + if json_print { + // API call returns routes in JSON format by default + let fallback_routes = resp.text().await?; + println!("{fallback_routes}"); + } else { + // Print routes in table format + let routes: Vec = resp.json().await?; + let mut table = Table::new(); + table.add_row(row!["Subnet", "Next Hop", "Metric", "Seq No"]); + + for route in routes.iter() { + table.add_row(row![ + &route.subnet, + &route.next_hop, + route.metric, + route.seqno, + ]); + } + + table.printstd(); + } + } + } + Ok(()) +} + +pub async fn list_queried_subnets( + server_addr: SocketAddr, + json_print: bool, +) -> Result<(), Box> { + let request_url = format!("http://{server_addr}/api/v1/admin/routes/queried"); + match reqwest::get(&request_url).await { + Err(e) => { + error!("Failed to retrieve queried subnets"); + return Err(e.into()); + } + Ok(resp) => { + debug!("Listing queried routes"); + + if json_print { + // API call returns routes in JSON format by default + let queried_routes = resp.text().await?; + println!("{queried_routes}"); + } else { + // Print routes in table format + let queries: Vec = resp.json().await?; + let mut table = Table::new(); + table.add_row(row!["Subnet", "Query expiration"]); + + for query in queries.iter() { + table.add_row(row![query.subnet, query.expiration,]); + } + + table.printstd(); + } + } + } + Ok(()) +} + +pub async fn list_no_route_entries( + server_addr: SocketAddr, + json_print: bool, +) -> Result<(), Box> { + let request_url = format!("http://{server_addr}/api/v1/admin/routes/no_route"); + match reqwest::get(&request_url).await { + Err(e) => { + error!("Failed to retrieve subnets with no route entries"); + return Err(e.into()); + } + Ok(resp) => { + debug!("Listing no route entries"); + + if json_print { + // API call returns routes in JSON format by default + let nrs = resp.text().await?; + println!("{nrs}"); + } else { + // Print routes in table format + let no_routes: Vec = resp.json().await?; + let mut table = Table::new(); + table.add_row(row!["Subnet", "Entry expiration"]); + + for nrs in no_routes.iter() { + table.add_row(row![nrs.subnet, nrs.expiration,]); + } + + table.printstd(); + } + } + } + Ok(()) +} diff --git a/components/mycelium/mycelium-metrics/Cargo.toml b/components/mycelium/mycelium-metrics/Cargo.toml new file mode 100644 index 0000000..6222f40 --- /dev/null +++ b/components/mycelium/mycelium-metrics/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "mycelium-metrics" +version = "0.6.1" +edition = "2021" +license-file = "../LICENSE" +readme = "../README.md" + +[features] +prometheus = ["dep:axum", "dep:prometheus", "dep:tokio", "dep:tracing"] + +[dependencies] +axum = { version = "0.8.4", default-features = false, optional = true, features = [ + "http1", + "http2", + "tokio", +] } +mycelium = { path = "../mycelium", default-features = false } +prometheus = { version = "0.14.0", default-features = false, optional = true, features = [ + "process", +] } +tokio = { version = "1.46.1", default-features = false, optional = true, features = [ + "net", + "rt", +] } +tracing = { version = "0.1.41", optional = true } diff --git a/components/mycelium/mycelium-metrics/src/lib.rs b/components/mycelium/mycelium-metrics/src/lib.rs new file mode 100644 index 0000000..a8fc263 --- /dev/null +++ b/components/mycelium/mycelium-metrics/src/lib.rs @@ -0,0 +1,11 @@ +//! This crate provides implementations of [`the Metrics trait`](mycelium::metrics::Metrics). +//! 2 options are exposed currently: a NOOP implementation which doesn't record anything, +//! and a prometheus exporter which exposes all metrics in a promtheus compatible format. + +mod noop; +pub use noop::NoMetrics; + +#[cfg(feature = "prometheus")] +mod prometheus; +#[cfg(feature = "prometheus")] +pub use prometheus::PrometheusExporter; diff --git a/components/mycelium/mycelium-metrics/src/noop.rs b/components/mycelium/mycelium-metrics/src/noop.rs new file mode 100644 index 0000000..cc0cef0 --- /dev/null +++ b/components/mycelium/mycelium-metrics/src/noop.rs @@ -0,0 +1,5 @@ +use mycelium::metrics::Metrics; + +#[derive(Clone)] +pub struct NoMetrics; +impl Metrics for NoMetrics {} diff --git a/components/mycelium/mycelium-metrics/src/prometheus.rs b/components/mycelium/mycelium-metrics/src/prometheus.rs new file mode 100644 index 0000000..b4202f9 --- /dev/null +++ b/components/mycelium/mycelium-metrics/src/prometheus.rs @@ -0,0 +1,446 @@ +use axum::{routing::get, Router}; +use mycelium::metrics::Metrics; +use prometheus::{ + opts, register_int_counter, register_int_counter_vec, register_int_gauge, Encoder, IntCounter, + IntCounterVec, IntGauge, TextEncoder, +}; +use tracing::{error, info}; + +use std::net::SocketAddr; + +/// A [`Metrics`] implementation which uses prometheus to expose the metrics to the outside world. +#[derive(Clone)] +pub struct PrometheusExporter { + router_processed_tlvs: IntCounterVec, + router_peer_added: IntCounter, + router_peer_removed: IntCounter, + router_peer_died: IntCounter, + router_route_selection_ran: IntCounter, + router_source_key_expired: IntCounter, + router_expired_routes: IntCounterVec, + router_selected_route_expired: IntCounter, + router_triggered_update: IntCounter, + router_route_packet: IntCounterVec, + router_seqno_action: IntCounterVec, + router_tlv_handling_time_spent: IntCounterVec, + router_update_dead_peer: IntCounter, + router_received_tlvs: IntCounter, + router_tlv_source_died: IntCounter, + router_tlv_discarded: IntCounter, + router_propage_selected_peers_time_spent: IntCounter, + router_update_skipped_route_selection: IntCounter, + router_update_denied_by_filter: IntCounter, + router_update_not_interested: IntCounter, + peer_manager_peer_added: IntCounterVec, + peer_manager_known_peers: IntGauge, + peer_manager_connection_attemps: IntCounterVec, +} + +impl PrometheusExporter { + /// Create a new [`PrometheusExporter`]. + pub fn new() -> Self { + Self { + router_processed_tlvs: register_int_counter_vec!( + opts!( + "mycelium_router_processed_tlvs", + "Amount of processed TLV's from peers, by type of TLV" + ), &["tlv_type"] + ).expect("Can register int counter vec in default registry"), + router_peer_added: register_int_counter!( + "mycelium_router_peer_added", + "Amount of times a peer was added to the router" + ).expect("Can register int counter in default registry"), + router_peer_removed: register_int_counter!( + "mycelium_router_peer_removed", + "Amount of times a peer was removed from the router" + ).expect("Can register int counter in default registry"), + router_peer_died: register_int_counter!( + "mycelium_router_peer_died", + "Amount of times the router noticed a peer was dead, or the peer noticed itself and informed the router", + ).expect("Can register int counter in default registry"), + router_route_selection_ran: register_int_counter!( + "mycelium_router_route_selections", + "Amount of times a route selection procedure was ran as result of routes expiring or peers being disconnected. Does not include route selection after an update", + ).expect("Can register int counte rin default registry"), + router_source_key_expired: register_int_counter!( + "mycelium_router_source_key_expired", + "Amount of source keys expired" + ) + .expect("Can register int counter in default registry"), + router_expired_routes: register_int_counter_vec!( + opts!( + "mycelium_router_expired_routes", + "Route expiration events and the action taken on the route", + ), + &["action"] + ) + .expect("Can register int counter vec in default registry"), + router_selected_route_expired: register_int_counter!( + "mycelium_router_selected_route_expired", + "Amount of times a selected route in the routing table expired" + ) + .expect("Can register int counter in default registry"), + router_triggered_update: register_int_counter!( + "mycelium_router_triggered_updates", + "Amount of triggered updates sent" + ) + .expect("Can register int counter in default registry"), + router_route_packet: register_int_counter_vec!( + opts!( + "mycelium_router_packets_routed", + "What happened to a routed data packet" + ), + &["verdict"], + ) + .expect("Can register int counter vec in default registry"), + router_seqno_action: register_int_counter_vec!( + opts!( + "mycelium_router_seqno_handling", + "What happened to a received seqno request", + ), + &["action"], + ) + .expect("Can register int counter vec in default registry"), + router_tlv_handling_time_spent: register_int_counter_vec!( + opts!( + "mycelium_router_tlv_handling_time", + "Amount of time spent handling incoming TLV packets, in nanoseconds", + ), + &["tlv_type"], + ) + .expect("Can register an int counter vec in default registry"), + router_update_dead_peer: register_int_counter!( + "mycelium_router_update_dead_peer", + "Amount of updates we tried to send to a peer, where we found the peer to be dead before actually sending" + ) + .expect("Can register an int counter in default registry"), + router_received_tlvs: register_int_counter!( + "mycelium_router_received_tlvs", + "Amount of tlv's received by peers", + ) + .expect("Can register an int counter in the default registry"), + router_tlv_source_died: register_int_counter!( + "mycelium_router_tlv_source_died", + "Dropped TLV's which have been received, but where the peer has died before they could be processed", + ) + .expect("Can register an int counter in default registry"), + router_tlv_discarded: register_int_counter!( + "mycelium_router_tlv_discarded", + "Dropped TLV's which have been received, but where not processed because the router couldn't keep up", + ) + .expect("Can register an int counter in default registry"), + router_propage_selected_peers_time_spent: register_int_counter!( + "mycelium_router_propagate_selected_route_time", + "Time spent in the propagate_selected_route task, which periodically announces selected routes to peers. Measurement is in nanoseconds", + ) + .expect("Can register an int counter in default registry"), + router_update_skipped_route_selection: register_int_counter!( + "mycelium_router_update_skipped_route_selection", + "Updates which were processed but did not run the route selection step, because the updated route could not be selected anyway", + ) + .expect("Can register an int counter in default registry"), + router_update_denied_by_filter: register_int_counter!( + "mycelium_router_update_denied", + "Updates which were received and immediately denied by a configured filter", + ) + .expect("Can register an int counter in default registry"), + router_update_not_interested: register_int_counter!( + "mycelium_router_update_not_interested", + "Updates which were allowed by the configured filters, but not of interest as they were either not feasible, or retractions, for an unknown subnet", + ) + .expect("Can register an int counter in default registry"), + peer_manager_peer_added: register_int_counter_vec!( + opts!( + "mycelium_peer_manager_peers_added", + "Peers added to the peer manager at runtime, by peer type" + ), + &["peer_type"], + ) + .expect("Can register int counter vec in default registry"), + peer_manager_known_peers: register_int_gauge!( + "mycelium_peer_manager_known_peers", + "Amount of known peers in the peer manager" + ) + .expect("Can register int gauge in default registry"), + peer_manager_connection_attemps: register_int_counter_vec!( + opts!( + "mycelium_peer_manager_connection_attempts", + "Count how many connections the peer manager started to remotes, and finished" + ), + &["connection_state"] + ) + .expect("Can register int counter vec in the default registry"), + } + } + + /// Spawns a HTTP server on the provided [`SocketAddr`], to export the gathered metrics. Metrics + /// are served under the /metrics endpoint. + pub fn spawn(self, listen_addr: SocketAddr) { + info!("Enable system metrics on http://{listen_addr}/metrics"); + let app = Router::new().route("/metrics", get(serve_metrics)); + tokio::spawn(async move { + let listener = match tokio::net::TcpListener::bind(listen_addr).await { + Ok(listener) => listener, + Err(e) => { + error!("Failed to bind listener for Http metrics server: {e}"); + error!("metrics disabled"); + return; + } + }; + + let server = axum::serve(listener, app.into_make_service()); + if let Err(e) = server.await { + error!("Http API server error: {e}"); + } + }); + } +} + +/// Expose prometheus formatted metrics +async fn serve_metrics() -> String { + let mut buffer = Vec::new(); + let encoder = TextEncoder::new(); + + // Gather the metrics. + let metric_families = prometheus::gather(); + // Encode them to send. + encoder + .encode(&metric_families, &mut buffer) + .expect("Can encode metrics"); + String::from_utf8(buffer).expect("Metrics are encoded in valid prometheus format") +} + +impl Metrics for PrometheusExporter { + #[inline] + fn router_process_hello(&self) { + self.router_processed_tlvs + .with_label_values(&["hello"]) + .inc() + } + + #[inline] + fn router_process_ihu(&self) { + self.router_processed_tlvs.with_label_values(&["ihu"]).inc() + } + + #[inline] + fn router_process_seqno_request(&self) { + self.router_processed_tlvs + .with_label_values(&["seqno_request"]) + .inc() + } + + #[inline] + fn router_process_route_request(&self, wildcard: bool) { + let label = if wildcard { + "wildcard_route_request" + } else { + "route_request" + }; + self.router_processed_tlvs.with_label_values(&[label]).inc() + } + + #[inline] + fn router_process_update(&self) { + self.router_processed_tlvs + .with_label_values(&["update"]) + .inc() + } + + #[inline] + fn router_peer_added(&self) { + self.router_peer_added.inc() + } + + #[inline] + fn router_peer_removed(&self) { + self.router_peer_removed.inc() + } + + #[inline] + fn router_peer_died(&self) { + self.router_peer_died.inc() + } + + #[inline] + fn router_route_selection_ran(&self) { + self.router_route_selection_ran.inc() + } + + #[inline] + fn router_source_key_expired(&self) { + self.router_source_key_expired.inc() + } + + #[inline] + fn router_route_key_expired(&self, removed: bool) { + let label = if removed { "removed" } else { "retracted" }; + self.router_expired_routes.with_label_values(&[label]).inc() + } + + #[inline] + fn router_selected_route_expired(&self) { + self.router_selected_route_expired.inc() + } + + #[inline] + fn router_triggered_update(&self) { + self.router_triggered_update.inc() + } + + #[inline] + fn router_route_packet_local(&self) { + self.router_route_packet.with_label_values(&["local"]).inc() + } + + #[inline] + fn router_route_packet_forward(&self) { + self.router_route_packet + .with_label_values(&["forward"]) + .inc() + } + + #[inline] + fn router_route_packet_ttl_expired(&self) { + self.router_route_packet + .with_label_values(&["ttl_expired"]) + .inc() + } + + #[inline] + fn router_route_packet_no_route(&self) { + self.router_route_packet + .with_label_values(&["no_route"]) + .inc() + } + + #[inline] + fn router_seqno_request_reply_local(&self) { + self.router_seqno_action + .with_label_values(&["reply_local"]) + .inc() + } + + #[inline] + fn router_seqno_request_bump_seqno(&self) { + self.router_seqno_action + .with_label_values(&["bump_seqno"]) + .inc() + } + + #[inline] + fn router_seqno_request_dropped_ttl(&self) { + self.router_seqno_action + .with_label_values(&["ttl_expired"]) + .inc() + } + + #[inline] + fn router_seqno_request_forward_feasible(&self) { + self.router_seqno_action + .with_label_values(&["forward_feasible"]) + .inc() + } + + #[inline] + fn router_seqno_request_forward_unfeasible(&self) { + self.router_seqno_action + .with_label_values(&["forward_unfeasible"]) + .inc() + } + + #[inline] + fn router_seqno_request_unhandled(&self) { + self.router_seqno_action + .with_label_values(&["unhandled"]) + .inc() + } + + #[inline] + fn router_time_spent_handling_tlv(&self, duration: std::time::Duration, tlv_type: &str) { + self.router_tlv_handling_time_spent + .with_label_values(&[tlv_type]) + .inc_by(duration.as_nanos() as u64) + } + + #[inline] + fn router_update_dead_peer(&self) { + self.router_update_dead_peer.inc() + } + + #[inline] + fn router_received_tlv(&self) { + self.router_received_tlvs.inc() + } + + #[inline] + fn router_tlv_source_died(&self) { + self.router_tlv_source_died.inc() + } + + #[inline] + fn router_tlv_discarded(&self) { + self.router_tlv_discarded.inc() + } + + #[inline] + fn router_time_spent_periodic_propagating_selected_routes( + &self, + duration: std::time::Duration, + ) { + self.router_propage_selected_peers_time_spent + .inc_by(duration.as_nanos() as u64) + } + + #[inline] + fn router_update_skipped_route_selection(&self) { + self.router_update_skipped_route_selection.inc() + } + + #[inline] + fn router_update_denied_by_filter(&self) { + self.router_update_denied_by_filter.inc() + } + + #[inline] + fn router_update_not_interested(&self) { + self.router_update_not_interested.inc() + } + + #[inline] + fn peer_manager_peer_added(&self, pt: mycelium::peer_manager::PeerType) { + let label = match pt { + mycelium::peer_manager::PeerType::Static => "static", + mycelium::peer_manager::PeerType::Inbound => "inbound", + mycelium::peer_manager::PeerType::LinkLocalDiscovery => "link_local", + }; + self.peer_manager_peer_added + .with_label_values(&[label]) + .inc() + } + + #[inline] + fn peer_manager_known_peers(&self, amount: usize) { + self.peer_manager_known_peers.set(amount as i64) + } + + #[inline] + fn peer_manager_connection_attempted(&self) { + self.peer_manager_connection_attemps + .with_label_values(&["started"]) + .inc() + } + + #[inline] + fn peer_manager_connection_finished(&self) { + self.peer_manager_connection_attemps + .with_label_values(&["finished"]) + .inc() + } +} + +impl Default for PrometheusExporter { + fn default() -> Self { + Self::new() + } +} diff --git a/components/mycelium/mycelium-ui/.gitignore b/components/mycelium/mycelium-ui/.gitignore new file mode 100644 index 0000000..884ebca --- /dev/null +++ b/components/mycelium/mycelium-ui/.gitignore @@ -0,0 +1,9 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/dist/ +/static/ +/.dioxus/ + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/components/mycelium/mycelium-ui/Cargo.lock b/components/mycelium/mycelium-ui/Cargo.lock new file mode 100644 index 0000000..3f64c58 --- /dev/null +++ b/components/mycelium/mycelium-ui/Cargo.lock @@ -0,0 +1,7232 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ashpd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "serde", + "serde_repr", + "tokio", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.0", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 0.38.44", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.4.0", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel 2.3.1", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.0", + "futures-lite", + "rustix 0.38.44", + "tracing", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.44", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa 1.0.15", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +dependencies = [ + "serde", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d59b4c170e16f0405a2e95aff44432a0d41aa97675f3d52623effe95792a037" +dependencies = [ + "objc2 0.6.0", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "c2rust-bitfields" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b43c3f07ab0ef604fa6f595aa46ec2f8a22172c975e186f6f5bf9829a3b72c41" +dependencies = [ + "c2rust-bitfields-derive", +] + +[[package]] +name = "c2rust-bitfields-derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3cbc102e2597c9744c8bd8c15915d554300601c91a079430d309816b0912545" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.9.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" +dependencies = [ + "bitflags 2.9.0", + "block", + "cocoa-foundation 0.2.0", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" +dependencies = [ + "bitflags 2.9.0", + "block", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "libc", + "objc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-serialize" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08259976d62c715c4826cb4a3d64a3a9e5c5f68f964ff6087319857f569f93a6" +dependencies = [ + "const-serialize-macro", + "serde", +] + +[[package]] +name = "const-serialize-macro" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04382d0d9df7434af6b1b49ea1a026ef39df1b0738b1cc373368cf175354f6eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "constcat" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7e35aee659887cbfb97aaf227ac12cad1a9d7c71e55ff3376839ed4e282d08" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.100", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.100", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dioxus" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9e3b0725e520250bf23213f996d241cca29cea4360a9bf08a44e0033f8e569" +dependencies = [ + "dioxus-core 0.4.3", + "dioxus-core-macro 0.4.3", + "dioxus-hooks 0.4.3", + "dioxus-hot-reload", + "dioxus-html 0.4.3", + "dioxus-rsx 0.4.3", +] + +[[package]] +name = "dioxus" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a247114500f1a78e87022defa8173de847accfada8e8809dfae23a118a580c" +dependencies = [ + "dioxus-cli-config", + "dioxus-config-macro", + "dioxus-core 0.6.3", + "dioxus-core-macro 0.6.3", + "dioxus-desktop", + "dioxus-devtools", + "dioxus-document", + "dioxus-fullstack", + "dioxus-history", + "dioxus-hooks 0.6.2", + "dioxus-html 0.6.3", + "dioxus-logger", + "dioxus-router", + "dioxus-signals", + "dioxus-web", + "manganis", + "warnings", +] + +[[package]] +name = "dioxus-charts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c862cacf3989713716763def8a4b3b1ab366f691cac5bd64187aec7cb27e61f" +dependencies = [ + "dioxus 0.6.3", + "log", +] + +[[package]] +name = "dioxus-cli-config" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd16948f1ffdb068dd9a64812158073a4250e2af4e98ea31fdac0312e6bce86" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "dioxus-config-macro" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cbf582fbb1c32d34a1042ea675469065574109c95154468710a4d73ee98b49" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "dioxus-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f33186615b2e90bceab24a195b3cfad4e0b4d91a33ec44a94845876bfb25c13" +dependencies = [ + "bumpalo", + "futures-channel", + "futures-util", + "longest-increasing-subsequence", + "rustc-hash 1.1.0", + "serde", + "slab", + "smallbox", + "tracing", +] + +[[package]] +name = "dioxus-core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c03f451a119e47433c16e2d8eb5b15bf7d6e6734eb1a4c47574e6711dadff8d" +dependencies = [ + "const_format", + "dioxus-core-types", + "futures-channel", + "futures-util", + "generational-box", + "longest-increasing-subsequence", + "rustc-hash 1.1.0", + "rustversion", + "serde", + "slab", + "slotmap", + "tracing", + "warnings", +] + +[[package]] +name = "dioxus-core-macro" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21afaccb28587aed0ba98856335912f5ce7052c0aafa74b213829a3b8bfd2345" +dependencies = [ + "constcat", + "dioxus-core 0.4.3", + "dioxus-rsx 0.4.3", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dioxus-core-macro" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "105c954caaaedf8cd10f3d1ba576b01e18aa8d33ad435182125eefe488cf0064" +dependencies = [ + "convert_case 0.6.0", + "dioxus-rsx 0.6.2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dioxus-core-types" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91a82fccfa48574eb7aa183e297769540904694844598433a9eb55896ad9f93b" +dependencies = [ + "once_cell", +] + +[[package]] +name = "dioxus-debug-cell" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea539174bb236e0e7dc9c12b19b88eae3cb574dedbd0252a2d43ea7e6de13e2" + +[[package]] +name = "dioxus-desktop" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b0cca3e7a10a4a3df37ea52c4cc7a53e5c9233489e03ee3f2829471fc3099a" +dependencies = [ + "async-trait", + "base64", + "cocoa 0.25.0", + "core-foundation 0.9.4", + "dioxus-cli-config", + "dioxus-core 0.6.3", + "dioxus-devtools", + "dioxus-document", + "dioxus-history", + "dioxus-hooks 0.6.2", + "dioxus-html 0.6.3", + "dioxus-interpreter-js", + "dioxus-signals", + "dunce", + "futures-channel", + "futures-util", + "generational-box", + "global-hotkey", + "infer", + "jni", + "lazy-js-bundle", + "muda 0.11.5", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "objc_id", + "once_cell", + "rfd", + "rustc-hash 1.1.0", + "serde", + "serde_json", + "signal-hook", + "slab", + "tao", + "thiserror 1.0.69", + "tokio", + "tracing", + "tray-icon", + "urlencoding", + "webbrowser", + "wry", +] + +[[package]] +name = "dioxus-devtools" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a7300f1e8181218187b03502044157eef04e0a25b518117c5ef9ae1096880" +dependencies = [ + "dioxus-core 0.6.3", + "dioxus-devtools-types", + "dioxus-signals", + "serde", + "serde_json", + "tracing", + "tungstenite", + "warnings", +] + +[[package]] +name = "dioxus-devtools-types" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62434973c0c9c5a3bc42e9cd5e7070401c2062a437fb5528f318c3e42ebf4ff" +dependencies = [ + "dioxus-core 0.6.3", + "serde", +] + +[[package]] +name = "dioxus-document" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802a2014d1662b6615eec0a275745822ee4fc66aacd9d0f2fb33d6c8da79b8f2" +dependencies = [ + "dioxus-core 0.6.3", + "dioxus-core-macro 0.6.3", + "dioxus-core-types", + "dioxus-html 0.6.3", + "futures-channel", + "futures-util", + "generational-box", + "lazy-js-bundle", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "dioxus-free-icons" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd226c24168bb63d12d69cc0e7a6d73faa970574445c8e79c29965892f1a2ad8" +dependencies = [ + "dioxus 0.6.3", +] + +[[package]] +name = "dioxus-fullstack" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe99b48a1348eec385b5c4bd3e80fd863b0d3b47257d34e2ddc58754dec5d128" +dependencies = [ + "base64", + "bytes", + "ciborium", + "dioxus-desktop", + "dioxus-devtools", + "dioxus-history", + "dioxus-lib", + "dioxus-web", + "dioxus_server_macro", + "futures-channel", + "futures-util", + "generational-box", + "once_cell", + "serde", + "server_fn", + "tracing", +] + +[[package]] +name = "dioxus-history" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ae4e22616c698f35b60727313134955d885de2d32e83689258e586ebc9b7909" +dependencies = [ + "dioxus-core 0.6.3", + "tracing", +] + +[[package]] +name = "dioxus-hooks" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb23ce82df4fb13e9ddaa01d1469f1f32d683dd4636204bd0a0eaf434b65946" +dependencies = [ + "dioxus-core 0.4.3", + "dioxus-debug-cell", + "futures-channel", + "slab", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "dioxus-hooks" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "948e2b3f20d9d4b2c300aaa60281b1755f3298684448920b27106da5841896d0" +dependencies = [ + "dioxus-core 0.6.3", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "rustversion", + "slab", + "tracing", + "warnings", +] + +[[package]] +name = "dioxus-hot-reload" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d8c9e89e866a6b84b8ad696f0ff2f6f6563d2235eb99acc6952f19e516cc09" +dependencies = [ + "dioxus-core 0.4.3", + "dioxus-html 0.4.3", + "dioxus-rsx 0.4.3", + "interprocess-docfix", + "serde", + "serde_json", +] + +[[package]] +name = "dioxus-html" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "828a42a2d70688a2412a8538c8b5a5eceadf68f682f899dc4455a0169db39dfd" +dependencies = [ + "async-channel 1.9.0", + "async-trait", + "dioxus-core 0.4.3", + "enumset", + "euclid", + "keyboard-types", + "serde", + "serde-value", + "serde_json", + "serde_repr", + "web-sys", +] + +[[package]] +name = "dioxus-html" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c9a40e6fee20ce7990095492dedb6a753eebe05e67d28271a249de74dc796d" +dependencies = [ + "async-trait", + "dioxus-core 0.6.3", + "dioxus-core-macro 0.6.3", + "dioxus-core-types", + "dioxus-hooks 0.6.2", + "dioxus-html-internal-macro", + "enumset", + "euclid", + "futures-channel", + "generational-box", + "keyboard-types", + "lazy-js-bundle", + "rustversion", + "serde", + "serde_json", + "serde_repr", + "tracing", +] + +[[package]] +name = "dioxus-html-internal-macro" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ba87b53688a2c9f619ecdf4b3b955bc1f08bd0570a80a0d626c405f6d14a76" +dependencies = [ + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dioxus-interpreter-js" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330707b10ca75cb0eb05f9e5f8d80217cd0d7e62116a8277ae363c1a09b57a22" +dependencies = [ + "dioxus-core 0.6.3", + "dioxus-core-types", + "dioxus-html 0.6.3", + "js-sys", + "lazy-js-bundle", + "rustc-hash 1.1.0", + "serde", + "sledgehammer_bindgen", + "sledgehammer_utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "dioxus-lib" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5405b71aa9b8b0c3e0d22728f12f34217ca5277792bd315878cc6ecab7301b72" +dependencies = [ + "dioxus-config-macro", + "dioxus-core 0.6.3", + "dioxus-core-macro 0.6.3", + "dioxus-document", + "dioxus-history", + "dioxus-hooks 0.6.2", + "dioxus-html 0.6.3", + "dioxus-rsx 0.6.2", + "dioxus-signals", + "warnings", +] + +[[package]] +name = "dioxus-logger" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545961e752f6c8bf59c274951b3c8b18a106db6ad2f9e2035b29e1f2a3e899b1" +dependencies = [ + "console_error_panic_hook", + "dioxus-cli-config", + "tracing", + "tracing-subscriber", + "tracing-wasm", +] + +[[package]] +name = "dioxus-router" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7266a76fc9e4a91f56499d1d1aecfff7168952b6627a6008b4e9748d6bf863e4" +dependencies = [ + "dioxus-cli-config", + "dioxus-history", + "dioxus-lib", + "dioxus-router-macro", + "rustversion", + "tracing", + "url", + "urlencoding", +] + +[[package]] +name = "dioxus-router-macro" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2743ffb79e9a7d33d779c87d6deea2a6c047d0736012f95d63b909b83f0a6fd2" +dependencies = [ + "proc-macro2", + "quote", + "slab", + "syn 2.0.100", +] + +[[package]] +name = "dioxus-rsx" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c974133c7c95497a486d587e40449927711430b308134b9cd374b8d35eceafb3" +dependencies = [ + "dioxus-core 0.4.3", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dioxus-rsx" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb588e05800b5a7eb90b2f40fca5bbd7626e823fb5e1ba21e011de649b45aa1" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dioxus-signals" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e032dbb3a2c0386ec8b8ee59bc20b5aeb67038147c855801237b45b13d72ac" +dependencies = [ + "dioxus-core 0.6.3", + "futures-channel", + "futures-util", + "generational-box", + "once_cell", + "parking_lot", + "rustc-hash 1.1.0", + "tracing", + "warnings", +] + +[[package]] +name = "dioxus-sortable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a7ef47192859a3a2e8947cd18f85373a26c9e1f10048b5d36286bb51c57c4f" +dependencies = [ + "dioxus 0.4.3", + "wasm-bindgen", +] + +[[package]] +name = "dioxus-web" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7c12475c3d360058b8afe1b68eb6dfc9cbb7dcd760aed37c5f85c561c83ed1" +dependencies = [ + "async-trait", + "ciborium", + "dioxus-cli-config", + "dioxus-core 0.6.3", + "dioxus-core-types", + "dioxus-devtools", + "dioxus-document", + "dioxus-history", + "dioxus-html 0.6.3", + "dioxus-interpreter-js", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "js-sys", + "lazy-js-bundle", + "rustc-hash 1.1.0", + "serde", + "serde-wasm-bindgen", + "serde_json", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "dioxus_server_macro" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "371a5b21989a06b53c5092e977b3f75d0e60a65a4c15a2aa1d07014c3b2dda97" +dependencies = [ + "proc-macro2", + "quote", + "server_fn_macro", + "syn 2.0.100", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "enumset" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a4b049558765cef5f0c1a273c3fc57084d768b44d2f98127aef4cceb17293" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "etherparse" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff83a5facf1a7cbfef93cfb48d6d4fb6a1f42d8ac2341a96b3255acb4d4f860" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +dependencies = [ + "event-listener 5.4.0", + "pin-project-lite", +] + +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generational-box" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a673cf4fb0ea6a91aa86c08695756dfe875277a912cdbf33db9a9f62d47ed82b" +dependencies = [ + "parking_lot", + "tracing", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.9.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "global-hotkey" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b436093d1598b05e3b7fddc097b2bad32763f53a1beb25ab6f9718c6a60acd09" +dependencies = [ + "bitflags 2.9.0", + "cocoa 0.25.0", + "crossbeam-channel", + "keyboard-types", + "objc", + "once_cell", + "thiserror 1.0.69", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.8.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.15", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "human_bytes" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" +dependencies = [ + "ryu", +] + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.15", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "infer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6c16b11a665b26aeeb9b1d7f954cdeb034be38dd00adab4f2ae921a8fee804" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interprocess-docfix" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b84ee245c606aeb0841649a9288e3eae8c61b853a8cd5c0e14450e96d53d28f" +dependencies = [ + "blocking", + "cfg-if", + "futures-core", + "futures-io", + "intmap", + "libc", + "once_cell", + "rustc_version", + "spinning", + "thiserror 1.0.69", + "to_method", + "winapi", +] + +[[package]] +name = "intmap" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae52f28f45ac2bc96edb7714de995cffc174a395fb0abf5bff453587c980d7b9" + +[[package]] +name = "ioctl-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd11f3a29434026f5ff98c730b668ba74b1033637b8817940b54d040696133c" + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpsee" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fba77a59c4c644fd48732367624d1bcf6f409f9c9a286fbc71d2f1fc0b2ea16" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-proc-macros", + "jsonrpsee-server", + "jsonrpsee-types", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693c93cbb7db25f4108ed121304b671a36002c2db67dff2ee4391a688c738547" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "parking_lot", + "pin-project", + "rand 0.9.1", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fa4f5daed39f982a1bb9d15449a28347490ad42b212f8eaa2a2a344a0dce9e9" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38b0bcf407ac68d241f90e2d46041e6a06988f97fe1721fb80b91c42584fae6" +dependencies = [ + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66df7256371c45621b3b7d2fb23aea923d577616b9c0e9c0b950a6ea5c2be0ca" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.9.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors", +] + +[[package]] +name = "lazy-js-bundle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49596223b9d9d4947a14a25c142a6e7d8ab3f27eb3ade269d238bb8b5c267e2" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "left-right" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabfddf3ad712b726484562039aa6fc2014bc1b5c088bb211b208052cf0439e6" +dependencies = [ + "loom", + "slab", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", +] + +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "longest-increasing-subsequence" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "manganis" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317af44b15e7605b85f04525449a3bb631753040156c9b318e6cba8a3ea4ef73" +dependencies = [ + "const-serialize", + "manganis-core", + "manganis-macro", +] + +[[package]] +name = "manganis-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38bee65cc725b2bba23b5dbb290f57c8be8fadbe2043fb7e2ce73022ea06519" +dependencies = [ + "const-serialize", + "dioxus-cli-config", + "dioxus-core-types", + "serde", +] + +[[package]] +name = "manganis-macro" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f4f71310913c40174d9f0cfcbcb127dad0329ecdb3945678a120db22d3d065" +dependencies = [ + "dunce", + "manganis-core", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "muda" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c47e7625990fc1af2226ea4f34fb2412b03c12639fcb91868581eb3a6893453" +dependencies = [ + "cocoa 0.25.0", + "crossbeam-channel", + "gtk", + "keyboard-types", + "libxdo", + "objc", + "once_cell", + "png", + "thiserror 1.0.69", + "windows-sys 0.52.0", +] + +[[package]] +name = "muda" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "once_cell", + "png", + "thiserror 1.0.69", + "windows-sys 0.59.0", +] + +[[package]] +name = "mycelium" +version = "0.6.1" +dependencies = [ + "aes-gcm", + "ahash", + "arc-swap", + "blake3", + "bytes", + "dashmap 6.1.0", + "etherparse", + "faster-hex", + "futures", + "ip_network_table-deps-treebitmap", + "ipnet", + "left-right", + "libc", + "netdev", + "nix 0.29.0", + "nix 0.30.1", + "quinn", + "rand 0.9.1", + "rcgen", + "rtnetlink", + "rustls", + "serde", + "tokio", + "tokio-stream", + "tokio-tun", + "tokio-util", + "tracing", + "tracing-logfmt", + "tracing-subscriber", + "tun", + "wintun 0.5.1", + "x25519-dalek", +] + +[[package]] +name = "mycelium-api" +version = "0.6.1" +dependencies = [ + "async-trait", + "axum", + "base64", + "jsonrpsee", + "mycelium", + "mycelium-metrics", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "mycelium-metrics" +version = "0.6.1" +dependencies = [ + "axum", + "mycelium", + "prometheus", + "tokio", + "tracing", +] + +[[package]] +name = "mycelium-ui" +version = "0.6.1" +dependencies = [ + "dioxus 0.6.3", + "dioxus-charts", + "dioxus-free-icons", + "dioxus-logger", + "dioxus-sortable", + "futures-util", + "human_bytes", + "manganis", + "mycelium", + "mycelium-api", + "reqwest", + "serde_json", + "tokio", + "tracing", + "urlencoding", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle 0.6.2", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "netdev" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2cc40be8e7967f3f90b0cd9de6885772468b576c122b91054517e1ff526b8d" +dependencies = [ + "dlopen2 0.5.0", + "ipnet", + "libc", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "once_cell", + "system-configuration", + "windows-sys 0.59.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-proto" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.12", +] + +[[package]] +name = "netlink-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +dependencies = [ + "bytes", + "futures", + "libc", + "log", + "tokio", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.9.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.0", + "objc2-core-foundation", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.9.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.0", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" +dependencies = [ + "bitflags 2.9.0", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" +dependencies = [ + "bitflags 2.9.0", + "block2 0.6.0", + "objc2 0.6.0", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.44", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.23", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +dependencies = [ + "proc-macro2", + "syn 2.0.100", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit 0.22.24", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "version_check", +] + +[[package]] +name = "procfs" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" +dependencies = [ + "bitflags 2.9.0", + "hex", + "procfs-core", + "rustix 0.38.44", +] + +[[package]] +name = "procfs-core" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +dependencies = [ + "bitflags 2.9.0", + "hex", +] + +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "libc", + "memchr", + "parking_lot", + "procfs", + "thiserror 2.0.12", +] + +[[package]] +name = "quinn" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +dependencies = [ + "bytes", + "getrandom 0.3.2", + "rand 0.9.1", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.12", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + +[[package]] +name = "rfd" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251" +dependencies = [ + "ashpd", + "block", + "dispatch", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "pollster", + "raw-window-handle 0.6.2", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rtnetlink" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb5850b5aa2c9c0ae44f157694bbe85107a2e13d76eb3178d0e3ee96c410f57" +dependencies = [ + "futures", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-packet-utils", + "netlink-proto", + "netlink-sys", + "nix 0.29.0", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.3", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7149975849f1abb3832b246010ef62ccc80d3a76169517ada7188252b9cfb437" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa 1.0.15", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa 1.0.15", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.15", + "ryu", + "serde", +] + +[[package]] +name = "server_fn" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fae7a3038a32e5a34ba32c6c45eb4852f8affaf8b794ebfcd4b1099e2d62ebe" +dependencies = [ + "bytes", + "const_format", + "dashmap 5.5.3", + "futures", + "gloo-net", + "http", + "js-sys", + "once_cell", + "reqwest", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror 1.0.69", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaaf648c6967aef78177c0610478abb5a3455811f401f3c62d10ae9bd3901a1" +dependencies = [ + "const_format", + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 2.0.100", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2aa8119b558a17992e0ac1fd07f080099564f24532858811ce04f742542440" +dependencies = [ + "server_fn_macro", + "syn 2.0.100", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33a1b4f13e2bbf2f5b29d09dfebc9de69229ffee245aed80e3b70f9b5fd28c06" +dependencies = [ + "quote", + "syn 2.0.100", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash 1.1.0", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "serde", + "version_check", +] + +[[package]] +name = "smallbox" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d92e0947c1c04c508c9fd39608a1557226141410fd33b5b314d73fa76508d3" + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "spinning" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938d512196766101d333398efde81bc1f37b00cb42c2f8350e5df639f040bbbe" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.30.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6682a07cf5bab0b8a2bd20d0a542917ab928b5edb75ebd4eda6b05cbaab872da" +dependencies = [ + "bitflags 2.9.0", + "cocoa 0.26.0", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "crossbeam-channel", + "dispatch", + "dlopen2 0.7.0", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "once_cell", + "parking_lot", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.58.0", + "windows-core 0.58.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix 1.0.3", + "windows-sys 0.59.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618" +dependencies = [ + "deranged", + "itoa 1.0.15", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + +[[package]] +name = "tokio" +version = "1.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tun" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be25a316a2d10d0ddd46de9ddd73cdb4262678e1e52f4a32a31421f1e2b816b4" +dependencies = [ + "libc", + "nix 0.29.0", + "thiserror 2.0.12", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.8.0", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.8.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap 2.8.0", + "toml_datetime", + "winnow 0.7.6", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-logfmt" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1f47d22deb79c3f59fcf2a1f00f60cbdc05462bf17d1cd356c1fefa3f444bd" +dependencies = [ + "nu-ansi-term 0.50.1", + "time", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term 0.46.0", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "tray-icon" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd75f5002e2513eaa19b2365f533090cc3e93abd38788452d9ea85cff7b48a" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda 0.15.3", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.0", + "once_cell", + "png", + "thiserror 2.0.12", + "windows-sys 0.59.0", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tun" +version = "0.6.1" +source = "git+https://github.com/LeeSmet/rust-tun#ae4c222e7a7aba5752cb08d2dbaf67bbc669c26a" +dependencies = [ + "byteorder", + "bytes", + "futures-core", + "ioctl-sys", + "libc", + "log", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "wintun 0.3.2", +] + +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "warnings" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f68998838dab65727c9b30465595c6f7c953313559371ca8bf31759b3680ad" +dependencies = [ + "pin-project", + "tracing", + "warnings-macro", +] + +[[package]] +name = "warnings-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59195a1db0e95b920366d949ba5e0d3fc0e70b67c09be15ce5abb790106b0571" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" +dependencies = [ + "core-foundation 0.9.4", + "home", + "jni", + "log", + "ndk-context", + "objc", + "raw-window-handle 0.5.2", + "url", + "web-sys", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.58.0", + "windows-core 0.58.0", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "webview2-com-sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" +dependencies = [ + "thiserror 1.0.69", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core 0.51.1", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.2", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-version" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +dependencies = [ + "memchr", +] + +[[package]] +name = "wintun" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b83b0eca06dd125dbcd48a45327c708a6da8aada3d95a3f06db0ce4b17e0d4" +dependencies = [ + "c2rust-bitfields", + "libloading 0.8.6", + "log", + "thiserror 1.0.69", + "windows 0.51.1", +] + +[[package]] +name = "wintun" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da99be64b5aa3de869c16977994314d0759a698d9a73ab0a5b1d52e2282033ae" +dependencies = [ + "c2rust-bitfields", + "libloading 0.8.6", + "log", + "thiserror 1.0.69", + "windows-sys 0.52.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wry" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac0099a336829fbf54c26b5f620c68980ebbe37196772aeaf6118df4931b5cb0" +dependencies = [ + "base64", + "block", + "cocoa 0.26.0", + "core-graphics 0.24.0", + "crossbeam-channel", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc", + "objc_id", + "once_cell", + "percent-encoding", + "raw-window-handle 0.6.2", + "sha2", + "soup3", + "tao-macros", + "thiserror 1.0.69", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.58.0", + "windows-core 0.58.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zbus" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "derivative", + "enumflags2", + "event-listener 5.4.0", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.27.1", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a3e850ff1e7217a3b7a07eba90d37fe9bb9e89a310f718afcde5885ca9b6d7" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zvariant" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e09e8be97d44eeab994d752f341e67b3b0d80512a8b315a0671d47232ef1b65" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a5857e2856435331636a9fbb415b09243df4521a267c5bedcd5289b4d5799e" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/components/mycelium/mycelium-ui/Cargo.toml b/components/mycelium/mycelium-ui/Cargo.toml new file mode 100644 index 0000000..3cb854b --- /dev/null +++ b/components/mycelium/mycelium-ui/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "mycelium-ui" +version = "0.6.1" +edition = "2021" +license-file = "../LICENSE" +readme = "../README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +dioxus = { version = "0.6.2", features = ["desktop", "router"] } +mycelium = { path = "../mycelium" } +mycelium-api = { path = "../mycelium-api" } + +# Debug +tracing = "0.1.40" +dioxus-logger = "0.6.2" +reqwest = { version = "0.12.5", features = ["json"] } +serde_json = "1.0.120" +dioxus-sortable = "0.1.2" +manganis = "0.6.2" +dioxus-free-icons = { version = "0.9.0", features = [ + "font-awesome-solid", + "font-awesome-brands", + "font-awesome-regular", +] } +human_bytes = { version = "0.4.3", features = ["fast"] } +tokio = "1.44.1" +dioxus-charts = "0.3.1" +futures-util = "0.3.31" +urlencoding = "2.1.3" + +[features] +bundle = [] diff --git a/components/mycelium/mycelium-ui/Dioxus.toml b/components/mycelium/mycelium-ui/Dioxus.toml new file mode 100644 index 0000000..4695426 --- /dev/null +++ b/components/mycelium/mycelium-ui/Dioxus.toml @@ -0,0 +1,48 @@ +[application] + +# App (Project) Name +name = "mycelium-ui" + +# Dioxus App Default Platform +# desktop, web +default_platform = "desktop" + +# `build` & `serve` dist path +out_dir = "dist" + +# assets file folder +asset_dir = "assets" + +[web.app] + +# HTML title tag content +title = "mycelium-ui" + +[web.watcher] + +# when watcher trigger, regenerate the `index.html` +reload_html = true + +# which files or dirs will be watcher monitoring +watch_path = ["src", "assets"] + +# add fallback 404 page +index_on_404 = true + +# include `assets` in web platform +[web.resource] + +# CSS style file + +style = [ + "https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap", +] + +# Javascript code file +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] diff --git a/components/mycelium/mycelium-ui/README.md b/components/mycelium/mycelium-ui/README.md new file mode 100644 index 0000000..6c711ba --- /dev/null +++ b/components/mycelium/mycelium-ui/README.md @@ -0,0 +1,54 @@ +# Mycelium Network Dashboard + +The Mycelium Network Dashboard is a GUI application built with Dioxus, a modern library for building +cross-platform applications using Rust. More information about Dioxus can be found [here](https://dioxuslabs.com/) + +## Getting Started + +To get started with the Mycelium Network Dashboard, you'll need to have the Dioxus CLI tool installed. +You can install it using the following command: + +`cargo install dioxus-cli` + +Before running the Mycelium Network Dashboard application, make sure that the `myceliumd` daemon is running on your system. +The myceliumd daemon is the background process that manages the Mycelium network connection +and provides the data that the dashboard application displays. For more information on setting up and +running `myceliumd`, please read [this](../README.md). + +Once you have the Dioxus CLI installed, you can build and run the application in development mode using +the following command (in the `mycelium-ui` directory): + +`dx serve` + +This will start a development server and launch the application in a WebView. + +## Bundling the application + +To bundle the application, you can use: + +`dx bundle --release --features bundle` + +This will create a bundled version of the application in the `dist/bundle/` directory. The bundled +application can be distributed and run on various platforms, including Windows, MacOS and Linux. Dioxus +also offers support for mobile, but note that this has not been tested. + +## Documentation + +The Mycelium Network Dashboard application provides the following features: + +- **Home**: Displays information about the node and allows to change address of the API server on which +the application should listen. +- **Peers**: Shows and overview of all the connected peers. Adding and removing peers can be done here. +- **Routes**: Provides information about the routing table and network routes + + +## Contributing + +If you would like to contribute to the Mycelium Network Dashboard project, please follow the standard GitHub workflow: + +1. Fork the repository +2. Create a new branch for your changes +3. Make your changes and commit them +4. Push your changes to your forked repository +5. Submit a pull request to the main repository + diff --git a/components/mycelium/mycelium-ui/assets/styles.css b/components/mycelium/mycelium-ui/assets/styles.css new file mode 100644 index 0000000..b40334f --- /dev/null +++ b/components/mycelium/mycelium-ui/assets/styles.css @@ -0,0 +1,514 @@ +@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap'); + +:root { + --primary-color: #3498db; + --secondary-color: #2c3e50; + --background-color: #ecf0f1; + --text-color: #34495e; + --border-color: #bdc3c7; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Lato', sans-serif; + background-color: var(--background-color); + color: var(--text-color); +} + +.app-container { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +header { + background-color: var(--primary-color); + color: white; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + position: fixed; + width: 100%; + z-index: 1000; + height: 60px; +} + +header h1 { + font-size: 1rem; + font-weight: 700; +} + +.node-info { + font-size: 0.9rem; + display: flex; + align-items: center; +} + +.node-info span { + margin-right: 1rem; +} + +.node-info .separator { + margin: 0 1rem; +} + +.content-container { + display: flex; + padding-top: 60px; + min-height: calc(100vh - 60px); +} + +.sidebar { + background-color: var(--secondary-color); + color: white; + width: 250px; + height: 100%; + position: fixed; + top: 60px; + left: 0; + transition: transform 0.3s ease-in-out; + z-index: 100; +} + +.sidebar.collapsed { + transform: translateX(-250px); +} + +.sidebar ul { + list-style-type: none; + padding: 1rem; +} + +.sidebar li { + margin-bottom: 1rem; +} + +.sidebar a { + color: white; + text-decoration: none; + font-size: 1.1rem; + transition: color 0.3s ease; +} + +.sidebar a:hover { + color: var(--primary-color); +} + +.main-content { + flex: 1; + padding: 2rem; + margin-left: 250px; + transition: margin-left 0.3s ease-in-out; +} + +.main-content.expanded { + margin-left: 0; +} + +.toggle-sidebar.collapsed { + left: 10px; + transform: translate(-10px) rotate(180deg); + transition: left 0.3s ease-in-out, transform: 0s; +} + +.toggle-sidebar { + background-color: var(--secondary-color); + color: white; + border: none; + width: 40px; + height: 40px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + position: fixed; + top: 70px; + left: 250px; + transition: left 0.3s ease-in-out, transform 0.3s ease-in-out; + z-index: 200; +} + +.table-container { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; + background-color: white; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid var(--border-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +th { + background-color: var(--primary-color); + color: white; + font-weight: 700; + cursor: pointer; + transition: background-color 0.3s ease; +} + +th:hover { + background-color: #2980b9; +} + +.subnet-column { width: 25%; } +.next-hop-column { width: 35%; } +.metric-column { width: 20%; } +.seqno-column { width: 20%; } + +.endpoint-column { width: 30%; } +.type-column { width: 15%; } +.connection-state-column { width: 20%; } +.tx-bytes-column { width: 17.5%; } +.rx-bytes-column { width: 17.5%; } + +.pagination { + display: flex; + justify-content: center; + align-items: center; + margin-top: 1rem; +} + +.pagination button { + background-color: var(--primary-color); + color: white; + border: none; + padding: 0.5rem 1rem; + margin: 0 0.5rem; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.pagination button:hover:not(:disabled) { + background-color: #2980b9; +} + +.pagination button:disabled { + background-color: var(--border-color); + cursor: not-allowed; +} + +.pagination span { + margin: 0 0.5rem; +} + +.peers-table, .selected-routes, .fallback-routes { + margin-top: 2rem; +} + +h2 { + color: var(--secondary-color); + margin-bottom: 1rem; +} + +.node-info h3 { + margin-bottom: 0.5rem; + font-size: 1.1rem; + font-weight: 400; +} + +/* Home component */ +.home-container { + max-width: 800px; + margin: 0 auto; + padding: 2rem; +} + +.home-container h2 { + color: var(--secondary-color); + margin-bottom: 1.5rem; +} + +.home-container p { + margin-bottom: 1rem; +} + +.bold { + font-weight: 700; +} + +/* API server */ +.server-input { + display: flex; + margin-bottom: 1rem; +} + +.server-input input { + flex-grow: 1; + padding: 0.5rem; + font-size: 1rem; + border: 1px solid var(--border-color); + border-radius: 4px 0 0 4px; +} + +.server-input button { + padding: 0.5rem 1rem; + font-size: 1rem; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 0 4px 4px 0; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.server-input button:hover { + background-color: #2980b9; +} + +.error { + color: #e74c3c; + margin-bottom: 1rem; +} + +.warning { + color: #f39c12; + margin-bottom: 1rem; +} + +/* Searching and adding */ +.search-and-add-container { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; + flex-wrap: wrap; +} + +/* Searching */ +.search-container { + flex: 0 0 60%; + display: flex; + margin-right: 1rem; + margin-bottom: 0.5rem; +} + +.search-container input { + flex-grow: 1; + padding: 0.5rem; + font-size: 1rem; + border: 1px solid var(--border-color); + border-radius: 4px 0 0 4px; + min-width: 200px; + height: 40px; +} + +.search-container select { + padding: 0.5rem; + font-size: 1rem; + border: 1px solid var(--border-color); + border-left: none; + border-radius: 0 4px 4px 0; + background-color: white; + min-width: 120px; + height: 40px; +} + +/* Add peer button */ +.add-peer-container { + flex: 0 0 35%; + display: flex; + flex-direction: column; + align-items: flex-start; + margin-bottom: 0.5rem; +} + +.add-peer-input-button { + display: flex; + width: 100%; +} + + +.add-peer-container button { + padding: 0.5rem 1rem; + font-size: 1rem; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s ease; + white-space: nowrap; + height: 40px; +} + +.add-peer-container button:hover { + background-color: #2980b9 +} + +.add-peer-error { + color: #e74c3c !important; + font-size: 0.9rem; + margin-top: 0.5rem; + width: 100%; +} + +.expanded-add-peer-container { + flex-grow: 1; + display: flex; + margin-right: 0.5rem; +} + +.expanded-add-peer-container input { + flex-grow: 1; + padding: 0.5rem; + font-size: 1rem; + border: 1px solid var(--border-color); + border-radius: 4px; + min-width: 150px; + height: 40px; +} + +/* Refresh button */ +.refresh-button { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--primary-color); + color: white; + border: none; + padding: 0.5rem 1rem; + margin-bottom: 1rem; + cursor: pointer; + transition: background-color 0.3s ease; + border-radius: 4px; + font-size: 1rem; +} + +.refresh-button:hover { + background-color: #2980b9; +} + +.refresh-button svg { + margin-right: 0.5rem; +} + +/* Expandable row styles */ +.expanded-row { + background-color: #f8f9fa; +} + +.graph-container { + width: calc(50% - 1rem); + margin-bottom: 2rem; +} + +.graph-title { + font-size: 1.2rem; + font-weight: 700; + margin-bottom: 1rem; + color: var(--secondary-color); + text-align: center; +} + +.expanded-content { + padding: 2rem; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: flex-start; +} + +.expanded-content p { + margin: 0; + font-size: 0.5rem; +} + +/* Style for both Tx and Rx charts */ +.expanded-content svg { + width: 100%; + height: auto; + margin-bottom: 1rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border-radius: 8px; + overflow: hidden; +} + +/* Style for chart lines */ +.expanded-content path { + stroke-width: 2; +} + +/* Style for Tx bytes chart */ +.graph-container:nth-child(1) path { + stroke: #3498db; +} + +/* Style for Rx bytes chart */ +.graph-container:nth-child(2) path { + stroke: #2ecc71; +} + +.button-container { + width: 100%; + display: flex; + justify-content: space-between; + margin-top: 1rem; +} + +.close-button { + background-color: var(--primary-color); + color: white; + border: none; + padding: 0.75rem 1.5rem; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.3s ease; + border-radius: 4px; + margin-top: 1rem; +} + +.remove-button { + background-color: var(--primary-color); + color: white; + border: none; + padding: 0.75rem 1.5rem; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.3s ease; + border-radius: 4px; + margin-top: 1rem; +} + +.remove-button:hover { + background-color: #c0392b; +} + +/* Make the table rows clickable */ +tbody tr { + cursor: pointer; +} + +tbody tr:hover { + background-color: #f1f3f5; +} + +/* Responsive design for smaller screens */ +@media (max-width: 768px) { + .expanded-content { + flex-direction: column; + align-items: center; + } + + .graph-container { + width: 100%; + max-width: 100%; + } +} + diff --git a/components/mycelium/mycelium-ui/src/api.rs b/components/mycelium/mycelium-ui/src/api.rs new file mode 100644 index 0000000..c225621 --- /dev/null +++ b/components/mycelium/mycelium-ui/src/api.rs @@ -0,0 +1,98 @@ +use mycelium::endpoint::Endpoint; +use mycelium_api::AddPeer; +use std::net::SocketAddr; +use urlencoding::encode; + +pub async fn get_peers( + server_addr: SocketAddr, +) -> Result, reqwest::Error> { + let request_url = format!("http://{server_addr}/api/v1/admin/peers"); + match reqwest::get(&request_url).await { + Err(e) => Err(e), + Ok(resp) => match resp.json::>().await { + Err(e) => Err(e), + Ok(peers) => Ok(peers), + }, + } +} + +pub async fn get_selected_routes( + server_addr: SocketAddr, +) -> Result, reqwest::Error> { + let request_url = format!("http://{server_addr}/api/v1/admin/routes/selected"); + match reqwest::get(&request_url).await { + Err(e) => Err(e), + Ok(resp) => match resp.json::>().await { + Err(e) => Err(e), + Ok(selected_routes) => Ok(selected_routes), + }, + } +} + +pub async fn get_fallback_routes( + server_addr: SocketAddr, +) -> Result, reqwest::Error> { + let request_url = format!("http://{server_addr}/api/v1/admin/routes/fallback"); + match reqwest::get(&request_url).await { + Err(e) => Err(e), + Ok(resp) => match resp.json::>().await { + Err(e) => Err(e), + Ok(selected_routes) => Ok(selected_routes), + }, + } +} + +pub async fn get_node_info(server_addr: SocketAddr) -> Result { + let request_url = format!("http://{server_addr}/api/v1/admin"); + match reqwest::get(&request_url).await { + Err(e) => Err(e), + Ok(resp) => match resp.json::().await { + Err(e) => Err(e), + Ok(node_info) => Ok(node_info), + }, + } +} + +pub async fn remove_peer( + server_addr: SocketAddr, + peer_endpoint: Endpoint, +) -> Result<(), reqwest::Error> { + let full_endpoint = format!( + "{}://{}", + peer_endpoint.proto().to_string().to_lowercase(), + peer_endpoint.address() + ); + let encoded_full_endpoint = encode(&full_endpoint); + let request_url = format!( + "http://{}/api/v1/admin/peers/{}", + server_addr, encoded_full_endpoint + ); + + let client = reqwest::Client::new(); + client + .delete(request_url) + .send() + .await? + .error_for_status()?; + + Ok(()) +} + +pub async fn add_peer( + server_addr: SocketAddr, + peer_endpoint: String, +) -> Result<(), reqwest::Error> { + println!("adding peer: {peer_endpoint}"); + let client = reqwest::Client::new(); + let request_url = format!("http://{server_addr}/api/v1/admin/peers"); + client + .post(request_url) + .json(&AddPeer { + endpoint: peer_endpoint, + }) + .send() + .await? + .error_for_status()?; + + Ok(()) +} diff --git a/components/mycelium/mycelium-ui/src/components.rs b/components/mycelium/mycelium-ui/src/components.rs new file mode 100644 index 0000000..c536aa1 --- /dev/null +++ b/components/mycelium/mycelium-ui/src/components.rs @@ -0,0 +1,4 @@ +pub mod home; +pub mod layout; +pub mod peers; +pub mod routes; diff --git a/components/mycelium/mycelium-ui/src/components/home.rs b/components/mycelium/mycelium-ui/src/components/home.rs new file mode 100644 index 0000000..08b2a32 --- /dev/null +++ b/components/mycelium/mycelium-ui/src/components/home.rs @@ -0,0 +1,68 @@ +use crate::api; +use crate::{ServerAddress, ServerConnected}; +use dioxus::prelude::*; +use std::net::SocketAddr; +use std::str::FromStr; + +#[component] +pub fn Home() -> Element { + let mut server_addr = use_context::>(); + let mut new_address = use_signal(|| server_addr.read().0.to_string()); + let mut node_info = use_resource(fetch_node_info); + + let try_connect = move |_| { + if let Ok(addr) = SocketAddr::from_str(&new_address.read()) { + server_addr.write().0 = addr; + node_info.restart(); + } + }; + + rsx! { + div { class: "home-container", + h2 { "Node information" } + div { class: "server-input", + input { + placeholder: "Server address (e.g. 127.0.0.1:8989)", + value: "{new_address}", + oninput: move |evt| new_address.set(evt.value().clone()), + } + button { onclick: try_connect, "Connect" } + } + {match node_info.read().as_ref() { + Some(Ok(info)) => rsx! { + p { + "Node subnet: ", + span { class: "bold", "{info.node_subnet}" } + } + p { + "Node public key: ", + span { class: "bold", "{info.node_pubkey}" } + } + }, + Some(Err(e)) => rsx! { + p { class: "error", "Error: {e}" } + }, + None => rsx! { + p { "Enter a server address and click 'Connect' to fetch node information." } + } + }} + } + } +} + +async fn fetch_node_info() -> Result { + let server_addr = use_context::>(); + let mut server_connected = use_context::>(); + let address = server_addr.read().0; + + match api::get_node_info(address).await { + Ok(info) => { + server_connected.write().0 = true; + Ok(info) + } + Err(e) => { + server_connected.write().0 = false; + Err(e) + } + } +} diff --git a/components/mycelium/mycelium-ui/src/components/layout.rs b/components/mycelium/mycelium-ui/src/components/layout.rs new file mode 100644 index 0000000..5c4127f --- /dev/null +++ b/components/mycelium/mycelium-ui/src/components/layout.rs @@ -0,0 +1,65 @@ +use crate::{api, Route, ServerAddress}; +use dioxus::prelude::*; +use dioxus_free_icons::{icons::fa_solid_icons::FaChevronLeft, Icon}; + +#[component] +pub fn Layout() -> Element { + let sidebar_collapsed = use_signal(|| false); + + rsx! { + div { class: "app-container", + Header {} + div { class: "content-container", + Sidebar { collapsed: sidebar_collapsed } + main { class: if *sidebar_collapsed.read() { "main-content expanded" } else { "main-content" }, + Outlet:: {} + } + } + } + } +} + +#[component] +pub fn Header() -> Element { + let server_addr = use_context::>(); + let fetched_node_info = use_resource(move || api::get_node_info(server_addr.read().0)); + + rsx! { + header { + h1 { "Mycelium Network Dashboard" } + div { class: "node-info", + { match &*fetched_node_info.read_unchecked() { + Some(Ok(info)) => rsx! { + span { "Subnet: {info.node_subnet}" } + span { class: "separator", "|" } + span { "Public Key: {info.node_pubkey}" } + }, + Some(Err(_)) => rsx! { span { "Error loading node info" } }, + None => rsx! { span { "Loading node info..." } }, + }} + } + } + } +} + +#[component] +pub fn Sidebar(collapsed: Signal) -> Element { + rsx! { + nav { class: if *collapsed.read() { "sidebar collapsed" } else { "sidebar" }, + ul { + li { Link { to: Route::Home {}, "Home" } } + li { Link { to: Route::Peers {}, "Peers" } } + li { Link { to: Route::Routes {}, "Routes" } } + } + } + button { class: if *collapsed.read() { "toggle-sidebar collapsed" } else { "toggle-sidebar" }, + onclick: { + let c = *collapsed.read(); + move |_| collapsed.set(!c) + }, + Icon { + icon: FaChevronLeft, + } + } + } +} diff --git a/components/mycelium/mycelium-ui/src/components/peers.rs b/components/mycelium/mycelium-ui/src/components/peers.rs new file mode 100644 index 0000000..7068ee3 --- /dev/null +++ b/components/mycelium/mycelium-ui/src/components/peers.rs @@ -0,0 +1,521 @@ +use crate::get_sort_indicator; +use crate::{api, SearchState, ServerAddress, SortDirection, StopFetchingPeerSignal}; +use dioxus::prelude::*; +use dioxus_charts::LineChart; +use human_bytes::human_bytes; +use mycelium::{ + endpoint::Endpoint, + peer_manager::{PeerStats, PeerType}, +}; +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet, VecDeque}, + str::FromStr, +}; +use tracing::{error, info}; + +const REFRESH_RATE_MS: u64 = 500; +const MAX_DATA_POINTS: usize = 8; // displays last 4 seconds + +#[component] +pub fn Peers() -> Element { + let server_addr = use_context::>().read().0; + let error = use_signal(|| None::); + let peer_data = use_signal(HashMap::::new); + + let _ = use_resource(move || async move { + to_owned![server_addr, error, peer_data]; + loop { + let stop_fetching_signal = use_context::>().read().0; + if !stop_fetching_signal { + match api::get_peers(server_addr).await { + Ok(fetched_peers) => { + peer_data.with_mut(|data| { + // Collect the endpoint from the fetched peers + let fetched_endpoints: HashSet = + fetched_peers.iter().map(|peer| peer.endpoint).collect(); + + // Remove peers that are no longer in the fetched data + data.retain(|endpoint, _| fetched_endpoints.contains(endpoint)); + + // Insert or update the fetched peers + for peer in fetched_peers { + data.insert(peer.endpoint, peer); + } + }); + error.set(None); + } + Err(e) => { + eprintln!("Error fetching peers: {}", e); + error.set(Some(format!( + "An error has occurred while fetching peers: {}", + e + ))) + } + } + } else { + break; + } + tokio::time::sleep(tokio::time::Duration::from_millis(REFRESH_RATE_MS)).await; + } + }); + + rsx! { + if let Some(err) = error.read().as_ref() { + div { class: "error-message", "{err}" } + } else { + PeersTable { peer_data: peer_data } + } + } +} + +#[component] +fn PeersTable(peer_data: Signal>) -> Element { + let mut current_page = use_signal(|| 0); + let items_per_page = 20; + let mut sort_column = use_signal(|| "Protocol".to_string()); + let mut sort_direction = use_signal(|| SortDirection::Ascending); + let peers_len = peer_data.read().len(); + + // Pagination + let mut change_page = move |delta: i32| { + let cur_page = *current_page.read() as i32; + current_page.set( + (cur_page + delta) + .max(0) + .min((peers_len - 1) as i32 / items_per_page), + ); + }; + + // Sorting + let mut sort_peers_signal = move |column: String| { + if column == *sort_column.read() { + let new_sort_direction = match *sort_direction.read() { + SortDirection::Ascending => SortDirection::Descending, + SortDirection::Descending => SortDirection::Ascending, + }; + sort_direction.set(new_sort_direction); + } else { + sort_column.set(column); + sort_direction.set(SortDirection::Descending); + } + // When sorting, we should jump back to the first page + current_page.set(0); + }; + + let sorted_peers = use_memo(move || { + let mut peers = peer_data.read().values().cloned().collect::>(); + sort_peers(&mut peers, &sort_column.read(), &sort_direction.read()); + peers + }); + + // Searching + let mut search_state = use_signal(|| SearchState { + query: String::new(), + column: "Protocol".to_string(), + }); + + let filtered_peers = use_memo(move || { + let query = search_state.read().query.to_lowercase(); + let column = &search_state.read().column; + let sorted_peers = sorted_peers.read(); + sorted_peers + .iter() + .filter(|peer| match column.as_str() { + "Protocol" => peer + .endpoint + .proto() + .to_string() + .to_lowercase() + .contains(&query), + "Address" => peer + .endpoint + .address() + .ip() + .to_string() + .to_lowercase() + .contains(&query), + "Port" => peer + .endpoint + .address() + .port() + .to_string() + .to_lowercase() + .contains(&query), + "Type" => peer.pt.to_string().to_lowercase().contains(&query), + "Connection State" => peer + .connection_state + .to_string() + .to_lowercase() + .contains(&query), + "Tx bytes" => peer.tx_bytes.to_string().to_lowercase().contains(&query), + "Rx bytes" => peer.rx_bytes.to_string().to_lowercase().contains(&query), + _ => false, + }) + .cloned() + .collect::>() + }); + + let peers_len = filtered_peers.read().len(); + let start = current_page * items_per_page; + let end = (start + items_per_page).min(peers_len as i32); + let current_peers = filtered_peers.read()[start as usize..end as usize].to_vec(); + + // Expanding peer to show rx/tx bytes graphs + let mut expanded_rows = use_signal(|| ExpandedRows(HashSet::new())); + let mut toggle_row_expansion = move |peer_endpoint: String| { + expanded_rows.with_mut(|rows| { + if rows.0.contains(&peer_endpoint) { + rows.0.remove(&peer_endpoint); + } else { + rows.0.insert(peer_endpoint); + } + }); + }; + + let toggle_add_peer_input = use_signal(|| true); //TODO: fix UX for adding peer + let mut add_peer_error = use_signal(|| None::); + let add_peer = move |peer_endpoint: String| { + spawn(async move { + let server_addr = use_context::>().read().0; + // Check correct endpoint format and add peer + match Endpoint::from_str(&peer_endpoint) { + Ok(_) => { + if let Err(e) = api::add_peer(server_addr, peer_endpoint.clone()).await { + error!("Error adding peer: {e}"); + add_peer_error.set(Some(format!("Error adding peer: {}", e))); + } else { + info!("Succesfully added peer: {peer_endpoint}"); + } + } + Err(e) => { + error!("Incorrect peer endpoint: {e}"); + add_peer_error.set(Some(format!("Incorrect peer endpoint: {}", e))); + } + } + }); + }; + let mut new_peer_endpoint = use_signal(|| "".to_string()); + + rsx! { + div { class: "peers-table", + h2 { "Peers" } + div { class: "search-and-add-container", + div { class: "search-container", + input { + placeholder: "Search...", + value: "{search_state.read().query}", + oninput: move |evt| search_state.write().query.clone_from(&evt.value()), + } + select { + value: "{search_state.read().column}", + onchange: move |evt| search_state.write().column.clone_from(&evt.value()), + option { value: "Protocol", "Protocol" } + option { value: "Address", "Address" } + option { value: "Port", "Port" } + option { value: "Type", "Type" } + option { value: "Connection State", "Connection State" } + option { value: "Tx bytes", "Tx bytes" } + option { value: "Rx bytes", "Rx bytes" } + } + } + div { class: "add-peer-container", + div { class: "add-peer-input-button", + if *toggle_add_peer_input.read() { + div { class: "expanded-add-peer-container", + input { + placeholder: "tcp://ipaddr:port", + oninput: move |evt| new_peer_endpoint.set(evt.value()) + } + } + } + button { + onclick: move |_| add_peer(new_peer_endpoint.read().to_string()), + "Add peer" + } + } + if let Some(error) = add_peer_error.read().as_ref() { + div { class: "add-peer-error", "{error}" } + } + } + } + div { class: "table-container", + table { + thead { + tr { + th { class: "protocol-column", + onclick: move |_| sort_peers_signal("Protocol".to_string()), + "Protocol {get_sort_indicator(sort_column, sort_direction, \"Protocol\".to_string())}" + } + th { class: "address-column", + onclick: move |_| sort_peers_signal("Address".to_string()), + "Address {get_sort_indicator(sort_column, sort_direction, \"Address\".to_string())}" + } + th { class: "port-column", + onclick: move |_| sort_peers_signal("Port".to_string()), + "Port {get_sort_indicator(sort_column, sort_direction, \"Port\".to_string())}" + } + th { class: "type-column", + onclick: move |_| sort_peers_signal("Type".to_string()), + "Type {get_sort_indicator(sort_column, sort_direction, \"Type\".to_string())}" + } + th { class: "connection-state-column", + onclick: move |_| sort_peers_signal("Connection State".to_string()), + "Connection State {get_sort_indicator(sort_column, sort_direction, \"Connection State\".to_string())}" + } + th { class: "tx-bytes-column", + onclick: move |_| sort_peers_signal("Tx bytes".to_string()), + "Tx bytes {get_sort_indicator(sort_column, sort_direction, \"Tx bytes\".to_string())}" + } + th { class: "rx-bytes-column", + onclick: move |_| sort_peers_signal("Rx bytes".to_string()), + "Rx bytes {get_sort_indicator(sort_column, sort_direction, \"Rx bytes\".to_string())}" + } + } + } + tbody { + for peer in current_peers.into_iter() { + tr { + onclick: move |_| toggle_row_expansion(peer.endpoint.to_string()), + td { class: "protocol-column", "{peer.endpoint.proto()}" } + td { class: "address-column", "{peer.endpoint.address().ip()}" } + td { class: "port-column", "{peer.endpoint.address().port()}" } + td { class: "type-column", "{peer.pt}" } + td { class: "connection-state-column", "{peer.connection_state}" } + td { class: "tx-bytes-column", "{human_bytes(peer.tx_bytes as f64)}" } + td { class: "rx-bytes-column", "{human_bytes(peer.rx_bytes as f64)}" } + } + { + let peer_expanded = expanded_rows.read().0.contains(&peer.endpoint.to_string()); + if peer_expanded { + rsx! { + ExpandedPeerRow { + peer_endpoint: peer.endpoint, + peer_data: peer_data, + on_close: move |_| toggle_row_expansion(peer.endpoint.to_string()), + } + } + } else { + rsx! {} + } + } + } + } + } + } + div { class: "pagination", + button { + disabled: *current_page.read() == 0, + onclick: move |_| change_page(-1), + "Previous" + } + span { "Page {current_page + 1}" } + button { + disabled: (current_page + 1) * items_per_page >= peers_len as i32, + onclick: move |_| change_page(1), + "Next" + } + } + } + } +} + +#[derive(Clone)] +struct BandwidthData { + tx_bytes: u64, + rx_bytes: u64, + timestamp: tokio::time::Duration, +} + +#[component] +fn ExpandedPeerRow( + peer_endpoint: Endpoint, + peer_data: Signal>, + on_close: EventHandler<()>, +) -> Element { + let bandwidth_data = use_signal(VecDeque::::new); + let start_time = use_signal(tokio::time::Instant::now); + + use_future(move || { + to_owned![bandwidth_data, start_time, peer_data, peer_endpoint]; + async move { + let mut last_tx = 0; + let mut last_rx = 0; + if let Some(peer_stats) = peer_data.read().get(&peer_endpoint) { + last_tx = peer_stats.tx_bytes; + last_rx = peer_stats.rx_bytes; + } + + loop { + let current_time = tokio::time::Instant::now(); + let elapsed_time = current_time.duration_since(*start_time.read()); + + if let Some(peer_stats) = peer_data.read().get(&peer_endpoint) { + let tx_rate = + (peer_stats.tx_bytes - last_tx) as f64 / (REFRESH_RATE_MS as f64 / 1000.0); + let rx_rate = + (peer_stats.rx_bytes - last_rx) as f64 / (REFRESH_RATE_MS as f64 / 1000.0); + + bandwidth_data.with_mut(|data| { + let new_data = BandwidthData { + tx_bytes: tx_rate as u64, + rx_bytes: rx_rate as u64, + timestamp: elapsed_time, + }; + data.push_back(new_data); + + if data.len() > MAX_DATA_POINTS { + data.pop_front(); + } + }); + + last_tx = peer_stats.tx_bytes; + last_rx = peer_stats.rx_bytes; + } + tokio::time::sleep(tokio::time::Duration::from_millis(REFRESH_RATE_MS)).await; + } + } + }); + + let tx_data = use_memo(move || { + bandwidth_data + .read() + .iter() + .map(|d| d.tx_bytes as f32) + .collect::>() + }); + + let rx_data = use_memo(move || { + bandwidth_data + .read() + .iter() + .map(|d| d.rx_bytes as f32) + .collect::>() + }); + let labels = bandwidth_data + .read() + .iter() + .map(|d| format!("{:.1}", d.timestamp.as_secs_f64())) + .collect::>(); + + let remove_peer = move |_| { + spawn(async move { + println!("Removing peer: {}", peer_endpoint); + let server_addr = use_context::>().read().0; + match api::remove_peer(server_addr, peer_endpoint).await { + Ok(_) => on_close.call(()), + Err(e) => eprintln!("Error removing peer: {e}"), + } + }); + }; + + rsx! { + tr { class: "expanded-row", + td { colspan: "7", + div { class: "expanded-content", + div { class: "graph-container", + // Tx chart + div { class: "graph-title", "Tx Bytes/s" } + LineChart { + show_grid: false, + show_dots: false, + padding_top: 80, + padding_left: 100, + padding_right: 80, + padding_bottom: 80, + label_interpolation: (|v| human_bytes(v as f64).to_string()) as fn(f32)-> String, + series: vec![tx_data.read().to_vec()], + labels: labels.clone(), + series_labels: vec!["Tx Bytes/s".into()], + } + } + div { class: "graph-container", + // Rx chart + div { class: "graph-title", "Rx Bytes/s" } + LineChart { + show_grid: false, + show_dots: false, + padding_top: 80, + padding_left: 100, + padding_right: 80, + padding_bottom: 80, + label_interpolation: (|v| human_bytes(v as f64).to_string()) as fn(f32)-> String, + series: vec![rx_data.read().clone()], + labels: labels.clone(), + series_labels: vec!["Rx Bytes/s".into()], + } + } + div { class: "button-container", + button { class: "close-button", + onclick: move |_| on_close.call(()), + "Close" + } + button { class: "remove-button", + onclick: remove_peer, + "Remove peer" + } + } + } + } + } + } +} + +#[derive(Clone, PartialEq)] +struct ExpandedRows(HashSet); + +fn sort_peers( + peers: &mut [mycelium::peer_manager::PeerStats], + column: &str, + direction: &SortDirection, +) { + peers.sort_by(|a, b| { + let cmp = match column { + "Protocol" => a.endpoint.proto().cmp(&b.endpoint.proto()), + "Address" => a.endpoint.address().ip().cmp(&b.endpoint.address().ip()), + "Port" => a + .endpoint + .address() + .port() + .cmp(&b.endpoint.address().port()), + "Type" => PeerTypeWrapper(a.pt.clone()).cmp(&PeerTypeWrapper(b.pt.clone())), + "Connection State" => a.connection_state.cmp(&b.connection_state), + "Tx bytes" => a.tx_bytes.cmp(&b.tx_bytes), + "Rx bytes" => a.rx_bytes.cmp(&b.rx_bytes), + _ => Ordering::Equal, + }; + match direction { + SortDirection::Ascending => cmp, + SortDirection::Descending => cmp.reverse(), + } + }); +} + +pub struct PeerTypeWrapper(pub mycelium::peer_manager::PeerType); +impl Ord for PeerTypeWrapper { + fn cmp(&self, other: &Self) -> Ordering { + match (&self.0, &other.0) { + (PeerType::Static, PeerType::Static) => Ordering::Equal, + (PeerType::Static, _) => Ordering::Less, + (PeerType::LinkLocalDiscovery, PeerType::Static) => Ordering::Greater, + (PeerType::LinkLocalDiscovery, PeerType::LinkLocalDiscovery) => Ordering::Equal, + (PeerType::LinkLocalDiscovery, PeerType::Inbound) => Ordering::Less, + (PeerType::Inbound, PeerType::Inbound) => Ordering::Equal, + (PeerType::Inbound, _) => Ordering::Greater, + } + } +} + +impl PartialOrd for PeerTypeWrapper { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for PeerTypeWrapper { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for PeerTypeWrapper {} diff --git a/components/mycelium/mycelium-ui/src/components/routes.rs b/components/mycelium/mycelium-ui/src/components/routes.rs new file mode 100644 index 0000000..b093a17 --- /dev/null +++ b/components/mycelium/mycelium-ui/src/components/routes.rs @@ -0,0 +1,193 @@ +use std::cmp::Ordering; + +use crate::api; +use crate::{get_sort_indicator, SearchState, ServerAddress, SortDirection}; +use dioxus::prelude::*; + +#[component] +pub fn Routes() -> Element { + rsx! { + SelectedRoutesTable {} + FallbackRoutesTable {} + } +} + +#[component] +pub fn SelectedRoutesTable() -> Element { + let server_addr = use_context::>(); + let fetched_selected_routes = + use_resource(move || api::get_selected_routes(server_addr.read().0)); + + match &*fetched_selected_routes.read_unchecked() { + Some(Ok(routes)) => { + rsx! { RoutesTable { routes: routes.clone(), table_name: "Selected"} } + } + Some(Err(e)) => rsx! { div { "An error has occurred while fetching selected routes: {e}" }}, + None => rsx! { div { "Loading selected routes..." }}, + } +} + +#[component] +pub fn FallbackRoutesTable() -> Element { + let server_addr = use_context::>(); + let fetched_fallback_routes = + use_resource(move || api::get_fallback_routes(server_addr.read().0)); + + match &*fetched_fallback_routes.read_unchecked() { + Some(Ok(routes)) => { + rsx! { RoutesTable { routes: routes.clone(), table_name: "Fallback"} } + } + Some(Err(e)) => rsx! { div { "An error has occurred while fetching fallback routes: {e}" }}, + None => rsx! { div { "Loading fallback routes..." }}, + } +} + +#[component] +fn RoutesTable(routes: Vec, table_name: String) -> Element { + let mut current_page = use_signal(|| 0); + let items_per_page = 10; + let mut sort_column = use_signal(|| "Subnet".to_string()); + let mut sort_direction = use_signal(|| SortDirection::Descending); + let routes_len = routes.len(); + + let mut change_page = move |delta: i32| { + let cur_page = *current_page.read() as i32; + current_page.set( + (cur_page + delta) + .max(0) + .min((routes_len - 1) as i32 / items_per_page as i32) as usize, + ); + }; + + let mut sort_routes_signal = move |column: String| { + if column == *sort_column.read() { + let new_sort_direction = match *sort_direction.read() { + SortDirection::Ascending => SortDirection::Descending, + SortDirection::Descending => SortDirection::Ascending, + }; + sort_direction.set(new_sort_direction); + } else { + sort_column.set(column); + sort_direction.set(SortDirection::Ascending); + } + current_page.set(0); + }; + + let sorted_routes = use_memo(move || { + let mut sorted = routes.clone(); + sort_routes(&mut sorted, &sort_column.read(), &sort_direction.read()); + sorted + }); + + let mut search_state = use_signal(|| SearchState { + query: String::new(), + column: "Subnet".to_string(), + }); + + let filtered_routes = use_memo(move || { + let query = search_state.read().query.to_lowercase(); + let column = &search_state.read().column; + sorted_routes + .read() + .iter() + .filter(|route| match column.as_str() { + "Subnet" => route.subnet.to_string().to_lowercase().contains(&query), + "Next-hop" => route.next_hop.to_string().to_lowercase().contains(&query), + "Metric" => route.metric.to_string().to_lowercase().contains(&query), + "Seqno" => route.seqno.to_string().to_lowercase().contains(&query), + _ => false, + }) + .cloned() + .collect::>() + }); + + let routes_len = filtered_routes.len(); + + let start = current_page * items_per_page; + let end = (start + items_per_page).min(routes_len); + let current_routes = &filtered_routes.read()[start..end]; + + rsx! { + div { class: "{table_name.to_lowercase()}-routes", + h2 { "{table_name} Routes" } + div { class: "search-container", + input { + placeholder: "Search...", + value: "{search_state.read().query}", + oninput: move |evt| search_state.write().query.clone_from(&evt.value()), + } + select { + value: "{search_state.read().column}", + onchange: move |evt| search_state.write().column.clone_from(&evt.value()), + option { value: "Subnet", "Subnet" } + option { value: "Next-hop", "Next-hop" } + option { value: "Metric", "Metric" } + option { value: "Seqno", "Seqno" } + } + } + div { class: "table-container", + table { + thead { + tr { + th { class: "subnet-column", + onclick: move |_| sort_routes_signal("Subnet".to_string()), + "Subnet {get_sort_indicator(sort_column, sort_direction, \"Subnet\".to_string())}" + } + th { class: "next-hop-column", + onclick: move |_| sort_routes_signal("Next-hop".to_string()), + "Next-hop {get_sort_indicator(sort_column, sort_direction, \"Next-hop\".to_string())}" + } + th { class: "metric-column", + onclick: move |_| sort_routes_signal("Metric".to_string()), + "Metric {get_sort_indicator(sort_column, sort_direction, \"Metric\".to_string())}" + } + th { class: "seqno_column", + onclick: move |_| sort_routes_signal("Seqno".to_string()), + "Seqno {get_sort_indicator(sort_column, sort_direction, \"Seqno\".to_string())}" + } + } + } + tbody { + for route in current_routes { + tr { + td { class: "subnet-column", "{route.subnet}" } + td { class: "next-hop-column", "{route.next_hop}" } + td { class: "metric-column", "{route.metric}" } + td { class: "seqno-column", "{route.seqno}" } + } + } + } + } + } + div { class: "pagination", + button { + disabled: *current_page.read() == 0, + onclick: move |_| change_page(-1), + "Previous" + } + span { "Page {current_page + 1}" } + button { + disabled: (current_page + 1) * items_per_page >= routes_len, + onclick: move |_| change_page(1), + "Next" + } + } + } + } +} + +fn sort_routes(routes: &mut [mycelium_api::Route], column: &str, direction: &SortDirection) { + routes.sort_by(|a, b| { + let cmp = match column { + "Subnet" => a.subnet.cmp(&b.subnet), + "Next-hop" => a.next_hop.cmp(&b.next_hop), + "Metric" => a.metric.cmp(&b.metric), + "Seqno" => a.seqno.cmp(&b.seqno), + _ => Ordering::Equal, + }; + match direction { + SortDirection::Ascending => cmp, + SortDirection::Descending => cmp.reverse(), + } + }); +} diff --git a/components/mycelium/mycelium-ui/src/main.rs b/components/mycelium/mycelium-ui/src/main.rs new file mode 100644 index 0000000..760eeb1 --- /dev/null +++ b/components/mycelium/mycelium-ui/src/main.rs @@ -0,0 +1,118 @@ +#![allow(non_snake_case)] +// Disable terminal popup on Windows +#![cfg_attr(feature = "bundle", windows_subsystem = "windows")] + +mod api; +mod components; + +use components::home::Home; +use components::peers::Peers; +use components::routes::Routes; + +use dioxus::prelude::*; +use mycelium::{endpoint::Endpoint, peer_manager::PeerStats}; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, SocketAddr}, +}; + +const _: manganis::Asset = manganis::asset!("assets/styles.css"); + +const DEFAULT_SERVER_ADDR: SocketAddr = + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8989); + +fn main() { + // Init logger + dioxus_logger::init(tracing::Level::INFO).expect("failed to init logger"); + + let config = dioxus::desktop::Config::new() + .with_custom_head(r#""#.to_string()); + LaunchBuilder::desktop().with_cfg(config).launch(App); + // dioxus::launch(App); +} + +#[component] +fn App() -> Element { + use_context_provider(|| Signal::new(ServerAddress(DEFAULT_SERVER_ADDR))); + use_context_provider(|| Signal::new(ServerConnected(false))); + use_context_provider(|| { + Signal::new(PeerSignalMapping( + HashMap::>::new(), + )) + }); + use_context_provider(|| Signal::new(StopFetchingPeerSignal(false))); + + rsx! { + Router:: { + config: || { + RouterConfig::default().on_update(|state| { + use_context::>().write().0 = state.current() != Route::Peers {}; + (state.current() == Route::Peers {}).then_some(NavigationTarget::Internal(Route::Peers {})) + }) + } + } + } +} + +#[derive(Clone, Routable, Debug, PartialEq)] +#[rustfmt::skip] +pub enum Route { + #[layout(components::layout::Layout)] + #[route("/")] + Home {}, + #[route("/peers")] + Peers, + #[route("/routes")] + Routes, + #[end_layout] + #[route("/:..route")] + PageNotFound { route: Vec }, +} +// +#[derive(Clone, PartialEq)] +struct SearchState { + query: String, + column: String, +} + +// This signal is used to stop the loop that keeps fetching information about the peers when +// looking at the peers table, e.g. when the user goes back to Home or Routes page. +#[derive(Clone, PartialEq)] +struct StopFetchingPeerSignal(bool); + +#[derive(Clone, PartialEq)] +struct ServerAddress(SocketAddr); + +#[derive(Clone, PartialEq)] +struct ServerConnected(bool); + +#[derive(Clone, PartialEq)] +struct PeerSignalMapping(HashMap>); + +pub fn get_sort_indicator( + sort_column: Signal, + sort_direction: Signal, + column: String, +) -> String { + if *sort_column.read() == column { + match *sort_direction.read() { + SortDirection::Ascending => " ↑".to_string(), + SortDirection::Descending => " ↓".to_string(), + } + } else { + "".to_string() + } +} + +#[component] +fn PageNotFound(route: Vec) -> Element { + rsx! { + p { "Page not found"} + } +} + +#[derive(Clone)] +pub enum SortDirection { + Ascending, + Descending, +} diff --git a/components/mycelium/mycelium/Cargo.toml b/components/mycelium/mycelium/Cargo.toml new file mode 100644 index 0000000..8be2334 --- /dev/null +++ b/components/mycelium/mycelium/Cargo.toml @@ -0,0 +1,79 @@ +[package] +name = "mycelium" +version = "0.6.1" +edition = "2021" +license-file = "../LICENSE" +readme = "../README.md" + +[features] +message = [] +private-network = ["dep:openssl", "dep:tokio-openssl"] +vendored-openssl = ["openssl/vendored"] +mactunfd = [ + "tun/appstore", +] #mactunfd is a flag to specify that macos should provide tun FD instead of tun name + +[dependencies] +cdn-meta = { git = "https://github.com/threefoldtech/mycelium-cdn-registry", package = "cdn-meta" } + +tokio = { version = "1.46.1", features = [ + "io-util", + "fs", + "macros", + "net", + "sync", + "time", + "rt-multi-thread", # FIXME: remove once tokio::task::block_in_place calls are resolved +] } +tokio-util = { version = "0.7.15", features = ["codec"] } +futures = "0.3.31" +serde = { version = "1.0.219", features = ["derive"] } +rand = "0.9.1" +bytes = "1.10.1" +x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] } +aes-gcm = "0.10.3" +tracing = { version = "0.1.41", features = ["release_max_level_debug"] } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tracing-logfmt = { version = "0.3.5", features = ["ansi_logs"] } +faster-hex = "0.10.0" +tokio-stream = { version = "0.1.17", features = ["sync"] } +left-right = "0.11.5" +ipnet = "2.11.0" +ip_network_table-deps-treebitmap = "0.5.0" +blake3 = "1.8.2" +etherparse = "0.18.0" +quinn = { version = "0.11.8", default-features = false, features = [ + "runtime-tokio", + "rustls", +] } +rustls = { version = "0.23.29", default-features = false, features = ["ring"] } +rcgen = "0.14.2" +netdev = "0.36.0" +openssl = { version = "0.10.73", optional = true } +tokio-openssl = { version = "0.6.5", optional = true } +arc-swap = "1.7.1" +dashmap = { version = "6.1.0", features = ["inline"] } +ahash = "0.8.11" +axum = "0.8.4" +axum-extra = "0.10.1" +reqwest = "0.12.22" +redis = { version = "0.32.4", features = ["tokio-comp"] } +reed-solomon-erasure = "6.0.0" +[target.'cfg(target_os = "linux")'.dependencies] +rtnetlink = "0.17.0" +tokio-tun = "0.13.2" +nix = { version = "0.30.1", features = ["socket"] } + +[target.'cfg(target_os = "macos")'.dependencies] +tun = { git = "https://github.com/LeeSmet/rust-tun", features = ["async"] } +libc = "0.2.174" +nix = { version = "0.29.0", features = ["net", "socket", "ioctl"] } + +[target.'cfg(target_os = "windows")'.dependencies] +wintun = "0.5.1" + +[target.'cfg(target_os = "android")'.dependencies] +tun = { git = "https://github.com/LeeSmet/rust-tun", features = ["async"] } + +[target.'cfg(target_os = "ios")'.dependencies] +tun = { git = "https://github.com/LeeSmet/rust-tun", features = ["async"] } diff --git a/components/mycelium/mycelium/src/babel.rs b/components/mycelium/mycelium/src/babel.rs new file mode 100644 index 0000000..942ef2e --- /dev/null +++ b/components/mycelium/mycelium/src/babel.rs @@ -0,0 +1,321 @@ +//! This module contains babel related structs. +//! +//! We don't fully implement the babel spec, and items which are implemented might deviate to fit +//! our specific use case. For reference, the implementation is based on [this +//! RFC](https://datatracker.ietf.org/doc/html/rfc8966). + +use std::io; + +use bytes::{Buf, BufMut}; +use tokio_util::codec::{Decoder, Encoder}; +use tracing::trace; + +pub use self::{ + hello::Hello, ihu::Ihu, route_request::RouteRequest, seqno_request::SeqNoRequest, + update::Update, +}; + +pub use self::tlv::Tlv; + +mod hello; +mod ihu; +mod route_request; +mod seqno_request; +mod tlv; +mod update; + +/// Magic byte to identify babel protocol packet. +const BABEL_MAGIC: u8 = 42; +/// The version of the protocol we are currently using. +const BABEL_VERSION: u8 = 3; + +/// Size of a babel header on the wire. +const HEADER_WIRE_SIZE: usize = 4; + +/// TLV type for the [`Hello`] tlv +const TLV_TYPE_HELLO: u8 = 4; +/// TLV type for the [`Ihu`] tlv +const TLV_TYPE_IHU: u8 = 5; +/// TLV type for the [`Update`] tlv +const TLV_TYPE_UPDATE: u8 = 8; +/// TLV type for the [`RouteRequest`] tlv +const TLV_TYPE_ROUTE_REQUEST: u8 = 9; +/// TLV type for the [`SeqNoRequest`] tlv +const TLV_TYPE_SEQNO_REQUEST: u8 = 10; + +/// Wildcard address, the value is empty (0 bytes length). +const AE_WILDCARD: u8 = 0; +/// IPv4 address, the value is _at most_ 4 bytes long. +const AE_IPV4: u8 = 1; +/// IPv6 address, the value is _at most_ 16 bytes long. +const AE_IPV6: u8 = 2; +/// Link-local IPv6 address, the value is 8 bytes long. This implies a `fe80::/64` prefix. +const AE_IPV6_LL: u8 = 3; + +/// A codec which can send and receive whole babel packets on the wire. +#[derive(Debug, Clone)] +pub struct Codec { + header: Option
, +} + +impl Codec { + /// Create a new `BabelCodec`. + pub fn new() -> Self { + Self { header: None } + } + + /// Resets the `BabelCodec` to its default state. + pub fn reset(&mut self) { + self.header = None; + } +} + +/// The header for a babel packet. This follows the definition of the header [in the +/// RFC](https://datatracker.ietf.org/doc/html/rfc8966#name-packet-format). Since the header +/// contains only hard-coded fields and the length of an encoded body, there is no need for users +/// to manually construct this. In fact, it exists only to make our lives slightly easier in +/// reading/writing the header on the wire. +#[derive(Debug, Clone)] +struct Header { + magic: u8, + version: u8, + /// This is the length of the whole body following this header. Also excludes any possible + /// trailers. + body_length: u16, +} + +impl Decoder for Codec { + type Item = Tlv; + + type Error = io::Error; + + fn decode(&mut self, src: &mut bytes::BytesMut) -> Result, Self::Error> { + // Read a header if we don't have one yet. + let header = if let Some(header) = self.header.take() { + trace!("Continue from stored header"); + header + } else { + if src.remaining() < HEADER_WIRE_SIZE { + trace!("Insufficient bytes to read a babel header"); + return Ok(None); + } + + trace!("Read babel header"); + + Header { + magic: src.get_u8(), + version: src.get_u8(), + body_length: src.get_u16(), + } + }; + + if src.remaining() < header.body_length as usize { + trace!("Insufficient bytes to read babel body"); + self.header = Some(header); + return Ok(None); + } + + // Siltently ignore packets which don't have the correct values set, as defined in the + // spec. Note that we consume the amount of bytes indentified so we leave the parser in the + // correct state for the next packet. + if header.magic != BABEL_MAGIC || header.version != BABEL_VERSION { + trace!("Dropping babel packet with wrong magic or version"); + src.advance(header.body_length as usize); + self.reset(); + return Ok(None); + } + + // at this point we have a whole body loaded in the buffer. We currently don't support sub + // TLV's + + trace!("Read babel TLV body"); + + // TODO: Technically we need to loop here as we can have multiple TLVs. + + // TLV header + let tlv_type = src.get_u8(); + let body_len = src.get_u8(); + // TLV payload + let tlv = match tlv_type { + TLV_TYPE_HELLO => Some(Hello::from_bytes(src).into()), + TLV_TYPE_IHU => Ihu::from_bytes(src, body_len).map(From::from), + TLV_TYPE_UPDATE => Update::from_bytes(src, body_len).map(From::from), + TLV_TYPE_ROUTE_REQUEST => RouteRequest::from_bytes(src, body_len).map(From::from), + TLV_TYPE_SEQNO_REQUEST => SeqNoRequest::from_bytes(src, body_len).map(From::from), + _ => { + // unrecoginized body type, silently drop + trace!("Dropping unrecognized tlv"); + // We already read 2 bytes + src.advance(header.body_length as usize - 2); + self.reset(); + return Ok(None); + } + }; + + Ok(tlv) + } +} + +impl Encoder for Codec { + type Error = io::Error; + + fn encode(&mut self, item: Tlv, dst: &mut bytes::BytesMut) -> Result<(), Self::Error> { + // Write header + dst.put_u8(BABEL_MAGIC); + dst.put_u8(BABEL_VERSION); + dst.put_u16(item.wire_size() as u16 + 2); // tlv payload + tlv header + + // Write TLV's, TODO: currently only 1 TLV/body + + // TLV header + match item { + Tlv::Hello(_) => dst.put_u8(TLV_TYPE_HELLO), + Tlv::Ihu(_) => dst.put_u8(TLV_TYPE_IHU), + Tlv::Update(_) => dst.put_u8(TLV_TYPE_UPDATE), + Tlv::RouteRequest(_) => dst.put_u8(TLV_TYPE_ROUTE_REQUEST), + Tlv::SeqNoRequest(_) => dst.put_u8(TLV_TYPE_SEQNO_REQUEST), + } + dst.put_u8(item.wire_size()); + item.write_bytes(dst); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::{net::Ipv6Addr, time::Duration}; + + use futures::{SinkExt, StreamExt}; + use tokio_util::codec::Framed; + + use crate::subnet::Subnet; + + #[tokio::test] + async fn codec_hello() { + let (tx, rx) = tokio::io::duplex(1024); + let mut sender = Framed::new(tx, super::Codec::new()); + let mut receiver = Framed::new(rx, super::Codec::new()); + + let hello = super::Hello::new_unicast(15.into(), 400); + + sender + .send(hello.clone().into()) + .await + .expect("Send on a non-networked buffer can never fail; qed"); + let recv_hello = receiver + .next() + .await + .expect("Buffer isn't closed so this is always `Some`; qed") + .expect("Can decode the previously encoded value"); + assert_eq!(super::Tlv::from(hello), recv_hello); + } + + #[tokio::test] + async fn codec_ihu() { + let (tx, rx) = tokio::io::duplex(1024); + let mut sender = Framed::new(tx, super::Codec::new()); + let mut receiver = Framed::new(rx, super::Codec::new()); + + let ihu = super::Ihu::new(27.into(), 400, None); + + sender + .send(ihu.clone().into()) + .await + .expect("Send on a non-networked buffer can never fail; qed"); + let recv_ihu = receiver + .next() + .await + .expect("Buffer isn't closed so this is always `Some`; qed") + .expect("Can decode the previously encoded value"); + assert_eq!(super::Tlv::from(ihu), recv_ihu); + } + + #[tokio::test] + async fn codec_update() { + let (tx, rx) = tokio::io::duplex(1024); + let mut sender = Framed::new(tx, super::Codec::new()); + let mut receiver = Framed::new(rx, super::Codec::new()); + + let update = super::Update::new( + Duration::from_secs(400), + 16.into(), + 25.into(), + Subnet::new(Ipv6Addr::new(0x400, 1, 2, 3, 0, 0, 0, 0).into(), 64) + .expect("64 is a valid IPv6 prefix size; qed"), + [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + ] + .into(), + ); + + sender + .send(update.clone().into()) + .await + .expect("Send on a non-networked buffer can never fail; qed"); + println!("Sent update packet"); + let recv_update = receiver + .next() + .await + .expect("Buffer isn't closed so this is always `Some`; qed") + .expect("Can decode the previously encoded value"); + println!("Received update packet"); + assert_eq!(super::Tlv::from(update), recv_update); + } + + #[tokio::test] + async fn codec_seqno_request() { + let (tx, rx) = tokio::io::duplex(1024); + let mut sender = Framed::new(tx, super::Codec::new()); + let mut receiver = Framed::new(rx, super::Codec::new()); + + let snr = super::SeqNoRequest::new( + 16.into(), + [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + ] + .into(), + Subnet::new(Ipv6Addr::new(0x400, 1, 2, 3, 0, 0, 0, 0).into(), 64) + .expect("64 is a valid IPv6 prefix size; qed"), + ); + + sender + .send(snr.clone().into()) + .await + .expect("Send on a non-networked buffer can never fail; qed"); + let recv_update = receiver + .next() + .await + .expect("Buffer isn't closed so this is always `Some`; qed") + .expect("Can decode the previously encoded value"); + assert_eq!(super::Tlv::from(snr), recv_update); + } + + #[tokio::test] + async fn codec_route_request() { + let (tx, rx) = tokio::io::duplex(1024); + let mut sender = Framed::new(tx, super::Codec::new()); + let mut receiver = Framed::new(rx, super::Codec::new()); + + let rr = super::RouteRequest::new( + Some( + Subnet::new(Ipv6Addr::new(0x400, 1, 2, 3, 0, 0, 0, 0).into(), 64) + .expect("64 is a valid IPv6 prefix size; qed"), + ), + 13, + ); + + sender + .send(rr.clone().into()) + .await + .expect("Send on a non-networked buffer can never fail; qed"); + let recv_update = receiver + .next() + .await + .expect("Buffer isn't closed so this is always `Some`; qed") + .expect("Can decode the previously encoded value"); + assert_eq!(super::Tlv::from(rr), recv_update); + } +} diff --git a/components/mycelium/mycelium/src/babel/hello.rs b/components/mycelium/mycelium/src/babel/hello.rs new file mode 100644 index 0000000..102594d --- /dev/null +++ b/components/mycelium/mycelium/src/babel/hello.rs @@ -0,0 +1,162 @@ +//! The babel [Hello TLV](https://datatracker.ietf.org/doc/html/rfc8966#section-4.6.5). + +use bytes::{Buf, BufMut}; +use tracing::trace; + +use crate::sequence_number::SeqNo; + +/// Flag bit indicating a [`Hello`] is sent as unicast hello. +const HELLO_FLAG_UNICAST: u16 = 0x8000; + +/// Mask to apply to [`Hello`] flags, leaving only valid flags. +const FLAG_MASK: u16 = 0b10000000_00000000; + +/// Wire size of a [`Hello`] TLV without TLV header. +const HELLO_WIRE_SIZE: u8 = 6; + +/// Hello TLV body as defined in https://datatracker.ietf.org/doc/html/rfc8966#section-4.6.5. +#[derive(Debug, Clone, PartialEq)] +pub struct Hello { + flags: u16, + seqno: SeqNo, + interval: u16, +} + +impl Hello { + /// Create a new unicast hello packet. + pub fn new_unicast(seqno: SeqNo, interval: u16) -> Self { + Self { + flags: HELLO_FLAG_UNICAST, + seqno, + interval, + } + } + + /// Calculates the size on the wire of this `Hello`. + pub fn wire_size(&self) -> u8 { + HELLO_WIRE_SIZE + } + + /// Construct a `Hello` from wire bytes. + /// + /// # Panics + /// + /// This function will panic if there are insufficient bytes present in the provided buffer to + /// decode a complete `Hello`. + pub fn from_bytes(src: &mut bytes::BytesMut) -> Self { + let flags = src.get_u16() & FLAG_MASK; + let seqno = src.get_u16().into(); + let interval = src.get_u16(); + + trace!("Read hello tlv body"); + + Self { + flags, + seqno, + interval, + } + } + + /// Encode this `Hello` tlv as part of a packet. + pub fn write_bytes(&self, dst: &mut bytes::BytesMut) { + dst.put_u16(self.flags); + dst.put_u16(self.seqno.into()); + dst.put_u16(self.interval); + } +} + +#[cfg(test)] +mod tests { + use bytes::Buf; + + #[test] + fn encoding() { + let mut buf = bytes::BytesMut::new(); + + let hello = super::Hello { + flags: 0, + seqno: 25.into(), + interval: 400, + }; + + hello.write_bytes(&mut buf); + + assert_eq!(buf.len(), 6); + assert_eq!(buf[..6], [0, 0, 0, 25, 1, 144]); + + let mut buf = bytes::BytesMut::new(); + + let hello = super::Hello { + flags: super::HELLO_FLAG_UNICAST, + seqno: 16.into(), + interval: 4000, + }; + + hello.write_bytes(&mut buf); + + assert_eq!(buf.len(), 6); + assert_eq!(buf[..6], [128, 0, 0, 16, 15, 160]); + } + + #[test] + fn decoding() { + let mut buf = bytes::BytesMut::from(&[0b10000000u8, 0b00000000, 0, 19, 2, 1][..]); + + let hello = super::Hello { + flags: super::HELLO_FLAG_UNICAST, + seqno: 19.into(), + interval: 513, + }; + + assert_eq!(super::Hello::from_bytes(&mut buf), hello); + assert_eq!(buf.remaining(), 0); + + let mut buf = bytes::BytesMut::from(&[0b00000000u8, 0b00000000, 1, 19, 200, 100][..]); + + let hello = super::Hello { + flags: 0, + seqno: 275.into(), + interval: 51300, + }; + + assert_eq!(super::Hello::from_bytes(&mut buf), hello); + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn decode_ignores_invalid_flag_bits() { + let mut buf = bytes::BytesMut::from(&[0b10001001u8, 0b00000000, 0, 100, 1, 144][..]); + + let hello = super::Hello { + flags: super::HELLO_FLAG_UNICAST, + seqno: 100.into(), + interval: 400, + }; + + assert_eq!(super::Hello::from_bytes(&mut buf), hello); + assert_eq!(buf.remaining(), 0); + + let mut buf = bytes::BytesMut::from(&[0b00001001u8, 0b00000000, 0, 100, 1, 144][..]); + + let hello = super::Hello { + flags: 0, + seqno: 100.into(), + interval: 400, + }; + + assert_eq!(super::Hello::from_bytes(&mut buf), hello); + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn roundtrip() { + let mut buf = bytes::BytesMut::new(); + + let hello_src = super::Hello::new_unicast(16.into(), 400); + hello_src.write_bytes(&mut buf); + let decoded = super::Hello::from_bytes(&mut buf); + + assert_eq!(hello_src, decoded); + assert_eq!(buf.remaining(), 0); + } +} diff --git a/components/mycelium/mycelium/src/babel/ihu.rs b/components/mycelium/mycelium/src/babel/ihu.rs new file mode 100644 index 0000000..5296a9b --- /dev/null +++ b/components/mycelium/mycelium/src/babel/ihu.rs @@ -0,0 +1,246 @@ +//! The babel [IHU TLV](https://datatracker.ietf.org/doc/html/rfc8966#name-ihu). + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use bytes::{Buf, BufMut}; +use tracing::trace; + +use crate::metric::Metric; + +use super::{AE_IPV4, AE_IPV6, AE_IPV6_LL, AE_WILDCARD}; + +/// Base wire size of an [`Ihu`] without variable length address encoding. +const IHU_BASE_WIRE_SIZE: u8 = 6; + +/// IHU TLV body as defined in https://datatracker.ietf.org/doc/html/rfc8966#name-ihu. +#[derive(Debug, Clone, PartialEq)] +pub struct Ihu { + rx_cost: Metric, + interval: u16, + address: Option, +} + +impl Ihu { + /// Create a new `Ihu` to be transmitted. + pub fn new(rx_cost: Metric, interval: u16, address: Option) -> Self { + // An interval of 0 is illegal according to the RFC, as this value is used by the receiver + // to calculate the hold time. + if interval == 0 { + panic!("Ihu interval MUST NOT be 0"); + } + Self { + rx_cost, + interval, + address, + } + } + + /// Calculates the size on the wire of this `Ihu`. + pub fn wire_size(&self) -> u8 { + IHU_BASE_WIRE_SIZE + + match self.address { + None => 0, + Some(IpAddr::V4(_)) => 4, + // TODO: link local should be encoded differently + Some(IpAddr::V6(_)) => 16, + } + } + + /// Construct a `Ihu` from wire bytes. + /// + /// # Panics + /// + /// This function will panic if there are insufficient bytes present in the provided buffer to + /// decode a complete `Ihu`. + pub fn from_bytes(src: &mut bytes::BytesMut, len: u8) -> Option { + let ae = src.get_u8(); + // read and ignore reserved byte + let _ = src.get_u8(); + let rx_cost = src.get_u16().into(); + let interval = src.get_u16(); + let address = match ae { + AE_WILDCARD => None, + AE_IPV4 => { + let mut raw_ip = [0; 4]; + raw_ip.copy_from_slice(&src[..4]); + src.advance(4); + Some(Ipv4Addr::from(raw_ip).into()) + } + AE_IPV6 => { + let mut raw_ip = [0; 16]; + raw_ip.copy_from_slice(&src[..16]); + src.advance(16); + Some(Ipv6Addr::from(raw_ip).into()) + } + AE_IPV6_LL => { + let mut raw_ip = [0; 16]; + raw_ip[0] = 0xfe; + raw_ip[1] = 0x80; + raw_ip[8..].copy_from_slice(&src[..8]); + src.advance(8); + Some(Ipv6Addr::from(raw_ip).into()) + } + _ => { + // Invalid AE type, skip reamining data and ignore + trace!("Invalid AE type in IHU TLV, drop TLV"); + src.advance(len as usize - 6); + return None; + } + }; + + trace!("Read ihu tlv body"); + + Some(Self { + rx_cost, + interval, + address, + }) + } + + /// Encode this `Ihu` tlv as part of a packet. + pub fn write_bytes(&self, dst: &mut bytes::BytesMut) { + dst.put_u8(match self.address { + None => AE_WILDCARD, + Some(IpAddr::V4(_)) => AE_IPV4, + Some(IpAddr::V6(_)) => AE_IPV6, + }); + // reserved byte, must be all 0 + dst.put_u8(0); + dst.put_u16(self.rx_cost.into()); + dst.put_u16(self.interval); + match self.address { + None => {} + Some(IpAddr::V4(ip)) => dst.put_slice(&ip.octets()), + Some(IpAddr::V6(ip)) => dst.put_slice(&ip.octets()), + } + } +} + +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, Ipv6Addr}; + + use bytes::Buf; + + #[test] + fn encoding() { + let mut buf = bytes::BytesMut::new(); + + let ihu = super::Ihu { + rx_cost: 25.into(), + interval: 400, + address: Some(Ipv4Addr::new(1, 1, 1, 1).into()), + }; + + ihu.write_bytes(&mut buf); + + assert_eq!(buf.len(), 10); + assert_eq!(buf[..10], [1, 0, 0, 25, 1, 144, 1, 1, 1, 1]); + + let mut buf = bytes::BytesMut::new(); + + let ihu = super::Ihu { + rx_cost: 100.into(), + interval: 4000, + address: Some(Ipv6Addr::new(2, 0, 1234, 2345, 3456, 4567, 5678, 1).into()), + }; + + ihu.write_bytes(&mut buf); + + assert_eq!(buf.len(), 22); + assert_eq!( + buf[..22], + [2, 0, 0, 100, 15, 160, 0, 2, 0, 0, 4, 210, 9, 41, 13, 128, 17, 215, 22, 46, 0, 1] + ); + } + + #[test] + fn decoding() { + let mut buf = bytes::BytesMut::from(&[0, 0, 0, 1, 1, 44][..]); + + let ihu = super::Ihu { + rx_cost: 1.into(), + interval: 300, + address: None, + }; + + let buf_len = buf.len(); + assert_eq!(super::Ihu::from_bytes(&mut buf, buf_len as u8), Some(ihu)); + assert_eq!(buf.remaining(), 0); + + let mut buf = bytes::BytesMut::from(&[1, 0, 0, 2, 0, 44, 3, 4, 5, 6][..]); + + let ihu = super::Ihu { + rx_cost: 2.into(), + interval: 44, + address: Some(Ipv4Addr::new(3, 4, 5, 6).into()), + }; + + let buf_len = buf.len(); + assert_eq!(super::Ihu::from_bytes(&mut buf, buf_len as u8), Some(ihu)); + assert_eq!(buf.remaining(), 0); + + let mut buf = bytes::BytesMut::from( + &[ + 2, 0, 0, 2, 0, 44, 4, 0, 0, 0, 0, 5, 0, 6, 7, 8, 9, 10, 11, 12, 13, 14, + ][..], + ); + + let ihu = super::Ihu { + rx_cost: 2.into(), + interval: 44, + address: Some(Ipv6Addr::new(0x400, 0, 5, 6, 0x708, 0x90a, 0xb0c, 0xd0e).into()), + }; + + let buf_len = buf.len(); + assert_eq!(super::Ihu::from_bytes(&mut buf, buf_len as u8), Some(ihu)); + assert_eq!(buf.remaining(), 0); + + let mut buf = bytes::BytesMut::from(&[3, 0, 1, 2, 0, 42, 7, 8, 9, 10, 11, 12, 13, 14][..]); + + let ihu = super::Ihu { + rx_cost: 258.into(), + interval: 42, + address: Some(Ipv6Addr::new(0xfe80, 0, 0, 0, 0x708, 0x90a, 0xb0c, 0xd0e).into()), + }; + + let buf_len = buf.len(); + assert_eq!(super::Ihu::from_bytes(&mut buf, buf_len as u8), Some(ihu)); + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn decode_ignores_invalid_ae_encoding() { + // AE 4 as it is the first one which should be used in protocol extension, causing this + // test to fail if we forget to update something + let mut buf = bytes::BytesMut::from( + &[ + 4, 0, 0, 2, 0, 44, 2, 0, 0, 0, 0, 5, 0, 6, 7, 8, 9, 10, 11, 12, 13, 14, + ][..], + ); + + let buf_len = buf.len(); + + assert_eq!(super::Ihu::from_bytes(&mut buf, buf_len as u8), None); + // Decode function should still consume the required amount of bytes to leave parser in a + // good state (assuming the length in the tlv preamble is good). + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn roundtrip() { + let mut buf = bytes::BytesMut::new(); + + let hello_src = super::Ihu::new( + 16.into(), + 400, + Some(Ipv6Addr::new(156, 5646, 4164, 1236, 872, 960, 10, 844).into()), + ); + hello_src.write_bytes(&mut buf); + let buf_len = buf.len(); + let decoded = super::Ihu::from_bytes(&mut buf, buf_len as u8); + + assert_eq!(Some(hello_src), decoded); + assert_eq!(buf.remaining(), 0); + } +} diff --git a/components/mycelium/mycelium/src/babel/route_request.rs b/components/mycelium/mycelium/src/babel/route_request.rs new file mode 100644 index 0000000..fde1130 --- /dev/null +++ b/components/mycelium/mycelium/src/babel/route_request.rs @@ -0,0 +1,301 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use bytes::{Buf, BufMut}; +use tracing::trace; + +use crate::subnet::Subnet; + +use super::{AE_IPV4, AE_IPV6, AE_IPV6_LL, AE_WILDCARD}; + +/// Base wire size of a [`RouteRequest`] without variable length address encoding. +const ROUTE_REQUEST_BASE_WIRE_SIZE: u8 = 3; + +/// Seqno request TLV body as defined in https://datatracker.ietf.org/doc/html/rfc8966#name-route-request +#[derive(Debug, Clone, PartialEq)] +pub struct RouteRequest { + /// The prefix being requested + prefix: Option, + /// The requests' generation + generation: u8, +} + +impl RouteRequest { + /// Creates a new `RouteRequest` for the given [`prefix`]. If no [`prefix`] is given, a full + /// route table dumb in requested. + /// + /// [`prefix`]: Subnet + pub fn new(prefix: Option, generation: u8) -> Self { + Self { prefix, generation } + } + + /// Return the [`prefix`](Subnet) associated with this `RouteRequest`. + pub fn prefix(&self) -> Option { + self.prefix + } + + /// Return the generation of the `RouteRequest`, which is the amount of times it has been + /// forwarded already. + pub fn generation(&self) -> u8 { + self.generation + } + + /// Increment the generation of the `RouteRequest`. + pub fn inc_generation(&mut self) { + self.generation += 1 + } + + /// Calculates the size on the wire of this `RouteRequest`. + pub fn wire_size(&self) -> u8 { + ROUTE_REQUEST_BASE_WIRE_SIZE + + (if let Some(prefix) = self.prefix { + prefix.prefix_len().div_ceil(8) + } else { + 0 + }) + } + + /// Construct a `RouteRequest` from wire bytes. + /// + /// # Panics + /// + /// This function will panic if there are insufficient bytes present in the provided buffer to + /// decode a complete `RouteRequest`. + pub fn from_bytes(src: &mut bytes::BytesMut, len: u8) -> Option { + let generation = src.get_u8(); + let ae = src.get_u8(); + let plen = src.get_u8(); + + let prefix_size = plen.div_ceil(8) as usize; + + let prefix_ip = match ae { + AE_WILDCARD => None, + AE_IPV4 => { + if plen > 32 { + return None; + } + let mut raw_ip = [0; 4]; + raw_ip[..prefix_size].copy_from_slice(&src[..prefix_size]); + src.advance(prefix_size); + Some(Ipv4Addr::from(raw_ip).into()) + } + AE_IPV6 => { + if plen > 128 { + return None; + } + let mut raw_ip = [0; 16]; + raw_ip[..prefix_size].copy_from_slice(&src[..prefix_size]); + src.advance(prefix_size); + Some(Ipv6Addr::from(raw_ip).into()) + } + AE_IPV6_LL => { + if plen != 64 { + return None; + } + let mut raw_ip = [0; 16]; + raw_ip[0] = 0xfe; + raw_ip[1] = 0x80; + raw_ip[8..].copy_from_slice(&src[..8]); + src.advance(8); + Some(Ipv6Addr::from(raw_ip).into()) + } + _ => { + // Invalid AE type, skip reamining data and ignore + trace!("Invalid AE type in route_request packet, drop packet"); + src.advance(len as usize - 3); + return None; + } + }; + + let prefix = prefix_ip.and_then(|prefix| Subnet::new(prefix, plen).ok()); + + trace!("Read route_request tlv body"); + + Some(RouteRequest { prefix, generation }) + } + + /// Encode this `RouteRequest` tlv as part of a packet. + pub fn write_bytes(&self, dst: &mut bytes::BytesMut) { + dst.put_u8(self.generation); + if let Some(prefix) = self.prefix { + dst.put_u8(match prefix.address() { + IpAddr::V4(_) => AE_IPV4, + IpAddr::V6(_) => AE_IPV6, + }); + dst.put_u8(prefix.prefix_len()); + let prefix_len = prefix.prefix_len().div_ceil(8) as usize; + match prefix.address() { + IpAddr::V4(ip) => dst.put_slice(&ip.octets()[..prefix_len]), + IpAddr::V6(ip) => dst.put_slice(&ip.octets()[..prefix_len]), + } + } else { + dst.put_u8(AE_WILDCARD); + // Prefix len MUST be 0 for wildcard requests + dst.put_u8(0); + } + } +} + +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, Ipv6Addr}; + + use bytes::Buf; + + use crate::subnet::Subnet; + + #[test] + fn encoding() { + let mut buf = bytes::BytesMut::new(); + + let rr = super::RouteRequest { + prefix: Some( + Subnet::new(Ipv6Addr::new(512, 25, 26, 27, 28, 0, 0, 29).into(), 64) + .expect("64 is a valid IPv6 prefix size; qed"), + ), + generation: 2, + }; + + rr.write_bytes(&mut buf); + + assert_eq!(buf.len(), 11); + assert_eq!(buf[..11], [2, 2, 64, 2, 0, 0, 25, 0, 26, 0, 27]); + + let mut buf = bytes::BytesMut::new(); + + let rr = super::RouteRequest { + prefix: Some( + Subnet::new(Ipv4Addr::new(10, 101, 4, 1).into(), 32) + .expect("32 is a valid IPv4 prefix size; qed"), + ), + generation: 3, + }; + + rr.write_bytes(&mut buf); + + assert_eq!(buf.len(), 7); + assert_eq!(buf[..7], [3, 1, 32, 10, 101, 4, 1]); + + let mut buf = bytes::BytesMut::new(); + + let rr = super::RouteRequest { + prefix: None, + generation: 0, + }; + + rr.write_bytes(&mut buf); + + assert_eq!(buf.len(), 3); + assert_eq!(buf[..3], [0, 0, 0]); + } + + #[test] + fn decoding() { + let mut buf = bytes::BytesMut::from(&[12, 0, 0][..]); + + let rr = super::RouteRequest { + prefix: None, + generation: 12, + }; + + let buf_len = buf.len(); + assert_eq!( + super::RouteRequest::from_bytes(&mut buf, buf_len as u8), + Some(rr) + ); + assert_eq!(buf.remaining(), 0); + + let mut buf = bytes::BytesMut::from(&[24, 1, 24, 10, 15, 19][..]); + + let rr = super::RouteRequest { + prefix: Some( + Subnet::new(Ipv4Addr::new(10, 15, 19, 0).into(), 24) + .expect("24 is a valid IPv4 prefix size; qed"), + ), + generation: 24, + }; + + let buf_len = buf.len(); + assert_eq!( + super::RouteRequest::from_bytes(&mut buf, buf_len as u8), + Some(rr) + ); + assert_eq!(buf.remaining(), 0); + + let mut buf = bytes::BytesMut::from(&[7, 2, 64, 0, 10, 0, 20, 0, 30, 0, 40][..]); + + let rr = super::RouteRequest { + prefix: Some( + Subnet::new(Ipv6Addr::new(10, 20, 30, 40, 0, 0, 0, 0).into(), 64) + .expect("64 is a valid IPv6 prefix size; qed"), + ), + generation: 7, + }; + + let buf_len = buf.len(); + assert_eq!( + super::RouteRequest::from_bytes(&mut buf, buf_len as u8), + Some(rr) + ); + assert_eq!(buf.remaining(), 0); + + let mut buf = bytes::BytesMut::from(&[4, 3, 64, 0, 10, 0, 20, 0, 30, 0, 40][..]); + + let rr = super::RouteRequest { + prefix: Some( + Subnet::new(Ipv6Addr::new(0xfe80, 0, 0, 0, 10, 20, 30, 40).into(), 64) + .expect("64 is a valid IPv6 prefix size; qed"), + ), + generation: 4, + }; + + let buf_len = buf.len(); + assert_eq!( + super::RouteRequest::from_bytes(&mut buf, buf_len as u8), + Some(rr) + ); + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn decode_ignores_invalid_ae_encoding() { + // AE 4 as it is the first one which should be used in protocol extension, causing this + // test to fail if we forget to update something + let mut buf = bytes::BytesMut::from( + &[ + 0, 4, 64, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + ][..], + ); + + let buf_len = buf.len(); + + assert_eq!( + super::RouteRequest::from_bytes(&mut buf, buf_len as u8), + None + ); + // Decode function should still consume the required amount of bytes to leave parser in a + // good state (assuming the length in the tlv preamble is good). + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn roundtrip() { + let mut buf = bytes::BytesMut::new(); + + let seqno_src = super::RouteRequest::new( + Some( + Subnet::new( + Ipv6Addr::new(0x21f, 0x4025, 0xabcd, 0xdead, 0, 0, 0, 0).into(), + 64, + ) + .expect("64 is a valid IPv6 prefix size; qed"), + ), + 27, + ); + seqno_src.write_bytes(&mut buf); + let buf_len = buf.len(); + let decoded = super::RouteRequest::from_bytes(&mut buf, buf_len as u8); + + assert_eq!(Some(seqno_src), decoded); + assert_eq!(buf.remaining(), 0); + } +} diff --git a/components/mycelium/mycelium/src/babel/seqno_request.rs b/components/mycelium/mycelium/src/babel/seqno_request.rs new file mode 100644 index 0000000..036d834 --- /dev/null +++ b/components/mycelium/mycelium/src/babel/seqno_request.rs @@ -0,0 +1,356 @@ +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + num::NonZeroU8, +}; + +use bytes::{Buf, BufMut}; +use tracing::{debug, trace}; + +use crate::{router_id::RouterId, sequence_number::SeqNo, subnet::Subnet}; + +use super::{AE_IPV4, AE_IPV6, AE_IPV6_LL, AE_WILDCARD}; + +/// The default HOP COUNT value used in new SeqNo requests, as per https://datatracker.ietf.org/doc/html/rfc8966#section-3.8.2.1 +// SAFETY: value is not zero. +const DEFAULT_HOP_COUNT: NonZeroU8 = NonZeroU8::new(64).unwrap(); + +/// Base wire size of a [`SeqNoRequest`] without variable length address encoding. +const SEQNO_REQUEST_BASE_WIRE_SIZE: u8 = 6 + RouterId::BYTE_SIZE as u8; + +/// Seqno request TLV body as defined in https://datatracker.ietf.org/doc/html/rfc8966#name-seqno-request +#[derive(Debug, Clone, PartialEq)] +pub struct SeqNoRequest { + /// The sequence number that is being requested. + seqno: SeqNo, + /// The maximum number of times this TLV may be forwarded, plus 1. + hop_count: NonZeroU8, + /// The router id that is being requested. + router_id: RouterId, + /// The prefix being requested + prefix: Subnet, +} + +impl SeqNoRequest { + /// Create a new `SeqNoRequest` for the given [prefix](Subnet) advertised by the [`RouterId`], + /// with the required new [`SeqNo`]. + pub fn new(seqno: SeqNo, router_id: RouterId, prefix: Subnet) -> SeqNoRequest { + Self { + seqno, + hop_count: DEFAULT_HOP_COUNT, + router_id, + prefix, + } + } + + /// Return the [`prefix`](Subnet) associated with this `SeqNoRequest`. + pub fn prefix(&self) -> Subnet { + self.prefix + } + + /// Return the [`RouterId`] associated with this `SeqNoRequest`. + pub fn router_id(&self) -> RouterId { + self.router_id + } + + /// Return the requested [`SeqNo`] associated with this `SeqNoRequest`. + pub fn seqno(&self) -> SeqNo { + self.seqno + } + + /// Get the hop count for this `SeqNoRequest`. + pub fn hop_count(&self) -> u8 { + self.hop_count.into() + } + + /// Decrement the hop count for this `SeqNoRequest`. + /// + /// # Panics + /// + /// This function will panic if the hop count before calling this function is 1, as that will + /// result in a hop count of 0, which is illegal for a `SeqNoRequest`. It is up to the caller + /// to ensure this condition holds. + pub fn decrement_hop_count(&mut self) { + // SAFETY: The panic from this expect is documented in the function signature. + self.hop_count = NonZeroU8::new(self.hop_count.get() - 1) + .expect("Decrementing a hop count of 1 is not allowed"); + } + + /// Calculates the size on the wire of this `Update`. + pub fn wire_size(&self) -> u8 { + SEQNO_REQUEST_BASE_WIRE_SIZE + self.prefix.prefix_len().div_ceil(8) + // TODO: Wildcard should be encoded differently + } + + /// Construct a `SeqNoRequest` from wire bytes. + /// + /// # Panics + /// + /// This function will panic if there are insufficient bytes present in the provided buffer to + /// decode a complete `SeqNoRequest`. + pub fn from_bytes(src: &mut bytes::BytesMut, len: u8) -> Option { + let ae = src.get_u8(); + let plen = src.get_u8(); + let seqno = src.get_u16().into(); + let hop_count = src.get_u8(); + // Read "reserved" value, we assume this is 0 + let _ = src.get_u8(); + + let mut router_id_bytes = [0u8; RouterId::BYTE_SIZE]; + router_id_bytes.copy_from_slice(&src[..RouterId::BYTE_SIZE]); + src.advance(RouterId::BYTE_SIZE); + + let router_id = RouterId::from(router_id_bytes); + + let prefix_size = plen.div_ceil(8) as usize; + + let prefix = match ae { + AE_WILDCARD => { + if plen != 0 { + return None; + } + // TODO: this is a temporary placeholder until we figure out how to handle this + Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0).into() + } + AE_IPV4 => { + if plen > 32 { + return None; + } + let mut raw_ip = [0; 4]; + raw_ip[..prefix_size].copy_from_slice(&src[..prefix_size]); + src.advance(prefix_size); + Ipv4Addr::from(raw_ip).into() + } + AE_IPV6 => { + if plen > 128 { + return None; + } + let mut raw_ip = [0; 16]; + raw_ip[..prefix_size].copy_from_slice(&src[..prefix_size]); + src.advance(prefix_size); + Ipv6Addr::from(raw_ip).into() + } + AE_IPV6_LL => { + if plen != 64 { + return None; + } + let mut raw_ip = [0; 16]; + raw_ip[0] = 0xfe; + raw_ip[1] = 0x80; + raw_ip[8..].copy_from_slice(&src[..8]); + src.advance(8); + Ipv6Addr::from(raw_ip).into() + } + _ => { + // Invalid AE type, skip reamining data and ignore + trace!("Invalid AE type in seqno_request packet, drop packet"); + src.advance(len as usize - 46); + return None; + } + }; + + let prefix = Subnet::new(prefix, plen).ok()?; + + trace!("Read seqno_request tlv body"); + + // Make sure hop_count is valid + let hop_count = if let Some(hc) = NonZeroU8::new(hop_count) { + hc + } else { + debug!("Dropping seqno_request as hop_count field is set to 0"); + return None; + }; + + Some(SeqNoRequest { + seqno, + hop_count, + router_id, + prefix, + }) + } + + /// Encode this `SeqNoRequest` tlv as part of a packet. + pub fn write_bytes(&self, dst: &mut bytes::BytesMut) { + dst.put_u8(match self.prefix.address() { + IpAddr::V4(_) => AE_IPV4, + IpAddr::V6(_) => AE_IPV6, + }); + dst.put_u8(self.prefix.prefix_len()); + dst.put_u16(self.seqno.into()); + dst.put_u8(self.hop_count.into()); + // Write "reserved" value. + dst.put_u8(0); + dst.put_slice(&self.router_id.as_bytes()[..]); + let prefix_len = self.prefix.prefix_len().div_ceil(8) as usize; + match self.prefix.address() { + IpAddr::V4(ip) => dst.put_slice(&ip.octets()[..prefix_len]), + IpAddr::V6(ip) => dst.put_slice(&ip.octets()[..prefix_len]), + } + } +} + +#[cfg(test)] +mod tests { + use std::{ + net::{Ipv4Addr, Ipv6Addr}, + num::NonZeroU8, + }; + + use crate::{router_id::RouterId, subnet::Subnet}; + use bytes::Buf; + + #[test] + fn encoding() { + let mut buf = bytes::BytesMut::new(); + + let snr = super::SeqNoRequest { + seqno: 17.into(), + hop_count: NonZeroU8::new(64).unwrap(), + prefix: Subnet::new(Ipv6Addr::new(512, 25, 26, 27, 28, 0, 0, 29).into(), 64) + .expect("64 is a valid IPv6 prefix size; qed"), + router_id: RouterId::from([1u8; RouterId::BYTE_SIZE]), + }; + + snr.write_bytes(&mut buf); + + assert_eq!(buf.len(), 54); + assert_eq!( + buf[..54], + [ + 2, 64, 0, 17, 64, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 0, 25, 0, 26, 0, 27, + ] + ); + + let mut buf = bytes::BytesMut::new(); + + let snr = super::SeqNoRequest { + seqno: 170.into(), + hop_count: NonZeroU8::new(111).unwrap(), + prefix: Subnet::new(Ipv4Addr::new(10, 101, 4, 1).into(), 32) + .expect("32 is a valid IPv4 prefix size; qed"), + router_id: RouterId::from([2u8; RouterId::BYTE_SIZE]), + }; + + snr.write_bytes(&mut buf); + + assert_eq!(buf.len(), 50); + assert_eq!( + buf[..50], + [ + 1, 32, 0, 170, 111, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10, 101, 4, 1, + ] + ); + } + + #[test] + fn decoding() { + let mut buf = bytes::BytesMut::from( + &[ + 0, 0, 0, 0, 1, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + ][..], + ); + + let snr = super::SeqNoRequest { + hop_count: NonZeroU8::new(1).unwrap(), + seqno: 0.into(), + prefix: Subnet::new(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0).into(), 0) + .expect("0 is a valid IPv6 prefix size; qed"), + router_id: RouterId::from([3u8; RouterId::BYTE_SIZE]), + }; + + let buf_len = buf.len(); + assert_eq!( + super::SeqNoRequest::from_bytes(&mut buf, buf_len as u8), + Some(snr) + ); + assert_eq!(buf.remaining(), 0); + + let mut buf = bytes::BytesMut::from( + &[ + 3, 64, 0, 42, 232, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 10, 0, 20, 0, 30, 0, + 40, + ][..], + ); + + let snr = super::SeqNoRequest { + seqno: 42.into(), + hop_count: NonZeroU8::new(232).unwrap(), + prefix: Subnet::new(Ipv6Addr::new(0xfe80, 0, 0, 0, 10, 20, 30, 40).into(), 64) + .expect("92 is a valid IPv6 prefix size; qed"), + router_id: RouterId::from([4u8; RouterId::BYTE_SIZE]), + }; + + let buf_len = buf.len(); + assert_eq!( + super::SeqNoRequest::from_bytes(&mut buf, buf_len as u8), + Some(snr) + ); + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn decode_ignores_invalid_ae_encoding() { + // AE 4 as it is the first one which should be used in protocol extension, causing this + // test to fail if we forget to update something + let mut buf = bytes::BytesMut::from( + &[ + 4, 64, 0, 0, 44, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19, 20, 21, + ][..], + ); + + let buf_len = buf.len(); + + assert_eq!( + super::SeqNoRequest::from_bytes(&mut buf, buf_len as u8), + None + ); + // Decode function should still consume the required amount of bytes to leave parser in a + // good state (assuming the length in the tlv preamble is good). + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn decode_ignores_invalid_hop_count() { + // Set all flag bits, only allowed bits should be set on the decoded value + let mut buf = bytes::BytesMut::from( + &[ + 3, 64, 92, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 10, 0, 20, 0, 30, 0, + 40, + ][..], + ); + + let buf_len = buf.len(); + assert_eq!( + super::SeqNoRequest::from_bytes(&mut buf, buf_len as u8), + None + ); + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn roundtrip() { + let mut buf = bytes::BytesMut::new(); + + let seqno_src = super::SeqNoRequest::new( + 64.into(), + RouterId::from([6; RouterId::BYTE_SIZE]), + Subnet::new( + Ipv6Addr::new(0x21f, 0x4025, 0xabcd, 0xdead, 0, 0, 0, 0).into(), + 64, + ) + .expect("64 is a valid IPv6 prefix size; qed"), + ); + seqno_src.write_bytes(&mut buf); + let buf_len = buf.len(); + let decoded = super::SeqNoRequest::from_bytes(&mut buf, buf_len as u8); + + assert_eq!(Some(seqno_src), decoded); + assert_eq!(buf.remaining(), 0); + } +} diff --git a/components/mycelium/mycelium/src/babel/tlv.rs b/components/mycelium/mycelium/src/babel/tlv.rs new file mode 100644 index 0000000..36788fb --- /dev/null +++ b/components/mycelium/mycelium/src/babel/tlv.rs @@ -0,0 +1,72 @@ +pub use super::{hello::Hello, ihu::Ihu, update::Update}; +use super::{route_request::RouteRequest, SeqNoRequest}; + +/// A single `Tlv` in a babel packet body. +#[derive(Debug, Clone, PartialEq)] +pub enum Tlv { + /// Hello Tlv type. + Hello(Hello), + /// Ihu Tlv type. + Ihu(Ihu), + /// Update Tlv type. + Update(Update), + /// RouteRequest Tlv type. + RouteRequest(RouteRequest), + /// SeqNoRequest Tlv type + SeqNoRequest(SeqNoRequest), +} + +impl Tlv { + /// Calculate the size on the wire for this `Tlv`. This DOES NOT included the TLV header size + /// (2 bytes). + pub fn wire_size(&self) -> u8 { + match self { + Self::Hello(hello) => hello.wire_size(), + Self::Ihu(ihu) => ihu.wire_size(), + Self::Update(update) => update.wire_size(), + Self::RouteRequest(route_request) => route_request.wire_size(), + Self::SeqNoRequest(seqno_request) => seqno_request.wire_size(), + } + } + + /// Encode this `Tlv` as part of a packet. + pub fn write_bytes(&self, dst: &mut bytes::BytesMut) { + match self { + Self::Hello(hello) => hello.write_bytes(dst), + Self::Ihu(ihu) => ihu.write_bytes(dst), + Self::Update(update) => update.write_bytes(dst), + Self::RouteRequest(route_request) => route_request.write_bytes(dst), + Self::SeqNoRequest(seqno_request) => seqno_request.write_bytes(dst), + } + } +} + +impl From for Tlv { + fn from(v: SeqNoRequest) -> Self { + Self::SeqNoRequest(v) + } +} + +impl From for Tlv { + fn from(v: RouteRequest) -> Self { + Self::RouteRequest(v) + } +} + +impl From for Tlv { + fn from(v: Update) -> Self { + Self::Update(v) + } +} + +impl From for Tlv { + fn from(v: Ihu) -> Self { + Self::Ihu(v) + } +} + +impl From for Tlv { + fn from(v: Hello) -> Self { + Self::Hello(v) + } +} diff --git a/components/mycelium/mycelium/src/babel/update.rs b/components/mycelium/mycelium/src/babel/update.rs new file mode 100644 index 0000000..ba610e8 --- /dev/null +++ b/components/mycelium/mycelium/src/babel/update.rs @@ -0,0 +1,385 @@ +//! The babel [Update TLV](https://datatracker.ietf.org/doc/html/rfc8966#name-update). + +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + time::Duration, +}; + +use bytes::{Buf, BufMut}; +use tracing::trace; + +use crate::{metric::Metric, router_id::RouterId, sequence_number::SeqNo, subnet::Subnet}; + +use super::{AE_IPV4, AE_IPV6, AE_IPV6_LL, AE_WILDCARD}; + +/// Flag bit indicating an [`Update`] TLV establishes a new default prefix. +#[allow(dead_code)] +const UPDATE_FLAG_PREFIX: u8 = 0x80; +/// Flag bit indicating an [`Update`] TLV establishes a new default router-id. +#[allow(dead_code)] +const UPDATE_FLAG_ROUTER_ID: u8 = 0x40; +/// Mask to apply to [`Update`] flags, leaving only valid flags. +const FLAG_MASK: u8 = 0b1100_0000; + +/// Base wire size of an [`Update`] without variable length address encoding. +const UPDATE_BASE_WIRE_SIZE: u8 = 10 + RouterId::BYTE_SIZE as u8; + +/// Update TLV body as defined in https://datatracker.ietf.org/doc/html/rfc8966#name-update. +#[derive(Debug, Clone, PartialEq)] +pub struct Update { + /// Flags set in the TLV. + flags: u8, + /// Upper bound in centiseconds after which a new `Update` is sent. Must not be 0. + interval: u16, + /// Senders sequence number. + seqno: SeqNo, + /// Senders metric for this route. + metric: Metric, + /// The [`Subnet`] contained in this update. An update packet itself can contain any allowed + /// subnet. + subnet: Subnet, + /// Router id of the sender. Importantly this is not part of the update itself, though we do + /// transmit it for now as such. + router_id: RouterId, +} + +impl Update { + /// Create a new `Update`. + pub fn new( + interval: Duration, + seqno: SeqNo, + metric: Metric, + subnet: Subnet, + router_id: RouterId, + ) -> Self { + let interval_centiseconds = (interval.as_millis() / 10) as u16; + Self { + // No flags used for now + flags: 0, + interval: interval_centiseconds, + seqno, + metric, + subnet, + router_id, + } + } + + /// Returns the [`SeqNo`] of the sender of this `Update`. + pub fn seqno(&self) -> SeqNo { + self.seqno + } + + /// Return the [`Metric`] of the sender for this route in the `Update`. + pub fn metric(&self) -> Metric { + self.metric + } + + /// Return the [`Subnet`] in this `Update.` + pub fn subnet(&self) -> Subnet { + self.subnet + } + + /// Return the [`router-id`](PublicKey) of the router who advertised this [`Prefix`](IpAddr). + pub fn router_id(&self) -> RouterId { + self.router_id + } + + /// Calculates the size on the wire of this `Update`. + pub fn wire_size(&self) -> u8 { + let address_bytes = self.subnet.prefix_len().div_ceil(8); + UPDATE_BASE_WIRE_SIZE + address_bytes + } + + /// Get the time until a new `Update` for the [`Subnet`] is received at the latest. + pub fn interval(&self) -> Duration { + // Interval is expressed as centiseconds on the wire. + Duration::from_millis(self.interval as u64 * 10) + } + + /// Construct an `Update` from wire bytes. + /// + /// # Panics + /// + /// This function will panic if there are insufficient bytes present in the provided buffer to + /// decode a complete `Update`. + pub fn from_bytes(src: &mut bytes::BytesMut, len: u8) -> Option { + let ae = src.get_u8(); + let flags = src.get_u8() & FLAG_MASK; + let plen = src.get_u8(); + // Read "omitted" value, we assume this is 0 + let _ = src.get_u8(); + let interval = src.get_u16(); + let seqno = src.get_u16().into(); + let metric = src.get_u16().into(); + let prefix_size = plen.div_ceil(8) as usize; + let prefix = match ae { + AE_WILDCARD => { + if prefix_size != 0 { + return None; + } + // TODO: this is a temporary placeholder until we figure out how to handle this + Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0).into() + } + AE_IPV4 => { + if plen > 32 { + return None; + } + let mut raw_ip = [0; 4]; + raw_ip[..prefix_size].copy_from_slice(&src[..prefix_size]); + src.advance(prefix_size); + Ipv4Addr::from(raw_ip).into() + } + AE_IPV6 => { + if plen > 128 { + return None; + } + let mut raw_ip = [0; 16]; + raw_ip[..prefix_size].copy_from_slice(&src[..prefix_size]); + src.advance(prefix_size); + Ipv6Addr::from(raw_ip).into() + } + AE_IPV6_LL => { + if plen != 64 { + return None; + } + let mut raw_ip = [0; 16]; + raw_ip[0] = 0xfe; + raw_ip[1] = 0x80; + raw_ip[8..].copy_from_slice(&src[..8]); + src.advance(8); + Ipv6Addr::from(raw_ip).into() + } + _ => { + // Invalid AE type, skip reamining data and ignore + trace!("Invalid AE type in update packet, drop packet"); + src.advance(len as usize - 10); + return None; + } + }; + + let subnet = Subnet::new(prefix, plen).ok()?; + + let mut router_id_bytes = [0u8; RouterId::BYTE_SIZE]; + router_id_bytes.copy_from_slice(&src[..RouterId::BYTE_SIZE]); + src.advance(RouterId::BYTE_SIZE); + + let router_id = RouterId::from(router_id_bytes); + + trace!("Read update tlv body"); + + Some(Update { + flags, + interval, + seqno, + metric, + subnet, + router_id, + }) + } + + /// Encode this `Update` tlv as part of a packet. + pub fn write_bytes(&self, dst: &mut bytes::BytesMut) { + dst.put_u8(match self.subnet.address() { + IpAddr::V4(_) => AE_IPV4, + IpAddr::V6(_) => AE_IPV6, + }); + dst.put_u8(self.flags); + dst.put_u8(self.subnet.prefix_len()); + // Write "omitted" value, currently not used in our encoding scheme. + dst.put_u8(0); + dst.put_u16(self.interval); + dst.put_u16(self.seqno.into()); + dst.put_u16(self.metric.into()); + let prefix_len = self.subnet.prefix_len().div_ceil(8) as usize; + match self.subnet.address() { + IpAddr::V4(ip) => dst.put_slice(&ip.octets()[..prefix_len]), + IpAddr::V6(ip) => dst.put_slice(&ip.octets()[..prefix_len]), + } + dst.put_slice(&self.router_id.as_bytes()[..]) + } +} + +#[cfg(test)] +mod tests { + use std::{ + net::{Ipv4Addr, Ipv6Addr}, + time::Duration, + }; + + use crate::{router_id::RouterId, subnet::Subnet}; + use bytes::Buf; + + #[test] + fn encoding() { + let mut buf = bytes::BytesMut::new(); + + let ihu = super::Update { + flags: 0b1100_0000, + interval: 400, + seqno: 17.into(), + metric: 25.into(), + subnet: Subnet::new(Ipv6Addr::new(512, 25, 26, 27, 28, 0, 0, 29).into(), 64) + .expect("64 is a valid IPv6 prefix size; qed"), + router_id: RouterId::from([1u8; RouterId::BYTE_SIZE]), + }; + + ihu.write_bytes(&mut buf); + + assert_eq!(buf.len(), 58); + assert_eq!( + buf[..58], + [ + 2, 192, 64, 0, 1, 144, 0, 17, 0, 25, 2, 0, 0, 25, 0, 26, 0, 27, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1 + ] + ); + + let mut buf = bytes::BytesMut::new(); + + let ihu = super::Update { + flags: 0b0000_0000, + interval: 600, + seqno: 170.into(), + metric: 256.into(), + subnet: Subnet::new(Ipv4Addr::new(10, 101, 4, 1).into(), 23) + .expect("23 is a valid IPv4 prefix size; qed"), + router_id: RouterId::from([2u8; RouterId::BYTE_SIZE]), + }; + + ihu.write_bytes(&mut buf); + + assert_eq!(buf.len(), 53); + assert_eq!( + buf[..53], + [ + 1, 0, 23, 0, 2, 88, 0, 170, 1, 0, 10, 101, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 + ] + ); + } + + #[test] + fn decoding() { + let mut buf = bytes::BytesMut::from( + &[ + 0, 64, 0, 0, 0, 100, 0, 70, 2, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + ][..], + ); + + let ihu = super::Update { + flags: 0b0100_0000, + interval: 100, + seqno: 70.into(), + metric: 512.into(), + subnet: Subnet::new(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0).into(), 0) + .expect("0 is a valid IPv6 prefix size; qed"), + router_id: RouterId::from([3u8; RouterId::BYTE_SIZE]), + }; + + let buf_len = buf.len(); + assert_eq!( + super::Update::from_bytes(&mut buf, buf_len as u8), + Some(ihu) + ); + assert_eq!(buf.remaining(), 0); + + let mut buf = bytes::BytesMut::from( + &[ + 3, 0, 64, 0, 3, 232, 0, 42, 3, 1, 0, 10, 0, 20, 0, 30, 0, 40, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, + ][..], + ); + + let ihu = super::Update { + flags: 0b0000_0000, + interval: 1000, + seqno: 42.into(), + metric: 769.into(), + subnet: Subnet::new(Ipv6Addr::new(0xfe80, 0, 0, 0, 10, 20, 30, 40).into(), 64) + .expect("92 is a valid IPv6 prefix size; qed"), + router_id: RouterId::from([4u8; RouterId::BYTE_SIZE]), + }; + + let buf_len = buf.len(); + assert_eq!( + super::Update::from_bytes(&mut buf, buf_len as u8), + Some(ihu) + ); + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn decode_ignores_invalid_ae_encoding() { + // AE 4 as it is the first one which should be used in protocol extension, causing this + // test to fail if we forget to update something + let mut buf = bytes::BytesMut::from( + &[ + 4, 0, 64, 0, 0, 44, 2, 0, 0, 10, 10, 5, 0, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + ][..], + ); + + let buf_len = buf.len(); + + assert_eq!(super::Update::from_bytes(&mut buf, buf_len as u8), None); + // Decode function should still consume the required amount of bytes to leave parser in a + // good state (assuming the length in the tlv preamble is good). + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn decode_ignores_invalid_flag_bits() { + // Set all flag bits, only allowed bits should be set on the decoded value + let mut buf = bytes::BytesMut::from( + &[ + 3, 255, 64, 0, 3, 232, 0, 42, 3, 1, 0, 10, 0, 20, 0, 30, 0, 40, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, + ][..], + ); + + let ihu = super::Update { + flags: super::UPDATE_FLAG_PREFIX | super::UPDATE_FLAG_ROUTER_ID, + interval: 1000, + seqno: 42.into(), + metric: 769.into(), + subnet: Subnet::new(Ipv6Addr::new(0xfe80, 0, 0, 0, 10, 20, 30, 40).into(), 64) + .expect("92 is a valid IPv6 prefix size; qed"), + router_id: RouterId::from([4u8; RouterId::BYTE_SIZE]), + }; + + let buf_len = buf.len(); + assert_eq!( + super::Update::from_bytes(&mut buf, buf_len as u8), + Some(ihu) + ); + assert_eq!(buf.remaining(), 0); + } + + #[test] + fn roundtrip() { + let mut buf = bytes::BytesMut::new(); + + let hello_src = super::Update::new( + Duration::from_secs(64), + 10.into(), + 25.into(), + Subnet::new( + Ipv6Addr::new(0x21f, 0x4025, 0xabcd, 0xdead, 0, 0, 0, 0).into(), + 64, + ) + .expect("64 is a valid IPv6 prefix size; qed"), + RouterId::from([6; RouterId::BYTE_SIZE]), + ); + hello_src.write_bytes(&mut buf); + let buf_len = buf.len(); + let decoded = super::Update::from_bytes(&mut buf, buf_len as u8); + + assert_eq!(Some(hello_src), decoded); + assert_eq!(buf.remaining(), 0); + } +} diff --git a/components/mycelium/mycelium/src/cdn.rs b/components/mycelium/mycelium/src/cdn.rs new file mode 100644 index 0000000..75ccf7b --- /dev/null +++ b/components/mycelium/mycelium/src/cdn.rs @@ -0,0 +1,338 @@ +use std::path::PathBuf; + +use aes_gcm::{aead::Aead, KeyInit}; +use axum::{ + extract::{Query, State}, + http::{HeaderMap, StatusCode}, + routing::get, + Router, +}; +use axum_extra::extract::Host; +use futures::{stream::FuturesUnordered, StreamExt}; +use reqwest::header::CONTENT_TYPE; +use tokio::net::TcpListener; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, warn}; + +/// Cdn functionality. Urls of specific format lead to donwnlaoding of metadata from the registry, +/// and serving of chunks. +pub struct Cdn { + cache: PathBuf, + cancel_token: CancellationToken, +} + +/// Cache for reconstructed blocks +#[derive(Clone)] +struct Cache { + base: PathBuf, +} + +impl Cdn { + pub fn new(cache: PathBuf) -> Self { + let cancel_token = CancellationToken::new(); + Self { + cache, + cancel_token, + } + } + + /// Start the Cdn server. This future runs until the server is stopped. + pub fn start(&self, listener: TcpListener) -> Result<(), Box> { + let state = Cache { + base: self.cache.clone(), + }; + + if !self.cache.exists() { + info!(dir = %self.cache.display(), "Creating cache dir"); + std::fs::create_dir(&self.cache)?; + } + + if !self.cache.is_dir() { + return Err("Cache dir is not a directory".into()); + } + + let router = Router::new().route("/", get(cdn)).with_state(state); + + let cancel_token = self.cancel_token.clone(); + + tokio::spawn(async { + axum::serve(listener, router) + .with_graceful_shutdown(cancel_token.cancelled_owned()) + .await + .map_err(|err| { + warn!(%err, "Cdn server error"); + }) + }); + + Ok(()) + } +} + +#[derive(Debug, serde::Deserialize)] +struct DecryptionKeyQuery { + key: Option, +} + +#[tracing::instrument(level = tracing::Level::DEBUG, skip(cache))] +async fn cdn( + Host(host): Host, + Query(query): Query, + State(cache): State, +) -> Result<(HeaderMap, Vec), StatusCode> { + debug!("Received request at {host}"); + let mut parts = host.split('.'); + let prefix = parts + .next() + .expect("Splitting a String always yields at least 1 result; Qed."); + if prefix.len() != 32 { + return Err(StatusCode::BAD_REQUEST); + } + + let mut hash = [0; 16]; + faster_hex::hex_decode(prefix.as_bytes(), &mut hash).map_err(|_| StatusCode::BAD_REQUEST)?; + + let registry_url = parts.collect::>().join("."); + + let decryption_key = if let Some(query_key) = query.key { + let mut key = [0; 16]; + faster_hex::hex_decode(query_key.as_bytes(), &mut key) + .map_err(|_| StatusCode::BAD_REQUEST)?; + Some(key) + } else { + None + }; + + let meta = load_meta(registry_url.clone(), hash, decryption_key).await?; + debug!("Metadata loaded"); + + let mut headers = HeaderMap::new(); + match meta { + cdn_meta::Metadata::File(file) => { + // + if let Some(mime) = file.mime { + debug!(%mime, "Setting mime type"); + headers.append( + CONTENT_TYPE, + mime.parse().map_err(|_| { + warn!("Not serving file with unprocessable mime type"); + StatusCode::UNPROCESSABLE_ENTITY + })?, + ); + } + + // File recombination + let mut content = vec![]; + for block in file.blocks { + content.extend_from_slice(cache.fetch_block(&block).await?.as_slice()); + } + Ok((headers, content)) + } + cdn_meta::Metadata::Directory(dir) => { + let mut out = r#" + + + + + + + +
    "# + .to_string(); + headers.append( + CONTENT_TYPE, + "text/html" + .parse() + .expect("Can parse \"text/html\" to content-type"), + ); + for (file_hash, encryption_key) in dir.files { + let meta = load_meta(registry_url.clone(), file_hash, encryption_key).await?; + let name = match meta { + cdn_meta::Metadata::File(file) => file.name, + cdn_meta::Metadata::Directory(dir) => dir.name, + }; + out.push_str(&format!( + "
  • {name}
  • \n", + faster_hex::hex_string(&file_hash), + &encryption_key + .map(|ek| faster_hex::hex_string(&ek)) + .unwrap_or_else(String::new), + )); + } + + out.push_str("
"); + Ok((headers, out.into())) + } + } +} + +/// Load a metadata blob from a metadata repository. +async fn load_meta( + registry_url: String, + hash: cdn_meta::Hash, + encryption_key: Option, +) -> Result { + let mut r_url = reqwest::Url::parse(&format!("http://{registry_url}")).map_err(|err| { + error!(%err, "Could not parse registry URL"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + let hex_hash = faster_hex::hex_string(&hash); + r_url.set_path(&format!("/api/v1/metadata/{hex_hash}")); + r_url.set_scheme("http").map_err(|_| { + error!("Could not set HTTP scheme"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + debug!(url = %r_url, "Fetching chunk"); + + let metadata_reply = reqwest::get(r_url).await.map_err(|err| { + error!(%err, "Could not load metadata from registry"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + // TODO: Should we just check if status code is success here? + if metadata_reply.status() != StatusCode::OK { + debug!( + status = %metadata_reply.status(), + "Registry replied with non-OK status code" + ); + return Err(metadata_reply.status()); + } + + let encrypted_metadata = metadata_reply.bytes().await.map_err(|err| { + error!(%err, "Could not load metadata response from registry"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let metadata = if let Some(encryption_key) = encryption_key { + if encrypted_metadata.len() < 12 { + debug!("Attempting to decrypt metadata with inufficient size"); + return Err(StatusCode::UNPROCESSABLE_ENTITY); + } + + let decryptor = aes_gcm::Aes128Gcm::new(&encryption_key.into()); + let plaintext = decryptor + .decrypt( + encrypted_metadata[encrypted_metadata.len() - 12..].into(), + &encrypted_metadata[..encrypted_metadata.len() - 12], + ) + .map_err(|_| { + warn!("Decryption of block failed"); + // Either the decryption key is wrong or the blob is corrupt, we assume the + // registry is not a fault so the decryption key is wrong, which is a user error. + StatusCode::UNPROCESSABLE_ENTITY + })?; + + plaintext + } else { + encrypted_metadata.into() + }; + + // If the metadata is not decodable, this is not really our fault, but also not the necessarily + // the users fault. + let (meta, consumed) = + cdn_meta::Metadata::from_binary(&metadata).map_err(|_| StatusCode::UNPROCESSABLE_ENTITY)?; + if consumed != metadata.len() { + warn!( + metadata_length = metadata.len(), + consumed, "Trailing binary metadata which wasn't decoded" + ); + } + + Ok(meta) +} + +impl Drop for Cdn { + fn drop(&mut self) { + self.cancel_token.cancel(); + } +} + +/// Download a shard from a 0-db. +async fn download_shard( + location: &cdn_meta::Location, + key: &[u8], +) -> Result, Box> { + let client = redis::Client::open(format!("redis://{}", location.host))?; + let mut con = client.get_multiplexed_async_connection().await?; + + redis::cmd("SELECT") + .arg(&location.namespace) + .query_async::<()>(&mut con) + .await?; + + Ok(redis::cmd("GET").arg(key).query_async(&mut con).await?) +} + +impl Cache { + async fn fetch_block(&self, block: &cdn_meta::Block) -> Result, StatusCode> { + let mut cached_file_path = self.base.clone(); + cached_file_path.push(faster_hex::hex_string(&block.encrypted_hash)); + // If we have the file in cache, just open it, load it, and return from there. + if cached_file_path.exists() { + return tokio::fs::read(&cached_file_path).await.map_err(|err| { + error!(%err, "Could not load cached file"); + StatusCode::INTERNAL_SERVER_ERROR + }); + } + + // File is not in cache, download and save + + // TODO: Rank based on expected latency + // FIXME: Only download the required amount + let mut shard_stream = block + .shards + .iter() + .enumerate() + .map(|(i, loc)| async move { (i, download_shard(loc, &block.encrypted_hash).await) }) + .collect::>(); + let mut shards = vec![None; block.shards.len()]; + while let Some((idx, shard)) = shard_stream.next().await { + let shard = shard.map_err(|err| { + warn!(err, "Could not load shard"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + shards[idx] = Some(shard); + } + // recombine + let encoder = reed_solomon_erasure::galois_8::ReedSolomon::new( + block.required_shards as usize, + block.shards.len() - block.required_shards as usize, + ) + .map_err(|err| { + error!(%err, "Failed to construct erausre codec"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + encoder.reconstruct_data(&mut shards).map_err(|err| { + error!(%err, "Shard recombination failed"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + // SAFETY: Since decoding was succesfull, the first shards (data shards) must be + // Option::Some + let mut encrypted_data = shards + .into_iter() + .map(Option::unwrap) + .take(block.required_shards as usize) + .flatten() + .collect::>(); + + let padding_len = encrypted_data[encrypted_data.len() - 1] as usize; + encrypted_data.resize(encrypted_data.len() - padding_len, 0); + + let decryptor = aes_gcm::Aes128Gcm::new(&block.content_hash.into()); + let c = decryptor + .decrypt(&block.nonce.into(), encrypted_data.as_slice()) + .map_err(|err| { + warn!(%err, "Decryption of content block failed"); + StatusCode::UNPROCESSABLE_ENTITY + })?; + + // Save file to cache, this is not critical if it fails + if let Err(err) = tokio::fs::write(&cached_file_path, &c).await { + warn!(%err, "Could not write block to cache"); + }; + + Ok(c) + } +} diff --git a/components/mycelium/mycelium/src/connection.rs b/components/mycelium/mycelium/src/connection.rs new file mode 100644 index 0000000..819cd7e --- /dev/null +++ b/components/mycelium/mycelium/src/connection.rs @@ -0,0 +1,158 @@ +use std::{io, net::SocketAddr, pin::Pin}; + +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, +}; + +mod tracked; +pub use tracked::Tracked; + +#[cfg(feature = "private-network")] +mod tls; + +/// Cost to add to the peer_link_cost for "local processing", when peers are connected over IPv6. +/// +/// The current peer link cost is calculated from a HELLO rtt. This is great to measure link +/// latency, since packets are processed in order. However, on local idle links, this value will +/// likely be 0 since we round down (from the amount of ms it took to process), which does not +/// accurately reflect the fact that there is in fact a cost associated with using a peer, even on +/// these local links. +const PACKET_PROCESSING_COST_IP6_TCP: u16 = 10; + +/// Cost to add to the peer_link_cost for "local processing", when peers are connected over IPv6. +/// +/// This is similar to [`PACKET_PROCESSING_COST_IP6`], but slightly higher so we skew towards IPv6 +/// connections if peers are connected over both IPv4 and IPv6. +const PACKET_PROCESSING_COST_IP4_TCP: u16 = 15; + +// TODO +const PACKET_PROCESSING_COST_IP6_QUIC: u16 = 7; +// TODO +const PACKET_PROCESSING_COST_IP4_QUIC: u16 = 12; + +pub trait Connection: AsyncRead + AsyncWrite { + /// Get an identifier for this connection, which shows details about the remote + fn identifier(&self) -> Result; + + /// The static cost of using this connection + fn static_link_cost(&self) -> Result; +} + +/// A wrapper around a quic send and quic receive stream, implementing the [`Connection`] trait. +pub struct Quic { + tx: quinn::SendStream, + rx: quinn::RecvStream, + remote: SocketAddr, +} + +impl Quic { + /// Create a new wrapper around Quic streams. + pub fn new(tx: quinn::SendStream, rx: quinn::RecvStream, remote: SocketAddr) -> Self { + Quic { tx, rx, remote } + } +} + +impl Connection for TcpStream { + fn identifier(&self) -> Result { + Ok(format!( + "TCP {} <-> {}", + self.local_addr()?, + self.peer_addr()? + )) + } + + fn static_link_cost(&self) -> Result { + Ok(match self.peer_addr()? { + SocketAddr::V4(_) => PACKET_PROCESSING_COST_IP4_TCP, + SocketAddr::V6(ip) if ip.ip().to_ipv4_mapped().is_some() => { + PACKET_PROCESSING_COST_IP4_TCP + } + SocketAddr::V6(_) => PACKET_PROCESSING_COST_IP6_TCP, + }) + } +} + +impl AsyncRead for Quic { + #[inline] + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + Pin::new(&mut self.rx).poll_read(cx, buf) + } +} + +impl AsyncWrite for Quic { + #[inline] + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + Pin::new(&mut self.tx) + .poll_write(cx, buf) + .map_err(From::from) + } + + #[inline] + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Pin::new(&mut self.tx).poll_flush(cx) + } + + #[inline] + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Pin::new(&mut self.tx).poll_shutdown(cx) + } + + #[inline] + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> std::task::Poll> { + Pin::new(&mut self.tx).poll_write_vectored(cx, bufs) + } + + #[inline] + fn is_write_vectored(&self) -> bool { + self.tx.is_write_vectored() + } +} + +impl Connection for Quic { + fn identifier(&self) -> Result { + Ok(format!("QUIC -> {}", self.remote)) + } + + fn static_link_cost(&self) -> Result { + Ok(match self.remote { + SocketAddr::V4(_) => PACKET_PROCESSING_COST_IP4_QUIC, + SocketAddr::V6(ip) if ip.ip().to_ipv4_mapped().is_some() => { + PACKET_PROCESSING_COST_IP4_QUIC + } + SocketAddr::V6(_) => PACKET_PROCESSING_COST_IP6_QUIC, + }) + } +} + +#[cfg(test)] +use tokio::io::DuplexStream; + +#[cfg(test)] +impl Connection for DuplexStream { + fn identifier(&self) -> Result { + Ok("Memory pipe".to_string()) + } + + fn static_link_cost(&self) -> Result { + Ok(1) + } +} diff --git a/components/mycelium/mycelium/src/connection/tls.rs b/components/mycelium/mycelium/src/connection/tls.rs new file mode 100644 index 0000000..a73571e --- /dev/null +++ b/components/mycelium/mycelium/src/connection/tls.rs @@ -0,0 +1,23 @@ +use std::{io, net::SocketAddr}; + +use tokio::net::TcpStream; + +impl super::Connection for tokio_openssl::SslStream { + fn identifier(&self) -> Result { + Ok(format!( + "TLS {} <-> {}", + self.get_ref().local_addr()?, + self.get_ref().peer_addr()? + )) + } + + fn static_link_cost(&self) -> Result { + Ok(match self.get_ref().peer_addr()? { + SocketAddr::V4(_) => super::PACKET_PROCESSING_COST_IP4_TCP, + SocketAddr::V6(ip) if ip.ip().to_ipv4_mapped().is_some() => { + super::PACKET_PROCESSING_COST_IP4_TCP + } + SocketAddr::V6(_) => super::PACKET_PROCESSING_COST_IP6_TCP, + }) + } +} diff --git a/components/mycelium/mycelium/src/connection/tracked.rs b/components/mycelium/mycelium/src/connection/tracked.rs new file mode 100644 index 0000000..74f12f5 --- /dev/null +++ b/components/mycelium/mycelium/src/connection/tracked.rs @@ -0,0 +1,120 @@ +use std::{ + pin::Pin, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + task::Poll, +}; + +use tokio::io::{AsyncRead, AsyncWrite}; + +use super::Connection; + +/// Wrapper which keeps track of how much bytes have been read and written from a connection. +pub struct Tracked { + /// Bytes read counter + read: Arc, + /// Bytes written counter + write: Arc, + /// Underlying connection we are measuring + con: C, +} + +impl Tracked +where + C: Connection + Unpin, +{ + /// Create a new instance of a tracked connections. Counters are passed in so they can be + /// reused accross connections. + pub fn new(read: Arc, write: Arc, con: C) -> Self { + Self { read, write, con } + } +} + +impl Connection for Tracked +where + C: Connection + Unpin, +{ + #[inline] + fn identifier(&self) -> Result { + self.con.identifier() + } + + #[inline] + fn static_link_cost(&self) -> Result { + self.con.static_link_cost() + } +} + +impl AsyncRead for Tracked +where + C: AsyncRead + Unpin, +{ + #[inline] + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + let start_len = buf.filled().len(); + let res = Pin::new(&mut self.con).poll_read(cx, buf); + if let Poll::Ready(Ok(())) = res { + self.read + .fetch_add((buf.filled().len() - start_len) as u64, Ordering::Relaxed); + } + res + } +} + +impl AsyncWrite for Tracked +where + C: AsyncWrite + Unpin, +{ + #[inline] + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + let res = Pin::new(&mut self.con).poll_write(cx, buf); + if let Poll::Ready(Ok(written)) = res { + self.write.fetch_add(written as u64, Ordering::Relaxed); + } + res + } + + #[inline] + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut self.con).poll_flush(cx) + } + + #[inline] + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut self.con).poll_shutdown(cx) + } + + #[inline] + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + let res = Pin::new(&mut self.con).poll_write_vectored(cx, bufs); + if let Poll::Ready(Ok(written)) = res { + self.write.fetch_add(written as u64, Ordering::Relaxed); + } + res + } + + #[inline] + fn is_write_vectored(&self) -> bool { + self.con.is_write_vectored() + } +} diff --git a/components/mycelium/mycelium/src/crypto.rs b/components/mycelium/mycelium/src/crypto.rs new file mode 100644 index 0000000..affd07d --- /dev/null +++ b/components/mycelium/mycelium/src/crypto.rs @@ -0,0 +1,450 @@ +//! Abstraction over diffie hellman, symmetric encryption, and hashing. + +use core::fmt; +use std::{ + error::Error, + fmt::Display, + net::Ipv6Addr, + ops::{Deref, DerefMut}, +}; + +use aes_gcm::{aead::OsRng, AeadCore, AeadInPlace, Aes256Gcm, Key, KeyInit}; +use serde::{de::Visitor, Deserialize, Serialize}; + +/// Default MTU for a packet. Ideally this would not be needed and the [`PacketBuffer`] takes a +/// const generic argument which is then expanded with the needed extra space for the buffer, +/// however as it stands const generics can only be used standalone and not in a constant +/// expression. This _is_ possible on nightly rust, with a feature gate (generic_const_exprs). +const PACKET_SIZE: usize = 1400; + +/// Size of an AES_GCM tag in bytes. +const AES_TAG_SIZE: usize = 16; + +/// Size of an AES_GCM nonce in bytes. +const AES_NONCE_SIZE: usize = 12; + +/// Size of user defined data header. This header will be part of the encrypted data. +const DATA_HEADER_SIZE: usize = 4; + +/// Size of a `PacketBuffer`. +const PACKET_BUFFER_SIZE: usize = PACKET_SIZE + AES_TAG_SIZE + AES_NONCE_SIZE + DATA_HEADER_SIZE; + +/// A public key used as part of Diffie Hellman key exchange. It is derived from a [`SecretKey`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct PublicKey(x25519_dalek::PublicKey); + +/// A secret used as part of Diffie Hellman key exchange. +/// +/// This type intentionally does not implement or derive [`Debug`] to avoid accidentally leaking +/// secrets in logs. +#[derive(Clone)] +pub struct SecretKey(x25519_dalek::StaticSecret); + +/// A statically computed secret from a [`SecretKey`] and a [`PublicKey`]. +/// +/// This type intentionally does not implement or derive [`Debug`] to avoid accidentally leaking +/// secrets in logs. +#[derive(Clone)] +pub struct SharedSecret([u8; 32]); + +/// A buffer for packets. This holds enough space to encrypt a packet in place without +/// reallocating. +/// +/// Internally, the buffer is created with an additional header. Because this header is part of the +/// encrypted content, it is not included in the global version set by the main packet header. As +/// such, an internal version is included. +pub struct PacketBuffer { + buf: Vec, + /// Amount of bytes written in the buffer + size: usize, +} + +/// A reference to the header in a [`PacketBuffer`]. +pub struct PacketBufferHeader<'a> { + data: &'a [u8; DATA_HEADER_SIZE], +} + +/// A mutable reference to the header in a [`PacketBuffer`]. +pub struct PacketBufferHeaderMut<'a> { + data: &'a mut [u8; DATA_HEADER_SIZE], +} + +/// Opaque type indicating decryption failed. +#[derive(Debug, Clone, Copy)] +pub struct DecryptionError; + +impl Display for DecryptionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Decryption failed, invalid or insufficient encrypted content for this key") + } +} + +impl Error for DecryptionError {} + +impl SecretKey { + /// Generate a new `StaticSecret` using [`OsRng`] as an entropy source. + pub fn new() -> Self { + SecretKey(x25519_dalek::StaticSecret::random_from_rng(OsRng)) + } + + /// View this `SecretKey` as a byte array. + #[inline] + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_bytes() + } + + /// Computes the [`SharedSecret`] from this `SecretKey` and a [`PublicKey`]. + pub fn shared_secret(&self, other: &PublicKey) -> SharedSecret { + SharedSecret(self.0.diffie_hellman(&other.0).to_bytes()) + } +} + +impl Default for SecretKey { + fn default() -> Self { + Self::new() + } +} + +impl PublicKey { + /// Generates an [`Ipv6Addr`] from a `PublicKey`. + /// + /// The generated address is guaranteed to be part of the `400::/7` range. + pub fn address(&self) -> Ipv6Addr { + let mut hasher = blake3::Hasher::new(); + hasher.update(self.as_bytes()); + let mut buf = [0; 16]; + hasher.finalize_xof().fill(&mut buf); + // Mangle the first byte to be of the expected form. Because of the network range + // requirement, we MUST set the third bit, and MAY set the last bit. Instead of discarding + // the first 7 bits of the hash, use the first byte to determine if the last bit is set. + // If there is an odd number of bits set in the first byte, set the last bit of the result. + let lsb = buf[0].count_ones() as u8 % 2; + buf[0] = 0x04 | lsb; + Ipv6Addr::from(buf) + } + + /// Convert this `PublicKey` to a byte array. + pub fn to_bytes(self) -> [u8; 32] { + self.0.to_bytes() + } + + /// View this `PublicKey` as a byte array. + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_bytes() + } +} + +impl SharedSecret { + /// Encrypt a [`PacketBuffer`] using the `SharedSecret` as key. + /// + /// Internally, a new random nonce will be generated using the OS's crypto rng generator. This + /// nonce is appended to the encrypted data. + pub fn encrypt(&self, mut data: PacketBuffer) -> Vec { + let key: Key = self.0.into(); + let nonce = Aes256Gcm::generate_nonce(OsRng); + + let cipher = Aes256Gcm::new(&key); + let tag = cipher + .encrypt_in_place_detached(&nonce, &[], &mut data.buf[..data.size]) + .expect("Encryption can't fail; qed."); + + data.buf[data.size..data.size + AES_TAG_SIZE].clone_from_slice(tag.as_slice()); + data.buf[data.size + AES_TAG_SIZE..data.size + AES_TAG_SIZE + AES_NONCE_SIZE] + .clone_from_slice(&nonce); + + data.buf.truncate(data.size + AES_NONCE_SIZE + AES_TAG_SIZE); + + data.buf + } + + /// Decrypt a message previously encrypted with an equivalent `SharedSecret`. In other words, a + /// message that was previously created by the [`SharedSecret::encrypt`] method. + /// + /// Internally, this messages assumes that a 12 byte nonce is present at the end of the data. + /// If the passed in data to decrypt does not contain a valid nonce, decryption fails and an + /// opaque error is returned. As an extension to this, if the data is not of sufficient length + /// to contain a valid nonce, an error is returned immediately. + pub fn decrypt(&self, mut data: Vec) -> Result { + // Make sure we have sufficient data (i.e. a nonce). + if data.len() < AES_NONCE_SIZE + AES_TAG_SIZE + DATA_HEADER_SIZE { + return Err(DecryptionError); + } + + let data_len = data.len(); + + let key: Key = self.0.into(); + { + let (data, nonce) = data.split_at_mut(data_len - AES_NONCE_SIZE); + let (data, tag) = data.split_at_mut(data.len() - AES_TAG_SIZE); + + let cipher = Aes256Gcm::new(&key); + cipher + .decrypt_in_place_detached((&*nonce).into(), &[], data, (&*tag).into()) + .map_err(|_| DecryptionError)?; + } + + Ok(PacketBuffer { + // We did not remove the scratch space used for TAG and NONCE. + size: data.len() - AES_TAG_SIZE - AES_NONCE_SIZE, + buf: data, + }) + } +} + +impl PacketBuffer { + /// Create a new blank `PacketBuffer`. + pub fn new() -> Self { + Self { + buf: vec![0; PACKET_BUFFER_SIZE], + size: 0, + } + } + + /// Get a reference to the packet header. + pub fn header(&self) -> PacketBufferHeader<'_> { + PacketBufferHeader { + data: self.buf[..DATA_HEADER_SIZE] + .try_into() + .expect("Header size constant is correct; qed"), + } + } + + /// Get a mutable reference to the packet header. + pub fn header_mut(&mut self) -> PacketBufferHeaderMut<'_> { + PacketBufferHeaderMut { + data: <&mut [u8] as TryInto<&mut [u8; DATA_HEADER_SIZE]>>::try_into( + &mut self.buf[..DATA_HEADER_SIZE], + ) + .expect("Header size constant is correct; qed"), + } + } + + /// Get a reference to the entire useable inner buffer. + pub fn buffer(&self) -> &[u8] { + let buf_end = self.buf.len() - AES_NONCE_SIZE - AES_TAG_SIZE; + &self.buf[DATA_HEADER_SIZE..buf_end] + } + + /// Get a mutable reference to the entire useable internal buffer. + pub fn buffer_mut(&mut self) -> &mut [u8] { + let buf_end = self.buf.len() - AES_NONCE_SIZE - AES_TAG_SIZE; + &mut self.buf[DATA_HEADER_SIZE..buf_end] + } + + /// Sets the amount of bytes in use by the buffer. + pub fn set_size(&mut self, size: usize) { + self.size = size + DATA_HEADER_SIZE; + } +} + +impl Default for PacketBuffer { + fn default() -> Self { + Self::new() + } +} + +impl From<[u8; 32]> for SecretKey { + /// Load a secret key from a byte array. + fn from(bytes: [u8; 32]) -> SecretKey { + SecretKey(x25519_dalek::StaticSecret::from(bytes)) + } +} + +impl fmt::Display for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&faster_hex::hex_string(self.as_bytes())) + } +} + +impl Serialize for PublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&faster_hex::hex_string(self.as_bytes())) + } +} + +struct PublicKeyVisitor; +impl Visitor<'_> for PublicKeyVisitor { + type Value = PublicKey; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("A hex encoded public key (64 characters)") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if v.len() != 64 { + Err(E::custom("Public key is 64 characters long")) + } else { + let mut backing = [0; 32]; + faster_hex::hex_decode(v.as_bytes(), &mut backing) + .map_err(|_| E::custom("PublicKey is not valid hex"))?; + Ok(PublicKey(backing.into())) + } + } +} + +impl<'de> Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(PublicKeyVisitor) + } +} + +impl From<[u8; 32]> for PublicKey { + /// Given a byte array, construct a `PublicKey`. + fn from(bytes: [u8; 32]) -> PublicKey { + PublicKey(x25519_dalek::PublicKey::from(bytes)) + } +} + +impl TryFrom<&str> for PublicKey { + type Error = faster_hex::Error; + + fn try_from(value: &str) -> Result { + let mut output = [0u8; 32]; + faster_hex::hex_decode(value.as_bytes(), &mut output)?; + Ok(PublicKey::from(output)) + } +} + +impl From<&SecretKey> for PublicKey { + fn from(value: &SecretKey) -> Self { + PublicKey(x25519_dalek::PublicKey::from(&value.0)) + } +} + +impl Deref for SharedSecret { + type Target = [u8; 32]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Deref for PacketBuffer { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.buf[DATA_HEADER_SIZE..self.size] + } +} + +impl Deref for PacketBufferHeader<'_> { + type Target = [u8; DATA_HEADER_SIZE]; + + fn deref(&self) -> &Self::Target { + self.data + } +} + +impl Deref for PacketBufferHeaderMut<'_> { + type Target = [u8; DATA_HEADER_SIZE]; + + fn deref(&self) -> &Self::Target { + self.data + } +} + +impl DerefMut for PacketBufferHeaderMut<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.data + } +} + +impl fmt::Debug for PacketBuffer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PacketBuffer") + .field("data", &"...") + .field("len", &self.size) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::{PacketBuffer, SecretKey, AES_NONCE_SIZE, AES_TAG_SIZE, DATA_HEADER_SIZE}; + + #[test] + /// Test if encryption works in general. We just create some random value and encrypt it. + /// Specifically, this will help to catch runtime panics in case AES_TAG_SIZE or AES_NONCE_SIZE + /// don't have a proper value aligned with the underlying AES_GCM implementation. + fn encryption_succeeds() { + let k1 = SecretKey::new(); + let k2 = SecretKey::new(); + + let ss = k1.shared_secret(&(&k2).into()); + + let mut pb = PacketBuffer::new(); + let data = b"vnno30nv f654q364 vfsv 44"; // Random keyboard smash. + + pb.buffer_mut()[..data.len()].copy_from_slice(data); + pb.set_size(data.len()); + + // We only care that this does not panic. + let res = ss.encrypt(pb); + // At the same time, check expected size. + assert_eq!( + res.len(), + data.len() + DATA_HEADER_SIZE + AES_TAG_SIZE + AES_NONCE_SIZE + ); + } + + #[test] + /// Encrypt a value and then decrypt it. This makes sure the decrypt flow and encrypt flow + /// match, and both follow the expected format. Also, we don't reuse the shared secret for + /// decryption, but instead generate the secret again the other way round, to simulate a remote + /// node. + fn encrypt_decrypt_roundtrip() { + let k1 = SecretKey::new(); + let k2 = SecretKey::new(); + + let ss1 = k1.shared_secret(&(&k2).into()); + let ss2 = k2.shared_secret(&(&k1).into()); + + // This assertion is not strictly necessary as it will be checked below implicitly. + assert_eq!(ss1.as_slice(), ss2.as_slice()); + + let data = b"dsafjiqjo23 u2953u8 3oid fjo321j"; + let mut pb = PacketBuffer::new(); + + pb.buffer_mut()[..data.len()].copy_from_slice(data); + pb.set_size(data.len()); + + let res = ss1.encrypt(pb); + + let original = ss2.decrypt(res).expect("Decryption works"); + + assert_eq!(&*original, &data[..]); + } + + #[test] + /// Test if PacketBufferHeaderMut actually modifies the PacketBuffer storage. + fn modify_header() { + let mut pb = PacketBuffer::new(); + let mut header = pb.header_mut(); + + header[0] = 1; + header[1] = 2; + header[2] = 3; + header[3] = 4; + + assert_eq!(pb.buf[..DATA_HEADER_SIZE], [1, 2, 3, 4]); + } + + #[test] + /// Verify [`PacketBuffer::buffer`] and [`PacketBuffer::buffer_mut`] actually have the + /// appropriate size. + fn buffer_mapping() { + let mut pb = PacketBuffer::new(); + + assert_eq!(pb.buffer().len(), super::PACKET_SIZE); + assert_eq!(pb.buffer_mut().len(), super::PACKET_SIZE); + } +} diff --git a/components/mycelium/mycelium/src/data.rs b/components/mycelium/mycelium/src/data.rs new file mode 100644 index 0000000..21ec08e --- /dev/null +++ b/components/mycelium/mycelium/src/data.rs @@ -0,0 +1,487 @@ +use std::net::{IpAddr, Ipv6Addr}; + +use etherparse::{ + icmpv6::{DestUnreachableCode, TimeExceededCode}, + Icmpv6Type, PacketBuilder, +}; +use futures::{Sink, SinkExt, Stream, StreamExt}; +use tokio::sync::mpsc::UnboundedReceiver; +use tracing::{debug, error, trace, warn}; + +use crate::{crypto::PacketBuffer, metrics::Metrics, packet::DataPacket, router::Router}; + +/// Current version of the user data header. +const USER_DATA_VERSION: u8 = 1; + +/// Type value indicating L3 data in the user data header. +const USER_DATA_L3_TYPE: u8 = 0; + +/// Type value indicating a user message in the data header. +const USER_DATA_MESSAGE_TYPE: u8 = 1; + +/// Type value indicating an ICMP packet not returned as regular IPv6 traffic. This is needed when +/// intermediate nodes send back icmp data, as the original data is encrypted. +const USER_DATA_OOB_ICMP: u8 = 2; + +/// Minimum size in bytes of an IPv6 header. +const IPV6_MIN_HEADER_SIZE: usize = 40; + +/// Size of an ICMPv6 header. +const ICMP6_HEADER_SIZE: usize = 8; + +/// Minimum MTU for IPV6 according to https://www.rfc-editor.org/rfc/rfc8200#section-5. +/// For ICMP, the packet must not be greater than this value. This is specified in +/// https://datatracker.ietf.org/doc/html/rfc4443#section-2.4, section (c). +const MIN_IPV6_MTU: usize = 1280; + +/// Mask applied to the first byte of an IP header to extract the version. +const IP_VERSION_MASK: u8 = 0b1111_0000; + +/// Version byte of an IP header indicating IPv6. Since the version is only 4 bits, the lower bits +/// must be masked first. +const IPV6_VERSION_BYTE: u8 = 0b0110_0000; + +/// Default hop limit for message packets. For now this is set to 64 hops. +/// +/// For regular l3 packets, we copy the hop limit from the packet itself. We can't do that here, so +/// 64 is used as sane default. +const MESSAGE_HOP_LIMIT: u8 = 64; + +/// The DataPlane manages forwarding/receiving of local data packets to the [`Router`], and the +/// encryption/decryption of them. +/// +/// DataPlane itself can be cloned, but this is not cheap on the router and should be avoided. +pub struct DataPlane { + router: Router, +} + +impl DataPlane +where + M: Metrics + Clone + Send + 'static, +{ + /// Create a new `DataPlane` using the given [`Router`] for packet handling. + /// + /// `l3_packet_stream` is a stream of l3 packets from the host, usually read from a TUN interface. + /// `l3_packet_sink` is a sink for l3 packets received from a romte, usually send to a TUN interface, + pub fn new( + router: Router, + l3_packet_stream: S, + l3_packet_sink: T, + message_packet_sink: U, + host_packet_source: UnboundedReceiver, + ) -> Self + where + S: Stream> + Send + Unpin + 'static, + T: Sink + Clone + Send + Unpin + 'static, + T::Error: std::fmt::Display, + U: Sink<(PacketBuffer, IpAddr, IpAddr)> + Send + Unpin + 'static, + U::Error: std::fmt::Display, + { + let dp = Self { router }; + + tokio::spawn( + dp.clone() + .inject_l3_packet_loop(l3_packet_stream, l3_packet_sink.clone()), + ); + tokio::spawn(dp.clone().extract_packet_loop( + l3_packet_sink, + message_packet_sink, + host_packet_source, + )); + + dp + } + + /// Get a reference to the [`Router`] used. + pub fn router(&self) -> &Router { + &self.router + } + + async fn inject_l3_packet_loop(self, mut l3_packet_stream: S, mut l3_packet_sink: T) + where + // TODO: no result + // TODO: should IP extraction be handled higher up? + S: Stream> + Send + Unpin + 'static, + T: Sink + Clone + Send + Unpin + 'static, + T::Error: std::fmt::Display, + { + let node_subnet = self.router.node_tun_subnet(); + + while let Some(packet) = l3_packet_stream.next().await { + let mut packet = match packet { + Err(e) => { + error!("Failed to read packet from TUN interface {e}"); + continue; + } + Ok(packet) => packet, + }; + + trace!("Received packet from tun"); + + // Parse an IPv6 header. We don't care about the full header in reality. What we want + // to know is: + // - This is an IPv6 header + // - Hop limit + // - Source address + // - Destination address + // This translates to the following requirements: + // - at least 40 bytes of data, as that is the minimum size of an IPv6 header + // - first 4 bits (version) are the constant 6 (0b0110) + // - src is byte 9-24 (8-23 0 indexed). + // - dst is byte 25-40 (24-39 0 indexed). + + if packet.len() < IPV6_MIN_HEADER_SIZE { + trace!("Packet can't contain an IPv6 header"); + continue; + } + + if packet[0] & IP_VERSION_MASK != IPV6_VERSION_BYTE { + trace!("Packet is not IPv6"); + continue; + } + + let hop_limit = u8::from_be_bytes([packet[7]]); + + let src_ip = Ipv6Addr::from( + <&[u8] as TryInto<[u8; 16]>>::try_into(&packet[8..24]) + .expect("Static range bounds on slice are correct length"), + ); + let dst_ip = Ipv6Addr::from( + <&[u8] as TryInto<[u8; 16]>>::try_into(&packet[24..40]) + .expect("Static range bounds on slice are correct length"), + ); + + // If this is a packet for our own Subnet, it means there is no local configuration for + // the destination ip or /64 subnet, and the IP is unreachable + if node_subnet.contains_ip(dst_ip.into()) { + trace!( + "Replying to local packet for unexisting address: {}", + dst_ip + ); + + let mut icmp_packet = PacketBuffer::new(); + let host = self.router.node_public_key().address().octets(); + let icmp = PacketBuilder::ipv6(host, src_ip.octets(), 64).icmpv6( + Icmpv6Type::DestinationUnreachable(DestUnreachableCode::Address), + ); + icmp_packet.set_size(icmp.size(packet.len().min(1280 - 48))); + let mut writer = &mut icmp_packet.buffer_mut()[..]; + if let Err(e) = icmp.write(&mut writer, &packet[..packet.len().min(1280 - 48)]) { + error!("Failed to construct ICMP packet: {e}"); + continue; + } + if let Err(e) = l3_packet_sink.send(icmp_packet).await { + error!("Failed to send ICMP packet to host: {e}"); + } + continue; + } + + trace!("Received packet from TUN with dest addr: {:?}", dst_ip); + // Check if the source address is part of 400::/7 + let first_src_byte = src_ip.segments()[0] >> 8; + if !(0x04..0x06).contains(&first_src_byte) { + let mut icmp_packet = PacketBuffer::new(); + let host = self.router.node_public_key().address().octets(); + let icmp = PacketBuilder::ipv6(host, src_ip.octets(), 64).icmpv6( + Icmpv6Type::DestinationUnreachable( + DestUnreachableCode::SourceAddressFailedPolicy, + ), + ); + icmp_packet.set_size(icmp.size(packet.len().min(1280 - 48))); + let mut writer = &mut icmp_packet.buffer_mut()[..]; + if let Err(e) = icmp.write(&mut writer, &packet[..packet.len().min(1280 - 48)]) { + error!("Failed to construct ICMP packet: {e}"); + continue; + } + if let Err(e) = l3_packet_sink.send(icmp_packet).await { + error!("Failed to send ICMP packet to host: {e}"); + } + continue; + } + + // No need to verify destination address, if it is not part of the global subnet there + // should not be a route for it, and therefore the route step will generate the + // appropriate ICMP. + + let mut header = packet.header_mut(); + header[0] = USER_DATA_VERSION; + header[1] = USER_DATA_L3_TYPE; + + if let Some(icmp) = self.encrypt_and_route_packet(src_ip, dst_ip, hop_limit, packet) { + if let Err(e) = l3_packet_sink.send(icmp).await { + error!("Could not forward icmp packet back to TUN interface {e}"); + } + } + } + + warn!("Data inject loop from host to router ended"); + } + + /// Inject a new packet where the content is a `message` fragment. + pub fn inject_message_packet( + &self, + src_ip: Ipv6Addr, + dst_ip: Ipv6Addr, + mut packet: PacketBuffer, + ) { + let mut header = packet.header_mut(); + header[0] = USER_DATA_VERSION; + header[1] = USER_DATA_MESSAGE_TYPE; + + self.encrypt_and_route_packet(src_ip, dst_ip, MESSAGE_HOP_LIMIT, packet); + } + + /// Encrypt the content of a packet based on the destination key, and then inject the packet + /// into the [`Router`] for processing. + /// + /// If no key exists for the destination, the content can'be encrypted, the packet is not injected + /// into the router, and a packet is returned containing an ICMP packet. Note that a return + /// value of [`Option::None`] does not mean the packet was successfully forwarded; + fn encrypt_and_route_packet( + &self, + src_ip: Ipv6Addr, + dst_ip: Ipv6Addr, + hop_limit: u8, + packet: PacketBuffer, + ) -> Option { + // If the packet only has a TTL of 1, we won't be able to route it to the destination + // regardless, so just reply with an unencrypted TTL exceeded ICMP. + if hop_limit < 2 { + debug!( + packet.ttl = hop_limit, + packet.src = %src_ip, + packet.dst = %dst_ip, + "Attempting to route packet with insufficient TTL", + ); + + let mut pb = PacketBuffer::new(); + // From self to self + let icmp = PacketBuilder::ipv6(src_ip.octets(), src_ip.octets(), hop_limit) + .icmpv6(Icmpv6Type::TimeExceeded(TimeExceededCode::HopLimitExceeded)); + // Scale to max size if needed + let orig_buf_end = packet + .buffer() + .len() + .min(MIN_IPV6_MTU - IPV6_MIN_HEADER_SIZE - ICMP6_HEADER_SIZE); + pb.set_size(icmp.size(orig_buf_end)); + let mut b = pb.buffer_mut(); + if let Err(e) = icmp.write(&mut b, &packet.buffer()[..orig_buf_end]) { + error!("Failed to construct time exceeded ICMP packet {e}"); + return None; + } + + return Some(pb); + } + + // Get shared secret from node and dest address + let shared_secret = match self.router.get_shared_secret_if_selected(dst_ip.into()) { + Some(ss) => ss, + // If we don't have a route to the destination subnet, reply with ICMP no route to + // host. Do this here as well to avoid encrypting the ICMP to ourselves. + None => { + debug!( + packet.src = %src_ip, + packet.dst = %dst_ip, + "No entry found for destination address, dropping packet", + ); + + let mut pb = PacketBuffer::new(); + // From self to self + let icmp = PacketBuilder::ipv6(src_ip.octets(), src_ip.octets(), hop_limit).icmpv6( + Icmpv6Type::DestinationUnreachable(DestUnreachableCode::NoRoute), + ); + // Scale to max size if needed + let orig_buf_end = packet + .buffer() + .len() + .min(MIN_IPV6_MTU - IPV6_MIN_HEADER_SIZE - ICMP6_HEADER_SIZE); + pb.set_size(icmp.size(orig_buf_end)); + let mut b = pb.buffer_mut(); + if let Err(e) = icmp.write(&mut b, &packet.buffer()[..orig_buf_end]) { + error!("Failed to construct no route to host ICMP packet {e}"); + return None; + } + + return Some(pb); + } + }; + + self.router.route_packet(DataPacket { + dst_ip, + src_ip, + hop_limit, + raw_data: shared_secret.encrypt(packet), + }); + + None + } + + async fn extract_packet_loop( + self, + mut l3_packet_sink: T, + mut message_packet_sink: U, + mut host_packet_source: UnboundedReceiver, + ) where + T: Sink + Send + Unpin + 'static, + T::Error: std::fmt::Display, + U: Sink<(PacketBuffer, IpAddr, IpAddr)> + Send + Unpin + 'static, + U::Error: std::fmt::Display, + { + while let Some(data_packet) = host_packet_source.recv().await { + // decrypt & send to TUN interface + let shared_secret = if let Some(ss) = self + .router + .get_shared_secret_from_dest(data_packet.src_ip.into()) + { + ss + } else { + trace!("Received packet from unknown sender"); + continue; + }; + let mut decrypted_packet = match shared_secret.decrypt(data_packet.raw_data) { + Ok(data) => data, + Err(_) => { + debug!("Dropping data packet with invalid encrypted content"); + continue; + } + }; + + // Check header + let header = decrypted_packet.header(); + if header[0] != USER_DATA_VERSION { + trace!("Dropping decrypted packet with unknown header version"); + continue; + } + + // Route based on packet type. + match header[1] { + USER_DATA_L3_TYPE => { + let real_packet = decrypted_packet.buffer_mut(); + if real_packet.len() < IPV6_MIN_HEADER_SIZE { + debug!( + "Decrypted packet is too short, can't possibly be a valid IPv6 packet" + ); + continue; + } + // Adjust the hop limit in the decrypted packet to the new value. + real_packet[7] = data_packet.hop_limit; + if let Err(e) = l3_packet_sink.send(decrypted_packet).await { + error!("Failed to send packet on local TUN interface: {e}",); + continue; + } + } + USER_DATA_MESSAGE_TYPE => { + if let Err(e) = message_packet_sink + .send(( + decrypted_packet, + IpAddr::V6(data_packet.src_ip), + IpAddr::V6(data_packet.dst_ip), + )) + .await + { + error!("Failed to send packet to message handler: {e}",); + continue; + } + } + USER_DATA_OOB_ICMP => { + let real_packet = &*decrypted_packet; + if real_packet.len() < IPV6_MIN_HEADER_SIZE + ICMP6_HEADER_SIZE + 16 { + debug!( + "Decrypted packet is too short, can't possibly be a valid IPv6 ICMP packet" + ); + continue; + } + if real_packet.len() > MIN_IPV6_MTU + 16 { + debug!("Discarding ICMP packet which is too large"); + continue; + } + + let dec_ip = Ipv6Addr::from( + <&[u8] as TryInto<[u8; 16]>>::try_into(&real_packet[..16]).unwrap(), + ); + trace!("ICMP for original target {dec_ip}"); + + let key = + if let Some(key) = self.router.get_shared_secret_from_dest(dec_ip.into()) { + key + } else { + debug!("Can't decrypt OOB ICMP packet from unknown host"); + continue; + }; + + let (_, body) = match etherparse::IpHeaders::from_slice(&real_packet[16..]) { + Ok(r) => r, + Err(e) => { + // This is a node which does not adhere to the protocol of sending back + // ICMP like this, or it is intentionally sending mallicious packets. + debug!( + "Dropping malformed OOB ICMP packet from {} for {e}", + data_packet.src_ip + ); + continue; + } + }; + let (header, body) = match etherparse::Icmpv6Header::from_slice(body.payload) { + Ok(r) => r, + Err(e) => { + // This is a node which does not adhere to the protocol of sending back + // ICMP like this, or it is intentionally sending mallicious packets. + debug!( + "Dropping OOB ICMP packet from {} with malformed ICMP header ({e})", + data_packet.src_ip + ); + continue; + } + }; + + // Where are the leftover bytes coming from + let orig_pb = match key.decrypt(body[..body.len()].to_vec()) { + Ok(pb) => pb, + Err(e) => { + warn!("Failed to decrypt ICMP data body {e}"); + continue; + } + }; + + let packet = etherparse::PacketBuilder::ipv6( + data_packet.src_ip.octets(), + data_packet.dst_ip.octets(), + data_packet.hop_limit, + ) + .icmpv6(header.icmp_type); + + let serialized_icmp = packet.size(orig_pb.len()); + let mut rp = PacketBuffer::new(); + rp.set_size(serialized_icmp); + if let Err(e) = + packet.write(&mut (&mut rp.buffer_mut()[..serialized_icmp]), &orig_pb) + { + error!("Could not reconstruct icmp packet {e}"); + continue; + } + if let Err(e) = l3_packet_sink.send(rp).await { + error!("Failed to send packet on local TUN interface: {e}",); + continue; + } + } + _ => { + trace!("Dropping decrypted packet with unknown protocol type"); + continue; + } + } + } + + warn!("Extract loop from router to host ended"); + } +} + +impl Clone for DataPlane +where + M: Clone, +{ + fn clone(&self) -> Self { + Self { + router: self.router.clone(), + } + } +} diff --git a/components/mycelium/mycelium/src/endpoint.rs b/components/mycelium/mycelium/src/endpoint.rs new file mode 100644 index 0000000..a09029a --- /dev/null +++ b/components/mycelium/mycelium/src/endpoint.rs @@ -0,0 +1,116 @@ +use std::{ + fmt, + net::{AddrParseError, SocketAddr}, + str::FromStr, +}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Error generated while processing improperly formatted endpoints. +pub enum EndpointParseError { + /// An address was specified without leading protocol information. + MissingProtocol, + /// An endpoint was specified using a protocol we (currently) do not understand. + UnknownProtocol, + /// Error while parsing the specific address. + Address(AddrParseError), +} + +/// Protocol used by an endpoint. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum Protocol { + /// Standard plain text Tcp. + Tcp, + /// Tls 1.3 with PSK over Tcp. + Tls, + /// Quic protocol (over UDP). + Quic, +} + +/// An endpoint defines a address and a protocol to use when communicating with it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Endpoint { + proto: Protocol, + socket_addr: SocketAddr, +} + +impl Endpoint { + /// Create a new `Endpoint` with given [`Protocol`] and address. + pub fn new(proto: Protocol, socket_addr: SocketAddr) -> Self { + Self { proto, socket_addr } + } + + /// Get the [`Protocol`] used by this `Endpoint`. + pub fn proto(&self) -> Protocol { + self.proto + } + + /// Get the [`SocketAddr`] used by this `Endpoint`. + pub fn address(&self) -> SocketAddr { + self.socket_addr + } +} + +impl FromStr for Endpoint { + type Err = EndpointParseError; + + fn from_str(s: &str) -> Result { + match s.split_once("://") { + None => Err(EndpointParseError::MissingProtocol), + Some((proto, socket)) => { + let proto = match proto.to_lowercase().as_str() { + "tcp" => Protocol::Tcp, + "quic" => Protocol::Quic, + "tls" => Protocol::Tls, + _ => return Err(EndpointParseError::UnknownProtocol), + }; + let socket_addr = SocketAddr::from_str(socket)?; + Ok(Endpoint { proto, socket_addr }) + } + } + } +} + +impl fmt::Display for Endpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("{} {}", self.proto, self.socket_addr)) + } +} + +impl fmt::Display for Protocol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Tcp => "Tcp", + Self::Tls => "Tls", + Self::Quic => "Quic", + }) + } +} + +impl fmt::Display for EndpointParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingProtocol => f.write_str("missing leading protocol identifier"), + Self::UnknownProtocol => f.write_str("protocol for endpoint is not supported"), + Self::Address(e) => f.write_fmt(format_args!("failed to parse address: {e}")), + } + } +} + +impl std::error::Error for EndpointParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Address(e) => Some(e), + _ => None, + } + } +} + +impl From for EndpointParseError { + fn from(value: AddrParseError) -> Self { + Self::Address(value) + } +} diff --git a/components/mycelium/mycelium/src/filters.rs b/components/mycelium/mycelium/src/filters.rs new file mode 100644 index 0000000..516aeb0 --- /dev/null +++ b/components/mycelium/mycelium/src/filters.rs @@ -0,0 +1,53 @@ +use crate::{babel, subnet::Subnet}; + +/// This trait is used to filter incoming updates from peers. Only updates which pass all +/// configured filters on the local [`Router`](crate::router::Router) will actually be forwarded +/// to the [`Router`](crate::router::Router) for processing. +pub trait RouteUpdateFilter { + /// Judge an incoming update. + fn allow(&self, update: &babel::Update) -> bool; +} + +/// Limit the subnet size of subnets announced in updates to be at most `N` bits. Note that "at +/// most" here means that the actual prefix length needs to be **AT LEAST** this value. +pub struct MaxSubnetSize; + +impl RouteUpdateFilter for MaxSubnetSize { + fn allow(&self, update: &babel::Update) -> bool { + update.subnet().prefix_len() >= N + } +} + +/// Limit the subnet announced to be included in the given subnet. +pub struct AllowedSubnet { + subnet: Subnet, +} + +impl AllowedSubnet { + /// Create a new `AllowedSubnet` filter, which only allows updates who's `Subnet` is contained + /// in the given `Subnet`. + pub fn new(subnet: Subnet) -> Self { + Self { subnet } + } +} + +impl RouteUpdateFilter for AllowedSubnet { + fn allow(&self, update: &babel::Update) -> bool { + self.subnet.contains_subnet(&update.subnet()) + } +} + +/// Limit the announced subnets to those which contain the derived IP from the `RouterId`. +/// +/// Since retractions can be sent by any node to indicate they don't have a route for the subnet, +/// these are also allowed. +pub struct RouterIdOwnsSubnet; + +impl RouteUpdateFilter for RouterIdOwnsSubnet { + fn allow(&self, update: &babel::Update) -> bool { + update.metric().is_infinite() + || update + .subnet() + .contains_ip(update.router_id().to_pubkey().address().into()) + } +} diff --git a/components/mycelium/mycelium/src/interval.rs b/components/mycelium/mycelium/src/interval.rs new file mode 100644 index 0000000..36475e9 --- /dev/null +++ b/components/mycelium/mycelium/src/interval.rs @@ -0,0 +1,38 @@ +//! Dedicated logic for +//! [intervals](https://datatracker.ietf.org/doc/html/rfc8966#name-solving-starvation-sequenci). + +use std::time::Duration; + +/// An interval in the babel protocol. +/// +/// Intervals represent a duration, and are expressed in centiseconds (0.01 second / 10 +/// milliseconds). `Interval` implements [`From`] [`u16`] to create a new interval from a raw +/// value, and [`From`] [`Duration`] to create a new `Interval` from an existing [`Duration`]. +/// There are also implementation to convert back to the aforementioned types. Note that in case of +/// duration, millisecond precision is lost. +#[derive(Debug, Clone)] +pub struct Interval(u16); + +impl From for Interval { + fn from(value: Duration) -> Self { + Interval((value.as_millis() / 10) as u16) + } +} + +impl From for Duration { + fn from(value: Interval) -> Self { + Duration::from_millis(value.0 as u64 * 10) + } +} + +impl From for Interval { + fn from(value: u16) -> Self { + Interval(value) + } +} + +impl From for u16 { + fn from(value: Interval) -> Self { + value.0 + } +} diff --git a/components/mycelium/mycelium/src/lib.rs b/components/mycelium/mycelium/src/lib.rs new file mode 100644 index 0000000..15f4023 --- /dev/null +++ b/components/mycelium/mycelium/src/lib.rs @@ -0,0 +1,462 @@ +use std::net::{IpAddr, Ipv6Addr}; +use std::path::PathBuf; +#[cfg(feature = "message")] +use std::{future::Future, time::Duration}; + +use crate::cdn::Cdn; +use crate::tun::TunConfig; +use bytes::BytesMut; +use data::DataPlane; +use endpoint::Endpoint; +#[cfg(feature = "message")] +use message::TopicConfig; +#[cfg(feature = "message")] +use message::{ + MessageId, MessageInfo, MessagePushResponse, MessageStack, PushMessageError, ReceivedMessage, +}; +use metrics::Metrics; +use peer_manager::{PeerExists, PeerNotFound, PeerStats, PrivateNetworkKey}; +use routing_table::{NoRouteSubnet, QueriedSubnet, RouteEntry}; +use subnet::Subnet; +use tokio::net::TcpListener; +use tracing::{error, info, warn}; + +mod babel; +pub mod cdn; +mod connection; +pub mod crypto; +pub mod data; +pub mod endpoint; +pub mod filters; +mod interval; +#[cfg(feature = "message")] +pub mod message; +mod metric; +pub mod metrics; +pub mod packet; +mod peer; +pub mod peer_manager; +pub mod router; +mod router_id; +mod routing_table; +mod rr_cache; +mod seqno_cache; +mod sequence_number; +mod source_table; +pub mod subnet; +pub mod task; +mod tun; + +/// The prefix of the global subnet used. +pub const GLOBAL_SUBNET_ADDRESS: IpAddr = IpAddr::V6(Ipv6Addr::new(0x400, 0, 0, 0, 0, 0, 0, 0)); +/// The prefix length of the global subnet used. +pub const GLOBAL_SUBNET_PREFIX_LEN: u8 = 7; + +/// Config for a mycelium [`Node`]. +pub struct Config { + /// The secret key of the node. + pub node_key: crypto::SecretKey, + /// Statically configured peers. + pub peers: Vec, + /// Tun interface should be disabled. + pub no_tun: bool, + /// Listen port for TCP connections. + pub tcp_listen_port: u16, + /// Listen port for Quic connections. + pub quic_listen_port: Option, + /// Udp port for peer discovery. + pub peer_discovery_port: Option, + /// Name for the TUN device. + #[cfg(any( + target_os = "linux", + all(target_os = "macos", not(feature = "mactunfd")), + target_os = "windows" + ))] + pub tun_name: String, + + /// Configuration for a private network, if run in that mode. To enable private networking, + /// this must be a name + a PSK. + pub private_network_config: Option<(String, PrivateNetworkKey)>, + /// Implementation of the `Metrics` trait, used to expose information about the system + /// internals. + pub metrics: M, + /// Mark that's set on all packets that we send on the underlying network + pub firewall_mark: Option, + + // tun_fd is android, iOS, macos on appstore specific option + // We can't create TUN device from the Rust code in android, iOS, and macos on appstore. + // So, we create the TUN device on Kotlin(android) or Swift(iOS, macos) then pass + // the TUN's file descriptor to mycelium. + #[cfg(any( + target_os = "android", + target_os = "ios", + all(target_os = "macos", feature = "mactunfd"), + ))] + pub tun_fd: Option, + + /// The maount of worker tasks spawned to process updates. Up to this amound of updates can be + /// processed in parallel. Because processing an update is a CPU bound task, it is pointless to + /// set this to a value which is higher than the amount of logical CPU cores available to the + /// system. + pub update_workers: usize, + + pub cdn_cache: Option, + + /// Configuration for message topics, if this is not set the default config will be used. + #[cfg(feature = "message")] + pub topic_config: Option, +} + +/// The Node is the main structure in mycelium. It governs the entire data flow. +pub struct Node { + router: router::Router, + peer_manager: peer_manager::PeerManager, + _cdn: Option, + #[cfg(feature = "message")] + message_stack: message::MessageStack, +} + +/// General info about a node. +pub struct NodeInfo { + /// The overlay subnet in use by the node. + pub node_subnet: Subnet, + /// The public key of the node + pub node_pubkey: crypto::PublicKey, +} + +impl Node +where + M: Metrics + Clone + Send + Sync + 'static, +{ + /// Setup a new `Node` with the provided [`Config`]. + pub async fn new(config: Config) -> Result> { + // If a private network is configured, validate network name + if let Some((net_name, _)) = &config.private_network_config { + if net_name.len() < 2 || net_name.len() > 64 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "network name must be between 2 and 64 characters", + ) + .into()); + } + } + let node_pub_key = crypto::PublicKey::from(&config.node_key); + let node_addr = node_pub_key.address(); + let (tun_tx, tun_rx) = tokio::sync::mpsc::unbounded_channel(); + + let node_subnet = Subnet::new( + // Truncate last 64 bits of address. + // TODO: find a better way to do this. + Subnet::new(node_addr.into(), 64) + .expect("64 is a valid IPv6 prefix size; qed") + .network(), + 64, + ) + .expect("64 is a valid IPv6 prefix size; qed"); + + // Creating a new Router instance + let router = match router::Router::new( + config.update_workers, + tun_tx, + node_subnet, + vec![node_subnet], + (config.node_key, node_pub_key), + vec![ + Box::new(filters::AllowedSubnet::new( + Subnet::new(GLOBAL_SUBNET_ADDRESS, GLOBAL_SUBNET_PREFIX_LEN) + .expect("Global subnet is properly defined; qed"), + )), + Box::new(filters::MaxSubnetSize::<64>), + Box::new(filters::RouterIdOwnsSubnet), + ], + config.metrics.clone(), + ) { + Ok(router) => { + info!( + "Router created. Pubkey: {:x}", + BytesMut::from(&router.node_public_key().as_bytes()[..]) + ); + router + } + Err(e) => { + error!("Error creating router: {e}"); + panic!("Error creating router: {e}"); + } + }; + + // Creating a new PeerManager instance + let pm = peer_manager::PeerManager::new( + router.clone(), + config.peers, + config.tcp_listen_port, + config.quic_listen_port, + config.peer_discovery_port.unwrap_or_default(), + config.peer_discovery_port.is_none(), + config.private_network_config, + config.metrics, + config.firewall_mark, + )?; + info!("Started peer manager"); + + #[cfg(feature = "message")] + let (tx, rx) = tokio::sync::mpsc::channel(100); + #[cfg(feature = "message")] + let msg_receiver = tokio_stream::wrappers::ReceiverStream::new(rx); + #[cfg(feature = "message")] + let msg_sender = tokio_util::sync::PollSender::new(tx); + #[cfg(not(feature = "message"))] + let msg_sender = futures::sink::drain(); + + let _data_plane = if config.no_tun { + warn!("Starting data plane without TUN interface, L3 functionality disabled"); + DataPlane::new( + router.clone(), + // No tun so create a dummy stream for L3 packets which never yields + tokio_stream::pending(), + // Similarly, create a sink which just discards every packet we would receive + futures::sink::drain(), + msg_sender, + tun_rx, + ) + } else { + #[cfg(not(any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" + )))] + { + panic!("On this platform, you can only run with --no-tun"); + } + #[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android", + target_os = "ios" + ))] + { + #[cfg(any( + target_os = "linux", + all(target_os = "macos", not(feature = "mactunfd")), + target_os = "windows" + ))] + let tun_config = TunConfig { + name: config.tun_name.clone(), + node_subnet: Subnet::new(node_addr.into(), 64) + .expect("64 is a valid subnet size for IPv6; qed"), + route_subnet: Subnet::new(GLOBAL_SUBNET_ADDRESS, GLOBAL_SUBNET_PREFIX_LEN) + .expect("Static configured TUN route is valid; qed"), + }; + #[cfg(any( + target_os = "android", + target_os = "ios", + all(target_os = "macos", feature = "mactunfd"), + ))] + let tun_config = TunConfig { + tun_fd: config.tun_fd.unwrap(), + }; + + let (rxhalf, txhalf) = tun::new(tun_config).await?; + + info!("Node overlay IP: {node_addr}"); + DataPlane::new(router.clone(), rxhalf, txhalf, msg_sender, tun_rx) + } + }; + + let cdn = config.cdn_cache.map(Cdn::new); + if let Some(ref cdn) = cdn { + let listener = TcpListener::bind("localhost:80").await?; + cdn.start(listener)?; + } + + #[cfg(feature = "message")] + let ms = MessageStack::new(_data_plane, msg_receiver, config.topic_config); + + Ok(Node { + router, + peer_manager: pm, + _cdn: cdn, + #[cfg(feature = "message")] + message_stack: ms, + }) + } + + /// Get information about the running `Node` + pub fn info(&self) -> NodeInfo { + NodeInfo { + node_subnet: self.router.node_tun_subnet(), + node_pubkey: self.router.node_public_key(), + } + } + + /// Get information about the current peers in the `Node` + pub fn peer_info(&self) -> Vec { + self.peer_manager.peers() + } + + /// Add a new peer to the system identified by an [`Endpoint`]. + pub fn add_peer(&self, endpoint: Endpoint) -> Result<(), PeerExists> { + self.peer_manager.add_peer(endpoint) + } + + /// Remove an existing peer identified by an [`Endpoint`] from the system. + pub fn remove_peer(&self, endpoint: Endpoint) -> Result<(), PeerNotFound> { + self.peer_manager.delete_peer(&endpoint) + } + + /// List all selected [`routes`](RouteEntry) in the system. + pub fn selected_routes(&self) -> Vec { + self.router.load_selected_routes() + } + + /// List all fallback [`routes`](RouteEntry) in the system. + pub fn fallback_routes(&self) -> Vec { + self.router.load_fallback_routes() + } + + /// List all [`queried subnets`](QueriedSubnet) in the system. + pub fn queried_subnets(&self) -> Vec { + self.router.load_queried_subnets() + } + + /// List all [`subnets with no route`](NoRouteSubnet) in the system. + pub fn no_route_entries(&self) -> Vec { + self.router.load_no_route_entries() + } + + /// Get public key from the IP of `Node` + pub fn get_pubkey_from_ip(&self, ip: IpAddr) -> Option { + self.router.get_pubkey(ip) + } +} + +#[cfg(feature = "message")] +impl Node +where + M: Metrics + Clone + Send + 'static, +{ + /// Wait for a messsage to arrive in the message stack. + /// + /// An the optional `topic` is provided, only messages which have exactly the same value in + /// `topic` will be returned. The `pop` argument decides if the message is removed from the + /// internal queue or not. If `pop` is `false`, the same message will be returned on the next + /// call (with the same topic). + /// + /// This method returns a future which will wait indefinitely until a message is received. It + /// is generally a good idea to put a limit on how long to wait by wrapping this in a [`tokio::time::timeout`]. + pub fn get_message( + &self, + pop: bool, + topic: Option>, + ) -> impl Future + '_ { + // First reborrow only the message stack from self, then manually construct a future. This + // avoids a lifetime issue on the router, which is not sync. If a regular 'async' fn would + // be used here, we can't specify that at this point sadly. + let ms = &self.message_stack; + async move { ms.message(pop, topic).await } + } + + /// Push a new message to the message stack. + /// + /// The system will attempt to transmit the message for `try_duration`. A message is considered + /// transmitted when the receiver has indicated it completely received the message. If + /// `subscribe_reply` is `true`, the second return value will be [`Option::Some`], with a + /// watcher which will resolve if a reply for this exact message comes in. Since this relies on + /// the receiver actually sending a reply, ther is no guarantee that this will eventually + /// resolve. + pub fn push_message( + &self, + dst: IpAddr, + data: Vec, + topic: Option>, + try_duration: Duration, + subscribe_reply: bool, + ) -> Result { + self.message_stack.new_message( + dst, + data, + topic.unwrap_or_default(), + try_duration, + subscribe_reply, + ) + } + + /// Get the status of a message sent previously. + /// + /// Returns [`Option::None`] if no message is found with the given id. Message info is only + /// retained for a limited time after a message has been received, or after the message has + /// been aborted due to a timeout. + pub fn message_status(&self, id: MessageId) -> Option { + self.message_stack.message_info(id) + } + + /// Send a reply to a previously received message. + pub fn reply_message( + &self, + id: MessageId, + dst: IpAddr, + data: Vec, + try_duration: Duration, + ) -> MessageId { + self.message_stack + .reply_message(id, dst, data, try_duration) + } + + /// Get a list of all configured topics + pub fn topics(&self) -> Vec> { + self.message_stack.topics() + } + + pub fn topic_allowed_sources(&self, topic: &Vec) -> Option> { + self.message_stack.topic_allowed_sources(topic) + } + + /// Sets the default topic action to accept or reject. This decides how topics which don't have + /// an explicit whitelist get handled. + pub fn accept_unconfigured_topic(&self, accept: bool) { + self.message_stack.set_default_topic_action(accept) + } + + /// Whether a topic without default configuration is accepted or not. + pub fn unconfigure_topic_action(&self) -> bool { + self.message_stack.get_default_topic_action() + } + + /// Add a topic to the whitelist without any configured allowed sources. + pub fn add_topic_whitelist(&self, topic: Vec) { + self.message_stack.add_topic_whitelist(topic) + } + + /// Remove a topic from the whitelist. Future messages will follow the default action. + pub fn remove_topic_whitelist(&self, topic: Vec) { + self.message_stack.remove_topic_whitelist(topic) + } + + /// Add a new whitelisted source for a topic. This creates the topic if it does not exist yet. + pub fn add_topic_whitelist_src(&self, topic: Vec, src: Subnet) { + self.message_stack.add_topic_whitelist_src(topic, src) + } + + /// Remove a whitelisted source for a topic. + pub fn remove_topic_whitelist_src(&self, topic: Vec, src: Subnet) { + self.message_stack.remove_topic_whitelist_src(topic, src) + } + + /// Set the forward socket for a topic. Creates the topic if it doesn't exist. + pub fn set_topic_forward_socket(&self, topic: Vec, socket_path: std::path::PathBuf) { + self.message_stack + .set_topic_forward_socket(topic, Some(socket_path)) + } + + /// Get the forward socket for a topic, if any. + pub fn get_topic_forward_socket(&self, topic: &Vec) -> Option { + self.message_stack.get_topic_forward_socket(topic) + } + + /// Removes the forward socket for the topic, if one exists + pub fn delete_topic_forward_socket(&self, topic: Vec) { + self.message_stack.set_topic_forward_socket(topic, None) + } +} diff --git a/components/mycelium/mycelium/src/message.rs b/components/mycelium/mycelium/src/message.rs new file mode 100644 index 0000000..013685a --- /dev/null +++ b/components/mycelium/mycelium/src/message.rs @@ -0,0 +1,1867 @@ +//! Module for working with "messages". +//! +//! A message is an arbitrary bag of bytes sent by a node to a different node. A message is +//! considered application defined data (L7), and we make no assumptions of any kind regarding the +//! structure. We only care about sending the message to the remote in the most reliable way +//! possible. + +use core::fmt; +#[cfg(target_family = "unix")] +use std::io; +#[cfg(target_family = "unix")] +use std::path::PathBuf; +use std::{ + collections::{HashMap, VecDeque}, + marker::PhantomData, + net::IpAddr, + ops::{Deref, DerefMut}, + sync::{Arc, Mutex, RwLock}, + time::{self, Duration}, +}; + +use futures::{Stream, StreamExt}; +use rand::Fill; +use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; +#[cfg(target_family = "unix")] +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +#[cfg(target_family = "unix")] +use tokio::net::UnixStream; +use tokio::sync::watch; +use topic::MessageAction; +use tracing::{debug, error, trace, warn}; + +use crate::{ + crypto::{PacketBuffer, PublicKey}, + data::DataPlane, + message::{chunk::MessageChunk, done::MessageDone, init::MessageInit}, + metrics::Metrics, + subnet::Subnet, +}; + +pub use topic::TopicConfig; + +mod chunk; +mod done; +mod init; +mod topic; + +/// The amount of time to try and send messages before we give up. +const MESSAGE_SEND_WINDOW: Duration = Duration::from_secs(60 * 5); + +/// The amount of time to wait before sending a chunk again if receipt is not acknowledged. +const RETRANSMISSION_DELAY: Duration = Duration::from_millis(100); + +/// Amount of time between sweeps of the subscriber list to clear orphaned subscribers. +const REPLY_SUBSCRIBER_CLEAR_DELAY: Duration = Duration::from_secs(60); + +/// Default timeout for waiting for a reply from a socket. +#[cfg(target_family = "unix")] +const SOCKET_REPLY_TIMEOUT: Duration = Duration::from_secs(5); + +/// The average size of a single chunk. This is mainly intended to preallocate the chunk array on +/// the receiver size. This value should allow reasonable overhead for standard MTU. +const AVERAGE_CHUNK_SIZE: usize = 1_300; +/// The minimum size of a data chunk. Chunks which have a size smaller than this are rejected. An +/// exception is made for the last chunk. +const MINIMUM_CHUNK_SIZE: u64 = 250; + +/// The size in bytes of the message header which starts each user message packet. +const MESSAGE_HEADER_SIZE: usize = 12; +/// The size in bytes of a message ID. +const MESSAGE_ID_SIZE: usize = 8; + +/// Flag indicating we are starting a new message. The message ID is specified in the header. The +/// body contains the length of the message. The receiver must create an entry for the new ID. This +/// flag must always be set on the first packet of a message stream. If a receiver already received +/// data for this message and a new packet comes in with this flag set for this message, all +/// existing data must be removed on the receiver side. +const FLAG_MESSAGE_INIT: u16 = 0b1000_0000_0000_0000; +// Flag indicating the message with the given ID is done, i.e. it has been fully transmitted. +const FLAG_MESSAGE_DONE: u16 = 0b0100_0000_0000_0000; +/// Indicates the message with this ID is aborted by the sender and the receiver should discard it. +/// The receiver can ignore this if it fully received the message. +const FLAG_MESSAGE_ABORTED: u16 = 0b0010_0000_0000_0000; +/// Flag indicating we are transferring a data chunk. +const FLAG_MESSAGE_CHUNK: u16 = 0b0001_0000_0000_0000; +/// Flag indicating the message with the given ID has been read by the receiver, that is it has +/// been transferred to an external process. +const FLAG_MESSAGE_READ: u16 = 0b0000_1000_0000_0000; +/// Flag indicating we are sending a reply to a received message. The message ID used is the same +/// as the received message. +const FLAG_MESSAGE_REPLY: u16 = 0b0000_0100_0000_0000; +/// Flag acknowledging receipt of a packet. Once this has been received, the packet __should not__ be +/// transmitted again by the sender. +const FLAG_MESSAGE_ACK: u16 = 0b0000_0001_0000_0000; + +/// Length of a message checksum in bytes. +const MESSAGE_CHECKSUM_LENGTH: usize = 32; + +/// Checksum of a message used to verify received message integrity. +pub type Checksum = [u8; MESSAGE_CHECKSUM_LENGTH]; + +/// Response type when pushing a message. +pub type MessagePushResponse = (MessageId, Option>>); + +pub struct MessageStack { + // The DataPlane is wrappen in a Mutex since it does not implement Sync. + data_plane: Arc>>, + inbox: Arc>, + outbox: Arc>, + /// Receiver handle for inbox listeners (basically a condvar). + subscriber: watch::Receiver<()>, + /// Subscribers for messages with specific ID's. These are intended to be used when waiting for + /// a reply. + /// This takes an Option as value to avoid the hassle of constructing a dummy value when + /// creating the watch channel. + reply_subscribers: Arc>>>>, + /// Topic-specific configuration + topic_config: Arc>, +} + +struct MessageOutbox { + msges: HashMap, +} + +struct MessageInbox { + /// Messages which are still being transmitted. + // TODO: MessageID is part of ReceivedMessageInfo, rework this into HashSet? + pending_msges: HashMap, + /// Messages which have been completed. + complete_msges: VecDeque, + /// Notification sender used to allert subscribed listeners. + notify: watch::Sender<()>, +} + +struct ReceivedMessageInfo { + id: MessageId, + is_reply: bool, + src: IpAddr, + dst: IpAddr, + /// Length of the finished message. + len: u64, + /// Optional topic of the message. + topic: Vec, + chunks: Vec>, +} + +#[derive(Clone)] +pub struct ReceivedMessage { + /// Id of the message. + pub id: MessageId, + /// This message is a reply to an initial message with the given id. + pub is_reply: bool, + /// The overlay ip of the sender. + pub src_ip: IpAddr, + /// The public key of the sender of the message. + pub src_pk: PublicKey, + /// The overlay ip of the receiver. + pub dst_ip: IpAddr, + /// The public key of the receiver of the message. This is always ours. + pub dst_pk: PublicKey, + /// The possible topic of the message. + pub topic: Vec, + /// Actual message. + pub data: Vec, +} + +/// A chunk of a message. This represents individual data pieces on the receiver side. +#[derive(Clone)] +struct Chunk { + data: Vec, +} + +/// Description of an individual chunk. +struct ChunkState { + /// Index of the chunk in the chunk stream. + chunk_idx: usize, + /// Offset of the chunk in the message. + chunk_offset: usize, + /// Size of the chunk. + // TODO: is this needed or can this be extrapolated by checking the next chunk in the list? + chunk_size: usize, + /// Transmit state of the chunk. + chunk_transmit_state: ChunkTransmitState, +} + +/// Transmission state of an individual chunk +enum ChunkTransmitState { + /// The chunk hasn't been transmitted yet. + Started, + /// The chunk has been sent but we did not receive an acknowledgment yet. The time the chunk + /// was sent is remembered so we can calulcate if we need to try sending it again. + Sent(std::time::Instant), + /// The receiver has acknowledged receipt of the chunk. + Acked, +} + +#[derive(PartialEq)] +enum TransmissionState { + /// Transmission has not started yet. + Init, + /// Transmission is in progress (ACK received for INIT). + InProgress, + /// Remote acknowledged full reception. + Received, + /// Remote indicated the message has been read by an external entity. + Read, + /// Transmission aborted by us. We indicated this by sending an abort flag to the receiver. + Aborted, +} + +#[derive(Debug, Clone, Copy)] +pub enum PushMessageError { + /// The topic set in the message is too large. + TopicTooLarge, +} + +impl MessageInbox { + fn new(notify: watch::Sender<()>) -> Self { + Self { + pending_msges: HashMap::new(), + complete_msges: VecDeque::new(), + notify, + } + } +} + +impl MessageOutbox { + /// Create a new `MessageOutbox` ready for use. + fn new() -> Self { + Self { + msges: HashMap::new(), + } + } + + /// Insert a new message for tracking during (and after) sending. + fn insert(&mut self, msg: OutboundMessageInfo) { + self.msges.insert(msg.msg.id, msg); + } +} + +impl MessageStack +where + M: Metrics + Clone + Send + 'static, +{ + /// Create a new `MessageStack`. This uses the provided [`DataPlane`] to inject message + /// packets. Received packets must be injected into the `MessageStack` through the provided + /// [`Stream`]. + pub fn new( + data_plane: DataPlane, + message_packet_stream: S, + topic_config: Option, + ) -> Self + where + S: Stream + Send + Unpin + 'static, + { + let (notify, subscriber) = watch::channel(()); + let ms = Self { + data_plane: Arc::new(Mutex::new(data_plane)), + inbox: Arc::new(Mutex::new(MessageInbox::new(notify))), + outbox: Arc::new(Mutex::new(MessageOutbox::new())), + subscriber, + reply_subscribers: Arc::new(Mutex::new(HashMap::new())), + topic_config: Arc::new(RwLock::new(topic_config.unwrap_or_default())), + }; + + tokio::task::spawn( + ms.clone() + .handle_incoming_message_packets(message_packet_stream), + ); + + // task to periodically clear leftover reply subscribers + { + let ms = ms.clone(); + tokio::task::spawn(async move { + loop { + tokio::time::sleep(REPLY_SUBSCRIBER_CLEAR_DELAY).await; + + let mut subs = ms.reply_subscribers.lock().unwrap(); + subs.retain(|id, v| { + if v.receiver_count() == 0 { + debug!( + "Clearing orphaned subscription for message id {}", + id.as_hex() + ); + false + } else { + true + } + }) + } + }); + } + ms + } + + /// Handle incoming messages from the [`DataPlane`]. + async fn handle_incoming_message_packets(self, mut message_packet_stream: S) + where + S: Stream + Send + Unpin + 'static, + { + while let Some((packet, src, dst)) = message_packet_stream.next().await { + let mp = MessagePacket::new(packet); + + trace!( + "Received message packet with flags {:b}", + mp.header().flags() + ); + + if mp.header().flags().ack() { + self.handle_message_reply(mp); + } else { + self.handle_message(mp, src, dst); + } + } + + warn!("Incoming message packet stream ended!"); + } + + /// Handle an incoming message packet which is a reply to a message we previously sent. + fn handle_message_reply(&self, mp: MessagePacket) { + let header = mp.header(); + let message_id = header.message_id(); + let flags = header.flags(); + if flags.init() { + let mut outbox = self.outbox.lock().unwrap(); + if let Some(message) = outbox.msges.get_mut(&message_id) { + if message.state != TransmissionState::Init { + debug!("Dropping INIT ACK for message not in init state"); + return; + } + message.state = TransmissionState::InProgress; + // Transform message into chunks. + let mut chunks = Vec::with_capacity(message.len.div_ceil(AVERAGE_CHUNK_SIZE)); + for (chunk_idx, data_chunk) in + message.msg.data.chunks(AVERAGE_CHUNK_SIZE).enumerate() + { + chunks.push(ChunkState { + chunk_idx, + chunk_offset: chunk_idx * AVERAGE_CHUNK_SIZE, + chunk_size: data_chunk.len(), + chunk_transmit_state: ChunkTransmitState::Started, + }) + } + message.chunks = chunks; + } + } else if flags.chunk() { + // ACK for a chunk, mark chunk as received so it is not retried again. + let mut outbox = self.outbox.lock().unwrap(); + if let Some(message) = outbox.msges.get_mut(&message_id) { + if message.state != TransmissionState::InProgress { + debug!("Dropping CHUNK ACK for message not being transmitted"); + return; + } + let mc = MessageChunk::new(mp); + // Sanity checks. This is just to protect ourselves, if the other party is + // malicious it can return any data it wants here. + if mc.chunk_idx() > message.chunks.len() as u64 { + debug!("Dropping CHUNK ACK for message because ACK'ed chunk is out of bounds"); + return; + } + // Don't check data size. It is the repsonsiblity of the other party to ensure he + // ACKs the right chunk. Additionally a malicious node could return a crafted input + // here anyway. + + message.chunks[mc.chunk_idx() as usize].chunk_transmit_state = + ChunkTransmitState::Acked; + } + } else if flags.done() { + // ACK for full message. + let mut outbox = self.outbox.lock().unwrap(); + if let Some(message) = outbox.msges.get_mut(&message_id) { + if message.state != TransmissionState::InProgress { + debug!("Dropping DONE ACK for message which is not being transmitted"); + return; + } + message.state = TransmissionState::Received; + } + } else if flags.read() { + // Ack for a read flag. Since the original read flag is sent by the receiver, this + // means the sender indicates he has successfully received the notification that a + // userspace process has read the message. Note that read flags are only sent once, and + // this ack is only sent at most once, even if it gets lost. As a result, there is + // nothing to really do here, and this behavior (ACK READ) might be dropped in the + // future. + debug!("Received READ ACK"); + } else { + debug!("Received unknown ACK message flags {:b}", flags); + } + } + + /// Handle an incoming message packet which is **not** a reply to a packet we previously sent. + fn handle_message(&self, mp: MessagePacket, src: IpAddr, dst: IpAddr) { + let header = mp.header(); + let message_id = header.message_id(); + let flags = header.flags(); + let reply = if flags.init() { + let is_reply = flags.reply(); + let mi = MessageInit::new(mp); + // If this is not a reply, verify ACL + if !is_reply && !self.topic_allowed(mi.topic(), src) { + debug!("Dropping message whos src isn't allowed by ACL"); + return; + } + // We receive a new message with an ID. If we already have a complete message, ignore + // it. + let mut inbox = self.inbox.lock().unwrap(); + if inbox.complete_msges.iter().any(|m| m.id == message_id) { + debug!("Dropping INIT message as we already have a complete message with this ID"); + return; + } + // Otherwise unilaterally reset the state. The message id space is large enough to + // avoid accidental collisions. + let expected_chunks = (mi.length() as usize).div_ceil(AVERAGE_CHUNK_SIZE); + let chunks = vec![None; expected_chunks]; + let message = ReceivedMessageInfo { + id: message_id, + is_reply, + src, + dst, + len: mi.length(), + topic: mi.topic().into(), + chunks, + }; + + if inbox.pending_msges.insert(message_id, message).is_some() { + debug!("Dropped current pending message because we received a new message with INIT flag set for the same ID"); + } + + Some(mi.into_reply().into_inner()) + } else if flags.chunk() { + // A chunk can only be received for incomplete messages. We don't have to check the + // completed messages. Either there is none, so no problem, or there is one, in which + // case we consider this to be a lingering chunk which was already accepted in the + // meantime (as the message is complete). + // + // SAFETY: a malicious node could send a lot of empty chunks, which trigger allocations + // to hold the chunk array, effectively exhausting memory. As such, we first need to + // determine if the chunk is feasible. + let mut inbox = self.inbox.lock().unwrap(); + if let Some(message) = inbox.pending_msges.get_mut(&message_id) { + let mc = MessageChunk::new(mp); + // Make sure the data is within bounds of the message being sent. + if message.len < mc.chunk_offset() + mc.chunk_size() { + debug!("Dropping invalid message CHUNK for being out of bounds"); + return; + } + // Check max chunk idx. + let max_chunk_idx = if message.len == 0 { + 0 + } else { + message.len.div_ceil(MINIMUM_CHUNK_SIZE) - 1 + }; + if mc.chunk_idx() > max_chunk_idx { + debug!("Dropping CHUNK because index is too high"); + return; + } + // Check chunk size, allow exception on last chunk. + if mc.chunk_size() < MINIMUM_CHUNK_SIZE && mc.chunk_idx() != max_chunk_idx { + debug!( + "Dropping CHUNK {}/{max_chunk_idx} which is too small ({} bytes / {MINIMUM_CHUNK_SIZE} bytes)", + mc.chunk_idx(), + mc.chunk_size() + ); + return; + } + // Finally check if we have sufficient space for our chunks. + if message.chunks.len() as u64 <= mc.chunk_idx() { + // TODO: optimize + let chunks = + vec![None; (mc.chunk_idx() + 1 - message.chunks.len() as u64) as usize]; + message.chunks.extend_from_slice(&chunks); + } + // Now insert the chunk. Overwrite any previous chunk. + message.chunks[mc.chunk_idx() as usize] = Some(Chunk { + data: mc.data().to_vec(), + }); + + Some(mc.into_reply().into_inner()) + } else { + None + } + } else if flags.done() { + let mut inbox = self.inbox.lock().unwrap(); + let md = MessageDone::new(mp); + // At this point, we should have all message chunks. Verify length and reassemble them. + if let Some(inbound_message) = inbox.pending_msges.get_mut(&message_id) { + // Check if we have sufficient chunks + if md.chunk_count() != inbound_message.chunks.len() as u64 { + // TODO: report error to sender + debug!("Message has invalid amount of chunks"); + return; + } + // Track total size of data we have allocated. + let mut chunk_size = 0; + let mut message_data = Vec::with_capacity(inbound_message.len as usize); + + // Chunks are inserted in order. + for chunk in &inbound_message.chunks { + if let Some(chunk) = chunk { + message_data.extend_from_slice(&chunk.data); + chunk_size += chunk.data.len(); + } else { + // A none chunk is not possible, we should have all chunks + debug!("DONE received for incomplete message"); + return; + } + } + + // TODO: report back here if there is an error. + if chunk_size as u64 != inbound_message.len { + debug!("Message has invalid size"); + return; + } + + let message = Message { + id: inbound_message.id, + src: inbound_message.src, + dst: inbound_message.dst, + topic: inbound_message.topic.clone(), + data: message_data, + }; + + let checksum = message.checksum(); + + if checksum != md.checksum() { + debug!( + "Message has wrong checksum, got {} expected {}", + md.checksum().to_hex(), + checksum.to_hex() + ); + return; + } + + // Convert the IP's to PublicKeys. + let dp = self.data_plane.lock().unwrap(); + let src_pubkey = if let Some(pk) = dp.router().get_pubkey(message.src) { + pk + } else { + warn!("No public key entry for IP we just received a message chunk from"); + return; + }; + // This always is our own key as we are receiving. + let dst_pubkey = dp.router().node_public_key(); + + let message = ReceivedMessage { + id: message.id, + is_reply: inbound_message.is_reply, + src_ip: message.src, + src_pk: src_pubkey, + dst_ip: message.dst, + dst_pk: dst_pubkey, + topic: message.topic, + data: message.data, + }; + + debug!("Message {} reception complete", message.id.as_hex()); + + // Check if we have any listeners and try to send the message to those first. + let mut subscribers = self.reply_subscribers.lock().unwrap(); + // Use remove here since we are done with the subscriber + // TODO: only check this if the is_reply flag is set? + if let Some(sub) = subscribers.remove(&message.id) { + if let Err(e) = sub.send(Some(message.clone())) { + debug!("Subscriber quit before we could send the reply"); + // Move message to be read if there were no subscribers. + inbox.complete_msges.push_back(e.0.unwrap()); + // Notify subscribers we have a new message. + inbox.notify.send_replace(()); + } else { + debug!("Informed subscriber of message reply"); + } + } else { + // Check if the topic has a configured socket path + let socket_path = self + .topic_config + .read() + .expect("Can get read lock on topic config") + .get_topic_forward_socket(&message.topic) + .cloned(); + + if let Some(socket_path) = socket_path { + debug!( + "Forwarding message {} to socket {}", + message.id.as_hex(), + socket_path.display() + ); + + // Clone the message for use in the async task + let message_clone = message.clone(); + let message_stack = self.clone(); + + // Drop the inbox lock before spawning the task to avoid deadlocks + std::mem::drop(inbox); + std::mem::drop(subscribers); + + // Spawn a task to handle the socket communication + #[cfg(target_family = "unix")] + tokio::task::spawn(async move { + // Forward the message to the socket + match message_stack + .forward_to_socket( + &message_clone, + &socket_path, + SOCKET_REPLY_TIMEOUT, + ) + .await + { + Ok(reply_data) => { + debug!(message_id = message_clone.id.as_hex(), "Received reply from socket, sending back to original sender"); + + // Send the reply back to the original sender + message_stack.reply_message( + message_clone.id, + message_clone.src_ip, + reply_data, + MESSAGE_SEND_WINDOW, + ); + } + Err(e) => { + // Log the error + error!(err = % e, "Failed to forward message to socket"); + + // Fall back to pushing to the queue + let mut inbox = message_stack.inbox.lock().unwrap(); + inbox.complete_msges.push_back(message_clone); + inbox.notify.send_replace(()); + } + } + }); + + #[cfg(not(target_family = "unix"))] + { + let mut inbox = message_stack.inbox.lock().unwrap(); + inbox.complete_msges.push_back(message_clone); + inbox.notify.send_replace(()); + } + + // Re-acquire the inbox lock to continue processing + inbox = self.inbox.lock().unwrap(); + } else { + // No socket path configured, push to the queue as usual + inbox.complete_msges.push_back(message); + // Notify subscribers we have a new message. + inbox.notify.send_replace(()); + } + } + inbox.pending_msges.remove(&message_id); + + Some(md.into_reply().into_inner()) + } else { + None + } + } else if flags.read() { + let mut outbox = self.outbox.lock().unwrap(); + if let Some(message) = outbox.msges.get_mut(&message_id) { + if message.state != TransmissionState::Received { + debug!("Got READ for message which is not in received state"); + return; + } + debug!("Receiver confirmed READ of message {}", message_id.as_hex()); + message.state = TransmissionState::Read; + } + None + } else if flags.aborted() { + // If the message is not finished yet, discard it completely. + // But if it is finished, ignore this, i.e, nothing to do. + let mut inbox = self.inbox.lock().unwrap(); + if inbox.pending_msges.remove(&message_id).is_some() { + debug!("Dropping pending message because we received an ABORT"); + } + None + } else { + debug!("Received unknown message flags {:b}", flags); + None + }; + if let Some(reply) = reply { + // This is a reply, so SRC -> DST and DST -> SRC + // FIXME: this can be fixed once the dataplane accepts generic IpAddr addresses. + match (src, dst) { + (IpAddr::V6(src), IpAddr::V6(dst)) => { + self.data_plane.lock().unwrap().inject_message_packet( + dst, + src, + reply.into_inner(), + ); + } + _ => debug!("can only reply to message fragments if both src and dst are IPv6"), + } + } + } + + /// Check if a topic is allowed for a given src + fn topic_allowed(&self, topic: &[u8], src: IpAddr) -> bool { + if let Some(whitelist_config) = self + .topic_config + .read() + .expect("Can get read lock on topic config") + .whitelist() + .get(topic) + { + debug!(?topic, %src, "Checking allow list for topic"); + for subnet in whitelist_config.subnets() { + if subnet.contains_ip(src) { + return true; + } + } + false + } else { + let action = self + .topic_config + .read() + .expect("Can get read lock on topic config") + .default(); + debug!(?action, ?topic, "Default action for topic"); + matches!(action, MessageAction::Accept) + } + } + + /// Forward a message to a Unix domain socket and wait for a reply + #[cfg(target_family = "unix")] + async fn forward_to_socket( + &self, + message: &ReceivedMessage, + socket_path: &PathBuf, + timeout: Duration, + ) -> Result, SocketError> { + // Connect to the socket + let mut stream = UnixStream::connect(socket_path).await.map_err(|e| { + error!( + "Failed to connect to socket {}: {}", + socket_path.display(), + e + ); + SocketError::IoError(e) + })?; + + // Send the message data, wrap in a timeout as we can't set read timeout on the socket + tokio::time::timeout(timeout, stream.write_all(&message.data)) + .await + .map_err(|_| SocketError::Timeout)? + .map_err(|e| { + error!("Failed to write to socket: {}", e); + SocketError::IoError(e) + })?; + + // Read the reply + let mut reply = Vec::new(); + match stream.read_to_end(&mut reply).await { + Ok(0) => { + debug!("Socket connection closed without sending a reply"); + Err(SocketError::ConnectionClosed) + } + Ok(_) => { + debug!(reply_len = reply.len(), "Received reply from socket"); + Ok(reply) + } + Err(e) + if e.kind() == io::ErrorKind::TimedOut || e.kind() == io::ErrorKind::WouldBlock => + { + debug!("Timeout waiting for socket reply"); + Err(SocketError::Timeout) + } + Err(e) => { + error!(err = %e, "Error reading from socket"); + Err(SocketError::IoError(e)) + } + } + } +} + +/// Error type for socket communication +#[derive(Debug)] +#[cfg(target_family = "unix")] +enum SocketError { + /// I/O error occurred during socket communication + IoError(io::Error), + /// Timeout occurred while waiting for a reply + Timeout, + /// Socket connection was closed unexpectedly + ConnectionClosed, +} + +#[cfg(target_family = "unix")] +impl core::fmt::Display for SocketError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IoError(e) => write!(f, "I/O Error {e}"), + Self::Timeout => f.pad("timeout waiting for reply"), + Self::ConnectionClosed => f.pad("socket closed before we read a reply"), + } + } +} + +impl MessageStack +where + M: Metrics + Clone + Send + 'static, +{ + /// Push a new message to be transmitted, which will be tried for the given duration. A + /// [message id](MessageId) will be randomly generated, and returned. + pub fn new_message( + &self, + dst: IpAddr, + data: Vec, + topic: Vec, + try_duration: Duration, + subscribe_reply: bool, + ) -> Result { + self.push_message(None, dst, data, topic, try_duration, subscribe_reply) + } + + /// Push a new message which is a reply to the message with [the provided id](MessageId). + pub fn reply_message( + &self, + reply_to: MessageId, + dst: IpAddr, + data: Vec, + try_duration: Duration, + ) -> MessageId { + debug!(reply_to = reply_to.as_hex(), %dst, data_size = data.len(), "Sending reply to message"); + self.push_message(Some(reply_to), dst, data, vec![], try_duration, false) + .expect("Empty topic is never too large") + .0 + } + + /// Get a list of all configured topics + pub fn topics(&self) -> Vec> { + self.topic_config + .read() + .expect("Can get read lock on topic config") + .whitelist() + .keys() + .cloned() + .collect() + } + + /// Get all allowed sources for a topic. If the topic does not exist, None is returned. If the + /// topic exists without any sources, Some is returend with an empty list. + pub fn topic_allowed_sources(&self, topic: &Vec) -> Option> { + self.topic_config + .read() + .expect("Can get read lock on topic config") + .whitelist() + .get(topic) + .map(|tc| tc.subnets().clone()) + } + + /// Gets whether unconfigured topics are accepted or not. + pub fn get_default_topic_action(&self) -> bool { + matches!( + self.topic_config + .read() + .expect("Can get read lock on topic config") + .default(), + MessageAction::Accept + ) + } + + /// Set the default action to take for an unconfigured topic (accept or reject) + pub fn set_default_topic_action(&self, accept: bool) { + self.topic_config + .write() + .expect("Can lock topic config for writing") + .set_default(if accept { + MessageAction::Accept + } else { + MessageAction::Reject + }); + } + + /// Add a topic to the whitelist without any configured allowed sources. + pub fn add_topic_whitelist(&self, topic: Vec) { + self.topic_config + .write() + .expect("Can lock topic config for writing") + .add_topic_whitelist(topic); + } + + /// Remove a topic from the whitelist. Future messages will follow the default action. + pub fn remove_topic_whitelist(&self, topic: Vec) { + self.topic_config + .write() + .expect("Can lock topic config for writing") + .remove_topic_whitelist(&topic); + } + + /// Add a new whitelisted source for a topic. This creates the topic if it does not exist yet. + pub fn add_topic_whitelist_src(&self, topic: Vec, src: Subnet) { + self.topic_config + .write() + .expect("Can lock topic config for writing") + .add_topic_whitelist_src(topic, src); + } + + /// Remove a whitelisted source for a topic. + pub fn remove_topic_whitelist_src(&self, topic: Vec, src: Subnet) { + self.topic_config + .write() + .expect("Can lock topic config for writing") + .remove_topic_whitelist_src(&topic, src); + } + + /// Set the forward socket for a topic. Does nothing if the topic doesn't exist. + pub fn set_topic_forward_socket( + &self, + topic: Vec, + socket_path: Option, + ) { + self.topic_config + .write() + .expect("Can lock topic config for writing") + .set_topic_forward_socket(topic, socket_path); + } + + /// Get the forward socket for a topic, if any. + pub fn get_topic_forward_socket(&self, topic: &Vec) -> Option { + self.topic_config + .read() + .expect("Can get read lock on topic config") + .get_topic_forward_socket(topic) + .cloned() + } + + /// Subscribe to a new message with the given ID. In practice, this will be a reply. + pub fn subscribe_id(&self, id: MessageId) -> watch::Receiver> { + let mut subscribers = self.reply_subscribers.lock().unwrap(); + if let Some(sub) = subscribers.get(&id) { + sub.subscribe() + } else { + // dummy initial value + let (tx, rx) = watch::channel(None); + subscribers.insert(id, tx); + rx + } + } + + /// Push a new message. If id is set, it is considered a reply to that id. If not, a new id is + /// generated. + fn push_message( + &self, + id: Option, + dst: IpAddr, + data: Vec, + topic: Vec, + try_duration: Duration, + subscribe: bool, + ) -> Result { + if topic.len() > 255 { + return Err(PushMessageError::TopicTooLarge); + } + + let src = self + .data_plane + .lock() + .unwrap() + .router() + .node_public_key() + .address() + .into(); + + let (id, reply) = if let Some(id) = id { + (id, true) + } else { + (MessageId::new(), false) + }; + + let len = data.len(); + let msg = Message { + id, + src, + dst, + topic, + data, + }; + + let created = std::time::SystemTime::now(); + let deadline = created + try_duration; + + let obmi = OutboundMessageInfo { + state: TransmissionState::Init, + created, + deadline, + len, + msg, + chunks: vec![], // leave Vec empty at start + }; + + let subscription = if subscribe { + Some(self.subscribe_id(id)) + } else { + None + }; + + // Already prepare the init packet for sending.. + let mut mp = MessagePacket::new(PacketBuffer::new()); + mp.header_mut().set_message_id(id); + if reply { + mp.header_mut().flags_mut().set_reply(); + } + + let mut mi = MessageInit::new(mp); + mi.set_length(len as u64); + mi.set_topic(&obmi.msg.topic); + + self.outbox + .lock() + .expect("Outbox lock isn't poisoned; qed") + .insert(obmi); + + // Actually send the init packet + match (src, dst) { + (IpAddr::V6(src), IpAddr::V6(dst)) => { + self.data_plane.lock().unwrap().inject_message_packet( + src, + dst, + mi.into_inner().into_inner(), + ); + } + _ => debug!("Can only send messages between two IPv6 addresses"), + } + + // Clone message stack so it can be injected in the task. + let message_stack = self.clone(); + tokio::task::spawn(async move { + let mut deadline = tokio::time::interval(MESSAGE_SEND_WINDOW); + let mut interval = tokio::time::interval(RETRANSMISSION_DELAY); + // Avoid a send burst if the system is slow. + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + // intervals tick immediately, so consume one tick each + deadline.tick().await; + interval.tick().await; + + let mut aborted = false; + + loop { + tokio::select! { + _ = interval.tick() => { + if aborted { + continue + } + if let Some(msg) = message_stack.outbox.lock().unwrap().msges.get_mut(&id) { + match msg.state { + TransmissionState::Init => { + // Send the init packet. + let mut mp = MessagePacket::new(PacketBuffer::new()); + mp.header_mut().set_message_id(id); + if reply { + mp.header_mut().flags_mut().set_reply(); + } + + let mut mi = MessageInit::new(mp); + mi.set_length(len as u64); + mi.set_topic(&msg.msg.topic); + match (msg.msg.src, msg.msg.dst) { + (IpAddr::V6(src), IpAddr::V6(dst)) => { + message_stack + .data_plane + .lock() + .unwrap() + .inject_message_packet( + src, + dst, + mi.into_inner().into_inner(), + ); + } + _ => debug!("Can only send messages between two IPv6 addresses"), + } + } + TransmissionState::InProgress => { + // Send chunks which haven't been sent yet. + let mut all_acked = true; + for chunk in msg.chunks.iter_mut() { + if !matches!(chunk.chunk_transmit_state, ChunkTransmitState::Acked) + { + all_acked = false; + } + match chunk.chunk_transmit_state { + ChunkTransmitState::Started => { + // Generate and send chunk, move chunk to state sent + let mut mp = MessagePacket::new(PacketBuffer::new()); + mp.header_mut().set_message_id(id); + + let mut mc = MessageChunk::new(mp); + mc.set_chunk_idx(chunk.chunk_idx as u64); + mc.set_chunk_offset(chunk.chunk_offset as u64); + if let Err(e) = mc.set_chunk_data( + &msg.msg.data[chunk.chunk_offset + ..chunk.chunk_offset + chunk.chunk_size], + ) { + error!("Failed to generate and send chunk: {e}"); + }; + + match (msg.msg.src, msg.msg.dst) { + (IpAddr::V6(src), IpAddr::V6(dst)) => { + message_stack + .data_plane + .lock() + .unwrap() + .inject_message_packet( + src, + dst, + mc.into_inner().into_inner(), + ); + } + _ => debug!( + "Can only send messages between two IPv6 addresses" + ), + } + chunk.chunk_transmit_state = + ChunkTransmitState::Sent(time::Instant::now()); + } + ChunkTransmitState::Sent(t) => { + if t.elapsed().as_secs() >= 1 { + // retransmit + let mut mp = MessagePacket::new(PacketBuffer::new()); + mp.header_mut().set_message_id(id); + + let mut mc = MessageChunk::new(mp); + mc.set_chunk_idx(chunk.chunk_idx as u64); + mc.set_chunk_offset(chunk.chunk_offset as u64); + if let Err(e) = mc.set_chunk_data( + &msg.msg.data[chunk.chunk_offset + ..chunk.chunk_offset + chunk.chunk_size], + ) { + error!("Failed to generate and send chunk: {e}"); + }; + + match (msg.msg.src, msg.msg.dst) { + (IpAddr::V6(src), IpAddr::V6(dst)) => { + message_stack + .data_plane + .lock() + .unwrap() + .inject_message_packet( + src, + dst, + mc.into_inner().into_inner(), + ); + } + _ => debug!( + "Can only send messages between two IPv6 addresses" + ), + } + chunk.chunk_transmit_state = + ChunkTransmitState::Sent(time::Instant::now()); + } + } + ChunkTransmitState::Acked => { + // chunk has been acknowledged, nothing to do here. + } + } + } + + // If every chunk is acked, send the done packet. + if all_acked { + let mut mp = MessagePacket::new(PacketBuffer::new()); + mp.header_mut().set_message_id(id); + + let mut md = MessageDone::new(mp); + md.set_chunk_count(msg.chunks.len() as u64); + md.set_checksum(msg.msg.checksum()); + + match (msg.msg.src, msg.msg.dst) { + (IpAddr::V6(src), IpAddr::V6(dst)) => { + message_stack + .data_plane + .lock() + .unwrap() + .inject_message_packet( + src, + dst, + md.into_inner().into_inner(), + ); + } + _ => { + debug!("Can only send messages between two IPv6 addresses") + } + }; + } + } + TransmissionState::Received => { + // Nothing to do if the remote acknowledged receipt. + } + TransmissionState::Read => { + // Nothing to do if the remote acknowledged that the message is read. + } + TransmissionState::Aborted => { + // Nothing to do if we aborted the message. + } + }; + } else { + // If the message is gone, just exit + return; + } + }, + _ = deadline.tick() => { + // The first time we get a tick to abort, abort the message if it is not + // received yet. + // The second time, clean up the storage. + if !aborted { + aborted = true; + if let Some(msg) = message_stack.outbox.lock().unwrap().msges.get_mut(&id) { + if matches!(msg.state, TransmissionState::Init | TransmissionState::InProgress) { + msg.state = TransmissionState::Aborted; + + // Inform receiver of message abortion. + let mut mp = MessagePacket::new(PacketBuffer::new()); + mp.header_mut().set_message_id(id); + mp.header_mut().flags_mut().set_aborted(); + + + match (msg.msg.src, msg.msg.dst) { + (IpAddr::V6(src), IpAddr::V6(dst)) => { + message_stack + .data_plane + .lock() + .unwrap() + .inject_message_packet( + src, + dst, + mp.into_inner(), + ); + } + _ => { + debug!("Can only send messages between two IPv6 addresses") + } + }; + } + } + continue + } + + // Second tick, clean up. + message_stack.outbox.lock().unwrap().msges.remove(&id); + return + } + } + } + }); + + Ok((id, subscription)) + } + + /// Get information about the status of an outbound message. + pub fn message_info(&self, id: MessageId) -> Option { + let outbox = self.outbox.lock().unwrap(); + outbox.msges.get(&id).map(|mi| MessageInfo { + dst: mi.msg.dst, + state: match mi.state { + TransmissionState::Init => TransmissionProgress::Pending, + TransmissionState::InProgress => { + let (pending, sent, acked) = mi.chunks.iter().fold( + (0, 0, 0), + |(mut pending, mut sent, mut acked), chunk| { + match chunk.chunk_transmit_state { + ChunkTransmitState::Started => pending += 1, + ChunkTransmitState::Sent(_) => sent += 1, + ChunkTransmitState::Acked => acked += 1, + }; + (pending, sent, acked) + }, + ); + TransmissionProgress::Sending { + pending, + sent, + acked, + } + } + TransmissionState::Received => TransmissionProgress::Received, + TransmissionState::Read => TransmissionProgress::Read, + TransmissionState::Aborted => TransmissionProgress::Aborted, + }, + created: mi + .created + .duration_since(time::UNIX_EPOCH) + .expect("Message was created after the epoch") + .as_secs() as i64, + deadline: mi + .deadline + .duration_since(time::UNIX_EPOCH) + .expect("Message expires after the epoch") + .as_secs() as i64, + msg_len: mi.len, + }) + } + + /// A future which eventually resolves to a new (inbound message)[`ReceivedMessage`], if new messages come in. + /// + /// If pop is false, the message is not removed and the next call of this method will return + /// the same message. + pub async fn message(&self, pop: bool, topic: Option>) -> ReceivedMessage { + // Copy the subscriber since we need mutable access to it. + let mut subscriber = self.subscriber.clone(); + + loop { + // Scope to ensure we drop the lock after we checked for a message and don't hold + // it while waiting for a new notification. + 'check: { + let mut inbox = self.inbox.lock().unwrap(); + // If a filter is set only check for those messages. + if let Some(ref topic) = topic { + if let Some((idx, _)) = inbox + .complete_msges + .iter() + .enumerate() + .find(|(_, v)| &v.topic == topic) + { + return inbox.complete_msges.remove(idx).unwrap(); + } else { + break 'check; + } + } + if let Some(msg) = if pop { + inbox.complete_msges.pop_front() + } else { + inbox.complete_msges.front().cloned() + } { + self.notify_read(&msg); + return msg; + }; + } + + // Sender can never be dropped since we hold a reference to self which contains the + // inbox. + let _ = subscriber.changed().await; + } + } + + /// Notify the sender of a message that it has been read. + fn notify_read(&self, msg: &ReceivedMessage) { + let mut mp = MessagePacket::new(PacketBuffer::new()); + let mut header = mp.header_mut(); + header.set_message_id(msg.id); + header.flags_mut().set_read(); + + debug!("Notify sender we read message {}", msg.id.as_hex()); + + match (msg.src_ip, msg.dst_ip) { + (IpAddr::V6(src), IpAddr::V6(dst)) => { + self.data_plane + .lock() + .unwrap() + // IMPORTANT: dst and src are reversed here. src is the sender of the message, + // which is the destination of this READ notification. + .inject_message_packet(dst, src, mp.into_inner()); + } + _ => { + debug!("Can only send messages between two IPv6 addresses") + } + }; + } +} + +impl Clone for MessageStack { + fn clone(&self) -> Self { + Self { + data_plane: self.data_plane.clone(), + inbox: self.inbox.clone(), + outbox: self.outbox.clone(), + subscriber: self.subscriber.clone(), + reply_subscribers: self.reply_subscribers.clone(), + topic_config: self.topic_config.clone(), + } + } +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageInfo { + /// The receiver of this message. + pub dst: IpAddr, + /// Transmission state of the message. + pub state: TransmissionProgress, + /// Time the message was created (received) by the system. + pub created: i64, + /// Time at which point we will give up sending the message. + pub deadline: i64, + /// Size of the message in bytes. + pub msg_len: usize, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TransmissionProgress { + /// Pending transmission, the remote has not yet acknowledged our init message. + Pending, + /// In transit, the remote acknowledged our init message and we are sending chunks. + Sending { + /// Chunks which have never been sent. + pending: usize, + /// Chunks which have been sent at least once, but haven't been acknowledged. + sent: usize, + /// Chunks which have been acknowledged and won't be sent again. + acked: usize, + }, + /// The remote acknowledged full reception, including checksum verification. + Received, + /// The remote notified us that the message has been read at least once. + Read, + /// We aborted sending this message, the remote __might__ have a full message and process it, + /// but that generally won't be the case. + Aborted, +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MessageId([u8; MESSAGE_ID_SIZE]); + +impl MessageId { + /// Generate a new random `MessageId`. + fn new() -> Self { + let mut id = Self([0u8; 8]); + + id.0.fill(&mut rand::rng()); + + id + } + + /// Get a hex representation of the `MessageId`. + pub fn as_hex(&self) -> String { + faster_hex::hex_string(&self.0) + } + + /// Decode a message id from hex. + pub fn from_hex(input: &[u8]) -> Result> { + let mut dst = [0; 8]; + faster_hex::hex_decode(input, &mut dst)?; + Ok(Self(dst)) + } +} + +impl Serialize for MessageId { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.as_hex()) + } +} + +struct MessageIdVisitor; + +impl Visitor<'_> for MessageIdVisitor { + type Value = MessageId; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("A hex encoded message id (16 characters)") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if v.len() != 16 { + Err(E::custom("Message ID is 16 characters long")) + } else { + let mut backing = [0; 8]; + faster_hex::hex_decode(v.as_bytes(), &mut backing) + .map_err(|_| E::custom("MessageID is not valid hex"))?; + Ok(MessageId(backing)) + } + } +} + +impl<'de> Deserialize<'de> for MessageId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(MessageIdVisitor) + } +} + +/// An owned [`PacketBuffer`] for working with messages. +pub struct MessagePacket { + packet: PacketBuffer, +} + +impl MessagePacket { + /// Create a new `MessagePacket` in the given [`PacketBuffer`]. + pub fn new(mut packet: PacketBuffer) -> Self { + // Set the size used by the buffer to already be sufficient for the header. + packet.set_size(MESSAGE_HEADER_SIZE); + Self { packet } + } + + /// Get a read only reference to the usable space in the buffer. + pub fn buffer(&self) -> &[u8] { + &self.packet.buffer()[MESSAGE_HEADER_SIZE..] + } + + /// Get a mutable reference to the usable space in the buffer. + pub fn buffer_mut(&mut self) -> &mut [u8] { + &mut self.packet.buffer_mut()[MESSAGE_HEADER_SIZE..] + } + + /// Get a read only reference to the header of the `MessagePacket`. + pub fn header(&self) -> MessagePacketHeader { + MessagePacketHeader { + header: self.packet.buffer()[..MESSAGE_HEADER_SIZE] + .try_into() + .expect("Packet contains enough data for a header; qed"), + } + } + + /// Get a mutable reference to the header of the `MessagePacket`. + pub fn header_mut(&mut self) -> MessagePacketHeaderMut { + MessagePacketHeaderMut { + header: <&mut [u8] as TryInto<&mut [u8; MESSAGE_HEADER_SIZE]>>::try_into( + &mut self.packet.buffer_mut()[..MESSAGE_HEADER_SIZE], + ) + .expect("Packet contains enough data for a header; qed"), + } + } + + /// Sets the size used by the buffer of the `MessagePacket`. + pub fn set_used_buffer_size(&mut self, size: usize) { + self.packet.set_size(size + MESSAGE_HEADER_SIZE) + } + + /// Consumes this `MessagePacket`, returning the underlying [`PacketBuffer`]. + pub fn into_inner(self) -> PacketBuffer { + self.packet + } +} + +/// A reference to a header in a message packet. +pub struct MessagePacketHeader<'a> { + header: &'a [u8; MESSAGE_HEADER_SIZE], +} + +/// A mutable reference to a header in a message packet. +pub struct MessagePacketHeaderMut<'a> { + header: &'a mut [u8; MESSAGE_HEADER_SIZE], +} + +// A reference to the flags in a message header. +struct Flags<'a> { + flags: u16, + // We explicitly tie the reference used to create the Flags struct to the lifetime of this + // struct to prevent modification of flags while we have a Flags struct. + _marker: PhantomData<&'a MessagePacketHeader<'a>>, +} + +impl Flags<'_> { + /// Check if the MESSAGE_INIT flag is set on the header. + fn init(&self) -> bool { + self.flags & FLAG_MESSAGE_INIT != 0 + } + + /// Check if the MESSAGE_DONE flag is set on the header. + fn done(&self) -> bool { + self.flags & FLAG_MESSAGE_DONE != 0 + } + + /// Check if the MESSAGE_ABORTED flag is set on the header. + fn aborted(&self) -> bool { + self.flags & FLAG_MESSAGE_ABORTED != 0 + } + + /// Check if the MESSAGE_CHUNK flag is set on the header. + fn chunk(&self) -> bool { + self.flags & FLAG_MESSAGE_CHUNK != 0 + } + + /// Check if the MESSAGE_READ flag is set on the header. + fn read(&self) -> bool { + self.flags & FLAG_MESSAGE_READ != 0 + } + + /// Check if the MESSAGE_REPLY flag is set on the header. + fn reply(&self) -> bool { + self.flags & FLAG_MESSAGE_REPLY != 0 + } + + /// Check if the MESSAGE_ACK flag is set on the header. + fn ack(&self) -> bool { + self.flags & FLAG_MESSAGE_ACK != 0 + } +} + +impl fmt::Binary for Flags<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("{:016b}", self.flags)) + } +} + +/// A mutable reference to the flags in a message header. +// We keep a separate struct because creating a u16 from a byte buffer will write the data in +// native endianness, but we need big endian. So we add a drop implementation which forces a big +// endian writeback to the buffer. +struct FlagsMut<'a, 'b> { + header: &'b mut MessagePacketHeaderMut<'a>, + flags: u16, +} + +impl Drop for FlagsMut<'_, '_> { + fn drop(&mut self) { + // Explicitly write back the flags in big endian format + self.header[MESSAGE_ID_SIZE..MESSAGE_ID_SIZE + 2].copy_from_slice(&self.flags.to_be_bytes()) + } +} + +impl FlagsMut<'_, '_> { + /// Sets the MESSAGE_INIT flag on the header. + fn set_init(&mut self) { + self.flags |= FLAG_MESSAGE_INIT; + } + + /// Sets the MESSAGE_DONE flag on the header. + fn set_done(&mut self) { + self.flags |= FLAG_MESSAGE_DONE; + } + + /// Sets the MESSAGE_ABORTED flag on the header. + fn set_aborted(&mut self) { + self.flags |= FLAG_MESSAGE_ABORTED; + } + + /// Sets the MESSAGE_CHUNK flag on the header. + fn set_chunk(&mut self) { + self.flags |= FLAG_MESSAGE_CHUNK; + } + + /// Sets the MESSAGE_READ flag on the header. + fn set_read(&mut self) { + self.flags |= FLAG_MESSAGE_READ; + } + + /// Sets the MESSAGE_REPLY flag on the header. + fn set_reply(&mut self) { + self.flags |= FLAG_MESSAGE_REPLY; + } + + /// Sets the MESSAGE_ACK flag on the header. + fn set_ack(&mut self) { + self.flags |= FLAG_MESSAGE_ACK; + } +} + +// Header layout: +// - 8 bytes message id +// - 2 bytes flags +// - 2 bytes reserved +impl MessagePacketHeader<'_> { + /// Get the [`MessageId`] from the buffer. + fn message_id(&self) -> MessageId { + MessageId( + self.header[..MESSAGE_ID_SIZE] + .try_into() + .expect("Buffer is properly sized; qed"), + ) + } + + /// Get a reference to the [`Flags`] in this header. + fn flags(&self) -> Flags<'_> { + let flags = u16::from_be_bytes( + self.header[MESSAGE_ID_SIZE..MESSAGE_ID_SIZE + 2] + .try_into() + .expect("Slice has a length of 2 which is valid for a u16; qed"), + ); + Flags { + flags, + _marker: PhantomData, + } + } +} + +// Header layout: +// - 8 bytes message id +// - 2 bytes flags +// - 2 bytes reserved +impl<'a> MessagePacketHeaderMut<'a> { + /// Set the [`MessageId`] in the buffer to the provided value. + fn set_message_id(&mut self, mid: MessageId) { + self.header[..MESSAGE_ID_SIZE].copy_from_slice(&mid.0[..]); + } + + /// Get a reference to the [`Flags`] in this header. + // TODO: fix tests to not use this method + #[allow(dead_code)] + fn flags(&self) -> Flags<'_> { + let flags = u16::from_be_bytes( + self.header[MESSAGE_ID_SIZE..MESSAGE_ID_SIZE + 2] + .try_into() + .expect("Slice has a length of 2 which is valid for a u16; qed"), + ); + Flags { + flags, + _marker: PhantomData, + } + } + + /// Get a mutable reference to the flags in this header. + // Note: we explicitly name lifetimes here, as elliding would give the mutable ref to self + // lifetime '1, which is not the same as the elided lifetime 'b on the struct, which would then + // cause the compiler to force the FlagsMut struct to as long as self and not be dropped. + fn flags_mut<'b>(&'b mut self) -> FlagsMut<'a, 'b> { + let flags = u16::from_be_bytes( + self.header[MESSAGE_ID_SIZE..MESSAGE_ID_SIZE + 2] + .try_into() + .expect("Slice has a length of 2 which is valid for a u16; qed"), + ); + FlagsMut { + header: self, + flags, + } + } +} + +impl Deref for MessagePacketHeader<'_> { + type Target = [u8; MESSAGE_HEADER_SIZE]; + + fn deref(&self) -> &Self::Target { + self.header + } +} + +impl Deref for MessagePacketHeaderMut<'_> { + type Target = [u8; MESSAGE_HEADER_SIZE]; + + fn deref(&self) -> &Self::Target { + self.header + } +} + +impl DerefMut for MessagePacketHeaderMut<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.header + } +} + +#[derive(Clone)] +pub struct Message { + /// Generated ID, used to identify the message on the wire + id: MessageId, + /// Source IP (ours) + src: IpAddr, + /// Destination IP + dst: IpAddr, + /// An optional topic of the message, useful to differentiate messages before reading. + topic: Vec, + /// Data of the message + data: Vec, +} + +pub struct OutboundMessageInfo { + /// The current state of the message + state: TransmissionState, + /// Timestamp when the message was created (received by this node). + created: time::SystemTime, + /// Timestamp indicating when we stop trying to send the message. + deadline: time::SystemTime, + /// Length of the message. + len: usize, + /// The message to send. + msg: Message, + /// Chunks of the message. + chunks: Vec, +} + +/// A message checksum. In practice this is a 32 byte blake3 digest of the entire message. +pub type MessageChecksum = blake3::Hash; + +impl Message { + /// Calculates the [`MessageChecksum`] of the message. + /// + /// Currently this is a 32 byte blake3 hash. + pub fn checksum(&self) -> MessageChecksum { + blake3::hash(&self.data) + } +} + +impl fmt::Display for PushMessageError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TopicTooLarge => f.write_str("topic too large, topic is limited to 255 bytes"), + } + } +} + +impl std::error::Error for PushMessageError {} + +#[cfg(test)] +mod tests { + + use super::{MessagePacketHeaderMut, MESSAGE_HEADER_SIZE}; + + #[test] + fn set_init_flag() { + let mut buf = [0; MESSAGE_HEADER_SIZE]; + let mut buf_mut = MessagePacketHeaderMut { header: &mut buf }; + buf_mut.flags_mut().set_init(); + + assert!(buf_mut.flags().init()); + assert_eq!(buf_mut.header[8], 0b1000_0000); + } + + #[test] + fn set_done_flag() { + let mut buf = [0; MESSAGE_HEADER_SIZE]; + let mut buf_mut = MessagePacketHeaderMut { header: &mut buf }; + buf_mut.flags_mut().set_done(); + + assert!(buf_mut.flags().done()); + assert_eq!(buf_mut.header[8], 0b0100_0000); + } + + #[test] + fn set_aborted_flag() { + let mut buf = [0; MESSAGE_HEADER_SIZE]; + let mut buf_mut = MessagePacketHeaderMut { header: &mut buf }; + buf_mut.flags_mut().set_aborted(); + + assert!(buf_mut.flags().aborted()); + assert_eq!(buf_mut.header[8], 0b0010_0000); + } + + #[test] + fn set_chunk_flag() { + let mut buf = [0; MESSAGE_HEADER_SIZE]; + let mut buf_mut = MessagePacketHeaderMut { header: &mut buf }; + buf_mut.flags_mut().set_chunk(); + + assert!(buf_mut.flags().chunk()); + assert_eq!(buf_mut.header[8], 0b0001_0000); + } + + #[test] + fn set_read_flag() { + let mut buf = [0; MESSAGE_HEADER_SIZE]; + let mut buf_mut = MessagePacketHeaderMut { header: &mut buf }; + buf_mut.flags_mut().set_read(); + + assert!(buf_mut.flags().read()); + assert_eq!(buf_mut.header[8], 0b0000_1000); + } + + #[test] + fn set_reply_flag() { + let mut buf = [0; MESSAGE_HEADER_SIZE]; + let mut buf_mut = MessagePacketHeaderMut { header: &mut buf }; + buf_mut.flags_mut().set_reply(); + + assert!(buf_mut.flags().reply()); + assert_eq!(buf_mut.header[8], 0b0000_0100); + } + + #[test] + fn set_ack_flag() { + let mut buf = [0; MESSAGE_HEADER_SIZE]; + let mut buf_mut = MessagePacketHeaderMut { header: &mut buf }; + buf_mut.flags_mut().set_ack(); + + assert!(buf_mut.flags().ack()); + assert_eq!(buf_mut.header[8], 0b0000_0001); + } + + #[test] + fn set_mutli_flag() { + let mut buf = [0; MESSAGE_HEADER_SIZE]; + let mut buf_mut = MessagePacketHeaderMut { header: &mut buf }; + buf_mut.flags_mut().set_init(); + buf_mut.flags_mut().set_ack(); + + assert!(buf_mut.flags().ack() && buf_mut.flags().init()); + assert_eq!(buf_mut.header[8], 0b1000_0001); + } +} diff --git a/components/mycelium/mycelium/src/message/chunk.rs b/components/mycelium/mycelium/src/message/chunk.rs new file mode 100644 index 0000000..8c175b3 --- /dev/null +++ b/components/mycelium/mycelium/src/message/chunk.rs @@ -0,0 +1,254 @@ +use std::fmt; + +use super::MessagePacket; + +/// A message representing a "chunk" message. +/// +/// The body of a chunk message has the following structure: +/// - 8 bytes: chunk index +/// - 8 bytes: chunk offset +/// - 8 bytes: chunk size +/// - remainder: chunk data of length based on field 3 +pub struct MessageChunk { + buffer: MessagePacket, +} + +impl MessageChunk { + /// Create a new `MessageChunk` in the provided [`MessagePacket`]. + pub fn new(mut buffer: MessagePacket) -> Self { + buffer.set_used_buffer_size(24); + buffer.header_mut().flags_mut().set_chunk(); + Self { buffer } + } + + /// Return the index of the chunk in the message, as written in the body. + pub fn chunk_idx(&self) -> u64 { + u64::from_be_bytes( + self.buffer.buffer()[..8] + .try_into() + .expect("Buffer contains a size field of valid length; qed"), + ) + } + + /// Set the index of the chunk in the message body. + pub fn set_chunk_idx(&mut self, chunk_idx: u64) { + self.buffer.buffer_mut()[..8].copy_from_slice(&chunk_idx.to_be_bytes()) + } + + /// Return the chunk offset in the message, as written in the body. + pub fn chunk_offset(&self) -> u64 { + u64::from_be_bytes( + self.buffer.buffer()[8..16] + .try_into() + .expect("Buffer contains a size field of valid length; qed"), + ) + } + + /// Set the offset of the chunk in the message body. + pub fn set_chunk_offset(&mut self, chunk_offset: u64) { + self.buffer.buffer_mut()[8..16].copy_from_slice(&chunk_offset.to_be_bytes()) + } + + /// Return the size of the chunk in the message, as written in the body. + pub fn chunk_size(&self) -> u64 { + // Shield against a corrupt value. + u64::min( + u64::from_be_bytes( + self.buffer.buffer()[16..24] + .try_into() + .expect("Buffer contains a size field of valid length; qed"), + ), + self.buffer.buffer().len() as u64 - 24, + ) + } + + /// Set the size of the chunk in the message body. + pub fn set_chunk_size(&mut self, chunk_size: u64) { + self.buffer.buffer_mut()[16..24].copy_from_slice(&chunk_size.to_be_bytes()) + } + + /// Return a reference to the chunk data in the message. + pub fn data(&self) -> &[u8] { + &self.buffer.buffer()[24..24 + self.chunk_size() as usize] + } + + /// Set the chunk data in this message. This will also set the size field to the proper value. + pub fn set_chunk_data(&mut self, data: &[u8]) -> Result<(), InsufficientChunkSpace> { + let buf = self.buffer.buffer_mut(); + let available_space = buf.len() - 24; + + if data.len() > available_space { + return Err(InsufficientChunkSpace { + available: available_space, + needed: data.len(), + }); + } + + // Slicing based on data.len() is fine here as we just checked to make sure we can handle + // this capacity. + buf[24..24 + data.len()].copy_from_slice(data); + + self.set_chunk_size(data.len() as u64); + // Also set the extra space used by the buffer on the underlying packet. + self.buffer.set_used_buffer_size(24 + data.len()); + + Ok(()) + } + + /// Convert the `MessageChunk` into a reply. This does nothing if it is already a reply. + pub fn into_reply(mut self) -> Self { + self.buffer.header_mut().flags_mut().set_ack(); + // We want to leave the length field in tact but don't want to copy the data in the reply. + // This needs additional work on the underlying buffer. + // TODO + self + } + + /// Consumes this `MessageChunk`, returning the underlying [`MessagePacket`]. + pub fn into_inner(self) -> MessagePacket { + self.buffer + } +} + +/// An error indicating not enough space is availbe in a message to set the chunk data. +#[derive(Debug)] +pub struct InsufficientChunkSpace { + /// Amount of space available in the chunk. + pub available: usize, + /// Amount of space needed to set the chunk data + pub needed: usize, +} + +impl fmt::Display for InsufficientChunkSpace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Insufficient capacity available, needed {} bytes, have {} bytes", + self.needed, self.available + ) + } +} + +impl std::error::Error for InsufficientChunkSpace {} + +#[cfg(test)] +mod tests { + use std::array; + + use crate::{crypto::PacketBuffer, message::MessagePacket}; + + use super::MessageChunk; + + #[test] + fn chunk_flag_set() { + let mc = MessageChunk::new(MessagePacket::new(PacketBuffer::new())); + + let mp = mc.into_inner(); + assert!(mp.header().flags().chunk()); + } + + #[test] + fn read_chunk_idx() { + let mut pb = PacketBuffer::new(); + pb.buffer_mut()[12..20].copy_from_slice(&[0, 0, 0, 0, 0, 0, 100, 73]); + + let ms = MessageChunk::new(MessagePacket::new(pb)); + + assert_eq!(ms.chunk_idx(), 25_673); + } + + #[test] + fn write_chunk_idx() { + let mut ms = MessageChunk::new(MessagePacket::new(PacketBuffer::new())); + + ms.set_chunk_idx(723); + + // Since we don't work with packet buffer we don't have to account for the message packet + // header. + assert_eq!(&ms.buffer.buffer()[..8], &[0, 0, 0, 0, 0, 0, 2, 211]); + assert_eq!(ms.chunk_idx(), 723); + } + + #[test] + fn read_chunk_offset() { + let mut pb = PacketBuffer::new(); + pb.buffer_mut()[20..28].copy_from_slice(&[0, 0, 0, 0, 0, 20, 40, 60]); + + let ms = MessageChunk::new(MessagePacket::new(pb)); + + assert_eq!(ms.chunk_offset(), 1_321_020); + } + + #[test] + fn write_chunk_offset() { + let mut ms = MessageChunk::new(MessagePacket::new(PacketBuffer::new())); + + ms.set_chunk_offset(1_000_000); + + // Since we don't work with packet buffer we don't have to account for the message packet + // header. + assert_eq!(&ms.buffer.buffer()[8..16], &[0, 0, 0, 0, 0, 15, 66, 64]); + assert_eq!(ms.chunk_offset(), 1_000_000); + } + + #[test] + fn read_chunk_size() { + let mut pb = PacketBuffer::new(); + pb.buffer_mut()[28..36].copy_from_slice(&[0, 0, 0, 0, 0, 0, 3, 232]); + + let ms = MessageChunk::new(MessagePacket::new(pb)); + + assert_eq!(ms.chunk_size(), 1_000); + } + + #[test] + fn write_chunk_size() { + let mut ms = MessageChunk::new(MessagePacket::new(PacketBuffer::new())); + + ms.set_chunk_size(1_300); + + // Since we don't work with packet buffer we don't have to account for the message packet + // header. + assert_eq!(&ms.buffer.buffer()[16..24], &[0, 0, 0, 0, 0, 0, 5, 20]); + assert_eq!(ms.chunk_size(), 1_300); + } + + #[test] + fn read_chunk_data() { + const CHUNK_DATA: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + let mut pb = PacketBuffer::new(); + // Set data len + pb.buffer_mut()[28..36].copy_from_slice(&CHUNK_DATA.len().to_be_bytes()); + pb.buffer_mut()[36..36 + CHUNK_DATA.len()].copy_from_slice(CHUNK_DATA); + + let ms = MessageChunk::new(MessagePacket::new(pb)); + + assert_eq!(ms.chunk_size(), 16); + assert_eq!(ms.data(), CHUNK_DATA); + } + + #[test] + fn write_chunk_data() { + const CHUNK_DATA: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + let mut ms = MessageChunk::new(MessagePacket::new(PacketBuffer::new())); + + let res = ms.set_chunk_data(CHUNK_DATA); + assert!(res.is_ok()); + + // Since we don't work with packet buffer we don't have to account for the message packet + // header. + // Check and make sure size is properly set. + assert_eq!(&ms.buffer.buffer()[16..24], &[0, 0, 0, 0, 0, 0, 0, 16]); + assert_eq!(ms.chunk_size(), 16); + assert_eq!(ms.data(), CHUNK_DATA); + } + + #[test] + fn write_chunk_data_oversized() { + let data: [u8; 1500] = array::from_fn(|_| 0xFF); + let mut ms = MessageChunk::new(MessagePacket::new(PacketBuffer::new())); + + let res = ms.set_chunk_data(&data); + assert!(res.is_err()); + } +} diff --git a/components/mycelium/mycelium/src/message/done.rs b/components/mycelium/mycelium/src/message/done.rs new file mode 100644 index 0000000..b3cf189 --- /dev/null +++ b/components/mycelium/mycelium/src/message/done.rs @@ -0,0 +1,131 @@ +use super::{MessageChecksum, MessagePacket, MESSAGE_CHECKSUM_LENGTH}; + +/// A message representing a "done" message. +/// +/// The body of a done message has the following structure: +/// - 8 bytes: chunks transmitted +/// - 32 bytes: checksum of the transmitted data +pub struct MessageDone { + buffer: MessagePacket, +} + +impl MessageDone { + /// Create a new `MessageDone` in the provided [`MessagePacket`]. + pub fn new(mut buffer: MessagePacket) -> Self { + buffer.set_used_buffer_size(40); + buffer.header_mut().flags_mut().set_done(); + Self { buffer } + } + + /// Return the amount of chunks in the message, as written in the body. + pub fn chunk_count(&self) -> u64 { + u64::from_be_bytes( + self.buffer.buffer()[..8] + .try_into() + .expect("Buffer contains a size field of valid length; qed"), + ) + } + + /// Set the amount of chunks field of the message body. + pub fn set_chunk_count(&mut self, chunk_count: u64) { + self.buffer.buffer_mut()[..8].copy_from_slice(&chunk_count.to_be_bytes()) + } + + /// Get the checksum of the message from the body. + pub fn checksum(&self) -> MessageChecksum { + MessageChecksum::from_bytes( + self.buffer.buffer()[8..8 + MESSAGE_CHECKSUM_LENGTH] + .try_into() + .expect("Buffer contains enough data for a checksum; qed"), + ) + } + + /// Set the checksum of the message in the body. + pub fn set_checksum(&mut self, checksum: MessageChecksum) { + self.buffer.buffer_mut()[8..8 + MESSAGE_CHECKSUM_LENGTH] + .copy_from_slice(checksum.as_bytes()) + } + + /// Convert the `MessageDone` into a reply. This does nothing if it is already a reply. + pub fn into_reply(mut self) -> Self { + self.buffer.header_mut().flags_mut().set_ack(); + self + } + + /// Consumes this `MessageDone`, returning the underlying [`MessagePacket`]. + pub fn into_inner(self) -> MessagePacket { + self.buffer + } +} + +#[cfg(test)] +mod tests { + use crate::{ + crypto::PacketBuffer, + message::{MessageChecksum, MessagePacket}, + }; + + use super::MessageDone; + + #[test] + fn done_flag_set() { + let md = MessageDone::new(MessagePacket::new(PacketBuffer::new())); + + let mp = md.into_inner(); + assert!(mp.header().flags().done()); + } + + #[test] + fn read_chunk_count() { + let mut pb = PacketBuffer::new(); + pb.buffer_mut()[12..20].copy_from_slice(&[0, 0, 0, 0, 0, 0, 73, 55]); + + let ms = MessageDone::new(MessagePacket::new(pb)); + + assert_eq!(ms.chunk_count(), 18_743); + } + + #[test] + fn write_chunk_count() { + let mut ms = MessageDone::new(MessagePacket::new(PacketBuffer::new())); + + ms.set_chunk_count(10_000); + + // Since we don't work with packet buffer we don't have to account for the message packet + // header. + assert_eq!(&ms.buffer.buffer()[..8], &[0, 0, 0, 0, 0, 0, 39, 16]); + assert_eq!(ms.chunk_count(), 10_000); + } + + #[test] + fn read_checksum() { + const CHECKSUM: MessageChecksum = MessageChecksum::from_bytes([ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, + 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, + 0x1C, 0x1D, 0x1E, 0x1F, + ]); + let mut pb = PacketBuffer::new(); + pb.buffer_mut()[20..52].copy_from_slice(CHECKSUM.as_bytes()); + + let ms = MessageDone::new(MessagePacket::new(pb)); + + assert_eq!(ms.checksum(), CHECKSUM); + } + + #[test] + fn write_checksum() { + const CHECKSUM: MessageChecksum = MessageChecksum::from_bytes([ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, + 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, + 0x1C, 0x1D, 0x1E, 0x1F, + ]); + let mut ms = MessageDone::new(MessagePacket::new(PacketBuffer::new())); + + ms.set_checksum(CHECKSUM); + + // Since we don't work with packet buffer we don't have to account for the message packet + // header. + assert_eq!(&ms.buffer.buffer()[8..40], CHECKSUM.as_bytes()); + assert_eq!(ms.checksum(), CHECKSUM); + } +} diff --git a/components/mycelium/mycelium/src/message/init.rs b/components/mycelium/mycelium/src/message/init.rs new file mode 100644 index 0000000..39f718c --- /dev/null +++ b/components/mycelium/mycelium/src/message/init.rs @@ -0,0 +1,101 @@ +use super::MessagePacket; + +/// A message representing an init message. +/// +/// The body of an init message has the following structure: +/// - 8 bytes size +pub struct MessageInit { + buffer: MessagePacket, +} + +impl MessageInit { + /// Create a new `MessageInit` in the provided [`MessagePacket`]. + pub fn new(mut buffer: MessagePacket) -> Self { + buffer.set_used_buffer_size(9); + buffer.header_mut().flags_mut().set_init(); + Self { buffer } + } + + /// Return the length of the message, as written in the body. + pub fn length(&self) -> u64 { + u64::from_be_bytes( + self.buffer.buffer()[..8] + .try_into() + .expect("Buffer contains a size field of valid length; qed"), + ) + } + + /// Return the topic of the message, as written in the body. + pub fn topic(&self) -> &[u8] { + let topic_len = self.buffer.buffer()[8] as usize; + &self.buffer.buffer()[9..9 + topic_len] + } + + /// Set the length field of the message body. + pub fn set_length(&mut self, length: u64) { + self.buffer.buffer_mut()[..8].copy_from_slice(&length.to_be_bytes()) + } + + /// Set the topic in the message body. + /// + /// # Panics + /// + /// This function panics if the topic is longer than 255 bytes. + pub fn set_topic(&mut self, topic: &[u8]) { + assert!( + topic.len() <= u8::MAX as usize, + "Topic can be 255 bytes long at most" + ); + self.buffer.set_used_buffer_size(9 + topic.len()); + self.buffer.buffer_mut()[8] = topic.len() as u8; + self.buffer.buffer_mut()[9..9 + topic.len()].copy_from_slice(topic); + } + + /// Convert the `MessageInit` into a reply. This does nothing if it is already a reply. + pub fn into_reply(mut self) -> Self { + self.buffer.header_mut().flags_mut().set_ack(); + self + } + + /// Consumes this `MessageInit`, returning the underlying [`MessagePacket`]. + pub fn into_inner(self) -> MessagePacket { + self.buffer + } +} + +#[cfg(test)] +mod tests { + use crate::{crypto::PacketBuffer, message::MessagePacket}; + + use super::MessageInit; + + #[test] + fn init_flag_set() { + let mi = MessageInit::new(MessagePacket::new(PacketBuffer::new())); + + let mp = mi.into_inner(); + assert!(mp.header().flags().init()); + } + + #[test] + fn read_length() { + let mut pb = PacketBuffer::new(); + pb.buffer_mut()[12..20].copy_from_slice(&[0, 0, 0, 0, 2, 3, 4, 5]); + + let ms = MessageInit::new(MessagePacket::new(pb)); + + assert_eq!(ms.length(), 33_752_069); + } + + #[test] + fn write_length() { + let mut ms = MessageInit::new(MessagePacket::new(PacketBuffer::new())); + + ms.set_length(3_432_634_632); + + // Since we don't work with packet buffer we don't have to account for the message packet + // header. + assert_eq!(&ms.buffer.buffer()[..8], &[0, 0, 0, 0, 204, 153, 217, 8]); + assert_eq!(ms.length(), 3_432_634_632); + } +} diff --git a/components/mycelium/mycelium/src/message/topic.rs b/components/mycelium/mycelium/src/message/topic.rs new file mode 100644 index 0000000..5973393 --- /dev/null +++ b/components/mycelium/mycelium/src/message/topic.rs @@ -0,0 +1,230 @@ +use crate::subnet::Subnet; +use core::fmt; +use serde::{ + de::{Deserialize, Deserializer, MapAccess, Visitor}, + Deserialize as DeserializeMacro, +}; +use std::collections::HashMap; +use std::path::PathBuf; + +/// Configuration for a topic whitelist, including allowed subnets and optional forward socket +#[derive(Debug, Default, Clone)] +pub struct TopicWhitelistConfig { + /// Subnets that are allowed to send messages to this topic + subnets: Vec, + /// Optional Unix domain socket path to forward messages to + forward_socket: Option, +} + +impl TopicWhitelistConfig { + /// Create a new empty whitelist config + pub fn new() -> Self { + Self::default() + } + + /// Get the list of whitelisted subnets + pub fn subnets(&self) -> &Vec { + &self.subnets + } + + /// Get the forward socket path, if any + pub fn forward_socket(&self) -> Option<&PathBuf> { + self.forward_socket.as_ref() + } + + /// Set the forward socket path + pub fn set_forward_socket(&mut self, path: Option) { + self.forward_socket = path; + } + + /// Add a subnet to the whitelist + pub fn add_subnet(&mut self, subnet: Subnet) { + self.subnets.push(subnet); + } + + /// Remove a subnet from the whitelist + pub fn remove_subnet(&mut self, subnet: &Subnet) { + self.subnets.retain(|s| s != subnet); + } +} + +#[derive(Debug, Default, Clone)] +pub struct TopicConfig { + /// The default action to to take if no acl is defined for a topic. + default: MessageAction, + /// Explicitly configured whitelists for topics. Ip's which aren't part of the whitelist will + /// not be allowed to send messages to that topic. If a topic is not in this map, the default + /// action will be used. + whitelist: HashMap, TopicWhitelistConfig>, +} + +impl TopicConfig { + /// Get the [`default action`](MessageAction) if the topic is not configured. + pub fn default(&self) -> MessageAction { + self.default + } + + /// Set the default [`action`](MessageAction) which does not have a whitelist configured. + pub fn set_default(&mut self, default: MessageAction) { + self.default = default; + } + + /// Get the fully configured whitelist + pub fn whitelist(&self) -> &HashMap, TopicWhitelistConfig> { + &self.whitelist + } + + /// Insert a new topic in the whitelist, without any configured allowed sources. + pub fn add_topic_whitelist(&mut self, topic: Vec) { + self.whitelist.entry(topic).or_default(); + } + + /// Set the forward socket for a topic. Does nothing if the topic doesn't exist. + pub fn set_topic_forward_socket(&mut self, topic: Vec, socket_path: Option) { + self.whitelist + .entry(topic) + .and_modify(|c| c.set_forward_socket(socket_path)); + } + + /// Get the forward socket for a topic, if any. + pub fn get_topic_forward_socket(&self, topic: &Vec) -> Option<&PathBuf> { + self.whitelist + .get(topic) + .and_then(|config| config.forward_socket()) + } + + /// Remove a topic from the whitelist. Future messages will follow the default action. + pub fn remove_topic_whitelist(&mut self, topic: &Vec) { + self.whitelist.remove(topic); + } + + /// Adds a new whitelisted source for a topic. This creates the topic if it does not exist yet. + pub fn add_topic_whitelist_src(&mut self, topic: Vec, src: Subnet) { + self.whitelist.entry(topic).or_default().add_subnet(src); + } + + /// Removes a whitelisted source for a topic. + /// + /// If the last source is removed for a topic, the entry remains, and must be cleared by calling + /// [`Self::remove_topic_whitelist`] to fall back to the default action. Note that an empty + /// whitelist effectively blocks all messages for a topic. + /// + /// This does nothing if the topic does not exist. + pub fn remove_topic_whitelist_src(&mut self, topic: &Vec, src: Subnet) { + if let Some(whitelist_config) = self.whitelist.get_mut(topic) { + whitelist_config.remove_subnet(&src); + } + } +} + +#[derive(Debug, Default, Clone, Copy, DeserializeMacro)] +pub enum MessageAction { + /// Accept the message + #[default] + Accept, + /// Reject the message + Reject, +} + +// Helper function to parse a subnet from a string +fn parse_subnet_str(s: &str) -> Result +where + E: serde::de::Error, +{ + // Try to parse as a subnet (with prefix) + if let Ok(ipnet) = s.parse::() { + return Subnet::new(ipnet.addr(), ipnet.prefix_len()) + .map_err(|e| serde::de::Error::custom(format!("Invalid subnet prefix length: {e}"))); + } + + // Try to parse as an IP address (convert to /32 or /128 subnet) + if let Ok(ip) = s.parse::() { + let prefix_len = match ip { + std::net::IpAddr::V4(_) => 32, + std::net::IpAddr::V6(_) => 128, + }; + return Subnet::new(ip, prefix_len) + .map_err(|e| serde::de::Error::custom(format!("Invalid subnet prefix length: {e}"))); + } + + Err(serde::de::Error::custom(format!( + "Invalid subnet or IP address: {s}", + ))) +} + +// Define a struct for deserializing the whitelist config +#[derive(DeserializeMacro)] +struct WhitelistConfigData { + #[serde(default)] + subnets: Vec, + #[serde(default)] + forward_socket: Option, +} + +// Add this implementation right after the TopicConfig struct definition +impl<'de> Deserialize<'de> for TopicConfig { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct TopicConfigVisitor; + + impl<'de> Visitor<'de> for TopicConfigVisitor { + type Value = TopicConfig; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a topic configuration") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut default = MessageAction::default(); + let mut whitelist = HashMap::new(); + + while let Some(key) = map.next_key::()? { + if key == "default" { + default = map.next_value()?; + } else { + // Try to parse as a WhitelistConfigData first + if let Ok(config_data) = map.next_value::() { + let mut whitelist_config = TopicWhitelistConfig::default(); + + // Process subnets + for subnet_str in config_data.subnets { + let subnet = parse_subnet_str(&subnet_str)?; + whitelist_config.add_subnet(subnet); + } + + // Process forward_socket + if let Some(socket_path) = config_data.forward_socket { + whitelist_config + .set_forward_socket(Some(PathBuf::from(socket_path))); + } + + // Convert string key to Vec + whitelist.insert(key.into_bytes(), whitelist_config); + } else { + // Fallback to old format: just a list of subnets + let subnet_strs = map.next_value::>()?; + let mut whitelist_config = TopicWhitelistConfig::default(); + + for subnet_str in subnet_strs { + let subnet = parse_subnet_str(&subnet_str)?; + whitelist_config.add_subnet(subnet); + } + + // Convert string key to Vec + whitelist.insert(key.into_bytes(), whitelist_config); + } + } + } + + Ok(TopicConfig { default, whitelist }) + } + } + + deserializer.deserialize_map(TopicConfigVisitor) + } +} diff --git a/components/mycelium/mycelium/src/metric.rs b/components/mycelium/mycelium/src/metric.rs new file mode 100644 index 0000000..3226a98 --- /dev/null +++ b/components/mycelium/mycelium/src/metric.rs @@ -0,0 +1,144 @@ +//! Dedicated logic for +//! [metrics](https://datatracker.ietf.org/doc/html/rfc8966#metric-computation). + +use core::fmt; +use std::ops::{Add, Sub}; + +/// Value of the infinite metric. +const METRIC_INFINITE: u16 = 0xFFFF; + +/// A `Metric` is used to indicate the cost associated with a route. A lower Metric means a route +/// is more favorable. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub struct Metric(u16); + +impl Metric { + /// Create a new `Metric` with the given value. + pub const fn new(value: u16) -> Self { + Metric(value) + } + + /// Creates a new infinite `Metric`. + pub const fn infinite() -> Self { + Metric(METRIC_INFINITE) + } + + /// Checks if this metric indicates a retracted route. + pub const fn is_infinite(&self) -> bool { + self.0 == METRIC_INFINITE + } + + /// Checks if this metric represents a directly connected route. + pub const fn is_direct(&self) -> bool { + self.0 == 0 + } + + /// Computes the absolute value of the difference between this and another `Metric`. + pub fn delta(&self, rhs: &Self) -> Metric { + Metric(if self > rhs { + self.0 - rhs.0 + } else { + rhs.0 - self.0 + }) + } +} + +impl fmt::Display for Metric { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_infinite() { + f.pad("Infinite") + } else { + f.write_fmt(format_args!("{}", self.0)) + } + } +} + +impl From for Metric { + fn from(value: u16) -> Self { + Metric(value) + } +} + +impl From for u16 { + fn from(value: Metric) -> Self { + value.0 + } +} + +impl Add for Metric { + type Output = Self; + + fn add(self, rhs: Metric) -> Self::Output { + if self.is_infinite() || rhs.is_infinite() { + return Metric::infinite(); + } + Metric( + self.0 + .checked_add(rhs.0) + .map(|r| if r == u16::MAX { r - 1 } else { r }) + .unwrap_or(u16::MAX - 1), + ) + } +} + +impl Add<&Metric> for &Metric { + type Output = Metric; + + fn add(self, rhs: &Metric) -> Self::Output { + if self.is_infinite() || rhs.is_infinite() { + return Metric::infinite(); + } + Metric( + self.0 + .checked_add(rhs.0) + .map(|r| if r == u16::MAX { r - 1 } else { r }) + .unwrap_or(u16::MAX - 1), + ) + } +} + +impl Add<&Metric> for Metric { + type Output = Self; + + fn add(self, rhs: &Metric) -> Self::Output { + if self.is_infinite() || rhs.is_infinite() { + return Metric::infinite(); + } + Metric( + self.0 + .checked_add(rhs.0) + .map(|r| if r == u16::MAX { r - 1 } else { r }) + .unwrap_or(u16::MAX - 1), + ) + } +} + +impl Add for &Metric { + type Output = Metric; + + fn add(self, rhs: Metric) -> Self::Output { + if self.is_infinite() || rhs.is_infinite() { + return Metric::infinite(); + } + Metric( + self.0 + .checked_add(rhs.0) + .map(|r| if r == u16::MAX { r - 1 } else { r }) + .unwrap_or(u16::MAX - 1), + ) + } +} + +impl Sub for Metric { + type Output = Metric; + + fn sub(self, rhs: Metric) -> Self::Output { + if rhs.is_infinite() { + panic!("Can't subtract an infinite metric"); + } + if self.is_infinite() { + return Metric::infinite(); + } + Metric(self.0.saturating_sub(rhs.0)) + } +} diff --git a/components/mycelium/mycelium/src/metrics.rs b/components/mycelium/mycelium/src/metrics.rs new file mode 100644 index 0000000..6385556 --- /dev/null +++ b/components/mycelium/mycelium/src/metrics.rs @@ -0,0 +1,195 @@ +//! This module is used for collection of runtime metrics of a `mycelium` system. The main item of +//! interest is the [`Metrics`] trait. Users can provide their own implementation of this, or use +//! the default provided implementation to disable gathering metrics. + +use crate::peer_manager::PeerType; + +/// The collection of all metrics exported by a [`mycelium node`](crate::Node). It is up to the +/// user to provide an implementation which implements the methods for metrics they are interested +/// in. All methods have a default implementation, so if the user is not interested in any metrics, +/// a NOOP handler can be implemented as follows: +/// +/// ```rust +/// use mycelium::metrics::Metrics; +/// +/// #[derive(Clone)] +/// struct NoMetrics; +/// impl Metrics for NoMetrics {} +/// ``` +pub trait Metrics { + /// The [`Router`](crate::router::Router) received a new Hello TLV from a peer. + #[inline] + fn router_process_hello(&self) {} + + /// The [`Router`](crate::router::Router) received a new IHU TLV from a peer. + #[inline] + fn router_process_ihu(&self) {} + + /// The [`Router`](crate::router::Router) received a new Seqno request TLV from a peer. + #[inline] + fn router_process_seqno_request(&self) {} + + /// The [`Router`](crate::router::Router) received a new Route request TLV from a peer. + /// Additionally, it is recorded if this is a wildcard request (route table dump request) + /// or a request for a specific subnet. + #[inline] + fn router_process_route_request(&self, _wildcard: bool) {} + + /// The [`Router`](crate::router::Router) received a new Update TLV from a peer. + #[inline] + fn router_process_update(&self) {} + + /// The [`Router`](crate::router::Router) tried to send an update to a peer, but before sending + /// it we found out the peer is actually already dead. + /// + /// This can happen, since a peer is a remote entity we have no control over, and it can be + /// removed at any time for any reason. However, in normal operation, the amount of times this + /// happens should be fairly small compared to the amount of updates we send/receive. + #[inline] + fn router_update_dead_peer(&self) {} + + /// The amount of TLV's received from peers, to be processed by the + /// [`Router`](crate::router::Router). + #[inline] + fn router_received_tlv(&self) {} + + /// The [`Router`](crate::router::Router) dropped a received TLV before processing it, as the + /// peer who sent it has already died in the meantime. + #[inline] + fn router_tlv_source_died(&self) {} + + /// The [`Router`](crate::router::Router) dropped a received TLV before processing it, because + /// it coulnd't keep up + #[inline] + fn router_tlv_discarded(&self) {} + + /// A [`Peer`](crate::peer::Peer) was added to the [`Router`](crate::router::Router). + #[inline] + fn router_peer_added(&self) {} + + /// A [`Peer`](crate::peer::Peer) was removed from the [`Router`](crate::router::Router). + #[inline] + fn router_peer_removed(&self) {} + + /// A [`Peer`](crate::peer::Peer) informed the [`Router`](crate::router::Router) it died, or + /// the router otherwise noticed the Peer is dead. + #[inline] + fn router_peer_died(&self) {} + + /// The [`Router`](crate::router::Router) ran a route selection procedure. + #[inline] + fn router_route_selection_ran(&self) {} + + /// A [`SourceKey`](crate::source_table::SourceKey) expired and got cleaned up by the [`Router`](crate::router::Router). + #[inline] + fn router_source_key_expired(&self) {} + + /// A [`RouteKey`](crate::routing_table::RouteKey) expired, and the router either set the + /// [`Metric`](crate::metric::Metric) of the route to infinity, or cleaned up the route entry + /// altogether. + #[inline] + fn router_route_key_expired(&self, _removed: bool) {} + + /// A route which expired was actually the selected route for the + /// [`Subnet`](crate::subnet::Subnet). Note that [`Self::router_route_key_expired`] will + /// also have been called. + #[inline] + fn router_selected_route_expired(&self) {} + + /// The [`Router`](crate::router::Router) sends a "triggered" update to it's peers. + #[inline] + fn router_triggered_update(&self) {} + + /// The [`Router`](crate::router::Router) extracted a packet for the local subnet. + #[inline] + fn router_route_packet_local(&self) {} + + /// The [`Router`](crate::router::Router) forwarded a packet to a peer. + #[inline] + fn router_route_packet_forward(&self) {} + + /// The [`Router`](crate::router::Router) dropped a packet it was routing because it's TTL + /// reached 0. + #[inline] + fn router_route_packet_ttl_expired(&self) {} + + /// The [`Router`](crate::router::Router) dropped a packet it was routing because there was no + /// route for the destination IP. + #[inline] + fn router_route_packet_no_route(&self) {} + + /// The [`Router`](crate::router::Router) replied to a seqno request with a local route, which + /// is more recent (bigger seqno) than the request. + #[inline] + fn router_seqno_request_reply_local(&self) {} + + /// The [`Router`](crate::router::Router) replied to a seqno request by bumping its own seqno + /// and advertising the local route. + #[inline] + fn router_seqno_request_bump_seqno(&self) {} + + /// The [`Router`](crate::router::Router) dropped a seqno request because the TTL reached 0. + #[inline] + fn router_seqno_request_dropped_ttl(&self) {} + + /// The [`Router`](crate::router::Router) forwarded a seqno request to a feasible route. + #[inline] + fn router_seqno_request_forward_feasible(&self) {} + + /// The [`Router`](crate::router::Router) forwarded a seqno request to a (potentially) + /// unfeasible route. + #[inline] + fn router_seqno_request_forward_unfeasible(&self) {} + + /// The [`Router`](crate::router::Router) dropped a seqno request becase none of the other + /// handling methods applied. + #[inline] + fn router_seqno_request_unhandled(&self) {} + + /// The [`time`](std::time::Duration) used by the [`Router`](crate::router::Router) to handle a + /// control packet. + #[inline] + fn router_time_spent_handling_tlv(&self, _duration: std::time::Duration, _tlv_type: &str) {} + + /// The [`time`](std::time::Duration) used by the [`Router`](crate::router::Router) to + /// periodically propagate selected routes to peers. + #[inline] + fn router_time_spent_periodic_propagating_selected_routes( + &self, + _duration: std::time::Duration, + ) { + } + + /// An update was processed and accepted by the router, but did not run route selection. + #[inline] + fn router_update_skipped_route_selection(&self) {} + + /// An update was denied by a configured filter. + #[inline] + fn router_update_denied_by_filter(&self) {} + + /// An update was accepted by the router filters, but was otherwise unfeasible or a retraction, + /// for an unknown subnet. + #[inline] + fn router_update_not_interested(&self) {} + + /// A new [`Peer`](crate::peer::Peer) was added to the + /// [`PeerManager`](crate::peer_manager::PeerManager) while it is running. + #[inline] + fn peer_manager_peer_added(&self, _pt: PeerType) {} + + /// Sets the amount of [`Peers`](crate::peer::Peer) known by the + /// [`PeerManager`](crate::peer_manager::PeerManager). + #[inline] + fn peer_manager_known_peers(&self, _amount: usize) {} + + /// The [`PeerManager`](crate::peer_manager::PeerManager) started an attempt to connect to a + /// remote endpoint. + #[inline] + fn peer_manager_connection_attempted(&self) {} + + /// The [`PeerManager`](crate::peer_manager::PeerManager) finished an attempt to connect to a + /// remote endpoint. The connection could have failed. + #[inline] + fn peer_manager_connection_finished(&self) {} +} diff --git a/components/mycelium/mycelium/src/packet.rs b/components/mycelium/mycelium/src/packet.rs new file mode 100644 index 0000000..8cd900c --- /dev/null +++ b/components/mycelium/mycelium/src/packet.rs @@ -0,0 +1,134 @@ +use bytes::{Buf, BufMut, BytesMut}; +pub use control::ControlPacket; +pub use data::DataPacket; +use tokio_util::codec::{Decoder, Encoder}; + +mod control; +mod data; + +/// Current version of the protocol being used. +const PROTOCOL_VERSION: u8 = 1; + +/// The size of a `Packet` header on the wire, in bytes. +const PACKET_HEADER_SIZE: usize = 4; + +#[derive(Debug, Clone)] +pub enum Packet { + DataPacket(DataPacket), + ControlPacket(ControlPacket), +} + +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum PacketType { + DataPacket = 0, + ControlPacket = 1, +} + +pub struct Codec { + packet_type: Option, + data_packet_codec: data::Codec, + control_packet_codec: control::Codec, +} + +impl Codec { + pub fn new() -> Self { + Codec { + packet_type: None, + data_packet_codec: data::Codec::new(), + control_packet_codec: control::Codec::new(), + } + } +} + +impl Decoder for Codec { + type Item = Packet; + type Error = std::io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + // Determine the packet_type + let packet_type = if let Some(packet_type) = self.packet_type { + packet_type + } else { + // Check we can read the header + if src.remaining() <= PACKET_HEADER_SIZE { + return Ok(None); + } + + let mut header = [0; PACKET_HEADER_SIZE]; + header.copy_from_slice(&src[..PACKET_HEADER_SIZE]); + src.advance(PACKET_HEADER_SIZE); + + // For now it's a hard error to not follow the 1 defined protocol version + if header[0] != PROTOCOL_VERSION { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Unknown protocol version", + )); + }; + + let packet_type_byte = header[1]; + let packet_type = match packet_type_byte { + 0 => PacketType::DataPacket, + 1 => PacketType::ControlPacket, + _ => { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid packet type", + )); + } + }; + + self.packet_type = Some(packet_type); + + packet_type + }; + + // Decode packet based on determined packet_type + match packet_type { + PacketType::DataPacket => { + match self.data_packet_codec.decode(src) { + Ok(Some(p)) => { + self.packet_type = None; // Reset state + Ok(Some(Packet::DataPacket(p))) + } + Ok(None) => Ok(None), + Err(e) => Err(e), + } + } + PacketType::ControlPacket => { + match self.control_packet_codec.decode(src) { + Ok(Some(p)) => { + self.packet_type = None; // Reset state + Ok(Some(Packet::ControlPacket(p))) + } + Ok(None) => Ok(None), + Err(e) => Err(e), + } + } + } + } +} + +impl Encoder for Codec { + type Error = std::io::Error; + + fn encode(&mut self, item: Packet, dst: &mut BytesMut) -> Result<(), Self::Error> { + match item { + Packet::DataPacket(datapacket) => { + dst.put_slice(&[PROTOCOL_VERSION, 0, 0, 0]); + self.data_packet_codec.encode(datapacket, dst) + } + Packet::ControlPacket(controlpacket) => { + dst.put_slice(&[PROTOCOL_VERSION, 1, 0, 0]); + self.control_packet_codec.encode(controlpacket, dst) + } + } + } +} + +impl Default for Codec { + fn default() -> Self { + Self::new() + } +} diff --git a/components/mycelium/mycelium/src/packet/control.rs b/components/mycelium/mycelium/src/packet/control.rs new file mode 100644 index 0000000..ea4baa7 --- /dev/null +++ b/components/mycelium/mycelium/src/packet/control.rs @@ -0,0 +1,64 @@ +use std::{io, net::IpAddr, time::Duration}; + +use bytes::BytesMut; +use tokio_util::codec::{Decoder, Encoder}; + +use crate::{ + babel, metric::Metric, peer::Peer, router_id::RouterId, sequence_number::SeqNo, subnet::Subnet, +}; + +pub type ControlPacket = babel::Tlv; + +pub struct Codec { + // TODO: wrapper to make it easier to deserialize + codec: babel::Codec, +} + +impl ControlPacket { + pub fn new_hello(dest_peer: &Peer, interval: Duration) -> Self { + let tlv: babel::Tlv = + babel::Hello::new_unicast(dest_peer.hello_seqno(), (interval.as_millis() / 10) as u16) + .into(); + dest_peer.increment_hello_seqno(); + tlv + } + + pub fn new_ihu(rx_cost: Metric, interval: Duration, dest_address: Option) -> Self { + babel::Ihu::new(rx_cost, (interval.as_millis() / 10) as u16, dest_address).into() + } + + pub fn new_update( + interval: Duration, + seqno: SeqNo, + metric: Metric, + subnet: Subnet, + router_id: RouterId, + ) -> Self { + babel::Update::new(interval, seqno, metric, subnet, router_id).into() + } +} + +impl Codec { + pub fn new() -> Self { + Codec { + codec: babel::Codec::new(), + } + } +} + +impl Decoder for Codec { + type Item = ControlPacket; + type Error = std::io::Error; + + fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { + self.codec.decode(buf) + } +} + +impl Encoder for Codec { + type Error = io::Error; + + fn encode(&mut self, message: ControlPacket, buf: &mut BytesMut) -> Result<(), Self::Error> { + self.codec.encode(message, buf) + } +} diff --git a/components/mycelium/mycelium/src/packet/data.rs b/components/mycelium/mycelium/src/packet/data.rs new file mode 100644 index 0000000..5e14879 --- /dev/null +++ b/components/mycelium/mycelium/src/packet/data.rs @@ -0,0 +1,154 @@ +use std::net::Ipv6Addr; + +use bytes::{Buf, BufMut, BytesMut}; +use tokio_util::codec::{Decoder, Encoder}; + +/// Size of the header start for a data packet (before the IP addresses). +const DATA_PACKET_HEADER_SIZE: usize = 4; + +/// Mask to extract data length from +const DATA_PACKET_LEN_MASK: u32 = (1 << 16) - 1; + +#[derive(Debug, Clone)] +pub struct DataPacket { + pub raw_data: Vec, // encrypted data itself, then append the nonce + /// Max amount of hops for the packet. + pub hop_limit: u8, + pub src_ip: Ipv6Addr, + pub dst_ip: Ipv6Addr, +} + +pub struct Codec { + header_vals: Option, + src_ip: Option, + dest_ip: Option, +} + +/// Data from the DataPacket header. +#[derive(Clone, Copy)] +struct HeaderValues { + len: u16, + hop_limit: u8, +} + +impl Codec { + pub fn new() -> Self { + Codec { + header_vals: None, + src_ip: None, + dest_ip: None, + } + } +} + +impl Decoder for Codec { + type Item = DataPacket; + type Error = std::io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + // Determine the length of the data + let HeaderValues { len, hop_limit } = if let Some(header_vals) = self.header_vals { + header_vals + } else { + // Check we have enough data to decode + if src.len() < DATA_PACKET_HEADER_SIZE { + return Ok(None); + } + + let raw_header = src.get_u32(); + // Hop limit is the last 8 bits. + let hop_limit = (raw_header & 0xFF) as u8; + let data_len = ((raw_header >> 8) & DATA_PACKET_LEN_MASK) as u16; + let header_vals = HeaderValues { + len: data_len, + hop_limit, + }; + + self.header_vals = Some(header_vals); + + header_vals + }; + + let data_len = len as usize; + + // Determine the source IP + let src_ip = if let Some(src_ip) = self.src_ip { + src_ip + } else { + if src.len() < 16 { + return Ok(None); + } + + // Decode octets + let mut ip_bytes = [0u8; 16]; + ip_bytes.copy_from_slice(&src[..16]); + let src_ip = Ipv6Addr::from(ip_bytes); + src.advance(16); + + self.src_ip = Some(src_ip); + src_ip + }; + + // Determine the destination IP + let dest_ip = if let Some(dest_ip) = self.dest_ip { + dest_ip + } else { + if src.len() < 16 { + return Ok(None); + } + + // Decode octets + let mut ip_bytes = [0u8; 16]; + ip_bytes.copy_from_slice(&src[..16]); + let dest_ip = Ipv6Addr::from(ip_bytes); + src.advance(16); + + self.dest_ip = Some(dest_ip); + dest_ip + }; + + // Check we have enough data to decode + if src.len() < data_len { + return Ok(None); + } + + // Decode octets + let mut data = vec![0u8; data_len]; + data.copy_from_slice(&src[..data_len]); + src.advance(data_len); + + // Reset state + self.header_vals = None; + self.dest_ip = None; + self.src_ip = None; + + Ok(Some(DataPacket { + raw_data: data, + hop_limit, + dst_ip: dest_ip, + src_ip, + })) + } +} + +impl Encoder for Codec { + type Error = std::io::Error; + + fn encode(&mut self, item: DataPacket, dst: &mut BytesMut) -> Result<(), Self::Error> { + dst.reserve(item.raw_data.len() + DATA_PACKET_HEADER_SIZE + 16 + 16); + let mut raw_header = 0; + // Add length of the data + raw_header |= (item.raw_data.len() as u32) << 8; + // And hop limit + raw_header |= item.hop_limit as u32; + dst.put_u32(raw_header); + // Write the source IP + dst.put_slice(&item.src_ip.octets()); + // Write the destination IP + dst.put_slice(&item.dst_ip.octets()); + // Write the data + dst.extend_from_slice(&item.raw_data); + + Ok(()) + } +} diff --git a/components/mycelium/mycelium/src/peer.rs b/components/mycelium/mycelium/src/peer.rs new file mode 100644 index 0000000..5b769b4 --- /dev/null +++ b/components/mycelium/mycelium/src/peer.rs @@ -0,0 +1,401 @@ +use futures::{SinkExt, StreamExt}; +use std::{ + error::Error, + io, + sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + Arc, RwLock, Weak, + }, +}; +use tokio::{ + select, + sync::{mpsc, Notify}, +}; +use tokio_util::codec::Framed; +use tracing::{debug, error, info, trace}; + +use crate::{ + connection::{self, Connection}, + packet::{self, Packet}, +}; +use crate::{ + packet::{ControlPacket, DataPacket}, + sequence_number::SeqNo, +}; + +/// The maximum amount of packets to immediately send if they are ready when the first one is +/// received. +const PACKET_COALESCE_WINDOW: usize = 50; + +/// The default link cost assigned to new peers before their actual cost is known. +/// +/// In theory, the best value would be U16::MAX - 1, however this value would take too long to be +/// flushed out of the smoothed metric. A default of a 1000 (1 second) should be sufficiently large +/// to cover very bad connections, so they also converge to a smaller value. While there is no +/// issue with converging to a higher value (in other words, underestimating the latency to a +/// peer), this means that bad peers would briefly be more likely to be selected. Additionally, +/// since the latency increases, downstream peers would eventually find that the announced route +/// would become unfeasible, and send a seqno request (which should solve this efficiently). As a +/// tradeoff, it means it takes longer for new peers in the network to decrease to their actual +/// metric (in comparisson with a lower starting metric), though this is in itself a usefull thing +/// to have as it means peers joining the network would need to have some stability before being +/// selected as hop. +const DEFAULT_LINK_COST: u16 = 1000; + +/// Multiplier for smoothed metric calculation of the existing smoothed metric. +const EXISTING_METRIC_FACTOR: u32 = 9; +/// Divisor for smoothed metric calcuation of the combined metric +const TOTAL_METRIC_DIVISOR: u32 = 10; + +#[derive(Debug, Clone)] +/// A peer represents a directly connected participant in the network. +pub struct Peer { + inner: Arc, +} + +/// A weak reference to a peer, which does not prevent it from being cleaned up. This can be used +/// to check liveliness of the [`Peer`] instance it originated from. +pub struct PeerRef { + inner: Weak, +} + +impl Peer { + pub fn new( + router_data_tx: mpsc::Sender, + router_control_tx: mpsc::UnboundedSender<(ControlPacket, Peer)>, + connection: C, + dead_peer_sink: mpsc::Sender, + bytes_written: Arc, + bytes_read: Arc, + ) -> Result { + // Wrap connection so we can get access to the counters. + let connection = connection::Tracked::new(bytes_read, bytes_written, connection); + + // Data channel for peer + let (to_peer_data, mut from_routing_data) = mpsc::unbounded_channel::(); + // Control channel for peer + let (to_peer_control, mut from_routing_control) = + mpsc::unbounded_channel::(); + let death_notifier = Arc::new(Notify::new()); + let death_watcher = death_notifier.clone(); + let peer = Peer { + inner: Arc::new(PeerInner { + state: RwLock::new(PeerState::new()), + to_peer_data, + to_peer_control, + connection_identifier: connection.identifier()?, + static_link_cost: connection.static_link_cost()?, + death_notifier, + alive: AtomicBool::new(true), + }), + }; + + // Framed for peer + // Used to send and receive packets from a TCP stream + let framed = Framed::with_capacity(connection, packet::Codec::new(), 128 << 10); + let (mut sink, mut stream) = framed.split(); + + { + let peer = peer.clone(); + + tokio::spawn(async move { + let mut needs_flush = false; + loop { + select! { + // Received over the TCP stream + frame = stream.next() => { + match frame { + Some(Ok(packet)) => { + match packet { + Packet::DataPacket(packet) => { + // An error here means the receiver is dropped/closed, + // this is not recoverable. + if let Err(error) = router_data_tx.send(packet).await{ + error!("Error sending to to_routing_data: {}", error); + break + } + } + Packet::ControlPacket(packet) => { + if let Err(error) = router_control_tx.send((packet, peer.clone())) { + // An error here means the receiver is dropped/closed, + // this is not recoverable. + error!("Error sending to to_routing_control: {}", error); + break + } + + } + } + } + Some(Err(e)) => { + error!("Frame error from {}: {e}", peer.connection_identifier()); + break; + }, + None => { + info!("Stream to {} is closed", peer.connection_identifier()); + break; + } + } + } + + rv = from_routing_data.recv(), if !needs_flush => { + match rv { + None => break, + Some(packet) => { + + needs_flush = true; + + if let Err(e) = sink.feed(Packet::DataPacket(packet)).await { + error!("Failed to feed data packet to connection: {e}"); + break + } + + + for _ in 1..PACKET_COALESCE_WINDOW { + // There can be 2 cases of errors here, empty channel and no more + // senders. In both cases we don't really care at this point. + if let Ok(packet) = from_routing_data.try_recv() { + if let Err(e) = sink.feed(Packet::DataPacket(packet)).await { + error!("Failed to feed data packet to connection: {e}"); + break + } + trace!("Instantly queued ready packet to transfer to peer"); + } else { + // No packets ready, flush currently buffered ones + break + } + } + } + } + } + + rv = from_routing_control.recv(), if !needs_flush => { + match rv { + None => break, + Some(packet) => { + + needs_flush = true; + + if let Err(e) = sink.feed(Packet::ControlPacket(packet)).await { + error!("Failed to feed control packet to connection: {e}"); + break + } + + for _ in 1..PACKET_COALESCE_WINDOW { + // There can be 2 cases of errors here, empty channel and no more + // senders. In both cases we don't really care at this point. + if let Ok(packet) = from_routing_control.try_recv() { + if let Err(e) = sink.feed(Packet::ControlPacket(packet)).await { + error!("Failed to feed data packet to connection: {e}"); + break + } + } else { + // No packets ready, flush currently buffered ones + break + } + } + } + } + } + + r = sink.flush(), if needs_flush => { + if let Err(err) = r { + error!("Failed to flush peer connection: {err}"); + break + } + needs_flush = false; + } + + _ = death_watcher.notified() => { + // Attempt gracefull shutdown + let mut framed = sink.reunite(stream).expect("SplitSink and SplitStream here can only be part of the same original Framned; Qed"); + let _ = framed.close().await; + break; + } + } + } + + // Notify router we are dead, also modify our internal state to declare that. + // Relaxed ordering is fine, we just care that the variable is set. + peer.inner.alive.store(false, Ordering::Relaxed); + let remote_id = peer.connection_identifier().clone(); + debug!("Notifying router peer {remote_id} is dead"); + if let Err(e) = dead_peer_sink.send(peer).await { + error!("Peer {remote_id} could not notify router of termination: {e}"); + } + }); + }; + + Ok(peer) + } + + /// Get current sequence number for this peer. + pub fn hello_seqno(&self) -> SeqNo { + self.inner.state.read().unwrap().hello_seqno + } + + /// Adds 1 to the sequence number of this peer . + pub fn increment_hello_seqno(&self) { + self.inner.state.write().unwrap().hello_seqno += 1; + } + + pub fn time_last_received_hello(&self) -> tokio::time::Instant { + self.inner.state.read().unwrap().time_last_received_hello + } + + pub fn set_time_last_received_hello(&self, time: tokio::time::Instant) { + self.inner.state.write().unwrap().time_last_received_hello = time + } + + /// For sending data packets towards a peer instance on this node. + /// It's send over the to_peer_data channel and read from the corresponding receiver. + /// The receiver sends the packet over the TCP stream towards the destined peer instance on another node + pub fn send_data_packet(&self, data_packet: DataPacket) -> Result<(), Box> { + Ok(self.inner.to_peer_data.send(data_packet)?) + } + + /// For sending control packets towards a peer instance on this node. + /// It's send over the to_peer_control channel and read from the corresponding receiver. + /// The receiver sends the packet over the TCP stream towards the destined peer instance on another node + pub fn send_control_packet(&self, control_packet: ControlPacket) -> Result<(), Box> { + Ok(self.inner.to_peer_control.send(control_packet)?) + } + + /// Get the cost to use the peer, i.e. the additional impact on the [`crate::metric::Metric`] + /// for using this `Peer`. + /// + /// This is a smoothed value, which is calculated over the recent history of link cost. + pub fn link_cost(&self) -> u16 { + self.inner.state.read().unwrap().link_cost + self.inner.static_link_cost + } + + /// Sets the link cost based on the provided value. + /// + /// The link cost is not set to the given value, but rather to an average of recent values. + /// This makes sure short-lived, hard spikes of the link cost of a peer don't influence the + /// routing. + pub fn set_link_cost(&self, new_link_cost: u16) { + // Calculate new link cost by multiplying (i.e. scaling) old and new link cost and + // averaging them. + let mut inner = self.inner.state.write().unwrap(); + inner.link_cost = (((inner.link_cost as u32) * EXISTING_METRIC_FACTOR + + (new_link_cost as u32) * (TOTAL_METRIC_DIVISOR - EXISTING_METRIC_FACTOR)) + / TOTAL_METRIC_DIVISOR) as u16; + } + + /// Identifier for the connection to the `Peer`. + pub fn connection_identifier(&self) -> &String { + &self.inner.connection_identifier + } + + pub fn time_last_received_ihu(&self) -> tokio::time::Instant { + self.inner.state.read().unwrap().time_last_received_ihu + } + + pub fn set_time_last_received_ihu(&self, time: tokio::time::Instant) { + self.inner.state.write().unwrap().time_last_received_ihu = time + } + + /// Notify this `Peer` that it died. + /// + /// While some [`Connection`] types can immediately detect that the connection itself is + /// broken, not all of them can. In this scenario, we need to rely on an outside signal to tell + /// us that we have, in fact, died. + pub fn died(&self) { + self.inner.alive.store(false, Ordering::Relaxed); + self.inner.death_notifier.notify_one(); + } + + /// Checks if the connection of this `Peer` is still alive. + /// + /// For connection types which don't have (real time) state information, this might return a + /// false positive if the connection has actually died, but the Peer did not notice this (yet) + /// and hasn't been informed. + pub fn alive(&self) -> bool { + self.inner.alive.load(Ordering::Relaxed) + } + + /// Create a new [`PeerRef`] that refers to this `Peer` instance. + pub fn refer(&self) -> PeerRef { + PeerRef { + inner: Arc::downgrade(&self.inner), + } + } +} + +impl PeerRef { + /// Contructs a new `PeerRef` which is not associated with any actually [`Peer`]. + /// [`PeerRef::alive`] will always return false when called on this `PeerRef`. + pub fn new() -> Self { + PeerRef { inner: Weak::new() } + } + + /// Check if the connection of the [`Peer`] this `PeerRef` points to is still alive. + pub fn alive(&self) -> bool { + if let Some(peer) = self.inner.upgrade() { + peer.alive.load(Ordering::Relaxed) + } else { + false + } + } + + /// Attempts to convert this `PeerRef` into a full [`Peer`]. + pub fn upgrade(&self) -> Option { + self.inner.upgrade().map(|inner| Peer { inner }) + } +} + +impl Default for PeerRef { + fn default() -> Self { + Self::new() + } +} + +impl PartialEq for Peer { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) + } +} + +#[derive(Debug)] +struct PeerInner { + state: RwLock, + to_peer_data: mpsc::UnboundedSender, + to_peer_control: mpsc::UnboundedSender, + /// Used to identify peer based on its connection params. + connection_identifier: String, + /// Static cost of using this link, to be added to the announced metric for routes through this + /// Peer. + static_link_cost: u16, + /// Channel to notify the connection of its decease. + death_notifier: Arc, + /// Keep track if the connection is alive. + alive: AtomicBool, +} + +#[derive(Debug)] +struct PeerState { + hello_seqno: SeqNo, + time_last_received_hello: tokio::time::Instant, + link_cost: u16, + time_last_received_ihu: tokio::time::Instant, +} + +impl PeerState { + /// Create a new `PeerInner`, holding the mutable state of a [`Peer`] + fn new() -> Self { + // Initialize last_sent_hello_seqno to 0 + let hello_seqno = SeqNo::default(); + let link_cost = DEFAULT_LINK_COST; + // Initialize time_last_received_hello to now + let time_last_received_hello = tokio::time::Instant::now(); + // Initialiwe time_last_send_ihu + let time_last_received_ihu = tokio::time::Instant::now(); + + Self { + hello_seqno, + link_cost, + time_last_received_ihu, + time_last_received_hello, + } + } +} diff --git a/components/mycelium/mycelium/src/peer_manager.rs b/components/mycelium/mycelium/src/peer_manager.rs new file mode 100644 index 0000000..1310542 --- /dev/null +++ b/components/mycelium/mycelium/src/peer_manager.rs @@ -0,0 +1,1361 @@ +use crate::connection::Quic; +use crate::endpoint::{Endpoint, Protocol}; +use crate::metrics::Metrics; +use crate::peer::{Peer, PeerRef}; +use crate::router::Router; +use crate::router_id::RouterId; +use futures::stream::FuturesUnordered; +use futures::{FutureExt, StreamExt}; +#[cfg(feature = "private-network")] +use openssl::ssl::{Ssl, SslAcceptor, SslConnector, SslMethod}; +use quinn::crypto::rustls::QuicClientConfig; +use quinn::{MtuDiscoveryConfig, ServerConfig, TransportConfig}; +use rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer, ServerName, UnixTime}; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::io; +use std::net::{IpAddr, SocketAddr, SocketAddrV6}; +#[cfg(target_os = "linux")] +use std::os::fd::AsFd; +#[cfg(feature = "private-network")] +use std::pin::Pin; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use std::{collections::hash_map::Entry, future::IntoFuture}; +use tokio::net::TcpStream; +use tokio::net::{TcpListener, UdpSocket}; +use tokio::task::AbortHandle; +use tokio::time::{Instant, MissedTickBehavior}; +use tracing::{debug, error, info, instrument, trace, warn}; + +/// Magic bytes to identify a multicast UDP packet used in link local peer discovery. +const MYCELIUM_MULTICAST_DISCOVERY_MAGIC: &[u8; 8] = b"mycelium"; +/// Size of a peer discovery beacon. +const PEER_DISCOVERY_BEACON_SIZE: usize = 8 + 2 + 40; +/// Link local peer discovery group joined by the UDP listener. +const LL_PEER_DISCOVERY_GROUP: &str = "ff02::cafe"; +/// The time between sending consecutive link local discovery beacons. +const LL_PEER_DISCOVERY_BEACON_INTERVAL: Duration = Duration::from_secs(60); +/// The time between checking known peer liveness and trying to reconnect. +const PEER_CONNECT_INTERVAL: Duration = Duration::from_secs(5); +/// The maximum amount of successive failures allowed when connecting to a local discovered peer, +/// before it is forgotten. +const MAX_FAILED_LOCAL_PEER_CONNECTION_ATTEMPTS: usize = 3; +/// The amount of time allowed for a peer to finish the quic handshake when it connects to us. This +/// prevents a (mallicious) peer from hogging server resources. 10 seconds should be a reasonable +/// default for this, though it can certainly be made more strict if required. +const INBOUND_QUIC_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10); +/// The maximum amount of concurrent quic handshakes to process from peers connecting to us. We +/// want to find a middle ground where we don't stall valid peers from connecting beceause a single +/// misbehaving peer stalls the connection task, but we also don't want to accept thousands of +/// these in parralel. For now, 10 in parallel should be sufficient, though this can be +/// increased/decreased based on observations. +const MAX_INBOUND_CONCURRENT_QUICK_HANDSHAKES: usize = 10; + +/// The PeerManager creates new peers by connecting to configured addresses, and setting up the +/// connection. Once a connection is established, the created [`Peer`] is handed over to the +/// [`Router`]. +pub struct PeerManager { + inner: Arc>, + /// Handles to background tasks so we can abort them when the PeerManager is dropped. + abort_handles: Vec, +} + +/// Details how the PeerManager learned about a remote. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PeerType { + /// Statically configured peer. + Static, + /// Peer found through link local discovery. + LinkLocalDiscovery, + /// A remote which initiated a connection to us. + Inbound, +} + +/// Local info about a peer. +struct PeerInfo { + /// Details how we found out about this peer. + pt: PeerType, + /// Are we currently connecting to this peer? + connecting: bool, + /// The [`PeerRef`] used to check liveliness. + pr: PeerRef, + /// Amount of failed times we tried to connect to this peer. This is reset after a successful + /// connection. + connection_attempts: usize, + /// Keep track of the amount of bytes we've sent to and received from this peer. + con_traffic: ConnectionTraffic, + /// The moment in time we learned about this peer. + discovered: Instant, + /// The moment we last connected to this peer. + connected: Option, +} + +/// Counters for the amount of traffic written to and received from a [`Peer`]. +#[derive(Debug, Clone)] +struct ConnectionTraffic { + /// Amount of bytes transmitted to this peer. + tx_bytes: Arc, + /// Amount of bytes received from this peer. + rx_bytes: Arc, +} + +/// General state about a connection to a [`Peer`]. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ConnectionState { + /// There is a working connection to the [`Peer`]. + Alive, + /// The system is currently in the process of establishing a new connection to the [`Peer`]. + Connecting, + /// There is no connection, or the existing connection is no longer functional. + Dead, +} + +/// Identification and information/statistics for a specific [`Peer`] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PeerStats { + /// The endpoint of the [`Peer`]. + pub endpoint: Endpoint, + /// The [`Type`](PeerType) of the [`Peer`]. + #[serde(rename = "type")] + pub pt: PeerType, + /// State of the connection to this [`Peer`] + pub connection_state: ConnectionState, + /// Amount of bytes transmitted to this [`Peer`]. + pub tx_bytes: u64, + /// Amount of bytes received from this [`Peer`]. + pub rx_bytes: u64, + /// Amount of time which passed since the system learned about this [`Peer`], in seconds. + pub discovered: u64, + /// Amount of seconds since the last succesfull connection to this [`Peer`]. + pub last_connected: Option, +} + +impl PeerInfo { + /// Return the amount of bytes read from this peer. + #[inline] + fn read(&self) -> u64 { + self.con_traffic.rx_bytes.load(Ordering::Relaxed) + } + + /// Return the amount of bytes written to this peer. + #[inline] + fn written(&self) -> u64 { + self.con_traffic.rx_bytes.load(Ordering::Relaxed) + } +} + +/// Marker error to indicate a [`peer`](Endpoint) is already known. +#[derive(Debug)] +pub struct PeerExists; + +/// Marker error to indicate a [`peer`](Endpoint) is not known. +#[derive(Debug)] +pub struct PeerNotFound; + +/// PSK used to set up a shared network. Currently 32 bytes though this might change in the future. +pub type PrivateNetworkKey = [u8; 32]; + +struct Inner { + /// Router is unfortunately wrapped in a Mutex, because router is not Sync. + router: Mutex>, + peers: Mutex>, + /// Listen port for new peer connections + tcp_listen_port: u16, + quic_socket: Option, + /// Identity and name of a private network, if one exists + private_network_config: Option<(String, [u8; 32])>, + metrics: M, + firewall_mark: Option, +} + +impl PeerManager +where + M: Metrics + Clone + Send + Sync + 'static, +{ + #[allow(clippy::too_many_arguments)] + pub fn new( + router: Router, + static_peers_sockets: Vec, + tcp_listen_port: u16, + quic_listen_port: Option, + peer_discovery_port: u16, + disable_peer_discovery: bool, + private_network_config: Option<(String, PrivateNetworkKey)>, + metrics: M, + firewall_mark: Option, + ) -> Result> { + let is_private_net = private_network_config.is_some(); + + // Currently we don't support Quic when a private network is used. + let quic_socket = if !is_private_net { + if let Some(quic_listen_port) = quic_listen_port { + Some(make_quic_endpoint( + router.router_id(), + quic_listen_port, + firewall_mark, + )?) + } else { + None + } + } else { + None + }; + + // Set the initially configured peer count in metrics. + metrics.peer_manager_known_peers(static_peers_sockets.len()); + + let mut peer_manager = PeerManager { + inner: Arc::new(Inner { + router: Mutex::new(router), + peers: Mutex::new( + static_peers_sockets + .into_iter() + // These peers are not alive, but we say they are because the reconnect + // loop will perform the actual check and figure out they are dead, then + // (re)connect. + .map(|s| { + let now = tokio::time::Instant::now(); + ( + s, + PeerInfo { + pt: PeerType::Static, + connecting: false, + pr: PeerRef::new(), + connection_attempts: 0, + con_traffic: ConnectionTraffic { + tx_bytes: Arc::new(AtomicU64::new(0)), + rx_bytes: Arc::new(AtomicU64::new(0)), + }, + discovered: now, + connected: None, + }, + ) + }) + .collect(), + ), + tcp_listen_port, + quic_socket, + private_network_config, + metrics, + firewall_mark, + }), + abort_handles: vec![], + }; + + // Start listeners for inbound connections. + // Start the tcp listener, in case we are running a private network the tcp listener will + // actually be a tls listener. + let handle = tokio::spawn(peer_manager.inner.clone().tcp_listener()); + peer_manager.abort_handles.push(handle.abort_handle()); + if is_private_net { + info!("Enabled private network mode"); + } else if peer_manager.inner.quic_socket.is_some() { + // Currently quic is not supported in private network mode. + let handle = tokio::spawn(peer_manager.inner.clone().quic_listener()); + peer_manager.abort_handles.push(handle.abort_handle()); + }; + + // Start (re)connecting to outbound/local peers + let handle = tokio::spawn(peer_manager.inner.clone().connect_to_peers()); + peer_manager.abort_handles.push(handle.abort_handle()); + + // Discover local peers, this does not actually connect to them. That is handle by the + // connect_to_peers task. + if !disable_peer_discovery { + let handle = tokio::spawn( + peer_manager + .inner + .clone() + .local_discovery(peer_discovery_port), + ); + peer_manager.abort_handles.push(handle.abort_handle()); + } + + Ok(peer_manager) + } + + /// Add a new peer to the system. + /// + /// The peer starts of as a dead peer, and connecting is handled in the reconnect loop. + /// + /// # Errors + /// + /// This function returns an error if the [`Endpoint`] is already known. + pub fn add_peer(&self, peer: Endpoint) -> Result<(), PeerExists> { + let mut peer_map = self.inner.peers.lock().unwrap(); + if peer_map.contains_key(&peer) { + return Err(PeerExists); + } + let now = tokio::time::Instant::now(); + peer_map.insert( + peer, + PeerInfo { + pt: PeerType::Static, + connecting: false, + pr: PeerRef::new(), + connection_attempts: 0, + con_traffic: ConnectionTraffic { + tx_bytes: Arc::new(AtomicU64::new(0)), + rx_bytes: Arc::new(AtomicU64::new(0)), + }, + discovered: now, + connected: None, + }, + ); + + Ok(()) + } + + /// Delete a peer from the system. + /// + /// The peer will be disconnected if it is currently connected. + /// + /// # Errors + /// + /// Returns an error if there is no peer identified by the given [`Endpoint`]. + pub fn delete_peer(&self, endpoint: &Endpoint) -> Result<(), PeerNotFound> { + let mut peer_map = self.inner.peers.lock().unwrap(); + peer_map.remove(endpoint).ok_or(PeerNotFound).map(|pi| { + // Make sure we kill the peer connection if one exists + if let Some(peer) = pi.pr.upgrade() { + peer.died(); + } + }) + } + + /// Get a view of all known peers and their stats. + pub fn peers(&self) -> Vec { + let peer_map = self.inner.peers.lock().unwrap(); + let mut pi = Vec::with_capacity(peer_map.len()); + for (endpoint, peer_info) in peer_map.iter() { + let connection_state = if peer_info.connecting { + ConnectionState::Connecting + } else if peer_info.pr.alive() { + ConnectionState::Alive + } else { + ConnectionState::Dead + }; + pi.push(PeerStats { + endpoint: *endpoint, + pt: peer_info.pt.clone(), + connection_state, + tx_bytes: peer_info.written(), + rx_bytes: peer_info.read(), + discovered: peer_info.discovered.elapsed().as_secs(), + last_connected: peer_info.connected.map(|i| i.elapsed().as_secs()), + }); + } + pi + } +} + +impl Drop for PeerManager { + fn drop(&mut self) { + // Cancel all background tasks + for ah in &self.abort_handles { + ah.abort(); + } + } +} + +impl Inner +where + M: Metrics + Clone + Send + 'static, +{ + /// Connect and if needed reconnect to known peers. + async fn connect_to_peers(self: Arc) { + let mut peer_check_interval = tokio::time::interval(PEER_CONNECT_INTERVAL); + // Avoid trying to spam connections. Since we track if we are connecting to a peer this + // won't be that bad, but this avoid unnecessary lock contention. + peer_check_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + + // A list of pending connection futures. Like this, we don't have to spawn a new future for + // every connection task. + let mut connection_futures = FuturesUnordered::new(); + + loop { + tokio::select! { + // We don't care about the none case, that can happen when we aren't connecting to + // any peers. + Some((endpoint, maybe_new_peer)) = connection_futures.next() => { + // Only insert the possible new peer if we actually still care about it + let mut peers = self.peers.lock().unwrap(); + if let Some(pi) = peers.get_mut(&endpoint) { + // Regardless of what happened, we are no longer connecting. + self.metrics.peer_manager_connection_finished(); + pi.connecting = false; + if let Some(peer) = maybe_new_peer { + // We did find a new Peer, insert into router and keep track of it + // Use fully qualified call to aid compiler in type inference. + pi.pr = Peer::refer(&peer); + self.router.lock().unwrap().add_peer_interface(peer); + + // We successfully connected, reset the connection_attempts counter to 0 + pi.connection_attempts = 0; + pi.connected = Some(tokio::time::Instant::now()); + } else { + // Only log with error level on the first connection failure, to avoid spamming the logs + if pi.connection_attempts == 0 { + error!(endpoint.address=%endpoint.address(), endpoint.proto=%endpoint.proto(), "Couldn't connect to endpoint, turn on debug logging for more details"); + } else { + debug!(endpoint.address=%endpoint.address(), endpoint.proto=%endpoint.proto(), attempt=%pi.connection_attempts+1, "Couldn't connect to endpoint") + } + + // Connection failed, add a failed attempt and forget about the peer if + // needed. + pi.connection_attempts += 1; + if pi.pt == PeerType::LinkLocalDiscovery + && pi.connection_attempts >= MAX_FAILED_LOCAL_PEER_CONNECTION_ATTEMPTS { + info!(endpoint.address=%endpoint.address(), endpoint.proto=%endpoint.proto(), "Forgetting about locally discovered peer after failing to connect to it"); + peers.remove(&endpoint); + } + } + } + } + _ = peer_check_interval.tick() => { + // Remove dead inbound peers + self.peers.lock().unwrap().retain(|_, v| v.pt != PeerType::Inbound || v.pr.alive()); + debug!("Looking for dead peers"); + // check if there is an entry for the peer in the router's peer list + for (endpoint, pi) in self.peers.lock().unwrap().iter_mut() { + if !pi.connecting && !pi.pr.alive() { + debug!(endpoint.address=%endpoint.address(), endpoint.proto=%endpoint.proto(), "Found dead peer"); + if pi.pt == PeerType::Inbound { + debug!(endpoint.address=%endpoint.address(), endpoint.proto=%endpoint.proto(), "Refusing to reconnect to inbound peer"); + continue + } + // Mark that we are connecting to the peer. + pi.connecting = true; + connection_futures.push(self.clone().connect_peer(*endpoint, pi.con_traffic.clone())); + self.metrics.peer_manager_connection_attempted(); + } + } + } + } + } + } + + /// Create a new connection to a remote peer + #[instrument(skip_all, fields(endpoint.proto=%endpoint.proto(), endpoint.address=%endpoint.address()))] + async fn connect_peer( + self: Arc, + endpoint: Endpoint, + ct: ConnectionTraffic, + ) -> (Endpoint, Option) { + debug!("Connecting"); + match endpoint.proto() { + Protocol::Tcp | Protocol::Tls => self.connect_tcp_peer(endpoint, ct).await, + Protocol::Quic => self.connect_quic_peer(endpoint, ct).await, + } + } + + async fn connect_tcp_peer( + self: Arc, + endpoint: Endpoint, + ct: ConnectionTraffic, + ) -> (Endpoint, Option) { + match (endpoint.proto(), &self.private_network_config) { + (Protocol::Tcp, Some(_)) => { + warn!("Attempting to connect over Tcp while a private network is configured, connection will be upgraded to Tls") + } + (Protocol::Tls, None) => { + warn!("Attempting to connect over Tls while a private network is not enabled, refusing to connect. Use \"Tcp\" instead"); + return (endpoint, None); + } + _ => {} + } + + #[cfg(feature = "private-network")] + let connector = if let Some((net_name, net_key)) = self.private_network_config.clone() { + let mut connector = SslConnector::builder(SslMethod::tls_client()).unwrap(); + connector.set_psk_client_callback(move |_, _, id, key| { + // Note: identity must be passed in as a 0-terminated C string. + if id.len() < net_name.len() + 1 { + error!("Can't pass in identity to SSL connector"); + return Ok(0); + } + id[..net_name.len()].copy_from_slice(net_name.as_bytes()); + id[net_name.len()] = 0; + if key.len() < 32 { + error!("Can't pass in key to SSL acceptor"); + return Ok(0); + } + // Copy key + key[..32].copy_from_slice(&net_key[..]); + + Ok(32) + }); + Some(connector.build()) + } else { + None + }; + + match TcpStream::connect(endpoint.address()) + .map(|result| result.and_then(|socket| set_fw_mark(socket, self.firewall_mark))) + .await + { + Ok(peer_stream) => { + debug!("Opened connection"); + // Make sure Nagle's algorithm is disabled as it can cause latency spikes. + if let Err(e) = peer_stream.set_nodelay(true) { + debug!(err=%e, "Couldn't disable Nagle's algorithm on stream"); + return (endpoint, None); + } + + // Scope the MutexGuard, if we don't do this the future won't be Send + let (router_data_tx, router_control_tx, dead_peer_sink) = { + let router = self.router.lock().unwrap(); + ( + router.router_data_tx(), + router.router_control_tx(), + router.dead_peer_sink().clone(), + ) + }; + + #[cfg(feature = "private-network")] + let res = { + if let Some(connector) = connector { + let ssl = match Ssl::new(connector.context()) { + Ok(ssl) => ssl, + Err(e) => { + debug!(err=%e, "Failed to create SSL object from acceptor after connecting to remote"); + return (endpoint, None); + } + }; + let mut ssl_stream = match tokio_openssl::SslStream::new(ssl, peer_stream) { + Ok(ssl_stream) => ssl_stream, + Err(e) => { + debug!(err=%e, "Failed to create TLS stream from tcp connection to endpoint"); + return (endpoint, None); + } + }; + + // Pin here is needed to call `connect`. + let pinned_stream = Pin::new(&mut ssl_stream); + if let Err(e) = pinned_stream.connect().await { + // Error here is likely a misconfigured server. + debug!(err=%e, "Could not initiate TLS stream"); + return (endpoint, None); + } + debug!("Completed TLS handshake"); + + Peer::new( + router_data_tx, + router_control_tx, + ssl_stream, + dead_peer_sink, + ct.tx_bytes, + ct.rx_bytes, + ) + } else { + Peer::new( + router_data_tx, + router_control_tx, + peer_stream, + dead_peer_sink, + ct.tx_bytes, + ct.rx_bytes, + ) + } + }; + + #[cfg(not(feature = "private-network"))] + let res = Peer::new( + router_data_tx, + router_control_tx, + peer_stream, + dead_peer_sink, + ct.tx_bytes, + ct.rx_bytes, + ); + + match res { + Ok(new_peer) => { + info!("Connected to new peer"); + (endpoint, Some(new_peer)) + } + Err(e) => { + debug!(err=%e, "Failed to spawn peer"); + (endpoint, None) + } + } + } + Err(e) => { + debug!(err=%e, "Couldn't connect"); + (endpoint, None) + } + } + } + + async fn connect_quic_peer( + self: Arc, + endpoint: Endpoint, + ct: ConnectionTraffic, + ) -> (Endpoint, Option) { + let quic_socket = if let Some(quic_socket) = &self.quic_socket { + quic_socket + } else { + debug!("Attempting to connect to quic peer while quic is disabled"); + return (endpoint, None); + }; + let provider = rustls::crypto::CryptoProvider::get_default() + .expect("We have a quic socket so there is a crypto provider installed"); + let qcc = match QuicClientConfig::try_from( + rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(SkipServerVerification::new(provider.clone())) + .with_no_client_auth(), + ) { + Ok(qcc) => qcc, + Err(err) => { + debug!(%err, "Failed to build quic client config"); + return (endpoint, None); + } + }; + let mut config = quinn::ClientConfig::new(Arc::new(qcc)); + // Todo: tweak transport config + let mut transport_config = TransportConfig::default(); + transport_config.max_concurrent_uni_streams(0_u8.into()); + // Larger than needed for now, just in case + transport_config.max_concurrent_bidi_streams(5_u8.into()); + // Connection timeout, set to higher than Hello interval to ensure connection does not randomly + // time out. + transport_config.max_idle_timeout(Some(Duration::from_secs(60).try_into().unwrap())); + transport_config.mtu_discovery_config(Some(MtuDiscoveryConfig::default())); + transport_config.keep_alive_interval(Some(Duration::from_secs(20))); + // we don't use datagrams. + transport_config.datagram_receive_buffer_size(None); + transport_config.datagram_send_buffer_size(0); + config.transport_config(Arc::new(transport_config)); + + match quic_socket.connect_with(config, endpoint.address(), "dummy.mycelium") { + Ok(connecting) => match connecting.await { + Ok(con) => match con.open_bi().await { + Ok((tx, rx)) => { + let q_con = Quic::new(tx, rx, endpoint.address()); + let res = { + let router = self.router.lock().unwrap(); + let router_data_tx = router.router_data_tx(); + let router_control_tx = router.router_control_tx(); + let dead_peer_sink = router.dead_peer_sink().clone(); + + Peer::new( + router_data_tx, + router_control_tx, + q_con, + dead_peer_sink, + ct.tx_bytes, + ct.rx_bytes, + ) + }; + match res { + Ok(new_peer) => { + info!("Connected to new peer"); + (endpoint, Some(new_peer)) + } + Err(e) => { + debug!(err=%e, "Failed to spawn peer"); + (endpoint, None) + } + } + } + Err(e) => { + debug!(err=%e, "Couldn't open bidirectional quic stream"); + (endpoint, None) + } + }, + Err(e) => { + debug!(err=%e, "Couldn't complete quic connection"); + (endpoint, None) + } + }, + Err(e) => { + debug!(err=%e, "Couldn't initiate connection"); + (endpoint, None) + } + } + } + + /// Start listening for new peers on a tcp socket. If a private network is configured, this + /// will instead listen for incoming tls connections. + async fn tcp_listener(self: Arc) { + // Setup TLS acceptor for private network, if required. + #[cfg(feature = "private-network")] + let acceptor = if let Some((net_name, net_key)) = self.private_network_config.clone() { + let mut acceptor = SslAcceptor::mozilla_modern_v5(SslMethod::tls_server()).unwrap(); + acceptor.set_psk_server_callback(move |_ssl_ref, id, key| { + if let Some(id) = id { + if id != net_name.as_bytes() { + debug!("id given by client does not match configured private network name"); + return Ok(0); + } + } else { + debug!("No name indicated by client"); + return Ok(0); + } + if key.len() < 32 { + warn!("Can't pass in key to SSL acceptor"); + return Ok(0); + } + // Copy key + key[..32].copy_from_slice(&net_key[..]); + + Ok(32) + }); + Some(acceptor.build()) + } else { + None + }; + + // Take a copy of every channel here first so we avoid lock contention in the loop later. + let router_data_tx = self.router.lock().unwrap().router_data_tx(); + let router_control_tx = self.router.lock().unwrap().router_control_tx(); + let dead_peer_sink = self.router.lock().unwrap().dead_peer_sink().clone(); + + let listener = TcpListener::bind(("::", self.tcp_listen_port)) + .map(|result| result.and_then(|listener| set_fw_mark(listener, self.firewall_mark))); + + match listener.await { + Ok(listener) => loop { + match listener.accept().await { + Ok((stream, remote)) => { + let tx_bytes = Arc::new(AtomicU64::new(0)); + let rx_bytes = Arc::new(AtomicU64::new(0)); + + if let Err(e) = stream.set_nodelay(true) { + error!(err=%e, "Couldn't disable Nagle's algorithm on stream"); + return; + } + + #[cfg(feature = "private-network")] + let new_peer = if let Some(acceptor) = &acceptor { + let ssl = match Ssl::new(acceptor.context()) { + Ok(ssl) => ssl, + Err(e) => { + error!(%remote, err=%e, "Failed to create SSL object from acceptor after remote connected"); + continue; + } + }; + let mut ssl_stream = match tokio_openssl::SslStream::new(ssl, stream) { + Ok(ssl_stream) => ssl_stream, + Err(e) => { + error!(%remote, err=%e, "Failed to create TLS stream from tcp connection"); + continue; + } + }; + + // Pin here is needed to call `accept`. + let pinned_stream = Pin::new(&mut ssl_stream); + if let Err(e) = pinned_stream.accept().await { + // An error at this point generally means the handshake failed, + // client error. + debug!(%remote, err=%e, "Could not accept TLS stream"); + continue; + } + debug!(%remote, "Accepted TLS handshake"); + + Peer::new( + router_data_tx.clone(), + router_control_tx.clone(), + ssl_stream, + dead_peer_sink.clone(), + tx_bytes.clone(), + rx_bytes.clone(), + ) + } else { + Peer::new( + router_data_tx.clone(), + router_control_tx.clone(), + stream, + dead_peer_sink.clone(), + tx_bytes.clone(), + rx_bytes.clone(), + ) + }; + + #[cfg(not(feature = "private-network"))] + let new_peer = Peer::new( + router_data_tx.clone(), + router_control_tx.clone(), + stream, + dead_peer_sink.clone(), + tx_bytes.clone(), + rx_bytes.clone(), + ); + + let new_peer = match new_peer { + Ok(peer) => peer, + Err(e) => { + error!(err=%e, "Failed to spawn peer"); + continue; + } + }; + info!("Accepted new inbound peer"); + self.add_peer( + Endpoint::new( + if self.private_network_config.is_some() { + Protocol::Tls + } else { + Protocol::Tcp + }, + remote, + ), + PeerType::Inbound, + ConnectionTraffic { tx_bytes, rx_bytes }, + Some(new_peer), + ); + } + Err(e) => { + error!(err=%e, "Error accepting connection"); + } + } + }, + Err(e) => { + error!(err=%e, "Error starting listener"); + } + } + } + + /// Start listening on the configured quic socket for new inbound peers. + /// + /// # Panics + /// + /// This method panics if `self.quic_socket` is [`None`]. + async fn quic_listener(self: Arc) { + // SAFETY: This is safe because this method only get's called if we have a quic socket. + let quic_socket = self.quic_socket.as_ref().unwrap(); + // Take a copy of every channel here first so we avoid lock contention in the loop later. + let router_data_tx = self.router.lock().unwrap().router_data_tx(); + let router_control_tx = self.router.lock().unwrap().router_control_tx(); + let dead_peer_sink = self.router.lock().unwrap().dead_peer_sink().clone(); + + let mut quic_con_futures = FuturesUnordered::new(); + + let mut pending_quic_handshakes = 0; + + loop { + tokio::select! { + // FuturesUnordered always returns Some(...). + Some(handshake_result) = quic_con_futures.next() => { + pending_quic_handshakes -=1; + // Since tyhpe inference failed here, use a fully quallified function call + if Result::<(),tokio::time::error::Elapsed>::is_err(&handshake_result) { + debug!("Dropping connection to peer who's handshake timed out"); + } + } + maybe_con = quic_socket.accept(), if pending_quic_handshakes < MAX_INBOUND_CONCURRENT_QUICK_HANDSHAKES => { + let Some(con) = maybe_con else { + break + }; + + + let con_future = async { + let con = match con.into_future().await { + Ok(con) => con, + Err(e) => { + debug!(err=%e, "Failed to accept quic connection"); + return; + } + }; + + let quic_peer = match con.accept_bi().await { + Ok((tx, rx)) => Quic::new(tx, rx, con.remote_address()), + Err(e) => { + debug!(err=%e, "Failed to accept bidirectional quic stream"); + return; + } + }; + + let tx_bytes = Arc::new(AtomicU64::new(0)); + let rx_bytes = Arc::new(AtomicU64::new(0)); + let new_peer = match Peer::new( + router_data_tx.clone(), + router_control_tx.clone(), + quic_peer, + dead_peer_sink.clone(), + tx_bytes.clone(), + rx_bytes.clone(), + ) { + Ok(peer) => peer, + Err(e) => { + error!(err=%e, "Failed to spawn peer"); + return; + } + }; + info!(remote=%con.remote_address(), "Accepted new inbound quic peer"); + self.add_peer( + Endpoint::new(Protocol::Quic, con.remote_address()), + PeerType::Inbound, + ConnectionTraffic { tx_bytes, rx_bytes }, + Some(new_peer), + ); + }; + + pending_quic_handshakes += 1; + quic_con_futures.push(tokio::time::timeout(INBOUND_QUIC_HANDSHAKE_TIMEOUT,con_future)); + } + } + } + + info!("Shutting down closed quic listener"); + } + + /// Add a new peer identifier we discovered. + #[instrument(skip_all,fields(peer.endpoint=%endpoint))] + fn add_peer( + &self, + endpoint: Endpoint, + discovery_type: PeerType, + con_traffic: ConnectionTraffic, + peer: Option, + ) { + self.metrics.peer_manager_peer_added(discovery_type.clone()); + let mut peers = self.peers.lock().unwrap(); + // Filter out link local IP's we already know (because of reverse detection) + if discovery_type == PeerType::LinkLocalDiscovery { + if let IpAddr::V6(ip) = endpoint.address().ip() { + if ip.octets()[..8] == [0xfe, 0x80, 0, 0, 0, 0, 0, 0] { + for known_endpoint in peers.keys() { + if known_endpoint.address().ip() == endpoint.address().ip() + && known_endpoint.proto() == endpoint.proto() + { + trace!(peer.known_endpoint=%known_endpoint, "Refusing to add link local discovered address as there already is a reverse connection"); + return; + } + } + } + } + } + // Only if we don't know it yet. + let now = tokio::time::Instant::now(); + if let Entry::Vacant(e) = peers.entry(endpoint) { + e.insert(PeerInfo { + pt: discovery_type, + connecting: false, + pr: if let Some(p) = &peer { + p.refer() + } else { + PeerRef::new() + }, + connection_attempts: 0, + con_traffic, + discovered: now, + connected: if peer.is_some() { Some(now) } else { None }, + }); + if let Some(p) = peer { + self.router.lock().unwrap().add_peer_interface(p); + } + info!("Added new peer"); + } else if discovery_type == PeerType::Inbound { + // We got an inbound peer with a duplicate entry. This is possible if the sending port + // is the same as the previous one, which generally happens with our Quic setup. In + // this case, the old connection needs to be replaced. + let discovered = peers + .get(&endpoint) + .map(|epi| epi.discovered) + .unwrap_or(now); + let old_peer_info = peers.insert( + endpoint, + PeerInfo { + pt: discovery_type, + connecting: false, + pr: if let Some(p) = &peer { + p.refer() + } else { + PeerRef::new() + }, + connection_attempts: 0, + con_traffic, + discovered, + connected: Some(now), + }, + ); + // If we have a new peer notify insert the new one in the router, then notify it that + // the old one is dead. + if let Some(p) = peer { + let router = self.router.lock().unwrap(); + router.add_peer_interface(p); + if let Some(old_peer) = old_peer_info + .expect("We already checked the entry was occupied so this is always Some; qed") + .pr + .upgrade() + { + router.handle_dead_peer(&[old_peer]); + } else { + warn!("Added duplicate inbound entry but did not kill old peer"); + } + } + info!("Replaced existing inbound peer"); + } else { + debug!("Ignoring request to add as it already exists"); + } + self.metrics.peer_manager_known_peers(peers.len()); + } + + /// Use multicast discovery to find local peers. + async fn local_discovery(self: Arc, peer_discovery_port: u16) { + let rid = self.router.lock().unwrap().router_id(); + + let multicast_destination = LL_PEER_DISCOVERY_GROUP + .parse() + .expect("Link local discovery group address is properly defined"); + let sock = match UdpSocket::bind(SocketAddr::new( + "::".parse().expect("Valid all interface IPv6 designator"), + peer_discovery_port, + )) + .map(|result| result.and_then(|sock| set_fw_mark(sock, self.firewall_mark))) + .await + { + Ok(sock) => sock, + Err(e) => { + // We won't participate in link local discovery + error!(err=%e, "Failed to bind multicast discovery socket"); + warn!("Link local peer discovery disabled"); + return; + } + }; + + info!( + bind_address=%sock.local_addr().expect("can look up our own address"), + "Bound multicast discovery interface", + ); + + // Keep track of which interfaces we are already a part of. + let mut joined_interfaces = HashSet::new(); + // Join the multicast discovery group on newly detected interfaces. + let join_new_interfaces = |joined_interfaces: &mut HashSet<_>| { + let ipv6_nics = list_ipv6_interface_ids()?; + // Keep the existing interfaces, removing interface ids we previously joined but are no + // longer found when listing ids. We simply discard unknown ids, and assume if the + // interface is gone (or it's IPv6), that we also implicitly left the group (i.e. no + // cleanup is needed on our end). + let kept_interfaces = joined_interfaces.intersection(&ipv6_nics); + *joined_interfaces = kept_interfaces.copied().collect(); + // Since [`HashSet::difference`] keeps a reference to both sets we need to exhaust the + // iterator first so we can later mutate the firs set. + let new_interfaces = ipv6_nics + .difference(joined_interfaces) + .copied() + .collect::>(); + for new_iface in new_interfaces { + match sock.join_multicast_v6(&multicast_destination, new_iface) { + Err(e) if e.kind() == tokio::io::ErrorKind::AddrInUse => { + // This could happen if the multicast listener is already bound but we + // somehow forgot about it. + debug!(%new_iface, "Multicast group on interface already in use, consider it to be joined"); + joined_interfaces.insert(new_iface); + } + Err(e) => { + warn!(%new_iface, err=%e, "Failed to join multicast group on interface"); + } + Ok(()) => { + debug!(%new_iface, "Joined multicast group on interface"); + joined_interfaces.insert(new_iface); + } + } + } + + // A user likely wants to know this. If there is intentionally no IPv6 enabled + // interface, then the peer discovery can be disabled entirely to save some CPU cycles + // and silence this. + if joined_interfaces.is_empty() { + warn!("Link local peer discovery enabled but discovery group is not joined on any interface"); + } + + Ok::<_, Box>(()) + }; + + // We don't care about our own multicast beacons + if let Err(e) = sock.set_multicast_loop_v6(false) { + warn!("Could not disable multicast loop: {e}"); + } + + let mut beacon = [0; PEER_DISCOVERY_BEACON_SIZE]; + beacon[..8].copy_from_slice(MYCELIUM_MULTICAST_DISCOVERY_MAGIC); + beacon[8..10].copy_from_slice(&self.tcp_listen_port.to_be_bytes()); + beacon[10..50].copy_from_slice(&rid.as_bytes()); + + let mut send_timer = tokio::time::interval(LL_PEER_DISCOVERY_BEACON_INTERVAL); + send_timer.set_missed_tick_behavior(MissedTickBehavior::Skip); + + loop { + let mut buf = [0; PEER_DISCOVERY_BEACON_SIZE]; + tokio::select! { + _ = send_timer.tick() => { + if let Err(e) = join_new_interfaces(&mut joined_interfaces) { + error!(err=%e, "Issue while joining new IPv6 multicast interfaces"); + }; + for iface in &joined_interfaces { + let dst = SocketAddrV6::new(multicast_destination, peer_discovery_port, 0, *iface); + debug!(%dst, %iface, "Sending multicast discovery beacon"); + if let Err(e) = sock.send_to( + &beacon, + dst, + ) + .await { + error!(iface=%iface, err=%e,"Could not send multicast discovery beacon on interface"); + } + } + }, + recv_res = sock.recv_from(&mut buf) => { + match recv_res { + Err(e) => { + warn!(err=%e, "Failed to receive multicast message"); + continue; + } + Ok((n, remote)) => { + trace!(%remote, bytes_received=%n, "Received bytes from remote"); + self.handle_discovery_packet(&buf[..n], remote); + } + } + } + } + } + } + + /// Validates an incoming discovery packet. If the packet is valid, the peer is added to the + /// `PeerManager`. + fn handle_discovery_packet(&self, packet: &[u8], mut remote: SocketAddr) { + if let IpAddr::V6(ip) = remote.ip() { + // Dumb subnet validation, we only want to discover link local addresses, + // i.e. part of fe80::/64 + if ip.octets()[..8] != [0xfe, 0x80, 0, 0, 0, 0, 0, 0] { + trace!("Ignoring non link local IPv6 discovery packet from {remote}"); + return; + } + } else { + trace!("Ignoring non IPv6 discovery packet from {remote}"); + return; + } + if packet.len() != PEER_DISCOVERY_BEACON_SIZE { + trace!("Ignore invalid sized multicast announcement"); + return; + } + if &packet[..8] != MYCELIUM_MULTICAST_DISCOVERY_MAGIC { + trace!("Ignore announcement with invalid multicast magic"); + return; + } + let port = u16::from_be_bytes( + packet[8..10] + .try_into() + .expect("Slice size is valid for u16"), + ); + let remote_rid = RouterId::from( + <&[u8] as TryInto<[u8; 40]>>::try_into(&packet[10..50]) + .expect("Slice size is valid for RouterId"), + ); + let rid = self.router.lock().unwrap().router_id(); + if remote_rid == rid { + debug!("Ignore discovery beacon we sent earlier"); + return; + } + // Override the port. Care must be taken since link local IPv6 expects the + // scope_id to be set. + remote.set_port(port); + self.add_peer( + Endpoint::new( + if self.private_network_config.is_some() { + Protocol::Tls + } else { + Protocol::Tcp + }, + remote, + ), + PeerType::LinkLocalDiscovery, + ConnectionTraffic { + tx_bytes: Arc::new(AtomicU64::new(0)), + rx_bytes: Arc::new(AtomicU64::new(0)), + }, + None, + ); + } +} + +/// Spawn a quic socket which can be used to both receive quic connections and initiate new quic +/// connections to remotes. +fn make_quic_endpoint( + router_id: RouterId, + quic_listen_port: u16, + firewall_mark: Option, +) -> Result> { + // Install ring crypto provider for rustls + rustls::crypto::CryptoProvider::install_default(rustls::crypto::ring::default_provider()) + .expect("Crypto provider has not been installed yet"); + // Generate self signed certificate certificate. + // TODO: sign with router keys + let cert = rcgen::generate_simple_self_signed(vec![format!("{router_id}")])?; + let certificate_der = CertificateDer::from(cert.cert); + let private_key = PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()); + let certificate_chain = vec![certificate_der]; + + let mut server_config = ServerConfig::with_single_cert(certificate_chain, private_key.into())?; + // We can unwrap this since it's the only current instance. + let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); + // We don't use unidirectional streams. + transport_config.max_concurrent_uni_streams(0_u8.into()); + // Larger than needed for now, just in case + transport_config.max_concurrent_bidi_streams(5_u8.into()); + // Connection timeout, set to higher than Hello interval to ensure connection does not randomly + // time out. + transport_config.max_idle_timeout(Some(Duration::from_secs(60).try_into()?)); + transport_config.mtu_discovery_config(Some(MtuDiscoveryConfig::default())); + transport_config.keep_alive_interval(Some(Duration::from_secs(20))); + // we don't use datagrams. + transport_config.datagram_receive_buffer_size(None); + transport_config.datagram_send_buffer_size(0); + // TODO: further tweak this. + + let socket = std::net::UdpSocket::bind(("::", quic_listen_port)) + .and_then(|socket| set_fw_mark(socket, firewall_mark))?; + debug!("Bound UDP socket for Quic"); + + //TODO tweak or confirm + let endpoint = quinn::Endpoint::new( + quinn::EndpointConfig::default(), + Some(server_config), + socket, + quinn::default_runtime() + .expect("We are inside the tokio-runtime so this always returns Some(); qed"), + )?; + + Ok(endpoint) +} + +// Firewall marks are only supported on Linux +#[cfg(target_os = "linux")] +fn set_fw_mark(socket: S, mark: Option) -> io::Result { + use nix::sys::socket::{setsockopt, sockopt}; + + if let Some(mark) = mark { + setsockopt(&socket, sockopt::Mark, &mark) + .map_or_else(|errno| Err(io::Error::other(errno)), |_| Ok(socket)) + } else { + Ok(socket) + } +} + +#[cfg(not(target_os = "linux"))] +fn set_fw_mark(socket: S, _mark: Option) -> io::Result { + Ok(socket) +} + +/// Dummy certificate verifier that treats any certificate as valid. +#[derive(Debug)] +struct SkipServerVerification(Arc); + +impl SkipServerVerification { + fn new(provider: Arc) -> Arc { + Arc::new(Self(provider)) + } +} + +impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } +} + +/// Get a list of the interface identifiers of every network interface with a local IPv6 IP. +fn list_ipv6_interface_ids() -> Result, Box> { + let mut nics = HashSet::new(); + for nic in netdev::get_interfaces() + .into_iter() + // Filter out interfaces we don't care about. + .filter(|nic| { + !nic.is_loopback() + && !nic.is_tun() + && !nic.is_point_to_point() + && nic.is_multicast() + && nic.is_up() + }) + { + for addr in nic.ipv6 { + if addr.addr().segments()[..4] == [0xfe80, 0, 0, 0] { + nics.insert(nic.index); + } + } + } + + Ok(nics) +} + +impl fmt::Display for ConnectionState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Alive => "Alive", + Self::Connecting => "Connecting", + Self::Dead => "Dead", + }) + } +} + +impl fmt::Display for PeerType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Static => "Static", + Self::Inbound => "Inbound", + Self::LinkLocalDiscovery => "LinkLocalDiscovery", + }) + } +} + +impl fmt::Display for PeerExists { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Peer identified by endpoint already exists") + } +} + +impl std::error::Error for PeerExists {} + +impl fmt::Display for PeerNotFound { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Peer identified by endpoint not known") + } +} + +impl std::error::Error for PeerNotFound {} diff --git a/components/mycelium/mycelium/src/router.rs b/components/mycelium/mycelium/src/router.rs new file mode 100644 index 0000000..de4400f --- /dev/null +++ b/components/mycelium/mycelium/src/router.rs @@ -0,0 +1,2216 @@ +use crate::{ + babel::{self, Hello, Ihu, RouteRequest, SeqNoRequest, Update}, + crypto::{PacketBuffer, PublicKey, SecretKey, SharedSecret}, + filters::RouteUpdateFilter, + metric::Metric, + metrics::Metrics, + packet::{ControlPacket, DataPacket}, + peer::Peer, + router_id::RouterId, + routing_table::{ + NoRouteSubnet, QueriedSubnet, RouteEntry, RouteKey, RouteList, Routes, RoutingTable, + }, + rr_cache::RouteRequestCache, + seqno_cache::{SeqnoCache, SeqnoRequestCacheKey}, + sequence_number::SeqNo, + source_table::{FeasibilityDistance, SourceKey, SourceTable}, + subnet::Subnet, +}; +use etherparse::{ + icmpv6::{DestUnreachableCode, TimeExceededCode}, + Icmpv6Type, +}; +use std::{ + error::Error, + hash::{Hash, Hasher}, + net::IpAddr, + sync::{Arc, RwLock}, + time::{Duration, Instant}, +}; +use tokio::sync::mpsc::{self, Receiver, Sender, UnboundedReceiver, UnboundedSender}; +use tracing::{debug, error, info, trace, warn}; + +/// Time between HELLO messags, in seconds +const HELLO_INTERVAL: u64 = 20; +/// Time filled in in IHU packet +const IHU_INTERVAL: Duration = Duration::from_secs(HELLO_INTERVAL * 3); +/// Max time used in UPDATE packets. For local (static) routes this is the timeout they are +/// advertised with. +const UPDATE_INTERVAL: Duration = Duration::from_secs(HELLO_INTERVAL * 3 * 5); +/// Time between selected route announcements to peers. +const ROUTE_PROPAGATION_INTERVAL: Duration = UPDATE_INTERVAL; +/// Amount of seconds that can elapse before we consider a [`Peer`] as dead from the routers POV. +/// Since IHU's are sent in response to HELLO packets, this MUST be greater than the +/// [`HELLO_INTERVAL`]. +/// +/// We allow missing 1 hello, + some latency, so 2 HELLO's + 3 seconds for latency. +const DEAD_PEER_THRESHOLD: Duration = Duration::from_secs(HELLO_INTERVAL * 2 + 3); +/// The duration between checks for dead peers in the router. This check only looks for peers where +/// time since the last IHU exceeds DEAD_PEER_THRESHOLD. +const DEAD_PEER_CHECK_INTERVAL: Duration = Duration::from_secs(10); + +/// Amount of time to wait between consecutive seqno bumps of the local router seqno. +const SEQNO_BUMP_TIMEOUT: Duration = Duration::from_secs(4); + +/// Metric change of more than 10 is considered a large change. +const BIG_METRIC_CHANGE_TRESHOLD: Metric = Metric::new(10); + +/// The amount a metric of a route needs to improve before we will consider switching to it. +const SIGNIFICANT_METRIC_IMPROVEMENT: Metric = Metric::new(10); + +/// Hold retracted routes for 1 minute before purging them from the [`RoutingTable`]. +const RETRACTED_ROUTE_HOLD_TIME: Duration = Duration::from_secs(6); + +/// The interval specified in updates if the update won't be repeated. +const INTERVAL_NOT_REPEATING: Duration = Duration::from_millis(6); + +/// The maximum generation of a [`RouteRequest`] we are still willing to transmit. +const MAX_RR_GENERATION: u8 = 16; + +/// Give a route query 5 seconds to resolve, this should be plenty generous. +const ROUTE_QUERY_TIMEOUT: Duration = Duration::from_secs(5); + +/// Amount of time to wait before checking if a queried route resolved. +// TODO: Remove once proper feedback is in place +const QUERY_CHECK_DURATION: Duration = Duration::from_millis(100); + +/// The threshold for route expiry under which we want to send a route requets for a subnet if it is used. +const ROUTE_ALMOST_EXPIRED_TRESHOLD: tokio::time::Duration = Duration::from_secs(15); + +pub struct Router { + routing_table: RoutingTable, + peer_interfaces: Arc>>, + source_table: Arc>, + // Router SeqNo and last time it was bumped + router_seqno: Arc>, + static_routes: Vec, + router_id: RouterId, + node_keypair: (SecretKey, PublicKey), + router_data_tx: Sender, + router_control_tx: UnboundedSender<(ControlPacket, Peer)>, + node_tun: UnboundedSender, + node_tun_subnet: Subnet, + update_filters: Arc>>, + /// Channel injected into peers, so they can notify the router if they exit. + dead_peer_sink: mpsc::Sender, + /// Channel to notify the router of expired SourceKey's. + expired_source_key_sink: mpsc::Sender, + seqno_cache: SeqnoCache, + rr_cache: RouteRequestCache, + update_workers: usize, + metrics: M, +} + +impl Router +where + M: Metrics + Clone + Send + 'static, +{ + /// Create a new `Router`. + /// + /// # Panics + /// + /// If update_workers is not in the range of [1..255], this will panic. + pub fn new( + update_workers: usize, + node_tun: UnboundedSender, + node_tun_subnet: Subnet, + static_routes: Vec, + node_keypair: (SecretKey, PublicKey), + update_filters: Vec>, + metrics: M, + ) -> Result> { + // We could use a NonZeroU8 here, but for now just handle this manually as this might get + // changed in the future. + if !(1..255).contains(&update_workers) { + panic!("update workers must be at least 1 and at most 255"); + } + + // Tx is passed onto each new peer instance. This enables peers to send control packets to the router. + let (router_control_tx, router_control_rx) = mpsc::unbounded_channel(); + // Tx is passed onto each new peer instance. This enables peers to send data packets to the router. + let (router_data_tx, router_data_rx) = mpsc::channel::(1000); + let (expired_source_key_sink, expired_source_key_stream) = mpsc::channel(1); + let (expired_route_entry_sink, expired_route_entry_stream) = mpsc::channel(1); + let (dead_peer_sink, dead_peer_stream) = mpsc::channel(100); + + let routing_table = RoutingTable::new(expired_route_entry_sink); + + let router_id = RouterId::new(node_keypair.1); + + let seqno_cache = SeqnoCache::new(); + let rr_cache = RouteRequestCache::new(ROUTE_ALMOST_EXPIRED_TRESHOLD); + + let router = Router { + routing_table, + peer_interfaces: Arc::new(RwLock::new(Vec::new())), + source_table: Arc::new(RwLock::new(SourceTable::new())), + router_seqno: Arc::new(RwLock::new((SeqNo::new(), Instant::now()))), + static_routes, + router_id, + node_keypair, + router_data_tx, + router_control_tx, + node_tun, + node_tun_subnet, + dead_peer_sink, + expired_source_key_sink, + seqno_cache, + rr_cache, + update_filters: Arc::new(update_filters), + update_workers, + metrics, + }; + + tokio::spawn(Router::start_periodic_hello_sender(router.clone())); + tokio::spawn(Router::handle_incoming_control_packet( + router.clone(), + router_control_rx, + )); + tokio::spawn(Router::handle_incoming_data_packet( + router.clone(), + router_data_rx, + )); + + tokio::spawn(Router::propagate_static_routes(router.clone())); + + tokio::spawn(Router::check_for_dead_peers(router.clone())); + + tokio::spawn(Router::process_expired_source_keys( + router.clone(), + expired_source_key_stream, + )); + + tokio::spawn(Router::process_expired_route_keys( + router.clone(), + expired_route_entry_stream, + )); + + tokio::spawn(Router::process_dead_peers(router.clone(), dead_peer_stream)); + + Ok(router) + } + + pub fn router_control_tx(&self) -> UnboundedSender<(ControlPacket, Peer)> { + self.router_control_tx.clone() + } + + pub fn router_data_tx(&self) -> Sender { + self.router_data_tx.clone() + } + + pub fn node_tun_subnet(&self) -> Subnet { + self.node_tun_subnet + } + + pub fn node_tun(&self) -> UnboundedSender { + self.node_tun.clone() + } + + /// Get all peer interfaces known on the router. + pub fn peer_interfaces(&self) -> Vec { + self.peer_interfaces.read().unwrap().clone() + } + + /// Add a peer interface to the router. + pub fn add_peer_interface(&self, peer: Peer) { + debug!("Adding peer {} to router", peer.connection_identifier()); + self.peer_interfaces.write().unwrap().push(peer.clone()); + self.metrics.router_peer_added(); + // Make sure to set the timers to current values in case lock acquisition takes time. Otherwise these + // might immediately cause timeout timers to fire. + peer.set_time_last_received_hello(tokio::time::Instant::now()); + peer.set_time_last_received_ihu(tokio::time::Instant::now()); + } + + /// Get the public key used by the router + pub fn node_public_key(&self) -> PublicKey { + self.node_keypair.1 + } + + /// Get the [`RouterId`] of the `Router`. + pub fn router_id(&self) -> RouterId { + self.router_id + } + + /// Get the [`PublicKey`] for an [`IpAddr`] if a route exists to the IP. + pub fn get_pubkey(&self, ip: IpAddr) -> Option { + if let Routes::Exist(routes) = self.routing_table.best_routes(ip) { + if routes.is_empty() { + None + } else { + Some(routes[0].source().router_id().to_pubkey()) + } + } else { + None + } + } + + /// Gets the cached [`SharedSecret`] for the remote. + pub fn get_shared_secret_from_dest(&self, dest: IpAddr) -> Option { + // TODO: proper fix + match self.routing_table.best_routes(dest) { + Routes::Exist(routes) => Some(routes.shared_secret().clone()), + Routes::Queried => { + tokio::task::block_in_place(|| std::thread::sleep(QUERY_CHECK_DURATION)); + self.get_shared_secret_from_dest(dest) + } + Routes::NoRoute => None, + Routes::None => { + // NOTE: we request the full /64 subnet + self.send_route_request( + Subnet::new(dest, 64) + .expect("64 is a valid subnet size for an IPv6 address; qed"), + ); + tokio::task::block_in_place(|| std::thread::sleep(QUERY_CHECK_DURATION)); + self.get_shared_secret_from_dest(dest) + } + } + } + + /// Get a [`SharedSecret`] for a remote, if a selected route exists to the remote. + // TODO: Naming + pub fn get_shared_secret_if_selected(&self, dest: IpAddr) -> Option { + // TODO: proper fix + match self.routing_table.best_routes(dest) { + Routes::Exist(routes) => { + if routes.selected().is_some() { + Some(routes.shared_secret().clone()) + } else { + // Optimistically try to fetch a new route for the subnet for next time. + // TODO: this can likely be handled better, but that relies on continuously + // quering routes in use, and handling unfeasible routes + if let Some(route_entry) = routes.iter().next() { + // We have a fallback route, use the source key from that to do a seqno + // request. Since the next hop might be dead, just do a broadcast. This + // might be blocked by the seqno request cache. + self.send_seqno_request(route_entry.source(), None, None); + } else { + // We don't have any routes, so send a route request. this might fail due + // to the source table. + self.send_route_request( + Subnet::new(dest, 64) + .expect("64 is a valid subnet size for an IPv6 address; qed"), + ); + } + + None + } + } + Routes::Queried => { + tokio::task::block_in_place(|| std::thread::sleep(QUERY_CHECK_DURATION)); + self.get_shared_secret_if_selected(dest) + } + Routes::NoRoute => None, + Routes::None => { + // NOTE: we request the full /64 subnet + self.send_route_request( + Subnet::new(dest, 64) + .expect("64 is a valid subnet size for an IPv6 address; qed"), + ); + tokio::task::block_in_place(|| std::thread::sleep(QUERY_CHECK_DURATION)); + self.get_shared_secret_if_selected(dest) + } + } + } + + /// Gets the cached [`SharedSecret`] based on the associated [`PublicKey`] of the remote. + #[inline] + pub fn get_shared_secret_by_pubkey(&self, dest: &PublicKey) -> Option { + self.get_shared_secret_from_dest(dest.address().into()) + } + + /// Get a reference to this `Router`s' dead peer sink. + pub fn dead_peer_sink(&self) -> &mpsc::Sender { + &self.dead_peer_sink + } + + /// Remove a peer from the Router. + fn remove_peer_interface(&self, peer: &Peer) { + debug!( + "Removing peer {} from the router", + peer.connection_identifier() + ); + peer.died(); + + let mut peers = self.peer_interfaces.write().unwrap(); + let old_peers = peers.len(); + peers.retain(|p| p != peer); + + let removed = old_peers - peers.len(); + for _ in 0..removed { + self.metrics.router_peer_removed(); + } + + if removed > 1 { + warn!( + "REMOVED {removed} peers from peer list while called with {}", + peer.connection_identifier() + ); + } + } + + /// Get a list of all selected route entries. + pub fn load_selected_routes(&self) -> Vec { + self.routing_table + .read() + .iter() + .filter_map(|(_, rl)| rl.selected().cloned()) + .collect() + } + + /// Get a list of all fallback route entries. + pub fn load_fallback_routes(&self) -> Vec { + self.routing_table + .read() + .iter() + .flat_map(|(_, rl)| rl.iter().cloned().collect::>()) + .filter(|re| !re.selected()) + .collect() + } + + /// Get a list of all [`queried subnets`](QueriedSubnet). + pub fn load_queried_subnets(&self) -> Vec { + self.routing_table.read().iter_queries().collect() + } + + pub fn load_no_route_entries(&self) -> Vec { + self.routing_table.read().iter_no_route().collect() + } + + /// Task which periodically checks for dead peers in the Router. + async fn check_for_dead_peers(self) { + loop { + // check for dead peers every second + tokio::time::sleep(DEAD_PEER_CHECK_INTERVAL).await; + + trace!("Checking for dead peers"); + + let dead_peers = { + // a peer is assumed dead when the peer's last sent ihu exceeds a threshold + let mut dead_peers = Vec::new(); + for peer in self.peer_interfaces.read().unwrap().iter() { + // check if the peer's last_received_ihu is greater than the threshold + if peer.time_last_received_ihu().elapsed() > DEAD_PEER_THRESHOLD { + // peer is dead + info!("Peer {} is dead", peer.connection_identifier()); + // Notify peer it's dead in case it's not aware of that yet. + peer.died(); + dead_peers.push(peer.clone()); + } + } + dead_peers + }; + + self.handle_dead_peer(&dead_peers); + } + } + + /// Remove a dead peer from the router. + pub fn handle_dead_peer(&self, dead_peers: &[Peer]) { + for dead_peer in dead_peers { + self.metrics.router_peer_died(); + debug!( + "Cleaning up peer {} which is reportedly dead", + dead_peer.connection_identifier() + ); + self.remove_peer_interface(dead_peer); + } + + // Scope for routing table write access. + let subnets_to_select = { + let mut rt_write = self.routing_table.write(); + let mut rt_write = rt_write.iter_mut(); + + let mut subnets_to_select = Vec::new(); + + while let Some((subnet, mut rl)) = rt_write.next() { + rl.update_routes(|routes, eres, ct| { + for dead_peer in dead_peers { + let Some(mut re) = routes.iter_mut().find(|re| re.neighbour() == dead_peer) + else { + continue; + }; + + if re.selected() { + subnets_to_select.push(subnet); + + // Don't clear selected flag yet, running route selection does that for us. + re.set_metric(Metric::infinite()); + re.set_expires( + tokio::time::Instant::now() + RETRACTED_ROUTE_HOLD_TIME, + eres.clone(), + ct.clone(), + ); + } else { + routes.remove(dead_peer); + } + } + }); + } + + subnets_to_select + }; + + // And run required route selection + for subnet in subnets_to_select { + self.route_selection(subnet); + } + } + + /// Run route selection for a given subnet. + /// + /// This will cause a triggered update if needed. + fn route_selection(&self, subnet: Subnet) { + self.metrics.router_route_selection_ran(); + debug!("Running route selection for {subnet}"); + + let Some(mut routes) = self.routing_table.routes_mut(subnet) else { + // Subnet not known + return; + }; + + // If there is no selected route there is nothing to do here. We keep expired routes in the + // table for a while so updates of those should already have propagated to peers. + let route_list = routes.routes(); + // If we have a new selected route we must have at least 1 item in the route list so + // accessing the 0th list element here is fine. + if let Some(new_selected) = self.find_best_route(&route_list).cloned() { + if new_selected.neighbour() == route_list[0].neighbour() && route_list[0].selected() { + debug!( + "New selected route for {subnet} is the same as the route alreayd installed" + ); + return; + } + + if new_selected.metric().is_infinite() + && route_list[0].metric().is_infinite() + && route_list[0].selected() + { + debug!("New selected route for {subnet} is retracted, like the previously selected route"); + return; + } + + routes.set_selected(new_selected.neighbour()); + } else if !route_list.is_empty() && route_list[0].selected() { + // This means we went from a selected route to a non-selected route. Unselect route and + // trigger update. + // At this point we also send a seqno request to all peers which advertised this route + // to us, to try and get an updated entry. This uses the source key of the unselected + // entry. + self.send_seqno_request(route_list[0].source(), None, None); + routes.unselect(); + } + + drop(routes); + + self.trigger_update(subnet, None); + } + + /// Remove expired source keys from the router state. + async fn process_expired_source_keys( + self, + mut expired_source_key_stream: mpsc::Receiver, + ) { + while let Some(sk) = expired_source_key_stream.recv().await { + debug!("Removing expired source entry {sk}"); + self.source_table.write().unwrap().remove(&sk); + self.metrics.router_source_key_expired(); + } + warn!("Expired source key processing halted"); + } + + /// Remove expired route keys from the router state. + async fn process_expired_route_keys( + self, + mut expired_route_key_stream: mpsc::Receiver, + ) { + while let Some(rk) = expired_route_key_stream.recv().await { + let subnet = rk.subnet(); + debug!(route.subnet = %subnet, "Got expiration event for route"); + // Scope mutable access to routes. + // TODO: Is this really needed? + { + // Load current key + let Some(mut routes) = self.routing_table.routes_mut(rk.subnet()) else { + // Subnet now known anymore. This means an expiration timer fired while the entry + // itself is gone already. + warn!(%subnet, "Route key expired for unknown subnet"); + continue; + }; + let route_selection = + routes.update_routes(|routes, eres, ct| { + let Some(mut entry) = routes + .iter_mut() + .find(|re| re.neighbour() == rk.neighbour()) + else { + return false; + }; + self.metrics.router_route_key_expired(!entry.selected()); + if entry.selected() { + debug!(%subnet, peer = rk.neighbour().connection_identifier(), "Selected route expired, increasing metric to infinity"); + entry.set_metric(Metric::infinite()); + entry.set_expires(tokio::time::Instant::now() + RETRACTED_ROUTE_HOLD_TIME, eres.clone(), ct.clone()); + } else { + debug!(%subnet, peer = rk.neighbour().connection_identifier(), "Unselected route expired, removing fallback route"); + routes.remove(rk.neighbour()); + // Removing a fallback route does not require any further changes. It does + // not affect the selected route and by extension does not require a + // triggered udpate. + return false; + } + true + + }); + + if !route_selection { + continue; + } + + // Re run route selection if this was the selected route. We should do this before + // publishing to potentially select a new route, however a time based expiraton of a + // selected route generally means no other routes are viable anyway, so the short lived + // black hole this could create is not really a concern. + self.metrics.router_selected_route_expired(); + // Only inject selected route if we are simply retracting it, otherwise it is + // actually already removed. + debug!("Rerun route selection after expiration event"); + if let Some(r) = self.find_best_route(&routes.routes()).cloned() { + routes.set_selected(r.neighbour()); + } else { + debug!("Route selection did not find a viable route, unselect existing routes"); + routes.unselect(); + } + } + + // TODO: Is this _always_ needed? + self.trigger_update(subnet, None); + } + warn!("Expired route key processing halted"); + } + + /// Process notifications about peers who are dead. This allows peers who can self-diagnose + /// connection states to notify us, and allow for more efficient cleanup. + async fn process_dead_peers(self, mut dead_peer_stream: mpsc::Receiver) { + let mut tx_buf = Vec::with_capacity(100); + loop { + let received = dead_peer_stream.recv_many(&mut tx_buf, 100).await; + if received == 0 { + break; + } + self.handle_dead_peer(&tx_buf[..received]); + tx_buf.clear(); + } + warn!("Processing of dead peers halted"); + } + + /// Task which ingests and processes control packets. This spawns another background task for + /// every TLV type, and forwards the inbound packets to the proper background task. + async fn handle_incoming_control_packet( + self, + mut router_control_rx: UnboundedReceiver<(ControlPacket, Peer)>, + ) { + let (hello_tx, hello_rx) = mpsc::unbounded_channel(); + let (ihu_tx, ihu_rx) = mpsc::unbounded_channel(); + let (update_tx, update_rx) = mpsc::channel(1_000_000); + let (rr_tx, rr_rx) = mpsc::unbounded_channel(); + let (sn_tx, sn_rx) = mpsc::channel(100_000); + + tokio::spawn(self.clone().hello_processor(hello_rx)); + tokio::spawn(self.clone().ihu_processor(ihu_rx)); + tokio::spawn(self.clone().update_processor(update_rx)); + tokio::spawn(self.clone().route_request_processor(rr_rx)); + tokio::spawn(self.clone().seqno_request_processor(sn_rx)); + + while let Some((control_packet, source_peer)) = router_control_rx.recv().await { + // First update metrics with the remaining outstanding TLV's + self.metrics.router_received_tlv(); + trace!( + "Received control packet from {}", + source_peer.connection_identifier() + ); + + // Route packet to proper work queue. + match control_packet { + babel::Tlv::Hello(hello) => { + if hello_tx.send((hello, source_peer)).is_err() { + break; + }; + } + babel::Tlv::Ihu(ihu) => { + if ihu_tx.send((ihu, source_peer)).is_err() { + break; + }; + } + babel::Tlv::Update(update) => { + if let Err(e) = update_tx.try_send((update, source_peer)) { + match e { + mpsc::error::TrySendError::Closed(_) => { + self.metrics.router_tlv_discarded(); + break; + } + mpsc::error::TrySendError::Full(update) => { + // If the metric is directly connected (0), always process the + // update. + if update.0.metric().is_direct() { + // Channel disconnected + if update_tx.send(update).await.is_err() { + self.metrics.router_tlv_discarded(); + break; + } + } else { + self.metrics.router_tlv_discarded(); + } + } + } + }; + } + babel::Tlv::RouteRequest(route_request) => { + if rr_tx.send((route_request, source_peer)).is_err() { + break; + }; + } + babel::Tlv::SeqNoRequest(seqno_request) => { + if let Err(e) = sn_tx.try_send((seqno_request, source_peer)) { + match e { + mpsc::error::TrySendError::Closed(_) => { + self.metrics.router_tlv_discarded(); + break; + } + mpsc::error::TrySendError::Full(_) => { + self.metrics.router_tlv_discarded(); + } + } + }; + } + } + } + } + + /// Background task to process hello TLV's. + async fn hello_processor(self, mut hello_rx: UnboundedReceiver<(Hello, Peer)>) { + while let Some((hello, source_peer)) = hello_rx.recv().await { + let start = std::time::Instant::now(); + + if !source_peer.alive() { + trace!("Dropping Hello TLV since sender is dead."); + self.metrics.router_tlv_source_died(); + continue; + } + + self.handle_incoming_hello(hello, source_peer); + + self.metrics + .router_time_spent_handling_tlv(start.elapsed(), "hello"); + } + } + + /// Background task to process IHU TLV's. + async fn ihu_processor(self, mut ihu_rx: UnboundedReceiver<(Ihu, Peer)>) { + while let Some((ihu, source_peer)) = ihu_rx.recv().await { + let start = std::time::Instant::now(); + + if !source_peer.alive() { + trace!("Dropping IHU TLV since sender is dead."); + self.metrics.router_tlv_source_died(); + continue; + } + + self.handle_incoming_ihu(ihu, source_peer); + + self.metrics + .router_time_spent_handling_tlv(start.elapsed(), "ihu"); + } + } + + /// Background task to process Update TLV's. + async fn update_processor(self, mut update_rx: Receiver<(Update, Peer)>) { + let mut senders = Vec::with_capacity(self.update_workers); + for _ in 0..self.update_workers { + let router = self.clone(); + let (tx, mut rx) = mpsc::channel::<(_, Peer)>(1_000_000); + + tokio::task::spawn_blocking(move || { + while let Some((update, source_peer)) = rx.blocking_recv() { + let start = std::time::Instant::now(); + + if !source_peer.alive() { + trace!("Dropping Update TLV since sender is dead."); + router.metrics.router_tlv_source_died(); + continue; + } + + router.handle_incoming_update(update, source_peer); + + router + .metrics + .router_time_spent_handling_tlv(start.elapsed(), "update"); + } + warn!("Update processor task exitted"); + }); + + senders.push(tx); + } + + while let Some(item) = update_rx.recv().await { + let mut hasher = ahash::AHasher::default(); + item.0.subnet().network().hash(&mut hasher); + let slot = hasher.finish() as usize % self.update_workers; + + if let Err(e) = senders[slot].try_send(item) { + match e { + mpsc::error::TrySendError::Closed(_) => { + self.metrics.router_tlv_discarded(); + break; + } + + mpsc::error::TrySendError::Full(update) => { + // If the metric is directly connected (0), always process the + // update. + if update.0.metric().is_direct() { + // Channel disconnected + if senders[slot].send(update).await.is_err() { + self.metrics.router_tlv_discarded(); + break; + } + } else { + self.metrics.router_tlv_discarded(); + } + } + } + }; + } + warn!("Update processor coordinator exitted"); + } + + /// Background task to process Route Request TLV's. + async fn route_request_processor(self, mut rr_rx: UnboundedReceiver<(RouteRequest, Peer)>) { + while let Some((rr, source_peer)) = rr_rx.recv().await { + let start = std::time::Instant::now(); + + if !source_peer.alive() { + trace!("Dropping Route request TLV since sender is dead."); + self.metrics.router_tlv_source_died(); + continue; + } + + self.handle_incoming_route_request(rr, source_peer); + + self.metrics + .router_time_spent_handling_tlv(start.elapsed(), "route_request"); + } + } + + /// Background task to process Seqno Request TLV's. + async fn seqno_request_processor(self, mut sn_rx: Receiver<(SeqNoRequest, Peer)>) { + while let Some((sn, source_peer)) = sn_rx.recv().await { + let start = std::time::Instant::now(); + + if !source_peer.alive() { + trace!("Dropping Route request TLV since sender is dead."); + self.metrics.router_tlv_source_died(); + continue; + } + + self.handle_incoming_seqno_request(sn, source_peer); + + self.metrics + .router_time_spent_handling_tlv(start.elapsed(), "seqno"); + } + } + + /// Handle a received hello TLV + fn handle_incoming_hello(&self, _: babel::Hello, source_peer: Peer) { + self.metrics.router_process_hello(); + // Upon receiving and Hello message from a peer, this node has to send a IHU back + // TODO: properly calculate RX cost, for now just set the link cost. + let ihu = ControlPacket::new_ihu(source_peer.link_cost().into(), IHU_INTERVAL, None); + if source_peer.send_control_packet(ihu).is_err() { + trace!( + "Failed to send IHU reply to peer: {}", + source_peer.connection_identifier() + ); + } + } + + /// Handle a received IHU TLV + fn handle_incoming_ihu(&self, _: babel::Ihu, source_peer: Peer) { + self.metrics.router_process_ihu(); + // reset the IHU timer associated with the peer + // measure time between Hello and and IHU and set the link cost + let time_diff = tokio::time::Instant::now() + .duration_since(source_peer.time_last_received_hello()) + .as_millis(); + + source_peer.set_link_cost(time_diff as u16); + + // set the last_received_ihu for this peer + source_peer.set_time_last_received_ihu(tokio::time::Instant::now()); + } + + /// Process a route request. We reply with an Update if we have a selected route for the + /// requested subnet. If no subnet is requested (in other words the wildcard address), we dump + /// the full routing table. + fn handle_incoming_route_request( + &self, + mut route_request: babel::RouteRequest, + source_peer: Peer, + ) { + self.metrics + .router_process_route_request(route_request.prefix().is_none()); + // Handle the case of a single subnet. + if let Some(subnet) = route_request.prefix() { + // Check if these are local routes by chance + let update = if let Some(static_route) = self + .static_routes + .iter() + .find(|sr| sr.contains_subnet(&subnet)) + { + debug!( + "Advertising static route {static_route} in response to route request for {subnet}" + ); + babel::Update::new( + UPDATE_INTERVAL, // Static route is advertised with the default interval + self.router_seqno.read().unwrap().0, // Updates receive the seqno of the router + Metric::from(0), // Static route has no further hop costs + *static_route, + self.router_id, + ) + } else { + // If we have existing routes, attempt to send the selected route + match self.routing_table.routes(subnet) { + Routes::Exist(routes) => { + if let Some(sre) = routes.selected() { + trace!("Advertising selected route for {subnet} after route request"); + // Optimization: Don't send an update if the selected route next-hop is the peer + // who requested the route, as per the babel protocol the update will never be + // accepted. + if sre.neighbour() == &source_peer { + trace!( + "Not advertising route since the next-hop is the requesting peer" + ); + return; + } + babel::Update::new( + advertised_update_interval(sre), + sre.seqno(), + sre.metric() + Metric::from(sre.neighbour().link_cost()), + subnet, + sre.source().router_id(), + ) + } else { + // We don't have a selected route but we do have routes. It seems all routes + // are currently unfeasible. Don't send an update, but also don't send a + // retraction since we know the subnet exists + return; + } + } + // If the route is queried already we don't have to do anything. + // TODO: metric + Routes::Queried => return, + Routes::NoRoute => { + // We explicitly don't have a route for this subnet, so send a retraction to + // notify our peer as soon as possible not to expect anything. + babel::Update::new( + INTERVAL_NOT_REPEATING, + self.router_seqno.read().unwrap().0, // Retractions receive the seqno of the router + Metric::infinite(), // Static route has no further hop costs + subnet, // Advertise the exact subnet requested + self.router_id, // Our own router ID, since we advertise this + ) + } + Routes::None => { + // We don't have a route, but we also don't have confirmation locally that the + // route does not exist. Forward the route request to the remainder of our + // peers and search for the route. + // + // First check the generation of the route request, if it is too high we simply + // ignore it. Check against the eventually bumped generation. + if route_request.generation() + 1 >= MAX_RR_GENERATION { + // Drop route request + // TODO: metric + return; + } + route_request.inc_generation(); + + debug!( + rr.subnet = %subnet, + rr.generation = route_request.generation(), + "Forwarding route request to peers" + ); + + self.routing_table.mark_queried( + subnet, + tokio::time::Instant::now() + ROUTE_QUERY_TIMEOUT, + ); + + let received_peers = + if let Some((gen, received_peers)) = self.rr_cache.info(subnet) { + // If this route request has a smaller generation than the one we + // recently sent, send regardless + if route_request.generation() < gen { + vec![] + } else { + received_peers + } + } else { + vec![] + }; + + let peers = self.peer_interfaces(); + + // Now transmit to all our peers, except the sender + for peer in peers.iter().filter(|p| *p != &source_peer) { + if received_peers.contains(peer) { + trace!(%subnet, peer = peer.connection_identifier(), "Not forwarding route request to peer who recently got a request from us"); + continue; + } + + if peer + .send_control_packet(route_request.clone().into()) + .is_err() + { + debug!( + rr.subnet = %subnet, + rr.generation = route_request.generation(), + peer = peer.connection_identifier(), + "Can't forward route request to dead peer" + ); + } + } + + self.rr_cache.sent_route_request(route_request, peers); + + return; + } + } + }; + + self.update_source_table(&update); + self.send_update(&source_peer, update); + } else { + // TODO: Is this still needed? + // Requested a full route table dump + trace!("Dumping route table after wildcard route request"); + self.propagate_selected_routes_to_peer(&source_peer); + self.propagate_static_route_to_peer(&source_peer); + } + } + + /// Handle a received SeqNo request TLV. + fn handle_incoming_seqno_request(&self, mut seqno_request: SeqNoRequest, source_peer: Peer) { + self.metrics.router_process_seqno_request(); + // According to the babel rfc, we shoudl maintain a table of recent SeqNo requests and + // periodically retry requests without reply. We will however not do this for now and rely + // on the fact that we have stable links in general. + if let Some((sent_at, _)) = self.seqno_cache.info(&SeqnoRequestCacheKey { + router_id: seqno_request.router_id(), + subnet: seqno_request.prefix(), + seqno: seqno_request.seqno(), + }) { + // Only forward 1 request per minute for a given subnet request + if sent_at.elapsed() <= Duration::from_secs(60) { + self.metrics.router_seqno_request_unhandled(); + return; + } + }; + + // If we have a selected route for the prefix, and its router id is different from the + // requested router id, or the router id is the same and the entries sequence number is + // not smaller than the requested sequence number, send an update for the route + // to the peer (triggered update). + if let Some(route_entry) = self.routing_table.routes(seqno_request.prefix()).selected() { + if !route_entry.metric().is_infinite() + && (seqno_request.router_id() != route_entry.source().router_id() + || !route_entry.seqno().lt(&seqno_request.seqno())) + { + // we have a more up to date route or a different route, send an update + debug!( + "Replying to seqno request for seqno {} of {} with update packet", + seqno_request.seqno(), + seqno_request.prefix() + ); + let update = babel::Update::new( + advertised_update_interval(route_entry), + route_entry.seqno(), // updates receive the seqno of the router + route_entry.metric() + Metric::from(source_peer.link_cost()), + // the cost of the route is the cost of the route + the cost of the link to the peer + route_entry.source().subnet(), + // we looked for the router_id, which is a public key, in the dest_pubkey_map + // if the router_id is not in the map, then the route came from the node itself + route_entry.source().router_id(), + ); + + self.update_source_table(&update); + + self.send_update(&source_peer, update); + + self.metrics.router_seqno_request_reply_local(); + + return; + } + } + + // Otherwise, if the router id in the request matches the router id in our selected route + // and the requested sequence number is larger than the one on our selected route, compare + // the router id with our own router id. If it matches, bump our own sequence number by 1. + // At this point, we also send an update for the route (triggered update), to distribute + // the route. + // + // Note that we currently don't install local routes in the routing table and as such + // can't check on this. Therefore this condition is reworked. We always advertise local + // routes with the current router id and the current router seqno. So we check if the + // prefix is part of our static routes, if the router id is our own, and if the + // requested seqno is greater than our own. + let (router_seqno, last_seqno_bump) = *self.router_seqno.read().unwrap(); + if seqno_request.router_id() == self.router_id + && seqno_request.seqno().gt(&router_seqno) + && self.static_routes.contains(&seqno_request.prefix()) + { + if last_seqno_bump.elapsed() >= SEQNO_BUMP_TIMEOUT { + trace!("Ignoring seqno bump request which happened too fast"); + return; + } + // Bump router seqno + // TODO: should we only send an update to the peer who sent the seqno request + // instad of updating all our peers? + debug!("Bumping local router sequence number"); + // Scope the write lock on seqno + { + let mut router_seqno = self.router_seqno.write().unwrap(); + // First check again if we should bump + if router_seqno.1.elapsed() >= SEQNO_BUMP_TIMEOUT { + trace!("Ignoring seqno bump request which happened too fast"); + return; + } + // Bump seqno + router_seqno.0 += 1; + // Set last modified time + router_seqno.1 = Instant::now(); + } + + self.propagate_static_routes_to_peers(); + + self.metrics.router_seqno_request_bump_seqno(); + + return; + } + + // Otherwise, if the router-id from the request is not our own, we check the hop count + // field. If it is at least 2, we decrement it by 1, and forward the packet. To do so, we + // try to find a route to the subnet. First we check for a feasible route and send the + // packet there if the next hop is not the sender of this packet. Otherwise, we check for + // any route which might potentially be unfeasible, which also did not originate the + // packet. + if seqno_request.hop_count() < 2 { + self.metrics.router_seqno_request_dropped_ttl(); + return; + } + + if seqno_request.router_id() != self.router_id { + seqno_request.decrement_hop_count(); + + let srck = SeqnoRequestCacheKey { + router_id: seqno_request.router_id(), + subnet: seqno_request.prefix(), + seqno: seqno_request.seqno(), + }; + + let mut visited_peers = vec![]; + if let Some((last_sent, visited)) = self.seqno_cache.info(&srck) { + if last_sent.elapsed() < SEQNO_BUMP_TIMEOUT { + visited_peers = visited; + } + } + + if let Routes::Exist(possible_routes) = + self.routing_table.routes(seqno_request.prefix()) + { + { + let source_table = self.source_table.read().unwrap(); + // First only consider feasible routes. + for re in possible_routes.iter().filter(|re| { + !visited_peers.contains(re.neighbour()) + && source_table.route_feasible(re) + && re.neighbour() != &source_peer + && re.neighbour().alive() + && !re.metric().is_infinite() + }) { + debug!( + "Forwarding seqno request {} for {} to {}", + seqno_request.seqno(), + seqno_request.prefix(), + re.neighbour().connection_identifier() + ); + if re + .neighbour() + .send_control_packet(seqno_request.clone().into()) + .is_err() + { + trace!( + "Failed to foward seqno request to {}", + re.neighbour().connection_identifier(), + ); + continue; + } + + self.seqno_cache + .forward(srck, re.neighbour().clone(), Some(source_peer)); + + self.metrics.router_seqno_request_forward_feasible(); + + return; + } + } + + // Finally consider infeasible routes as well. + for re in possible_routes.iter().filter(|re| { + !visited_peers.contains(re.neighbour()) + && re.neighbour() != &source_peer + && re.neighbour().alive() + && !re.metric().is_infinite() + }) { + debug!( + "Forwarding seqno request {} for {} to {}", + seqno_request.seqno(), + seqno_request.prefix(), + re.neighbour().connection_identifier() + ); + if re + .neighbour() + .send_control_packet(seqno_request.clone().into()) + .is_err() + { + trace!( + "Failed to foward seqno request to infeasible peer {}", + re.neighbour().connection_identifier(), + ); + continue; + } + + self.seqno_cache + .forward(srck, re.neighbour().clone(), Some(source_peer)); + + self.metrics.router_seqno_request_forward_unfeasible(); + + return; + } + } + } + + self.metrics.router_seqno_request_unhandled(); + } + + /// Finds the best feasible route from a list of [`route entries`](RouteEntry). It is possible + /// for this method to select a retracted route. In this case, retraction updates should be + /// send out. + /// + /// This method only selects a different best route if it is significantly better compared to + /// the current route. + fn find_best_route<'a>(&self, routes: &'a RouteList) -> Option<&'a RouteEntry> { + let source_table = self.source_table.read().unwrap(); + let current = routes.selected(); + let best = routes + .iter() + // Infinite metrics are technically feasible, but for route selection we explicitly + // don't want infinite metrics as those routes are unreachable. + .filter(|re| !re.metric().is_infinite() && source_table.route_feasible(re)) + .min_by_key(|re| re.metric() + Metric::from(re.neighbour().link_cost())); + + if let (Some(best), Some(current)) = (best, current) { + // If we swap to an actually different route, only do so if the metric is + // significantly better OR if it is directly connected (metric 0). + if (best.source() != current.source() || best.neighbour() != current.neighbour()) + && !(best.metric() + Metric::from(best.neighbour().link_cost()) + < current.metric() + Metric::from(current.neighbour().link_cost()) + - SIGNIFICANT_METRIC_IMPROVEMENT + || best.metric().is_direct()) + { + debug!("maintaining currently selected route since new route is not significantly better"); + return Some(current); + } + } + + best + } + + /// Handle a received update TLV + fn handle_incoming_update(&self, update: babel::Update, source_peer: Peer) { + self.metrics.router_process_update(); + // Check if we actually allow this update based on filters. + for filter in &*self.update_filters { + if !filter.allow(&update) { + debug!("Update denied by filter"); + self.metrics.router_update_denied_by_filter(); + return; + } + } + + let metric = update.metric(); + let router_id = update.router_id(); + let seqno = update.seqno(); + let subnet = update.subnet(); + + // Make sure we don't process an update for one of our own subnets. + if self.is_static_subnet(subnet) { + return; + } + + // We accepted the update, check if we have a seqno request sent for this update + let interested_peers = self.seqno_cache.get(&SeqnoRequestCacheKey { + router_id, + subnet, + seqno, + }); + + let update_feasible = self + .source_table + .read() + .unwrap() + .is_update_feasible(&update); + + // We load all routes here for the subnet. Because we hold the mutex for the + // writer, this view is accurate and we can't diverge until the mutex is released. + let mut routing_table_entries = { + if let Some(rte) = self.routing_table.routes_mut(subnet) { + rte + } else { + if !update_feasible || metric.is_infinite() { + if !update_feasible { + self.send_seqno_request( + SourceKey::new(update.subnet(), update.router_id()), + Some(source_peer), + None, + ); + } + debug!(%subnet, "Ignore unfeasible update | retraction for unknown subnet"); + self.metrics.router_update_not_interested(); + return; + } + let ss = self.node_keypair.0.shared_secret(&router_id.to_pubkey()); + self.routing_table.add_subnet(subnet, ss) + } + }; + + let mut old_selected_route = None; + let route_selection = routing_table_entries.update_routes(|routes, eres, ct| { + // Take a deep copy of the old selected route if there is one, deep copy since we will + // potentially mutate the route list so we can't keep a reference to it. + old_selected_route = routes.selected().cloned(); + + let maybe_existing_entry = routes.iter_mut().find(|re| re.neighbour() == &source_peer); + + debug!( + subnet = %subnet, + metric = %metric, + seqno = %seqno, + router_id = %router_id, + entry_exists = maybe_existing_entry.is_some(), + update_feasible = update_feasible, + peer = source_peer.connection_identifier(), + "Processing update packet", + ); + + if let Some(mut existing_entry) = maybe_existing_entry { + // Unfeasible updates to the selected route are not applied, but we do request a seqno + // bump. + if existing_entry.selected() + && !update_feasible + && existing_entry.source().router_id() == router_id + { + self.send_seqno_request( + existing_entry.source(), + Some(source_peer.clone()), + None, + ); + return false; + } + + // Otherwise we just update the entry. + if existing_entry.metric().is_infinite() && update.metric().is_infinite() { + // Retraction for retracted route, don't do anything. If we don't filter this + // retracted routes can stay stuck if peer keep sending retractions to eachother + // for this route. + return false; + } + existing_entry.set_seqno(seqno); + existing_entry.set_metric(metric); + existing_entry.set_router_id(router_id); + + // Only reset the timer if the update is feasible, this will allow unfeasible updates + // to be flushed out naturally. Note that a retraction is always feasbile. + if update_feasible { + existing_entry.set_expires( + tokio::time::Instant::now() + route_hold_time(&update), + eres.clone(), + ct.clone(), + ); + } + + // If the route is not selected, and the update is unfeasible, the route will not be + // selected by a subsequent route selection, so we can skip it and avoid wasting time + // here. + if !existing_entry.selected() && !update_feasible { + trace!("Ignoring route selection for unfeasible update to unselected route"); + return false; + } + + // If the update is unfeasible the route must be unselected. + if existing_entry.selected() && !update_feasible { + existing_entry.set_selected(false); + } + } else { + // If there is no entry yet ignore unfeasible updates and retractions. + if metric.is_infinite() || !update_feasible { + // If the update is not feasible, and we don't have a selected route for the + // subnet, request a seqno bump. + if !update_feasible && routes.selected().is_none() { + self.send_seqno_request( + SourceKey::new(update.subnet(), update.router_id()), + Some(source_peer.clone()), + None, + ); + } + debug!("Received unfeasible update | retraction for unknown route - neighbour"); + return false; + } + + // Create new entry in the route table + routes.insert( + RouteEntry::new( + SourceKey::new(subnet, router_id), + source_peer.clone(), + metric, + seqno, + false, + tokio::time::Instant::now() + route_hold_time(&update), + ), + eres.clone(), + ct.clone(), + ); + } + + true + }); + + if !route_selection { + self.metrics.router_update_skipped_route_selection(); + return; + } + + // Now that we applied the update, run route selection. + let new_selected_route = self + .find_best_route(&routing_table_entries.routes()) + .cloned(); + if let Some(ref nbr) = new_selected_route { + // Install this route in the routing table. + routing_table_entries.set_selected(nbr.neighbour()); + } else if let Some(ref osr) = old_selected_route { + // If there is no new selected route, but there was one previously, update the routing + // table. This is not covered above, as there only unfeasible updates cause a selected + // route to be unselected. However, this may also be the result of a feasible update + // (e.g. retraction with no feasible fallback routes). + // Only do this if we did not unselect the existing route previously. + // Regardless if we unselect the route here or not, we lost a selected route for the + // subnet, so we try to refresh the route by sending out a seqno request to all + // neigbours who advertised the route at some point. + self.send_seqno_request(osr.source(), None, None); + + routing_table_entries.unselect(); + }; + + // Drop these here so the update gets reflected in the routing table, which is needed for a + // possible triggered update later on. + drop(routing_table_entries); + + // At this point we are done, though we would like to understand if we need to send a + // triggered update to our peers. This is done if there is a sufficiently large change. We + // consider a sufficiently large change to be: + // - change in router_id, + // - aquired a route, i.e. previously there was no selected route but there is now, + // - lost the route (i.e. it is retracted). + // - significant metric change + // What doesn't constitue a large change: + // - small metric change + // - seqno increase (unless it is requested by a peer) + let trigger_update = match (&old_selected_route, new_selected_route) { + (Some(old_route), Some(new_route)) => { + if new_route.neighbour() != old_route.neighbour() { + debug!( + subnet = %subnet, + old_next_hop = old_route.neighbour().connection_identifier(), + new_next_hop = new_route.neighbour().connection_identifier(), + "Selected route changed next-hop", + ); + } + // Router id changed. + new_route.source().router_id() != old_route.source().router_id() + || new_route.metric().delta(&old_route.metric()) > BIG_METRIC_CHANGE_TRESHOLD + } + (None, Some(new_route)) => { + info!( + subnet = %subnet, + peer = new_route.neighbour().connection_identifier(), + "Acquired route", + ); + true + } + (Some(old_route), None) => { + info!( + subnet = %subnet, + peer = old_route.neighbour().connection_identifier(), + "Lost route", + ); + true + } + (None, None) => false, + }; + + if trigger_update { + debug!(subnet = %subnet, "Send triggered update in response to update"); + self.trigger_update(subnet, None); + } else if interested_peers.is_some() { + debug!( + subnet = %subnet, + "Send update to peers who registered interest through a seqno request" + ); + // If we have some interest in an update because we forwarded a seqno request but we + // aren't triggering an update, notify just the interested peers. + self.trigger_update(subnet, interested_peers); + } + } + + /// Trigger an update for the given [`Subnet`]. If `peers` is [`None`], send the update to all + /// peers the `Router` knows. + fn trigger_update(&self, subnet: Subnet, peers: Option>) { + self.metrics.router_triggered_update(); + self.propagate_selected_route(subnet, peers); + } + + /// Send a seqno request for a subnet. This can be sent to a given peer, or to all peers for + /// the subnet if no peer is given. + /// + /// The SourceKey must exist in the source table. + fn send_seqno_request( + &self, + source: SourceKey, + to: Option, + request_origin: Option, + ) { + let fd = match self.source_table.read().unwrap().get(&source) { + Some(fd) => *fd, + None => { + // This can happen if you only have 1 peer, or in case we haven't announced our + // subnets yet. + debug!("Requesting seqno for source key {source} which does not exist in the source table"); + // Since we don't know the real seqno, just use the default. Either the peer will + // reply if it has an adequate route, or it will forward the request. + FeasibilityDistance::new(Metric::new(0), SeqNo::new()) + } + }; + + let sn: ControlPacket = + SeqNoRequest::new(fd.seqno() + 1, source.router_id(), source.subnet()).into(); + + let srck = SeqnoRequestCacheKey { + router_id: source.router_id(), + subnet: source.subnet(), + seqno: fd.seqno() + 1, + }; + + let seqno_info = self.seqno_cache.info(&srck); + + let targets = if let Some(target) = to { + vec![target] + } else { + // If we don't have a dedicated peer to send to, just send to every peer which + // announced the subnet. But avoid repetitions. Additionally, if we don't have any + // peer which announced the subnet (because all anounces are unfeasible) + let mut peers_sent = vec![]; + if let Some((last_sent, visited)) = seqno_info { + // If it's more than some time since we last sent an update to any peer for this + // seqno request, send it again. We use the seqno bump timeout here, since that is + // the quickest time between bumps from a peer. + if last_sent.elapsed() < SEQNO_BUMP_TIMEOUT { + peers_sent = visited; + } + }; + + let known_routes = self.routing_table.routes(source.subnet()); + + // Make sure a broadcast only happens in case the local node originated the request. + if known_routes.is_none() && request_origin.is_none() { + // If we don't know the route just ask all our peers. + self.peer_interfaces + .read() + .unwrap() + .iter() + .cloned() + .collect() + } else if let Routes::Exist(known_routes) = known_routes { + known_routes + .iter() + .filter_map(|re| { + if !peers_sent.contains(re.neighbour()) { + Some(re.neighbour().clone()) + } else { + None + } + }) + .collect() + } else { + vec![] + } + }; + + // Don't overload peers with request for the same source key + let already_received = self.seqno_cache.get(&srck).unwrap_or_default(); + + for peer in targets { + if already_received.contains(&peer) { + trace!( + peer = peer.connection_identifier(), + "Don't send seqno request to peer which already received it recently" + ); + continue; + } + + debug!( + "Sending seqno_request to {} for seqno {} of {}", + peer.connection_identifier(), + fd.seqno() + 1, + source.subnet(), + ); + + if peer.send_control_packet(sn.clone()).is_err() { + trace!( + "Failed to send seqno request to {}", + peer.connection_identifier() + ); + continue; + } + + self.seqno_cache.forward(srck, peer, request_origin.clone()); + } + } + + /// Checks if a route key is an exact match for a static route. + #[inline] + fn is_static_subnet(&self, subnet: Subnet) -> bool { + self.static_routes.contains(&subnet) + } + + pub fn route_packet(&self, mut data_packet: DataPacket) { + let node_tun_subnet = self.node_tun_subnet(); + + trace!( + "Incoming data packet {} -> {}", + data_packet.src_ip, + data_packet.dst_ip, + ); + + if data_packet.hop_limit < 2 { + self.metrics.router_route_packet_ttl_expired(); + self.time_exceeded(data_packet); + return; + } + data_packet.hop_limit -= 1; + + if node_tun_subnet.contains_ip(data_packet.dst_ip.into()) { + self.metrics.router_route_packet_local(); + if let Err(e) = self.node_tun().send(data_packet) { + error!("Error sending data packet to TUN interface: {:?}", e); + } + } else { + match self.routing_table.best_routes(data_packet.dst_ip.into()) { + Routes::Exist(routes) => match routes.selected() { + Some(route_entry) => { + // Prematurely send a route request if the route is almost expired + if route_entry + .expires() + .duration_since(tokio::time::Instant::now()) + < ROUTE_ALMOST_EXPIRED_TRESHOLD + { + self.send_route_request( + Subnet::new(data_packet.dst_ip.into(), 64) + .expect("64 is a valid subnet size for IPv6, qed;"), + ); + } + + self.metrics.router_route_packet_forward(); + + if let Err(e) = route_entry.neighbour().send_data_packet(data_packet) { + error!( + "Error sending data packet to peer {}: {:?}", + route_entry.neighbour().connection_identifier(), + e + ); + } + } + None => { + self.metrics.router_route_packet_no_route(); + self.no_route_to_host(data_packet); + } + }, + Routes::Queried => { + // Wait for query resolution + // TODO: proper fix + tokio::task::block_in_place(|| std::thread::sleep(QUERY_CHECK_DURATION)); + self.route_packet(data_packet); + } + Routes::NoRoute => { + self.metrics.router_route_packet_no_route(); + self.no_route_to_host(data_packet); + } + Routes::None => { + // Send route request + // NOTE: we request the full /64 subnet + self.send_route_request( + Subnet::new(data_packet.dst_ip.into(), 64) + .expect("64 is a valid subnet size for IPv6 addresses"), + ); + // TODO: proper fix + tokio::task::block_in_place(|| std::thread::sleep(QUERY_CHECK_DURATION)); + self.route_packet(data_packet); + } + } + } + } + + /// Send a [`RouteRequest`] for the [`Subnet`] to all peers. + fn send_route_request(&self, subnet: Subnet) { + let route_request = RouteRequest::new(Some(subnet), 0); + + self.routing_table + .mark_queried(subnet, tokio::time::Instant::now() + ROUTE_QUERY_TIMEOUT); + + // Don't care about generation here + let received_peers = if let Some((gen, receivers)) = self.rr_cache.info(subnet) { + // If the previous request was a higher generation, send the reques with lower + // generation to all our peers again + if gen > 0 { + vec![] + } else { + receivers + } + } else { + vec![] + }; + + let peers = self.peer_interfaces(); + + for peer in peers.iter() { + if received_peers.contains(peer) { + trace!(%subnet, peer = peer.connection_identifier(), "Not forwarding route request to peer who recently got a request from us"); + continue; + } + + if peer + .send_control_packet(route_request.clone().into()) + .is_err() + { + debug!(subnet = %subnet, "Could not send route request to peer"); + } + } + + self.rr_cache.sent_route_request(route_request, peers); + } + + /// Handle a received data packet. + async fn handle_incoming_data_packet(self, mut router_data_rx: Receiver) { + while let Some(data_packet) = router_data_rx.recv().await { + self.route_packet(data_packet); + } + warn!("Router data receiver stream ended"); + } + + /// Handle a packet who's TTL is too low. + fn time_exceeded(&self, data_packet: DataPacket) { + trace!("Refusing to forward expired packet"); + self.oob_icmp( + Icmpv6Type::TimeExceeded(TimeExceededCode::HopLimitExceeded), + data_packet, + ) + } + + /// Handle a packet if we have no route for the destination address. + fn no_route_to_host(&self, data_packet: DataPacket) { + trace!( + "Could not forward data packet, no route found for {}", + data_packet.dst_ip + ); + + self.oob_icmp( + Icmpv6Type::DestinationUnreachable(DestUnreachableCode::NoRoute), + data_packet, + ) + } + + /// Send an oob icmp packet of the specified type in reply to the given DataPakcet. + fn oob_icmp(&self, icmp_type: Icmpv6Type, mut data_packet: DataPacket) { + let src_ip = if let IpAddr::V6(ip) = self.node_tun_subnet.address() { + ip + } else { + panic!("IPv4 not supported yet") + }; + + let icmp_header = + etherparse::PacketBuilder::ipv6(src_ip.octets(), data_packet.src_ip.octets(), 64) + .icmpv6(icmp_type); + + let mut pb = PacketBuffer::new(); + // Don't exceed MIN_MTU for the constructed packet + // TODO: use proper consts + if data_packet.raw_data.len() > (1280 - 48) { + // Just drop raw_data, we don't need it anymore in this case and by doing this we have + // a unified code path later. Also we release the no longer used memory just a tad bit + // slower, though it's unlikely that this matters. + data_packet.raw_data = vec![]; + } + let serialized_icmp_size = icmp_header.size(data_packet.raw_data.len()); + pb.set_size(serialized_icmp_size + 16); + pb.buffer_mut()[..16].copy_from_slice(&data_packet.dst_ip.octets()); + let mut ps = &mut pb.buffer_mut()[16..16 + serialized_icmp_size]; + if let Err(e) = icmp_header.write(&mut ps, &data_packet.raw_data) { + error!("Failed to write ICMP packet {e}"); + return; + } + + // TODO: import consts + let mut header = pb.header_mut(); + header[0] = 1; + header[1] = 2; + + // Get shared secret from node and dest address + let shared_secret = match self.get_shared_secret_from_dest(data_packet.src_ip.into()) { + Some(ss) => ss, + None => { + debug!( + "No entry found for destination address {}, dropping packet", + data_packet.src_ip + ); + return; + } + }; + + let enc = shared_secret.encrypt(pb); + + self.route_packet(DataPacket { + dst_ip: data_packet.src_ip, + src_ip, + hop_limit: 64, + raw_data: enc, + }); + } + + /// Propagate all static routes to all known peers. + fn propagate_static_routes_to_peers(&self) { + for peer in self.peer_interfaces.read().unwrap().iter() { + self.propagate_static_route_to_peer(peer); + } + } + + /// Applies the effect of an [`Update`] to the [`SourceTable`]. + fn update_source_table(&self, update: &Update) { + let metric = update.metric(); + let seqno = update.seqno(); + + let source_key = SourceKey::new(update.subnet(), update.router_id()); + + let mut source_table = self.source_table.write().unwrap(); + + if let Some(source_entry) = source_table.get(&source_key) { + // if seqno of the update is greater than the seqno in the source table, update the source table + if seqno.gt(&source_entry.seqno()) { + source_table.insert( + source_key, + FeasibilityDistance::new(metric, seqno), + self.expired_source_key_sink.clone(), + ); + } + // if seqno of the update is equal to the seqno in the source table, update the source table if the metric (of the update) is lower + else if seqno == source_entry.seqno() && source_entry.metric() > metric { + source_table.insert( + source_key, + // Technically the seqno in the feasibility distance comes from the source + // entry, but that gives a borrow conflict so we use seqno from the update, + // which we just verified is the same. + FeasibilityDistance::new(metric, seqno), + self.expired_source_key_sink.clone(), + ) + } + // We also reset the garbage collection timer (unless the update is a retraction) + else if !metric.is_infinite() { + source_table.reset_timer(source_key, self.expired_source_key_sink.clone()); + } + } + // no entry for this source key, so insert it + else { + source_table.insert( + source_key, + FeasibilityDistance::new(metric, seqno), + self.expired_source_key_sink.clone(), + ) + }; + } + + /// Task which periodically sends a Hello TLV to all known peers + async fn start_periodic_hello_sender(self) { + let hello_interval = Duration::from_secs(HELLO_INTERVAL); + loop { + tokio::time::sleep(hello_interval).await; + + for peer in self.peer_interfaces.read().unwrap().iter() { + let hello = ControlPacket::new_hello(peer, hello_interval); + peer.set_time_last_received_hello(tokio::time::Instant::now()); + + if peer.send_control_packet(hello).is_err() { + trace!( + "Failed to send Hello TLV to dead peer {}", + peer.connection_identifier() + ); + } + } + } + } + + /// Send a control packet to a peer. + /// + /// Errors are not propagated to the caller. + fn send_update(&self, peer: &Peer, update: Update) { + trace!("Sending update to peer"); + + // Sanity check, verify what we are doing is actually usefull + if !peer.alive() { + trace!("Cowardly refusing to sent update to peer which we know is dead"); + self.metrics.router_update_dead_peer(); + return; + } + + if peer + .send_control_packet(ControlPacket::Update(update)) + .is_err() + { + // An error indicates the peer is dead + trace!( + "Failed to send update to dead peer {}", + peer.connection_identifier() + ); + } + } + + /// Task to propagate the static routes periodically + async fn propagate_static_routes(self) { + loop { + tokio::time::sleep(ROUTE_PROPAGATION_INTERVAL).await; + + trace!("Propagating static routes"); + + for peer in self.peer_interfaces.read().unwrap().iter() { + self.propagate_static_route_to_peer(peer) + } + } + } + + /// Propagate the static routes to a single peer + fn propagate_static_route_to_peer(&self, peer: &Peer) { + for sr in self.static_routes.iter() { + let update = babel::Update::new( + UPDATE_INTERVAL, + self.router_seqno.read().unwrap().0, // updates receive the seqno of the router + Metric::from(0), // Static route has no further hop costs + *sr, + self.router_id, + ); + self.update_source_table(&update); + + // send the update to the peer + self.send_update(peer, update); + } + } + + /// Propagate a selected route. Unless peers are specified, all knwon peers in the router are + /// used. + fn propagate_selected_route(&self, subnet: Subnet, peers: Option>) { + let (update, maybe_neigh) = + if let Some(sre) = self.routing_table.selected_route(subnet.address()) { + let update = babel::Update::new( + advertised_update_interval(&sre), + sre.seqno(), + sre.metric() + Metric::from(sre.neighbour().link_cost()), + sre.source().subnet(), + sre.source().router_id(), + ); + (update, Some(sre.neighbour().clone())) + } else { + // This can happen if the only feasible route gets an infinite metric, as those are + // never selected. + debug!(subnet = %subnet, "Retracting route"); + let update = babel::Update::new( + UPDATE_INTERVAL, + self.router_seqno.read().unwrap().0, + Metric::infinite(), + subnet, + self.router_id, + ); + (update, None) + }; + + self.update_source_table(&update); + + let send_update = |peer: &Peer| { + // Don't send updates for a route to the next hop of the route, as that peer will never + // select the route through us (that would caus a routing loop). The protocol can + // handle this just fine, leaving this out is essentially an easy optimization. + if let Some(ref neigh) = maybe_neigh { + if peer == neigh { + return; + } + } + debug!( + subnet = %subnet, + peer = peer.connection_identifier(), + "Propagating route update", + ); + self.send_update(peer, update.clone()); + }; + + if let Some(peers) = peers { + for peer in peers { + send_update(&peer); + } + } else { + for peer in self.peer_interfaces.read().unwrap().iter() { + send_update(peer); + } + }; + } + + /// Propagate all selected routes to all peers known in the router. + fn propagate_selected_routes_to_peer(&self, peer: &Peer) { + for (subnet, sre) in self + .routing_table + .read() + .iter() + .filter_map(|(subnet, route_list)| route_list.selected().map(|sr| (subnet, sr.clone()))) + { + let neigh_link_cost = Metric::from(sre.neighbour().link_cost()); + // Don't send updates for a route to the next hop of the route, as that peer will never + // select the route through us (that would caus a routing loop). The protocol can + // handle this just fine, leaving this out is essentially an easy optimization. + if peer == sre.neighbour() { + continue; + } + let update = babel::Update::new( + advertised_update_interval(&sre), + sre.seqno(), + // the cost of the route is the cost of the route + the cost of the link to the next-hop + sre.metric() + neigh_link_cost, + subnet, + sre.source().router_id(), + ); + debug!( + subnet = %subnet, + metric = %sre.metric() + neigh_link_cost, + seqno = %sre.seqno(), + peer = peer.connection_identifier(), + "Propagating route update", + ); + + self.update_source_table(&update); + + // send the update to the peer + self.send_update(peer, update); + } + } +} + +/// Manual clone implementation to avoid placing a where bound on `Router`, which would in turn +/// require a where bound on all structs which end up containing Router. +impl Clone for Router +where + M: Clone, +{ + fn clone(&self) -> Self { + Self { + routing_table: self.routing_table.clone(), + peer_interfaces: self.peer_interfaces.clone(), + source_table: self.source_table.clone(), + router_seqno: self.router_seqno.clone(), + static_routes: self.static_routes.clone(), + router_id: self.router_id, + node_keypair: self.node_keypair.clone(), + router_data_tx: self.router_data_tx.clone(), + router_control_tx: self.router_control_tx.clone(), + node_tun: self.node_tun.clone(), + node_tun_subnet: self.node_tun_subnet, + update_filters: self.update_filters.clone(), + dead_peer_sink: self.dead_peer_sink.clone(), + expired_source_key_sink: self.expired_source_key_sink.clone(), + seqno_cache: self.seqno_cache.clone(), + rr_cache: self.rr_cache.clone(), + update_workers: self.update_workers, + metrics: self.metrics.clone(), + } + } +} + +/// Calculate the hold time for a [`RouteEntry`] from an [`Update`](babel::Update) . +fn route_hold_time(update: &babel::Update) -> Duration { + // According to https://datatracker.ietf.org/doc/html/rfc8966#section-appendix.b a good value + // would be 3.5 times the update inteval. + // In case of a retracted route: in general this should not be added to the routing table, so + // the only reason this is called is because a route was retracted through an update. Even if + // the peer won't send this again, hold the route for some time so it can get flushed properly. + if update.metric().is_infinite() { + RETRACTED_ROUTE_HOLD_TIME + } else { + // Route expiry time -> 3.5 times advertised Update interval. + Duration::from_millis((update.interval().as_millis() * 7 / 2) as u64) + } +} + +/// Calculates the interval to use when announcing updates on (selected) routes. +fn advertised_update_interval(sre: &RouteEntry) -> Duration { + // We actually just need to set the value of the update interval, since that is the upper bound + // on when we will advertise the route again. + // One caveat is an expired route. If an entry is expired, it means that it will change state + // so: Infinite metric -> route entry will be removed. Finite metric -> route entry will be + // retracted but will be announced again. In practice this shouldn't really happen anyway. + if sre.metric().is_infinite() && sre.expires().elapsed() != Duration::from_millis(0) { + INTERVAL_NOT_REPEATING + } else { + UPDATE_INTERVAL + } +} + +#[cfg(test)] +mod tests { + use std::{ + net::{IpAddr, Ipv6Addr}, + sync::{atomic::AtomicU64, Arc}, + time::Duration, + }; + + use tokio::sync::mpsc; + + use crate::{ + babel::Update, crypto::PublicKey, metric::Metric, peer::Peer, router_id::RouterId, + sequence_number::SeqNo, source_table::SourceKey, subnet::Subnet, + }; + + #[test] + fn calculate_route_hold_time() { + let router_id = RouterId::new(PublicKey::from([0; 32])); + let seqno = SeqNo::new(); + let metric = Metric::new(0); + let subnet = Subnet::new( + IpAddr::V6(Ipv6Addr::new( + 0x400, 0x0123, 0x4567, 0x89AB, 0xCDEF, 0, 0, 0, + )), + 64, + ) + .expect("Valid subnet definition"); + let update = Update::new(Duration::from_secs(60), seqno, metric, subnet, router_id); + assert_eq!( + Duration::from_millis(210_000), + super::route_hold_time(&update) + ); + let update = Update::new(Duration::from_secs(1), seqno, metric, subnet, router_id); + assert_eq!( + Duration::from_millis(3_500), + super::route_hold_time(&update) + ); + // Since update is expressed in centiseconds, we lose precision and + // Duration::from_milis(478) is equal to Duration::from_millis(470); + let update = Update::new(Duration::from_millis(478), seqno, metric, subnet, router_id); + assert_eq!( + Duration::from_millis(1_645), + super::route_hold_time(&update) + ); + + // Retractions are also held for some time + let update = Update::new( + Duration::from_millis(0), + seqno, + Metric::infinite(), + subnet, + router_id, + ); + assert_eq!( + super::RETRACTED_ROUTE_HOLD_TIME, + super::route_hold_time(&update) + ); + } + + #[tokio::test] + async fn calculate_advertised_update_interval() { + // Set up a dummy peer since that is needed to create a `RouteEntry` + let (router_data_tx, _router_data_rx) = mpsc::channel(1); + let (router_control_tx, _router_control_rx) = mpsc::unbounded_channel(); + let (dead_peer_sink, _dead_peer_stream) = mpsc::channel(1); + let (con1, _con2) = tokio::io::duplex(1500); + let neighbor = Peer::new( + router_data_tx, + router_control_tx, + con1, + dead_peer_sink, + Arc::new(AtomicU64::new(0)), + Arc::new(AtomicU64::new(0)), + ) + .expect("Can create a dummy peer"); + let subnet = Subnet::new(IpAddr::V6(Ipv6Addr::new(0x400, 0, 0, 0, 0, 0, 0, 0)), 64) + .expect("Valid subnet definition"); + let router_id = RouterId::new(PublicKey::from([0; 32])); + let source = SourceKey::new(subnet, router_id); + let metric = Metric::new(0); + let seqno = SeqNo::new(); + let selected = true; + + let expiration = tokio::time::Instant::now() + Duration::from_secs(15); + let re = super::RouteEntry::new( + source, + neighbor.clone(), + metric, + seqno, + selected, + expiration, + ); + // We can't match exactly here since everything takes a non instant amount of time to do, + // but basically verify that the calculated interval is within expected parameters. + let advertised_interval = super::advertised_update_interval(&re); + assert_eq!(advertised_interval, super::UPDATE_INTERVAL); + + // Expired route with finite metric + let expiration = tokio::time::Instant::now() + Duration::from_secs(0); + let re = super::RouteEntry::new( + source, + neighbor.clone(), + metric, + seqno, + selected, + expiration, + ); + let advertised_interval = super::advertised_update_interval(&re); + assert_eq!(advertised_interval, super::UPDATE_INTERVAL); + + // Expired route with infinite metric + let re = super::RouteEntry::new( + source, + neighbor.clone(), + Metric::infinite(), + seqno, + selected, + expiration, + ); + let advertised_interval = super::advertised_update_interval(&re); + assert_eq!(advertised_interval, super::INTERVAL_NOT_REPEATING); + + // Check that the interval is properly capped + let expiration = tokio::time::Instant::now() + Duration::from_secs(600); + let re = super::RouteEntry::new(source, neighbor, metric, seqno, selected, expiration); + // We can't match exactly here since everything takes a non instant amount of time to do, + // but basically verify that the calculated interval is within expected parameters. + let advertised_interval = super::advertised_update_interval(&re); + assert_eq!(advertised_interval, super::UPDATE_INTERVAL); + } +} diff --git a/components/mycelium/mycelium/src/router_id.rs b/components/mycelium/mycelium/src/router_id.rs new file mode 100644 index 0000000..0002efc --- /dev/null +++ b/components/mycelium/mycelium/src/router_id.rs @@ -0,0 +1,60 @@ +use core::fmt; + +use crate::crypto::PublicKey; + +/// A `RouterId` uniquely identifies a router in the network. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct RouterId { + pk: PublicKey, + zone: [u8; 2], + rnd: [u8; 6], +} + +impl RouterId { + /// Size in bytes of a `RouterId` + pub const BYTE_SIZE: usize = 40; + + /// Create a new `RouterId` from a [`PublicKey`]. + pub fn new(pk: PublicKey) -> Self { + Self { + pk, + zone: [0; 2], + rnd: rand::random(), + } + } + + /// View this `RouterId` as a byte array. + pub fn as_bytes(&self) -> [u8; Self::BYTE_SIZE] { + let mut out = [0; Self::BYTE_SIZE]; + out[..32].copy_from_slice(self.pk.as_bytes()); + out[32..34].copy_from_slice(&self.zone); + out[34..].copy_from_slice(&self.rnd); + out + } + + /// Converts this `RouterId` to a [`PublicKey`]. + pub fn to_pubkey(self) -> PublicKey { + self.pk + } +} + +impl From<[u8; Self::BYTE_SIZE]> for RouterId { + fn from(bytes: [u8; Self::BYTE_SIZE]) -> RouterId { + RouterId { + pk: PublicKey::from(<&[u8] as TryInto<[u8; 32]>>::try_into(&bytes[..32]).unwrap()), + zone: bytes[32..34].try_into().unwrap(), + rnd: bytes[34..Self::BYTE_SIZE].try_into().unwrap(), + } + } +} + +impl fmt::Display for RouterId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let RouterId { pk, zone, rnd } = self; + f.write_fmt(format_args!( + "{pk}-{}-{}", + faster_hex::hex_string(zone), + faster_hex::hex_string(rnd) + )) + } +} diff --git a/components/mycelium/mycelium/src/routing_table.rs b/components/mycelium/mycelium/src/routing_table.rs new file mode 100644 index 0000000..b037a04 --- /dev/null +++ b/components/mycelium/mycelium/src/routing_table.rs @@ -0,0 +1,687 @@ +use std::{ + net::{IpAddr, Ipv6Addr}, + ops::Deref, + sync::{Arc, Mutex, MutexGuard}, +}; + +use ip_network_table_deps_treebitmap::IpLookupTable; +use iter::{RoutingTableNoRouteIter, RoutingTableQueryIter}; +use subnet_entry::SubnetEntry; +use tokio::{select, sync::mpsc, time::Duration}; +use tokio_util::sync::CancellationToken; +use tracing::{error, trace}; + +use crate::{crypto::SharedSecret, peer::Peer, subnet::Subnet}; + +pub use iter::RoutingTableIter; +pub use iter_mut::RoutingTableIterMut; +pub use no_route::NoRouteSubnet; +pub use queried_subnet::QueriedSubnet; +pub use route_entry::RouteEntry; +pub use route_key::RouteKey; +pub use route_list::RouteList; + +mod iter; +mod iter_mut; +mod no_route; +mod queried_subnet; +mod route_entry; +mod route_key; +mod route_list; +mod subnet_entry; + +const NO_ROUTE_EXPIRATION: Duration = Duration::from_secs(60); + +pub enum Routes { + Exist(RouteListReadGuard), + Queried, + NoRoute, + None, +} + +impl Routes { + /// Returns the selected route if one exists. + pub fn selected(&self) -> Option<&RouteEntry> { + if let Routes::Exist(routes) = self { + routes.selected() + } else { + None + } + } + + /// Returns true if there are no routes + pub fn is_none(&self) -> bool { + !matches!(self, Routes::Exist { .. }) + } +} + +impl From<&SubnetEntry> for Routes { + fn from(value: &SubnetEntry) -> Self { + match value { + SubnetEntry::Exists { list } => { + Routes::Exist(RouteListReadGuard { inner: list.load() }) + } + SubnetEntry::Queried { .. } => Routes::Queried, + SubnetEntry::NoRoute { .. } => Routes::NoRoute, + } + } +} + +impl From> for Routes { + fn from(value: Option<&SubnetEntry>) -> Self { + match value { + Some(v) => v.into(), + None => Routes::None, + } + } +} + +/// The routing table holds a list of route entries for every known subnet. +#[derive(Clone)] +pub struct RoutingTable { + writer: Arc>>, + reader: left_right::ReadHandle, + + shared: Arc, +} + +struct RoutingTableShared { + expired_route_entry_sink: mpsc::Sender, + cancel_token: CancellationToken, +} + +#[derive(Default)] +struct RoutingTableInner { + table: IpLookupTable>, +} + +/// Hold an exclusive write lock over the routing table. While this item is in scope, no other +/// calls can get a mutable refernce to the content of a routing table. Once this guard goes out of +/// scope, changes to the contained RouteList will be applied. +pub struct WriteGuard<'a> { + routing_table: &'a RoutingTable, + /// Owned copy of the RouteList, this is populated once mutable access the the RouteList has + /// been requested. + value: Arc, + /// Did the RouteList exist initially? + exists: bool, + /// The subnet we are writing to. + subnet: Subnet, + expired_route_entry_sink: mpsc::Sender, + cancellation_token: CancellationToken, +} + +impl RoutingTable { + /// Create a new empty RoutingTable. The passed channel is used to notify an external observer + /// of route entry expiration events. It is the callers responsibility to ensure these events + /// are properly handled. + /// + /// # Panics + /// + /// This will panic if not executed in the context of a tokio runtime. + pub fn new(expired_route_entry_sink: mpsc::Sender) -> Self { + let (writer, reader) = left_right::new(); + let writer = Arc::new(Mutex::new(writer)); + + let cancel_token = CancellationToken::new(); + let shared = Arc::new(RoutingTableShared { + expired_route_entry_sink, + cancel_token, + }); + + RoutingTable { + writer, + reader, + shared, + } + } + + /// Get a list of the routes for the most precises [`Subnet`] known which contains the given + /// [`IpAddr`]. + pub fn best_routes(&self, ip: IpAddr) -> Routes { + let IpAddr::V6(ip) = ip else { + panic!("Only IPv6 is supported currently"); + }; + self.reader + .enter() + .expect("Write handle is saved on the router so it is not dropped yet.") + .table + .longest_match(ip) + .map(|(_, _, rl)| rl.as_ref()) + .into() + } + + /// Get a list of all routes for the given subnet. Changes to the RoutingTable after this + /// method returns will not be visible and require this method to be called again to be + /// observed. + pub fn routes(&self, subnet: Subnet) -> Routes { + let subnet_ip = if let IpAddr::V6(ip) = subnet.address() { + ip + } else { + return Routes::None; + }; + + self.reader + .enter() + .expect("Write handle is saved on the router so it is not dropped yet.") + .table + .exact_match(subnet_ip, subnet.prefix_len().into()) + .map(Arc::as_ref) + .into() + } + + /// Gets continued read access to the `RoutingTable`. While the returned + /// [`guard`](RoutingTableReadGuard) is held, updates to the `RoutingTable` will be blocked. + pub fn read(&self) -> RoutingTableReadGuard { + RoutingTableReadGuard { + guard: self + .reader + .enter() + .expect("Write handle is saved on RoutingTable, so this is always Some; qed"), + } + } + + /// Locks the `RoutingTable` for continued write access. While the returned + /// [`guard`](RoutingTableWriteGuard) is held, methods trying to mutate the `RoutingTable`, or + /// get mutable access otherwise, will be blocked. When the [`guard`](`RoutingTableWriteGuard`) + /// is dropped, all queued changes will be applied. + pub fn write(&self) -> RoutingTableWriteGuard { + RoutingTableWriteGuard { + write_guard: self.writer.lock().unwrap(), + read_guard: self + .reader + .enter() + .expect("Write handle is saved on RoutingTable, so this is always Some; qed"), + expired_route_entry_sink: self.shared.expired_route_entry_sink.clone(), + cancel_token: self.shared.cancel_token.clone(), + } + } + + /// Get mutable access to the list of routes for the given [`Subnet`]. + pub fn routes_mut(&self, subnet: Subnet) -> Option { + let subnet_address = if let IpAddr::V6(ip) = subnet.address() { + ip + } else { + panic!("IP v4 addresses are not supported") + }; + + let value = self + .reader + .enter() + .expect("Write handle is saved next to read handle so this is always Some; qed") + .table + .exact_match(subnet_address, subnet.prefix_len().into())? + .clone(); + + if matches!(*value, SubnetEntry::Exists { .. }) { + Some(WriteGuard { + routing_table: self, + // If we didn't find a route list in the route table we create a new empty list, + // therefore we immediately own it. + value, + exists: true, + subnet, + expired_route_entry_sink: self.shared.expired_route_entry_sink.clone(), + cancellation_token: self.shared.cancel_token.clone(), + }) + } else { + None + } + } + + /// Adds a new [`Subnet`] to the `RoutingTable`. The returned [`WriteGuard`] can be used to + /// insert entries. If no entry is inserted before the guard is dropped, the [`Subnet`] won't + /// be added. + pub fn add_subnet(&self, subnet: Subnet, shared_secret: SharedSecret) -> WriteGuard { + if !matches!(subnet.address(), IpAddr::V6(_)) { + panic!("IP v4 addresses are not supported") + }; + + let value = Arc::new(SubnetEntry::Exists { + list: Arc::new(RouteList::new(shared_secret)).into(), + }); + + WriteGuard { + routing_table: self, + value, + exists: false, + subnet, + expired_route_entry_sink: self.shared.expired_route_entry_sink.clone(), + cancellation_token: self.shared.cancel_token.clone(), + } + } + + /// Gets the selected route for an IpAddr if one exists. + /// + /// # Panics + /// + /// This will panic if the IP address is not an IPV6 address. + pub fn selected_route(&self, address: IpAddr) -> Option { + let IpAddr::V6(ip) = address else { + panic!("IP v4 addresses are not supported") + }; + self.reader + .enter() + .expect("Write handle is saved on RoutingTable, so this is always Some; qed") + .table + .longest_match(ip) + .and_then(|(_, _, rl)| { + let SubnetEntry::Exists { list } = &**rl else { + return None; + }; + let rl = list.load(); + if rl.is_empty() || !rl[0].selected() { + None + } else { + Some(rl[0].clone()) + } + }) + } + + /// Marks a subnet as queried in the route table. + /// + /// This function will not do anything if the subnet contains valid routes. + pub fn mark_queried(&self, subnet: Subnet, query_timeout: tokio::time::Instant) { + if !matches!(subnet.address(), IpAddr::V6(_)) { + panic!("IP v4 addresses are not supported") + }; + + // Start a task to expire the queried state if we didn't have any results in time. + { + // We only need the write handle in the task + let writer = self.writer.clone(); + let cancel_token = self.shared.cancel_token.clone(); + tokio::task::spawn(async move { + select! { + _ = cancel_token.cancelled() => { + // Future got cancelled, nothing to do + return + } + _ = tokio::time::sleep_until(query_timeout) => { + // Timeout fired, mark as no route + } + } + + let expiry = tokio::time::Instant::now() + NO_ROUTE_EXPIRATION; + + // Scope this so the lock for the write_handle goes out of scope when we are done + // here, as we don't want to hold the write_handle lock while sleeping for the + // second timeout. + { + let mut write_handle = writer.lock().expect("Can lock writer"); + write_handle.append(RoutingTableOplogEntry::QueryExpired( + subnet, + Arc::new(SubnetEntry::NoRoute { expiry }), + )); + write_handle.flush(); + } + + // TODO: Check if we are indeed marked as NoRoute here, if we aren't this can be + // cancelled now + + select! { + _ = cancel_token.cancelled() => { + // Future got cancelled, nothing to do + return + } + _ = tokio::time::sleep_until(expiry) => { + // Timeout fired, remove no route entry + } + } + + let mut write_handle = writer.lock().expect("Can lock writer"); + write_handle.append(RoutingTableOplogEntry::NoRouteExpired(subnet)); + write_handle.flush(); + }); + } + + let mut write_handle = self.writer.lock().expect("Can lock writer"); + write_handle.append(RoutingTableOplogEntry::Queried( + subnet, + Arc::new(SubnetEntry::Queried { query_timeout }), + )); + write_handle.flush(); + } +} + +pub struct RouteListReadGuard { + inner: arc_swap::Guard>, +} + +impl Deref for RouteListReadGuard { + type Target = RouteList; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} + +/// A write guard over the [`RoutingTable`]. While this guard is held, updates won't be able to +/// complete. +pub struct RoutingTableWriteGuard<'a> { + write_guard: MutexGuard<'a, left_right::WriteHandle>, + read_guard: left_right::ReadGuard<'a, RoutingTableInner>, + expired_route_entry_sink: mpsc::Sender, + cancel_token: CancellationToken, +} + +impl<'a, 'b> RoutingTableWriteGuard<'a> { + pub fn iter_mut(&'b mut self) -> RoutingTableIterMut<'a, 'b> { + RoutingTableIterMut::new( + &mut self.write_guard, + self.read_guard.table.iter(), + self.expired_route_entry_sink.clone(), + self.cancel_token.clone(), + ) + } +} + +impl Drop for RoutingTableWriteGuard<'_> { + fn drop(&mut self) { + self.write_guard.publish(); + } +} + +/// A read guard over the [`RoutingTable`]. While this guard is held, updates won't be able to +/// complete. +pub struct RoutingTableReadGuard<'a> { + guard: left_right::ReadGuard<'a, RoutingTableInner>, +} + +impl RoutingTableReadGuard<'_> { + pub fn iter(&self) -> RoutingTableIter { + RoutingTableIter::new(self.guard.table.iter()) + } + + /// Create an iterator for all queried subnets in the routing table + pub fn iter_queries(&self) -> RoutingTableQueryIter { + RoutingTableQueryIter::new(self.guard.table.iter()) + } + + /// Create an iterator for all subnets which are currently marked as `NoRoute` in the routing + /// table. + pub fn iter_no_route(&self) -> RoutingTableNoRouteIter { + RoutingTableNoRouteIter::new(self.guard.table.iter()) + } +} + +impl WriteGuard<'_> { + /// Loads the current [`RouteList`]. + #[inline] + pub fn routes(&self) -> RouteListReadGuard { + let SubnetEntry::Exists { list } = &*self.value else { + panic!("Write guard for non-route SubnetEntry") + }; + RouteListReadGuard { inner: list.load() } + } + + /// Get mutable access to the [`RouteList`]. This will update the [`RouteList`] in place + /// without locking the [`RoutingTable`]. + // TODO: Proper abstractions + pub fn update_routes< + F: FnMut(&mut RouteList, &mpsc::Sender, &CancellationToken) -> bool, + >( + &mut self, + mut op: F, + ) -> bool { + let mut res = false; + let mut delete = false; + if let SubnetEntry::Exists { list } = &*self.value { + list.rcu(|rl| { + let mut new_val = rl.clone(); + let v = Arc::make_mut(&mut new_val); + + res = op(v, &self.expired_route_entry_sink, &self.cancellation_token); + delete = v.is_empty(); + + new_val + }); + + if delete && self.exists { + trace!(subnet = %self.subnet, "Deleting subnet which became empty after updating"); + let mut writer = self.routing_table.writer.lock().unwrap(); + + writer.append(RoutingTableOplogEntry::Delete(self.subnet)); + writer.publish(); + } + + res + } else { + false + } + } + + /// Set the [`RouteEntry`] with the given [`neighbour`](Peer) as the selected route. + pub fn set_selected(&mut self, neighbour: &Peer) { + if let SubnetEntry::Exists { list } = &*self.value { + list.rcu(|routes| { + let mut new_routes = routes.clone(); + let routes = Arc::make_mut(&mut new_routes); + let Some(pos) = routes.iter().position(|re| re.neighbour() == neighbour) else { + error!( + neighbour = neighbour.connection_identifier(), + "Failed to select route entry with given route key, no such entry" + ); + return new_routes; + }; + + // We don't need a check for an empty list here, since we found a selected route there + // _MUST_ be at least 1 entry. + // Set the first element to unselected, then select the proper element so this also works + // in case the existing route is "reselected". + routes[0].set_selected(false); + routes[pos].set_selected(true); + routes.swap(0, pos); + + new_routes + }); + } + } + + /// Unconditionally unselects the selected route, if one is present. + /// + /// In case no route is selected, this is a no-op. + pub fn unselect(&mut self) { + if let SubnetEntry::Exists { list } = &*self.value { + list.rcu(|v| { + let mut new_val = v.clone(); + let new_ref = Arc::make_mut(&mut new_val); + + if let Some(e) = new_ref.get_mut(0) { + e.set_selected(false); + } + + new_val + }); + } + } +} + +impl Drop for WriteGuard<'_> { + fn drop(&mut self) { + // FIXME: try to get rid of clones on the Arc here + if let SubnetEntry::Exists { list } = &*self.value { + let value = list.load(); + match self.exists { + // The route list did not exist, and now it is not empty, so an entry was added. We + // need to add the route list to the routing table. + false if !value.is_empty() => { + trace!(subnet = %self.subnet, "Inserting new route list for subnet"); + let mut writer = self.routing_table.writer.lock().unwrap(); + writer.append(RoutingTableOplogEntry::Upsert( + self.subnet, + Arc::clone(&self.value), + )); + writer.publish(); + } + // There was an existing route list which is now empty, so the entry for this subnet + // needs to be deleted in the routing table. + true if value.is_empty() => { + trace!(subnet = %self.subnet, "Removing route list for subnet"); + let mut writer = self.routing_table.writer.lock().unwrap(); + writer.append(RoutingTableOplogEntry::Delete(self.subnet)); + writer.publish(); + } + // Nothing to do in these cases. Either no value was inserted in a non existing + // routelist, or an existing one was updated in place. + _ => {} + } + } + } +} + +/// Operations allowed on the left_right for the routing table. +enum RoutingTableOplogEntry { + /// Insert or Update the value for the given subnet. + Upsert(Subnet, Arc), + /// Mark a subnet as queried. + Queried(Subnet, Arc), + /// Delete the entry for the given subnet. + Delete(Subnet), + /// The route request for a subnet expired, if it is still in query state mark it as not + /// existing + QueryExpired(Subnet, Arc), + /// The marker for explicitly not having a route to a subnet has expired + NoRouteExpired(Subnet), +} + +/// Convert an [`IpAddr`] into an [`Ipv6Addr`]. Panics if the contained addrss is not an IPv6 +/// address. +fn expect_ipv6(ip: IpAddr) -> Ipv6Addr { + let IpAddr::V6(ip) = ip else { + panic!("Expected ipv6 address") + }; + + ip +} + +impl left_right::Absorb for RoutingTableInner { + fn absorb_first(&mut self, operation: &mut RoutingTableOplogEntry, _other: &Self) { + match operation { + RoutingTableOplogEntry::Upsert(subnet, list) => { + self.table.insert( + expect_ipv6(subnet.address()), + subnet.prefix_len().into(), + Arc::clone(list), + ); + } + + RoutingTableOplogEntry::Queried(subnet, se) => { + // Mark a query only if we don't have a valid entry + let entry = self + .table + .exact_match(expect_ipv6(subnet.address()), subnet.prefix_len().into()) + .map(Arc::deref); + + // If we have no route, transition to query, if we have a route or existing query, + // do nothing + if matches!(entry, None | Some(SubnetEntry::NoRoute { .. })) { + self.table.insert( + expect_ipv6(subnet.address()), + subnet.prefix_len().into(), + Arc::clone(se), + ); + } + } + RoutingTableOplogEntry::Delete(subnet) => { + self.table + .remove(expect_ipv6(subnet.address()), subnet.prefix_len().into()); + } + RoutingTableOplogEntry::QueryExpired(subnet, nre) => { + if let Some(entry) = self + .table + .exact_match(expect_ipv6(subnet.address()), subnet.prefix_len().into()) + { + if let SubnetEntry::Queried { .. } = &**entry { + self.table.insert( + expect_ipv6(subnet.address()), + subnet.prefix_len().into(), + Arc::clone(nre), + ); + } + } + } + RoutingTableOplogEntry::NoRouteExpired(subnet) => { + if let Some(entry) = self + .table + .exact_match(expect_ipv6(subnet.address()), subnet.prefix_len().into()) + { + if let SubnetEntry::NoRoute { .. } = &**entry { + self.table + .remove(expect_ipv6(subnet.address()), subnet.prefix_len().into()); + } + } + } + } + } + + fn sync_with(&mut self, first: &Self) { + for (k, ss, v) in first.table.iter() { + self.table.insert(k, ss, v.clone()); + } + } + + fn absorb_second(&mut self, operation: RoutingTableOplogEntry, _: &Self) { + match operation { + RoutingTableOplogEntry::Upsert(subnet, list) => { + self.table.insert( + expect_ipv6(subnet.address()), + subnet.prefix_len().into(), + list, + ); + } + RoutingTableOplogEntry::Queried(subnet, se) => { + // Mark a query only if we don't have a valid entry + let entry = self + .table + .exact_match(expect_ipv6(subnet.address()), subnet.prefix_len().into()) + .map(Arc::deref); + + // If we have no route, transition to query, if we have a route or existing query, + // do nothing + if matches!(entry, None | Some(SubnetEntry::NoRoute { .. })) { + self.table.insert( + expect_ipv6(subnet.address()), + subnet.prefix_len().into(), + se, + ); + } + } + RoutingTableOplogEntry::Delete(subnet) => { + self.table + .remove(expect_ipv6(subnet.address()), subnet.prefix_len().into()); + } + RoutingTableOplogEntry::QueryExpired(subnet, nre) => { + if let Some(entry) = self + .table + .exact_match(expect_ipv6(subnet.address()), subnet.prefix_len().into()) + { + if let SubnetEntry::Queried { .. } = &**entry { + self.table.insert( + expect_ipv6(subnet.address()), + subnet.prefix_len().into(), + nre, + ); + } + } + } + RoutingTableOplogEntry::NoRouteExpired(subnet) => { + if let Some(entry) = self + .table + .exact_match(expect_ipv6(subnet.address()), subnet.prefix_len().into()) + { + if let SubnetEntry::NoRoute { .. } = &**entry { + self.table + .remove(expect_ipv6(subnet.address()), subnet.prefix_len().into()); + } + } + } + } + } +} + +impl Drop for RoutingTableShared { + fn drop(&mut self) { + self.cancel_token.cancel(); + } +} diff --git a/components/mycelium/mycelium/src/routing_table/iter.rs b/components/mycelium/mycelium/src/routing_table/iter.rs new file mode 100644 index 0000000..46bc223 --- /dev/null +++ b/components/mycelium/mycelium/src/routing_table/iter.rs @@ -0,0 +1,100 @@ +use std::{net::Ipv6Addr, sync::Arc}; + +use crate::subnet::Subnet; + +use super::{subnet_entry::SubnetEntry, NoRouteSubnet, QueriedSubnet, RouteListReadGuard}; + +/// An iterator over a [`routing table`](super::RoutingTable) giving read only access to +/// [`RouteList`]'s. +pub struct RoutingTableIter<'a>( + ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, +); + +impl<'a> RoutingTableIter<'a> { + /// Create a new `RoutingTableIter` which will iterate over all entries in a [`RoutingTable`]. + pub(super) fn new( + inner: ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, + ) -> Self { + Self(inner) + } +} + +impl Iterator for RoutingTableIter<'_> { + type Item = (Subnet, RouteListReadGuard); + + fn next(&mut self) -> Option { + for (ip, prefix_size, rl) in self.0.by_ref() { + if let SubnetEntry::Exists { list } = &**rl { + return Some(( + Subnet::new(ip.into(), prefix_size as u8) + .expect("Routing table contains valid subnets"), + RouteListReadGuard { inner: list.load() }, + )); + } + } + None + } +} + +/// Iterator over queried routes in the routing table. +pub struct RoutingTableQueryIter<'a>( + ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, +); + +impl<'a> RoutingTableQueryIter<'a> { + /// Create a new `RoutingTableQueryIter` which will iterate over all queried entries in a [`RoutingTable`]. + pub(super) fn new( + inner: ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, + ) -> Self { + Self(inner) + } +} + +impl Iterator for RoutingTableQueryIter<'_> { + type Item = QueriedSubnet; + + fn next(&mut self) -> Option { + for (ip, prefix_size, rl) in self.0.by_ref() { + if let SubnetEntry::Queried { query_timeout } = &**rl { + return Some(QueriedSubnet::new( + Subnet::new(ip.into(), prefix_size as u8) + .expect("Routing table contains valid subnets"), + *query_timeout, + )); + } + } + None + } +} + +/// Iterator for entries which are explicitly marked as "no route"in the routing table. +pub struct RoutingTableNoRouteIter<'a>( + ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, +); + +impl<'a> RoutingTableNoRouteIter<'a> { + /// Create a new `RoutingTableNoRouteIter` which will iterate over all entries in a [`RoutingTable`] + /// which are explicitly marked as `NoRoute` + pub(super) fn new( + inner: ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, + ) -> Self { + Self(inner) + } +} + +impl Iterator for RoutingTableNoRouteIter<'_> { + type Item = NoRouteSubnet; + + fn next(&mut self) -> Option { + for (ip, prefix_size, rl) in self.0.by_ref() { + if let SubnetEntry::NoRoute { expiry } = &**rl { + return Some(NoRouteSubnet::new( + Subnet::new(ip.into(), prefix_size as u8) + .expect("Routing table contains valid subnets"), + *expiry, + )); + } + } + None + } +} diff --git a/components/mycelium/mycelium/src/routing_table/iter_mut.rs b/components/mycelium/mycelium/src/routing_table/iter_mut.rs new file mode 100644 index 0000000..839ba3b --- /dev/null +++ b/components/mycelium/mycelium/src/routing_table/iter_mut.rs @@ -0,0 +1,107 @@ +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; +use tracing::trace; + +use crate::subnet::Subnet; + +use super::{ + subnet_entry::SubnetEntry, RouteKey, RouteList, RoutingTableInner, RoutingTableOplogEntry, +}; +use std::{ + net::Ipv6Addr, + sync::{Arc, MutexGuard}, +}; + +/// An iterator over a [`routing table`](super::RoutingTable), yielding mutable access to the +/// entries in the table. +pub struct RoutingTableIterMut<'a, 'b> { + write_guard: + &'b mut MutexGuard<'a, left_right::WriteHandle>, + iter: ip_network_table_deps_treebitmap::Iter<'b, Ipv6Addr, Arc>, + + expired_route_entry_sink: mpsc::Sender, + cancel_token: CancellationToken, +} + +impl<'a, 'b> RoutingTableIterMut<'a, 'b> { + pub(super) fn new( + write_guard: &'b mut MutexGuard< + 'a, + left_right::WriteHandle, + >, + iter: ip_network_table_deps_treebitmap::Iter<'b, Ipv6Addr, Arc>, + + expired_route_entry_sink: mpsc::Sender, + cancel_token: CancellationToken, + ) -> Self { + Self { + write_guard, + iter, + expired_route_entry_sink, + cancel_token, + } + } + + /// Get the next item in this iterator. This is not implemented as the [`Iterator`] trait, + /// since we hand out items which are lifetime bound to this struct. + pub fn next<'c>(&'c mut self) -> Option<(Subnet, RoutingTableIterMutEntry<'a, 'c>)> { + for (ip, prefix_size, rl) in self.iter.by_ref() { + if matches!(&**rl, SubnetEntry::Exists { .. }) { + let subnet = Subnet::new(ip.into(), prefix_size as u8) + .expect("Routing table contains valid subnets"); + return Some(( + subnet, + RoutingTableIterMutEntry { + writer: self.write_guard, + store: Arc::clone(rl), + subnet, + expired_route_entry_sink: self.expired_route_entry_sink.clone(), + cancellation_token: self.cancel_token.clone(), + }, + )); + }; + } + + None + } +} + +/// A smart pointer giving mutable access to a [`RouteList`]. +pub struct RoutingTableIterMutEntry<'a, 'b> { + writer: + &'b mut MutexGuard<'a, left_right::WriteHandle>, + /// Owned copy of the RouteList, this is populated once mutable access the the RouteList has + /// been requested. + store: Arc, + /// The subnet we are writing to. + subnet: Subnet, + expired_route_entry_sink: mpsc::Sender, + cancellation_token: CancellationToken, +} + +impl RoutingTableIterMutEntry<'_, '_> { + /// Updates the routes for this entry + pub fn update_routes, &CancellationToken)>( + &mut self, + mut op: F, + ) { + let mut delete = false; + if let SubnetEntry::Exists { list } = &*self.store { + list.rcu(|rl| { + let mut new_val = rl.clone(); + let v = Arc::make_mut(&mut new_val); + + op(v, &self.expired_route_entry_sink, &self.cancellation_token); + delete = v.is_empty(); + + new_val + }); + + if delete { + trace!(subnet = %self.subnet, "Queue subnet for deletion since route list is now empty"); + self.writer + .append(RoutingTableOplogEntry::Delete(self.subnet)); + } + } + } +} diff --git a/components/mycelium/mycelium/src/routing_table/no_route.rs b/components/mycelium/mycelium/src/routing_table/no_route.rs new file mode 100644 index 0000000..9a7f91c --- /dev/null +++ b/components/mycelium/mycelium/src/routing_table/no_route.rs @@ -0,0 +1,35 @@ +use tokio::time::Instant; + +use crate::subnet::Subnet; + +/// Information about a [`subnet`](Subnet) which is currently marked as NoRoute. +#[derive(Debug, Clone, Copy)] +pub struct NoRouteSubnet { + /// The subnet which has no route. + subnet: Subnet, + /// Time at which the entry expires. After this timeout expires, the entry is removed and a new + /// query can be performed. + entry_expires: Instant, +} + +impl NoRouteSubnet { + /// Create a new `NoRouteSubnet` for the given [`subnet`](Subnet), expiring at the provided + /// [`time`](Instant). + pub fn new(subnet: Subnet, entry_expires: Instant) -> Self { + Self { + subnet, + entry_expires, + } + } + + /// The [`subnet`](Subnet) for which there is no route. + pub fn subnet(&self) -> Subnet { + self.subnet + } + + /// The moment this entry expires. Once this timeout expires, a new query can be launched for + /// route discovery for this [`subnet`](Subnet). + pub fn entry_expires(&self) -> Instant { + self.entry_expires + } +} diff --git a/components/mycelium/mycelium/src/routing_table/queried_subnet.rs b/components/mycelium/mycelium/src/routing_table/queried_subnet.rs new file mode 100644 index 0000000..371b646 --- /dev/null +++ b/components/mycelium/mycelium/src/routing_table/queried_subnet.rs @@ -0,0 +1,35 @@ +use tokio::time::Instant; + +use crate::subnet::Subnet; + +/// Information about a [`subnet`](Subnet) which is currently in the queried state +#[derive(Debug, Clone, Copy)] +pub struct QueriedSubnet { + /// The subnet which was queried. + subnet: Subnet, + /// Time at which the query expires. If no feasible updates come in before this, the subnet is + /// marked as no route temporarily. + query_expires: Instant, +} + +impl QueriedSubnet { + /// Create a new `QueriedSubnet` for the given [`subnet`](Subnet), expiring at the provided + /// [`time`](Instant). + pub fn new(subnet: Subnet, query_expires: Instant) -> Self { + Self { + subnet, + query_expires, + } + } + + /// The [`subnet`](Subnet) being queried. + pub fn subnet(&self) -> Subnet { + self.subnet + } + + /// The moment this query expires. If no route is discovered before this, the [`subnet`](Subnet) + /// is marked as no route temporarily. + pub fn query_expires(&self) -> Instant { + self.query_expires + } +} diff --git a/components/mycelium/mycelium/src/routing_table/route_entry.rs b/components/mycelium/mycelium/src/routing_table/route_entry.rs new file mode 100644 index 0000000..f3f488e --- /dev/null +++ b/components/mycelium/mycelium/src/routing_table/route_entry.rs @@ -0,0 +1,120 @@ +use tokio::time::Instant; + +use crate::{ + metric::Metric, peer::Peer, router_id::RouterId, sequence_number::SeqNo, + source_table::SourceKey, +}; + +/// RouteEntry holds all relevant information about a specific route. Since this includes the next +/// hop, a single subnet can have multiple route entries. +#[derive(Clone)] +pub struct RouteEntry { + source: SourceKey, + neighbour: Peer, + metric: Metric, + seqno: SeqNo, + selected: bool, + expires: Instant, +} + +impl RouteEntry { + /// Create a new `RouteEntry` with the provided values. + pub fn new( + source: SourceKey, + neighbour: Peer, + metric: Metric, + seqno: SeqNo, + selected: bool, + expires: Instant, + ) -> Self { + Self { + source, + neighbour, + metric, + seqno, + selected, + expires, + } + } + + /// Return the [`SourceKey`] for this `RouteEntry`. + pub fn source(&self) -> SourceKey { + self.source + } + + /// Return the [`neighbour`](Peer) used as next hop for this `RouteEntry`. + pub fn neighbour(&self) -> &Peer { + &self.neighbour + } + + /// Return the [`Metric`] of this `RouteEntry`. + pub fn metric(&self) -> Metric { + self.metric + } + + /// Return the [`sequence number`](SeqNo) for the `RouteEntry`. + pub fn seqno(&self) -> SeqNo { + self.seqno + } + + /// Return if this [`RouteEntry`] is selected. + pub fn selected(&self) -> bool { + self.selected + } + + /// Return the [`Instant`] when this `RouteEntry` expires if it doesn't get updated before + /// then. + pub fn expires(&self) -> Instant { + self.expires + } + + /// Set the [`SourceKey`] for this `RouteEntry`. + pub fn set_source(&mut self, source: SourceKey) { + self.source = source; + } + + /// Set the [`RouterId`] for this `RouteEntry`. + pub fn set_router_id(&mut self, router_id: RouterId) { + self.source.set_router_id(router_id) + } + + /// Sets the [`neighbour`](Peer) for this `RouteEntry`. + pub fn set_neighbour(&mut self, neighbour: Peer) { + self.neighbour = neighbour; + } + + /// Sets the [`Metric`] for this `RouteEntry`. + pub fn set_metric(&mut self, metric: Metric) { + self.metric = metric; + } + + /// Sets the [`sequence number`](SeqNo) for this `RouteEntry`. + pub fn set_seqno(&mut self, seqno: SeqNo) { + self.seqno = seqno; + } + + /// Sets if this `RouteEntry` is the selected route for the associated + /// [`Subnet`](crate::subnet::Subnet). + pub fn set_selected(&mut self, selected: bool) { + self.selected = selected; + } + + /// Sets the expiration time for this [`RouteEntry`]. + pub(super) fn set_expires(&mut self, expires: Instant) { + self.expires = expires; + } +} + +// Manual Debug implementation since SharedSecret is explicitly not Debug +impl std::fmt::Debug for RouteEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RouteEntry") + .field("source", &self.source) + .field("neighbour", &self.neighbour) + .field("metric", &self.metric) + .field("seqno", &self.seqno) + .field("selected", &self.selected) + .field("expires", &self.expires) + .finish() + } +} diff --git a/components/mycelium/mycelium/src/routing_table/route_key.rs b/components/mycelium/mycelium/src/routing_table/route_key.rs new file mode 100644 index 0000000..7d3edea --- /dev/null +++ b/components/mycelium/mycelium/src/routing_table/route_key.rs @@ -0,0 +1,38 @@ +use crate::{peer::Peer, subnet::Subnet}; + +/// RouteKey uniquely defines a route via a peer. +#[derive(Debug, Clone, PartialEq)] +pub struct RouteKey { + subnet: Subnet, + neighbour: Peer, +} + +impl RouteKey { + /// Creates a new `RouteKey` for the given [`Subnet`] and [`neighbour`](Peer). + #[inline] + pub fn new(subnet: Subnet, neighbour: Peer) -> Self { + Self { subnet, neighbour } + } + + /// Get's the [`Subnet`] identified by this `RouteKey`. + #[inline] + pub fn subnet(&self) -> Subnet { + self.subnet + } + + /// Gets the [`neighbour`](Peer) identified by this `RouteKey`. + #[inline] + pub fn neighbour(&self) -> &Peer { + &self.neighbour + } +} + +impl std::fmt::Display for RouteKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{} via {}", + self.subnet, + self.neighbour.connection_identifier() + )) + } +} diff --git a/components/mycelium/mycelium/src/routing_table/route_list.rs b/components/mycelium/mycelium/src/routing_table/route_list.rs new file mode 100644 index 0000000..b74378c --- /dev/null +++ b/components/mycelium/mycelium/src/routing_table/route_list.rs @@ -0,0 +1,201 @@ +use std::{ + ops::{Deref, DerefMut, Index, IndexMut}, + sync::Arc, +}; + +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error}; + +use crate::{crypto::SharedSecret, peer::Peer, task::AbortHandle}; + +use super::{RouteEntry, RouteKey}; + +/// The RouteList holds all routes for a specific subnet. +// By convention, if a route is selected, it will always be at index 0 in the list. +#[derive(Clone)] +pub struct RouteList { + list: Vec<(Arc, RouteEntry)>, + shared_secret: SharedSecret, +} + +impl RouteList { + /// Create a new empty RouteList + pub(crate) fn new(shared_secret: SharedSecret) -> Self { + Self { + list: Vec::new(), + shared_secret, + } + } + + /// Returns the [`SharedSecret`] used for encryption of packets to and from the associated + /// [`Subnet`]. + #[inline] + pub fn shared_secret(&self) -> &SharedSecret { + &self.shared_secret + } + + /// Checks if there are any actual routes in the list. + #[inline] + pub fn is_empty(&self) -> bool { + self.list.is_empty() + } + + /// Returns the selected route for the [`Subnet`] this is the `RouteList` for, if one exists. + pub fn selected(&self) -> Option<&RouteEntry> { + self.list + .first() + .map(|(_, re)| re) + .and_then(|re| if re.selected() { Some(re) } else { None }) + } + + /// Returns an iterator over the `RouteList`. + /// + /// The iterator yields all [`route entries`](RouteEntry) in the list. + pub fn iter(&self) -> RouteListIter { + RouteListIter::new(self) + } + + /// Returns an iterator over the `RouteList` yielding mutable access to the elements. + /// + /// The iterator yields all [`route entries`](RouteEntry) in the list. + pub fn iter_mut(&mut self) -> impl Iterator { + self.list.iter_mut().map(|item| RouteGuard { item }) + } + + /// Removes a [`RouteEntry`] from the `RouteList`. + /// + /// This does nothing if the neighbour does not exist. + pub fn remove(&mut self, neighbour: &Peer) { + let Some(pos) = self + .list + .iter() + .position(|re| re.1.neighbour() == neighbour) + else { + return; + }; + + let old = self.list.swap_remove(pos); + old.0.abort(); + } + + /// Swaps the position of 2 `RouteEntry`s in the route list. + pub fn swap(&mut self, first: usize, second: usize) { + self.list.swap(first, second) + } + + pub fn get_mut(&mut self, index: usize) -> Option<&mut RouteEntry> { + self.list.get_mut(index).map(|(_, re)| re) + } + + /// Insert a new [`RouteEntry`] in the `RouteList`. + pub fn insert( + &mut self, + re: RouteEntry, + expired_route_entry_sink: mpsc::Sender, + cancellation_token: CancellationToken, + ) { + let expiration = re.expires(); + let rk = RouteKey::new(re.source().subnet(), re.neighbour().clone()); + let abort_handle = Arc::new( + tokio::spawn(async move { + tokio::select! { + _ = cancellation_token.cancelled() => {} + _ = tokio::time::sleep_until(expiration) => { + debug!(route_key = %rk, "Expired route entry for route key"); + if let Err(e) = expired_route_entry_sink.send(rk).await { + error!(route_key = %e.0, "Failed to send expired route key on cleanup channel"); + } + } + } + }) + .abort_handle().into(), + ); + + self.list.push((abort_handle, re)); + } +} + +pub struct RouteGuard<'a> { + item: &'a mut (Arc, RouteEntry), +} + +impl Deref for RouteGuard<'_> { + type Target = RouteEntry; + + fn deref(&self) -> &Self::Target { + &self.item.1 + } +} + +impl DerefMut for RouteGuard<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.item.1 + } +} + +impl RouteGuard<'_> { + pub fn set_expires( + &mut self, + expires: tokio::time::Instant, + expired_route_entry_sink: mpsc::Sender, + cancellation_token: CancellationToken, + ) { + let re = &mut self.item.1; + re.set_expires(expires); + let expiration = re.expires(); + let rk = RouteKey::new(re.source().subnet(), re.neighbour().clone()); + let abort_handle = Arc::new( + tokio::spawn(async move { + tokio::select! { + _ = cancellation_token.cancelled() => {} + _ = tokio::time::sleep_until(expiration) => { + debug!(route_key = %rk, "Expired route entry for route key"); + if let Err(e) = expired_route_entry_sink.send(rk).await { + error!(route_key = %e.0, "Failed to send expired route key on cleanup channel"); + } + } + } + }) + .abort_handle().into(), + ); + + self.item.0.abort(); + self.item.0 = abort_handle; + } +} + +impl Index for RouteList { + type Output = RouteEntry; + + fn index(&self, index: usize) -> &Self::Output { + &self.list[index].1 + } +} + +impl IndexMut for RouteList { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.list[index].1 + } +} + +pub struct RouteListIter<'a> { + route_list: &'a RouteList, + idx: usize, +} + +impl<'a> RouteListIter<'a> { + /// Create a new `RouteListIter` which will iterate over the given [`RouteList`]. + fn new(route_list: &'a RouteList) -> Self { + Self { route_list, idx: 0 } + } +} + +impl<'a> Iterator for RouteListIter<'a> { + type Item = &'a RouteEntry; + + fn next(&mut self) -> Option { + self.idx += 1; + self.route_list.list.get(self.idx - 1).map(|(_, re)| re) + } +} diff --git a/components/mycelium/mycelium/src/routing_table/subnet_entry.rs b/components/mycelium/mycelium/src/routing_table/subnet_entry.rs new file mode 100644 index 0000000..ee18646 --- /dev/null +++ b/components/mycelium/mycelium/src/routing_table/subnet_entry.rs @@ -0,0 +1,16 @@ +use arc_swap::ArcSwap; + +use super::RouteList; + +/// An entry for a [Subnet](crate::subnet::Subnet) in the routing table. +#[allow(dead_code)] +pub enum SubnetEntry { + /// Routes for the given subnet exist + Exists { list: ArcSwap }, + /// Routes are being queried from peers for the given subnet, but we haven't gotten a response + /// yet + Queried { query_timeout: tokio::time::Instant }, + /// We queried our peers for the subnet, but we didn't get a valid response in time, so there + /// is for sure no route to the subnet. + NoRoute { expiry: tokio::time::Instant }, +} diff --git a/components/mycelium/mycelium/src/rr_cache.rs b/components/mycelium/mycelium/src/rr_cache.rs new file mode 100644 index 0000000..ec9f9a4 --- /dev/null +++ b/components/mycelium/mycelium/src/rr_cache.rs @@ -0,0 +1,103 @@ +//! This module contains a cache implementation for route requests + +use std::{ + net::{IpAddr, Ipv6Addr}, + sync::Arc, +}; + +use dashmap::DashMap; +use tokio::time::{Duration, Instant}; +use tracing::trace; + +use crate::{babel::RouteRequest, peer::Peer, subnet::Subnet, task::AbortHandle}; + +/// Clean the route request cache every 5 seconds +const CACHE_CLEANING_INTERVAL: Duration = Duration::from_secs(5); + +/// IP used for the [`Subnet`] in the cache in case there is no prefix specified. +const GLOBAL_SUBNET_IP: IpAddr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)); + +/// Prefix size to use for the [`Subnet`] in case there is no prefix specified. +const GLOBAL_SUBNET_PREFIX_SIZE: u8 = 0; + +/// A self cleaning cache for route requests. +#[derive(Clone)] +pub struct RouteRequestCache { + /// The actual cache, mapping an instance of a route request to the peers which we've sent this + /// to. + cache: Arc>, + + _cleanup_task: Arc, +} + +struct RouteRequestInfo { + /// The lowest generation we've forwarded. + generation: u8, + /// Peers which we've sent this route request to already. + receivers: Vec, + /// The moment we've sent this route request + sent: Instant, +} + +impl RouteRequestCache { + /// Create a new cache which cleans entries which are older than the given expiration. + /// + /// The cache cleaning is done periodically, so entries might live slightly longer than the + /// allowed expiration. + pub fn new(expiration: Duration) -> Self { + let cache = Arc::new(DashMap::with_hasher(ahash::RandomState::new())); + + let _cleanup_task = Arc::new( + tokio::spawn({ + let cache = cache.clone(); + async move { + loop { + tokio::time::sleep(CACHE_CLEANING_INTERVAL).await; + + trace!("Cleaning route request cache"); + + cache.retain(|subnet, info: &mut RouteRequestInfo| { + if info.sent.elapsed() < expiration { + false + } else { + trace!(%subnet, "Removing exired route request from cache"); + true + } + }); + } + } + }) + .abort_handle() + .into(), + ); + + Self { + cache, + _cleanup_task, + } + } + + /// Record a route request which has been sent to peers. + pub fn sent_route_request(&self, rr: RouteRequest, receivers: Vec) { + let subnet = rr.prefix().unwrap_or( + Subnet::new(GLOBAL_SUBNET_IP, GLOBAL_SUBNET_PREFIX_SIZE) + .expect("Static global IPv6 subnet is valid; qed"), + ); + let generation = rr.generation(); + + let rri = RouteRequestInfo { + generation, + receivers, + sent: Instant::now(), + }; + + self.cache.insert(subnet, rri); + } + + /// Get cached info about a route request for a subnet, if it exists. + pub fn info(&self, subnet: Subnet) -> Option<(u8, Vec)> { + self.cache + .get(&subnet) + .map(|rri| (rri.generation, rri.receivers.clone())) + } +} diff --git a/components/mycelium/mycelium/src/seqno_cache.rs b/components/mycelium/mycelium/src/seqno_cache.rs new file mode 100644 index 0000000..379a00c --- /dev/null +++ b/components/mycelium/mycelium/src/seqno_cache.rs @@ -0,0 +1,176 @@ +//! The seqno request cache keeps track of seqno requests sent by the node. This allows us to drop +//! duplicate requests, and to notify the source of requests (if it wasn't the local node) about +//! relevant updates. + +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; + +use dashmap::DashMap; +use tokio::time::MissedTickBehavior; +use tracing::{debug, trace}; + +use crate::{peer::Peer, router_id::RouterId, sequence_number::SeqNo, subnet::Subnet}; + +/// The amount of time to remember a seqno request (since it was first seen), before we remove it +/// (assuming it was not removed manually before that). +const SEQNO_DEDUP_TTL: Duration = Duration::from_secs(60); + +/// A sequence number request, either forwarded or originated by the local node. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct SeqnoRequestCacheKey { + pub router_id: RouterId, + pub subnet: Subnet, + pub seqno: SeqNo, +} + +impl std::fmt::Display for SeqnoRequestCacheKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "seqno {} for {} from {}", + self.seqno, self.subnet, self.router_id + ) + } +} + +/// Information retained for sequence number requests we've sent. +struct SeqnoForwardInfo { + /// Which peers have asked us to forward this seqno request. + sources: Vec, + /// Which peers have we sent this request to. + targets: Vec, + /// Time at which we first forwarded the requets. + first_sent: Instant, + /// When did we last sent a seqno request. + last_sent: Instant, +} + +/// A cache for outbound seqno requests. Entries in the cache are automatically removed after a +/// certain amount of time. The cache does not account for the source table. That is, if the +/// requested seqno is smaller, it might pass the cache, but should have been blocked earlier by +/// the source table check. As such, this cache should be the last step in deciding if a seqno +/// request is forwarded. +#[derive(Clone)] +pub struct SeqnoCache { + /// Actual cache wrapped in an Arc to make it sharaeble. + cache: Arc>, +} + +impl SeqnoCache { + /// Create a new [`SeqnoCache`]. + pub fn new() -> Self { + trace!(capacity = 0, "Creating new seqno cache"); + + let cache = Arc::new(DashMap::with_hasher_and_shard_amount( + ahash::RandomState::new(), + // This number has been chosen completely at random + 1024, + )); + let sc = Self { cache }; + + // Spawn background cleanup task. + tokio::spawn(sc.clone().sweep_entries()); + + sc + } + + /// Record a forwarded seqno request to a given target. Also keep track of the origin of the + /// request. If the local node generated the request, source must be [`None`] + pub fn forward(&self, request: SeqnoRequestCacheKey, target: Peer, source: Option) { + let mut info = self.cache.entry(request).or_default(); + info.last_sent = Instant::now(); + if !info.targets.contains(&target) { + info.targets.push(target); + } else { + debug!( + seqno_request = %request, + "Already sent seqno request to target {}", + target.connection_identifier() + ); + } + if let Some(source) = source { + if !info.sources.contains(&source) { + info.sources.push(source); + } else { + debug!(seqno_request = %request, "Peer {} is requesting the same seqno again", source.connection_identifier()); + } + } + } + + /// Get a list of all peers which we've already sent the given seqno request to, as well as + /// when we've last sent a request. + pub fn info(&self, request: &SeqnoRequestCacheKey) -> Option<(Instant, Vec)> { + self.cache + .get(request) + .map(|info| (info.last_sent, info.targets.clone())) + } + + /// Removes forwarding info from the seqno cache. If forwarding info is available, the source + /// peers (peers which requested us to forward this request) are returned. + // TODO: cleanup if needed + #[allow(dead_code)] + pub fn remove(&self, request: &SeqnoRequestCacheKey) -> Option> { + self.cache.remove(request).map(|(_, info)| info.sources) + } + + /// Get forwarding info from the seqno cache. If forwarding info is available, the source + /// peers (peers which requested us to forward this request) are returned. + // TODO: cleanup if needed + #[allow(dead_code)] + pub fn get(&self, request: &SeqnoRequestCacheKey) -> Option> { + self.cache.get(request).map(|info| info.sources.clone()) + } + + /// Periodic task to clear old entries for which no reply came in. + async fn sweep_entries(self) { + let mut interval = tokio::time::interval(SEQNO_DEDUP_TTL); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + + loop { + interval.tick().await; + + debug!("Cleaning up expired seqno requests from seqno cache"); + + let prev_entries = self.cache.len(); + let prev_cap = self.cache.capacity(); + self.cache + .retain(|_, info| info.first_sent.elapsed() <= SEQNO_DEDUP_TTL); + self.cache.shrink_to_fit(); + + debug!( + cleaned_entries = prev_entries - self.cache.len(), + removed_capacity = prev_cap - self.cache.capacity(), + "Cleaned up stale seqno request cache entries" + ); + } + } +} + +impl Default for SeqnoCache { + fn default() -> Self { + Self::new() + } +} + +impl Default for SeqnoForwardInfo { + fn default() -> Self { + Self { + sources: vec![], + targets: vec![], + first_sent: Instant::now(), + last_sent: Instant::now(), + } + } +} + +impl std::fmt::Debug for SeqnoRequestCacheKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SeqnoRequestCacheKey") + .field("router_id", &self.router_id.to_string()) + .field("subnet", &self.subnet.to_string()) + .field("seqno", &self.seqno.to_string()) + .finish() + } +} diff --git a/components/mycelium/mycelium/src/sequence_number.rs b/components/mycelium/mycelium/src/sequence_number.rs new file mode 100644 index 0000000..633e42f --- /dev/null +++ b/components/mycelium/mycelium/src/sequence_number.rs @@ -0,0 +1,153 @@ +//! Dedicated logic for +//! [sequence numbers](https://datatracker.ietf.org/doc/html/rfc8966#name-solving-starvation-sequenci). + +use core::fmt; +use core::ops::{Add, AddAssign}; + +/// This value is compared against when deciding if a `SeqNo` is larger or smaller, [as defined in +/// the babel rfc](https://datatracker.ietf.org/doc/html/rfc8966#section-3.2.1). +const SEQNO_COMPARE_TRESHOLD: u16 = 32_768; + +/// A sequence number on a route. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SeqNo(u16); + +impl SeqNo { + /// Create a new `SeqNo` with the default value. + pub fn new() -> Self { + Self::default() + } + + /// Custom PartialOrd implementation as defined in [the babel rfc](https://datatracker.ietf.org/doc/html/rfc8966#section-3.2.1). + /// Note that we don't implement the [`PartialOrd`](std::cmd::PartialOrd) trait, as the contract on + /// that trait specifically defines that it is transitive, which is clearly not the case here. + /// + /// There is a quirk in this equality comparison where values which are exactly 32_768 apart, + /// will result in false in either way of ordering the arguments, which is counterintuitive to + /// our understanding that a < b generally implies !(b < a). + pub fn lt(&self, other: &Self) -> bool { + if self.0 == other.0 { + false + } else { + other.0.wrapping_sub(self.0) < SEQNO_COMPARE_TRESHOLD + } + } + + /// Custom PartialOrd implementation as defined in [the babel rfc](https://datatracker.ietf.org/doc/html/rfc8966#section-3.2.1). + /// Note that we don't implement the [`PartialOrd`](std::cmd::PartialOrd) trait, as the contract on + /// that trait specifically defines that it is transitive, which is clearly not the case here. + /// + /// There is a quirk in this equality comparison where values which are exactly 32_768 apart, + /// will result in false in either way of ordering the arguments, which is counterintuitive to + /// our understanding that a < b generally implies !(b < a). + pub fn gt(&self, other: &Self) -> bool { + if self.0 == other.0 { + false + } else { + other.0.wrapping_sub(self.0) > SEQNO_COMPARE_TRESHOLD + } + } +} + +impl fmt::Display for SeqNo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("{}", self.0)) + } +} + +impl From for SeqNo { + fn from(value: u16) -> Self { + SeqNo(value) + } +} + +impl From for u16 { + fn from(value: SeqNo) -> Self { + value.0 + } +} + +impl Add for SeqNo { + type Output = Self; + + fn add(self, rhs: u16) -> Self::Output { + SeqNo(self.0.wrapping_add(rhs)) + } +} + +impl AddAssign for SeqNo { + fn add_assign(&mut self, rhs: u16) { + *self = SeqNo(self.0.wrapping_add(rhs)) + } +} + +#[cfg(test)] +mod tests { + use super::SeqNo; + + #[test] + fn cmp_eq_seqno() { + let s1 = SeqNo::from(1); + let s2 = SeqNo::from(1); + assert_eq!(s1, s2); + + let s1 = SeqNo::from(10_000); + let s2 = SeqNo::from(10_000); + assert_eq!(s1, s2); + } + + #[test] + fn cmp_small_seqno_increase() { + let s1 = SeqNo::from(1); + let s2 = SeqNo::from(2); + assert!(s1.lt(&s2)); + assert!(!s2.lt(&s1)); + + assert!(s2.gt(&s1)); + assert!(!s1.gt(&s2)); + + let s1 = SeqNo::from(3); + let s2 = SeqNo::from(30_000); + assert!(s1.lt(&s2)); + assert!(!s2.lt(&s1)); + + assert!(s2.gt(&s1)); + assert!(!s1.gt(&s2)); + } + + #[test] + fn cmp_big_seqno_increase() { + let s1 = SeqNo::from(0); + let s2 = SeqNo::from(32_767); + assert!(s1.lt(&s2)); + assert!(!s2.lt(&s1)); + + assert!(s2.gt(&s1)); + assert!(!s1.gt(&s2)); + + // Test equality quirk at cutoff point. + let s1 = SeqNo::from(0); + let s2 = SeqNo::from(32_768); + assert!(!s1.lt(&s2)); + assert!(!s2.lt(&s1)); + + assert!(!s2.gt(&s1)); + assert!(!s1.gt(&s2)); + + let s1 = SeqNo::from(0); + let s2 = SeqNo::from(32_769); + assert!(!s1.lt(&s2)); + assert!(s2.lt(&s1)); + + assert!(!s2.gt(&s1)); + assert!(s1.gt(&s2)); + + let s1 = SeqNo::from(6); + let s2 = SeqNo::from(60_000); + assert!(!s1.lt(&s2)); + assert!(s2.lt(&s1)); + + assert!(!s2.gt(&s1)); + assert!(s1.gt(&s2)); + } +} diff --git a/components/mycelium/mycelium/src/source_table.rs b/components/mycelium/mycelium/src/source_table.rs new file mode 100644 index 0000000..2839065 --- /dev/null +++ b/components/mycelium/mycelium/src/source_table.rs @@ -0,0 +1,489 @@ +use core::fmt; +use std::{collections::HashMap, time::Duration}; + +use tokio::{sync::mpsc, task::JoinHandle}; +use tracing::error; + +use crate::{ + babel, metric::Metric, router_id::RouterId, routing_table::RouteEntry, sequence_number::SeqNo, + subnet::Subnet, +}; + +/// Duration after which a source entry is deleted if it is not updated. +const SOURCE_HOLD_DURATION: Duration = Duration::from_secs(60 * 30); + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)] +pub struct SourceKey { + subnet: Subnet, + router_id: RouterId, +} + +#[derive(Debug, Clone, Copy)] +pub struct FeasibilityDistance { + metric: Metric, + seqno: SeqNo, +} + +#[derive(Debug)] +pub struct SourceTable { + table: HashMap, FeasibilityDistance)>, +} + +impl FeasibilityDistance { + pub fn new(metric: Metric, seqno: SeqNo) -> Self { + FeasibilityDistance { metric, seqno } + } + + /// Returns the metric for this `FeasibilityDistance`. + pub const fn metric(&self) -> Metric { + self.metric + } + + /// Returns the sequence number for this `FeasibilityDistance`. + pub const fn seqno(&self) -> SeqNo { + self.seqno + } +} + +impl SourceKey { + /// Create a new `SourceKey`. + pub const fn new(subnet: Subnet, router_id: RouterId) -> Self { + Self { subnet, router_id } + } + + /// Returns the [`RouterId`] for this `SourceKey`. + pub const fn router_id(&self) -> RouterId { + self.router_id + } + + /// Returns the [`Subnet`] for this `SourceKey`. + pub const fn subnet(&self) -> Subnet { + self.subnet + } + + /// Updates the [`RouterId`] of this `SourceKey` + pub fn set_router_id(&mut self, router_id: RouterId) { + self.router_id = router_id + } +} + +impl SourceTable { + pub fn new() -> Self { + Self { + table: HashMap::new(), + } + } + + pub fn insert( + &mut self, + key: SourceKey, + feas_dist: FeasibilityDistance, + sink: mpsc::Sender, + ) { + let expiration_handle = tokio::spawn(async move { + tokio::time::sleep(SOURCE_HOLD_DURATION).await; + + if let Err(e) = sink.send(key).await { + error!("Failed to notify router of expired source key {e}"); + } + }); + // Abort the old task if present. + if let Some((old_timeout, _)) = self.table.insert(key, (expiration_handle, feas_dist)) { + old_timeout.abort(); + } + } + + /// Remove an entry from the source table. + pub fn remove(&mut self, key: &SourceKey) { + if let Some((old_timeout, _)) = self.table.remove(key) { + old_timeout.abort(); + }; + } + + /// Resets the garbage collection timer for a given source key. + /// + /// Does nothing if the source key is not present. + pub fn reset_timer(&mut self, key: SourceKey, sink: mpsc::Sender) { + self.table + .entry(key) + .and_modify(|(old_expiration_handle, _)| { + // First cancel the existing task + old_expiration_handle.abort(); + // Then set the new one + *old_expiration_handle = tokio::spawn(async move { + tokio::time::sleep(SOURCE_HOLD_DURATION).await; + + if let Err(e) = sink.send(key).await { + error!("Failed to notify router of expired source key {e}"); + } + }); + }); + } + + /// Get the [`FeasibilityDistance`] currently associated with the [`SourceKey`]. + pub fn get(&self, key: &SourceKey) -> Option<&FeasibilityDistance> { + self.table.get(key).map(|(_, v)| v) + } + + /// Indicates if an update is feasible in the context of the current `SoureTable`. + pub fn is_update_feasible(&self, update: &babel::Update) -> bool { + // Before an update is accepted it should be checked against the feasbility condition + // If an entry in the source table with the same source key exists, we perform the feasbility check + // If no entry exists yet, the update is accepted as there is no better alternative available (yet) + let source_key = SourceKey::new(update.subnet(), update.router_id()); + match self.get(&source_key) { + Some(entry) => { + (update.seqno().gt(&entry.seqno())) + || (update.seqno() == entry.seqno() && update.metric() < entry.metric()) + || update.metric().is_infinite() + } + None => true, + } + } + + /// Indicates if a [`RouteEntry`] is feasible according to the `SourceTable`. + pub fn route_feasible(&self, route: &RouteEntry) -> bool { + match self.get(&route.source()) { + Some(fd) => { + (route.seqno().gt(&fd.seqno)) + || (route.seqno() == fd.seqno && route.metric() < fd.metric) + || route.metric().is_infinite() + } + None => true, + } + } +} + +impl fmt::Display for SourceKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!( + "{} advertised by {}", + self.subnet, self.router_id + )) + } +} + +#[cfg(test)] +mod tests { + use tokio::sync::mpsc; + + use crate::{ + babel, + crypto::SecretKey, + metric::Metric, + peer::Peer, + router_id::RouterId, + routing_table::RouteEntry, + sequence_number::SeqNo, + source_table::{FeasibilityDistance, SourceKey, SourceTable}, + subnet::Subnet, + }; + use std::{ + net::Ipv6Addr, + sync::{atomic::AtomicU64, Arc}, + time::Duration, + }; + + /// A retraction is always considered to be feasible. + #[tokio::test] + async fn retraction_update_is_feasible() { + let (sink, _) = tokio::sync::mpsc::channel(1); + let sk = SecretKey::new(); + let pk = (&sk).into(); + let sn = Subnet::new(Ipv6Addr::new(0x400, 0, 0, 0, 0, 0, 0, 1).into(), 64) + .expect("Valid subnet in test case"); + let rid = RouterId::new(pk); + + let mut st = SourceTable::new(); + st.insert( + SourceKey::new(sn, rid), + FeasibilityDistance::new(Metric::new(10), SeqNo::from(1)), + sink, + ); + + let update = babel::Update::new( + Duration::from_secs(60), + SeqNo::from(0), + Metric::infinite(), + sn, + rid, + ); + + assert!(st.is_update_feasible(&update)); + } + + /// An update with a smaller metric but with the same seqno is feasible. + #[tokio::test] + async fn smaller_metric_update_is_feasible() { + let (sink, _) = tokio::sync::mpsc::channel(1); + let sk = SecretKey::new(); + let pk = (&sk).into(); + let sn = Subnet::new(Ipv6Addr::new(0x400, 0, 0, 0, 0, 0, 0, 1).into(), 64) + .expect("Valid subnet in test case"); + let rid = RouterId::new(pk); + + let mut st = SourceTable::new(); + st.insert( + SourceKey::new(sn, rid), + FeasibilityDistance::new(Metric::new(10), SeqNo::from(1)), + sink, + ); + + let update = babel::Update::new( + Duration::from_secs(60), + SeqNo::from(1), + Metric::from(9), + sn, + rid, + ); + + assert!(st.is_update_feasible(&update)); + } + + /// An update with the same metric and seqno is not feasible. + #[tokio::test] + async fn equal_metric_update_is_unfeasible() { + let (sink, _) = tokio::sync::mpsc::channel(1); + let sk = SecretKey::new(); + let pk = (&sk).into(); + let sn = Subnet::new(Ipv6Addr::new(0x400, 0, 0, 0, 0, 0, 0, 1).into(), 64) + .expect("Valid subnet in test case"); + let rid = RouterId::new(pk); + + let mut st = SourceTable::new(); + st.insert( + SourceKey::new(sn, rid), + FeasibilityDistance::new(Metric::new(10), SeqNo::from(1)), + sink, + ); + + let update = babel::Update::new( + Duration::from_secs(60), + SeqNo::from(1), + Metric::from(10), + sn, + rid, + ); + + assert!(!st.is_update_feasible(&update)); + } + + /// An update with a larger metric and the same seqno is not feasible. + #[tokio::test] + async fn larger_metric_update_is_unfeasible() { + let (sink, _) = tokio::sync::mpsc::channel(1); + let sk = SecretKey::new(); + let pk = (&sk).into(); + let sn = Subnet::new(Ipv6Addr::new(0x400, 0, 0, 0, 0, 0, 0, 1).into(), 64) + .expect("Valid subnet in test case"); + let rid = RouterId::new(pk); + + let mut st = SourceTable::new(); + st.insert( + SourceKey::new(sn, rid), + FeasibilityDistance::new(Metric::new(10), SeqNo::from(1)), + sink, + ); + + let update = babel::Update::new( + Duration::from_secs(60), + SeqNo::from(1), + Metric::from(11), + sn, + rid, + ); + + assert!(!st.is_update_feasible(&update)); + } + + /// An update with a lower seqno is not feasible. + #[tokio::test] + async fn lower_seqno_update_is_unfeasible() { + let (sink, _) = tokio::sync::mpsc::channel(1); + let sk = SecretKey::new(); + let pk = (&sk).into(); + let sn = Subnet::new(Ipv6Addr::new(0x400, 0, 0, 0, 0, 0, 0, 1).into(), 64) + .expect("Valid subnet in test case"); + let rid = RouterId::new(pk); + + let mut st = SourceTable::new(); + st.insert( + SourceKey::new(sn, rid), + FeasibilityDistance::new(Metric::new(10), SeqNo::from(1)), + sink, + ); + + let update = babel::Update::new( + Duration::from_secs(60), + SeqNo::from(0), + Metric::from(1), + sn, + rid, + ); + + assert!(!st.is_update_feasible(&update)); + } + + /// An update with a higher seqno is feasible. + #[tokio::test] + async fn higher_seqno_update_is_feasible() { + let (sink, _) = tokio::sync::mpsc::channel(1); + let sk = SecretKey::new(); + let pk = (&sk).into(); + let sn = Subnet::new(Ipv6Addr::new(0x400, 0, 0, 0, 0, 0, 0, 1).into(), 64) + .expect("Valid subnet in test case"); + let rid = RouterId::new(pk); + + let mut st = SourceTable::new(); + st.insert( + SourceKey::new(sn, rid), + FeasibilityDistance::new(Metric::new(10), SeqNo::from(1)), + sink, + ); + + let update = babel::Update::new( + Duration::from_secs(60), + SeqNo::from(2), + Metric::from(200), + sn, + rid, + ); + + assert!(st.is_update_feasible(&update)); + } + + /// A route with a smaller metric but with the same seqno is feasible. + #[tokio::test] + async fn smaller_metric_route_is_feasible() { + let (sink, _) = tokio::sync::mpsc::channel(1); + let sk = SecretKey::new(); + let pk = (&sk).into(); + let sn = Subnet::new(Ipv6Addr::new(0x400, 0, 0, 0, 0, 0, 0, 1).into(), 64) + .expect("Valid subnet in test case"); + let rid = RouterId::new(pk); + + let source_key = SourceKey::new(sn, rid); + + let mut st = SourceTable::new(); + st.insert( + source_key, + FeasibilityDistance::new(Metric::new(10), SeqNo::from(1)), + sink, + ); + + let (router_data_tx, _router_data_rx) = mpsc::channel(1); + let (router_control_tx, _router_control_rx) = mpsc::unbounded_channel(); + let (dead_peer_sink, _dead_peer_stream) = mpsc::channel(1); + let (con1, _con2) = tokio::io::duplex(1500); + let neighbor = Peer::new( + router_data_tx, + router_control_tx, + con1, + dead_peer_sink, + Arc::new(AtomicU64::new(0)), + Arc::new(AtomicU64::new(0)), + ) + .expect("Can create a dummy peer"); + + let re = RouteEntry::new( + source_key, + neighbor, + Metric::new(9), + SeqNo::from(1), + true, + tokio::time::Instant::now() + Duration::from_secs(60), + ); + + assert!(st.route_feasible(&re)); + } + + /// If a route has the same metric as the source table it is not feasible. + #[tokio::test] + async fn equal_metric_route_is_unfeasible() { + let (sink, _) = tokio::sync::mpsc::channel(1); + let sk = SecretKey::new(); + let pk = (&sk).into(); + let sn = Subnet::new(Ipv6Addr::new(0x400, 0, 0, 0, 0, 0, 0, 1).into(), 64) + .expect("Valid subnet in test case"); + let rid = RouterId::new(pk); + + let source_key = SourceKey::new(sn, rid); + + let mut st = SourceTable::new(); + st.insert( + source_key, + FeasibilityDistance::new(Metric::new(10), SeqNo::from(1)), + sink, + ); + + let (router_data_tx, _router_data_rx) = mpsc::channel(1); + let (router_control_tx, _router_control_rx) = mpsc::unbounded_channel(); + let (dead_peer_sink, _dead_peer_stream) = mpsc::channel(1); + let (con1, _con2) = tokio::io::duplex(1500); + let neighbor = Peer::new( + router_data_tx, + router_control_tx, + con1, + dead_peer_sink, + Arc::new(AtomicU64::new(0)), + Arc::new(AtomicU64::new(0)), + ) + .expect("Can create a dummy peer"); + + let re = RouteEntry::new( + source_key, + neighbor, + Metric::new(10), + SeqNo::from(1), + true, + tokio::time::Instant::now() + Duration::from_secs(60), + ); + + assert!(!st.route_feasible(&re)); + } + + /// If a route has a higher metric as the source table it is not feasible. + #[tokio::test] + async fn higher_metric_route_is_unfeasible() { + let (sink, _) = tokio::sync::mpsc::channel(1); + let sk = SecretKey::new(); + let pk = (&sk).into(); + let sn = Subnet::new(Ipv6Addr::new(0x400, 0, 0, 0, 0, 0, 0, 1).into(), 64) + .expect("Valid subnet in test case"); + let rid = RouterId::new(pk); + + let source_key = SourceKey::new(sn, rid); + + let mut st = SourceTable::new(); + st.insert( + source_key, + FeasibilityDistance::new(Metric::new(10), SeqNo::from(1)), + sink, + ); + + let (router_data_tx, _router_data_rx) = mpsc::channel(1); + let (router_control_tx, _router_control_rx) = mpsc::unbounded_channel(); + let (dead_peer_sink, _dead_peer_stream) = mpsc::channel(1); + let (con1, _con2) = tokio::io::duplex(1500); + let neighbor = Peer::new( + router_data_tx, + router_control_tx, + con1, + dead_peer_sink, + Arc::new(AtomicU64::new(0)), + Arc::new(AtomicU64::new(0)), + ) + .expect("Can create a dummy peer"); + + let re = RouteEntry::new( + source_key, + neighbor, + Metric::new(11), + SeqNo::from(1), + true, + tokio::time::Instant::now() + Duration::from_secs(60), + ); + + assert!(!st.route_feasible(&re)); + } +} diff --git a/components/mycelium/mycelium/src/subnet.rs b/components/mycelium/mycelium/src/subnet.rs new file mode 100644 index 0000000..7c429f1 --- /dev/null +++ b/components/mycelium/mycelium/src/subnet.rs @@ -0,0 +1,277 @@ +//! A dedicated subnet module. +//! +//! The standard library only exposes [`IpAddr`], and types related to +//! specific IPv4 and IPv6 addresses. It does not however, expose dedicated types to represent +//! appropriate subnets. +//! +//! This code is not meant to fully support subnets, but rather only the subset as needed by the +//! main application code. As such, this implementation is optimized for the specific use case, and +//! might not be optimal for other uses. + +use core::fmt; +use std::{ + hash::Hash, + net::{IpAddr, Ipv6Addr}, + str::FromStr, +}; + +use ipnet::IpNet; + +/// Representation of a subnet. A subnet can be either IPv4 or IPv6. +#[derive(Debug, Clone, Copy, Eq, PartialOrd, Ord)] +pub struct Subnet { + inner: IpNet, +} + +/// An error returned when creating a new [`Subnet`] with an invalid prefix length. +/// +/// For IPv4, the max prefix length is 32, and for IPv6 it is 128; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PrefixLenError; + +impl Subnet { + /// Create a new `Subnet` from the given [`IpAddr`] and prefix length. + pub fn new(addr: IpAddr, prefix_len: u8) -> Result { + Ok(Self { + inner: IpNet::new(addr, prefix_len).map_err(|_| PrefixLenError)?, + }) + } + + /// Returns the size of the prefix in bits. + pub fn prefix_len(&self) -> u8 { + self.inner.prefix_len() + } + + /// Retuns the address in this subnet. + /// + /// The returned address is a full IP address, used to construct this `Subnet`. + /// + /// # Examples + /// + /// ``` + /// use mycelium::subnet::Subnet; + /// use std::net::Ipv6Addr; + /// + /// let address = Ipv6Addr::new(12,34,56,78,90,0xab,0xcd,0xef).into(); + /// let subnet = Subnet::new(address, 64).unwrap(); + /// + /// assert_eq!(subnet.address(), address); + /// ``` + pub fn address(&self) -> IpAddr { + self.inner.addr() + } + + /// Checks if this `Subnet` contains the provided `Subnet`, i.e. all addresses of the provided + /// `Subnet` are also part of this `Subnet` + /// + /// # Examples + /// + /// ``` + /// use mycelium::subnet::Subnet; + /// use std::net::Ipv4Addr; + /// + /// let global = Subnet::new(Ipv4Addr::new(0,0,0,0).into(), 0).expect("Defined a valid subnet"); + /// let local = Subnet::new(Ipv4Addr::new(10,0,0,0).into(), 8).expect("Defined a valid subnet"); + /// + /// assert!(global.contains_subnet(&local)); + /// assert!(!local.contains_subnet(&global)); + /// ``` + pub fn contains_subnet(&self, other: &Self) -> bool { + self.inner.contains(&other.inner) + } + + /// Checks if this `Subnet` contains the provided [`IpAddr`]. + /// + /// # Examples + /// + /// ``` + /// use mycelium::subnet::Subnet; + /// use std::net::{Ipv4Addr,Ipv6Addr}; + /// + /// let ip_1 = Ipv6Addr::new(12,34,56,78,90,0xab,0xcd,0xef).into(); + /// let ip_2 = Ipv6Addr::new(90,0xab,0xcd,0xef,12,34,56,78).into(); + /// let ip_3 = Ipv4Addr::new(10,1,2,3).into(); + /// let subnet = Subnet::new(Ipv6Addr::new(12,34,5,6,7,8,9,0).into(), 32).unwrap(); + /// + /// assert!(subnet.contains_ip(ip_1)); + /// assert!(!subnet.contains_ip(ip_2)); + /// assert!(!subnet.contains_ip(ip_3)); + /// ``` + pub fn contains_ip(&self, ip: IpAddr) -> bool { + self.inner.contains(&ip) + } + + /// Returns the network part of the `Subnet`. All non prefix bits are set to 0. + /// + /// # Examples + /// + /// ``` + /// use mycelium::subnet::Subnet; + /// use std::net::{IpAddr, Ipv4Addr,Ipv6Addr}; + /// + /// let subnet_1 = Subnet::new(Ipv6Addr::new(12,34,56,78,90,0xab,0xcd,0xef).into(), + /// 32).unwrap(); + /// let subnet_2 = Subnet::new(Ipv4Addr::new(10,1,2,3).into(), 8).unwrap(); + /// + /// assert_eq!(subnet_1.network(), IpAddr::V6(Ipv6Addr::new(12,34,0,0,0,0,0,0))); + /// assert_eq!(subnet_2.network(), IpAddr::V4(Ipv4Addr::new(10,0,0,0))); + /// ``` + pub fn network(&self) -> IpAddr { + self.inner.network() + } + + /// Returns the braodcast address for the subnet. + /// + /// # Examples + /// + /// ``` + /// use mycelium::subnet::Subnet; + /// use std::net::{IpAddr, Ipv4Addr,Ipv6Addr}; + /// + /// let subnet_1 = Subnet::new(Ipv6Addr::new(12,34,56,78,90,0xab,0xcd,0xef).into(), + /// 32).unwrap(); + /// let subnet_2 = Subnet::new(Ipv4Addr::new(10,1,2,3).into(), 8).unwrap(); + /// + /// assert_eq!(subnet_1.broadcast_addr(), + /// IpAddr::V6(Ipv6Addr::new(12,34,0xffff,0xffff,0xffff,0xffff,0xffff,0xffff))); + /// assert_eq!(subnet_2.broadcast_addr(), IpAddr::V4(Ipv4Addr::new(10,255,255,255))); + /// ``` + pub fn broadcast_addr(&self) -> IpAddr { + self.inner.broadcast() + } + + /// Returns the netmask of the subnet as an [`IpAddr`]. + pub fn mask(&self) -> IpAddr { + self.inner.netmask() + } +} + +impl From for Subnet { + fn from(value: Ipv6Addr) -> Self { + Self::new(value.into(), 128).expect("128 is a valid subnet size for an IPv6 address; qed") + } +} + +#[derive(Debug, Clone)] +/// An error indicating a malformed subnet +pub struct SubnetParseError { + _private: (), +} + +impl SubnetParseError { + /// Create a new SubnetParseError + fn new() -> Self { + Self { _private: () } + } +} + +impl core::fmt::Display for SubnetParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.pad("malformed subnet") + } +} + +impl std::error::Error for SubnetParseError {} + +impl FromStr for Subnet { + type Err = SubnetParseError; + + fn from_str(s: &str) -> Result { + if let Ok(ipnet) = s.parse::() { + return Ok( + Subnet::new(ipnet.addr(), ipnet.prefix_len()).expect("Parsed subnet size is valid") + ); + } + + // Try to parse as an IP address (convert to /32 or /128 subnet) + if let Ok(ip) = s.parse::() { + let prefix_len = match ip { + std::net::IpAddr::V4(_) => 32, + std::net::IpAddr::V6(_) => 128, + }; + return Ok(Subnet::new(ip, prefix_len).expect("Static subnet sizes are valid")); + } + + Err(SubnetParseError::new()) + } +} + +impl fmt::Display for Subnet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.inner) + } +} + +impl PartialEq for Subnet { + fn eq(&self, other: &Self) -> bool { + // Quic check, subnets of different sizes are never equal. + if self.prefix_len() != other.prefix_len() { + return false; + } + + // Full check + self.network() == other.network() + } +} + +impl Hash for Subnet { + fn hash(&self, state: &mut H) { + // First write the subnet size + state.write_u8(self.prefix_len()); + // Then write the IP of the network. This sets the non prefix bits to 0, so hash values + // will be equal according to the PartialEq rules. + self.network().hash(state) + } +} + +impl fmt::Display for PrefixLenError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Invalid prefix length for this address") + } +} + +impl std::error::Error for PrefixLenError {} + +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, Ipv6Addr}; + + use super::Subnet; + + #[test] + fn test_subnet_equality() { + let subnet_1 = + Subnet::new(Ipv6Addr::new(12, 23, 34, 45, 56, 67, 78, 89).into(), 64).unwrap(); + let subnet_2 = + Subnet::new(Ipv6Addr::new(12, 23, 34, 45, 67, 78, 89, 90).into(), 64).unwrap(); + let subnet_3 = + Subnet::new(Ipv6Addr::new(12, 23, 34, 40, 67, 78, 89, 90).into(), 64).unwrap(); + let subnet_4 = Subnet::new(Ipv6Addr::new(12, 23, 34, 45, 0, 0, 0, 0).into(), 64).unwrap(); + let subnet_5 = Subnet::new( + Ipv6Addr::new(12, 23, 34, 45, 0xffff, 0xffff, 0xffff, 0xffff).into(), + 64, + ) + .unwrap(); + let subnet_6 = + Subnet::new(Ipv6Addr::new(12, 23, 34, 45, 56, 67, 78, 89).into(), 63).unwrap(); + + assert_eq!(subnet_1, subnet_2); + assert_ne!(subnet_1, subnet_3); + assert_eq!(subnet_1, subnet_4); + assert_eq!(subnet_1, subnet_5); + assert_ne!(subnet_1, subnet_6); + + let subnet_1 = Subnet::new(Ipv4Addr::new(10, 1, 2, 3).into(), 24).unwrap(); + let subnet_2 = Subnet::new(Ipv4Addr::new(10, 1, 2, 102).into(), 24).unwrap(); + let subnet_3 = Subnet::new(Ipv4Addr::new(10, 1, 4, 3).into(), 24).unwrap(); + let subnet_4 = Subnet::new(Ipv4Addr::new(10, 1, 2, 0).into(), 24).unwrap(); + let subnet_5 = Subnet::new(Ipv4Addr::new(10, 1, 2, 255).into(), 24).unwrap(); + let subnet_6 = Subnet::new(Ipv4Addr::new(10, 1, 2, 3).into(), 16).unwrap(); + + assert_eq!(subnet_1, subnet_2); + assert_ne!(subnet_1, subnet_3); + assert_eq!(subnet_1, subnet_4); + assert_eq!(subnet_1, subnet_5); + assert_ne!(subnet_1, subnet_6); + } +} diff --git a/components/mycelium/mycelium/src/task.rs b/components/mycelium/mycelium/src/task.rs new file mode 100644 index 0000000..6db0769 --- /dev/null +++ b/components/mycelium/mycelium/src/task.rs @@ -0,0 +1,29 @@ +//! This module provides some task abstractions which add custom logic to the default behavior. + +/// A handle to a task, which is only used to abort the task. In case this handle is dropped, the +/// task is cancelled automatically. +pub struct AbortHandle(tokio::task::AbortHandle); + +impl AbortHandle { + /// Abort the task this `AbortHandle` is referencing. It is safe to call this method multiple + /// times, but only the first call is actually usefull. It is possible for the task to still + /// finish succesfully, even after abort is called. + #[inline] + pub fn abort(&self) { + self.0.abort() + } +} + +impl Drop for AbortHandle { + #[inline] + fn drop(&mut self) { + self.0.abort() + } +} + +impl From for AbortHandle { + #[inline] + fn from(value: tokio::task::AbortHandle) -> Self { + Self(value) + } +} diff --git a/components/mycelium/mycelium/src/tun.rs b/components/mycelium/mycelium/src/tun.rs new file mode 100644 index 0000000..c4831f2 --- /dev/null +++ b/components/mycelium/mycelium/src/tun.rs @@ -0,0 +1,54 @@ +//! The tun module implements a platform independent Tun interface. + +#[cfg(any( + target_os = "linux", + all(target_os = "macos", not(feature = "mactunfd")), + target_os = "windows" +))] +use crate::subnet::Subnet; + +#[cfg(any( + target_os = "linux", + all(target_os = "macos", not(feature = "mactunfd")), + target_os = "windows" +))] +pub struct TunConfig { + pub name: String, + pub node_subnet: Subnet, + pub route_subnet: Subnet, +} + +#[cfg(any( + target_os = "android", + target_os = "ios", + all(target_os = "macos", feature = "mactunfd"), +))] +pub struct TunConfig { + pub tun_fd: i32, +} +#[cfg(target_os = "linux")] +mod linux; + +#[cfg(target_os = "linux")] +pub use linux::new; + +#[cfg(all(target_os = "macos", not(feature = "mactunfd")))] +mod darwin; + +#[cfg(all(target_os = "macos", not(feature = "mactunfd")))] +pub use darwin::new; + +#[cfg(target_os = "windows")] +mod windows; +#[cfg(target_os = "windows")] +pub use windows::new; + +#[cfg(target_os = "android")] +mod android; +#[cfg(target_os = "android")] +pub use android::new; + +#[cfg(any(target_os = "ios", all(target_os = "macos", feature = "mactunfd")))] +mod ios; +#[cfg(any(target_os = "ios", all(target_os = "macos", feature = "mactunfd")))] +pub use ios::new; diff --git a/components/mycelium/mycelium/src/tun/android.rs b/components/mycelium/mycelium/src/tun/android.rs new file mode 100644 index 0000000..bebcc0d --- /dev/null +++ b/components/mycelium/mycelium/src/tun/android.rs @@ -0,0 +1,108 @@ +//! android specific tun interface setup. + +use std::io::{self}; + +use futures::{Sink, Stream}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + select, + sync::mpsc, +}; +use tracing::{error, info}; + +use crate::crypto::PacketBuffer; +use crate::tun::TunConfig; + +// TODO +const LINK_MTU: i32 = 1400; + +/// Create a new tun interface and set required routes +/// +/// # Panics +/// +/// This function will panic if called outside of the context of a tokio runtime. +pub async fn new( + tun_config: TunConfig, +) -> Result< + ( + impl Stream>, + impl Sink + Clone, + ), + Box, +> { + let name = "tun0"; + let mut tun = create_tun_interface(name, tun_config.tun_fd)?; + + let (tun_sink, mut sink_receiver) = mpsc::channel::(1000); + let (tun_stream, stream_receiver) = mpsc::unbounded_channel(); + + // Spawn a single task to manage the TUN interface + tokio::spawn(async move { + let mut buf_hold = None; + loop { + let mut buf = if let Some(buf) = buf_hold.take() { + buf + } else { + PacketBuffer::new() + }; + + select! { + data = sink_receiver.recv() => { + match data { + None => return, + Some(data) => { + if let Err(e) = tun.write(&data).await { + error!("Failed to send data to tun interface {e}"); + } + } + } + // Save the buffer as we didn't use it + buf_hold = Some(buf); + } + read_result = tun.read(buf.buffer_mut()) => { + let rr = read_result.map(|n| { + buf.set_size(n); + buf + }); + + + if tun_stream.send(rr).is_err() { + error!("Could not forward data to tun stream, receiver is gone"); + break; + }; + } + } + } + info!("Stop reading from / writing to tun interface"); + }); + + Ok(( + tokio_stream::wrappers::UnboundedReceiverStream::new(stream_receiver), + tokio_util::sync::PollSender::new(tun_sink), + )) +} + +/// Create a new TUN interface +fn create_tun_interface( + name: &str, + tun_fd: i32, +) -> Result> { + let mut config = tun::Configuration::default(); + config + .name(name) + .layer(tun::Layer::L3) + .mtu(LINK_MTU) + .queues(1) + .raw_fd(tun_fd) + .up(); + info!("create_tun_interface"); + let tun = match tun::create_as_async(&config) { + Ok(tun) => tun, + Err(err) => { + error!("[android]failed to create tun interface: {err}"); + return Err(Box::new(err)); + } + }; + + Ok(tun) +} diff --git a/components/mycelium/mycelium/src/tun/darwin.rs b/components/mycelium/mycelium/src/tun/darwin.rs new file mode 100644 index 0000000..11b84f2 --- /dev/null +++ b/components/mycelium/mycelium/src/tun/darwin.rs @@ -0,0 +1,347 @@ +//! macos specific tun interface setup. + +use std::{ + ffi::CString, + io::{self, IoSlice}, + net::IpAddr, + os::fd::AsRawFd, + str::FromStr, +}; + +use futures::{Sink, Stream}; +use nix::sys::socket::SockaddrIn6; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + select, + sync::mpsc, +}; +use tracing::{debug, error, info, warn}; + +use crate::crypto::PacketBuffer; +use crate::subnet::Subnet; +use crate::tun::TunConfig; + +// TODO +const LINK_MTU: i32 = 1400; + +/// The 4 byte packet header written before a packet is sent on the TUN +// TODO: figure out structure and values, but for now this seems to work. +const HEADER: [u8; 4] = [0, 0, 0, 30]; + +const IN6_IFF_NODAD: u32 = 0x0020; // netinet6/in6_var.h +const IN6_IFF_SECURED: u32 = 0x0400; // netinet6/in6_var.h +const ND6_INFINITE_LIFETIME: u32 = 0xFFFFFFFF; // netinet6/nd6.h + +/// Wrapper for an OS-specific interface name +// Allways hold the max size of an interface. This includes the 0 byte for termination. +// repr transparent so this can be used with libc calls. +#[repr(transparent)] +#[derive(Clone, Copy)] +pub struct IfaceName([libc::c_char; libc::IFNAMSIZ as _]); + +/// Wrapped interface handle. +#[derive(Clone, Copy)] +struct Iface { + /// Name of the interface + iface_name: IfaceName, +} + +/// Struct to add IPv6 route to interface +#[repr(C)] +pub struct IfaliasReq { + ifname: IfaceName, + addr: SockaddrIn6, + dst_addr: SockaddrIn6, + mask: SockaddrIn6, + flags: u32, + lifetime: AddressLifetime, +} + +#[repr(C)] +pub struct AddressLifetime { + /// Not used for userspace -> kernel space + expire: libc::time_t, + /// Not used for userspace -> kernel space + preferred: libc::time_t, + vltime: u32, + pltime: u32, +} + +/// Create a new tun interface and set required routes +/// +/// # Panics +/// +/// This function will panic if called outside of the context of a tokio runtime. +pub async fn new( + tun_config: TunConfig, +) -> Result< + ( + impl Stream>, + impl Sink + Clone, + ), + Box, +> { + let tun_name = find_available_utun_name(&tun_config.name)?; + + let mut tun = match create_tun_interface(&tun_name) { + Ok(tun) => tun, + Err(e) => { + error!(tun_name=%tun_name, err=%e, "Could not create TUN device. Make sure the name is not yet in use, and you have sufficient privileges to create a network device"); + return Err(e); + } + }; + let iface = Iface::by_name(&tun_name)?; + iface.add_address(tun_config.node_subnet, tun_config.route_subnet)?; + + let (tun_sink, mut sink_receiver) = mpsc::channel::(1000); + let (tun_stream, stream_receiver) = mpsc::unbounded_channel(); + + // Spawn a single task to manage the TUN interface + tokio::spawn(async move { + let mut buf_hold = None; + loop { + let mut buf: PacketBuffer = buf_hold.take().unwrap_or_default(); + + select! { + data = sink_receiver.recv() => { + match data { + None => return, + Some(data) => { + // We need to append a 4 byte header here + if let Err(e) = tun.write_vectored(&[IoSlice::new(&HEADER), IoSlice::new(&data)]).await { + error!("Failed to send data to tun interface {e}"); + } + } + } + // Save the buffer as we didn't use it + buf_hold = Some(buf); + } + read_result = tun.read(buf.buffer_mut()) => { + let rr = read_result.map(|n| { + buf.set_size(n); + // Trim header + buf.buffer_mut().copy_within(4.., 0); + buf.set_size(n-4); + buf + }); + + + if tun_stream.send(rr).is_err() { + error!("Could not forward data to tun stream, receiver is gone"); + break; + }; + } + } + } + info!("Stop reading from / writing to tun interface"); + }); + + Ok(( + tokio_stream::wrappers::UnboundedReceiverStream::new(stream_receiver), + tokio_util::sync::PollSender::new(tun_sink), + )) +} + +/// Checks if a name is valid for a utun interface +/// +/// Rules: +/// - must start with "utun" +/// - followed by only digits +/// - 15 chars total at most +fn validate_utun_name(input: &str) -> bool { + if input.len() > 15 { + return false; + } + if !input.starts_with("utun") { + return false; + } + + input + .strip_prefix("utun") + .expect("We just checked that name starts with 'utun' so this is always some") + .parse::() + .is_ok() +} + +/// Validates the user-supplied TUN interface name +/// +/// - If the name is valid and not in use, it will be the TUN name +/// - If the name is valid but already in use, an error will be thrown +/// - If the name is not valid, we try to find the first freely available TUN name +fn find_available_utun_name(preferred_name: &str) -> Result { + // Get the list of existing utun interfaces. + let interfaces = netdev::get_interfaces(); + let utun_interfaces: Vec<_> = interfaces + .iter() + .filter_map(|iface| { + if iface.name.starts_with("utun") { + Some(iface.name.as_str()) + } else { + None + } + }) + .collect(); + + // Check if the preferred name is valid and not in use. + if validate_utun_name(preferred_name) && !utun_interfaces.contains(&preferred_name) { + return Ok(preferred_name.to_string()); + } + + // If the preferred name is invalid or already in use, find the first available utun name. + if !validate_utun_name(preferred_name) { + warn!(tun_name=%preferred_name, "Invalid TUN name. Looking for the first available TUN name"); + } else { + warn!(tun_name=%preferred_name, "TUN name already in use. Looking for the next available TUN name."); + } + + // Extract and sort the utun numbers. + let mut utun_numbers = utun_interfaces + .iter() + .filter_map(|iface| iface[4..].parse::().ok()) + .collect::>(); + + utun_numbers.sort_unstable(); + + // Find the first available utun index. + let mut first_free_index = 0; + for (i, &num) in utun_numbers.iter().enumerate() { + if num != i { + first_free_index = i; + break; + } + first_free_index = i + 1; + } + + // Create new utun name based on the first free index. + let new_utun_name = format!("utun{}", first_free_index); + if validate_utun_name(&new_utun_name) { + info!(tun_name=%new_utun_name, "Automatically assigned TUN name."); + Ok(new_utun_name) + } else { + error!("No available TUN name found"); + Err(io::Error::new( + io::ErrorKind::Other, + "No available TUN name", + )) + } +} + +/// Create a new TUN interface +fn create_tun_interface(name: &str) -> Result> { + let mut config = tun::Configuration::default(); + config + .name(name) + .layer(tun::Layer::L3) + .mtu(LINK_MTU) + .queues(1) + .up(); + let tun = tun::create_as_async(&config)?; + + Ok(tun) +} + +impl IfaceName { + fn as_ptr(&self) -> *const libc::c_char { + self.0.as_ptr() + } +} + +impl FromStr for IfaceName { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + // Equal len is not allowed because we need to add the 0 byte terminator. + if s.len() >= libc::IFNAMSIZ { + return Err("Interface name too long"); + } + + // TODO: Is this err possible in a &str? + let raw_name = CString::new(s).map_err(|_| "Interface name contains 0 byte")?; + let mut backing = [0; libc::IFNAMSIZ]; + let name_bytes = raw_name.to_bytes_with_nul(); + backing[..name_bytes.len()].copy_from_slice(name_bytes); + // SAFETY: This doesn't do any weird things with the bits when converting from u8 to i8 + let backing = unsafe { std::mem::transmute::<[u8; 16], [i8; 16]>(backing) }; + Ok(Self(backing)) + } +} + +impl Iface { + /// Retrieve the link index of an interface with the given name + fn by_name(name: &str) -> Result> { + let iface_name: IfaceName = name.parse()?; + match unsafe { libc::if_nametoindex(iface_name.as_ptr()) } { + 0 => Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "interface not found", + ))?, + _ => Ok(Iface { iface_name }), + } + } + + /// Add an address to an interface. + /// + /// # Panics + /// + /// Only IPv6 is supported, this function will panic when adding an IPv4 subnet. + fn add_address( + &self, + subnet: Subnet, + route_subnet: Subnet, + ) -> Result<(), Box> { + let addr = if let IpAddr::V6(addr) = subnet.address() { + addr + } else { + panic!("IPv4 subnets are not supported"); + }; + let mask_addr = if let IpAddr::V6(mask) = route_subnet.mask() { + mask + } else { + // We already know we are IPv6 here + panic!("IPv4 routes are not supported"); + }; + + let sock_addr = SockaddrIn6::from(std::net::SocketAddrV6::new(addr, 0, 0, 0)); + let mask = SockaddrIn6::from(std::net::SocketAddrV6::new(mask_addr, 0, 0, 0)); + + let req = IfaliasReq { + ifname: self.iface_name, + addr: sock_addr, + // SAFETY: kernel expects this to be fully zeroed + dst_addr: unsafe { std::mem::zeroed() }, + mask, + flags: IN6_IFF_NODAD | IN6_IFF_SECURED, + lifetime: AddressLifetime { + expire: 0, + preferred: 0, + vltime: ND6_INFINITE_LIFETIME, + pltime: ND6_INFINITE_LIFETIME, + }, + }; + + let sock = random_socket()?; + + match unsafe { siocaifaddr_in6(sock.as_raw_fd(), &req) } { + Err(e) => { + error!("Failed to add ipv6 addresst to interface {e}"); + Err(std::io::Error::last_os_error())? + } + Ok(_) => { + debug!("Added {subnet} to tun interfacel"); + Ok(()) + } + } + } +} +// Create a socket to talk to the kernel. +fn random_socket() -> Result { + std::net::UdpSocket::bind("[::1]:0") +} + +nix::ioctl_write_ptr!( + /// Add an IPv6 subnet to an interface. + siocaifaddr_in6, + b'i', + 26, + IfaliasReq +); diff --git a/components/mycelium/mycelium/src/tun/ios.rs b/components/mycelium/mycelium/src/tun/ios.rs new file mode 100644 index 0000000..a21db19 --- /dev/null +++ b/components/mycelium/mycelium/src/tun/ios.rs @@ -0,0 +1,100 @@ +//! ios specific tun interface setup. + +use std::io::{self, IoSlice}; + +use futures::{Sink, Stream}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + select, + sync::mpsc, +}; +use tracing::{error, info}; + +use crate::crypto::PacketBuffer; +use crate::tun::TunConfig; + +// TODO +const LINK_MTU: i32 = 1400; + +/// The 4 byte packet header written before a packet is sent on the TUN +// TODO: figure out structure and values, but for now this seems to work. +const HEADER: [u8; 4] = [0, 0, 0, 30]; + +/// Create a new tun interface and set required routes +/// +/// # Panics +/// +/// This function will panic if called outside of the context of a tokio runtime. +pub async fn new( + tun_config: TunConfig, +) -> Result< + ( + impl Stream>, + impl Sink + Clone, + ), + Box, +> { + let mut tun = create_tun_interface(tun_config.tun_fd)?; + + let (tun_sink, mut sink_receiver) = mpsc::channel::(1000); + let (tun_stream, stream_receiver) = mpsc::unbounded_channel(); + + // Spawn a single task to manage the TUN interface + tokio::spawn(async move { + let mut buf_hold = None; + loop { + let mut buf: PacketBuffer = buf_hold.take().unwrap_or_default(); + + select! { + data = sink_receiver.recv() => { + match data { + None => return, + Some(data) => { + // We need to append a 4 byte header here + if let Err(e) = tun.write_vectored(&[IoSlice::new(&HEADER), IoSlice::new(&data)]).await { + error!("Failed to send data to tun interface {e}"); + } + } + } + // Save the buffer as we didn't use it + buf_hold = Some(buf); + } + read_result = tun.read(buf.buffer_mut()) => { + let rr = read_result.map(|n| { + buf.set_size(n); + // Trim header + buf.buffer_mut().copy_within(4.., 0); + buf.set_size(n-4); + buf + }); + + + if tun_stream.send(rr).is_err() { + error!("Could not forward data to tun stream, receiver is gone"); + break; + }; + } + } + } + info!("Stop reading from / writing to tun interface"); + }); + + Ok(( + tokio_stream::wrappers::UnboundedReceiverStream::new(stream_receiver), + tokio_util::sync::PollSender::new(tun_sink), + )) +} + +/// Create a new TUN interface +fn create_tun_interface(tun_fd: i32) -> Result> { + let mut config = tun::Configuration::default(); + config + .layer(tun::Layer::L3) + .mtu(LINK_MTU) + .queues(1) + .raw_fd(tun_fd) + .up(); + let tun = tun::create_as_async(&config)?; + + Ok(tun) +} diff --git a/components/mycelium/mycelium/src/tun/linux.rs b/components/mycelium/mycelium/src/tun/linux.rs new file mode 100644 index 0000000..4958a21 --- /dev/null +++ b/components/mycelium/mycelium/src/tun/linux.rs @@ -0,0 +1,156 @@ +//! Linux specific tun interface setup. + +use std::io; + +use futures::{Sink, Stream, TryStreamExt}; +use rtnetlink::Handle; +use tokio::{select, sync::mpsc}; +use tokio_tun::{Tun, TunBuilder}; +use tracing::{error, info}; + +use crate::crypto::PacketBuffer; +use crate::subnet::Subnet; +use crate::tun::TunConfig; + +// TODO +const LINK_MTU: i32 = 1400; + +/// Create a new tun interface and set required routes +/// +/// # Panics +/// +/// This function will panic if called outside of the context of a tokio runtime. +pub async fn new( + tun_config: TunConfig, +) -> Result< + ( + impl Stream>, + impl Sink + Clone, + ), + Box, +> { + let tun = match create_tun_interface(&tun_config.name) { + Ok(tun) => tun, + Err(e) => { + error!( + "Could not create tun device named \"{}\", make sure the name is not yet in use, and you have sufficient privileges to create a network device", + tun_config.name, + ); + return Err(e); + } + }; + + let (conn, handle, _) = rtnetlink::new_connection()?; + let netlink_task_handle = tokio::spawn(conn); + + let tun_index = link_index_by_name(handle.clone(), tun_config.name).await?; + + if let Err(e) = add_address( + handle.clone(), + tun_index, + Subnet::new( + tun_config.node_subnet.address(), + tun_config.route_subnet.prefix_len(), + ) + .unwrap(), + ) + .await + { + error!( + "Failed to add address {0} to TUN interface: {e}", + tun_config.node_subnet + ); + return Err(e); + } + + // We are done with our netlink connection, abort the task so we can properly clean up. + netlink_task_handle.abort(); + + let (tun_sink, mut sink_receiver) = mpsc::channel::(1000); + let (tun_stream, stream_receiver) = mpsc::unbounded_channel(); + + // Spawn a single task to manage the TUN interface + tokio::spawn(async move { + let mut buf_hold = None; + loop { + let mut buf: PacketBuffer = buf_hold.take().unwrap_or_default(); + + select! { + data = sink_receiver.recv() => { + match data { + None => return, + Some(data) => { + if let Err(e) = tun.send(&data).await { + error!("Failed to send data to tun interface {e}"); + } + } + } + // Save the buffer as we didn't use it + buf_hold = Some(buf); + } + read_result = tun.recv(buf.buffer_mut()) => { + let rr = read_result.map(|n| { + buf.set_size(n); + buf + }); + + if tun_stream.send(rr).is_err() { + error!("Could not forward data to tun stream, receiver is gone"); + break; + }; + } + } + } + info!("Stop reading from / writing to tun interface"); + }); + + Ok(( + tokio_stream::wrappers::UnboundedReceiverStream::new(stream_receiver), + tokio_util::sync::PollSender::new(tun_sink), + )) +} + +/// Create a new TUN interface +fn create_tun_interface(name: &str) -> Result> { + let tun = TunBuilder::new() + .name(name) + .mtu(LINK_MTU) + .queues(1) + .up() + .build()? + .pop() + .expect("Succesfully build tun interface has 1 queue"); + + Ok(tun) +} + +/// Retrieve the link index of an interface with the given name +async fn link_index_by_name( + handle: Handle, + name: String, +) -> Result> { + handle + .link() + .get() + .match_name(name) + .execute() + .try_next() + .await? + .map(|link_message| link_message.header.index) + .ok_or(io::Error::new(io::ErrorKind::NotFound, "link not found").into()) +} + +/// Add an address to an interface. +/// +/// The kernel will automatically add a route entry for the subnet assigned to the interface. +async fn add_address( + handle: Handle, + link_index: u32, + subnet: Subnet, +) -> Result<(), Box> { + Ok(handle + .address() + .add(link_index, subnet.address(), subnet.prefix_len()) + .execute() + .await?) +} diff --git a/components/mycelium/mycelium/src/tun/windows.rs b/components/mycelium/mycelium/src/tun/windows.rs new file mode 100644 index 0000000..561a666 --- /dev/null +++ b/components/mycelium/mycelium/src/tun/windows.rs @@ -0,0 +1,166 @@ +use std::{io, ops::Deref, sync::Arc}; + +use futures::{Sink, Stream}; +use tokio::sync::mpsc; +use tracing::{error, info, warn}; + +use crate::tun::TunConfig; +use crate::{crypto::PacketBuffer, subnet::Subnet}; + +// TODO +const LINK_MTU: usize = 1400; + +/// Type of the tunnel used, specified when creating the tunnel. +const WINDOWS_TUNNEL_TYPE: &str = "Mycelium"; + +pub async fn new( + tun_config: TunConfig, +) -> Result< + ( + impl Stream>, + impl Sink + Clone, + ), + Box, +> { + // SAFETY: for now we assume a valid wintun.dll file exists in the root directory when we are + // running this. + let wintun = unsafe { wintun::load() }?; + let wintun_version = match wintun::get_running_driver_version(&wintun) { + Ok(v) => format!("{v}"), + Err(e) => { + warn!("Failed to read wintun.dll version: {e}"); + "Unknown".to_string() + } + }; + info!("Loaded wintun.dll - running version {wintun_version}"); + let tun = wintun::Adapter::create(&wintun, &tun_config.name, WINDOWS_TUNNEL_TYPE, None)?; + info!("Created wintun tunnel interface"); + // Configure created network adapter. + set_adapter_mtu(&tun_config.name, LINK_MTU)?; + // Set address, this will use a `netsh` command under the hood unfortunately. + // TODO: fix in library + // tun.set_network_addresses_tuple(node_subnet.address(), route_subnet.mask(), None)?; + add_address( + &tun_config.name, + tun_config.node_subnet, + tun_config.route_subnet, + )?; + // Build 2 separate sessions - one for receiving, one for sending. + let rx_session = Arc::new(tun.start_session(wintun::MAX_RING_CAPACITY)?); + let tx_session = rx_session.clone(); + + let (tun_sink, mut sink_receiver) = mpsc::channel::(1000); + let (tun_stream, stream_receiver) = mpsc::unbounded_channel(); + + // Ingress path + tokio::task::spawn_blocking(move || { + loop { + let packet = rx_session + .receive_blocking() + .map(|tun_packet| { + let mut buffer = PacketBuffer::new(); + // SAFETY: The configured MTU is smaller than the static PacketBuffer size. + let packet_len = tun_packet.bytes().len(); + buffer.buffer_mut()[..packet_len].copy_from_slice(tun_packet.bytes()); + buffer.set_size(packet_len); + buffer + }) + .map_err(wintun_to_io_error); + + if tun_stream.send(packet).is_err() { + error!("Could not forward data to tun stream, receiver is gone"); + break; + }; + } + + info!("Stop reading from tun interface"); + }); + + // Egress path + tokio::task::spawn_blocking(move || { + loop { + match sink_receiver.blocking_recv() { + None => break, + Some(data) => { + let mut tun_packet = + match tx_session.allocate_send_packet(data.deref().len() as u16) { + Ok(tun_packet) => tun_packet, + Err(e) => { + error!("Could not allocate packet on TUN: {e}"); + break; + } + }; + // SAFETY: packet allocation is done on the length of &data. + tun_packet.bytes_mut().copy_from_slice(&data); + tx_session.send_packet(tun_packet); + } + } + } + info!("Stop writing to tun interface"); + }); + + Ok(( + tokio_stream::wrappers::UnboundedReceiverStream::new(stream_receiver), + tokio_util::sync::PollSender::new(tun_sink), + )) +} + +/// Helper method to convert a [`wintun::Error`] to a [`std::io::Error`]. +fn wintun_to_io_error(err: wintun::Error) -> io::Error { + match err { + wintun::Error::Io(e) => e, + _ => io::Error::other("unknown wintun error"), + } +} + +/// Set an address on an interface by shelling out to `netsh` +/// +/// We assume this is an IPv6 address. +fn add_address(adapter_name: &str, subnet: Subnet, route_subnet: Subnet) -> Result<(), io::Error> { + let exit_code = std::process::Command::new("netsh") + .args([ + "interface", + "ipv6", + "set", + "address", + adapter_name, + &format!("{}/{}", subnet.address(), route_subnet.prefix_len()), + ]) + .spawn()? + .wait()?; + + match exit_code.code() { + Some(0) => Ok(()), + Some(x) => Err(io::Error::from_raw_os_error(x)), + None => { + warn!("Failed to determine `netsh` exit status"); + Ok(()) + } + } +} + +fn set_adapter_mtu(name: &str, mtu: usize) -> Result<(), io::Error> { + let args = &[ + "interface", + "ipv6", + "set", + "subinterface", + &format!("\"{name}\""), + &format!("mtu={mtu}"), + "store=persistent", + ]; + + let exit_code = std::process::Command::new("netsh") + .args(args) + .spawn()? + .wait()?; + + match exit_code.code() { + Some(0) => Ok(()), + Some(x) => Err(io::Error::from_raw_os_error(x)), + None => { + warn!("Failed to determine `netsh` exit status"); + Ok(()) + } + } +} diff --git a/components/mycelium/myceliumd-private/.gitignore b/components/mycelium/myceliumd-private/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/components/mycelium/myceliumd-private/.gitignore @@ -0,0 +1 @@ +/target diff --git a/components/mycelium/myceliumd-private/Cargo.lock b/components/mycelium/myceliumd-private/Cargo.lock new file mode 100644 index 0000000..266a252 --- /dev/null +++ b/components/mycelium/myceliumd-private/Cargo.lock @@ -0,0 +1,4439 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byte-unit" +version = "5.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +dependencies = [ + "rust_decimal", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "c2rust-bitfields" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b43c3f07ab0ef604fa6f595aa46ec2f8a22172c975e186f6f5bf9829a3b72c41" +dependencies = [ + "c2rust-bitfields-derive", +] + +[[package]] +name = "c2rust-bitfields-derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3cbc102e2597c9744c8bd8c15915d554300601c91a079430d309816b0912545" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] + +[[package]] +name = "cdn-meta" +version = "0.1.0" +source = "git+https://github.com/threefoldtech/mycelium-cdn-registry#2fac04710b60502a5165f1b1d90671730346e977" +dependencies = [ + "aes-gcm", + "bincode", + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "config" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1eb4fb07bc7f012422df02766c7bd5971effb894f573865642f06fa3265440" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "etherparse" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff83a5facf1a7cbfef93cfb48d6d4fb6a1f42d8ac2341a96b3255acb4d4f860" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.2", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.10", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", +] + +[[package]] +name = "ioctl-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd11f3a29434026f5ff98c730b668ba74b1033637b8817940b54d040696133c" + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonrpsee" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fba77a59c4c644fd48732367624d1bcf6f409f9c9a286fbc71d2f1fc0b2ea16" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-proc-macros", + "jsonrpsee-server", + "jsonrpsee-types", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693c93cbb7db25f4108ed121304b671a36002c2db67dff2ee4391a688c738547" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "parking_lot 0.12.3", + "pin-project", + "rand 0.9.1", + "rustc-hash", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fa4f5daed39f982a1bb9d15449a28347490ad42b212f8eaa2a2a344a0dce9e9" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38b0bcf407ac68d241f90e2d46041e6a06988f97fe1721fb80b91c42584fae6" +dependencies = [ + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66df7256371c45621b3b7d2fb23aea923d577616b9c0e9c0b950a6ea5c2be0ca" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "left-right" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabfddf3ad712b726484562039aa6fc2014bc1b5c088bb211b208052cf0439e6" +dependencies = [ + "loom", + "slab", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "mycelium" +version = "0.6.1" +dependencies = [ + "aes-gcm", + "ahash 0.8.11", + "arc-swap", + "axum", + "axum-extra", + "blake3", + "bytes", + "cdn-meta", + "dashmap", + "etherparse", + "faster-hex", + "futures", + "ip_network_table-deps-treebitmap", + "ipnet", + "left-right", + "libc", + "netdev", + "nix 0.29.0", + "nix 0.30.1", + "openssl", + "quinn", + "rand 0.9.1", + "rcgen", + "redis", + "reed-solomon-erasure", + "reqwest", + "rtnetlink", + "rustls", + "serde", + "tokio", + "tokio-openssl", + "tokio-stream", + "tokio-tun", + "tokio-util", + "tracing", + "tracing-logfmt", + "tracing-subscriber", + "tun", + "wintun 0.5.1", + "x25519-dalek", +] + +[[package]] +name = "mycelium-api" +version = "0.6.1" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "jsonrpsee", + "mycelium", + "mycelium-metrics", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "mycelium-cli" +version = "0.6.1" +dependencies = [ + "base64 0.22.1", + "byte-unit", + "mycelium", + "mycelium-api", + "prettytable-rs", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "urlencoding", +] + +[[package]] +name = "mycelium-metrics" +version = "0.6.1" +dependencies = [ + "axum", + "mycelium", + "prometheus", + "tokio", + "tracing", +] + +[[package]] +name = "myceliumd-private" +version = "0.6.1" +dependencies = [ + "base64 0.22.1", + "clap", + "config", + "dirs", + "mycelium", + "mycelium-api", + "mycelium-cli", + "mycelium-metrics", + "reqwest", + "serde", + "serde_json", + "tokio", + "toml", + "tracing", + "tracing-logfmt", + "tracing-subscriber", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "netdev" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862209dce034f82a44c95ce2b5183730d616f2a68746b9c1959aa2572e77c0a1" +dependencies = [ + "dlopen2", + "ipnet", + "libc", + "netlink-packet-core", + "netlink-packet-route 0.22.0", + "netlink-sys", + "once_cell", + "system-configuration", + "windows-sys 0.59.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d83370a96813d7c977f8b63054f1162df6e5784f1c598d689236564fb5a6f2" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-proto" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.12", +] + +[[package]] +name = "netlink-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +dependencies = [ + "bytes", + "futures", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-src" +version = "300.4.2+3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.10", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "pest_meta" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.23", +] + +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" +dependencies = [ + "bitflags 2.9.0", + "hex", + "procfs-core", + "rustix 0.38.44", +] + +[[package]] +name = "procfs-core" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +dependencies = [ + "bitflags 2.9.0", + "hex", +] + +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "libc", + "memchr", + "parking_lot 0.12.3", + "procfs", + "thiserror 2.0.12", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.1", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.1", +] + +[[package]] +name = "rcgen" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49bc8ffa8a832eb1d7c8000337f8b0d2f4f2f5ec3cf4ddc26f125e3ad2451824" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redis" +version = "0.32.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f66bf4cac9733a23bcdf1e0e01effbaaad208567beba68be8f67e5f4af3ee1" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.6.0", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.12", +] + +[[package]] +name = "reed-solomon-erasure" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7263373d500d4d4f505d43a2a662d475a894aa94503a1ee28e9188b5f3960d4f" +dependencies = [ + "libm", + "lru", + "parking_lot 0.11.2", + "smallvec", + "spin", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.9.0", + "serde", + "serde_derive", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rtnetlink" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbe0a03f6b9b483c67d4b328fc5d66c8db0b6aa274e0fa2def71b5e442a69acf" +dependencies = [ + "futures", + "log", + "netlink-packet-core", + "netlink-packet-route 0.24.0", + "netlink-packet-utils", + "netlink-proto", + "netlink-sys", + "nix 0.29.0", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +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 = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.59.0", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" + +[[package]] +name = "time-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.5.10", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-openssl" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59df6849caa43bb7567f9a36f863c447d95a11d5903c9cc334ba32576a27eadd" +dependencies = [ + "openssl", + "openssl-sys", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tun" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be25a316a2d10d0ddd46de9ddd73cdb4262678e1e52f4a32a31421f1e2b816b4" +dependencies = [ + "libc", + "nix 0.29.0", + "thiserror 2.0.12", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "toml_datetime 0.6.9", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-logfmt" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1f47d22deb79c3f59fcf2a1f00f60cbdc05462bf17d1cd356c1fefa3f444bd" +dependencies = [ + "nu-ansi-term 0.50.1", + "time", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term 0.46.0", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tun" +version = "0.6.1" +source = "git+https://github.com/LeeSmet/rust-tun#ae4c222e7a7aba5752cb08d2dbaf67bbc669c26a" +dependencies = [ + "byteorder", + "bytes", + "futures-core", + "ioctl-sys", + "libc", + "log", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "wintun 0.3.2", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + +[[package]] +name = "wintun" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b83b0eca06dd125dbcd48a45327c708a6da8aada3d95a3f06db0ce4b17e0d4" +dependencies = [ + "c2rust-bitfields", + "libloading", + "log", + "thiserror 1.0.69", + "windows 0.51.1", +] + +[[package]] +name = "wintun" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da99be64b5aa3de869c16977994314d0759a698d9a73ab0a5b1d52e2282033ae" +dependencies = [ + "c2rust-bitfields", + "libloading", + "log", + "thiserror 1.0.69", + "windows-sys 0.52.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "yaml-rust2" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232bdb534d65520716bef0bbb205ff8f2db72d807b19c0bc3020853b92a0cd4b" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] diff --git a/components/mycelium/myceliumd-private/Cargo.toml b/components/mycelium/myceliumd-private/Cargo.toml new file mode 100644 index 0000000..e03f8b0 --- /dev/null +++ b/components/mycelium/myceliumd-private/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "myceliumd-private" +version = "0.6.1" +edition = "2021" +license-file = "../LICENSE" +readme = "./README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +vendored-openssl = ["mycelium/vendored-openssl"] + +[[bin]] +name = "mycelium-private" +path = "src/main.rs" + +[dependencies] +clap = { version = "4.5.41", features = ["derive"] } +tracing = { version = "0.1.41", features = ["release_max_level_debug"] } +tracing-logfmt = { version = "0.3.5", features = ["ansi_logs"] } +tracing-subscriber = { version = "0.3.19", features = [ + "env-filter", + "nu-ansi-term", +] } +mycelium = { path = "../mycelium", features = ["private-network", "message"] } +mycelium-metrics = { path = "../mycelium-metrics", features = ["prometheus"] } +mycelium-api = { path = "../mycelium-api", features = ["message"] } +mycelium-cli = { path = "../mycelium-cli/", features = ["message"] } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +tokio = { version = "1.46.1", features = [ + "macros", + "rt-multi-thread", + "signal", +] } +reqwest = { version = "0.12.9", default-features = false, features = ["json"] } +base64 = "0.22.1" +config = "0.15.13" +dirs = "6.0.0" +toml = "0.9.2" diff --git a/components/mycelium/myceliumd-private/README.md b/components/mycelium/myceliumd-private/README.md new file mode 100644 index 0000000..d07d04f --- /dev/null +++ b/components/mycelium/myceliumd-private/README.md @@ -0,0 +1,26 @@ +# Myceliumd-private + +This is the main binary to use when connecting to [a private network](../docs/private_network.md). +You can either get a release from [the GitHub release page](https://github.com/threefoldtech/myceliumd/releases), +or build the code yourself here. While we intend to keep the master branch as stable, +i.e. a build from latest master should succeed, this is not a hard guarantee. Additionally, +the master branch might not be compatible with the latest release, in case of a +breaking change. + +Building, with the rust toolchain installed, can be done by running `cargo build` +in this directory. Since we rely on `openssl` for the private network functionality, +that will also need to be installed. The produced binary will be dynamically linked +with this system installation. Alternatively, you can use the `vendored-openssl` +feature to compile `openssl` from source and statically link it. You won't need +to have `openssl` installed, though building will of course take longer. To stay +in line with the regular `mycelium` binary, the produced binary here is renamed +to `mycelium-private`. Optionally, the `--release` flag can be used when building. +This is recommended if you are not just developing. + +While this binary can currently be used to connect to the public network, it is +not guaranteed that this will always be the case. If you intend to connect to the +public network, consider using [`the mycelium binary`](../myceliumd/README.md) +instead. + +For more information about the project, please refer to [the README file in the +repository root](../README.md). diff --git a/components/mycelium/myceliumd-private/src/main.rs b/components/mycelium/myceliumd-private/src/main.rs new file mode 100644 index 0000000..6af5197 --- /dev/null +++ b/components/mycelium/myceliumd-private/src/main.rs @@ -0,0 +1,840 @@ +use std::io::{self, Read}; +use std::net::Ipv4Addr; +use std::path::Path; +use std::sync::Arc; +use std::{ + error::Error, + net::{IpAddr, SocketAddr}, + path::PathBuf, +}; +use std::{fmt::Display, str::FromStr}; + +use clap::{Args, Parser, Subcommand}; +use mycelium::message::TopicConfig; +use serde::{Deserialize, Deserializer}; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +#[cfg(target_family = "unix")] +use tokio::signal::{self, unix::SignalKind}; +use tokio::sync::Mutex; +use tracing::{debug, error, info, warn}; + +use crypto::PublicKey; +use mycelium::endpoint::Endpoint; +use mycelium::{crypto, Node}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; + +/// The default port on the underlay to listen on for incoming TCP connections. +const DEFAULT_TCP_LISTEN_PORT: u16 = 9651; +/// The default port on the underlay to listen on for incoming Quic connections. +const DEFAULT_QUIC_LISTEN_PORT: u16 = 9651; +/// The default port to use for IPv6 link local peer discovery (UDP). +const DEFAULT_PEER_DISCOVERY_PORT: u16 = 9650; +/// The default listening address for the HTTP API. +const DEFAULT_HTTP_API_SERVER_ADDRESS: SocketAddr = + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8989); +/// The default listening address for the JSON-RPC API. +const DEFAULT_JSONRPC_API_SERVER_ADDRESS: SocketAddr = + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8990); + +const DEFAULT_KEY_FILE: &str = "priv_key.bin"; + +/// Default name of tun interface +#[cfg(not(target_os = "macos"))] +const TUN_NAME: &str = "mycelium"; +/// Default name of tun interface +#[cfg(target_os = "macos")] +const TUN_NAME: &str = "utun0"; + +/// The logging formats that can be selected. +#[derive(Clone, PartialEq, Eq)] +enum LoggingFormat { + Compact, + Logfmt, + /// Same as Logfmt but with color statically disabled + Plain, +} + +impl Display for LoggingFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + LoggingFormat::Compact => "compact", + LoggingFormat::Logfmt => "logfmt", + LoggingFormat::Plain => "plain", + } + ) + } +} + +impl FromStr for LoggingFormat { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + "compact" => LoggingFormat::Compact, + "logfmt" => LoggingFormat::Logfmt, + "plain" => LoggingFormat::Plain, + _ => return Err("invalid logging format"), + }) + } +} + +#[derive(Parser)] +#[command(version)] +struct Cli { + /// Path to the private key file. This will be created if it does not exist. Default + /// [priv_key.bin]. + #[arg(short = 'k', long = "key-file", global = true)] + key_file: Option, + + // Configuration file + #[arg(short = 'c', long = "config-file", global = true)] + config_file: Option, + + /// Enable debug logging. Does nothing if `--silent` is set. + #[arg(short = 'd', long = "debug", default_value_t = false)] + debug: bool, + + /// Disable all logs except error logs. + #[arg(long = "silent", default_value_t = false)] + silent: bool, + + /// The logging format to use. `logfmt` and `compact` is supported. + #[arg(long = "log-format", default_value_t = LoggingFormat::Compact)] + logging_format: LoggingFormat, + + #[clap(flatten)] + node_args: NodeArguments, + + #[command(subcommand)] + command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + /// Inspect a public key provided in hex format, or export the local public key if no key is + /// given. + Inspect { + /// Output in json format. + #[arg(long = "json")] + json: bool, + + /// The key to inspect. + key: Option, + }, + + /// Actions on the message subsystem + Message { + #[command(subcommand)] + command: MessageCommand, + }, + + /// Actions related to peers (list, remove, add) + Peers { + #[command(subcommand)] + command: PeersCommand, + }, + + /// Actions related to routes (selected, fallback) + Routes { + #[command(subcommand)] + command: RoutesCommand, + }, +} + +#[derive(Debug, Subcommand)] +pub enum MessageCommand { + Send { + /// Wait for a reply from the receiver. + #[arg(short = 'w', long = "wait", default_value_t = false)] + wait: bool, + /// An optional timeout to wait for. This does nothing if the `--wait` flag is not set. If + /// `--wait` is set and this flag isn't, wait forever for a reply. + #[arg(long = "timeout")] + timeout: Option, + /// Optional topic of the message. Receivers can filter on this to only receive messages + /// for a chosen topic. + #[arg(short = 't', long = "topic")] + topic: Option, + /// Optional file to use as message body. + #[arg(long = "msg-path")] + msg_path: Option, + /// Optional message ID to reply to. + #[arg(long = "reply-to")] + reply_to: Option, + /// Destination of the message, either a hex encoded public key, or an IPv6 address in the + /// 400::/7 range. + destination: String, + /// The message to send. This is required if `--msg_path` is not set + message: Option, + }, + Receive { + /// An optional timeout to wait for a message. If this is not set, wait forever. + #[arg(long = "timeout")] + timeout: Option, + /// Optional topic of the message. Only messages with this topic will be received by this + /// command. + #[arg(short = 't', long = "topic")] + topic: Option, + /// Optional file in which the message body will be saved. + #[arg(long = "msg-path")] + msg_path: Option, + /// Don't print the metadata + #[arg(long = "raw")] + raw: bool, + }, +} + +#[derive(Debug, Subcommand)] +pub enum PeersCommand { + /// List the connected peers + List { + /// Print the peers list in JSON format + #[arg(long = "json", default_value_t = false)] + json: bool, + }, + /// Add peer(s) + Add { peers: Vec }, + /// Remove peer(s) + Remove { peers: Vec }, +} + +#[derive(Debug, Subcommand)] +pub enum RoutesCommand { + /// Print all selected routes + Selected { + /// Print selected routes in JSON format + #[arg(long = "json", default_value_t = false)] + json: bool, + }, + /// Print all fallback routes + Fallback { + /// Print fallback routes in JSON format + #[arg(long = "json", default_value_t = false)] + json: bool, + }, + /// Print the currently queried subnets + Queried { + /// Print queried subnets in JSON format + #[arg(long = "json", default_value_t = false)] + json: bool, + }, + /// Print all subnets which are explicitly marked as not having a route + NoRoute { + /// Print subnets in JSON format + #[arg(long = "json", default_value_t = false)] + json: bool, + }, +} + +#[derive(Debug, Args)] +pub struct NodeArguments { + /// Peers to connect to. + #[arg(long = "peers", num_args = 1..)] + static_peers: Vec, + + /// Port to listen on for tcp connections. + #[arg(short = 't', long = "tcp-listen-port", default_value_t = DEFAULT_TCP_LISTEN_PORT)] + tcp_listen_port: u16, + + /// Disable quic protocol for connecting to peers + #[arg(long = "disable-quic", default_value_t = false)] + disable_quic: bool, + + /// Port to listen on for quic connections. + #[arg(short = 'q', long = "quic-listen-port", default_value_t = DEFAULT_QUIC_LISTEN_PORT)] + quic_listen_port: u16, + + /// Port to use for link local peer discovery. This uses the UDP protocol. + #[arg(long = "peer-discovery-port", default_value_t = DEFAULT_PEER_DISCOVERY_PORT)] + peer_discovery_port: u16, + + /// Disable peer discovery. + /// + /// If this flag is passed, the automatic link local peer discovery will not be enabled, and + /// peers must be configured manually. If this is disabled on all local peers, communication + /// between them will go over configured external peers. + #[arg(long = "disable-peer-discovery", default_value_t = false)] + disable_peer_discovery: bool, + + /// Address of the HTTP API server. + #[arg(long = "api-addr", default_value_t = DEFAULT_HTTP_API_SERVER_ADDRESS)] + api_addr: SocketAddr, + + /// Address of the JSON-RPC API server. + #[arg(long = "jsonrpc-addr", default_value_t = DEFAULT_JSONRPC_API_SERVER_ADDRESS)] + jsonrpc_addr: SocketAddr, + + /// Run without creating a TUN interface. + /// + /// The system will participate in the network as usual, but won't be able to send out L3 + /// packets. Inbound L3 traffic will be silently discarded. The message subsystem will still + /// work however. + #[arg(long = "no-tun", default_value_t = false)] + no_tun: bool, + + /// Name to use for the TUN interface, if one is created. + /// + /// Setting this only matters if a TUN interface is actually created, i.e. if the `--no-tun` + /// flag is **not** set. The name set here must be valid for the current platform, e.g. on OSX, + /// the name must start with `utun` and be followed by digits. + #[arg(long = "tun-name")] + tun_name: Option, + + /// Enable a private network, with this name. + /// + /// If this flag is set, the system will run in "private network mode", and use Tls connections + /// instead of plain Tcp connections. The name provided here is used as the network name, other + /// nodes must use the same name or the connection will be rejected. Note that the name is + /// public, and is communicated when connecting to a remote. Do not put confidential data here. + #[arg(long = "network-name", requires = "network_key_file")] + network_name: Option, + + /// The path to the file with the key to use for the private network. + /// + /// The key is expected to be exactly 32 bytes. The key must be shared between all nodes + /// participating in the network, and is secret. If the key leaks, anyone can then join the + /// network. + #[arg(long = "network-key-file", requires = "network_name")] + network_key_file: Option, + + /// The address on which to expose prometheus metrics, if desired. + /// + /// Setting this flag will attempt to start an HTTP server on the provided address, to serve + /// prometheus metrics on the /metrics endpoint. If this flag is not set, metrics are also not + /// collected. + #[arg(long = "metrics-api-address")] + metrics_api_address: Option, + + /// The firewall mark to set on the mycelium sockets. + /// + /// This allows to identify packets that contain encapsulated mycelium packets so that + /// different routing policies can be applied to them. + /// This option only has an effect on Linux. + #[arg(long = "firewall-mark")] + firewall_mark: Option, + + /// The amount of worker tasks to spawn to handle updates. + /// + /// By default, updates are processed on a single task only. This is sufficient for most use + /// cases. In case you notice that the node can't keep up with the incoming updates (typically + /// because you are running a public node with a lot of connections), this value can be + /// increased to process updates in parallel. + #[arg(long = "update-workers", default_value_t = 1)] + update_workers: usize, + + /// The topic configuration. + /// + /// A .toml file containing topic configuration. This is a default action in case the topic is + /// not listed, and an explicit whitelist for allowed subnets/ips which are otherwise allowed + /// to use a topic. + #[arg(long = "topic-config")] + topic_config: Option, + + /// The cache directory for the mycelium CDN module + /// + /// This directory will be used to cache reconstructed content blocks which were loaded through + /// the CDN functionallity for faster access next time. + #[arg(long = "cdn-cache")] + cdn_cache: Option, +} + +#[derive(Debug, Deserialize)] +pub struct MergedNodeConfig { + peers: Vec, + tcp_listen_port: u16, + disable_quic: bool, + quic_listen_port: u16, + peer_discovery_port: u16, + disable_peer_discovery: bool, + api_addr: SocketAddr, + jsonrpc_addr: SocketAddr, + no_tun: bool, + tun_name: String, + metrics_api_address: Option, + network_key_file: Option, + network_name: Option, + firewall_mark: Option, + update_workers: usize, + topic_config: Option, + cdn_cache: Option, +} + +#[derive(Debug, Deserialize, Default)] +struct MyceliumConfig { + #[serde(deserialize_with = "deserialize_optional_endpoint_str_from_toml")] + peers: Option>, + tcp_listen_port: Option, + disable_quic: Option, + quic_listen_port: Option, + no_tun: Option, + tun_name: Option, + disable_peer_discovery: Option, + peer_discovery_port: Option, + api_addr: Option, + jsonrpc_addr: Option, + metrics_api_address: Option, + network_name: Option, + network_key_file: Option, + firewall_mark: Option, + update_workers: Option, + topic_config: Option, + cdn_cache: Option, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + // Init default configuration + let mut mycelium_config = MyceliumConfig::default(); + + // Load configuration file + if let Some(config_file_path) = &cli.config_file { + if Path::new(config_file_path).exists() { + let config = config::Config::builder() + .add_source(config::File::new( + config_file_path.to_str().unwrap(), + config::FileFormat::Toml, + )) + .build()?; + + mycelium_config = config.try_deserialize()?; + } else { + let error_msg = format!("Config file {config_file_path:?} not found"); + return Err(io::Error::new(io::ErrorKind::NotFound, error_msg).into()); + } + } else if let Some(mut conf) = dirs::config_dir() { + // Windows: %APPDATA%/ThreeFold Tech/Mycelium/mycelium.conf + #[cfg(target_os = "windows")] + { + conf = conf + .join("ThreeFold Tech") + .join("Mycelium") + .join("mycelium.toml") + }; + // Linux: $HOME/.config/mycelium/mycelium.conf + #[allow(clippy::unnecessary_operation)] + #[cfg(target_os = "linux")] + { + conf = conf.join("mycelium").join("mycelium.toml") + }; + // MacOS: $HOME/Library/Application Support/ThreeFold Tech/Mycelium/mycelium.conf + #[cfg(target_os = "macos")] + { + conf = conf + .join("ThreeFold Tech") + .join("Mycelium") + .join("mycelium.toml") + }; + + if conf.exists() { + info!( + conf_dir = conf.to_str().unwrap(), + "Mycelium is starting with configuration file", + ); + let config = config::Config::builder() + .add_source(config::File::new( + conf.to_str().unwrap(), + config::FileFormat::Toml, + )) + .build()?; + mycelium_config = config.try_deserialize()?; + } + } + + let level = if cli.silent { + tracing::Level::ERROR + } else if cli.debug { + tracing::Level::DEBUG + } else { + tracing::Level::INFO + }; + + tracing_subscriber::registry() + .with( + EnvFilter::builder() + .with_default_directive(level.into()) + .from_env() + .expect("invalid RUST_LOG"), + ) + .with( + (cli.logging_format == LoggingFormat::Compact) + .then(|| tracing_subscriber::fmt::Layer::new().compact()), + ) + .with((cli.logging_format == LoggingFormat::Logfmt).then(tracing_logfmt::layer)) + .with((cli.logging_format == LoggingFormat::Plain).then(|| { + tracing_logfmt::builder() + // Explicitly force color off + .with_ansi_color(false) + .layer() + })) + .init(); + + let key_path = cli + .key_file + .unwrap_or_else(|| PathBuf::from(DEFAULT_KEY_FILE)); + + match cli.command { + None => { + let merged_config = merge_config(cli.node_args, mycelium_config); + let topic_config = merged_config.topic_config.as_ref().and_then(|path| { + let mut content = String::new(); + let mut file = std::fs::File::open(path).ok()?; + file.read_to_string(&mut content).ok()?; + toml::from_str::(&content).ok() + }); + + if topic_config.is_some() { + info!(path = ?merged_config.topic_config, "Loaded topic cofig"); + } + + let private_network_config = + match (merged_config.network_name, merged_config.network_key_file) { + (Some(network_name), Some(network_key_file)) => { + let net_key = load_key_file(&network_key_file).await?; + + Some((network_name, net_key)) + } + _ => None, + }; + + let node_keys = get_node_keys(&key_path).await?; + let node_secret_key = if let Some((node_secret_key, _)) = node_keys { + node_secret_key + } else { + warn!("Node key file {key_path:?} not found, generating new keys"); + let secret_key = crypto::SecretKey::new(); + save_key_file(&secret_key, &key_path).await?; + secret_key + }; + + let _api = if let Some(metrics_api_addr) = merged_config.metrics_api_address { + let metrics = mycelium_metrics::PrometheusExporter::new(); + let config = mycelium::Config { + node_key: node_secret_key, + peers: merged_config.peers, + no_tun: merged_config.no_tun, + tcp_listen_port: merged_config.tcp_listen_port, + quic_listen_port: if merged_config.disable_quic { + None + } else { + Some(merged_config.quic_listen_port) + }, + peer_discovery_port: if merged_config.disable_peer_discovery { + None + } else { + Some(merged_config.peer_discovery_port) + }, + tun_name: merged_config.tun_name, + private_network_config, + metrics: metrics.clone(), + firewall_mark: merged_config.firewall_mark, + update_workers: merged_config.update_workers, + topic_config, + cdn_cache: merged_config.cdn_cache, + }; + metrics.spawn(metrics_api_addr); + + let node = Arc::new(Mutex::new(Node::new(config).await?)); + let http_api = mycelium_api::Http::spawn(node.clone(), merged_config.api_addr); + + // Initialize the JSON-RPC server + let rpc_api = + mycelium_api::rpc::JsonRpc::spawn(node, merged_config.jsonrpc_addr).await; + + (http_api, rpc_api) + } else { + let config = mycelium::Config { + node_key: node_secret_key, + peers: merged_config.peers, + no_tun: merged_config.no_tun, + tcp_listen_port: merged_config.tcp_listen_port, + quic_listen_port: if merged_config.disable_quic { + None + } else { + Some(merged_config.quic_listen_port) + }, + peer_discovery_port: if merged_config.disable_peer_discovery { + None + } else { + Some(merged_config.peer_discovery_port) + }, + tun_name: merged_config.tun_name, + private_network_config, + metrics: mycelium_metrics::NoMetrics, + firewall_mark: merged_config.firewall_mark, + update_workers: merged_config.update_workers, + topic_config, + cdn_cache: merged_config.cdn_cache, + }; + + let node = Arc::new(Mutex::new(Node::new(config).await?)); + let http_api = mycelium_api::Http::spawn(node.clone(), merged_config.api_addr); + + // Initialize the JSON-RPC server + let rpc_api = + mycelium_api::rpc::JsonRpc::spawn(node, merged_config.jsonrpc_addr).await; + + (http_api, rpc_api) + }; + + // TODO: put in dedicated file so we can only rely on certain signals on unix platforms + #[cfg(target_family = "unix")] + { + let mut sigint = signal::unix::signal(SignalKind::interrupt()) + .expect("Can install SIGINT handler"); + let mut sigterm = signal::unix::signal(SignalKind::terminate()) + .expect("Can install SIGTERM handler"); + + tokio::select! { + _ = sigint.recv() => { } + _ = sigterm.recv() => { } + } + } + #[cfg(not(target_family = "unix"))] + { + if let Err(e) = tokio::signal::ctrl_c().await { + error!("Failed to wait for SIGINT: {e}"); + } + } + } + Some(cmd) => match cmd { + Command::Inspect { json, key } => { + let node_keys = get_node_keys(&key_path).await?; + let key = if let Some(key) = key { + PublicKey::try_from(key.as_str())? + } else if let Some((_, node_pub_key)) = node_keys { + node_pub_key + } else { + error!("No key to inspect provided and no key found at {key_path:?}"); + return Err(io::Error::new( + io::ErrorKind::NotFound, + "no key to inspect and key file not found", + ) + .into()); + }; + mycelium_cli::inspect(key, json)?; + + return Ok(()); + } + Command::Message { command } => match command { + MessageCommand::Send { + wait, + timeout, + topic, + msg_path, + reply_to, + destination, + message, + } => { + return mycelium_cli::send_msg( + destination, + message, + wait, + timeout, + reply_to, + topic, + msg_path, + cli.node_args.api_addr, + ) + .await + } + MessageCommand::Receive { + timeout, + topic, + msg_path, + raw, + } => { + return mycelium_cli::recv_msg( + timeout, + topic, + msg_path, + raw, + cli.node_args.api_addr, + ) + .await + } + }, + Command::Peers { command } => match command { + PeersCommand::List { json } => { + return mycelium_cli::list_peers(cli.node_args.api_addr, json).await; + } + PeersCommand::Add { peers } => { + return mycelium_cli::add_peers(cli.node_args.api_addr, peers).await; + } + PeersCommand::Remove { peers } => { + return mycelium_cli::remove_peers(cli.node_args.api_addr, peers).await; + } + }, + Command::Routes { command } => match command { + RoutesCommand::Selected { json } => { + return mycelium_cli::list_selected_routes(cli.node_args.api_addr, json).await; + } + RoutesCommand::Fallback { json } => { + return mycelium_cli::list_fallback_routes(cli.node_args.api_addr, json).await; + } + RoutesCommand::Queried { json } => { + return mycelium_cli::list_queried_subnets(cli.node_args.api_addr, json).await; + } + RoutesCommand::NoRoute { json } => { + return mycelium_cli::list_no_route_entries(cli.node_args.api_addr, json).await; + } + }, + }, + } + + Ok(()) +} + +async fn get_node_keys( + key_path: &PathBuf, +) -> Result, io::Error> { + if key_path.exists() { + let sk = load_key_file(key_path).await?; + let pk = crypto::PublicKey::from(&sk); + debug!("Loaded key file at {key_path:?}"); + Ok(Some((sk, pk))) + } else { + Ok(None) + } +} + +async fn load_key_file(path: &Path) -> Result +where + T: From<[u8; 32]>, +{ + let mut file = File::open(path).await?; + let mut secret_bytes = [0u8; 32]; + file.read_exact(&mut secret_bytes).await?; + + Ok(T::from(secret_bytes)) +} + +async fn save_key_file(key: &crypto::SecretKey, path: &Path) -> io::Result<()> { + #[cfg(target_family = "unix")] + { + use tokio::fs::OpenOptions; + + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .mode(0o600) // rw by the owner, not readable by group or others + .open(path) + .await?; + file.write_all(key.as_bytes()).await?; + } + #[cfg(not(target_family = "unix"))] + { + let mut file = File::create(path).await?; + file.write_all(key.as_bytes()).await?; + } + + Ok(()) +} + +fn merge_config(cli_args: NodeArguments, file_config: MyceliumConfig) -> MergedNodeConfig { + MergedNodeConfig { + peers: if !cli_args.static_peers.is_empty() { + cli_args.static_peers + } else { + file_config.peers.unwrap_or_default() + }, + tcp_listen_port: if cli_args.tcp_listen_port != DEFAULT_TCP_LISTEN_PORT { + cli_args.tcp_listen_port + } else { + file_config + .tcp_listen_port + .unwrap_or(DEFAULT_TCP_LISTEN_PORT) + }, + disable_quic: cli_args.disable_quic || file_config.disable_quic.unwrap_or(false), + quic_listen_port: if cli_args.quic_listen_port != DEFAULT_QUIC_LISTEN_PORT { + cli_args.quic_listen_port + } else { + file_config + .quic_listen_port + .unwrap_or(DEFAULT_QUIC_LISTEN_PORT) + }, + peer_discovery_port: if cli_args.peer_discovery_port != DEFAULT_PEER_DISCOVERY_PORT { + cli_args.peer_discovery_port + } else { + file_config + .peer_discovery_port + .unwrap_or(DEFAULT_PEER_DISCOVERY_PORT) + }, + disable_peer_discovery: cli_args.disable_peer_discovery + || file_config.disable_peer_discovery.unwrap_or(false), + api_addr: if cli_args.api_addr != DEFAULT_HTTP_API_SERVER_ADDRESS { + cli_args.api_addr + } else { + file_config + .api_addr + .unwrap_or(DEFAULT_HTTP_API_SERVER_ADDRESS) + }, + jsonrpc_addr: if cli_args.jsonrpc_addr != DEFAULT_JSONRPC_API_SERVER_ADDRESS { + cli_args.jsonrpc_addr + } else { + file_config + .jsonrpc_addr + .unwrap_or(DEFAULT_JSONRPC_API_SERVER_ADDRESS) + }, + no_tun: cli_args.no_tun || file_config.no_tun.unwrap_or(false), + tun_name: if let Some(tun_name_cli) = cli_args.tun_name { + tun_name_cli + } else if let Some(tun_name_config) = file_config.tun_name { + tun_name_config + } else { + TUN_NAME.to_string() + }, + metrics_api_address: cli_args + .metrics_api_address + .or(file_config.metrics_api_address), + network_name: cli_args.network_name.or(file_config.network_name), + network_key_file: cli_args.network_key_file.or(file_config.network_key_file), + firewall_mark: cli_args.firewall_mark.or(file_config.firewall_mark), + update_workers: if cli_args.update_workers != 1 { + cli_args.update_workers + } else { + file_config.update_workers.unwrap_or(1) + }, + topic_config: cli_args.topic_config.or(file_config.topic_config), + cdn_cache: cli_args.cdn_cache.or(file_config.cdn_cache), + } +} + +/// Deserialize an optional list of endpoints from TOML format. The endpoints can be provided +/// either as a list `[...]`, or in case there is only 1 endpoint, it can also be provided as a +/// single string element. If no value is provided, it returns None. +fn deserialize_optional_endpoint_str_from_toml<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrVec { + String(String), + Vec(Vec), + } + + Ok(match Option::::deserialize(deserializer)? { + Some(StringOrVec::Vec(v)) => Some( + v.into_iter() + .map(|s| { + ::from_str(&s).map_err(serde::de::Error::custom) + }) + .collect::, _>>()?, + ), + Some(StringOrVec::String(s)) => Some(vec![ + ::from_str(&s).map_err(serde::de::Error::custom)? + ]), + None => None, + }) +} diff --git a/components/mycelium/myceliumd/.gitignore b/components/mycelium/myceliumd/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/components/mycelium/myceliumd/.gitignore @@ -0,0 +1 @@ +/target diff --git a/components/mycelium/myceliumd/Cargo.lock b/components/mycelium/myceliumd/Cargo.lock new file mode 100644 index 0000000..31e1699 --- /dev/null +++ b/components/mycelium/myceliumd/Cargo.lock @@ -0,0 +1,4419 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byte-unit" +version = "5.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +dependencies = [ + "rust_decimal", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "c2rust-bitfields" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b43c3f07ab0ef604fa6f595aa46ec2f8a22172c975e186f6f5bf9829a3b72c41" +dependencies = [ + "c2rust-bitfields-derive", +] + +[[package]] +name = "c2rust-bitfields-derive" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3cbc102e2597c9744c8bd8c15915d554300601c91a079430d309816b0912545" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] + +[[package]] +name = "cdn-meta" +version = "0.1.0" +source = "git+https://github.com/threefoldtech/mycelium-cdn-registry#0030e6523e7152d27d095307f7fea5332999de2c" +dependencies = [ + "aes-gcm", + "bincode", + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "config" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1eb4fb07bc7f012422df02766c7bd5971effb894f573865642f06fa3265440" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "etherparse" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff83a5facf1a7cbfef93cfb48d6d4fb6a1f42d8ac2341a96b3255acb4d4f860" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.2", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.10", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", +] + +[[package]] +name = "ioctl-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd11f3a29434026f5ff98c730b668ba74b1033637b8817940b54d040696133c" + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonrpsee" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fba77a59c4c644fd48732367624d1bcf6f409f9c9a286fbc71d2f1fc0b2ea16" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-proc-macros", + "jsonrpsee-server", + "jsonrpsee-types", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693c93cbb7db25f4108ed121304b671a36002c2db67dff2ee4391a688c738547" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "parking_lot 0.12.3", + "pin-project", + "rand 0.9.1", + "rustc-hash", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fa4f5daed39f982a1bb9d15449a28347490ad42b212f8eaa2a2a344a0dce9e9" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38b0bcf407ac68d241f90e2d46041e6a06988f97fe1721fb80b91c42584fae6" +dependencies = [ + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66df7256371c45621b3b7d2fb23aea923d577616b9c0e9c0b950a6ea5c2be0ca" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "left-right" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabfddf3ad712b726484562039aa6fc2014bc1b5c088bb211b208052cf0439e6" +dependencies = [ + "loom", + "slab", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "mycelium" +version = "0.6.1" +dependencies = [ + "aes-gcm", + "ahash 0.8.11", + "arc-swap", + "axum", + "axum-extra", + "blake3", + "bytes", + "cdn-meta", + "dashmap", + "etherparse", + "faster-hex", + "futures", + "ip_network_table-deps-treebitmap", + "ipnet", + "left-right", + "libc", + "netdev", + "nix 0.29.0", + "nix 0.30.1", + "quinn", + "rand 0.9.1", + "rcgen", + "redis", + "reed-solomon-erasure", + "reqwest", + "rtnetlink", + "rustls", + "serde", + "tokio", + "tokio-stream", + "tokio-tun", + "tokio-util", + "tracing", + "tracing-logfmt", + "tracing-subscriber", + "tun", + "wintun 0.5.1", + "x25519-dalek", +] + +[[package]] +name = "mycelium-api" +version = "0.6.1" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "jsonrpsee", + "mycelium", + "mycelium-metrics", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "mycelium-cli" +version = "0.6.1" +dependencies = [ + "base64 0.22.1", + "byte-unit", + "mycelium", + "mycelium-api", + "prettytable-rs", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "urlencoding", +] + +[[package]] +name = "mycelium-metrics" +version = "0.6.1" +dependencies = [ + "axum", + "mycelium", + "prometheus", + "tokio", + "tracing", +] + +[[package]] +name = "myceliumd" +version = "0.6.1" +dependencies = [ + "base64 0.22.1", + "byte-unit", + "clap", + "config", + "dirs", + "mycelium", + "mycelium-api", + "mycelium-cli", + "mycelium-metrics", + "prettytable-rs", + "reqwest", + "serde", + "serde_json", + "tokio", + "toml", + "tracing", + "tracing-logfmt", + "tracing-subscriber", + "urlencoding", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "netdev" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862209dce034f82a44c95ce2b5183730d616f2a68746b9c1959aa2572e77c0a1" +dependencies = [ + "dlopen2", + "ipnet", + "libc", + "netlink-packet-core", + "netlink-packet-route 0.22.0", + "netlink-sys", + "once_cell", + "system-configuration", + "windows-sys 0.59.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d83370a96813d7c977f8b63054f1162df6e5784f1c598d689236564fb5a6f2" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-proto" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.12", +] + +[[package]] +name = "netlink-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" +dependencies = [ + "bytes", + "futures", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.10", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "pest_meta" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.23", +] + +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" +dependencies = [ + "bitflags 2.9.0", + "hex", + "procfs-core", + "rustix 0.38.44", +] + +[[package]] +name = "procfs-core" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +dependencies = [ + "bitflags 2.9.0", + "hex", +] + +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "libc", + "memchr", + "parking_lot 0.12.3", + "procfs", + "thiserror 2.0.12", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.1", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.1", +] + +[[package]] +name = "rcgen" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49bc8ffa8a832eb1d7c8000337f8b0d2f4f2f5ec3cf4ddc26f125e3ad2451824" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "redis" +version = "0.32.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f66bf4cac9733a23bcdf1e0e01effbaaad208567beba68be8f67e5f4af3ee1" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.6.0", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.12", +] + +[[package]] +name = "reed-solomon-erasure" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7263373d500d4d4f505d43a2a662d475a894aa94503a1ee28e9188b5f3960d4f" +dependencies = [ + "libm", + "lru", + "parking_lot 0.11.2", + "smallvec", + "spin", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.9.0", + "serde", + "serde_derive", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rtnetlink" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbe0a03f6b9b483c67d4b328fc5d66c8db0b6aa274e0fa2def71b5e442a69acf" +dependencies = [ + "futures", + "log", + "netlink-packet-core", + "netlink-packet-route 0.24.0", + "netlink-packet-utils", + "netlink-proto", + "netlink-sys", + "nix 0.29.0", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +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 = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.59.0", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" + +[[package]] +name = "time-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.5.10", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tun" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be25a316a2d10d0ddd46de9ddd73cdb4262678e1e52f4a32a31421f1e2b816b4" +dependencies = [ + "libc", + "nix 0.29.0", + "thiserror 2.0.12", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "toml_datetime 0.6.9", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-logfmt" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1f47d22deb79c3f59fcf2a1f00f60cbdc05462bf17d1cd356c1fefa3f444bd" +dependencies = [ + "nu-ansi-term 0.50.1", + "time", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term 0.46.0", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tun" +version = "0.6.1" +source = "git+https://github.com/LeeSmet/rust-tun#ae4c222e7a7aba5752cb08d2dbaf67bbc669c26a" +dependencies = [ + "byteorder", + "bytes", + "futures-core", + "ioctl-sys", + "libc", + "log", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "wintun 0.3.2", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + +[[package]] +name = "wintun" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b83b0eca06dd125dbcd48a45327c708a6da8aada3d95a3f06db0ce4b17e0d4" +dependencies = [ + "c2rust-bitfields", + "libloading", + "log", + "thiserror 1.0.69", + "windows 0.51.1", +] + +[[package]] +name = "wintun" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da99be64b5aa3de869c16977994314d0759a698d9a73ab0a5b1d52e2282033ae" +dependencies = [ + "c2rust-bitfields", + "libloading", + "log", + "thiserror 1.0.69", + "windows-sys 0.52.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "yaml-rust2" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232bdb534d65520716bef0bbb205ff8f2db72d807b19c0bc3020853b92a0cd4b" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive 0.8.23", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] diff --git a/components/mycelium/myceliumd/Cargo.toml b/components/mycelium/myceliumd/Cargo.toml new file mode 100644 index 0000000..96f3b50 --- /dev/null +++ b/components/mycelium/myceliumd/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "myceliumd" +version = "0.6.1" +edition = "2021" +license-file = "../LICENSE" +readme = "./README.md" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "mycelium" +path = "src/main.rs" + +[dependencies] +clap = { version = "4.5.41", features = ["derive"] } +tracing = { version = "0.1.41", features = ["release_max_level_debug"] } +tracing-logfmt = { version = "0.3.5", features = ["ansi_logs"] } +tracing-subscriber = { version = "0.3.19", features = [ + "env-filter", + "nu-ansi-term", +] } +mycelium = { path = "../mycelium", features = ["message"] } +mycelium-metrics = { path = "../mycelium-metrics", features = ["prometheus"] } +mycelium-cli = { path = "../mycelium-cli/", features = ["message"] } +mycelium-api = { path = "../mycelium-api", features = ["message"] } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +tokio = { version = "1.46.1", features = [ + "macros", + "rt-multi-thread", + "signal", +] } +reqwest = { version = "0.12.9", default-features = false, features = ["json"] } +base64 = "0.22.1" +prettytable-rs = "0.10.0" +urlencoding = "2.1.3" +byte-unit = "5.1.6" +config = "0.15.13" +dirs = "6.0.0" +toml = "0.9.2" diff --git a/components/mycelium/myceliumd/README.md b/components/mycelium/myceliumd/README.md new file mode 100644 index 0000000..8398adc --- /dev/null +++ b/components/mycelium/myceliumd/README.md @@ -0,0 +1,16 @@ +# Myceliumd + +This is the main binary to use for joining a/the public [`mycelium`] network. You can +either get the latest release from [the GitHub release page](https://github.com/threefoldtech/myceliumd/releases/latest), +or build the code yourself here. While we intend to keep the master branch as stable, +i.e. a build from latest master should succeed, this is not a hard guarantee. Additionally, +the master branch might not be compatible with the latest release, in case of a +breaking change. + +Building, with the rust toolchain installed, can be done by running `cargo build` +in this directory. For compatibility reasons, the binary is renamed to `mycelium`. +Optionally, the `--release` flag can be used when building. This is recommended +if you are not just developing. + +For more information about the project, please refer to [the README file in the +repository root](../README.md). diff --git a/components/mycelium/myceliumd/src/main.rs b/components/mycelium/myceliumd/src/main.rs new file mode 100644 index 0000000..cbae536 --- /dev/null +++ b/components/mycelium/myceliumd/src/main.rs @@ -0,0 +1,805 @@ +use std::io::{self, Read}; +use std::net::Ipv4Addr; +use std::path::Path; +use std::sync::Arc; +use std::{ + error::Error, + net::{IpAddr, SocketAddr}, + path::PathBuf, +}; +use std::{fmt::Display, str::FromStr}; + +use clap::{Args, Parser, Subcommand}; +use mycelium::message::TopicConfig; +use serde::{Deserialize, Deserializer}; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +#[cfg(target_family = "unix")] +use tokio::signal::{self, unix::SignalKind}; +use tokio::sync::Mutex; +use tracing::{debug, error, info, warn}; + +use crypto::PublicKey; +use mycelium::endpoint::Endpoint; +use mycelium::{crypto, Node}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; + +/// The default port on the underlay to listen on for incoming TCP connections. +const DEFAULT_TCP_LISTEN_PORT: u16 = 9651; +/// The default port on the underlay to listen on for incoming Quic connections. +const DEFAULT_QUIC_LISTEN_PORT: u16 = 9651; +/// The default port to use for IPv6 link local peer discovery (UDP). +const DEFAULT_PEER_DISCOVERY_PORT: u16 = 9650; +/// The default listening address for the HTTP API. +const DEFAULT_HTTP_API_SERVER_ADDRESS: SocketAddr = + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8989); +/// The default listening address for the JSON-RPC API. +const DEFAULT_JSONRPC_API_SERVER_ADDRESS: SocketAddr = + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8990); + +const DEFAULT_KEY_FILE: &str = "priv_key.bin"; + +/// Default name of tun interface +#[cfg(not(target_os = "macos"))] +const TUN_NAME: &str = "mycelium"; +/// Default name of tun interface +#[cfg(target_os = "macos")] +const TUN_NAME: &str = "utun0"; + +/// The logging formats that can be selected. +#[derive(Clone, PartialEq, Eq)] +enum LoggingFormat { + Compact, + Logfmt, + /// Same as Logfmt but with color statically disabled + Plain, +} + +impl Display for LoggingFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + LoggingFormat::Compact => "compact", + LoggingFormat::Logfmt => "logfmt", + LoggingFormat::Plain => "plain", + } + ) + } +} + +impl FromStr for LoggingFormat { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + "compact" => LoggingFormat::Compact, + "logfmt" => LoggingFormat::Logfmt, + "plain" => LoggingFormat::Plain, + _ => return Err("invalid logging format"), + }) + } +} + +#[derive(Parser)] +#[command(version)] +struct Cli { + /// Path to the private key file. This will be created if it does not exist. Default + /// [priv_key.bin]. + #[arg(short = 'k', long = "key-file", global = true)] + key_file: Option, + + // Configuration file + #[arg(short = 'c', long = "config-file", global = true)] + config_file: Option, + + /// Enable debug logging. Does nothing if `--silent` is set. + #[arg(short = 'd', long = "debug", default_value_t = false)] + debug: bool, + + /// Disable all logs except error logs. + #[arg(long = "silent", default_value_t = false)] + silent: bool, + + /// The logging format to use. `logfmt` and `compact` is supported. + #[arg(long = "log-format", default_value_t = LoggingFormat::Compact)] + logging_format: LoggingFormat, + + #[clap(flatten)] + node_args: NodeArguments, + + #[command(subcommand)] + command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + /// Inspect a public key provided in hex format, or export the local public key if no key is + /// given. + Inspect { + /// Output in json format. + #[arg(long = "json")] + json: bool, + + /// The key to inspect. + key: Option, + }, + + /// Actions on the message subsystem + Message { + #[command(subcommand)] + command: MessageCommand, + }, + + /// Actions related to peers (list, remove, add) + Peers { + #[command(subcommand)] + command: PeersCommand, + }, + + /// Actions related to routes (selected, fallback, queried, no route) + Routes { + #[command(subcommand)] + command: RoutesCommand, + }, +} + +#[derive(Debug, Subcommand)] +pub enum MessageCommand { + Send { + /// Wait for a reply from the receiver. + #[arg(short = 'w', long = "wait", default_value_t = false)] + wait: bool, + /// An optional timeout to wait for. This does nothing if the `--wait` flag is not set. If + /// `--wait` is set and this flag isn't, wait forever for a reply. + #[arg(long = "timeout")] + timeout: Option, + /// Optional topic of the message. Receivers can filter on this to only receive messages + /// for a chosen topic. + #[arg(short = 't', long = "topic")] + topic: Option, + /// Optional file to use as message body. + #[arg(long = "msg-path")] + msg_path: Option, + /// Optional message ID to reply to. + #[arg(long = "reply-to")] + reply_to: Option, + /// Destination of the message, either a hex encoded public key, or an IPv6 address in the + /// 400::/7 range. + destination: String, + /// The message to send. This is required if `--msg_path` is not set + message: Option, + }, + Receive { + /// An optional timeout to wait for a message. If this is not set, wait forever. + #[arg(long = "timeout")] + timeout: Option, + /// Optional topic of the message. Only messages with this topic will be received by this + /// command. + #[arg(short = 't', long = "topic")] + topic: Option, + /// Optional file in which the message body will be saved. + #[arg(long = "msg-path")] + msg_path: Option, + /// Don't print the metadata + #[arg(long = "raw")] + raw: bool, + }, +} + +#[derive(Debug, Subcommand)] +pub enum PeersCommand { + /// List the connected peers + List { + /// Print the peers list in JSON format + #[arg(long = "json", default_value_t = false)] + json: bool, + }, + /// Add peer(s) + Add { peers: Vec }, + /// Remove peer(s) + Remove { peers: Vec }, +} + +#[derive(Debug, Subcommand)] +pub enum RoutesCommand { + /// Print all selected routes + Selected { + /// Print selected routes in JSON format + #[arg(long = "json", default_value_t = false)] + json: bool, + }, + /// Print all fallback routes + Fallback { + /// Print fallback routes in JSON format + #[arg(long = "json", default_value_t = false)] + json: bool, + }, + /// Print the currently queried subnets + Queried { + /// Print queried subnets in JSON format + #[arg(long = "json", default_value_t = false)] + json: bool, + }, + /// Print all subnets which are explicitly marked as not having a route + NoRoute { + /// Print subnets in JSON format + #[arg(long = "json", default_value_t = false)] + json: bool, + }, +} + +#[derive(Debug, Args)] +pub struct NodeArguments { + /// Peers to connect to. + #[arg(long = "peers", num_args = 1..)] + static_peers: Vec, + + /// Port to listen on for tcp connections. + #[arg(short = 't', long = "tcp-listen-port", default_value_t = DEFAULT_TCP_LISTEN_PORT)] + tcp_listen_port: u16, + + /// Disable quic protocol for connecting to peers + #[arg(long = "disable-quic", default_value_t = false)] + disable_quic: bool, + + /// Port to listen on for quic connections. + #[arg(short = 'q', long = "quic-listen-port", default_value_t = DEFAULT_QUIC_LISTEN_PORT)] + quic_listen_port: u16, + + /// Port to use for link local peer discovery. This uses the UDP protocol. + #[arg(long = "peer-discovery-port", default_value_t = DEFAULT_PEER_DISCOVERY_PORT)] + peer_discovery_port: u16, + + /// Disable peer discovery. + /// + /// If this flag is passed, the automatic link local peer discovery will not be enabled, and + /// peers must be configured manually. If this is disabled on all local peers, communication + /// between them will go over configured external peers. + #[arg(long = "disable-peer-discovery", default_value_t = false)] + disable_peer_discovery: bool, + + /// Address of the HTTP API server. + #[arg(long = "api-addr", default_value_t = DEFAULT_HTTP_API_SERVER_ADDRESS)] + api_addr: SocketAddr, + + /// Address of the JSON-RPC API server. + #[arg(long = "jsonrpc-addr", default_value_t = DEFAULT_JSONRPC_API_SERVER_ADDRESS)] + jsonrpc_addr: SocketAddr, + + /// Run without creating a TUN interface. + /// + /// The system will participate in the network as usual, but won't be able to send out L3 + /// packets. Inbound L3 traffic will be silently discarded. The message subsystem will still + /// work however. + #[arg(long = "no-tun", default_value_t = false)] + no_tun: bool, + + /// Name to use for the TUN interface, if one is created. + /// + /// Setting this only matters if a TUN interface is actually created, i.e. if the `--no-tun` + /// flag is **not** set. The name set here must be valid for the current platform, e.g. on OSX, + /// the name must start with `utun` and be followed by digits. + #[arg(long = "tun-name")] + tun_name: Option, + + /// The address on which to expose prometheus metrics, if desired. + /// + /// Setting this flag will attempt to start an HTTP server on the provided address, to serve + /// prometheus metrics on the /metrics endpoint. If this flag is not set, metrics are also not + /// collected. + #[arg(long = "metrics-api-address")] + metrics_api_address: Option, + + /// The firewall mark to set on the mycelium sockets. + /// + /// This allows to identify packets that contain encapsulated mycelium packets so that + /// different routing policies can be applied to them. + /// This option only has an effect on Linux. + #[arg(long = "firewall-mark")] + firewall_mark: Option, + + /// The amount of worker tasks to spawn to handle updates. + /// + /// By default, updates are processed on a single task only. This is sufficient for most use + /// cases. In case you notice that the node can't keep up with the incoming updates (typically + /// because you are running a public node with a lot of connections), this value can be + /// increased to process updates in parallel. + #[arg(long = "update-workers", default_value_t = 1)] + update_workers: usize, + + /// The topic configuration. + /// + /// A .toml file containing topic configuration. This is a default action in case the topic is + /// not listed, and an explicit whitelist for allowed subnets/ips which are otherwise allowed + /// to use a topic. + #[arg(long = "topic-config")] + topic_config: Option, + + /// The cache directory for the mycelium CDN module + /// + /// This directory will be used to cache reconstructed content blocks which were loaded through + /// the CDN functionallity for faster access next time. + #[arg(long = "cdn-cache")] + cdn_cache: Option, +} + +#[derive(Debug, Deserialize)] +pub struct MergedNodeConfig { + peers: Vec, + tcp_listen_port: u16, + disable_quic: bool, + quic_listen_port: u16, + peer_discovery_port: u16, + disable_peer_discovery: bool, + api_addr: SocketAddr, + jsonrpc_addr: SocketAddr, + no_tun: bool, + tun_name: String, + metrics_api_address: Option, + firewall_mark: Option, + update_workers: usize, + topic_config: Option, + cdn_cache: Option, +} + +#[derive(Debug, Deserialize, Default)] +struct MyceliumConfig { + #[serde(deserialize_with = "deserialize_optional_endpoint_str_from_toml")] + peers: Option>, + tcp_listen_port: Option, + disable_quic: Option, + quic_listen_port: Option, + no_tun: Option, + tun_name: Option, + disable_peer_discovery: Option, + peer_discovery_port: Option, + api_addr: Option, + jsonrpc_addr: Option, + metrics_api_address: Option, + firewall_mark: Option, + update_workers: Option, + topic_config: Option, + cdn_cache: Option, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + // Init default configuration + let mut mycelium_config = MyceliumConfig::default(); + + // Load configuration file + if let Some(config_file_path) = &cli.config_file { + if Path::new(config_file_path).exists() { + let config = config::Config::builder() + .add_source(config::File::new( + config_file_path.to_str().unwrap(), + config::FileFormat::Toml, + )) + .build()?; + + mycelium_config = config.try_deserialize()?; + } else { + let error_msg = format!("Config file {config_file_path:?} not found"); + return Err(io::Error::new(io::ErrorKind::NotFound, error_msg).into()); + } + } else if let Some(mut conf) = dirs::config_dir() { + // Windows: %APPDATA%/ThreeFold Tech/Mycelium/mycelium.conf + #[cfg(target_os = "windows")] + { + conf = conf + .join("ThreeFold Tech") + .join("Mycelium") + .join("mycelium.toml") + }; + // Linux: $HOME/.config/mycelium/mycelium.conf + #[allow(clippy::unnecessary_operation)] + #[cfg(target_os = "linux")] + { + conf = conf.join("mycelium").join("mycelium.toml") + }; + // MacOS: $HOME/Library/Application Support/ThreeFold Tech/Mycelium/mycelium.conf + #[cfg(target_os = "macos")] + { + conf = conf + .join("ThreeFold Tech") + .join("Mycelium") + .join("mycelium.toml") + }; + + if conf.exists() { + info!( + conf_dir = conf.to_str().unwrap(), + "Mycelium is starting with configuration file", + ); + let config = config::Config::builder() + .add_source(config::File::new( + conf.to_str().unwrap(), + config::FileFormat::Toml, + )) + .build()?; + mycelium_config = config.try_deserialize()?; + } + } + + let level = if cli.silent { + tracing::Level::ERROR + } else if cli.debug { + tracing::Level::DEBUG + } else { + tracing::Level::INFO + }; + + tracing_subscriber::registry() + .with( + EnvFilter::builder() + .with_default_directive(level.into()) + .from_env() + .expect("invalid RUST_LOG"), + ) + .with( + (cli.logging_format == LoggingFormat::Compact) + .then(|| tracing_subscriber::fmt::Layer::new().compact()), + ) + .with((cli.logging_format == LoggingFormat::Logfmt).then(tracing_logfmt::layer)) + .with((cli.logging_format == LoggingFormat::Plain).then(|| { + tracing_logfmt::builder() + // Explicitly force color off + .with_ansi_color(false) + .layer() + })) + .init(); + + let key_path = cli + .key_file + .unwrap_or_else(|| PathBuf::from(DEFAULT_KEY_FILE)); + + match cli.command { + None => { + let merged_config = merge_config(cli.node_args, mycelium_config); + let topic_config = merged_config.topic_config.as_ref().and_then(|path| { + let mut content = String::new(); + let mut file = std::fs::File::open(path).ok()?; + file.read_to_string(&mut content).ok()?; + toml::from_str::(&content).ok() + }); + + if topic_config.is_some() { + info!(path = ?merged_config.topic_config, "Loaded topic cofig"); + } + + let node_keys = get_node_keys(&key_path).await?; + let node_secret_key = if let Some((node_secret_key, _)) = node_keys { + node_secret_key + } else { + warn!("Node key file {key_path:?} not found, generating new keys"); + let secret_key = crypto::SecretKey::new(); + save_key_file(&secret_key, &key_path).await?; + secret_key + }; + + let _api = if let Some(metrics_api_addr) = merged_config.metrics_api_address { + let metrics = mycelium_metrics::PrometheusExporter::new(); + let config = mycelium::Config { + node_key: node_secret_key, + peers: merged_config.peers, + no_tun: merged_config.no_tun, + tcp_listen_port: merged_config.tcp_listen_port, + quic_listen_port: if merged_config.disable_quic { + None + } else { + Some(merged_config.quic_listen_port) + }, + peer_discovery_port: if merged_config.disable_peer_discovery { + None + } else { + Some(merged_config.peer_discovery_port) + }, + tun_name: merged_config.tun_name, + private_network_config: None, + metrics: metrics.clone(), + firewall_mark: merged_config.firewall_mark, + update_workers: merged_config.update_workers, + topic_config, + cdn_cache: merged_config.cdn_cache, + }; + metrics.spawn(metrics_api_addr); + let node = Arc::new(Mutex::new(Node::new(config).await?)); + let http_api = mycelium_api::Http::spawn(node.clone(), merged_config.api_addr); + + // Initialize the JSON-RPC server + let rpc_api = + mycelium_api::rpc::JsonRpc::spawn(node, merged_config.jsonrpc_addr).await; + + (http_api, rpc_api) + } else { + let config = mycelium::Config { + node_key: node_secret_key, + peers: merged_config.peers, + no_tun: merged_config.no_tun, + tcp_listen_port: merged_config.tcp_listen_port, + quic_listen_port: if merged_config.disable_quic { + None + } else { + Some(merged_config.quic_listen_port) + }, + peer_discovery_port: if merged_config.disable_peer_discovery { + None + } else { + Some(merged_config.peer_discovery_port) + }, + tun_name: merged_config.tun_name, + private_network_config: None, + metrics: mycelium_metrics::NoMetrics, + firewall_mark: merged_config.firewall_mark, + update_workers: merged_config.update_workers, + topic_config, + cdn_cache: merged_config.cdn_cache, + }; + let node = Arc::new(Mutex::new(Node::new(config).await?)); + let http_api = mycelium_api::Http::spawn(node.clone(), merged_config.api_addr); + + // Initialize the JSON-RPC server + let rpc_api = + mycelium_api::rpc::JsonRpc::spawn(node, merged_config.jsonrpc_addr).await; + + (http_api, rpc_api) + }; + + // TODO: put in dedicated file so we can only rely on certain signals on unix platforms + #[cfg(target_family = "unix")] + { + let mut sigint = signal::unix::signal(SignalKind::interrupt()) + .expect("Can install SIGINT handler"); + let mut sigterm = signal::unix::signal(SignalKind::terminate()) + .expect("Can install SIGTERM handler"); + + tokio::select! { + _ = sigint.recv() => { } + _ = sigterm.recv() => { } + } + } + #[cfg(not(target_family = "unix"))] + { + if let Err(e) = tokio::signal::ctrl_c().await { + error!("Failed to wait for SIGINT: {e}"); + } + } + } + Some(cmd) => match cmd { + Command::Inspect { json, key } => { + let node_keys = get_node_keys(&key_path).await?; + let key = if let Some(key) = key { + PublicKey::try_from(key.as_str())? + } else if let Some((_, node_pub_key)) = node_keys { + node_pub_key + } else { + error!("No key to inspect provided and no key found at {key_path:?}"); + return Err(io::Error::new( + io::ErrorKind::NotFound, + "no key to inspect and key file not found", + ) + .into()); + }; + mycelium_cli::inspect(key, json)?; + + return Ok(()); + } + Command::Message { command } => match command { + MessageCommand::Send { + wait, + timeout, + topic, + msg_path, + reply_to, + destination, + message, + } => { + return mycelium_cli::send_msg( + destination, + message, + wait, + timeout, + reply_to, + topic, + msg_path, + cli.node_args.api_addr, + ) + .await + } + MessageCommand::Receive { + timeout, + topic, + msg_path, + raw, + } => { + return mycelium_cli::recv_msg( + timeout, + topic, + msg_path, + raw, + cli.node_args.api_addr, + ) + .await + } + }, + Command::Peers { command } => match command { + PeersCommand::List { json } => { + return mycelium_cli::list_peers(cli.node_args.api_addr, json).await; + } + PeersCommand::Add { peers } => { + return mycelium_cli::add_peers(cli.node_args.api_addr, peers).await; + } + PeersCommand::Remove { peers } => { + return mycelium_cli::remove_peers(cli.node_args.api_addr, peers).await; + } + }, + Command::Routes { command } => match command { + RoutesCommand::Selected { json } => { + return mycelium_cli::list_selected_routes(cli.node_args.api_addr, json).await; + } + RoutesCommand::Fallback { json } => { + return mycelium_cli::list_fallback_routes(cli.node_args.api_addr, json).await; + } + RoutesCommand::Queried { json } => { + return mycelium_cli::list_queried_subnets(cli.node_args.api_addr, json).await; + } + RoutesCommand::NoRoute { json } => { + return mycelium_cli::list_no_route_entries(cli.node_args.api_addr, json).await; + } + }, + }, + } + + Ok(()) +} + +async fn get_node_keys( + key_path: &PathBuf, +) -> Result, io::Error> { + if key_path.exists() { + let sk = load_key_file(key_path).await?; + let pk = crypto::PublicKey::from(&sk); + debug!("Loaded key file at {key_path:?}"); + Ok(Some((sk, pk))) + } else { + Ok(None) + } +} + +async fn load_key_file(path: &Path) -> Result +where + T: From<[u8; 32]>, +{ + let mut file = File::open(path).await?; + let mut secret_bytes = [0u8; 32]; + file.read_exact(&mut secret_bytes).await?; + + Ok(T::from(secret_bytes)) +} + +async fn save_key_file(key: &crypto::SecretKey, path: &Path) -> io::Result<()> { + #[cfg(target_family = "unix")] + { + use tokio::fs::OpenOptions; + + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .mode(0o600) // rw by the owner, not readable by group or others + .open(path) + .await?; + file.write_all(key.as_bytes()).await?; + } + #[cfg(not(target_family = "unix"))] + { + let mut file = File::create(path).await?; + file.write_all(key.as_bytes()).await?; + } + + Ok(()) +} + +fn merge_config(cli_args: NodeArguments, file_config: MyceliumConfig) -> MergedNodeConfig { + MergedNodeConfig { + peers: if !cli_args.static_peers.is_empty() { + cli_args.static_peers + } else { + file_config.peers.unwrap_or_default() + }, + tcp_listen_port: if cli_args.tcp_listen_port != DEFAULT_TCP_LISTEN_PORT { + cli_args.tcp_listen_port + } else { + file_config + .tcp_listen_port + .unwrap_or(DEFAULT_TCP_LISTEN_PORT) + }, + disable_quic: cli_args.disable_quic || file_config.disable_quic.unwrap_or(false), + quic_listen_port: if cli_args.quic_listen_port != DEFAULT_QUIC_LISTEN_PORT { + cli_args.quic_listen_port + } else { + file_config + .quic_listen_port + .unwrap_or(DEFAULT_QUIC_LISTEN_PORT) + }, + peer_discovery_port: if cli_args.peer_discovery_port != DEFAULT_PEER_DISCOVERY_PORT { + cli_args.peer_discovery_port + } else { + file_config + .peer_discovery_port + .unwrap_or(DEFAULT_PEER_DISCOVERY_PORT) + }, + disable_peer_discovery: cli_args.disable_peer_discovery + || file_config.disable_peer_discovery.unwrap_or(false), + api_addr: if cli_args.api_addr != DEFAULT_HTTP_API_SERVER_ADDRESS { + cli_args.api_addr + } else { + file_config + .api_addr + .unwrap_or(DEFAULT_HTTP_API_SERVER_ADDRESS) + }, + jsonrpc_addr: if cli_args.jsonrpc_addr != DEFAULT_JSONRPC_API_SERVER_ADDRESS { + cli_args.jsonrpc_addr + } else { + file_config + .jsonrpc_addr + .unwrap_or(DEFAULT_JSONRPC_API_SERVER_ADDRESS) + }, + no_tun: cli_args.no_tun || file_config.no_tun.unwrap_or(false), + tun_name: if let Some(tun_name_cli) = cli_args.tun_name { + tun_name_cli + } else if let Some(tun_name_config) = file_config.tun_name { + tun_name_config + } else { + TUN_NAME.to_string() + }, + metrics_api_address: cli_args + .metrics_api_address + .or(file_config.metrics_api_address), + firewall_mark: cli_args.firewall_mark.or(file_config.firewall_mark), + update_workers: if cli_args.update_workers != 1 { + cli_args.update_workers + } else { + file_config.update_workers.unwrap_or(1) + }, + topic_config: cli_args.topic_config.or(file_config.topic_config), + cdn_cache: cli_args.cdn_cache.or(file_config.cdn_cache), + } +} + +/// Deserialize an optional list of endpoints from TOML format. The endpoints can be provided +/// either as a list `[...]`, or in case there is only 1 endpoint, it can also be provided as a +/// single string element. If no value is provided, it returns None. +fn deserialize_optional_endpoint_str_from_toml<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrVec { + String(String), + Vec(Vec), + } + + Ok(match Option::::deserialize(deserializer)? { + Some(StringOrVec::Vec(v)) => Some( + v.into_iter() + .map(|s| { + ::from_str(&s).map_err(serde::de::Error::custom) + }) + .collect::, _>>()?, + ), + Some(StringOrVec::String(s)) => Some(vec![ + ::from_str(&s).map_err(serde::de::Error::custom)? + ]), + None => None, + }) +} diff --git a/components/mycelium/scripts/README.md b/components/mycelium/scripts/README.md new file mode 100644 index 0000000..1f59cd5 --- /dev/null +++ b/components/mycelium/scripts/README.md @@ -0,0 +1,58 @@ +# Development / test scripts + +## `setup_network.sh` + +`setup_network.sh` is used as-is, and as_root :-/ +This little thing adds some LINUX network namespaces in which you can run mycelium +U're a dev so deal with it + + +## testing mycelium (`bigmush.sh`) +This lill skrip will just start $NUMOFNS network namespaces in your LINUX box where you can SUDO (because I __know__ for a fact that you weren't root, of course), start a mycelium daemon in the main namespace and one in each NS. + +### Usage : + +Start with +```bash +source ./bigmush.sh +getmycelium +``` +will get the latest __release__ binary from github + +Then +```bash +source ./bigmush.sh +doit +``` + +will create and start a 50 node mycelium with one central + +```bash +source ./bigmush +dropit +``` +will kill with little mercy mycelium daemons and delete the namespaces + + +```bash +source ./bigmush.sh +cleanit +``` +will do a `dropit` and clean `*.{bin,out}` files + +```bash +showit +``` + +will send a USR1 signal to all mycelium daemons that will + - send routing tables and peers to stdout + - where stdout will be captured in `xx.out` for each NS + +### logging +every namespace has an `xx.out` file that is stout and stderr +the `xx.bin` file is the namespace daemon's privkey. + +### behaviour testing + +1) verify if you can reach all mycelium namespaces +2) also when running another machine in your net, verify if it's automatically detected diff --git a/components/mycelium/scripts/bigmush.fish b/components/mycelium/scripts/bigmush.fish new file mode 100644 index 0000000..2e6b696 --- /dev/null +++ b/components/mycelium/scripts/bigmush.fish @@ -0,0 +1,90 @@ +set NATNET 172.16.0.0/16 +set NUMOFNS 32 + +function IPN + sudo ip net $argv +end + +function IPL + sudo ip link $argv +end + +function IPA + sudo ip addr add $argv +end + +set peers tcp://188.40.132.242:9651 quic://[2a01:4f8:212:fa6::2]:9651 tcp://185.69.166.7:9651 quic://[2a02:1802:5e:0:ec4:7aff:fe51:e36b]:9651 tcp://65.21.231.58:9651 quic://[2a01:4f9:5a:1042::2]:9651 tcp://[2604:a00:50:17b:9e6b:ff:fe1f:e054]:9651 quic://5.78.122.16:9651 tcp://[2a01:4ff:2f0:3621::1]:9651 quic://142.93.217.194:9651 + +function IPNA + set name $argv[1] + set -e argv[1] + sudo ip -n $name addr add $argv +end + +function IPNL + set name $argv[1] + set -e argv[1] + sudo ip -n $name link $argv +end + +function IPNR + set name $argv[1] + set defrtr (string replace -r '/24$' '' $argv[2]) + set -e argv[1] + set -e argv[2] + sudo ip -n $name route add default via $defrtr +end + +function createns + set iname $argv[1] + set in_ip $argv[2] + set out_ip $argv[3] + set name n-$iname + IPN add $name + IPL add in_$iname type veth peer name out_$iname + IPL set in_$iname netns $name + IPNL $name set lo up + IPNL $name set in_$iname up + IPL set out_$iname up + IPNA $name $in_ip dev in_$iname + IPA $out_ip dev out_$iname + IPNR $name $out_ip + nohup sudo ip netns exec $name ./mycelium --key-file $name.bin --api-addr (string replace -r '/24$' '' $in_ip):8989 --peers tcp://(string replace -r '/24$' '' $out_ip):9651 > $iname.out & +end + +function dropns + set iname $argv[1] + set name n-$iname + IPL del out_$iname + IPN del $name +end + +function doit + nohup sudo ./mycelium --key-file host.bin --api-addr 127.0.0.1:8989 --peers $peers > host.out & + for i in (seq 1 $NUMOFNS) + createns $i 172.16.$i.2/24 172.16.$i.1/24 + end +end + +function dropit + sudo pkill -9 mycelium + for i in (seq 1 $NUMOFNS) + dropns $i + end +end + +function cleanit + dropit + sudo rm ./*.bin + sudo rm ./*.out +end + +function showit + sudo killall -USR1 mycelium +end + +function getmycelium + wget https://github.com/threefoldtech/mycelium/releases/latest/download/mycelium-x86_64-unknown-linux-musl.tar.gz \ + -O- | gunzip -c | tar xvf - -C $PWD +end + diff --git a/components/mycelium/scripts/bigmush.sh b/components/mycelium/scripts/bigmush.sh new file mode 100755 index 0000000..4bf48fa --- /dev/null +++ b/components/mycelium/scripts/bigmush.sh @@ -0,0 +1,79 @@ +#!/usr/bin/bash +# +NATNET=172.16.0.0/16 +NUMOFNS=32 +alias IPN='sudo ip net' +alias IPL='sudo ip link' +alias IPA='sudo ip addr add' + +peers='tcp://188.40.132.242:9651 quic://[2a01:4f8:212:fa6::2]:9651 tcp://185.69.166.7:9651 quic://[2a02:1802:5e:0:ec4:7aff:fe51:e36b]:9651 tcp://65.21.231.58:9651 quic://[2a01:4f9:5a:1042::2]:9651 tcp://[2604:a00:50:17b:9e6b:ff:fe1f:e054]:9651 quic://5.78.122.16:9651 tcp://[2a01:4ff:2f0:3621::1]:9651 quic://142.93.217.194:9651' + +function IPNA() { + local name=$1 + shift + sudo ip -n ${name} addr add $@ +} +function IPNL() { + local name=$1 + shift + sudo ip -n ${name} link $@ +} +function IPNR() { + local name=$1 + shift + local defrtr=${1/\/24/} + shift + sudo ip -n ${name} route add default via ${defrtr} +} +function createns() { + local iname=$1 + local in_ip=$2 + local out_ip=$3 + local name=n-${iname} + IPN add $name + IPL add in_${iname} type veth peer name out_${iname} + IPL set in_${iname} netns ${name} + IPNL ${name} set lo up + IPNL ${name} set in_${iname} up + IPL set out_${iname} up + IPNA ${name} ${in_ip} dev in_${iname} + IPA ${out_ip} dev out_${iname} + IPNR ${name} ${out_ip} + # start mycelium, relying on local discovery + nohup sudo ip netns exec ${name} ./mycelium --key-file ${name}.bin --api-addr ${in_ip/\/24/}:8989 --peers tcp://${out_ip/\/24/}:9651 > ${iname}.out & +} +function dropns() { + local iname=$1 + local name=n-${iname} + IPL del out_${iname} + IPN del ${name} +} + +function doit() { + nohup sudo ./mycelium --key-file host.bin --api-addr 127.0.0.1:8989 --peers ${peers} >host.out & + for i in $(seq 1 $NUMOFNS); do + createns ${i} 172.16.${i}.2/24 172.16.${i}.1/24 + done +} +function dropit() { + sudo pkill -9 mycelium + for i in $(seq 1 $NUMOFNS); do + dropns ${i} + done +} + +function cleanit() { + dropit + sudo rm ./*.bin + sudo rm ./*.out +} + +function showit() { + sudo killall -USR1 mycelium +} + +function getmycelium() { + wget https://github.com/threefoldtech/mycelium/releases/latest/download/mycelium-x86_64-unknown-linux-musl.tar.gz \ + -O- | gunzip -c | tar xvf - -C ${PWD} + +} diff --git a/components/mycelium/scripts/setup_network.sh b/components/mycelium/scripts/setup_network.sh new file mode 100755 index 0000000..bfb3973 --- /dev/null +++ b/components/mycelium/scripts/setup_network.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# +# A simple shell script to setup a local network using network namespaces. This relies on the presence of the `ip` tool, part of `iproute2` package on linux. +# +# Note: this script requires root privilege + +set -ex + +# Create veth pairs +ip l add p0 type veth peer p1 +ip l add p2 type veth peer p3 +ip l add p4 type veth peer p5 +ip l add p6 type veth peer p7 + +# Create bridge +ip l add mycelium-br type bridge + +# Add 1 part of every veth pair in the bridge +ip l set p1 master mycelium-br +ip l set p3 master mycelium-br +ip l set p5 master mycelium-br +ip l set p7 master mycelium-br + +# Add network namespaces +ip netns add net1 +ip netns add net2 +ip netns add net3 + +# Enable loopback devices in network namespaces +ip -n net1 l set lo up +ip -n net2 l set lo up +ip -n net3 l set lo up + +# Add 1 veth end to every network namespace +ip l set p2 netns net1 +ip l set p4 netns net2 +ip l set p6 netns net3 + +# Set underlay IP addresses on the veth parts which are not in the bridge +ip a add 10.0.2.1/24 dev p0 +ip -n net1 a add 10.0.2.2/24 dev p2 +ip -n net2 a add 10.0.2.3/24 dev p4 +ip -n net3 a add 10.0.2.4/24 dev p6 + +# Set all veth interface to up +ip l set p0 up +ip l set p1 up +ip -n net1 l set p2 up +ip l set p3 up +ip -n net2 l set p4 up +ip l set p5 up +ip -n net3 l set p6 up +ip l set p7 up + +# Set bridge as up +ip l set mycelium-br up diff --git a/components/mycelium/shell.nix b/components/mycelium/shell.nix new file mode 100644 index 0000000..d7c46b9 --- /dev/null +++ b/components/mycelium/shell.nix @@ -0,0 +1,14 @@ +( + import + ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + {src = ./.;} +) +.shellNix diff --git a/components/mycelium/systemd/mycelium.service b/components/mycelium/systemd/mycelium.service new file mode 100644 index 0000000..ecac42c --- /dev/null +++ b/components/mycelium/systemd/mycelium.service @@ -0,0 +1,21 @@ +[Unit] +Description=End-2-end encrypted IPv6 overlay network +Wants=network.target +After=network.target +Documentation=https://github.com/threefoldtech/mycelium + +[Service] +ProtectHome=true +ProtectSystem=true +SyslogIdentifier=mycelium +CapabilityBoundingSet=CAP_NET_ADMIN +StateDirectory=mycelium +StateDirectoryMode=0700 +ExecStartPre=+-/sbin/modprobe tun +ExecStart=/usr/bin/mycelium --tun-name mycelium -k %S/mycelium/key.bin --peers tcp://188.40.132.242:9651 quic://[2a01:4f8:212:fa6::2]:9651 tcp://185.69.166.7:9651 quic://[2a02:1802:5e:0:ec4:7aff:fe51:e36b]:9651 tcp://65.21.231.58:9651 quic://[2a01:4f9:5a:1042::2]:9651 tcp://[2604:a00:50:17b:9e6b:ff:fe1f:e054]:9651 quic://5.78.122.16:9651 tcp://[2a01:4ff:2f0:3621::1]:9651 quic://142.93.217.194:9651 +Restart=always +RestartSec=5 +TimeoutStopSec=5 + +[Install] +WantedBy=multi-user.target