├── .github ├── dependabot.yml └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── CHANGELOG.md ├── CODEOWNERS ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── config_example.toml ├── default.nix ├── docs ├── api.yaml ├── data_packet.md ├── message.md ├── openrpc.json ├── packet.md ├── private_network.md └── topic_configuration.md ├── flake.lock ├── flake.nix ├── installers └── windows │ └── wix │ ├── LICENSE.rtf │ ├── mycelium.en-us.wxl │ └── mycelium.wxs ├── mobile ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── mycelium-api ├── Cargo.toml └── src │ ├── lib.rs │ ├── message.rs │ ├── rpc.rs │ └── rpc │ ├── admin.rs │ ├── message.rs │ ├── models.rs │ ├── peer.rs │ ├── route.rs │ ├── spec.rs │ └── traits.rs ├── mycelium-cli ├── Cargo.toml └── src │ ├── inspect.rs │ ├── lib.rs │ ├── message.rs │ ├── peer.rs │ └── routes.rs ├── mycelium-metrics ├── Cargo.toml └── src │ ├── lib.rs │ ├── noop.rs │ └── prometheus.rs ├── mycelium-ui ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dioxus.toml ├── README.md ├── assets │ └── styles.css └── src │ ├── api.rs │ ├── components.rs │ ├── components │ ├── home.rs │ ├── layout.rs │ ├── peers.rs │ └── routes.rs │ └── main.rs ├── mycelium ├── Cargo.toml └── src │ ├── babel.rs │ ├── babel │ ├── hello.rs │ ├── ihu.rs │ ├── route_request.rs │ ├── seqno_request.rs │ ├── tlv.rs │ └── update.rs │ ├── connection.rs │ ├── connection │ ├── tls.rs │ └── tracked.rs │ ├── crypto.rs │ ├── data.rs │ ├── endpoint.rs │ ├── filters.rs │ ├── interval.rs │ ├── lib.rs │ ├── message.rs │ ├── message │ ├── chunk.rs │ ├── done.rs │ ├── init.rs │ └── topic.rs │ ├── metric.rs │ ├── metrics.rs │ ├── packet.rs │ ├── packet │ ├── control.rs │ └── data.rs │ ├── peer.rs │ ├── peer_manager.rs │ ├── router.rs │ ├── router_id.rs │ ├── routing_table.rs │ ├── routing_table │ ├── iter.rs │ ├── iter_mut.rs │ ├── no_route.rs │ ├── queried_subnet.rs │ ├── route_entry.rs │ ├── route_key.rs │ ├── route_list.rs │ └── subnet_entry.rs │ ├── rr_cache.rs │ ├── seqno_cache.rs │ ├── sequence_number.rs │ ├── source_table.rs │ ├── subnet.rs │ ├── task.rs │ ├── tun.rs │ └── tun │ ├── android.rs │ ├── darwin.rs │ ├── ios.rs │ ├── linux.rs │ └── windows.rs ├── myceliumd-private ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── myceliumd ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── scripts ├── README.md ├── bigmush.fish ├── bigmush.sh └── setup_network.sh ├── shell.nix └── systemd └── mycelium.service /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | mycelium: 14 | patterns: 15 | - "*" 16 | - package-ecosystem: "cargo" # See documentation for possible values 17 | directory: "/myceliumd" # Location of package manifests 18 | schedule: 19 | interval: "weekly" 20 | groups: 21 | myceliumd: 22 | patterns: 23 | - "*" 24 | - package-ecosystem: "cargo" # See documentation for possible values 25 | directory: "/myceliumd-private" # Location of package manifests 26 | schedule: 27 | interval: "weekly" 28 | groups: 29 | myceliumd-private: 30 | patterns: 31 | - "*" 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | check_fmt: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: dtolnay/rust-toolchain@nightly 18 | with: 19 | components: rustfmt 20 | - uses: clechasseur/rs-fmt-check@v2 21 | 22 | clippy: 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest, macos-latest, windows-latest] 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - name: Set windows VCPKG_ROOT env variable 29 | run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append 30 | if: runner.os == 'Windows' 31 | - name: Install windows openssl 32 | run: vcpkg install openssl:x64-windows-static-md 33 | if: runner.os == 'Windows' 34 | - uses: actions/checkout@v4 35 | - name: Run Clippy 36 | run: cargo clippy --all-features -- -Dwarnings 37 | 38 | check_library: 39 | strategy: 40 | matrix: 41 | os: [ubuntu-latest, macos-latest,windows-latest] 42 | runs-on: ${{ matrix.os }} 43 | steps: 44 | - name: Set windows VCPKG_ROOT env variable 45 | run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append 46 | if: runner.os == 'Windows' 47 | - name: Install windows openssl 48 | run: vcpkg install openssl:x64-windows-static-md 49 | if: runner.os == 'Windows' 50 | - uses: actions/checkout@v4 51 | - name: Build 52 | run: cargo build -p mycelium --all-features --verbose 53 | - name: Run tests 54 | run: cargo test -p mycelium --all-features --verbose 55 | 56 | check_ios_library: 57 | runs-on: macos-latest 58 | steps: 59 | - uses: actions/checkout@v4 60 | - name: install ios target 61 | run: rustup target add aarch64-apple-ios 62 | - name: Cache cargo 63 | uses: actions/cache@v3 64 | with: 65 | path: ~/.cargo/registry 66 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 67 | - name: Build 68 | run: cargo build --target aarch64-apple-ios 69 | working-directory: mobile 70 | 71 | check_android_library: 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v4 75 | - name: install android target 76 | run: rustup target add aarch64-linux-android 77 | - name: Setup Java 78 | uses: actions/setup-java@v2 79 | with: 80 | distribution: 'adopt' 81 | java-version: '17' 82 | - name: Set up Android NDK 83 | uses: android-actions/setup-android@v3 84 | - name: Accept Android Licenses 85 | run: yes | sdkmanager --licenses || true 86 | - name: Cache cargo 87 | uses: actions/cache@v3 88 | with: 89 | path: ~/.cargo/registry 90 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 91 | - name: install cargo NDK 92 | run: cargo install cargo-ndk 93 | - name: Build 94 | run: cargo ndk -t arm64-v8a build 95 | working-directory: mobile 96 | 97 | check_binaries: 98 | strategy: 99 | matrix: 100 | os: [ubuntu-latest, macos-latest, windows-latest] 101 | binary: [myceliumd, myceliumd-private] 102 | runs-on: ${{ matrix.os }} 103 | steps: 104 | - name: Set windows VCPKG_ROOT env variable 105 | run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append 106 | if: runner.os == 'Windows' 107 | - name: Install windows openssl 108 | run: vcpkg install openssl:x64-windows-static-md 109 | if: runner.os == 'Windows' 110 | - uses: actions/checkout@v4 111 | - name: Change directory to binary 112 | run: cd ${{ matrix.binary }} 113 | - name: Build 114 | run: cargo build --verbose 115 | - name: Run tests 116 | run: cargo test --verbose 117 | - name: Run Clippy 118 | run: cargo clippy --all-features -- -Dwarnings 119 | 120 | check_flake: 121 | strategy: 122 | matrix: 123 | os: [ubuntu-latest, macos-latest] 124 | runs-on: ${{ matrix.os }} 125 | permissions: 126 | id-token: "write" 127 | contents: "read" 128 | steps: 129 | - uses: actions/checkout@v4 130 | - uses: DeterminateSystems/nix-installer-action@main 131 | - uses: DeterminateSystems/flake-checker-action@main 132 | - name: Run `nix build` 133 | run: nix build . 134 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | changelog: CHANGELOG.md 19 | # (required) GitHub token for creating GitHub Releases. 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | upload-assets-mycelium: 23 | needs: create-release 24 | strategy: 25 | matrix: 26 | include: 27 | - target: aarch64-apple-darwin 28 | os: macos-latest 29 | - target: x86_64-unknown-linux-musl 30 | os: ubuntu-latest 31 | - target: x86_64-apple-darwin 32 | os: macos-latest 33 | - target: aarch64-unknown-linux-musl 34 | os: ubuntu-latest 35 | runs-on: ${{ matrix.os }} 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: taiki-e/upload-rust-binary-action@v1 39 | with: 40 | # Name of the compiled binary, also name of the non-extension part of the produced file 41 | bin: mycelium 42 | # --target flag value, default is host 43 | target: ${{ matrix.target }} 44 | # Name of the archive when uploaded 45 | archive: $bin-$target 46 | # (required) GitHub token for uploading assets to GitHub Releases. 47 | token: ${{ secrets.GITHUB_TOKEN }} 48 | # Specify manifest since we are in a subdirectory 49 | manifest-path: myceliumd/Cargo.toml 50 | 51 | # TODO: Figure out the correct matrix setup to have this in a single action 52 | upload-assets-myceliumd-private: 53 | needs: create-release 54 | strategy: 55 | matrix: 56 | include: 57 | - target: aarch64-apple-darwin 58 | os: macos-latest 59 | - target: x86_64-unknown-linux-musl 60 | os: ubuntu-latest 61 | - target: x86_64-apple-darwin 62 | os: macos-latest 63 | - target: aarch64-unknown-linux-musl 64 | os: ubuntu-latest 65 | runs-on: ${{ matrix.os }} 66 | steps: 67 | - uses: actions/checkout@v4 68 | - uses: taiki-e/upload-rust-binary-action@v1 69 | with: 70 | # Name of the compiled binary, also name of the non-extension part of the produced file 71 | bin: mycelium-private 72 | # Set the vendored-openssl flag for provided release builds 73 | features: vendored-openssl 74 | # --target flag value, default is host 75 | target: ${{ matrix.target }} 76 | # Name of the archive when uploaded 77 | archive: $bin-$target 78 | # (required) GitHub token for uploading assets to GitHub Releases. 79 | token: ${{ secrets.GITHUB_TOKEN }} 80 | # Specify manifest since we are in a subdirectory 81 | manifest-path: myceliumd-private/Cargo.toml 82 | 83 | build-msi: 84 | needs: create-release 85 | runs-on: windows-latest 86 | steps: 87 | 88 | - name: Checkout repo 89 | uses: actions/checkout@v4 90 | 91 | - name: Create .exe file 92 | shell: bash 93 | run: cd myceliumd && RUSTFLAGS="-C target-feature=+crt-static" cargo build --release && cd .. 94 | 95 | - name: Setup .NET Core SDK 96 | uses: actions/setup-dotnet@v4.0.0 97 | 98 | - name: Install WiX Toolset 99 | run: dotnet tool install --global wix 100 | 101 | - name: Add WixToolset.UI.wixext extension 102 | run: wix extension add WixToolset.UI.wixext 103 | 104 | - name: Download Wintun zip file 105 | run: curl -o wintun.zip https://www.wintun.net/builds/wintun-0.14.1.zip 106 | 107 | - name: Unzip Wintun 108 | run: unzip wintun.zip 109 | 110 | - name: Move .dll file to myceliumd directory 111 | run: move wintun\bin\amd64\wintun.dll myceliumd 112 | 113 | - name: Build MSI package 114 | 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 115 | 116 | - name: Upload MSI artifact 117 | uses: alexellis/upload-assets@0.4.0 118 | env: 119 | GITHUB_TOKEN: ${{ github.token }} 120 | with: 121 | asset_paths: '["mycelium_installer.msi"]' 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | nodeconfig.toml 3 | keys.txt 4 | priv_key.bin 5 | 6 | # Profile output 7 | *.profraw 8 | *.profdata 9 | profile.json 10 | 11 | # vscode settings, keep these locally 12 | .vscode 13 | # visual studio project stuff 14 | .vs 15 | 16 | # wintun.dll, windows tun driver in repo root for windows development 17 | wintun.dll 18 | 19 | result/ 20 | 21 | .idea 22 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # global code owners 2 | * @leesmet 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["mycelium", "mycelium-metrics", "mycelium-api", "mycelium-cli"] 3 | exclude = ["myceliumd", "myceliumd-private", "mycelium-ui", "mobile"] 4 | resolver = "2" 5 | 6 | 7 | [profile.release] 8 | lto = "fat" 9 | codegen-units = 1 10 | -------------------------------------------------------------------------------- /config_example.toml: -------------------------------------------------------------------------------- 1 | # REMOVE/ADD COMMENT TO ENABLE/DISABLE LINE 2 | 3 | peers = [ 4 | "tcp://188.40.132.242:9651", 5 | "quic://[2a01:4f8:212:fa6::2]:9651", 6 | "quic://185.69.166.7:9651", 7 | "tcp://[2a02:1802:5e:0:8c9e:7dff:fec9:f0d2]:9651", 8 | "quic://65.21.231.58:9651", 9 | "tcp://[2a01:4f9:5a:1042::2]:9651", 10 | ] 11 | api_addr = "127.0.0.1:8989" 12 | tcp_listen_port = 9651 13 | quic_listen_port = 9651 14 | tun_name = "mycelium" 15 | disable_peer_discovery = false 16 | no_tun = false 17 | #metrics_api_address = 0.0.0.0:9999 18 | #firewall_mark = 30 19 | 20 | ## Options below only apply when myceliumd-private is used 21 | #network_name = "private network name" 22 | #network_key_file = "path_to_key_file" 23 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | ( 2 | import 3 | ( 4 | let 5 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 6 | in 7 | fetchTarball { 8 | url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 9 | sha256 = lock.nodes.flake-compat.locked.narHash; 10 | } 11 | ) 12 | {src = ./.;} 13 | ) 14 | .defaultNix 15 | -------------------------------------------------------------------------------- /docs/data_packet.md: -------------------------------------------------------------------------------- 1 | # Data packet 2 | 3 | A `data packet` contains user specified data. This can be any data, as long as the sender and receiver 4 | both understand what it is, without further help. Intermediate hops, which route the data have sufficient 5 | information with the header to know where to forward the packet. In practice, the data will be encrypted 6 | to avoid eavesdropping by intermediate hops. 7 | 8 | ## Packet header 9 | 10 | The packet header has a fixed size of 36 bytes, with the following layout: 11 | 12 | ``` 13 | 0 1 2 3 14 | 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 15 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 16 | | Reserved | Length | Hop Limit | 17 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 18 | | | 19 | + + 20 | | | 21 | + Source IP + 22 | | | 23 | + + 24 | | | 25 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 26 | | | 27 | + + 28 | | | 29 | + Destination IP + 30 | | | 31 | + + 32 | | | 33 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 34 | ``` 35 | 36 | The first 8 bits are reserved and must be set to 0. 37 | 38 | The next 16 bits are used to specify the length of the body. It is expected that 39 | the actual length of a packet does not exceed 65K right now, and overhead related 40 | to encryption should be handled by the client before sending the packet. 41 | 42 | The next byte is the hop-limit. Every node decrements this value by 1 before sending 43 | the packet. If a node decrements this value to 0, the packet is discarded. 44 | 45 | The next 16 bytes contain the sender IP address. 46 | 47 | The final 16 bytes contain the destination IP address. 48 | 49 | ## Body 50 | 51 | Following the header is a variable length body. The protocol does not have any requirements for the 52 | body, and the only requirement imposed is that the body is as long as specified in the header length 53 | field. It is technically legal according to the protocol to transmit a data packet without a body, 54 | i.e. a body length of 0. This is useless however, as there will not be any data to interpret. 55 | -------------------------------------------------------------------------------- /docs/message.md: -------------------------------------------------------------------------------- 1 | # Message subsystem 2 | 3 | The message subsystem can be used to send arbitrary length messages to receivers. A receiver is any 4 | other node in the network. It can be identified both by its public key, or an IP address in its announced 5 | range. The message subsystem can be interacted with both via the HTTP API, which is 6 | [documented here](./api.yaml), or via the `mycelium` binary. By default, the messages do not interpret 7 | the data in any way. When using the binary, the message is slightly modified to include an optional 8 | topic at the start of the message. Note that in the HTTP API, all messages are encoded in base64. This 9 | might make it difficult to consume these messages without additional tooling. 10 | 11 | Messages can be categorized by topics, which can be configured with whitelisted subnets and socket forwarding paths. 12 | For detailed information on how to configure topics, see the [Topic Configuration Guide](./topic_configuration.md). 13 | 14 | ## JSON-RPC API Examples 15 | 16 | These examples assume you have at least 2 nodes running, and that they are both part of the same network. 17 | 18 | Send a message on node1, waiting up to 2 minutes for a possible reply: 19 | 20 | ```json 21 | { 22 | "jsonrpc": "2.0", 23 | "method": "pushMessage", 24 | "params": [ 25 | { 26 | "dst": {"pk": "bb39b4a3a4efd70f3e05e37887677e02efbda14681d0acd3882bc0f754792c32"}, 27 | "payload": "xuV+" 28 | }, 29 | 120 30 | ], 31 | "id": 1 32 | } 33 | ``` 34 | 35 | Using curl: 36 | 37 | ```bash 38 | curl -X POST http://localhost:8990/rpc \ 39 | -H "Content-Type: application/json" \ 40 | -d '{ 41 | "jsonrpc": "2.0", 42 | "method": "pushMessage", 43 | "params": [ 44 | { 45 | "dst": {"pk": "bb39b4a3a4efd70f3e05e37887677e02efbda14681d0acd3882bc0f754792c32"}, 46 | "payload": "xuV+" 47 | }, 48 | 120 49 | ], 50 | "id": 1 51 | }' 52 | ``` 53 | 54 | Listen for a message on node2. Note that messages received while nothing is listening are added to 55 | a queue for later consumption. Wait for up to 1 minute. 56 | 57 | ```json 58 | { 59 | "jsonrpc": "2.0", 60 | "method": "popMessage", 61 | "params": [false, 60, null], 62 | "id": 1 63 | } 64 | ``` 65 | 66 | Using curl: 67 | 68 | ```bash 69 | curl -X POST http://localhost:8990/rpc \ 70 | -H "Content-Type: application/json" \ 71 | -d '{ 72 | "jsonrpc": "2.0", 73 | "method": "popMessage", 74 | "params": [false, 60, null], 75 | "id": 1 76 | }' 77 | ``` 78 | 79 | The system will (immediately) receive our previously sent message: 80 | 81 | ```json 82 | {"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+"} 83 | ``` 84 | 85 | To send a reply, we can post a message on the reply path, with the received message `id` (still on 86 | node2): 87 | 88 | ```json 89 | { 90 | "jsonrpc": "2.0", 91 | "method": "pushMessageReply", 92 | "params": [ 93 | "e47b25063912f4a9", 94 | { 95 | "dst": {"pk":"955bf6bea5e1150fd8e270c12e5b2fc08f08f7c5f3799d10550096cc137d671b"}, 96 | "payload": "xuC+" 97 | } 98 | ], 99 | "id": 1 100 | } 101 | ``` 102 | 103 | Using curl: 104 | 105 | ```bash 106 | curl -X POST http://localhost:8990/rpc \ 107 | -H "Content-Type: application/json" \ 108 | -d '{ 109 | "jsonrpc": "2.0", 110 | "method": "pushMessageReply", 111 | "params": [ 112 | "e47b25063912f4a9", 113 | { 114 | "dst": {"pk":"955bf6bea5e1150fd8e270c12e5b2fc08f08f7c5f3799d10550096cc137d671b"}, 115 | "payload": "xuC+" 116 | } 117 | ], 118 | "id": 1 119 | }' 120 | ``` 121 | 122 | If you did this fast enough, the initial sender (node1) will now receive the reply. 123 | 124 | ## Mycelium binary examples 125 | 126 | As explained above, while using the binary the message is slightly modified to insert the optional 127 | topic. As such, when using the binary to send messages, it is suggested to make sure the receiver is 128 | also using the binary to listen for messages. The options discussed here are not covering all possibilities, 129 | use the `--help` flag (`mycelium message send --help` and `mycelium message receive --help`) for a 130 | full overview. 131 | 132 | Once again, send a message. This time using a topic (example.topic). Note that there are no constraints 133 | on what a valid topic is, other than that it is valid UTF-8, and at most 255 bytes in size. The `--wait` 134 | flag can be used to indicate that we are waiting for a reply. If it is set, we can also use an additional 135 | `--timeout` flag to govern exactly how long (in seconds) to wait for. The default is to wait forever. 136 | 137 | ```bash 138 | mycelium message send 2e4:9ace:9252:630:beee:e405:74c0:d876 'this is a message' -t example.topic --wait 139 | ``` 140 | 141 | On the second node, listen for messages with this topic. If a different topic is used, the previous 142 | message won't be received. If no topic is set, all messages are received. An optional timeout flag 143 | can be specified, which indicates how long to wait for. Absence of this flag will cause the binary 144 | to wait forever. 145 | 146 | ```bash 147 | mycelium message receive -t example.topic 148 | ``` 149 | 150 | Again, if the previous command was executed a message will be received immediately: 151 | 152 | ```json 153 | {"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"} 154 | ``` 155 | 156 | And once again, we can use the ID from this message to reply to the original sender, who might be waiting 157 | for this reply (notice we used the hex encoded public key to identify the receiver here, rather than an IP): 158 | 159 | ```bash 160 | mycelium message send 955bf6bea5e1150fd8e270c12e5b2fc08f08f7c5f3799d10550096cc137d671b "this is a reply" --reply-to 4a6c956e8d36381f 161 | ``` 162 | -------------------------------------------------------------------------------- /docs/packet.md: -------------------------------------------------------------------------------- 1 | # Packet 2 | 3 | A `Packet` is the largest communication object between established `peers`. All communication is done 4 | via these `packets`. The `packet` itself consists of a fixed size header, and a variable size body. 5 | The body contains a more specific type of data. 6 | 7 | ## Packet header 8 | 9 | The packet header has a fixed size of 4 bytes, with the following layout: 10 | 11 | ``` 12 | 0 1 2 3 13 | 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 14 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 15 | | Version | Type | Reserved | 16 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 17 | ``` 18 | 19 | The first byte is used to indicate the version of the protocol. Currently, only version 1 is supported 20 | (0x01). The next byte is used to indicate the type of the body. `0x00` indicates a data packet, while 21 | `0x01` indicates a control packet. The remaining 16 bits are currently reserved, and should be set to 22 | all 0. 23 | -------------------------------------------------------------------------------- /docs/private_network.md: -------------------------------------------------------------------------------- 1 | # Private network 2 | 3 | > Private network functionality is currently in an experimental stage 4 | 5 | While traffic is end-to-end encrypted in mycelium, any node in the network learns 6 | every available connected subnet (and can derive the associated default address 7 | in that subnet). As a result, running a mycelium node adds what is effectively 8 | a public interface to your computer, so everyone can send traffic to it. On top 9 | of this, the routing table consumes memory in relation to the amount of nodes in 10 | the network. To remedy this, people can opt to run a "private network". By configuring 11 | a pre shared key (and network name), only nodes which know the key associated to 12 | the name can connect to your network. 13 | 14 | ## Implementation 15 | 16 | Private networks are implemented entirely in the connection layer (no specific 17 | protocol logic is implemented to support this). This relies on the pre shared key 18 | functionality of TLS 1.3. As such, you need both a `network name` (an `identity`), 19 | and the `PSK` itself. Next to the limitations in the protocol, we currently further 20 | limit the network name and PSK as follows: 21 | 22 | - Network name must be a UTF-8 encoded string of 2 to 64 bytes. 23 | - PSK must be exactly 32 bytes. 24 | 25 | Not all cipher suites supported in TLS1.3 are supported. At present, _at least_ 26 | `TLS_AES_128_GCM_SHA256` and `TLS_CHACHA20_POLY1305_SHA256` are supported. 27 | 28 | ## Enable private network 29 | 30 | In order to use the private network implementation of `mycelium`, a separate `mycelium-private` 31 | binary is available. Private network functionality can be enabled by setting both 32 | the `network-name` and `network-key-file` flags on the command line. All nodes who 33 | wish to join the network must use the same values for both flags. 34 | 35 | > ⚠️ Network name is public, do not put any confidential data here. 36 | -------------------------------------------------------------------------------- /docs/topic_configuration.md: -------------------------------------------------------------------------------- 1 | # Topic Configuration Guide 2 | 3 | This document explains how to configure message topics in Mycelium, including how to add new topics, configure socket forwarding paths, and manage whitelisted subnets. 4 | 5 | ## Overview 6 | 7 | Mycelium's messaging system uses topics to categorize and route messages. Each topic can be configured with: 8 | 9 | - **Whitelisted subnets**: IP subnets that are allowed to send messages to this topic 10 | - **Forward socket**: A Unix domain socket path where messages for this topic will be forwarded 11 | 12 | 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. 13 | 14 | ## Configuration Using JSON-RPC API 15 | 16 | The JSON-RPC API provides a comprehensive set of methods for managing topics, socket forwarding paths, and whitelisted subnets. 17 | 18 | ## Adding a New Topic 19 | 20 | ### Using the JSON-RPC API 21 | 22 | ```json 23 | { 24 | "jsonrpc": "2.0", 25 | "method": "addTopic", 26 | "params": ["dGVzdC10b3BpYw=="], // base64 encoding of "test-topic" 27 | "id": 1 28 | } 29 | ``` 30 | 31 | Example using curl: 32 | 33 | ```bash 34 | curl -X POST http://localhost:8990/rpc \ 35 | -H "Content-Type: application/json" \ 36 | -d '{ 37 | "jsonrpc": "2.0", 38 | "method": "addTopic", 39 | "params": ["dGVzdC10b3BpYw=="], 40 | "id": 1 41 | }' 42 | ``` 43 | 44 | 45 | ## Configuring a Socket Forwarding Path 46 | 47 | 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. 48 | 49 | ### Using the JSON-RPC API 50 | 51 | ```json 52 | { 53 | "jsonrpc": "2.0", 54 | "method": "setTopicForwardSocket", 55 | "params": ["dGVzdC10b3BpYw==", "/path/to/socket"], 56 | "id": 1 57 | } 58 | ``` 59 | 60 | Example using curl: 61 | 62 | ```bash 63 | curl -X POST http://localhost:8990/rpc \ 64 | -H "Content-Type: application/json" \ 65 | -d '{ 66 | "jsonrpc": "2.0", 67 | "method": "setTopicForwardSocket", 68 | "params": ["dGVzdC10b3BpYw==", "/path/to/socket"], 69 | "id": 1 70 | }' 71 | ``` 72 | ``` 73 | 74 | ## Adding a Whitelisted Subnet 75 | 76 | 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. 77 | 78 | ### Using the JSON-RPC API 79 | 80 | ```json 81 | { 82 | "jsonrpc": "2.0", 83 | "method": "addTopicSource", 84 | "params": ["dGVzdC10b3BpYw==", "192.168.1.0/24"], 85 | "id": 1 86 | } 87 | ``` 88 | 89 | Example using curl: 90 | 91 | ```bash 92 | curl -X POST http://localhost:8990/rpc \ 93 | -H "Content-Type: application/json" \ 94 | -d '{ 95 | "jsonrpc": "2.0", 96 | "method": "addTopicSource", 97 | "params": ["dGVzdC10b3BpYw==", "192.168.1.0/24"], 98 | "id": 1 99 | }' 100 | ``` 101 | ``` 102 | 103 | ## Setting the Default Topic Action 104 | 105 | You can configure the default action to take for topics that don't have explicit whitelist configurations: 106 | 107 | ### Using the JSON-RPC API 108 | 109 | ```json 110 | { 111 | "jsonrpc": "2.0", 112 | "method": "setDefaultTopicAction", 113 | "params": [true], 114 | "id": 1 115 | } 116 | ``` 117 | 118 | Example using curl: 119 | 120 | ```bash 121 | curl -X POST http://localhost:8990/rpc \ 122 | -H "Content-Type: application/json" \ 123 | -d '{ 124 | "jsonrpc": "2.0", 125 | "method": "setDefaultTopicAction", 126 | "params": [true], 127 | "id": 1 128 | }' 129 | ``` 130 | ``` 131 | 132 | ## Socket Protocol 133 | 134 | 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. 135 | 136 | The socket protocol is simple: 137 | 1. The message data is written to the socket 138 | 2. The system waits for a reply from the socket (with a configurable timeout) 139 | 3. The reply data is read from the socket and sent back to the original sender 140 | 141 | ## Example: Creating a Socket Server 142 | 143 | Here's an example of a simple socket server that echoes back the received data: 144 | 145 | ```rust 146 | use std::{ 147 | io::{Read, Write}, 148 | os::unix::net::UnixListener, 149 | path::Path, 150 | thread, 151 | }; 152 | 153 | fn main() { 154 | let socket_path = "/tmp/mycelium-socket"; 155 | 156 | // Remove the socket file if it already exists 157 | if Path::new(socket_path).exists() { 158 | std::fs::remove_file(socket_path).unwrap(); 159 | } 160 | 161 | // Create the Unix domain socket 162 | let listener = UnixListener::bind(socket_path).unwrap(); 163 | println!("Socket server listening on {}", socket_path); 164 | 165 | // Accept connections in a loop 166 | for stream in listener.incoming() { 167 | match stream { 168 | Ok(mut stream) => { 169 | // Spawn a thread to handle the connection 170 | thread::spawn(move || { 171 | // Read the data 172 | let mut buffer = Vec::new(); 173 | stream.read_to_end(&mut buffer).unwrap(); 174 | println!("Received {} bytes", buffer.len()); 175 | 176 | // Process the data (in this case, just echo it back) 177 | // In a real application, you would parse and process the message here 178 | 179 | // Send the reply 180 | stream.write_all(&buffer).unwrap(); 181 | println!("Sent reply"); 182 | }); 183 | } 184 | Err(e) => { 185 | eprintln!("Error accepting connection: {}", e); 186 | } 187 | } 188 | } 189 | } 190 | ``` 191 | 192 | ## Troubleshooting 193 | 194 | ### Message Not Being Forwarded to Socket 195 | 196 | 1. Check that the topic is correctly configured with a socket path 197 | 2. Verify that the socket server is running and the socket file exists 198 | 3. Ensure that the sender's IP is in the whitelisted subnets for the topic 199 | 4. Check the logs for any socket connection or timeout errors 200 | 201 | ### Socket Server Not Receiving Messages 202 | 203 | 1. Verify that the socket path is correct and accessible 204 | 2. Check that the socket server has the necessary permissions to read/write to the socket 205 | 3. Ensure that the socket server is properly handling the connection 206 | 207 | ### Reply Not Being Sent Back 208 | 209 | 1. Verify that the socket server is sending a reply 210 | 2. Check for any timeout errors in the logs 211 | 3. Ensure that the original sender is still connected and able to receive the reply 212 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "locked": { 5 | "lastModified": 1742317686, 6 | "narHash": "sha256-ScJYnUykEDhYeCepoAWBbZWx2fpQ8ottyvOyGry7HqE=", 7 | "owner": "ipetkov", 8 | "repo": "crane", 9 | "rev": "66cb0013f9a99d710b167ad13cbd8cc4e64f2ddb", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "ipetkov", 14 | "repo": "crane", 15 | "type": "github" 16 | } 17 | }, 18 | "flake-compat": { 19 | "locked": { 20 | "lastModified": 1733328505, 21 | "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 22 | "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 23 | "revCount": 69, 24 | "type": "tarball", 25 | "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz" 26 | }, 27 | "original": { 28 | "type": "tarball", 29 | "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" 30 | } 31 | }, 32 | "flake-utils": { 33 | "inputs": { 34 | "systems": "systems" 35 | }, 36 | "locked": { 37 | "lastModified": 1731533236, 38 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 39 | "owner": "numtide", 40 | "repo": "flake-utils", 41 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 42 | "type": "github" 43 | }, 44 | "original": { 45 | "id": "flake-utils", 46 | "type": "indirect" 47 | } 48 | }, 49 | "nix-filter": { 50 | "locked": { 51 | "lastModified": 1731533336, 52 | "narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=", 53 | "owner": "numtide", 54 | "repo": "nix-filter", 55 | "rev": "f7653272fd234696ae94229839a99b73c9ab7de0", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "numtide", 60 | "repo": "nix-filter", 61 | "type": "github" 62 | } 63 | }, 64 | "nixpkgs": { 65 | "locked": { 66 | "lastModified": 1742288794, 67 | "narHash": "sha256-Txwa5uO+qpQXrNG4eumPSD+hHzzYi/CdaM80M9XRLCo=", 68 | "owner": "NixOS", 69 | "repo": "nixpkgs", 70 | "rev": "b6eaf97c6960d97350c584de1b6dcff03c9daf42", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "NixOS", 75 | "ref": "nixos-unstable", 76 | "repo": "nixpkgs", 77 | "type": "github" 78 | } 79 | }, 80 | "root": { 81 | "inputs": { 82 | "crane": "crane", 83 | "flake-compat": "flake-compat", 84 | "flake-utils": "flake-utils", 85 | "nix-filter": "nix-filter", 86 | "nixpkgs": "nixpkgs" 87 | } 88 | }, 89 | "systems": { 90 | "locked": { 91 | "lastModified": 1681028828, 92 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 93 | "owner": "nix-systems", 94 | "repo": "default", 95 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 96 | "type": "github" 97 | }, 98 | "original": { 99 | "owner": "nix-systems", 100 | "repo": "default", 101 | "type": "github" 102 | } 103 | } 104 | }, 105 | "root": "root", 106 | "version": 7 107 | } 108 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | 5 | crane.url = "github:ipetkov/crane"; 6 | 7 | flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; 8 | 9 | nix-filter.url = "github:numtide/nix-filter"; 10 | }; 11 | 12 | outputs = 13 | { self 14 | , crane 15 | , flake-utils 16 | , nix-filter 17 | , ... 18 | }@inputs: 19 | { 20 | overlays.default = final: prev: 21 | let 22 | inherit (final) lib stdenv darwin; 23 | craneLib = crane.mkLib final; 24 | in 25 | { 26 | myceliumd = 27 | let 28 | cargoToml = ./myceliumd/Cargo.toml; 29 | cargoLock = ./myceliumd/Cargo.lock; 30 | manifest = craneLib.crateNameFromCargoToml { inherit cargoToml; }; 31 | in 32 | lib.makeOverridable craneLib.buildPackage { 33 | src = nix-filter { 34 | root = ./.; 35 | 36 | # If no include is passed, it will include all the paths. 37 | include = [ 38 | ./Cargo.toml 39 | ./Cargo.lock 40 | ./mycelium 41 | ./mycelium-api 42 | ./mycelium-cli 43 | ./mycelium-metrics 44 | ./myceliumd 45 | ./myceliumd-private 46 | ./mobile 47 | ./docs 48 | ]; 49 | }; 50 | 51 | inherit (manifest) pname version; 52 | inherit cargoToml cargoLock; 53 | sourceRoot = "source/myceliumd"; 54 | 55 | doCheck = false; 56 | 57 | nativeBuildInputs = [ 58 | final.pkg-config 59 | # openssl base library 60 | final.openssl 61 | # required by openssl-sys 62 | final.perl 63 | ]; 64 | 65 | buildInputs = lib.optionals stdenv.isDarwin [ 66 | darwin.apple_sdk.frameworks.Security 67 | darwin.apple_sdk.frameworks.SystemConfiguration 68 | final.libiconv 69 | ]; 70 | 71 | meta = { 72 | mainProgram = "mycelium"; 73 | }; 74 | }; 75 | myceliumd-private = 76 | let 77 | cargoToml = ./myceliumd-private/Cargo.toml; 78 | cargoLock = ./myceliumd-private/Cargo.lock; 79 | manifest = craneLib.crateNameFromCargoToml { inherit cargoToml; }; 80 | in 81 | lib.makeOverridable craneLib.buildPackage { 82 | src = nix-filter { 83 | root = ./.; 84 | 85 | include = [ 86 | ./Cargo.toml 87 | ./Cargo.lock 88 | ./mycelium 89 | ./mycelium-api 90 | ./mycelium-cli 91 | ./mycelium-metrics 92 | ./myceliumd 93 | ./myceliumd-private 94 | ./mobile 95 | ./docs 96 | ]; 97 | }; 98 | 99 | inherit (manifest) pname version; 100 | inherit cargoToml cargoLock; 101 | sourceRoot = "source/myceliumd-private"; 102 | 103 | doCheck = false; 104 | 105 | nativeBuildInputs = [ 106 | final.pkg-config 107 | # openssl base library 108 | final.openssl 109 | # required by openssl-sys 110 | final.perl 111 | ]; 112 | 113 | buildInputs = lib.optionals stdenv.isDarwin [ 114 | darwin.apple_sdk.frameworks.Security 115 | darwin.apple_sdk.frameworks.SystemConfiguration 116 | final.libiconv 117 | ]; 118 | 119 | meta = { 120 | mainProgram = "mycelium-private"; 121 | }; 122 | }; 123 | }; 124 | } // 125 | flake-utils.lib.eachSystem 126 | [ 127 | flake-utils.lib.system.x86_64-linux 128 | flake-utils.lib.system.aarch64-linux 129 | flake-utils.lib.system.x86_64-darwin 130 | flake-utils.lib.system.aarch64-darwin 131 | ] 132 | (system: 133 | let 134 | craneLib = crane.mkLib pkgs; 135 | 136 | pkgs = import inputs.nixpkgs { 137 | inherit system; 138 | overlays = [ self.overlays.default ]; 139 | }; 140 | in 141 | { 142 | devShells.default = craneLib.devShell { 143 | packages = [ 144 | pkgs.rust-analyzer 145 | ]; 146 | 147 | RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; 148 | }; 149 | 150 | packages = { 151 | default = self.packages.${system}.myceliumd; 152 | 153 | inherit (pkgs) myceliumd myceliumd-private; 154 | }; 155 | }); 156 | } 157 | -------------------------------------------------------------------------------- /installers/windows/wix/mycelium.en-us.wxl: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /installers/windows/wix/mycelium.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 38 | 39 | 40 | 41 | 42 | 43 | 48 | 53 | 60 | 61 | 70 | 71 | 77 | 82 | 83 | 84 | 85 | 86 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /mobile/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /mobile/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mobile" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | mactunfd = ["mycelium/mactunfd"] 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | mycelium = { path = "../mycelium", features = ["vendored-openssl"] } 13 | tokio = { version = "1.44.0", features = ["signal", "rt-multi-thread"] } 14 | thiserror = "2.0.12" 15 | tracing = { version = "0.1.41", features = ["release_max_level_debug"] } 16 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 17 | once_cell = "1.21.1" 18 | 19 | [target.'cfg(target_os = "android")'.dependencies] 20 | tracing-android = "0.2.0" 21 | 22 | [target.'cfg(target_os = "ios")'.dependencies] 23 | tracing-oslog = "0.2.0" 24 | 25 | [target.'cfg(target_os = "macos")'.dependencies] 26 | tracing-oslog = "0.2.0" 27 | -------------------------------------------------------------------------------- /mobile/README.md: -------------------------------------------------------------------------------- 1 | # mobile crate 2 | 3 | This crate will be called from Dart/Flutter, Kotlin(Android), or Swift(iOS) -------------------------------------------------------------------------------- /mycelium-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mycelium-api" 3 | version = "0.6.1" 4 | edition = "2021" 5 | license-file = "../LICENSE" 6 | readme = "../README.md" 7 | 8 | [features] 9 | message = ["mycelium/message"] 10 | 11 | [dependencies] 12 | axum = { version = "0.8.4", default-features = false, features = [ 13 | "http1", 14 | "http2", 15 | "json", 16 | "query", 17 | "tokio", 18 | ] } 19 | base64 = "0.22.1" 20 | jsonrpsee = { version = "0.25.1", features = [ 21 | "server", 22 | "macros", 23 | "jsonrpsee-types", 24 | ] } 25 | serde_json = "1.0.140" 26 | tracing = "0.1.41" 27 | tokio = { version = "1.44.2", default-features = false, features = [ 28 | "net", 29 | "rt", 30 | ] } 31 | mycelium = { path = "../mycelium" } 32 | mycelium-metrics = { path = "../mycelium-metrics", features = ["prometheus"] } 33 | serde = { version = "1.0.219", features = ["derive"] } 34 | async-trait = "0.1.88" 35 | 36 | [dev-dependencies] 37 | serde_json = "1.0.140" 38 | -------------------------------------------------------------------------------- /mycelium-api/src/rpc/admin.rs: -------------------------------------------------------------------------------- 1 | //! Admin-related JSON-RPC methods for the Mycelium API 2 | 3 | use jsonrpc_core::{Error, ErrorCode, Result as RpcResult}; 4 | use std::net::IpAddr; 5 | use std::str::FromStr; 6 | use tracing::debug; 7 | 8 | use mycelium::crypto::PublicKey; 9 | use mycelium::metrics::Metrics; 10 | 11 | use crate::HttpServerState; 12 | use crate::Info; 13 | use crate::rpc::models::error_codes; 14 | use crate::rpc::traits::AdminApi; 15 | 16 | /// Implementation of Admin-related JSON-RPC methods 17 | pub struct AdminRpc 18 | where 19 | M: Metrics + Clone + Send + Sync + 'static, 20 | { 21 | state: HttpServerState, 22 | } 23 | 24 | impl AdminRpc 25 | where 26 | M: Metrics + Clone + Send + Sync + 'static, 27 | { 28 | /// Create a new AdminRpc instance 29 | pub fn new(state: HttpServerState) -> Self { 30 | Self { state } 31 | } 32 | } 33 | 34 | impl AdminApi for AdminRpc 35 | where 36 | M: Metrics + Clone + Send + Sync + 'static, 37 | { 38 | fn get_info(&self) -> RpcResult { 39 | debug!("Getting node info via RPC"); 40 | let info = self.state.node.blocking_lock().info(); 41 | Ok(Info { 42 | node_subnet: info.node_subnet.to_string(), 43 | node_pubkey: info.node_pubkey, 44 | }) 45 | } 46 | 47 | fn get_pubkey_from_ip(&self, mycelium_ip: String) -> RpcResult { 48 | debug!(ip = %mycelium_ip, "Getting public key from IP via RPC"); 49 | let ip = IpAddr::from_str(&mycelium_ip).map_err(|e| Error { 50 | code: ErrorCode::InvalidParams, 51 | message: format!("Invalid IP address: {}", e), 52 | data: None, 53 | })?; 54 | 55 | match self.state.node.blocking_lock().get_pubkey_from_ip(ip) { 56 | Some(pubkey) => Ok(pubkey), 57 | None => Err(Error { 58 | code: ErrorCode::ServerError(error_codes::PUBKEY_NOT_FOUND), 59 | message: "Public key not found".to_string(), 60 | data: None, 61 | }), 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /mycelium-api/src/rpc/models.rs: -------------------------------------------------------------------------------- 1 | //! Models for the Mycelium JSON-RPC API 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | // Define any additional models needed for the JSON-RPC API 6 | // Most models can be reused from the existing REST API 7 | 8 | /// Error codes for the JSON-RPC API 9 | pub mod error_codes { 10 | /// Invalid parameters error code 11 | pub const INVALID_PARAMS: i64 = -32602; 12 | 13 | /// Peer already exists error code 14 | pub const PEER_EXISTS: i64 = 409; 15 | 16 | /// Peer not found error code 17 | pub const PEER_NOT_FOUND: i64 = 404; 18 | 19 | /// Message not found error code 20 | pub const MESSAGE_NOT_FOUND: i64 = 404; 21 | 22 | /// Public key not found error code 23 | pub const PUBKEY_NOT_FOUND: i64 = 404; 24 | 25 | /// No message ready error code 26 | pub const NO_MESSAGE_READY: i64 = 204; 27 | 28 | /// Timeout waiting for reply error code 29 | pub const TIMEOUT_WAITING_FOR_REPLY: i64 = 408; 30 | } -------------------------------------------------------------------------------- /mycelium-api/src/rpc/peer.rs: -------------------------------------------------------------------------------- 1 | //! Peer-related JSON-RPC methods for the Mycelium API 2 | 3 | use jsonrpc_core::{Error, ErrorCode, Result as RpcResult}; 4 | use std::str::FromStr; 5 | use tracing::debug; 6 | 7 | use mycelium::endpoint::Endpoint; 8 | use mycelium::metrics::Metrics; 9 | use mycelium::peer_manager::{PeerExists, PeerNotFound, PeerStats}; 10 | 11 | use crate::rpc::models::error_codes; 12 | use crate::rpc::traits::PeerApi; 13 | use crate::HttpServerState; 14 | 15 | /// Implementation of Peer-related JSON-RPC methods 16 | pub struct PeerRpc 17 | where 18 | M: Metrics + Clone + Send + Sync + 'static, 19 | { 20 | state: HttpServerState, 21 | } 22 | 23 | impl PeerRpc 24 | where 25 | M: Metrics + Clone + Send + Sync + 'static, 26 | { 27 | /// Create a new PeerRpc instance 28 | pub fn new(state: HttpServerState) -> Self { 29 | Self { state } 30 | } 31 | } 32 | 33 | impl PeerApi for PeerRpc 34 | where 35 | M: Metrics + Clone + Send + Sync + 'static, 36 | { 37 | fn get_peers(&self) -> RpcResult> { 38 | debug!("Fetching peer stats via RPC"); 39 | Ok(self.state.node.blocking_lock().peer_info()) 40 | } 41 | 42 | fn add_peer(&self, endpoint: String) -> RpcResult { 43 | debug!( 44 | peer.endpoint = endpoint, 45 | "Attempting to add peer to the system via RPC" 46 | ); 47 | 48 | let endpoint = Endpoint::from_str(&endpoint).map_err(|e| Error { 49 | code: ErrorCode::InvalidParams, 50 | message: e.to_string(), 51 | data: None, 52 | })?; 53 | 54 | match self.state.node.blocking_lock().add_peer(endpoint) { 55 | Ok(()) => Ok(true), 56 | Err(PeerExists) => Err(Error { 57 | code: ErrorCode::ServerError(error_codes::PEER_EXISTS), 58 | message: "A peer identified by that endpoint already exists".to_string(), 59 | data: None, 60 | }), 61 | } 62 | } 63 | 64 | fn delete_peer(&self, endpoint: String) -> RpcResult { 65 | debug!( 66 | peer.endpoint = endpoint, 67 | "Attempting to remove peer from the system via RPC" 68 | ); 69 | 70 | let endpoint = Endpoint::from_str(&endpoint).map_err(|e| Error { 71 | code: ErrorCode::InvalidParams, 72 | message: e.to_string(), 73 | data: None, 74 | })?; 75 | 76 | match self.state.node.blocking_lock().remove_peer(endpoint) { 77 | Ok(()) => Ok(true), 78 | Err(PeerNotFound) => Err(Error { 79 | code: ErrorCode::ServerError(error_codes::PEER_NOT_FOUND), 80 | message: "A peer identified by that endpoint does not exist".to_string(), 81 | data: None, 82 | }), 83 | } 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /mycelium-api/src/rpc/route.rs: -------------------------------------------------------------------------------- 1 | //! Route-related JSON-RPC methods for the Mycelium API 2 | 3 | use jsonrpc_core::Result as RpcResult; 4 | use tracing::debug; 5 | 6 | use mycelium::metrics::Metrics; 7 | 8 | use crate::HttpServerState; 9 | use crate::Route; 10 | use crate::QueriedSubnet; 11 | use crate::NoRouteSubnet; 12 | use crate::Metric; 13 | use crate::rpc::traits::RouteApi; 14 | 15 | /// Implementation of Route-related JSON-RPC methods 16 | pub struct RouteRpc 17 | where 18 | M: Metrics + Clone + Send + Sync + 'static, 19 | { 20 | state: HttpServerState, 21 | } 22 | 23 | impl RouteRpc 24 | where 25 | M: Metrics + Clone + Send + Sync + 'static, 26 | { 27 | /// Create a new RouteRpc instance 28 | pub fn new(state: HttpServerState) -> Self { 29 | Self { state } 30 | } 31 | } 32 | 33 | impl RouteApi for RouteRpc 34 | where 35 | M: Metrics + Clone + Send + Sync + 'static, 36 | { 37 | fn get_selected_routes(&self) -> RpcResult> { 38 | debug!("Loading selected routes via RPC"); 39 | let routes = self.state 40 | .node 41 | .blocking_lock() 42 | .selected_routes() 43 | .into_iter() 44 | .map(|sr| Route { 45 | subnet: sr.source().subnet().to_string(), 46 | next_hop: sr.neighbour().connection_identifier().clone(), 47 | metric: if sr.metric().is_infinite() { 48 | Metric::Infinite 49 | } else { 50 | Metric::Value(sr.metric().into()) 51 | }, 52 | seqno: sr.seqno().into(), 53 | }) 54 | .collect(); 55 | 56 | Ok(routes) 57 | } 58 | 59 | fn get_fallback_routes(&self) -> RpcResult> { 60 | debug!("Loading fallback routes via RPC"); 61 | let routes = self.state 62 | .node 63 | .blocking_lock() 64 | .fallback_routes() 65 | .into_iter() 66 | .map(|sr| Route { 67 | subnet: sr.source().subnet().to_string(), 68 | next_hop: sr.neighbour().connection_identifier().clone(), 69 | metric: if sr.metric().is_infinite() { 70 | Metric::Infinite 71 | } else { 72 | Metric::Value(sr.metric().into()) 73 | }, 74 | seqno: sr.seqno().into(), 75 | }) 76 | .collect(); 77 | 78 | Ok(routes) 79 | } 80 | 81 | fn get_queried_subnets(&self) -> RpcResult> { 82 | debug!("Loading queried subnets via RPC"); 83 | let queries = self.state 84 | .node 85 | .blocking_lock() 86 | .queried_subnets() 87 | .into_iter() 88 | .map(|qs| QueriedSubnet { 89 | subnet: qs.subnet().to_string(), 90 | expiration: qs 91 | .query_expires() 92 | .duration_since(tokio::time::Instant::now()) 93 | .as_secs() 94 | .to_string(), 95 | }) 96 | .collect(); 97 | 98 | Ok(queries) 99 | } 100 | 101 | fn get_no_route_entries(&self) -> RpcResult> { 102 | debug!("Loading no route entries via RPC"); 103 | let entries = self.state 104 | .node 105 | .blocking_lock() 106 | .no_route_entries() 107 | .into_iter() 108 | .map(|nrs| NoRouteSubnet { 109 | subnet: nrs.subnet().to_string(), 110 | expiration: nrs 111 | .entry_expires() 112 | .duration_since(tokio::time::Instant::now()) 113 | .as_secs() 114 | .to_string(), 115 | }) 116 | .collect(); 117 | 118 | Ok(entries) 119 | } 120 | } -------------------------------------------------------------------------------- /mycelium-api/src/rpc/spec.rs: -------------------------------------------------------------------------------- 1 | //! OpenRPC specification for the Mycelium JSON-RPC API 2 | 3 | /// The OpenRPC specification for the Mycelium JSON-RPC API 4 | pub const OPENRPC_SPEC: &str = include_str!("../../../docs/openrpc.json"); 5 | -------------------------------------------------------------------------------- /mycelium-api/src/rpc/traits.rs: -------------------------------------------------------------------------------- 1 | //! RPC trait definitions for the Mycelium JSON-RPC API 2 | 3 | use jsonrpc_core::Result as RpcResult; 4 | use jsonrpc_derive::rpc; 5 | 6 | use crate::Info; 7 | use crate::Route; 8 | use crate::QueriedSubnet; 9 | use crate::NoRouteSubnet; 10 | use mycelium::crypto::PublicKey; 11 | use mycelium::peer_manager::PeerStats; 12 | use mycelium::message::{MessageId, MessageInfo}; 13 | 14 | // Admin-related RPC methods 15 | #[rpc] 16 | pub trait AdminApi { 17 | /// Get general info about the node 18 | #[rpc(name = "getInfo")] 19 | fn get_info(&self) -> RpcResult; 20 | 21 | /// Get the pubkey from node ip 22 | #[rpc(name = "getPublicKeyFromIp")] 23 | fn get_pubkey_from_ip(&self, mycelium_ip: String) -> RpcResult; 24 | } 25 | 26 | // Peer-related RPC methods 27 | #[rpc] 28 | pub trait PeerApi { 29 | /// List known peers 30 | #[rpc(name = "getPeers")] 31 | fn get_peers(&self) -> RpcResult>; 32 | 33 | /// Add a new peer 34 | #[rpc(name = "addPeer")] 35 | fn add_peer(&self, endpoint: String) -> RpcResult; 36 | 37 | /// Remove an existing peer 38 | #[rpc(name = "deletePeer")] 39 | fn delete_peer(&self, endpoint: String) -> RpcResult; 40 | } 41 | 42 | // Route-related RPC methods 43 | #[rpc] 44 | pub trait RouteApi { 45 | /// List all selected routes 46 | #[rpc(name = "getSelectedRoutes")] 47 | fn get_selected_routes(&self) -> RpcResult>; 48 | 49 | /// List all active fallback routes 50 | #[rpc(name = "getFallbackRoutes")] 51 | fn get_fallback_routes(&self) -> RpcResult>; 52 | 53 | /// List all currently queried subnets 54 | #[rpc(name = "getQueriedSubnets")] 55 | fn get_queried_subnets(&self) -> RpcResult>; 56 | 57 | /// List all subnets which are explicitly marked as no route 58 | #[rpc(name = "getNoRouteEntries")] 59 | fn get_no_route_entries(&self) -> RpcResult>; 60 | } 61 | 62 | // Message-related RPC methods 63 | #[rpc] 64 | pub trait MessageApi { 65 | /// Get a message from the inbound message queue 66 | #[rpc(name = "popMessage")] 67 | fn pop_message(&self, peek: Option, timeout: Option, topic: Option) -> RpcResult; 68 | 69 | /// Submit a new message to the system 70 | #[rpc(name = "pushMessage")] 71 | fn push_message(&self, message: crate::message::MessageSendInfo, reply_timeout: Option) -> RpcResult; 72 | 73 | /// Reply to a message with the given ID 74 | #[rpc(name = "pushMessageReply")] 75 | fn push_message_reply(&self, id: String, message: crate::message::MessageSendInfo) -> RpcResult; 76 | 77 | /// Get the status of an outbound message 78 | #[rpc(name = "getMessageInfo")] 79 | fn get_message_info(&self, id: String) -> RpcResult; 80 | } -------------------------------------------------------------------------------- /mycelium-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mycelium-cli" 3 | version = "0.6.1" 4 | edition = "2021" 5 | license-file = "../LICENSE" 6 | readme = "./README.md" 7 | 8 | [features] 9 | message = ["mycelium/message", "mycelium-api/message"] 10 | 11 | [dependencies] 12 | mycelium = { path = "../mycelium" } 13 | mycelium-api = { path = "../mycelium-api" } 14 | serde = { version = "1.0.219", features = ["derive"] } 15 | serde_json = "1.0.140" 16 | base64 = "0.22.1" 17 | prettytable-rs = "0.10.0" 18 | tracing = "0.1.41" 19 | tokio = { version = "1.44.2", default-features = false, features = [ 20 | "net", 21 | "rt", 22 | "fs", 23 | ] } 24 | reqwest = { version = "0.12.15", default-features = false, features = ["json"] } 25 | byte-unit = "5.1.6" 26 | urlencoding = "2.1.3" 27 | -------------------------------------------------------------------------------- /mycelium-cli/src/inspect.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | use mycelium::crypto::PublicKey; 4 | use serde::Serialize; 5 | 6 | #[derive(Debug, Serialize)] 7 | struct InspectOutput { 8 | #[serde(rename = "publicKey")] 9 | public_key: PublicKey, 10 | address: IpAddr, 11 | } 12 | 13 | /// Inspect the given pubkey, or the local key if no pubkey is given 14 | pub fn inspect(pubkey: PublicKey, json: bool) -> Result<(), Box> { 15 | let address = pubkey.address().into(); 16 | if json { 17 | let out = InspectOutput { 18 | public_key: pubkey, 19 | address, 20 | }; 21 | 22 | let out_string = serde_json::to_string_pretty(&out)?; 23 | println!("{out_string}"); 24 | } else { 25 | println!("Public key: {pubkey}"); 26 | println!("Address: {address}"); 27 | } 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /mycelium-cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod inspect; 2 | #[cfg(feature = "message")] 3 | mod message; 4 | mod peer; 5 | mod routes; 6 | 7 | pub use inspect::inspect; 8 | #[cfg(feature = "message")] 9 | pub use message::{recv_msg, send_msg}; 10 | pub use peer::{add_peers, list_peers, remove_peers}; 11 | pub use routes::{ 12 | list_fallback_routes, list_no_route_entries, list_queried_subnets, list_selected_routes, 13 | }; 14 | -------------------------------------------------------------------------------- /mycelium-cli/src/peer.rs: -------------------------------------------------------------------------------- 1 | use mycelium::peer_manager::PeerStats; 2 | use mycelium_api::AddPeer; 3 | use prettytable::{row, Table}; 4 | use std::net::SocketAddr; 5 | use tracing::{debug, error}; 6 | 7 | /// List the peers the current node is connected to 8 | pub async fn list_peers( 9 | server_addr: SocketAddr, 10 | json_print: bool, 11 | ) -> Result<(), Box> { 12 | // Make API call 13 | let request_url = format!("http://{server_addr}/api/v1/admin/peers"); 14 | match reqwest::get(&request_url).await { 15 | Err(e) => { 16 | error!("Failed to retrieve peers"); 17 | return Err(e.into()); 18 | } 19 | Ok(resp) => { 20 | debug!("Listing connected peers"); 21 | match resp.json::>().await { 22 | Err(e) => { 23 | error!("Failed to load response json: {e}"); 24 | return Err(e.into()); 25 | } 26 | Ok(peers) => { 27 | if json_print { 28 | // Print peers in JSON format 29 | let json_output = serde_json::to_string_pretty(&peers)?; 30 | println!("{json_output}"); 31 | } else { 32 | // Print peers in table format 33 | let mut table = Table::new(); 34 | table.add_row(row![ 35 | "Protocol", 36 | "Socket", 37 | "Type", 38 | "Connection", 39 | "Rx total", 40 | "Tx total", 41 | "Discovered", 42 | "Last connection" 43 | ]); 44 | for peer in peers.iter() { 45 | table.add_row(row![ 46 | peer.endpoint.proto(), 47 | peer.endpoint.address(), 48 | peer.pt, 49 | peer.connection_state, 50 | format_bytes(peer.rx_bytes), 51 | format_bytes(peer.tx_bytes), 52 | format_seconds(peer.discovered), 53 | peer.last_connected 54 | .map(format_seconds) 55 | .unwrap_or("Never connected".to_string()), 56 | ]); 57 | } 58 | table.printstd(); 59 | } 60 | } 61 | } 62 | } 63 | }; 64 | 65 | Ok(()) 66 | } 67 | 68 | fn format_bytes(bytes: u64) -> String { 69 | let byte = byte_unit::Byte::from_u64(bytes); 70 | let adjusted_byte = byte.get_appropriate_unit(byte_unit::UnitType::Binary); 71 | format!( 72 | "{:.2} {}", 73 | adjusted_byte.get_value(), 74 | adjusted_byte.get_unit() 75 | ) 76 | } 77 | 78 | /// Convert an amount of seconds into a human readable string. 79 | fn format_seconds(total_seconds: u64) -> String { 80 | let seconds = total_seconds % 60; 81 | let minutes = (total_seconds / 60) % 60; 82 | let hours = (total_seconds / 3600) % 60; 83 | let days = (total_seconds / 86400) % 60; 84 | 85 | if days > 0 { 86 | format!("{days}d {hours}h {minutes}m {seconds}s") 87 | } else if hours > 0 { 88 | format!("{hours}h {minutes}m {seconds}s") 89 | } else if minutes > 0 { 90 | format!("{minutes}m {seconds}s") 91 | } else { 92 | format!("{seconds}s") 93 | } 94 | } 95 | 96 | /// Remove peer(s) by (underlay) IP 97 | pub async fn remove_peers( 98 | server_addr: SocketAddr, 99 | peers: Vec, 100 | ) -> Result<(), Box> { 101 | let client = reqwest::Client::new(); 102 | for peer in peers.iter() { 103 | // encode to pass in URL 104 | let peer_encoded = urlencoding::encode(peer); 105 | let request_url = format!("http://{server_addr}/api/v1/admin/peers/{peer_encoded}"); 106 | if let Err(e) = client 107 | .delete(&request_url) 108 | .send() 109 | .await 110 | .and_then(|res| res.error_for_status()) 111 | { 112 | error!("Failed to delete peer: {e}"); 113 | return Err(e.into()); 114 | } 115 | } 116 | 117 | Ok(()) 118 | } 119 | 120 | /// Add peer(s) by (underlay) IP 121 | pub async fn add_peers( 122 | server_addr: SocketAddr, 123 | peers: Vec, 124 | ) -> Result<(), Box> { 125 | let client = reqwest::Client::new(); 126 | for peer in peers.into_iter() { 127 | let request_url = format!("http://{server_addr}/api/v1/admin/peers"); 128 | if let Err(e) = client 129 | .post(&request_url) 130 | .json(&AddPeer { endpoint: peer }) 131 | .send() 132 | .await 133 | .and_then(|res| res.error_for_status()) 134 | { 135 | error!("Failed to add peer: {e}"); 136 | return Err(e.into()); 137 | } 138 | } 139 | 140 | Ok(()) 141 | } 142 | -------------------------------------------------------------------------------- /mycelium-cli/src/routes.rs: -------------------------------------------------------------------------------- 1 | use mycelium_api::{NoRouteSubnet, QueriedSubnet, Route}; 2 | use prettytable::{row, Table}; 3 | use std::net::SocketAddr; 4 | 5 | use tracing::{debug, error}; 6 | 7 | pub async fn list_selected_routes( 8 | server_addr: SocketAddr, 9 | json_print: bool, 10 | ) -> Result<(), Box> { 11 | let request_url = format!("http://{server_addr}/api/v1/admin/routes/selected"); 12 | match reqwest::get(&request_url).await { 13 | Err(e) => { 14 | error!("Failed to retrieve selected routes"); 15 | return Err(e.into()); 16 | } 17 | Ok(resp) => { 18 | debug!("Listing selected routes"); 19 | 20 | if json_print { 21 | // API call returns routes in JSON format by default 22 | let selected_routes = resp.text().await?; 23 | println!("{selected_routes}"); 24 | } else { 25 | // Print routes in table format 26 | let routes: Vec = resp.json().await?; 27 | let mut table = Table::new(); 28 | table.add_row(row!["Subnet", "Next Hop", "Metric", "Seq No"]); 29 | 30 | for route in routes.iter() { 31 | table.add_row(row![ 32 | &route.subnet, 33 | &route.next_hop, 34 | route.metric, 35 | route.seqno, 36 | ]); 37 | } 38 | 39 | table.printstd(); 40 | } 41 | } 42 | } 43 | 44 | Ok(()) 45 | } 46 | 47 | pub async fn list_fallback_routes( 48 | server_addr: SocketAddr, 49 | json_print: bool, 50 | ) -> Result<(), Box> { 51 | let request_url = format!("http://{server_addr}/api/v1/admin/routes/fallback"); 52 | match reqwest::get(&request_url).await { 53 | Err(e) => { 54 | error!("Failed to retrieve fallback routes"); 55 | return Err(e.into()); 56 | } 57 | Ok(resp) => { 58 | debug!("Listing fallback routes"); 59 | 60 | if json_print { 61 | // API call returns routes in JSON format by default 62 | let fallback_routes = resp.text().await?; 63 | println!("{fallback_routes}"); 64 | } else { 65 | // Print routes in table format 66 | let routes: Vec = resp.json().await?; 67 | let mut table = Table::new(); 68 | table.add_row(row!["Subnet", "Next Hop", "Metric", "Seq No"]); 69 | 70 | for route in routes.iter() { 71 | table.add_row(row![ 72 | &route.subnet, 73 | &route.next_hop, 74 | route.metric, 75 | route.seqno, 76 | ]); 77 | } 78 | 79 | table.printstd(); 80 | } 81 | } 82 | } 83 | Ok(()) 84 | } 85 | 86 | pub async fn list_queried_subnets( 87 | server_addr: SocketAddr, 88 | json_print: bool, 89 | ) -> Result<(), Box> { 90 | let request_url = format!("http://{server_addr}/api/v1/admin/routes/queried"); 91 | match reqwest::get(&request_url).await { 92 | Err(e) => { 93 | error!("Failed to retrieve queried subnets"); 94 | return Err(e.into()); 95 | } 96 | Ok(resp) => { 97 | debug!("Listing queried routes"); 98 | 99 | if json_print { 100 | // API call returns routes in JSON format by default 101 | let queried_routes = resp.text().await?; 102 | println!("{queried_routes}"); 103 | } else { 104 | // Print routes in table format 105 | let queries: Vec = resp.json().await?; 106 | let mut table = Table::new(); 107 | table.add_row(row!["Subnet", "Query expiration"]); 108 | 109 | for query in queries.iter() { 110 | table.add_row(row![query.subnet, query.expiration,]); 111 | } 112 | 113 | table.printstd(); 114 | } 115 | } 116 | } 117 | Ok(()) 118 | } 119 | 120 | pub async fn list_no_route_entries( 121 | server_addr: SocketAddr, 122 | json_print: bool, 123 | ) -> Result<(), Box> { 124 | let request_url = format!("http://{server_addr}/api/v1/admin/routes/no_route"); 125 | match reqwest::get(&request_url).await { 126 | Err(e) => { 127 | error!("Failed to retrieve subnets with no route entries"); 128 | return Err(e.into()); 129 | } 130 | Ok(resp) => { 131 | debug!("Listing no route entries"); 132 | 133 | if json_print { 134 | // API call returns routes in JSON format by default 135 | let nrs = resp.text().await?; 136 | println!("{nrs}"); 137 | } else { 138 | // Print routes in table format 139 | let no_routes: Vec = resp.json().await?; 140 | let mut table = Table::new(); 141 | table.add_row(row!["Subnet", "Entry expiration"]); 142 | 143 | for nrs in no_routes.iter() { 144 | table.add_row(row![nrs.subnet, nrs.expiration,]); 145 | } 146 | 147 | table.printstd(); 148 | } 149 | } 150 | } 151 | Ok(()) 152 | } 153 | -------------------------------------------------------------------------------- /mycelium-metrics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mycelium-metrics" 3 | version = "0.6.1" 4 | edition = "2021" 5 | license-file = "../LICENSE" 6 | readme = "../README.md" 7 | 8 | [features] 9 | prometheus = ["dep:axum", "dep:prometheus", "dep:tokio", "dep:tracing"] 10 | 11 | [dependencies] 12 | axum = { version = "0.8.4", default-features = false, optional = true, features = [ 13 | "http1", 14 | "http2", 15 | "tokio", 16 | ] } 17 | mycelium = { path = "../mycelium", default-features = false } 18 | prometheus = { version = "0.14.0", default-features = false, optional = true, features = [ 19 | "process", 20 | ] } 21 | tokio = { version = "1.44.2", default-features = false, optional = true, features = [ 22 | "net", 23 | "rt", 24 | ] } 25 | tracing = { version = "0.1.41", optional = true } 26 | -------------------------------------------------------------------------------- /mycelium-metrics/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides implementations of [`the Metrics trait`](mycelium::metrics::Metrics). 2 | //! 2 options are exposed currently: a NOOP implementation which doesn't record anything, 3 | //! and a prometheus exporter which exposes all metrics in a promtheus compatible format. 4 | 5 | mod noop; 6 | pub use noop::NoMetrics; 7 | 8 | #[cfg(feature = "prometheus")] 9 | mod prometheus; 10 | #[cfg(feature = "prometheus")] 11 | pub use prometheus::PrometheusExporter; 12 | -------------------------------------------------------------------------------- /mycelium-metrics/src/noop.rs: -------------------------------------------------------------------------------- 1 | use mycelium::metrics::Metrics; 2 | 3 | #[derive(Clone)] 4 | pub struct NoMetrics; 5 | impl Metrics for NoMetrics {} 6 | -------------------------------------------------------------------------------- /mycelium-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /dist/ 5 | /static/ 6 | /.dioxus/ 7 | 8 | # These are backup files generated by rustfmt 9 | **/*.rs.bk 10 | -------------------------------------------------------------------------------- /mycelium-ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mycelium-ui" 3 | version = "0.6.1" 4 | edition = "2021" 5 | license-file = "../LICENSE" 6 | readme = "../README.md" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | 12 | dioxus = { version = "0.6.2", features = ["desktop", "router"] } 13 | mycelium = { path = "../mycelium" } 14 | mycelium-api = { path = "../mycelium-api" } 15 | 16 | # Debug 17 | tracing = "0.1.40" 18 | dioxus-logger = "0.6.2" 19 | reqwest = { version = "0.12.5", features = ["json"] } 20 | serde_json = "1.0.120" 21 | dioxus-sortable = "0.1.2" 22 | manganis = "0.6.2" 23 | dioxus-free-icons = { version = "0.9.0", features = [ 24 | "font-awesome-solid", 25 | "font-awesome-brands", 26 | "font-awesome-regular", 27 | ] } 28 | human_bytes = { version = "0.4.3", features = ["fast"] } 29 | tokio = "1.44.1" 30 | dioxus-charts = "0.3.1" 31 | futures-util = "0.3.31" 32 | urlencoding = "2.1.3" 33 | 34 | [features] 35 | bundle = [] 36 | -------------------------------------------------------------------------------- /mycelium-ui/Dioxus.toml: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | # App (Project) Name 4 | name = "mycelium-ui" 5 | 6 | # Dioxus App Default Platform 7 | # desktop, web 8 | default_platform = "desktop" 9 | 10 | # `build` & `serve` dist path 11 | out_dir = "dist" 12 | 13 | # assets file folder 14 | asset_dir = "assets" 15 | 16 | [web.app] 17 | 18 | # HTML title tag content 19 | title = "mycelium-ui" 20 | 21 | [web.watcher] 22 | 23 | # when watcher trigger, regenerate the `index.html` 24 | reload_html = true 25 | 26 | # which files or dirs will be watcher monitoring 27 | watch_path = ["src", "assets"] 28 | 29 | # add fallback 404 page 30 | index_on_404 = true 31 | 32 | # include `assets` in web platform 33 | [web.resource] 34 | 35 | # CSS style file 36 | 37 | style = [ 38 | "https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap", 39 | ] 40 | 41 | # Javascript code file 42 | script = [] 43 | 44 | [web.resource.dev] 45 | 46 | # Javascript code file 47 | # serve: [dev-server] only 48 | script = [] 49 | -------------------------------------------------------------------------------- /mycelium-ui/README.md: -------------------------------------------------------------------------------- 1 | # Mycelium Network Dashboard 2 | 3 | The Mycelium Network Dashboard is a GUI application built with Dioxus, a modern library for building 4 | cross-platform applications using Rust. More information about Dioxus can be found [here](https://dioxuslabs.com/) 5 | 6 | ## Getting Started 7 | 8 | To get started with the Mycelium Network Dashboard, you'll need to have the Dioxus CLI tool installed. 9 | You can install it using the following command: 10 | 11 | `cargo install dioxus-cli` 12 | 13 | Before running the Mycelium Network Dashboard application, make sure that the `myceliumd` daemon is running on your system. 14 | The myceliumd daemon is the background process that manages the Mycelium network connection 15 | and provides the data that the dashboard application displays. For more information on setting up and 16 | running `myceliumd`, please read [this](../README.md). 17 | 18 | Once you have the Dioxus CLI installed, you can build and run the application in development mode using 19 | the following command (in the `mycelium-ui` directory): 20 | 21 | `dx serve` 22 | 23 | This will start a development server and launch the application in a WebView. 24 | 25 | ## Bundling the application 26 | 27 | To bundle the application, you can use: 28 | 29 | `dx bundle --release --features bundle` 30 | 31 | This will create a bundled version of the application in the `dist/bundle/` directory. The bundled 32 | application can be distributed and run on various platforms, including Windows, MacOS and Linux. Dioxus 33 | also offers support for mobile, but note that this has not been tested. 34 | 35 | ## Documentation 36 | 37 | The Mycelium Network Dashboard application provides the following features: 38 | 39 | - **Home**: Displays information about the node and allows to change address of the API server on which 40 | the application should listen. 41 | - **Peers**: Shows and overview of all the connected peers. Adding and removing peers can be done here. 42 | - **Routes**: Provides information about the routing table and network routes 43 | 44 | 45 | ## Contributing 46 | 47 | If you would like to contribute to the Mycelium Network Dashboard project, please follow the standard GitHub workflow: 48 | 49 | 1. Fork the repository 50 | 2. Create a new branch for your changes 51 | 3. Make your changes and commit them 52 | 4. Push your changes to your forked repository 53 | 5. Submit a pull request to the main repository 54 | 55 | -------------------------------------------------------------------------------- /mycelium-ui/src/api.rs: -------------------------------------------------------------------------------- 1 | use mycelium::endpoint::Endpoint; 2 | use mycelium_api::AddPeer; 3 | use std::net::SocketAddr; 4 | use urlencoding::encode; 5 | 6 | pub async fn get_peers( 7 | server_addr: SocketAddr, 8 | ) -> Result, reqwest::Error> { 9 | let request_url = format!("http://{server_addr}/api/v1/admin/peers"); 10 | match reqwest::get(&request_url).await { 11 | Err(e) => Err(e), 12 | Ok(resp) => match resp.json::>().await { 13 | Err(e) => Err(e), 14 | Ok(peers) => Ok(peers), 15 | }, 16 | } 17 | } 18 | 19 | pub async fn get_selected_routes( 20 | server_addr: SocketAddr, 21 | ) -> Result, reqwest::Error> { 22 | let request_url = format!("http://{server_addr}/api/v1/admin/routes/selected"); 23 | match reqwest::get(&request_url).await { 24 | Err(e) => Err(e), 25 | Ok(resp) => match resp.json::>().await { 26 | Err(e) => Err(e), 27 | Ok(selected_routes) => Ok(selected_routes), 28 | }, 29 | } 30 | } 31 | 32 | pub async fn get_fallback_routes( 33 | server_addr: SocketAddr, 34 | ) -> Result, reqwest::Error> { 35 | let request_url = format!("http://{server_addr}/api/v1/admin/routes/fallback"); 36 | match reqwest::get(&request_url).await { 37 | Err(e) => Err(e), 38 | Ok(resp) => match resp.json::>().await { 39 | Err(e) => Err(e), 40 | Ok(selected_routes) => Ok(selected_routes), 41 | }, 42 | } 43 | } 44 | 45 | pub async fn get_node_info(server_addr: SocketAddr) -> Result { 46 | let request_url = format!("http://{server_addr}/api/v1/admin"); 47 | match reqwest::get(&request_url).await { 48 | Err(e) => Err(e), 49 | Ok(resp) => match resp.json::().await { 50 | Err(e) => Err(e), 51 | Ok(node_info) => Ok(node_info), 52 | }, 53 | } 54 | } 55 | 56 | pub async fn remove_peer( 57 | server_addr: SocketAddr, 58 | peer_endpoint: Endpoint, 59 | ) -> Result<(), reqwest::Error> { 60 | let full_endpoint = format!( 61 | "{}://{}", 62 | peer_endpoint.proto().to_string().to_lowercase(), 63 | peer_endpoint.address() 64 | ); 65 | let encoded_full_endpoint = encode(&full_endpoint); 66 | let request_url = format!( 67 | "http://{}/api/v1/admin/peers/{}", 68 | server_addr, encoded_full_endpoint 69 | ); 70 | 71 | let client = reqwest::Client::new(); 72 | client 73 | .delete(request_url) 74 | .send() 75 | .await? 76 | .error_for_status()?; 77 | 78 | Ok(()) 79 | } 80 | 81 | pub async fn add_peer( 82 | server_addr: SocketAddr, 83 | peer_endpoint: String, 84 | ) -> Result<(), reqwest::Error> { 85 | println!("adding peer: {peer_endpoint}"); 86 | let client = reqwest::Client::new(); 87 | let request_url = format!("http://{server_addr}/api/v1/admin/peers"); 88 | client 89 | .post(request_url) 90 | .json(&AddPeer { 91 | endpoint: peer_endpoint, 92 | }) 93 | .send() 94 | .await? 95 | .error_for_status()?; 96 | 97 | Ok(()) 98 | } 99 | -------------------------------------------------------------------------------- /mycelium-ui/src/components.rs: -------------------------------------------------------------------------------- 1 | pub mod home; 2 | pub mod layout; 3 | pub mod peers; 4 | pub mod routes; 5 | -------------------------------------------------------------------------------- /mycelium-ui/src/components/home.rs: -------------------------------------------------------------------------------- 1 | use crate::api; 2 | use crate::{ServerAddress, ServerConnected}; 3 | use dioxus::prelude::*; 4 | use std::net::SocketAddr; 5 | use std::str::FromStr; 6 | 7 | #[component] 8 | pub fn Home() -> Element { 9 | let mut server_addr = use_context::>(); 10 | let mut new_address = use_signal(|| server_addr.read().0.to_string()); 11 | let mut node_info = use_resource(fetch_node_info); 12 | 13 | let try_connect = move |_| { 14 | if let Ok(addr) = SocketAddr::from_str(&new_address.read()) { 15 | server_addr.write().0 = addr; 16 | node_info.restart(); 17 | } 18 | }; 19 | 20 | rsx! { 21 | div { class: "home-container", 22 | h2 { "Node information" } 23 | div { class: "server-input", 24 | input { 25 | placeholder: "Server address (e.g. 127.0.0.1:8989)", 26 | value: "{new_address}", 27 | oninput: move |evt| new_address.set(evt.value().clone()), 28 | } 29 | button { onclick: try_connect, "Connect" } 30 | } 31 | {match node_info.read().as_ref() { 32 | Some(Ok(info)) => rsx! { 33 | p { 34 | "Node subnet: ", 35 | span { class: "bold", "{info.node_subnet}" } 36 | } 37 | p { 38 | "Node public key: ", 39 | span { class: "bold", "{info.node_pubkey}" } 40 | } 41 | }, 42 | Some(Err(e)) => rsx! { 43 | p { class: "error", "Error: {e}" } 44 | }, 45 | None => rsx! { 46 | p { "Enter a server address and click 'Connect' to fetch node information." } 47 | } 48 | }} 49 | } 50 | } 51 | } 52 | 53 | async fn fetch_node_info() -> Result { 54 | let server_addr = use_context::>(); 55 | let mut server_connected = use_context::>(); 56 | let address = server_addr.read().0; 57 | 58 | match api::get_node_info(address).await { 59 | Ok(info) => { 60 | server_connected.write().0 = true; 61 | Ok(info) 62 | } 63 | Err(e) => { 64 | server_connected.write().0 = false; 65 | Err(e) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /mycelium-ui/src/components/layout.rs: -------------------------------------------------------------------------------- 1 | use crate::{api, Route, ServerAddress}; 2 | use dioxus::prelude::*; 3 | use dioxus_free_icons::{icons::fa_solid_icons::FaChevronLeft, Icon}; 4 | 5 | #[component] 6 | pub fn Layout() -> Element { 7 | let sidebar_collapsed = use_signal(|| false); 8 | 9 | rsx! { 10 | div { class: "app-container", 11 | Header {} 12 | div { class: "content-container", 13 | Sidebar { collapsed: sidebar_collapsed } 14 | main { class: if *sidebar_collapsed.read() { "main-content expanded" } else { "main-content" }, 15 | Outlet:: {} 16 | } 17 | } 18 | } 19 | } 20 | } 21 | 22 | #[component] 23 | pub fn Header() -> Element { 24 | let server_addr = use_context::>(); 25 | let fetched_node_info = use_resource(move || api::get_node_info(server_addr.read().0)); 26 | 27 | rsx! { 28 | header { 29 | h1 { "Mycelium Network Dashboard" } 30 | div { class: "node-info", 31 | { match &*fetched_node_info.read_unchecked() { 32 | Some(Ok(info)) => rsx! { 33 | span { "Subnet: {info.node_subnet}" } 34 | span { class: "separator", "|" } 35 | span { "Public Key: {info.node_pubkey}" } 36 | }, 37 | Some(Err(_)) => rsx! { span { "Error loading node info" } }, 38 | None => rsx! { span { "Loading node info..." } }, 39 | }} 40 | } 41 | } 42 | } 43 | } 44 | 45 | #[component] 46 | pub fn Sidebar(collapsed: Signal) -> Element { 47 | rsx! { 48 | nav { class: if *collapsed.read() { "sidebar collapsed" } else { "sidebar" }, 49 | ul { 50 | li { Link { to: Route::Home {}, "Home" } } 51 | li { Link { to: Route::Peers {}, "Peers" } } 52 | li { Link { to: Route::Routes {}, "Routes" } } 53 | } 54 | } 55 | button { class: if *collapsed.read() { "toggle-sidebar collapsed" } else { "toggle-sidebar" }, 56 | onclick: { 57 | let c = *collapsed.read(); 58 | move |_| collapsed.set(!c) 59 | }, 60 | Icon { 61 | icon: FaChevronLeft, 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /mycelium-ui/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | // Disable terminal popup on Windows 3 | #![cfg_attr(feature = "bundle", windows_subsystem = "windows")] 4 | 5 | mod api; 6 | mod components; 7 | 8 | use components::home::Home; 9 | use components::peers::Peers; 10 | use components::routes::Routes; 11 | 12 | use dioxus::prelude::*; 13 | use mycelium::{endpoint::Endpoint, peer_manager::PeerStats}; 14 | use std::{ 15 | collections::HashMap, 16 | net::{IpAddr, Ipv4Addr, SocketAddr}, 17 | }; 18 | 19 | const _: manganis::Asset = manganis::asset!("assets/styles.css"); 20 | 21 | const DEFAULT_SERVER_ADDR: SocketAddr = 22 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8989); 23 | 24 | fn main() { 25 | // Init logger 26 | dioxus_logger::init(tracing::Level::INFO).expect("failed to init logger"); 27 | 28 | let config = dioxus::desktop::Config::new() 29 | .with_custom_head(r#""#.to_string()); 30 | LaunchBuilder::desktop().with_cfg(config).launch(App); 31 | // dioxus::launch(App); 32 | } 33 | 34 | #[component] 35 | fn App() -> Element { 36 | use_context_provider(|| Signal::new(ServerAddress(DEFAULT_SERVER_ADDR))); 37 | use_context_provider(|| Signal::new(ServerConnected(false))); 38 | use_context_provider(|| { 39 | Signal::new(PeerSignalMapping( 40 | HashMap::>::new(), 41 | )) 42 | }); 43 | use_context_provider(|| Signal::new(StopFetchingPeerSignal(false))); 44 | 45 | rsx! { 46 | Router:: { 47 | config: || { 48 | RouterConfig::default().on_update(|state| { 49 | use_context::>().write().0 = state.current() != Route::Peers {}; 50 | (state.current() == Route::Peers {}).then_some(NavigationTarget::Internal(Route::Peers {})) 51 | }) 52 | } 53 | } 54 | } 55 | } 56 | 57 | #[derive(Clone, Routable, Debug, PartialEq)] 58 | #[rustfmt::skip] 59 | pub enum Route { 60 | #[layout(components::layout::Layout)] 61 | #[route("/")] 62 | Home {}, 63 | #[route("/peers")] 64 | Peers, 65 | #[route("/routes")] 66 | Routes, 67 | #[end_layout] 68 | #[route("/:..route")] 69 | PageNotFound { route: Vec }, 70 | } 71 | // 72 | #[derive(Clone, PartialEq)] 73 | struct SearchState { 74 | query: String, 75 | column: String, 76 | } 77 | 78 | // This signal is used to stop the loop that keeps fetching information about the peers when 79 | // looking at the peers table, e.g. when the user goes back to Home or Routes page. 80 | #[derive(Clone, PartialEq)] 81 | struct StopFetchingPeerSignal(bool); 82 | 83 | #[derive(Clone, PartialEq)] 84 | struct ServerAddress(SocketAddr); 85 | 86 | #[derive(Clone, PartialEq)] 87 | struct ServerConnected(bool); 88 | 89 | #[derive(Clone, PartialEq)] 90 | struct PeerSignalMapping(HashMap>); 91 | 92 | pub fn get_sort_indicator( 93 | sort_column: Signal, 94 | sort_direction: Signal, 95 | column: String, 96 | ) -> String { 97 | if *sort_column.read() == column { 98 | match *sort_direction.read() { 99 | SortDirection::Ascending => " ↑".to_string(), 100 | SortDirection::Descending => " ↓".to_string(), 101 | } 102 | } else { 103 | "".to_string() 104 | } 105 | } 106 | 107 | #[component] 108 | fn PageNotFound(route: Vec) -> Element { 109 | rsx! { 110 | p { "Page not found"} 111 | } 112 | } 113 | 114 | #[derive(Clone)] 115 | pub enum SortDirection { 116 | Ascending, 117 | Descending, 118 | } 119 | -------------------------------------------------------------------------------- /mycelium/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mycelium" 3 | version = "0.6.1" 4 | edition = "2021" 5 | license-file = "../LICENSE" 6 | readme = "../README.md" 7 | 8 | [features] 9 | message = [] 10 | private-network = ["dep:openssl", "dep:tokio-openssl"] 11 | vendored-openssl = ["openssl/vendored"] 12 | mactunfd = [ 13 | "tun/appstore", 14 | ] #mactunfd is a flag to specify that macos should provide tun FD instead of tun name 15 | 16 | [dependencies] 17 | tokio = { version = "1.44.2", features = [ 18 | "io-util", 19 | "fs", 20 | "macros", 21 | "net", 22 | "sync", 23 | "time", 24 | "rt-multi-thread", # FIXME: remove once tokio::task::block_in_place calls are resolved 25 | ] } 26 | tokio-util = { version = "0.7.15", features = ["codec"] } 27 | futures = "0.3.31" 28 | serde = { version = "1.0.219", features = ["derive"] } 29 | rand = "0.9.1" 30 | bytes = "1.10.1" 31 | x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] } 32 | aes-gcm = "0.10.3" 33 | tracing = { version = "0.1.41", features = ["release_max_level_debug"] } 34 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 35 | tracing-logfmt = { version = "0.3.5", features = ["ansi_logs"] } 36 | faster-hex = "0.10.0" 37 | tokio-stream = { version = "0.1.17", features = ["sync"] } 38 | left-right = "0.11.5" 39 | ipnet = "2.11.0" 40 | ip_network_table-deps-treebitmap = "0.5.0" 41 | blake3 = "1.8.2" 42 | etherparse = "0.18.0" 43 | quinn = { version = "0.11.7", default-features = false, features = [ 44 | "runtime-tokio", 45 | "rustls", 46 | ] } 47 | rustls = { version = "0.23.27", default-features = false, features = ["ring"] } 48 | rcgen = "0.13.2" 49 | netdev = "0.34.0" 50 | openssl = { version = "0.10.72", optional = true } 51 | tokio-openssl = { version = "0.6.5", optional = true } 52 | arc-swap = "1.7.1" 53 | dashmap = { version = "6.1.0", features = ["inline"] } 54 | ahash = "0.8.11" 55 | 56 | [target.'cfg(target_os = "linux")'.dependencies] 57 | rtnetlink = "0.16.0" 58 | tokio-tun = "0.13.2" 59 | nix = { version = "0.30.1", features = ["socket"] } 60 | 61 | [target.'cfg(target_os = "macos")'.dependencies] 62 | tun = { git = "https://github.com/LeeSmet/rust-tun", features = ["async"] } 63 | libc = "0.2.172" 64 | nix = { version = "0.29.0", features = ["net", "socket", "ioctl"] } 65 | 66 | [target.'cfg(target_os = "windows")'.dependencies] 67 | wintun = "0.5.1" 68 | 69 | [target.'cfg(target_os = "android")'.dependencies] 70 | tun = { git = "https://github.com/LeeSmet/rust-tun", features = ["async"] } 71 | 72 | [target.'cfg(target_os = "ios")'.dependencies] 73 | tun = { git = "https://github.com/LeeSmet/rust-tun", features = ["async"] } 74 | -------------------------------------------------------------------------------- /mycelium/src/babel/hello.rs: -------------------------------------------------------------------------------- 1 | //! The babel [Hello TLV](https://datatracker.ietf.org/doc/html/rfc8966#section-4.6.5). 2 | 3 | use bytes::{Buf, BufMut}; 4 | use tracing::trace; 5 | 6 | use crate::sequence_number::SeqNo; 7 | 8 | /// Flag bit indicating a [`Hello`] is sent as unicast hello. 9 | const HELLO_FLAG_UNICAST: u16 = 0x8000; 10 | 11 | /// Mask to apply to [`Hello`] flags, leaving only valid flags. 12 | const FLAG_MASK: u16 = 0b10000000_00000000; 13 | 14 | /// Wire size of a [`Hello`] TLV without TLV header. 15 | const HELLO_WIRE_SIZE: u8 = 6; 16 | 17 | /// Hello TLV body as defined in https://datatracker.ietf.org/doc/html/rfc8966#section-4.6.5. 18 | #[derive(Debug, Clone, PartialEq)] 19 | pub struct Hello { 20 | flags: u16, 21 | seqno: SeqNo, 22 | interval: u16, 23 | } 24 | 25 | impl Hello { 26 | /// Create a new unicast hello packet. 27 | pub fn new_unicast(seqno: SeqNo, interval: u16) -> Self { 28 | Self { 29 | flags: HELLO_FLAG_UNICAST, 30 | seqno, 31 | interval, 32 | } 33 | } 34 | 35 | /// Calculates the size on the wire of this `Hello`. 36 | pub fn wire_size(&self) -> u8 { 37 | HELLO_WIRE_SIZE 38 | } 39 | 40 | /// Construct a `Hello` from wire bytes. 41 | /// 42 | /// # Panics 43 | /// 44 | /// This function will panic if there are insufficient bytes present in the provided buffer to 45 | /// decode a complete `Hello`. 46 | pub fn from_bytes(src: &mut bytes::BytesMut) -> Self { 47 | let flags = src.get_u16() & FLAG_MASK; 48 | let seqno = src.get_u16().into(); 49 | let interval = src.get_u16(); 50 | 51 | trace!("Read hello tlv body"); 52 | 53 | Self { 54 | flags, 55 | seqno, 56 | interval, 57 | } 58 | } 59 | 60 | /// Encode this `Hello` tlv as part of a packet. 61 | pub fn write_bytes(&self, dst: &mut bytes::BytesMut) { 62 | dst.put_u16(self.flags); 63 | dst.put_u16(self.seqno.into()); 64 | dst.put_u16(self.interval); 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use bytes::Buf; 71 | 72 | #[test] 73 | fn encoding() { 74 | let mut buf = bytes::BytesMut::new(); 75 | 76 | let hello = super::Hello { 77 | flags: 0, 78 | seqno: 25.into(), 79 | interval: 400, 80 | }; 81 | 82 | hello.write_bytes(&mut buf); 83 | 84 | assert_eq!(buf.len(), 6); 85 | assert_eq!(buf[..6], [0, 0, 0, 25, 1, 144]); 86 | 87 | let mut buf = bytes::BytesMut::new(); 88 | 89 | let hello = super::Hello { 90 | flags: super::HELLO_FLAG_UNICAST, 91 | seqno: 16.into(), 92 | interval: 4000, 93 | }; 94 | 95 | hello.write_bytes(&mut buf); 96 | 97 | assert_eq!(buf.len(), 6); 98 | assert_eq!(buf[..6], [128, 0, 0, 16, 15, 160]); 99 | } 100 | 101 | #[test] 102 | fn decoding() { 103 | let mut buf = bytes::BytesMut::from(&[0b10000000u8, 0b00000000, 0, 19, 2, 1][..]); 104 | 105 | let hello = super::Hello { 106 | flags: super::HELLO_FLAG_UNICAST, 107 | seqno: 19.into(), 108 | interval: 513, 109 | }; 110 | 111 | assert_eq!(super::Hello::from_bytes(&mut buf), hello); 112 | assert_eq!(buf.remaining(), 0); 113 | 114 | let mut buf = bytes::BytesMut::from(&[0b00000000u8, 0b00000000, 1, 19, 200, 100][..]); 115 | 116 | let hello = super::Hello { 117 | flags: 0, 118 | seqno: 275.into(), 119 | interval: 51300, 120 | }; 121 | 122 | assert_eq!(super::Hello::from_bytes(&mut buf), hello); 123 | assert_eq!(buf.remaining(), 0); 124 | } 125 | 126 | #[test] 127 | fn decode_ignores_invalid_flag_bits() { 128 | let mut buf = bytes::BytesMut::from(&[0b10001001u8, 0b00000000, 0, 100, 1, 144][..]); 129 | 130 | let hello = super::Hello { 131 | flags: super::HELLO_FLAG_UNICAST, 132 | seqno: 100.into(), 133 | interval: 400, 134 | }; 135 | 136 | assert_eq!(super::Hello::from_bytes(&mut buf), hello); 137 | assert_eq!(buf.remaining(), 0); 138 | 139 | let mut buf = bytes::BytesMut::from(&[0b00001001u8, 0b00000000, 0, 100, 1, 144][..]); 140 | 141 | let hello = super::Hello { 142 | flags: 0, 143 | seqno: 100.into(), 144 | interval: 400, 145 | }; 146 | 147 | assert_eq!(super::Hello::from_bytes(&mut buf), hello); 148 | assert_eq!(buf.remaining(), 0); 149 | } 150 | 151 | #[test] 152 | fn roundtrip() { 153 | let mut buf = bytes::BytesMut::new(); 154 | 155 | let hello_src = super::Hello::new_unicast(16.into(), 400); 156 | hello_src.write_bytes(&mut buf); 157 | let decoded = super::Hello::from_bytes(&mut buf); 158 | 159 | assert_eq!(hello_src, decoded); 160 | assert_eq!(buf.remaining(), 0); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /mycelium/src/babel/tlv.rs: -------------------------------------------------------------------------------- 1 | pub use super::{hello::Hello, ihu::Ihu, update::Update}; 2 | use super::{route_request::RouteRequest, SeqNoRequest}; 3 | 4 | /// A single `Tlv` in a babel packet body. 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub enum Tlv { 7 | /// Hello Tlv type. 8 | Hello(Hello), 9 | /// Ihu Tlv type. 10 | Ihu(Ihu), 11 | /// Update Tlv type. 12 | Update(Update), 13 | /// RouteRequest Tlv type. 14 | RouteRequest(RouteRequest), 15 | /// SeqNoRequest Tlv type 16 | SeqNoRequest(SeqNoRequest), 17 | } 18 | 19 | impl Tlv { 20 | /// Calculate the size on the wire for this `Tlv`. This DOES NOT included the TLV header size 21 | /// (2 bytes). 22 | pub fn wire_size(&self) -> u8 { 23 | match self { 24 | Self::Hello(hello) => hello.wire_size(), 25 | Self::Ihu(ihu) => ihu.wire_size(), 26 | Self::Update(update) => update.wire_size(), 27 | Self::RouteRequest(route_request) => route_request.wire_size(), 28 | Self::SeqNoRequest(seqno_request) => seqno_request.wire_size(), 29 | } 30 | } 31 | 32 | /// Encode this `Tlv` as part of a packet. 33 | pub fn write_bytes(&self, dst: &mut bytes::BytesMut) { 34 | match self { 35 | Self::Hello(hello) => hello.write_bytes(dst), 36 | Self::Ihu(ihu) => ihu.write_bytes(dst), 37 | Self::Update(update) => update.write_bytes(dst), 38 | Self::RouteRequest(route_request) => route_request.write_bytes(dst), 39 | Self::SeqNoRequest(seqno_request) => seqno_request.write_bytes(dst), 40 | } 41 | } 42 | } 43 | 44 | impl From for Tlv { 45 | fn from(v: SeqNoRequest) -> Self { 46 | Self::SeqNoRequest(v) 47 | } 48 | } 49 | 50 | impl From for Tlv { 51 | fn from(v: RouteRequest) -> Self { 52 | Self::RouteRequest(v) 53 | } 54 | } 55 | 56 | impl From for Tlv { 57 | fn from(v: Update) -> Self { 58 | Self::Update(v) 59 | } 60 | } 61 | 62 | impl From for Tlv { 63 | fn from(v: Ihu) -> Self { 64 | Self::Ihu(v) 65 | } 66 | } 67 | 68 | impl From for Tlv { 69 | fn from(v: Hello) -> Self { 70 | Self::Hello(v) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /mycelium/src/connection.rs: -------------------------------------------------------------------------------- 1 | use std::{io, net::SocketAddr, pin::Pin}; 2 | 3 | use tokio::{ 4 | io::{AsyncRead, AsyncWrite}, 5 | net::TcpStream, 6 | }; 7 | 8 | mod tracked; 9 | pub use tracked::Tracked; 10 | 11 | #[cfg(feature = "private-network")] 12 | mod tls; 13 | 14 | /// Cost to add to the peer_link_cost for "local processing", when peers are connected over IPv6. 15 | /// 16 | /// The current peer link cost is calculated from a HELLO rtt. This is great to measure link 17 | /// latency, since packets are processed in order. However, on local idle links, this value will 18 | /// likely be 0 since we round down (from the amount of ms it took to process), which does not 19 | /// accurately reflect the fact that there is in fact a cost associated with using a peer, even on 20 | /// these local links. 21 | const PACKET_PROCESSING_COST_IP6_TCP: u16 = 10; 22 | 23 | /// Cost to add to the peer_link_cost for "local processing", when peers are connected over IPv6. 24 | /// 25 | /// This is similar to [`PACKET_PROCESSING_COST_IP6`], but slightly higher so we skew towards IPv6 26 | /// connections if peers are connected over both IPv4 and IPv6. 27 | const PACKET_PROCESSING_COST_IP4_TCP: u16 = 15; 28 | 29 | // TODO 30 | const PACKET_PROCESSING_COST_IP6_QUIC: u16 = 7; 31 | // TODO 32 | const PACKET_PROCESSING_COST_IP4_QUIC: u16 = 12; 33 | 34 | pub trait Connection: AsyncRead + AsyncWrite { 35 | /// Get an identifier for this connection, which shows details about the remote 36 | fn identifier(&self) -> Result; 37 | 38 | /// The static cost of using this connection 39 | fn static_link_cost(&self) -> Result; 40 | } 41 | 42 | /// A wrapper around a quic send and quic receive stream, implementing the [`Connection`] trait. 43 | pub struct Quic { 44 | tx: quinn::SendStream, 45 | rx: quinn::RecvStream, 46 | remote: SocketAddr, 47 | } 48 | 49 | impl Quic { 50 | /// Create a new wrapper around Quic streams. 51 | pub fn new(tx: quinn::SendStream, rx: quinn::RecvStream, remote: SocketAddr) -> Self { 52 | Quic { tx, rx, remote } 53 | } 54 | } 55 | 56 | impl Connection for TcpStream { 57 | fn identifier(&self) -> Result { 58 | Ok(format!( 59 | "TCP {} <-> {}", 60 | self.local_addr()?, 61 | self.peer_addr()? 62 | )) 63 | } 64 | 65 | fn static_link_cost(&self) -> Result { 66 | Ok(match self.peer_addr()? { 67 | SocketAddr::V4(_) => PACKET_PROCESSING_COST_IP4_TCP, 68 | SocketAddr::V6(ip) if ip.ip().to_ipv4_mapped().is_some() => { 69 | PACKET_PROCESSING_COST_IP4_TCP 70 | } 71 | SocketAddr::V6(_) => PACKET_PROCESSING_COST_IP6_TCP, 72 | }) 73 | } 74 | } 75 | 76 | impl AsyncRead for Quic { 77 | #[inline] 78 | fn poll_read( 79 | mut self: std::pin::Pin<&mut Self>, 80 | cx: &mut std::task::Context<'_>, 81 | buf: &mut tokio::io::ReadBuf<'_>, 82 | ) -> std::task::Poll> { 83 | Pin::new(&mut self.rx).poll_read(cx, buf) 84 | } 85 | } 86 | 87 | impl AsyncWrite for Quic { 88 | #[inline] 89 | fn poll_write( 90 | mut self: Pin<&mut Self>, 91 | cx: &mut std::task::Context<'_>, 92 | buf: &[u8], 93 | ) -> std::task::Poll> { 94 | Pin::new(&mut self.tx) 95 | .poll_write(cx, buf) 96 | .map_err(From::from) 97 | } 98 | 99 | #[inline] 100 | fn poll_flush( 101 | mut self: Pin<&mut Self>, 102 | cx: &mut std::task::Context<'_>, 103 | ) -> std::task::Poll> { 104 | Pin::new(&mut self.tx).poll_flush(cx) 105 | } 106 | 107 | #[inline] 108 | fn poll_shutdown( 109 | mut self: Pin<&mut Self>, 110 | cx: &mut std::task::Context<'_>, 111 | ) -> std::task::Poll> { 112 | Pin::new(&mut self.tx).poll_shutdown(cx) 113 | } 114 | 115 | #[inline] 116 | fn poll_write_vectored( 117 | mut self: Pin<&mut Self>, 118 | cx: &mut std::task::Context<'_>, 119 | bufs: &[io::IoSlice<'_>], 120 | ) -> std::task::Poll> { 121 | Pin::new(&mut self.tx).poll_write_vectored(cx, bufs) 122 | } 123 | 124 | #[inline] 125 | fn is_write_vectored(&self) -> bool { 126 | self.tx.is_write_vectored() 127 | } 128 | } 129 | 130 | impl Connection for Quic { 131 | fn identifier(&self) -> Result { 132 | Ok(format!("QUIC -> {}", self.remote)) 133 | } 134 | 135 | fn static_link_cost(&self) -> Result { 136 | Ok(match self.remote { 137 | SocketAddr::V4(_) => PACKET_PROCESSING_COST_IP4_QUIC, 138 | SocketAddr::V6(ip) if ip.ip().to_ipv4_mapped().is_some() => { 139 | PACKET_PROCESSING_COST_IP4_QUIC 140 | } 141 | SocketAddr::V6(_) => PACKET_PROCESSING_COST_IP6_QUIC, 142 | }) 143 | } 144 | } 145 | 146 | #[cfg(test)] 147 | use tokio::io::DuplexStream; 148 | 149 | #[cfg(test)] 150 | impl Connection for DuplexStream { 151 | fn identifier(&self) -> Result { 152 | Ok("Memory pipe".to_string()) 153 | } 154 | 155 | fn static_link_cost(&self) -> Result { 156 | Ok(1) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /mycelium/src/connection/tls.rs: -------------------------------------------------------------------------------- 1 | use std::{io, net::SocketAddr}; 2 | 3 | use tokio::net::TcpStream; 4 | 5 | impl super::Connection for tokio_openssl::SslStream { 6 | fn identifier(&self) -> Result { 7 | Ok(format!( 8 | "TLS {} <-> {}", 9 | self.get_ref().local_addr()?, 10 | self.get_ref().peer_addr()? 11 | )) 12 | } 13 | 14 | fn static_link_cost(&self) -> Result { 15 | Ok(match self.get_ref().peer_addr()? { 16 | SocketAddr::V4(_) => super::PACKET_PROCESSING_COST_IP4_TCP, 17 | SocketAddr::V6(ip) if ip.ip().to_ipv4_mapped().is_some() => { 18 | super::PACKET_PROCESSING_COST_IP4_TCP 19 | } 20 | SocketAddr::V6(_) => super::PACKET_PROCESSING_COST_IP6_TCP, 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /mycelium/src/connection/tracked.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | sync::{ 4 | atomic::{AtomicU64, Ordering}, 5 | Arc, 6 | }, 7 | task::Poll, 8 | }; 9 | 10 | use tokio::io::{AsyncRead, AsyncWrite}; 11 | 12 | use super::Connection; 13 | 14 | /// Wrapper which keeps track of how much bytes have been read and written from a connection. 15 | pub struct Tracked { 16 | /// Bytes read counter 17 | read: Arc, 18 | /// Bytes written counter 19 | write: Arc, 20 | /// Underlying connection we are measuring 21 | con: C, 22 | } 23 | 24 | impl Tracked 25 | where 26 | C: Connection + Unpin, 27 | { 28 | /// Create a new instance of a tracked connections. Counters are passed in so they can be 29 | /// reused accross connections. 30 | pub fn new(read: Arc, write: Arc, con: C) -> Self { 31 | Self { read, write, con } 32 | } 33 | } 34 | 35 | impl Connection for Tracked 36 | where 37 | C: Connection + Unpin, 38 | { 39 | #[inline] 40 | fn identifier(&self) -> Result { 41 | self.con.identifier() 42 | } 43 | 44 | #[inline] 45 | fn static_link_cost(&self) -> Result { 46 | self.con.static_link_cost() 47 | } 48 | } 49 | 50 | impl AsyncRead for Tracked 51 | where 52 | C: AsyncRead + Unpin, 53 | { 54 | #[inline] 55 | fn poll_read( 56 | mut self: std::pin::Pin<&mut Self>, 57 | cx: &mut std::task::Context<'_>, 58 | buf: &mut tokio::io::ReadBuf<'_>, 59 | ) -> std::task::Poll> { 60 | let start_len = buf.filled().len(); 61 | let res = Pin::new(&mut self.con).poll_read(cx, buf); 62 | if let Poll::Ready(Ok(())) = res { 63 | self.read 64 | .fetch_add((buf.filled().len() - start_len) as u64, Ordering::Relaxed); 65 | } 66 | res 67 | } 68 | } 69 | 70 | impl AsyncWrite for Tracked 71 | where 72 | C: AsyncWrite + Unpin, 73 | { 74 | #[inline] 75 | fn poll_write( 76 | mut self: Pin<&mut Self>, 77 | cx: &mut std::task::Context<'_>, 78 | buf: &[u8], 79 | ) -> Poll> { 80 | let res = Pin::new(&mut self.con).poll_write(cx, buf); 81 | if let Poll::Ready(Ok(written)) = res { 82 | self.write.fetch_add(written as u64, Ordering::Relaxed); 83 | } 84 | res 85 | } 86 | 87 | #[inline] 88 | fn poll_flush( 89 | mut self: Pin<&mut Self>, 90 | cx: &mut std::task::Context<'_>, 91 | ) -> Poll> { 92 | Pin::new(&mut self.con).poll_flush(cx) 93 | } 94 | 95 | #[inline] 96 | fn poll_shutdown( 97 | mut self: Pin<&mut Self>, 98 | cx: &mut std::task::Context<'_>, 99 | ) -> Poll> { 100 | Pin::new(&mut self.con).poll_shutdown(cx) 101 | } 102 | 103 | #[inline] 104 | fn poll_write_vectored( 105 | mut self: Pin<&mut Self>, 106 | cx: &mut std::task::Context<'_>, 107 | bufs: &[std::io::IoSlice<'_>], 108 | ) -> Poll> { 109 | let res = Pin::new(&mut self.con).poll_write_vectored(cx, bufs); 110 | if let Poll::Ready(Ok(written)) = res { 111 | self.write.fetch_add(written as u64, Ordering::Relaxed); 112 | } 113 | res 114 | } 115 | 116 | #[inline] 117 | fn is_write_vectored(&self) -> bool { 118 | self.con.is_write_vectored() 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /mycelium/src/endpoint.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | net::{AddrParseError, SocketAddr}, 4 | str::FromStr, 5 | }; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Debug, Clone, PartialEq, Eq)] 10 | /// Error generated while processing improperly formatted endpoints. 11 | pub enum EndpointParseError { 12 | /// An address was specified without leading protocol information. 13 | MissingProtocol, 14 | /// An endpoint was specified using a protocol we (currently) do not understand. 15 | UnknownProtocol, 16 | /// Error while parsing the specific address. 17 | Address(AddrParseError), 18 | } 19 | 20 | /// Protocol used by an endpoint. 21 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 22 | #[serde(rename_all = "camelCase")] 23 | pub enum Protocol { 24 | /// Standard plain text Tcp. 25 | Tcp, 26 | /// Tls 1.3 with PSK over Tcp. 27 | Tls, 28 | /// Quic protocol (over UDP). 29 | Quic, 30 | } 31 | 32 | /// An endpoint defines a address and a protocol to use when communicating with it. 33 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct Endpoint { 36 | proto: Protocol, 37 | socket_addr: SocketAddr, 38 | } 39 | 40 | impl Endpoint { 41 | /// Create a new `Endpoint` with given [`Protocol`] and address. 42 | pub fn new(proto: Protocol, socket_addr: SocketAddr) -> Self { 43 | Self { proto, socket_addr } 44 | } 45 | 46 | /// Get the [`Protocol`] used by this `Endpoint`. 47 | pub fn proto(&self) -> Protocol { 48 | self.proto 49 | } 50 | 51 | /// Get the [`SocketAddr`] used by this `Endpoint`. 52 | pub fn address(&self) -> SocketAddr { 53 | self.socket_addr 54 | } 55 | } 56 | 57 | impl FromStr for Endpoint { 58 | type Err = EndpointParseError; 59 | 60 | fn from_str(s: &str) -> Result { 61 | match s.split_once("://") { 62 | None => Err(EndpointParseError::MissingProtocol), 63 | Some((proto, socket)) => { 64 | let proto = match proto.to_lowercase().as_str() { 65 | "tcp" => Protocol::Tcp, 66 | "quic" => Protocol::Quic, 67 | "tls" => Protocol::Tls, 68 | _ => return Err(EndpointParseError::UnknownProtocol), 69 | }; 70 | let socket_addr = SocketAddr::from_str(socket)?; 71 | Ok(Endpoint { proto, socket_addr }) 72 | } 73 | } 74 | } 75 | } 76 | 77 | impl fmt::Display for Endpoint { 78 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 79 | f.write_fmt(format_args!("{} {}", self.proto, self.socket_addr)) 80 | } 81 | } 82 | 83 | impl fmt::Display for Protocol { 84 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 85 | f.write_str(match self { 86 | Self::Tcp => "Tcp", 87 | Self::Tls => "Tls", 88 | Self::Quic => "Quic", 89 | }) 90 | } 91 | } 92 | 93 | impl fmt::Display for EndpointParseError { 94 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 95 | match self { 96 | Self::MissingProtocol => f.write_str("missing leading protocol identifier"), 97 | Self::UnknownProtocol => f.write_str("protocol for endpoint is not supported"), 98 | Self::Address(e) => f.write_fmt(format_args!("failed to parse address: {}", e)), 99 | } 100 | } 101 | } 102 | 103 | impl std::error::Error for EndpointParseError { 104 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 105 | match self { 106 | Self::Address(e) => Some(e), 107 | _ => None, 108 | } 109 | } 110 | } 111 | 112 | impl From for EndpointParseError { 113 | fn from(value: AddrParseError) -> Self { 114 | Self::Address(value) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /mycelium/src/filters.rs: -------------------------------------------------------------------------------- 1 | use crate::{babel, subnet::Subnet}; 2 | 3 | /// This trait is used to filter incoming updates from peers. Only updates which pass all 4 | /// configured filters on the local [`Router`](crate::router::Router) will actually be forwarded 5 | /// to the [`Router`](crate::router::Router) for processing. 6 | pub trait RouteUpdateFilter { 7 | /// Judge an incoming update. 8 | fn allow(&self, update: &babel::Update) -> bool; 9 | } 10 | 11 | /// Limit the subnet size of subnets announced in updates to be at most `N` bits. Note that "at 12 | /// most" here means that the actual prefix length needs to be **AT LEAST** this value. 13 | pub struct MaxSubnetSize; 14 | 15 | impl RouteUpdateFilter for MaxSubnetSize { 16 | fn allow(&self, update: &babel::Update) -> bool { 17 | update.subnet().prefix_len() >= N 18 | } 19 | } 20 | 21 | /// Limit the subnet announced to be included in the given subnet. 22 | pub struct AllowedSubnet { 23 | subnet: Subnet, 24 | } 25 | 26 | impl AllowedSubnet { 27 | /// Create a new `AllowedSubnet` filter, which only allows updates who's `Subnet` is contained 28 | /// in the given `Subnet`. 29 | pub fn new(subnet: Subnet) -> Self { 30 | Self { subnet } 31 | } 32 | } 33 | 34 | impl RouteUpdateFilter for AllowedSubnet { 35 | fn allow(&self, update: &babel::Update) -> bool { 36 | self.subnet.contains_subnet(&update.subnet()) 37 | } 38 | } 39 | 40 | /// Limit the announced subnets to those which contain the derived IP from the `RouterId`. 41 | /// 42 | /// Since retractions can be sent by any node to indicate they don't have a route for the subnet, 43 | /// these are also allowed. 44 | pub struct RouterIdOwnsSubnet; 45 | 46 | impl RouteUpdateFilter for RouterIdOwnsSubnet { 47 | fn allow(&self, update: &babel::Update) -> bool { 48 | update.metric().is_infinite() 49 | || update 50 | .subnet() 51 | .contains_ip(update.router_id().to_pubkey().address().into()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /mycelium/src/interval.rs: -------------------------------------------------------------------------------- 1 | //! Dedicated logic for 2 | //! [intervals](https://datatracker.ietf.org/doc/html/rfc8966#name-solving-starvation-sequenci). 3 | 4 | use std::time::Duration; 5 | 6 | /// An interval in the babel protocol. 7 | /// 8 | /// Intervals represent a duration, and are expressed in centiseconds (0.01 second / 10 9 | /// milliseconds). `Interval` implements [`From`] [`u16`] to create a new interval from a raw 10 | /// value, and [`From`] [`Duration`] to create a new `Interval` from an existing [`Duration`]. 11 | /// There are also implementation to convert back to the aforementioned types. Note that in case of 12 | /// duration, millisecond precision is lost. 13 | #[derive(Debug, Clone)] 14 | pub struct Interval(u16); 15 | 16 | impl From for Interval { 17 | fn from(value: Duration) -> Self { 18 | Interval((value.as_millis() / 10) as u16) 19 | } 20 | } 21 | 22 | impl From for Duration { 23 | fn from(value: Interval) -> Self { 24 | Duration::from_millis(value.0 as u64 * 10) 25 | } 26 | } 27 | 28 | impl From for Interval { 29 | fn from(value: u16) -> Self { 30 | Interval(value) 31 | } 32 | } 33 | 34 | impl From for u16 { 35 | fn from(value: Interval) -> Self { 36 | value.0 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mycelium/src/message/done.rs: -------------------------------------------------------------------------------- 1 | use super::{MessageChecksum, MessagePacket, MESSAGE_CHECKSUM_LENGTH}; 2 | 3 | /// A message representing a "done" message. 4 | /// 5 | /// The body of a done message has the following structure: 6 | /// - 8 bytes: chunks transmitted 7 | /// - 32 bytes: checksum of the transmitted data 8 | pub struct MessageDone { 9 | buffer: MessagePacket, 10 | } 11 | 12 | impl MessageDone { 13 | /// Create a new `MessageDone` in the provided [`MessagePacket`]. 14 | pub fn new(mut buffer: MessagePacket) -> Self { 15 | buffer.set_used_buffer_size(40); 16 | buffer.header_mut().flags_mut().set_done(); 17 | Self { buffer } 18 | } 19 | 20 | /// Return the amount of chunks in the message, as written in the body. 21 | pub fn chunk_count(&self) -> u64 { 22 | u64::from_be_bytes( 23 | self.buffer.buffer()[..8] 24 | .try_into() 25 | .expect("Buffer contains a size field of valid length; qed"), 26 | ) 27 | } 28 | 29 | /// Set the amount of chunks field of the message body. 30 | pub fn set_chunk_count(&mut self, chunk_count: u64) { 31 | self.buffer.buffer_mut()[..8].copy_from_slice(&chunk_count.to_be_bytes()) 32 | } 33 | 34 | /// Get the checksum of the message from the body. 35 | pub fn checksum(&self) -> MessageChecksum { 36 | MessageChecksum::from_bytes( 37 | self.buffer.buffer()[8..8 + MESSAGE_CHECKSUM_LENGTH] 38 | .try_into() 39 | .expect("Buffer contains enough data for a checksum; qed"), 40 | ) 41 | } 42 | 43 | /// Set the checksum of the message in the body. 44 | pub fn set_checksum(&mut self, checksum: MessageChecksum) { 45 | self.buffer.buffer_mut()[8..8 + MESSAGE_CHECKSUM_LENGTH] 46 | .copy_from_slice(checksum.as_bytes()) 47 | } 48 | 49 | /// Convert the `MessageDone` into a reply. This does nothing if it is already a reply. 50 | pub fn into_reply(mut self) -> Self { 51 | self.buffer.header_mut().flags_mut().set_ack(); 52 | self 53 | } 54 | 55 | /// Consumes this `MessageDone`, returning the underlying [`MessagePacket`]. 56 | pub fn into_inner(self) -> MessagePacket { 57 | self.buffer 58 | } 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use crate::{ 64 | crypto::PacketBuffer, 65 | message::{MessageChecksum, MessagePacket}, 66 | }; 67 | 68 | use super::MessageDone; 69 | 70 | #[test] 71 | fn done_flag_set() { 72 | let md = MessageDone::new(MessagePacket::new(PacketBuffer::new())); 73 | 74 | let mp = md.into_inner(); 75 | assert!(mp.header().flags().done()); 76 | } 77 | 78 | #[test] 79 | fn read_chunk_count() { 80 | let mut pb = PacketBuffer::new(); 81 | pb.buffer_mut()[12..20].copy_from_slice(&[0, 0, 0, 0, 0, 0, 73, 55]); 82 | 83 | let ms = MessageDone::new(MessagePacket::new(pb)); 84 | 85 | assert_eq!(ms.chunk_count(), 18_743); 86 | } 87 | 88 | #[test] 89 | fn write_chunk_count() { 90 | let mut ms = MessageDone::new(MessagePacket::new(PacketBuffer::new())); 91 | 92 | ms.set_chunk_count(10_000); 93 | 94 | // Since we don't work with packet buffer we don't have to account for the message packet 95 | // header. 96 | assert_eq!(&ms.buffer.buffer()[..8], &[0, 0, 0, 0, 0, 0, 39, 16]); 97 | assert_eq!(ms.chunk_count(), 10_000); 98 | } 99 | 100 | #[test] 101 | fn read_checksum() { 102 | const CHECKSUM: MessageChecksum = MessageChecksum::from_bytes([ 103 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 104 | 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 105 | 0x1C, 0x1D, 0x1E, 0x1F, 106 | ]); 107 | let mut pb = PacketBuffer::new(); 108 | pb.buffer_mut()[20..52].copy_from_slice(CHECKSUM.as_bytes()); 109 | 110 | let ms = MessageDone::new(MessagePacket::new(pb)); 111 | 112 | assert_eq!(ms.checksum(), CHECKSUM); 113 | } 114 | 115 | #[test] 116 | fn write_checksum() { 117 | const CHECKSUM: MessageChecksum = MessageChecksum::from_bytes([ 118 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 119 | 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 120 | 0x1C, 0x1D, 0x1E, 0x1F, 121 | ]); 122 | let mut ms = MessageDone::new(MessagePacket::new(PacketBuffer::new())); 123 | 124 | ms.set_checksum(CHECKSUM); 125 | 126 | // Since we don't work with packet buffer we don't have to account for the message packet 127 | // header. 128 | assert_eq!(&ms.buffer.buffer()[8..40], CHECKSUM.as_bytes()); 129 | assert_eq!(ms.checksum(), CHECKSUM); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /mycelium/src/message/init.rs: -------------------------------------------------------------------------------- 1 | use super::MessagePacket; 2 | 3 | /// A message representing an init message. 4 | /// 5 | /// The body of an init message has the following structure: 6 | /// - 8 bytes size 7 | pub struct MessageInit { 8 | buffer: MessagePacket, 9 | } 10 | 11 | impl MessageInit { 12 | /// Create a new `MessageInit` in the provided [`MessagePacket`]. 13 | pub fn new(mut buffer: MessagePacket) -> Self { 14 | buffer.set_used_buffer_size(9); 15 | buffer.header_mut().flags_mut().set_init(); 16 | Self { buffer } 17 | } 18 | 19 | /// Return the length of the message, as written in the body. 20 | pub fn length(&self) -> u64 { 21 | u64::from_be_bytes( 22 | self.buffer.buffer()[..8] 23 | .try_into() 24 | .expect("Buffer contains a size field of valid length; qed"), 25 | ) 26 | } 27 | 28 | /// Return the topic of the message, as written in the body. 29 | pub fn topic(&self) -> &[u8] { 30 | let topic_len = self.buffer.buffer()[8] as usize; 31 | &self.buffer.buffer()[9..9 + topic_len] 32 | } 33 | 34 | /// Set the length field of the message body. 35 | pub fn set_length(&mut self, length: u64) { 36 | self.buffer.buffer_mut()[..8].copy_from_slice(&length.to_be_bytes()) 37 | } 38 | 39 | /// Set the topic in the message body. 40 | /// 41 | /// # Panics 42 | /// 43 | /// This function panics if the topic is longer than 255 bytes. 44 | pub fn set_topic(&mut self, topic: &[u8]) { 45 | assert!( 46 | topic.len() <= u8::MAX as usize, 47 | "Topic can be 255 bytes long at most" 48 | ); 49 | self.buffer.set_used_buffer_size(9 + topic.len()); 50 | self.buffer.buffer_mut()[8] = topic.len() as u8; 51 | self.buffer.buffer_mut()[9..9 + topic.len()].copy_from_slice(topic); 52 | } 53 | 54 | /// Convert the `MessageInit` into a reply. This does nothing if it is already a reply. 55 | pub fn into_reply(mut self) -> Self { 56 | self.buffer.header_mut().flags_mut().set_ack(); 57 | self 58 | } 59 | 60 | /// Consumes this `MessageInit`, returning the underlying [`MessagePacket`]. 61 | pub fn into_inner(self) -> MessagePacket { 62 | self.buffer 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use crate::{crypto::PacketBuffer, message::MessagePacket}; 69 | 70 | use super::MessageInit; 71 | 72 | #[test] 73 | fn init_flag_set() { 74 | let mi = MessageInit::new(MessagePacket::new(PacketBuffer::new())); 75 | 76 | let mp = mi.into_inner(); 77 | assert!(mp.header().flags().init()); 78 | } 79 | 80 | #[test] 81 | fn read_length() { 82 | let mut pb = PacketBuffer::new(); 83 | pb.buffer_mut()[12..20].copy_from_slice(&[0, 0, 0, 0, 2, 3, 4, 5]); 84 | 85 | let ms = MessageInit::new(MessagePacket::new(pb)); 86 | 87 | assert_eq!(ms.length(), 33_752_069); 88 | } 89 | 90 | #[test] 91 | fn write_length() { 92 | let mut ms = MessageInit::new(MessagePacket::new(PacketBuffer::new())); 93 | 94 | ms.set_length(3_432_634_632); 95 | 96 | // Since we don't work with packet buffer we don't have to account for the message packet 97 | // header. 98 | assert_eq!(&ms.buffer.buffer()[..8], &[0, 0, 0, 0, 204, 153, 217, 8]); 99 | assert_eq!(ms.length(), 3_432_634_632); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /mycelium/src/metric.rs: -------------------------------------------------------------------------------- 1 | //! Dedicated logic for 2 | //! [metrics](https://datatracker.ietf.org/doc/html/rfc8966#metric-computation). 3 | 4 | use core::fmt; 5 | use std::ops::{Add, Sub}; 6 | 7 | /// Value of the infinite metric. 8 | const METRIC_INFINITE: u16 = 0xFFFF; 9 | 10 | /// A `Metric` is used to indicate the cost associated with a route. A lower Metric means a route 11 | /// is more favorable. 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] 13 | pub struct Metric(u16); 14 | 15 | impl Metric { 16 | /// Create a new `Metric` with the given value. 17 | pub const fn new(value: u16) -> Self { 18 | Metric(value) 19 | } 20 | 21 | /// Creates a new infinite `Metric`. 22 | pub const fn infinite() -> Self { 23 | Metric(METRIC_INFINITE) 24 | } 25 | 26 | /// Checks if this metric indicates a retracted route. 27 | pub const fn is_infinite(&self) -> bool { 28 | self.0 == METRIC_INFINITE 29 | } 30 | 31 | /// Checks if this metric represents a directly connected route. 32 | pub const fn is_direct(&self) -> bool { 33 | self.0 == 0 34 | } 35 | 36 | /// Computes the absolute value of the difference between this and another `Metric`. 37 | pub fn delta(&self, rhs: &Self) -> Metric { 38 | Metric(if self > rhs { 39 | self.0 - rhs.0 40 | } else { 41 | rhs.0 - self.0 42 | }) 43 | } 44 | } 45 | 46 | impl fmt::Display for Metric { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | if self.is_infinite() { 49 | f.pad("Infinite") 50 | } else { 51 | f.write_fmt(format_args!("{}", self.0)) 52 | } 53 | } 54 | } 55 | 56 | impl From for Metric { 57 | fn from(value: u16) -> Self { 58 | Metric(value) 59 | } 60 | } 61 | 62 | impl From for u16 { 63 | fn from(value: Metric) -> Self { 64 | value.0 65 | } 66 | } 67 | 68 | impl Add for Metric { 69 | type Output = Self; 70 | 71 | fn add(self, rhs: Metric) -> Self::Output { 72 | if self.is_infinite() || rhs.is_infinite() { 73 | return Metric::infinite(); 74 | } 75 | Metric( 76 | self.0 77 | .checked_add(rhs.0) 78 | .map(|r| if r == u16::MAX { r - 1 } else { r }) 79 | .unwrap_or(u16::MAX - 1), 80 | ) 81 | } 82 | } 83 | 84 | impl Add<&Metric> for &Metric { 85 | type Output = Metric; 86 | 87 | fn add(self, rhs: &Metric) -> Self::Output { 88 | if self.is_infinite() || rhs.is_infinite() { 89 | return Metric::infinite(); 90 | } 91 | Metric( 92 | self.0 93 | .checked_add(rhs.0) 94 | .map(|r| if r == u16::MAX { r - 1 } else { r }) 95 | .unwrap_or(u16::MAX - 1), 96 | ) 97 | } 98 | } 99 | 100 | impl Add<&Metric> for Metric { 101 | type Output = Self; 102 | 103 | fn add(self, rhs: &Metric) -> Self::Output { 104 | if self.is_infinite() || rhs.is_infinite() { 105 | return Metric::infinite(); 106 | } 107 | Metric( 108 | self.0 109 | .checked_add(rhs.0) 110 | .map(|r| if r == u16::MAX { r - 1 } else { r }) 111 | .unwrap_or(u16::MAX - 1), 112 | ) 113 | } 114 | } 115 | 116 | impl Add for &Metric { 117 | type Output = Metric; 118 | 119 | fn add(self, rhs: Metric) -> Self::Output { 120 | if self.is_infinite() || rhs.is_infinite() { 121 | return Metric::infinite(); 122 | } 123 | Metric( 124 | self.0 125 | .checked_add(rhs.0) 126 | .map(|r| if r == u16::MAX { r - 1 } else { r }) 127 | .unwrap_or(u16::MAX - 1), 128 | ) 129 | } 130 | } 131 | 132 | impl Sub for Metric { 133 | type Output = Metric; 134 | 135 | fn sub(self, rhs: Metric) -> Self::Output { 136 | if rhs.is_infinite() { 137 | panic!("Can't subtract an infinite metric"); 138 | } 139 | if self.is_infinite() { 140 | return Metric::infinite(); 141 | } 142 | Metric(self.0.saturating_sub(rhs.0)) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /mycelium/src/packet.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, BufMut, BytesMut}; 2 | pub use control::ControlPacket; 3 | pub use data::DataPacket; 4 | use tokio_util::codec::{Decoder, Encoder}; 5 | 6 | mod control; 7 | mod data; 8 | 9 | /// Current version of the protocol being used. 10 | const PROTOCOL_VERSION: u8 = 1; 11 | 12 | /// The size of a `Packet` header on the wire, in bytes. 13 | const PACKET_HEADER_SIZE: usize = 4; 14 | 15 | #[derive(Debug, Clone)] 16 | pub enum Packet { 17 | DataPacket(DataPacket), 18 | ControlPacket(ControlPacket), 19 | } 20 | 21 | #[derive(Debug, Clone, Copy)] 22 | #[repr(u8)] 23 | pub enum PacketType { 24 | DataPacket = 0, 25 | ControlPacket = 1, 26 | } 27 | 28 | pub struct Codec { 29 | packet_type: Option, 30 | data_packet_codec: data::Codec, 31 | control_packet_codec: control::Codec, 32 | } 33 | 34 | impl Codec { 35 | pub fn new() -> Self { 36 | Codec { 37 | packet_type: None, 38 | data_packet_codec: data::Codec::new(), 39 | control_packet_codec: control::Codec::new(), 40 | } 41 | } 42 | } 43 | 44 | impl Decoder for Codec { 45 | type Item = Packet; 46 | type Error = std::io::Error; 47 | 48 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 49 | // Determine the packet_type 50 | let packet_type = if let Some(packet_type) = self.packet_type { 51 | packet_type 52 | } else { 53 | // Check we can read the header 54 | if src.remaining() <= PACKET_HEADER_SIZE { 55 | return Ok(None); 56 | } 57 | 58 | let mut header = [0; PACKET_HEADER_SIZE]; 59 | header.copy_from_slice(&src[..PACKET_HEADER_SIZE]); 60 | src.advance(PACKET_HEADER_SIZE); 61 | 62 | // For now it's a hard error to not follow the 1 defined protocol version 63 | if header[0] != PROTOCOL_VERSION { 64 | return Err(std::io::Error::new( 65 | std::io::ErrorKind::InvalidData, 66 | "Unknown protocol version", 67 | )); 68 | }; 69 | 70 | let packet_type_byte = header[1]; 71 | let packet_type = match packet_type_byte { 72 | 0 => PacketType::DataPacket, 73 | 1 => PacketType::ControlPacket, 74 | _ => { 75 | return Err(std::io::Error::new( 76 | std::io::ErrorKind::InvalidData, 77 | "Invalid packet type", 78 | )); 79 | } 80 | }; 81 | 82 | self.packet_type = Some(packet_type); 83 | 84 | packet_type 85 | }; 86 | 87 | // Decode packet based on determined packet_type 88 | match packet_type { 89 | PacketType::DataPacket => { 90 | match self.data_packet_codec.decode(src) { 91 | Ok(Some(p)) => { 92 | self.packet_type = None; // Reset state 93 | Ok(Some(Packet::DataPacket(p))) 94 | } 95 | Ok(None) => Ok(None), 96 | Err(e) => Err(e), 97 | } 98 | } 99 | PacketType::ControlPacket => { 100 | match self.control_packet_codec.decode(src) { 101 | Ok(Some(p)) => { 102 | self.packet_type = None; // Reset state 103 | Ok(Some(Packet::ControlPacket(p))) 104 | } 105 | Ok(None) => Ok(None), 106 | Err(e) => Err(e), 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | impl Encoder for Codec { 114 | type Error = std::io::Error; 115 | 116 | fn encode(&mut self, item: Packet, dst: &mut BytesMut) -> Result<(), Self::Error> { 117 | match item { 118 | Packet::DataPacket(datapacket) => { 119 | dst.put_slice(&[PROTOCOL_VERSION, 0, 0, 0]); 120 | self.data_packet_codec.encode(datapacket, dst) 121 | } 122 | Packet::ControlPacket(controlpacket) => { 123 | dst.put_slice(&[PROTOCOL_VERSION, 1, 0, 0]); 124 | self.control_packet_codec.encode(controlpacket, dst) 125 | } 126 | } 127 | } 128 | } 129 | 130 | impl Default for Codec { 131 | fn default() -> Self { 132 | Self::new() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /mycelium/src/packet/control.rs: -------------------------------------------------------------------------------- 1 | use std::{io, net::IpAddr, time::Duration}; 2 | 3 | use bytes::BytesMut; 4 | use tokio_util::codec::{Decoder, Encoder}; 5 | 6 | use crate::{ 7 | babel, metric::Metric, peer::Peer, router_id::RouterId, sequence_number::SeqNo, subnet::Subnet, 8 | }; 9 | 10 | pub type ControlPacket = babel::Tlv; 11 | 12 | pub struct Codec { 13 | // TODO: wrapper to make it easier to deserialize 14 | codec: babel::Codec, 15 | } 16 | 17 | impl ControlPacket { 18 | pub fn new_hello(dest_peer: &Peer, interval: Duration) -> Self { 19 | let tlv: babel::Tlv = 20 | babel::Hello::new_unicast(dest_peer.hello_seqno(), (interval.as_millis() / 10) as u16) 21 | .into(); 22 | dest_peer.increment_hello_seqno(); 23 | tlv 24 | } 25 | 26 | pub fn new_ihu(rx_cost: Metric, interval: Duration, dest_address: Option) -> Self { 27 | babel::Ihu::new(rx_cost, (interval.as_millis() / 10) as u16, dest_address).into() 28 | } 29 | 30 | pub fn new_update( 31 | interval: Duration, 32 | seqno: SeqNo, 33 | metric: Metric, 34 | subnet: Subnet, 35 | router_id: RouterId, 36 | ) -> Self { 37 | babel::Update::new(interval, seqno, metric, subnet, router_id).into() 38 | } 39 | } 40 | 41 | impl Codec { 42 | pub fn new() -> Self { 43 | Codec { 44 | codec: babel::Codec::new(), 45 | } 46 | } 47 | } 48 | 49 | impl Decoder for Codec { 50 | type Item = ControlPacket; 51 | type Error = std::io::Error; 52 | 53 | fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { 54 | self.codec.decode(buf) 55 | } 56 | } 57 | 58 | impl Encoder for Codec { 59 | type Error = io::Error; 60 | 61 | fn encode(&mut self, message: ControlPacket, buf: &mut BytesMut) -> Result<(), Self::Error> { 62 | self.codec.encode(message, buf) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /mycelium/src/packet/data.rs: -------------------------------------------------------------------------------- 1 | use std::net::Ipv6Addr; 2 | 3 | use bytes::{Buf, BufMut, BytesMut}; 4 | use tokio_util::codec::{Decoder, Encoder}; 5 | 6 | /// Size of the header start for a data packet (before the IP addresses). 7 | const DATA_PACKET_HEADER_SIZE: usize = 4; 8 | 9 | /// Mask to extract data length from 10 | const DATA_PACKET_LEN_MASK: u32 = (1 << 16) - 1; 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct DataPacket { 14 | pub raw_data: Vec, // encrypted data itself, then append the nonce 15 | /// Max amount of hops for the packet. 16 | pub hop_limit: u8, 17 | pub src_ip: Ipv6Addr, 18 | pub dst_ip: Ipv6Addr, 19 | } 20 | 21 | pub struct Codec { 22 | header_vals: Option, 23 | src_ip: Option, 24 | dest_ip: Option, 25 | } 26 | 27 | /// Data from the DataPacket header. 28 | #[derive(Clone, Copy)] 29 | struct HeaderValues { 30 | len: u16, 31 | hop_limit: u8, 32 | } 33 | 34 | impl Codec { 35 | pub fn new() -> Self { 36 | Codec { 37 | header_vals: None, 38 | src_ip: None, 39 | dest_ip: None, 40 | } 41 | } 42 | } 43 | 44 | impl Decoder for Codec { 45 | type Item = DataPacket; 46 | type Error = std::io::Error; 47 | 48 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 49 | // Determine the length of the data 50 | let HeaderValues { len, hop_limit } = if let Some(header_vals) = self.header_vals { 51 | header_vals 52 | } else { 53 | // Check we have enough data to decode 54 | if src.len() < DATA_PACKET_HEADER_SIZE { 55 | return Ok(None); 56 | } 57 | 58 | let raw_header = src.get_u32(); 59 | // Hop limit is the last 8 bits. 60 | let hop_limit = (raw_header & 0xFF) as u8; 61 | let data_len = ((raw_header >> 8) & DATA_PACKET_LEN_MASK) as u16; 62 | let header_vals = HeaderValues { 63 | len: data_len, 64 | hop_limit, 65 | }; 66 | 67 | self.header_vals = Some(header_vals); 68 | 69 | header_vals 70 | }; 71 | 72 | let data_len = len as usize; 73 | 74 | // Determine the source IP 75 | let src_ip = if let Some(src_ip) = self.src_ip { 76 | src_ip 77 | } else { 78 | if src.len() < 16 { 79 | return Ok(None); 80 | } 81 | 82 | // Decode octets 83 | let mut ip_bytes = [0u8; 16]; 84 | ip_bytes.copy_from_slice(&src[..16]); 85 | let src_ip = Ipv6Addr::from(ip_bytes); 86 | src.advance(16); 87 | 88 | self.src_ip = Some(src_ip); 89 | src_ip 90 | }; 91 | 92 | // Determine the destination IP 93 | let dest_ip = if let Some(dest_ip) = self.dest_ip { 94 | dest_ip 95 | } else { 96 | if src.len() < 16 { 97 | return Ok(None); 98 | } 99 | 100 | // Decode octets 101 | let mut ip_bytes = [0u8; 16]; 102 | ip_bytes.copy_from_slice(&src[..16]); 103 | let dest_ip = Ipv6Addr::from(ip_bytes); 104 | src.advance(16); 105 | 106 | self.dest_ip = Some(dest_ip); 107 | dest_ip 108 | }; 109 | 110 | // Check we have enough data to decode 111 | if src.len() < data_len { 112 | return Ok(None); 113 | } 114 | 115 | // Decode octets 116 | let mut data = vec![0u8; data_len]; 117 | data.copy_from_slice(&src[..data_len]); 118 | src.advance(data_len); 119 | 120 | // Reset state 121 | self.header_vals = None; 122 | self.dest_ip = None; 123 | self.src_ip = None; 124 | 125 | Ok(Some(DataPacket { 126 | raw_data: data, 127 | hop_limit, 128 | dst_ip: dest_ip, 129 | src_ip, 130 | })) 131 | } 132 | } 133 | 134 | impl Encoder for Codec { 135 | type Error = std::io::Error; 136 | 137 | fn encode(&mut self, item: DataPacket, dst: &mut BytesMut) -> Result<(), Self::Error> { 138 | dst.reserve(item.raw_data.len() + DATA_PACKET_HEADER_SIZE + 16 + 16); 139 | let mut raw_header = 0; 140 | // Add length of the data 141 | raw_header |= (item.raw_data.len() as u32) << 8; 142 | // And hop limit 143 | raw_header |= item.hop_limit as u32; 144 | dst.put_u32(raw_header); 145 | // Write the source IP 146 | dst.put_slice(&item.src_ip.octets()); 147 | // Write the destination IP 148 | dst.put_slice(&item.dst_ip.octets()); 149 | // Write the data 150 | dst.extend_from_slice(&item.raw_data); 151 | 152 | Ok(()) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /mycelium/src/router_id.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use crate::crypto::PublicKey; 4 | 5 | /// A `RouterId` uniquely identifies a router in the network. 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 7 | pub struct RouterId { 8 | pk: PublicKey, 9 | zone: [u8; 2], 10 | rnd: [u8; 6], 11 | } 12 | 13 | impl RouterId { 14 | /// Size in bytes of a `RouterId` 15 | pub const BYTE_SIZE: usize = 40; 16 | 17 | /// Create a new `RouterId` from a [`PublicKey`]. 18 | pub fn new(pk: PublicKey) -> Self { 19 | Self { 20 | pk, 21 | zone: [0; 2], 22 | rnd: rand::random(), 23 | } 24 | } 25 | 26 | /// View this `RouterId` as a byte array. 27 | pub fn as_bytes(&self) -> [u8; Self::BYTE_SIZE] { 28 | let mut out = [0; Self::BYTE_SIZE]; 29 | out[..32].copy_from_slice(self.pk.as_bytes()); 30 | out[32..34].copy_from_slice(&self.zone); 31 | out[34..].copy_from_slice(&self.rnd); 32 | out 33 | } 34 | 35 | /// Converts this `RouterId` to a [`PublicKey`]. 36 | pub fn to_pubkey(self) -> PublicKey { 37 | self.pk 38 | } 39 | } 40 | 41 | impl From<[u8; Self::BYTE_SIZE]> for RouterId { 42 | fn from(bytes: [u8; Self::BYTE_SIZE]) -> RouterId { 43 | RouterId { 44 | pk: PublicKey::from(<&[u8] as TryInto<[u8; 32]>>::try_into(&bytes[..32]).unwrap()), 45 | zone: bytes[32..34].try_into().unwrap(), 46 | rnd: bytes[34..Self::BYTE_SIZE].try_into().unwrap(), 47 | } 48 | } 49 | } 50 | 51 | impl fmt::Display for RouterId { 52 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 53 | let RouterId { pk, zone, rnd } = self; 54 | f.write_fmt(format_args!( 55 | "{pk}-{}-{}", 56 | faster_hex::hex_string(zone), 57 | faster_hex::hex_string(rnd) 58 | )) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mycelium/src/routing_table/iter.rs: -------------------------------------------------------------------------------- 1 | use std::{net::Ipv6Addr, sync::Arc}; 2 | 3 | use crate::subnet::Subnet; 4 | 5 | use super::{subnet_entry::SubnetEntry, NoRouteSubnet, QueriedSubnet, RouteListReadGuard}; 6 | 7 | /// An iterator over a [`routing table`](super::RoutingTable) giving read only access to 8 | /// [`RouteList`]'s. 9 | pub struct RoutingTableIter<'a>( 10 | ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, 11 | ); 12 | 13 | impl<'a> RoutingTableIter<'a> { 14 | /// Create a new `RoutingTableIter` which will iterate over all entries in a [`RoutingTable`]. 15 | pub(super) fn new( 16 | inner: ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, 17 | ) -> Self { 18 | Self(inner) 19 | } 20 | } 21 | 22 | impl Iterator for RoutingTableIter<'_> { 23 | type Item = (Subnet, RouteListReadGuard); 24 | 25 | fn next(&mut self) -> Option { 26 | for (ip, prefix_size, rl) in self.0.by_ref() { 27 | if let SubnetEntry::Exists { list } = &**rl { 28 | return Some(( 29 | Subnet::new(ip.into(), prefix_size as u8) 30 | .expect("Routing table contains valid subnets"), 31 | RouteListReadGuard { inner: list.load() }, 32 | )); 33 | } 34 | } 35 | None 36 | } 37 | } 38 | 39 | /// Iterator over queried routes in the routing table. 40 | pub struct RoutingTableQueryIter<'a>( 41 | ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, 42 | ); 43 | 44 | impl<'a> RoutingTableQueryIter<'a> { 45 | /// Create a new `RoutingTableQueryIter` which will iterate over all queried entries in a [`RoutingTable`]. 46 | pub(super) fn new( 47 | inner: ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, 48 | ) -> Self { 49 | Self(inner) 50 | } 51 | } 52 | 53 | impl Iterator for RoutingTableQueryIter<'_> { 54 | type Item = QueriedSubnet; 55 | 56 | fn next(&mut self) -> Option { 57 | for (ip, prefix_size, rl) in self.0.by_ref() { 58 | if let SubnetEntry::Queried { query_timeout } = &**rl { 59 | return Some(QueriedSubnet::new( 60 | Subnet::new(ip.into(), prefix_size as u8) 61 | .expect("Routing table contains valid subnets"), 62 | *query_timeout, 63 | )); 64 | } 65 | } 66 | None 67 | } 68 | } 69 | 70 | /// Iterator for entries which are explicitly marked as "no route"in the routing table. 71 | pub struct RoutingTableNoRouteIter<'a>( 72 | ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, 73 | ); 74 | 75 | impl<'a> RoutingTableNoRouteIter<'a> { 76 | /// Create a new `RoutingTableNoRouteIter` which will iterate over all entries in a [`RoutingTable`] 77 | /// which are explicitly marked as `NoRoute` 78 | pub(super) fn new( 79 | inner: ip_network_table_deps_treebitmap::Iter<'a, Ipv6Addr, Arc>, 80 | ) -> Self { 81 | Self(inner) 82 | } 83 | } 84 | 85 | impl Iterator for RoutingTableNoRouteIter<'_> { 86 | type Item = NoRouteSubnet; 87 | 88 | fn next(&mut self) -> Option { 89 | for (ip, prefix_size, rl) in self.0.by_ref() { 90 | if let SubnetEntry::NoRoute { expiry } = &**rl { 91 | return Some(NoRouteSubnet::new( 92 | Subnet::new(ip.into(), prefix_size as u8) 93 | .expect("Routing table contains valid subnets"), 94 | *expiry, 95 | )); 96 | } 97 | } 98 | None 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /mycelium/src/routing_table/iter_mut.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::mpsc; 2 | use tokio_util::sync::CancellationToken; 3 | use tracing::trace; 4 | 5 | use crate::subnet::Subnet; 6 | 7 | use super::{ 8 | subnet_entry::SubnetEntry, RouteKey, RouteList, RoutingTableInner, RoutingTableOplogEntry, 9 | }; 10 | use std::{ 11 | net::Ipv6Addr, 12 | sync::{Arc, MutexGuard}, 13 | }; 14 | 15 | /// An iterator over a [`routing table`](super::RoutingTable), yielding mutable access to the 16 | /// entries in the table. 17 | pub struct RoutingTableIterMut<'a, 'b> { 18 | write_guard: 19 | &'b mut MutexGuard<'a, left_right::WriteHandle>, 20 | iter: ip_network_table_deps_treebitmap::Iter<'b, Ipv6Addr, Arc>, 21 | 22 | expired_route_entry_sink: mpsc::Sender, 23 | cancel_token: CancellationToken, 24 | } 25 | 26 | impl<'a, 'b> RoutingTableIterMut<'a, 'b> { 27 | pub(super) fn new( 28 | write_guard: &'b mut MutexGuard< 29 | 'a, 30 | left_right::WriteHandle, 31 | >, 32 | iter: ip_network_table_deps_treebitmap::Iter<'b, Ipv6Addr, Arc>, 33 | 34 | expired_route_entry_sink: mpsc::Sender, 35 | cancel_token: CancellationToken, 36 | ) -> Self { 37 | Self { 38 | write_guard, 39 | iter, 40 | expired_route_entry_sink, 41 | cancel_token, 42 | } 43 | } 44 | 45 | /// Get the next item in this iterator. This is not implemented as the [`Iterator`] trait, 46 | /// since we hand out items which are lifetime bound to this struct. 47 | pub fn next<'c>(&'c mut self) -> Option<(Subnet, RoutingTableIterMutEntry<'a, 'c>)> { 48 | for (ip, prefix_size, rl) in self.iter.by_ref() { 49 | if matches!(&**rl, SubnetEntry::Exists { .. }) { 50 | let subnet = Subnet::new(ip.into(), prefix_size as u8) 51 | .expect("Routing table contains valid subnets"); 52 | return Some(( 53 | subnet, 54 | RoutingTableIterMutEntry { 55 | writer: self.write_guard, 56 | store: Arc::clone(rl), 57 | subnet, 58 | expired_route_entry_sink: self.expired_route_entry_sink.clone(), 59 | cancellation_token: self.cancel_token.clone(), 60 | }, 61 | )); 62 | }; 63 | } 64 | 65 | None 66 | } 67 | } 68 | 69 | /// A smart pointer giving mutable access to a [`RouteList`]. 70 | pub struct RoutingTableIterMutEntry<'a, 'b> { 71 | writer: 72 | &'b mut MutexGuard<'a, left_right::WriteHandle>, 73 | /// Owned copy of the RouteList, this is populated once mutable access the the RouteList has 74 | /// been requested. 75 | store: Arc, 76 | /// The subnet we are writing to. 77 | subnet: Subnet, 78 | expired_route_entry_sink: mpsc::Sender, 79 | cancellation_token: CancellationToken, 80 | } 81 | 82 | impl RoutingTableIterMutEntry<'_, '_> { 83 | /// Updates the routes for this entry 84 | pub fn update_routes, &CancellationToken)>( 85 | &mut self, 86 | mut op: F, 87 | ) { 88 | let mut delete = false; 89 | if let SubnetEntry::Exists { list } = &*self.store { 90 | list.rcu(|rl| { 91 | let mut new_val = rl.clone(); 92 | let v = Arc::make_mut(&mut new_val); 93 | 94 | op(v, &self.expired_route_entry_sink, &self.cancellation_token); 95 | delete = v.is_empty(); 96 | 97 | new_val 98 | }); 99 | 100 | if delete { 101 | trace!(subnet = %self.subnet, "Queue subnet for deletion since route list is now empty"); 102 | self.writer 103 | .append(RoutingTableOplogEntry::Delete(self.subnet)); 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /mycelium/src/routing_table/no_route.rs: -------------------------------------------------------------------------------- 1 | use tokio::time::Instant; 2 | 3 | use crate::subnet::Subnet; 4 | 5 | /// Information about a [`subnet`](Subnet) which is currently marked as NoRoute. 6 | #[derive(Debug, Clone, Copy)] 7 | pub struct NoRouteSubnet { 8 | /// The subnet which has no route. 9 | subnet: Subnet, 10 | /// Time at which the entry expires. After this timeout expires, the entry is removed and a new 11 | /// query can be performed. 12 | entry_expires: Instant, 13 | } 14 | 15 | impl NoRouteSubnet { 16 | /// Create a new `NoRouteSubnet` for the given [`subnet`](Subnet), expiring at the provided 17 | /// [`time`](Instant). 18 | pub fn new(subnet: Subnet, entry_expires: Instant) -> Self { 19 | Self { 20 | subnet, 21 | entry_expires, 22 | } 23 | } 24 | 25 | /// The [`subnet`](Subnet) for which there is no route. 26 | pub fn subnet(&self) -> Subnet { 27 | self.subnet 28 | } 29 | 30 | /// The moment this entry expires. Once this timeout expires, a new query can be launched for 31 | /// route discovery for this [`subnet`](Subnet). 32 | pub fn entry_expires(&self) -> Instant { 33 | self.entry_expires 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mycelium/src/routing_table/queried_subnet.rs: -------------------------------------------------------------------------------- 1 | use tokio::time::Instant; 2 | 3 | use crate::subnet::Subnet; 4 | 5 | /// Information about a [`subnet`](Subnet) which is currently in the queried state 6 | #[derive(Debug, Clone, Copy)] 7 | pub struct QueriedSubnet { 8 | /// The subnet which was queried. 9 | subnet: Subnet, 10 | /// Time at which the query expires. If no feasible updates come in before this, the subnet is 11 | /// marked as no route temporarily. 12 | query_expires: Instant, 13 | } 14 | 15 | impl QueriedSubnet { 16 | /// Create a new `QueriedSubnet` for the given [`subnet`](Subnet), expiring at the provided 17 | /// [`time`](Instant). 18 | pub fn new(subnet: Subnet, query_expires: Instant) -> Self { 19 | Self { 20 | subnet, 21 | query_expires, 22 | } 23 | } 24 | 25 | /// The [`subnet`](Subnet) being queried. 26 | pub fn subnet(&self) -> Subnet { 27 | self.subnet 28 | } 29 | 30 | /// The moment this query expires. If no route is discovered before this, the [`subnet`](Subnet) 31 | /// is marked as no route temporarily. 32 | pub fn query_expires(&self) -> Instant { 33 | self.query_expires 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mycelium/src/routing_table/route_entry.rs: -------------------------------------------------------------------------------- 1 | use tokio::time::Instant; 2 | 3 | use crate::{ 4 | metric::Metric, peer::Peer, router_id::RouterId, sequence_number::SeqNo, 5 | source_table::SourceKey, 6 | }; 7 | 8 | /// RouteEntry holds all relevant information about a specific route. Since this includes the next 9 | /// hop, a single subnet can have multiple route entries. 10 | #[derive(Clone)] 11 | pub struct RouteEntry { 12 | source: SourceKey, 13 | neighbour: Peer, 14 | metric: Metric, 15 | seqno: SeqNo, 16 | selected: bool, 17 | expires: Instant, 18 | } 19 | 20 | impl RouteEntry { 21 | /// Create a new `RouteEntry` with the provided values. 22 | pub fn new( 23 | source: SourceKey, 24 | neighbour: Peer, 25 | metric: Metric, 26 | seqno: SeqNo, 27 | selected: bool, 28 | expires: Instant, 29 | ) -> Self { 30 | Self { 31 | source, 32 | neighbour, 33 | metric, 34 | seqno, 35 | selected, 36 | expires, 37 | } 38 | } 39 | 40 | /// Return the [`SourceKey`] for this `RouteEntry`. 41 | pub fn source(&self) -> SourceKey { 42 | self.source 43 | } 44 | 45 | /// Return the [`neighbour`](Peer) used as next hop for this `RouteEntry`. 46 | pub fn neighbour(&self) -> &Peer { 47 | &self.neighbour 48 | } 49 | 50 | /// Return the [`Metric`] of this `RouteEntry`. 51 | pub fn metric(&self) -> Metric { 52 | self.metric 53 | } 54 | 55 | /// Return the [`sequence number`](SeqNo) for the `RouteEntry`. 56 | pub fn seqno(&self) -> SeqNo { 57 | self.seqno 58 | } 59 | 60 | /// Return if this [`RouteEntry`] is selected. 61 | pub fn selected(&self) -> bool { 62 | self.selected 63 | } 64 | 65 | /// Return the [`Instant`] when this `RouteEntry` expires if it doesn't get updated before 66 | /// then. 67 | pub fn expires(&self) -> Instant { 68 | self.expires 69 | } 70 | 71 | /// Set the [`SourceKey`] for this `RouteEntry`. 72 | pub fn set_source(&mut self, source: SourceKey) { 73 | self.source = source; 74 | } 75 | 76 | /// Set the [`RouterId`] for this `RouteEntry`. 77 | pub fn set_router_id(&mut self, router_id: RouterId) { 78 | self.source.set_router_id(router_id) 79 | } 80 | 81 | /// Sets the [`neighbour`](Peer) for this `RouteEntry`. 82 | pub fn set_neighbour(&mut self, neighbour: Peer) { 83 | self.neighbour = neighbour; 84 | } 85 | 86 | /// Sets the [`Metric`] for this `RouteEntry`. 87 | pub fn set_metric(&mut self, metric: Metric) { 88 | self.metric = metric; 89 | } 90 | 91 | /// Sets the [`sequence number`](SeqNo) for this `RouteEntry`. 92 | pub fn set_seqno(&mut self, seqno: SeqNo) { 93 | self.seqno = seqno; 94 | } 95 | 96 | /// Sets if this `RouteEntry` is the selected route for the associated 97 | /// [`Subnet`](crate::subnet::Subnet). 98 | pub fn set_selected(&mut self, selected: bool) { 99 | self.selected = selected; 100 | } 101 | 102 | /// Sets the expiration time for this [`RouteEntry`]. 103 | pub(super) fn set_expires(&mut self, expires: Instant) { 104 | self.expires = expires; 105 | } 106 | } 107 | 108 | // Manual Debug implementation since SharedSecret is explicitly not Debug 109 | impl std::fmt::Debug for RouteEntry { 110 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 111 | f.debug_struct("RouteEntry") 112 | .field("source", &self.source) 113 | .field("neighbour", &self.neighbour) 114 | .field("metric", &self.metric) 115 | .field("seqno", &self.seqno) 116 | .field("selected", &self.selected) 117 | .field("expires", &self.expires) 118 | .finish() 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /mycelium/src/routing_table/route_key.rs: -------------------------------------------------------------------------------- 1 | use crate::{peer::Peer, subnet::Subnet}; 2 | 3 | /// RouteKey uniquely defines a route via a peer. 4 | #[derive(Debug, Clone, PartialEq)] 5 | pub struct RouteKey { 6 | subnet: Subnet, 7 | neighbour: Peer, 8 | } 9 | 10 | impl RouteKey { 11 | /// Creates a new `RouteKey` for the given [`Subnet`] and [`neighbour`](Peer). 12 | #[inline] 13 | pub fn new(subnet: Subnet, neighbour: Peer) -> Self { 14 | Self { subnet, neighbour } 15 | } 16 | 17 | /// Get's the [`Subnet`] identified by this `RouteKey`. 18 | #[inline] 19 | pub fn subnet(&self) -> Subnet { 20 | self.subnet 21 | } 22 | 23 | /// Gets the [`neighbour`](Peer) identified by this `RouteKey`. 24 | #[inline] 25 | pub fn neighbour(&self) -> &Peer { 26 | &self.neighbour 27 | } 28 | } 29 | 30 | impl std::fmt::Display for RouteKey { 31 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | f.write_fmt(format_args!( 33 | "{} via {}", 34 | self.subnet, 35 | self.neighbour.connection_identifier() 36 | )) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mycelium/src/routing_table/route_list.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::{Deref, DerefMut, Index, IndexMut}, 3 | sync::Arc, 4 | }; 5 | 6 | use tokio::sync::mpsc; 7 | use tokio_util::sync::CancellationToken; 8 | use tracing::{debug, error}; 9 | 10 | use crate::{crypto::SharedSecret, peer::Peer, task::AbortHandle}; 11 | 12 | use super::{RouteEntry, RouteKey}; 13 | 14 | /// The RouteList holds all routes for a specific subnet. 15 | // By convention, if a route is selected, it will always be at index 0 in the list. 16 | #[derive(Clone)] 17 | pub struct RouteList { 18 | list: Vec<(Arc, RouteEntry)>, 19 | shared_secret: SharedSecret, 20 | } 21 | 22 | impl RouteList { 23 | /// Create a new empty RouteList 24 | pub(crate) fn new(shared_secret: SharedSecret) -> Self { 25 | Self { 26 | list: Vec::new(), 27 | shared_secret, 28 | } 29 | } 30 | 31 | /// Returns the [`SharedSecret`] used for encryption of packets to and from the associated 32 | /// [`Subnet`]. 33 | #[inline] 34 | pub fn shared_secret(&self) -> &SharedSecret { 35 | &self.shared_secret 36 | } 37 | 38 | /// Checks if there are any actual routes in the list. 39 | #[inline] 40 | pub fn is_empty(&self) -> bool { 41 | self.list.is_empty() 42 | } 43 | 44 | /// Returns the selected route for the [`Subnet`] this is the `RouteList` for, if one exists. 45 | pub fn selected(&self) -> Option<&RouteEntry> { 46 | self.list 47 | .first() 48 | .map(|(_, re)| re) 49 | .and_then(|re| if re.selected() { Some(re) } else { None }) 50 | } 51 | 52 | /// Returns an iterator over the `RouteList`. 53 | /// 54 | /// The iterator yields all [`route entries`](RouteEntry) in the list. 55 | pub fn iter(&self) -> RouteListIter { 56 | RouteListIter::new(self) 57 | } 58 | 59 | /// Returns an iterator over the `RouteList` yielding mutable access to the elements. 60 | /// 61 | /// The iterator yields all [`route entries`](RouteEntry) in the list. 62 | pub fn iter_mut(&mut self) -> impl Iterator { 63 | self.list.iter_mut().map(|item| RouteGuard { item }) 64 | } 65 | 66 | /// Removes a [`RouteEntry`] from the `RouteList`. 67 | /// 68 | /// This does nothing if the neighbour does not exist. 69 | pub fn remove(&mut self, neighbour: &Peer) { 70 | let Some(pos) = self 71 | .list 72 | .iter() 73 | .position(|re| re.1.neighbour() == neighbour) 74 | else { 75 | return; 76 | }; 77 | 78 | let old = self.list.swap_remove(pos); 79 | old.0.abort(); 80 | } 81 | 82 | /// Swaps the position of 2 `RouteEntry`s in the route list. 83 | pub fn swap(&mut self, first: usize, second: usize) { 84 | self.list.swap(first, second) 85 | } 86 | 87 | pub fn get_mut(&mut self, index: usize) -> Option<&mut RouteEntry> { 88 | self.list.get_mut(index).map(|(_, re)| re) 89 | } 90 | 91 | /// Insert a new [`RouteEntry`] in the `RouteList`. 92 | pub fn insert( 93 | &mut self, 94 | re: RouteEntry, 95 | expired_route_entry_sink: mpsc::Sender, 96 | cancellation_token: CancellationToken, 97 | ) { 98 | let expiration = re.expires(); 99 | let rk = RouteKey::new(re.source().subnet(), re.neighbour().clone()); 100 | let abort_handle = Arc::new( 101 | tokio::spawn(async move { 102 | tokio::select! { 103 | _ = cancellation_token.cancelled() => {} 104 | _ = tokio::time::sleep_until(expiration) => { 105 | debug!(route_key = %rk, "Expired route entry for route key"); 106 | if let Err(e) = expired_route_entry_sink.send(rk).await { 107 | error!(route_key = %e.0, "Failed to send expired route key on cleanup channel"); 108 | } 109 | } 110 | } 111 | }) 112 | .abort_handle().into(), 113 | ); 114 | 115 | self.list.push((abort_handle, re)); 116 | } 117 | } 118 | 119 | pub struct RouteGuard<'a> { 120 | item: &'a mut (Arc, RouteEntry), 121 | } 122 | 123 | impl Deref for RouteGuard<'_> { 124 | type Target = RouteEntry; 125 | 126 | fn deref(&self) -> &Self::Target { 127 | &self.item.1 128 | } 129 | } 130 | 131 | impl DerefMut for RouteGuard<'_> { 132 | fn deref_mut(&mut self) -> &mut Self::Target { 133 | &mut self.item.1 134 | } 135 | } 136 | 137 | impl RouteGuard<'_> { 138 | pub fn set_expires( 139 | &mut self, 140 | expires: tokio::time::Instant, 141 | expired_route_entry_sink: mpsc::Sender, 142 | cancellation_token: CancellationToken, 143 | ) { 144 | let re = &mut self.item.1; 145 | re.set_expires(expires); 146 | let expiration = re.expires(); 147 | let rk = RouteKey::new(re.source().subnet(), re.neighbour().clone()); 148 | let abort_handle = Arc::new( 149 | tokio::spawn(async move { 150 | tokio::select! { 151 | _ = cancellation_token.cancelled() => {} 152 | _ = tokio::time::sleep_until(expiration) => { 153 | debug!(route_key = %rk, "Expired route entry for route key"); 154 | if let Err(e) = expired_route_entry_sink.send(rk).await { 155 | error!(route_key = %e.0, "Failed to send expired route key on cleanup channel"); 156 | } 157 | } 158 | } 159 | }) 160 | .abort_handle().into(), 161 | ); 162 | 163 | self.item.0.abort(); 164 | self.item.0 = abort_handle; 165 | } 166 | } 167 | 168 | impl Index for RouteList { 169 | type Output = RouteEntry; 170 | 171 | fn index(&self, index: usize) -> &Self::Output { 172 | &self.list[index].1 173 | } 174 | } 175 | 176 | impl IndexMut for RouteList { 177 | fn index_mut(&mut self, index: usize) -> &mut Self::Output { 178 | &mut self.list[index].1 179 | } 180 | } 181 | 182 | pub struct RouteListIter<'a> { 183 | route_list: &'a RouteList, 184 | idx: usize, 185 | } 186 | 187 | impl<'a> RouteListIter<'a> { 188 | /// Create a new `RouteListIter` which will iterate over the given [`RouteList`]. 189 | fn new(route_list: &'a RouteList) -> Self { 190 | Self { route_list, idx: 0 } 191 | } 192 | } 193 | 194 | impl<'a> Iterator for RouteListIter<'a> { 195 | type Item = &'a RouteEntry; 196 | 197 | fn next(&mut self) -> Option { 198 | self.idx += 1; 199 | self.route_list.list.get(self.idx - 1).map(|(_, re)| re) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /mycelium/src/routing_table/subnet_entry.rs: -------------------------------------------------------------------------------- 1 | use arc_swap::ArcSwap; 2 | 3 | use super::RouteList; 4 | 5 | /// An entry for a [Subnet](crate::subnet::Subnet) in the routing table. 6 | #[allow(dead_code)] 7 | pub enum SubnetEntry { 8 | /// Routes for the given subnet exist 9 | Exists { list: ArcSwap }, 10 | /// Routes are being queried from peers for the given subnet, but we haven't gotten a response 11 | /// yet 12 | Queried { query_timeout: tokio::time::Instant }, 13 | /// We queried our peers for the subnet, but we didn't get a valid response in time, so there 14 | /// is for sure no route to the subnet. 15 | NoRoute { expiry: tokio::time::Instant }, 16 | } 17 | -------------------------------------------------------------------------------- /mycelium/src/rr_cache.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a cache implementation for route requests 2 | 3 | use std::{ 4 | net::{IpAddr, Ipv6Addr}, 5 | sync::Arc, 6 | }; 7 | 8 | use dashmap::DashMap; 9 | use tokio::time::{Duration, Instant}; 10 | use tracing::trace; 11 | 12 | use crate::{babel::RouteRequest, peer::Peer, subnet::Subnet, task::AbortHandle}; 13 | 14 | /// Clean the route request cache every 5 seconds 15 | const CACHE_CLEANING_INTERVAL: Duration = Duration::from_secs(5); 16 | 17 | /// IP used for the [`Subnet`] in the cache in case there is no prefix specified. 18 | const GLOBAL_SUBNET_IP: IpAddr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)); 19 | 20 | /// Prefix size to use for the [`Subnet`] in case there is no prefix specified. 21 | const GLOBAL_SUBNET_PREFIX_SIZE: u8 = 0; 22 | 23 | /// A self cleaning cache for route requests. 24 | #[derive(Clone)] 25 | pub struct RouteRequestCache { 26 | /// The actual cache, mapping an instance of a route request to the peers which we've sent this 27 | /// to. 28 | cache: Arc>, 29 | 30 | _cleanup_task: Arc, 31 | } 32 | 33 | struct RouteRequestInfo { 34 | /// The lowest generation we've forwarded. 35 | generation: u8, 36 | /// Peers which we've sent this route request to already. 37 | receivers: Vec, 38 | /// The moment we've sent this route request 39 | sent: Instant, 40 | } 41 | 42 | impl RouteRequestCache { 43 | /// Create a new cache which cleans entries which are older than the given expiration. 44 | /// 45 | /// The cache cleaning is done periodically, so entries might live slightly longer than the 46 | /// allowed expiration. 47 | pub fn new(expiration: Duration) -> Self { 48 | let cache = Arc::new(DashMap::with_hasher(ahash::RandomState::new())); 49 | 50 | let _cleanup_task = Arc::new( 51 | tokio::spawn({ 52 | let cache = cache.clone(); 53 | async move { 54 | loop { 55 | tokio::time::sleep(CACHE_CLEANING_INTERVAL).await; 56 | 57 | trace!("Cleaning route request cache"); 58 | 59 | cache.retain(|subnet, info: &mut RouteRequestInfo| { 60 | if info.sent.elapsed() < expiration { 61 | false 62 | } else { 63 | trace!(%subnet, "Removing exired route request from cache"); 64 | true 65 | } 66 | }); 67 | } 68 | } 69 | }) 70 | .abort_handle() 71 | .into(), 72 | ); 73 | 74 | Self { 75 | cache, 76 | _cleanup_task, 77 | } 78 | } 79 | 80 | /// Record a route request which has been sent to peers. 81 | pub fn sent_route_request(&self, rr: RouteRequest, receivers: Vec) { 82 | let subnet = rr.prefix().unwrap_or( 83 | Subnet::new(GLOBAL_SUBNET_IP, GLOBAL_SUBNET_PREFIX_SIZE) 84 | .expect("Static global IPv6 subnet is valid; qed"), 85 | ); 86 | let generation = rr.generation(); 87 | 88 | let rri = RouteRequestInfo { 89 | generation, 90 | receivers, 91 | sent: Instant::now(), 92 | }; 93 | 94 | self.cache.insert(subnet, rri); 95 | } 96 | 97 | /// Get cached info about a route request for a subnet, if it exists. 98 | pub fn info(&self, subnet: Subnet) -> Option<(u8, Vec)> { 99 | self.cache 100 | .get(&subnet) 101 | .map(|rri| (rri.generation, rri.receivers.clone())) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /mycelium/src/seqno_cache.rs: -------------------------------------------------------------------------------- 1 | //! The seqno request cache keeps track of seqno requests sent by the node. This allows us to drop 2 | //! duplicate requests, and to notify the source of requests (if it wasn't the local node) about 3 | //! relevant updates. 4 | 5 | use std::{ 6 | sync::Arc, 7 | time::{Duration, Instant}, 8 | }; 9 | 10 | use dashmap::DashMap; 11 | use tokio::time::MissedTickBehavior; 12 | use tracing::{debug, trace}; 13 | 14 | use crate::{peer::Peer, router_id::RouterId, sequence_number::SeqNo, subnet::Subnet}; 15 | 16 | /// The amount of time to remember a seqno request (since it was first seen), before we remove it 17 | /// (assuming it was not removed manually before that). 18 | const SEQNO_DEDUP_TTL: Duration = Duration::from_secs(60); 19 | 20 | /// A sequence number request, either forwarded or originated by the local node. 21 | #[derive(Clone, Copy, PartialEq, Eq, Hash)] 22 | pub struct SeqnoRequestCacheKey { 23 | pub router_id: RouterId, 24 | pub subnet: Subnet, 25 | pub seqno: SeqNo, 26 | } 27 | 28 | impl std::fmt::Display for SeqnoRequestCacheKey { 29 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 | write!( 31 | f, 32 | "seqno {} for {} from {}", 33 | self.seqno, self.subnet, self.router_id 34 | ) 35 | } 36 | } 37 | 38 | /// Information retained for sequence number requests we've sent. 39 | struct SeqnoForwardInfo { 40 | /// Which peers have asked us to forward this seqno request. 41 | sources: Vec, 42 | /// Which peers have we sent this request to. 43 | targets: Vec, 44 | /// Time at which we first forwarded the requets. 45 | first_sent: Instant, 46 | /// When did we last sent a seqno request. 47 | last_sent: Instant, 48 | } 49 | 50 | /// A cache for outbound seqno requests. Entries in the cache are automatically removed after a 51 | /// certain amount of time. The cache does not account for the source table. That is, if the 52 | /// requested seqno is smaller, it might pass the cache, but should have been blocked earlier by 53 | /// the source table check. As such, this cache should be the last step in deciding if a seqno 54 | /// request is forwarded. 55 | #[derive(Clone)] 56 | pub struct SeqnoCache { 57 | /// Actual cache wrapped in an Arc to make it sharaeble. 58 | cache: Arc>, 59 | } 60 | 61 | impl SeqnoCache { 62 | /// Create a new [`SeqnoCache`]. 63 | pub fn new() -> Self { 64 | trace!(capacity = 0, "Creating new seqno cache"); 65 | 66 | let cache = Arc::new(DashMap::with_hasher_and_shard_amount( 67 | ahash::RandomState::new(), 68 | // This number has been chosen completely at random 69 | 1024, 70 | )); 71 | let sc = Self { cache }; 72 | 73 | // Spawn background cleanup task. 74 | tokio::spawn(sc.clone().sweep_entries()); 75 | 76 | sc 77 | } 78 | 79 | /// Record a forwarded seqno request to a given target. Also keep track of the origin of the 80 | /// request. If the local node generated the request, source must be [`None`] 81 | pub fn forward(&self, request: SeqnoRequestCacheKey, target: Peer, source: Option) { 82 | let mut info = self.cache.entry(request).or_default(); 83 | info.last_sent = Instant::now(); 84 | if !info.targets.contains(&target) { 85 | info.targets.push(target); 86 | } else { 87 | debug!( 88 | seqno_request = %request, 89 | "Already sent seqno request to target {}", 90 | target.connection_identifier() 91 | ); 92 | } 93 | if let Some(source) = source { 94 | if !info.sources.contains(&source) { 95 | info.sources.push(source); 96 | } else { 97 | debug!(seqno_request = %request, "Peer {} is requesting the same seqno again", source.connection_identifier()); 98 | } 99 | } 100 | } 101 | 102 | /// Get a list of all peers which we've already sent the given seqno request to, as well as 103 | /// when we've last sent a request. 104 | pub fn info(&self, request: &SeqnoRequestCacheKey) -> Option<(Instant, Vec)> { 105 | self.cache 106 | .get(request) 107 | .map(|info| (info.last_sent, info.targets.clone())) 108 | } 109 | 110 | /// Removes forwarding info from the seqno cache. If forwarding info is available, the source 111 | /// peers (peers which requested us to forward this request) are returned. 112 | // TODO: cleanup if needed 113 | #[allow(dead_code)] 114 | pub fn remove(&self, request: &SeqnoRequestCacheKey) -> Option> { 115 | self.cache.remove(request).map(|(_, info)| info.sources) 116 | } 117 | 118 | /// Get forwarding info from the seqno cache. If forwarding info is available, the source 119 | /// peers (peers which requested us to forward this request) are returned. 120 | // TODO: cleanup if needed 121 | #[allow(dead_code)] 122 | pub fn get(&self, request: &SeqnoRequestCacheKey) -> Option> { 123 | self.cache.get(request).map(|info| info.sources.clone()) 124 | } 125 | 126 | /// Periodic task to clear old entries for which no reply came in. 127 | async fn sweep_entries(self) { 128 | let mut interval = tokio::time::interval(SEQNO_DEDUP_TTL); 129 | interval.set_missed_tick_behavior(MissedTickBehavior::Skip); 130 | 131 | loop { 132 | interval.tick().await; 133 | 134 | debug!("Cleaning up expired seqno requests from seqno cache"); 135 | 136 | let prev_entries = self.cache.len(); 137 | let prev_cap = self.cache.capacity(); 138 | self.cache 139 | .retain(|_, info| info.first_sent.elapsed() <= SEQNO_DEDUP_TTL); 140 | self.cache.shrink_to_fit(); 141 | 142 | debug!( 143 | cleaned_entries = prev_entries - self.cache.len(), 144 | removed_capacity = prev_cap - self.cache.capacity(), 145 | "Cleaned up stale seqno request cache entries" 146 | ); 147 | } 148 | } 149 | } 150 | 151 | impl Default for SeqnoCache { 152 | fn default() -> Self { 153 | Self::new() 154 | } 155 | } 156 | 157 | impl Default for SeqnoForwardInfo { 158 | fn default() -> Self { 159 | Self { 160 | sources: vec![], 161 | targets: vec![], 162 | first_sent: Instant::now(), 163 | last_sent: Instant::now(), 164 | } 165 | } 166 | } 167 | 168 | impl std::fmt::Debug for SeqnoRequestCacheKey { 169 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 170 | f.debug_struct("SeqnoRequestCacheKey") 171 | .field("router_id", &self.router_id.to_string()) 172 | .field("subnet", &self.subnet.to_string()) 173 | .field("seqno", &self.seqno.to_string()) 174 | .finish() 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /mycelium/src/sequence_number.rs: -------------------------------------------------------------------------------- 1 | //! Dedicated logic for 2 | //! [sequence numbers](https://datatracker.ietf.org/doc/html/rfc8966#name-solving-starvation-sequenci). 3 | 4 | use core::fmt; 5 | use core::ops::{Add, AddAssign}; 6 | 7 | /// This value is compared against when deciding if a `SeqNo` is larger or smaller, [as defined in 8 | /// the babel rfc](https://datatracker.ietf.org/doc/html/rfc8966#section-3.2.1). 9 | const SEQNO_COMPARE_TRESHOLD: u16 = 32_768; 10 | 11 | /// A sequence number on a route. 12 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] 13 | pub struct SeqNo(u16); 14 | 15 | impl SeqNo { 16 | /// Create a new `SeqNo` with the default value. 17 | pub fn new() -> Self { 18 | Self::default() 19 | } 20 | 21 | /// Custom PartialOrd implementation as defined in [the babel rfc](https://datatracker.ietf.org/doc/html/rfc8966#section-3.2.1). 22 | /// Note that we don't implement the [`PartialOrd`](std::cmd::PartialOrd) trait, as the contract on 23 | /// that trait specifically defines that it is transitive, which is clearly not the case here. 24 | /// 25 | /// There is a quirk in this equality comparison where values which are exactly 32_768 apart, 26 | /// will result in false in either way of ordering the arguments, which is counterintuitive to 27 | /// our understanding that a < b generally implies !(b < a). 28 | pub fn lt(&self, other: &Self) -> bool { 29 | if self.0 == other.0 { 30 | false 31 | } else { 32 | other.0.wrapping_sub(self.0) < SEQNO_COMPARE_TRESHOLD 33 | } 34 | } 35 | 36 | /// Custom PartialOrd implementation as defined in [the babel rfc](https://datatracker.ietf.org/doc/html/rfc8966#section-3.2.1). 37 | /// Note that we don't implement the [`PartialOrd`](std::cmd::PartialOrd) trait, as the contract on 38 | /// that trait specifically defines that it is transitive, which is clearly not the case here. 39 | /// 40 | /// There is a quirk in this equality comparison where values which are exactly 32_768 apart, 41 | /// will result in false in either way of ordering the arguments, which is counterintuitive to 42 | /// our understanding that a < b generally implies !(b < a). 43 | pub fn gt(&self, other: &Self) -> bool { 44 | if self.0 == other.0 { 45 | false 46 | } else { 47 | other.0.wrapping_sub(self.0) > SEQNO_COMPARE_TRESHOLD 48 | } 49 | } 50 | } 51 | 52 | impl fmt::Display for SeqNo { 53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 54 | f.write_fmt(format_args!("{}", self.0)) 55 | } 56 | } 57 | 58 | impl From for SeqNo { 59 | fn from(value: u16) -> Self { 60 | SeqNo(value) 61 | } 62 | } 63 | 64 | impl From for u16 { 65 | fn from(value: SeqNo) -> Self { 66 | value.0 67 | } 68 | } 69 | 70 | impl Add for SeqNo { 71 | type Output = Self; 72 | 73 | fn add(self, rhs: u16) -> Self::Output { 74 | SeqNo(self.0.wrapping_add(rhs)) 75 | } 76 | } 77 | 78 | impl AddAssign for SeqNo { 79 | fn add_assign(&mut self, rhs: u16) { 80 | *self = SeqNo(self.0.wrapping_add(rhs)) 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::SeqNo; 87 | 88 | #[test] 89 | fn cmp_eq_seqno() { 90 | let s1 = SeqNo::from(1); 91 | let s2 = SeqNo::from(1); 92 | assert_eq!(s1, s2); 93 | 94 | let s1 = SeqNo::from(10_000); 95 | let s2 = SeqNo::from(10_000); 96 | assert_eq!(s1, s2); 97 | } 98 | 99 | #[test] 100 | fn cmp_small_seqno_increase() { 101 | let s1 = SeqNo::from(1); 102 | let s2 = SeqNo::from(2); 103 | assert!(s1.lt(&s2)); 104 | assert!(!s2.lt(&s1)); 105 | 106 | assert!(s2.gt(&s1)); 107 | assert!(!s1.gt(&s2)); 108 | 109 | let s1 = SeqNo::from(3); 110 | let s2 = SeqNo::from(30_000); 111 | assert!(s1.lt(&s2)); 112 | assert!(!s2.lt(&s1)); 113 | 114 | assert!(s2.gt(&s1)); 115 | assert!(!s1.gt(&s2)); 116 | } 117 | 118 | #[test] 119 | fn cmp_big_seqno_increase() { 120 | let s1 = SeqNo::from(0); 121 | let s2 = SeqNo::from(32_767); 122 | assert!(s1.lt(&s2)); 123 | assert!(!s2.lt(&s1)); 124 | 125 | assert!(s2.gt(&s1)); 126 | assert!(!s1.gt(&s2)); 127 | 128 | // Test equality quirk at cutoff point. 129 | let s1 = SeqNo::from(0); 130 | let s2 = SeqNo::from(32_768); 131 | assert!(!s1.lt(&s2)); 132 | assert!(!s2.lt(&s1)); 133 | 134 | assert!(!s2.gt(&s1)); 135 | assert!(!s1.gt(&s2)); 136 | 137 | let s1 = SeqNo::from(0); 138 | let s2 = SeqNo::from(32_769); 139 | assert!(!s1.lt(&s2)); 140 | assert!(s2.lt(&s1)); 141 | 142 | assert!(!s2.gt(&s1)); 143 | assert!(s1.gt(&s2)); 144 | 145 | let s1 = SeqNo::from(6); 146 | let s2 = SeqNo::from(60_000); 147 | assert!(!s1.lt(&s2)); 148 | assert!(s2.lt(&s1)); 149 | 150 | assert!(!s2.gt(&s1)); 151 | assert!(s1.gt(&s2)); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /mycelium/src/task.rs: -------------------------------------------------------------------------------- 1 | //! This module provides some task abstractions which add custom logic to the default behavior. 2 | 3 | /// A handle to a task, which is only used to abort the task. In case this handle is dropped, the 4 | /// task is cancelled automatically. 5 | pub struct AbortHandle(tokio::task::AbortHandle); 6 | 7 | impl AbortHandle { 8 | /// Abort the task this `AbortHandle` is referencing. It is safe to call this method multiple 9 | /// times, but only the first call is actually usefull. It is possible for the task to still 10 | /// finish succesfully, even after abort is called. 11 | #[inline] 12 | pub fn abort(&self) { 13 | self.0.abort() 14 | } 15 | } 16 | 17 | impl Drop for AbortHandle { 18 | #[inline] 19 | fn drop(&mut self) { 20 | self.0.abort() 21 | } 22 | } 23 | 24 | impl From for AbortHandle { 25 | #[inline] 26 | fn from(value: tokio::task::AbortHandle) -> Self { 27 | Self(value) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /mycelium/src/tun.rs: -------------------------------------------------------------------------------- 1 | //! The tun module implements a platform independent Tun interface. 2 | 3 | #[cfg(any( 4 | target_os = "linux", 5 | all(target_os = "macos", not(feature = "mactunfd")), 6 | target_os = "windows" 7 | ))] 8 | use crate::subnet::Subnet; 9 | 10 | #[cfg(any( 11 | target_os = "linux", 12 | all(target_os = "macos", not(feature = "mactunfd")), 13 | target_os = "windows" 14 | ))] 15 | pub struct TunConfig { 16 | pub name: String, 17 | pub node_subnet: Subnet, 18 | pub route_subnet: Subnet, 19 | } 20 | 21 | #[cfg(any( 22 | target_os = "android", 23 | target_os = "ios", 24 | all(target_os = "macos", feature = "mactunfd"), 25 | ))] 26 | pub struct TunConfig { 27 | pub tun_fd: i32, 28 | } 29 | #[cfg(target_os = "linux")] 30 | mod linux; 31 | 32 | #[cfg(target_os = "linux")] 33 | pub use linux::new; 34 | 35 | #[cfg(all(target_os = "macos", not(feature = "mactunfd")))] 36 | mod darwin; 37 | 38 | #[cfg(all(target_os = "macos", not(feature = "mactunfd")))] 39 | pub use darwin::new; 40 | 41 | #[cfg(target_os = "windows")] 42 | mod windows; 43 | #[cfg(target_os = "windows")] 44 | pub use windows::new; 45 | 46 | #[cfg(target_os = "android")] 47 | mod android; 48 | #[cfg(target_os = "android")] 49 | pub use android::new; 50 | 51 | #[cfg(any(target_os = "ios", all(target_os = "macos", feature = "mactunfd")))] 52 | mod ios; 53 | #[cfg(any(target_os = "ios", all(target_os = "macos", feature = "mactunfd")))] 54 | pub use ios::new; 55 | -------------------------------------------------------------------------------- /mycelium/src/tun/android.rs: -------------------------------------------------------------------------------- 1 | //! android specific tun interface setup. 2 | 3 | use std::io::{self}; 4 | 5 | use futures::{Sink, Stream}; 6 | use tokio::{ 7 | io::{AsyncReadExt, AsyncWriteExt}, 8 | select, 9 | sync::mpsc, 10 | }; 11 | use tracing::{error, info}; 12 | 13 | use crate::crypto::PacketBuffer; 14 | use crate::tun::TunConfig; 15 | 16 | // TODO 17 | const LINK_MTU: i32 = 1400; 18 | 19 | /// Create a new tun interface and set required routes 20 | /// 21 | /// # Panics 22 | /// 23 | /// This function will panic if called outside of the context of a tokio runtime. 24 | pub async fn new( 25 | tun_config: TunConfig, 26 | ) -> Result< 27 | ( 28 | impl Stream>, 29 | impl Sink + Clone, 30 | ), 31 | Box, 32 | > { 33 | let name = "tun0"; 34 | let mut tun = create_tun_interface(name, tun_config.tun_fd)?; 35 | 36 | let (tun_sink, mut sink_receiver) = mpsc::channel::(1000); 37 | let (tun_stream, stream_receiver) = mpsc::unbounded_channel(); 38 | 39 | // Spawn a single task to manage the TUN interface 40 | tokio::spawn(async move { 41 | let mut buf_hold = None; 42 | loop { 43 | let mut buf = if let Some(buf) = buf_hold.take() { 44 | buf 45 | } else { 46 | PacketBuffer::new() 47 | }; 48 | 49 | select! { 50 | data = sink_receiver.recv() => { 51 | match data { 52 | None => return, 53 | Some(data) => { 54 | if let Err(e) = tun.write(&data).await { 55 | error!("Failed to send data to tun interface {e}"); 56 | } 57 | } 58 | } 59 | // Save the buffer as we didn't use it 60 | buf_hold = Some(buf); 61 | } 62 | read_result = tun.read(buf.buffer_mut()) => { 63 | let rr = read_result.map(|n| { 64 | buf.set_size(n); 65 | buf 66 | }); 67 | 68 | 69 | if tun_stream.send(rr).is_err() { 70 | error!("Could not forward data to tun stream, receiver is gone"); 71 | break; 72 | }; 73 | } 74 | } 75 | } 76 | info!("Stop reading from / writing to tun interface"); 77 | }); 78 | 79 | Ok(( 80 | tokio_stream::wrappers::UnboundedReceiverStream::new(stream_receiver), 81 | tokio_util::sync::PollSender::new(tun_sink), 82 | )) 83 | } 84 | 85 | /// Create a new TUN interface 86 | fn create_tun_interface( 87 | name: &str, 88 | tun_fd: i32, 89 | ) -> Result> { 90 | let mut config = tun::Configuration::default(); 91 | config 92 | .name(name) 93 | .layer(tun::Layer::L3) 94 | .mtu(LINK_MTU) 95 | .queues(1) 96 | .raw_fd(tun_fd) 97 | .up(); 98 | info!("create_tun_interface"); 99 | let tun = match tun::create_as_async(&config) { 100 | Ok(tun) => tun, 101 | Err(err) => { 102 | error!("[android]failed to create tun interface: {err}"); 103 | return Err(Box::new(err)); 104 | } 105 | }; 106 | 107 | Ok(tun) 108 | } 109 | -------------------------------------------------------------------------------- /mycelium/src/tun/ios.rs: -------------------------------------------------------------------------------- 1 | //! ios specific tun interface setup. 2 | 3 | use std::io::{self, IoSlice}; 4 | 5 | use futures::{Sink, Stream}; 6 | use tokio::{ 7 | io::{AsyncReadExt, AsyncWriteExt}, 8 | select, 9 | sync::mpsc, 10 | }; 11 | use tracing::{error, info}; 12 | 13 | use crate::crypto::PacketBuffer; 14 | use crate::tun::TunConfig; 15 | 16 | // TODO 17 | const LINK_MTU: i32 = 1400; 18 | 19 | /// The 4 byte packet header written before a packet is sent on the TUN 20 | // TODO: figure out structure and values, but for now this seems to work. 21 | const HEADER: [u8; 4] = [0, 0, 0, 30]; 22 | 23 | /// Create a new tun interface and set required routes 24 | /// 25 | /// # Panics 26 | /// 27 | /// This function will panic if called outside of the context of a tokio runtime. 28 | pub async fn new( 29 | tun_config: TunConfig, 30 | ) -> Result< 31 | ( 32 | impl Stream>, 33 | impl Sink + Clone, 34 | ), 35 | Box, 36 | > { 37 | let mut tun = create_tun_interface(tun_config.tun_fd)?; 38 | 39 | let (tun_sink, mut sink_receiver) = mpsc::channel::(1000); 40 | let (tun_stream, stream_receiver) = mpsc::unbounded_channel(); 41 | 42 | // Spawn a single task to manage the TUN interface 43 | tokio::spawn(async move { 44 | let mut buf_hold = None; 45 | loop { 46 | let mut buf = if let Some(buf) = buf_hold.take() { 47 | buf 48 | } else { 49 | PacketBuffer::new() 50 | }; 51 | 52 | select! { 53 | data = sink_receiver.recv() => { 54 | match data { 55 | None => return, 56 | Some(data) => { 57 | // We need to append a 4 byte header here 58 | if let Err(e) = tun.write_vectored(&[IoSlice::new(&HEADER), IoSlice::new(&data)]).await { 59 | error!("Failed to send data to tun interface {e}"); 60 | } 61 | } 62 | } 63 | // Save the buffer as we didn't use it 64 | buf_hold = Some(buf); 65 | } 66 | read_result = tun.read(buf.buffer_mut()) => { 67 | let rr = read_result.map(|n| { 68 | buf.set_size(n); 69 | // Trim header 70 | buf.buffer_mut().copy_within(4.., 0); 71 | buf.set_size(n-4); 72 | buf 73 | }); 74 | 75 | 76 | if tun_stream.send(rr).is_err() { 77 | error!("Could not forward data to tun stream, receiver is gone"); 78 | break; 79 | }; 80 | } 81 | } 82 | } 83 | info!("Stop reading from / writing to tun interface"); 84 | }); 85 | 86 | Ok(( 87 | tokio_stream::wrappers::UnboundedReceiverStream::new(stream_receiver), 88 | tokio_util::sync::PollSender::new(tun_sink), 89 | )) 90 | } 91 | 92 | /// Create a new TUN interface 93 | fn create_tun_interface(tun_fd: i32) -> Result> { 94 | let mut config = tun::Configuration::default(); 95 | config 96 | .layer(tun::Layer::L3) 97 | .mtu(LINK_MTU) 98 | .queues(1) 99 | .raw_fd(tun_fd) 100 | .up(); 101 | let tun = tun::create_as_async(&config)?; 102 | 103 | Ok(tun) 104 | } 105 | -------------------------------------------------------------------------------- /mycelium/src/tun/linux.rs: -------------------------------------------------------------------------------- 1 | //! Linux specific tun interface setup. 2 | 3 | use std::io; 4 | 5 | use futures::{Sink, Stream, TryStreamExt}; 6 | use rtnetlink::Handle; 7 | use tokio::{select, sync::mpsc}; 8 | use tokio_tun::{Tun, TunBuilder}; 9 | use tracing::{error, info}; 10 | 11 | use crate::crypto::PacketBuffer; 12 | use crate::subnet::Subnet; 13 | use crate::tun::TunConfig; 14 | 15 | // TODO 16 | const LINK_MTU: i32 = 1400; 17 | 18 | /// Create a new tun interface and set required routes 19 | /// 20 | /// # Panics 21 | /// 22 | /// This function will panic if called outside of the context of a tokio runtime. 23 | pub async fn new( 24 | tun_config: TunConfig, 25 | ) -> Result< 26 | ( 27 | impl Stream>, 28 | impl Sink + Clone, 29 | ), 30 | Box, 31 | > { 32 | let tun = match create_tun_interface(&tun_config.name) { 33 | Ok(tun) => tun, 34 | Err(e) => { 35 | error!( 36 | "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", 37 | tun_config.name, 38 | ); 39 | return Err(e); 40 | } 41 | }; 42 | 43 | let (conn, handle, _) = rtnetlink::new_connection()?; 44 | let netlink_task_handle = tokio::spawn(conn); 45 | 46 | let tun_index = link_index_by_name(handle.clone(), tun_config.name).await?; 47 | 48 | if let Err(e) = add_address( 49 | handle.clone(), 50 | tun_index, 51 | Subnet::new( 52 | tun_config.node_subnet.address(), 53 | tun_config.route_subnet.prefix_len(), 54 | ) 55 | .unwrap(), 56 | ) 57 | .await 58 | { 59 | error!( 60 | "Failed to add address {0} to TUN interface: {e}", 61 | tun_config.node_subnet 62 | ); 63 | return Err(e); 64 | } 65 | 66 | // We are done with our netlink connection, abort the task so we can properly clean up. 67 | netlink_task_handle.abort(); 68 | 69 | let (tun_sink, mut sink_receiver) = mpsc::channel::(1000); 70 | let (tun_stream, stream_receiver) = mpsc::unbounded_channel(); 71 | 72 | // Spawn a single task to manage the TUN interface 73 | tokio::spawn(async move { 74 | let mut buf_hold = None; 75 | loop { 76 | let mut buf = if let Some(buf) = buf_hold.take() { 77 | buf 78 | } else { 79 | PacketBuffer::new() 80 | }; 81 | 82 | select! { 83 | data = sink_receiver.recv() => { 84 | match data { 85 | None => return, 86 | Some(data) => { 87 | if let Err(e) = tun.send(&data).await { 88 | error!("Failed to send data to tun interface {e}"); 89 | } 90 | } 91 | } 92 | // Save the buffer as we didn't use it 93 | buf_hold = Some(buf); 94 | } 95 | read_result = tun.recv(buf.buffer_mut()) => { 96 | let rr = read_result.map(|n| { 97 | buf.set_size(n); 98 | buf 99 | }); 100 | 101 | if tun_stream.send(rr).is_err() { 102 | error!("Could not forward data to tun stream, receiver is gone"); 103 | break; 104 | }; 105 | } 106 | } 107 | } 108 | info!("Stop reading from / writing to tun interface"); 109 | }); 110 | 111 | Ok(( 112 | tokio_stream::wrappers::UnboundedReceiverStream::new(stream_receiver), 113 | tokio_util::sync::PollSender::new(tun_sink), 114 | )) 115 | } 116 | 117 | /// Create a new TUN interface 118 | fn create_tun_interface(name: &str) -> Result> { 119 | let tun = TunBuilder::new() 120 | .name(name) 121 | .mtu(LINK_MTU) 122 | .queues(1) 123 | .up() 124 | .build()? 125 | .pop() 126 | .expect("Succesfully build tun interface has 1 queue"); 127 | 128 | Ok(tun) 129 | } 130 | 131 | /// Retrieve the link index of an interface with the given name 132 | async fn link_index_by_name( 133 | handle: Handle, 134 | name: String, 135 | ) -> Result> { 136 | handle 137 | .link() 138 | .get() 139 | .match_name(name) 140 | .execute() 141 | .try_next() 142 | .await? 143 | .map(|link_message| link_message.header.index) 144 | .ok_or(io::Error::new(io::ErrorKind::NotFound, "link not found").into()) 145 | } 146 | 147 | /// Add an address to an interface. 148 | /// 149 | /// The kernel will automatically add a route entry for the subnet assigned to the interface. 150 | async fn add_address( 151 | handle: Handle, 152 | link_index: u32, 153 | subnet: Subnet, 154 | ) -> Result<(), Box> { 155 | Ok(handle 156 | .address() 157 | .add(link_index, subnet.address(), subnet.prefix_len()) 158 | .execute() 159 | .await?) 160 | } 161 | -------------------------------------------------------------------------------- /mycelium/src/tun/windows.rs: -------------------------------------------------------------------------------- 1 | use std::{io, ops::Deref, sync::Arc}; 2 | 3 | use futures::{Sink, Stream}; 4 | use tokio::sync::mpsc; 5 | use tracing::{error, info, warn}; 6 | 7 | use crate::tun::TunConfig; 8 | use crate::{crypto::PacketBuffer, subnet::Subnet}; 9 | 10 | // TODO 11 | const LINK_MTU: usize = 1400; 12 | 13 | /// Type of the tunnel used, specified when creating the tunnel. 14 | const WINDOWS_TUNNEL_TYPE: &str = "Mycelium"; 15 | 16 | pub async fn new( 17 | tun_config: TunConfig, 18 | ) -> Result< 19 | ( 20 | impl Stream>, 21 | impl Sink + Clone, 22 | ), 23 | Box, 24 | > { 25 | // SAFETY: for now we assume a valid wintun.dll file exists in the root directory when we are 26 | // running this. 27 | let wintun = unsafe { wintun::load() }?; 28 | let wintun_version = match wintun::get_running_driver_version(&wintun) { 29 | Ok(v) => format!("{v}"), 30 | Err(e) => { 31 | warn!("Failed to read wintun.dll version: {e}"); 32 | "Unknown".to_string() 33 | } 34 | }; 35 | info!("Loaded wintun.dll - running version {wintun_version}"); 36 | let tun = wintun::Adapter::create(&wintun, &tun_config.name, WINDOWS_TUNNEL_TYPE, None)?; 37 | info!("Created wintun tunnel interface"); 38 | // Configure created network adapter. 39 | set_adapter_mtu(&tun_config.name, LINK_MTU)?; 40 | // Set address, this will use a `netsh` command under the hood unfortunately. 41 | // TODO: fix in library 42 | // tun.set_network_addresses_tuple(node_subnet.address(), route_subnet.mask(), None)?; 43 | add_address( 44 | &tun_config.name, 45 | tun_config.node_subnet, 46 | tun_config.route_subnet, 47 | )?; 48 | // Build 2 separate sessions - one for receiving, one for sending. 49 | let rx_session = Arc::new(tun.start_session(wintun::MAX_RING_CAPACITY)?); 50 | let tx_session = rx_session.clone(); 51 | 52 | let (tun_sink, mut sink_receiver) = mpsc::channel::(1000); 53 | let (tun_stream, stream_receiver) = mpsc::unbounded_channel(); 54 | 55 | // Ingress path 56 | tokio::task::spawn_blocking(move || { 57 | loop { 58 | let packet = rx_session 59 | .receive_blocking() 60 | .map(|tun_packet| { 61 | let mut buffer = PacketBuffer::new(); 62 | // SAFETY: The configured MTU is smaller than the static PacketBuffer size. 63 | let packet_len = tun_packet.bytes().len(); 64 | buffer.buffer_mut()[..packet_len].copy_from_slice(tun_packet.bytes()); 65 | buffer.set_size(packet_len); 66 | buffer 67 | }) 68 | .map_err(wintun_to_io_error); 69 | 70 | if tun_stream.send(packet).is_err() { 71 | error!("Could not forward data to tun stream, receiver is gone"); 72 | break; 73 | }; 74 | } 75 | 76 | info!("Stop reading from tun interface"); 77 | }); 78 | 79 | // Egress path 80 | tokio::task::spawn_blocking(move || { 81 | loop { 82 | match sink_receiver.blocking_recv() { 83 | None => break, 84 | Some(data) => { 85 | let mut tun_packet = 86 | match tx_session.allocate_send_packet(data.deref().len() as u16) { 87 | Ok(tun_packet) => tun_packet, 88 | Err(e) => { 89 | error!("Could not allocate packet on TUN: {e}"); 90 | break; 91 | } 92 | }; 93 | // SAFETY: packet allocation is done on the length of &data. 94 | tun_packet.bytes_mut().copy_from_slice(&data); 95 | tx_session.send_packet(tun_packet); 96 | } 97 | } 98 | } 99 | info!("Stop writing to tun interface"); 100 | }); 101 | 102 | Ok(( 103 | tokio_stream::wrappers::UnboundedReceiverStream::new(stream_receiver), 104 | tokio_util::sync::PollSender::new(tun_sink), 105 | )) 106 | } 107 | 108 | /// Helper method to convert a [`wintun::Error`] to a [`std::io::Error`]. 109 | fn wintun_to_io_error(err: wintun::Error) -> io::Error { 110 | match err { 111 | wintun::Error::Io(e) => e, 112 | _ => io::Error::new(io::ErrorKind::Other, "unknown wintun error"), 113 | } 114 | } 115 | 116 | /// Set an address on an interface by shelling out to `netsh` 117 | /// 118 | /// We assume this is an IPv6 address. 119 | fn add_address(adapter_name: &str, subnet: Subnet, route_subnet: Subnet) -> Result<(), io::Error> { 120 | let exit_code = std::process::Command::new("netsh") 121 | .args([ 122 | "interface", 123 | "ipv6", 124 | "set", 125 | "address", 126 | adapter_name, 127 | &format!("{}/{}", subnet.address(), route_subnet.prefix_len()), 128 | ]) 129 | .spawn()? 130 | .wait()?; 131 | 132 | match exit_code.code() { 133 | Some(0) => Ok(()), 134 | Some(x) => Err(io::Error::from_raw_os_error(x)), 135 | None => { 136 | warn!("Failed to determine `netsh` exit status"); 137 | Ok(()) 138 | } 139 | } 140 | } 141 | 142 | fn set_adapter_mtu(name: &str, mtu: usize) -> Result<(), io::Error> { 143 | let args = &[ 144 | "interface", 145 | "ipv6", 146 | "set", 147 | "subinterface", 148 | &format!("\"{}\"", name), 149 | &format!("mtu={}", mtu), 150 | "store=persistent", 151 | ]; 152 | 153 | let exit_code = std::process::Command::new("netsh") 154 | .args(args) 155 | .spawn()? 156 | .wait()?; 157 | 158 | match exit_code.code() { 159 | Some(0) => Ok(()), 160 | Some(x) => Err(io::Error::from_raw_os_error(x)), 161 | None => { 162 | warn!("Failed to determine `netsh` exit status"); 163 | Ok(()) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /myceliumd-private/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /myceliumd-private/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "myceliumd-private" 3 | version = "0.6.1" 4 | edition = "2021" 5 | license-file = "../LICENSE" 6 | readme = "./README.md" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | [features] 10 | vendored-openssl = ["mycelium/vendored-openssl"] 11 | 12 | [[bin]] 13 | name = "mycelium-private" 14 | path = "src/main.rs" 15 | 16 | [dependencies] 17 | clap = { version = "4.5.38", features = ["derive"] } 18 | tracing = { version = "0.1.41", features = ["release_max_level_debug"] } 19 | tracing-logfmt = { version = "0.3.5", features = ["ansi_logs"] } 20 | tracing-subscriber = { version = "0.3.19", features = [ 21 | "env-filter", 22 | "nu-ansi-term", 23 | ] } 24 | mycelium = { path = "../mycelium", features = ["private-network", "message"] } 25 | mycelium-metrics = { path = "../mycelium-metrics", features = ["prometheus"] } 26 | mycelium-api = { path = "../mycelium-api", features = ["message"] } 27 | mycelium-cli = { path = "../mycelium-cli/", features = ["message"] } 28 | serde = { version = "1.0.219", features = ["derive"] } 29 | serde_json = "1.0.140" 30 | tokio = { version = "1.45.0", features = [ 31 | "macros", 32 | "rt-multi-thread", 33 | "signal", 34 | ] } 35 | reqwest = { version = "0.12.9", default-features = false, features = ["json"] } 36 | base64 = "0.22.1" 37 | config = "0.15.9" 38 | dirs = "6.0.0" 39 | toml = "0.8.22" 40 | -------------------------------------------------------------------------------- /myceliumd-private/README.md: -------------------------------------------------------------------------------- 1 | # Myceliumd-private 2 | 3 | This is the main binary to use when connecting to [a private network](../docs/private_network.md). 4 | You can either get a release from [the GitHub release page](https://github.com/threefoldtech/myceliumd/releases), 5 | or build the code yourself here. While we intend to keep the master branch as stable, 6 | i.e. a build from latest master should succeed, this is not a hard guarantee. Additionally, 7 | the master branch might not be compatible with the latest release, in case of a 8 | breaking change. 9 | 10 | Building, with the rust toolchain installed, can be done by running `cargo build` 11 | in this directory. Since we rely on `openssl` for the private network functionality, 12 | that will also need to be installed. The produced binary will be dynamically linked 13 | with this system installation. Alternatively, you can use the `vendored-openssl` 14 | feature to compile `openssl` from source and statically link it. You won't need 15 | to have `openssl` installed, though building will of course take longer. To stay 16 | in line with the regular `mycelium` binary, the produced binary here is renamed 17 | to `mycelium-private`. Optionally, the `--release` flag can be used when building. 18 | This is recommended if you are not just developing. 19 | 20 | While this binary can currently be used to connect to the public network, it is 21 | not guaranteed that this will always be the case. If you intend to connect to the 22 | public network, consider using [`the mycelium binary`](../myceliumd/README.md) 23 | instead. 24 | 25 | For more information about the project, please refer to [the README file in the 26 | repository root](../README.md). 27 | -------------------------------------------------------------------------------- /myceliumd/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /myceliumd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "myceliumd" 3 | version = "0.6.1" 4 | edition = "2021" 5 | license-file = "../LICENSE" 6 | readme = "./README.md" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [[bin]] 11 | name = "mycelium" 12 | path = "src/main.rs" 13 | 14 | [dependencies] 15 | clap = { version = "4.5.38", features = ["derive"] } 16 | tracing = { version = "0.1.41", features = ["release_max_level_debug"] } 17 | tracing-logfmt = { version = "0.3.5", features = ["ansi_logs"] } 18 | tracing-subscriber = { version = "0.3.19", features = [ 19 | "env-filter", 20 | "nu-ansi-term", 21 | ] } 22 | mycelium = { path = "../mycelium", features = ["message"] } 23 | mycelium-metrics = { path = "../mycelium-metrics", features = ["prometheus"] } 24 | mycelium-cli = { path = "../mycelium-cli/", features = ["message"] } 25 | mycelium-api = { path = "../mycelium-api", features = ["message"] } 26 | serde = { version = "1.0.219", features = ["derive"] } 27 | serde_json = "1.0.140" 28 | tokio = { version = "1.45.0", features = [ 29 | "macros", 30 | "rt-multi-thread", 31 | "signal", 32 | ] } 33 | reqwest = { version = "0.12.9", default-features = false, features = ["json"] } 34 | base64 = "0.22.1" 35 | prettytable-rs = "0.10.0" 36 | urlencoding = "2.1.3" 37 | byte-unit = "5.1.6" 38 | config = "0.15.9" 39 | dirs = "6.0.0" 40 | toml = "0.8.22" 41 | -------------------------------------------------------------------------------- /myceliumd/README.md: -------------------------------------------------------------------------------- 1 | # Myceliumd 2 | 3 | This is the main binary to use for joining a/the public [`mycelium`] network. You can 4 | either get the latest release from [the GitHub release page](https://github.com/threefoldtech/myceliumd/releases/latest), 5 | or build the code yourself here. While we intend to keep the master branch as stable, 6 | i.e. a build from latest master should succeed, this is not a hard guarantee. Additionally, 7 | the master branch might not be compatible with the latest release, in case of a 8 | breaking change. 9 | 10 | Building, with the rust toolchain installed, can be done by running `cargo build` 11 | in this directory. For compatibility reasons, the binary is renamed to `mycelium`. 12 | Optionally, the `--release` flag can be used when building. This is recommended 13 | if you are not just developing. 14 | 15 | For more information about the project, please refer to [the README file in the 16 | repository root](../README.md). 17 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Development / test scripts 2 | 3 | ## `setup_network.sh` 4 | 5 | `setup_network.sh` is used as-is, and as_root :-/ 6 | This little thing adds some LINUX network namespaces in which you can run mycelium 7 | U're a dev so deal with it 8 | 9 | 10 | ## testing mycelium (`bigmush.sh`) 11 | 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. 12 | 13 | ### Usage : 14 | 15 | Start with 16 | ```bash 17 | source ./bigmush.sh 18 | getmycelium 19 | ``` 20 | will get the latest __release__ binary from github 21 | 22 | Then 23 | ```bash 24 | source ./bigmush.sh 25 | doit 26 | ``` 27 | 28 | will create and start a 50 node mycelium with one central 29 | 30 | ```bash 31 | source ./bigmush 32 | dropit 33 | ``` 34 | will kill with little mercy mycelium daemons and delete the namespaces 35 | 36 | 37 | ```bash 38 | source ./bigmush.sh 39 | cleanit 40 | ``` 41 | will do a `dropit` and clean `*.{bin,out}` files 42 | 43 | ```bash 44 | showit 45 | ``` 46 | 47 | will send a USR1 signal to all mycelium daemons that will 48 | - send routing tables and peers to stdout 49 | - where stdout will be captured in `xx.out` for each NS 50 | 51 | ### logging 52 | every namespace has an `xx.out` file that is stout and stderr 53 | the `xx.bin` file is the namespace daemon's privkey. 54 | 55 | ### behaviour testing 56 | 57 | 1) verify if you can reach all mycelium namespaces 58 | 2) also when running another machine in your net, verify if it's automatically detected 59 | -------------------------------------------------------------------------------- /scripts/bigmush.fish: -------------------------------------------------------------------------------- 1 | set NATNET 172.16.0.0/16 2 | set NUMOFNS 32 3 | 4 | function IPN 5 | sudo ip net $argv 6 | end 7 | 8 | function IPL 9 | sudo ip link $argv 10 | end 11 | 12 | function IPA 13 | sudo ip addr add $argv 14 | end 15 | 16 | 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 17 | 18 | function IPNA 19 | set name $argv[1] 20 | set -e argv[1] 21 | sudo ip -n $name addr add $argv 22 | end 23 | 24 | function IPNL 25 | set name $argv[1] 26 | set -e argv[1] 27 | sudo ip -n $name link $argv 28 | end 29 | 30 | function IPNR 31 | set name $argv[1] 32 | set defrtr (string replace -r '/24$' '' $argv[2]) 33 | set -e argv[1] 34 | set -e argv[2] 35 | sudo ip -n $name route add default via $defrtr 36 | end 37 | 38 | function createns 39 | set iname $argv[1] 40 | set in_ip $argv[2] 41 | set out_ip $argv[3] 42 | set name n-$iname 43 | IPN add $name 44 | IPL add in_$iname type veth peer name out_$iname 45 | IPL set in_$iname netns $name 46 | IPNL $name set lo up 47 | IPNL $name set in_$iname up 48 | IPL set out_$iname up 49 | IPNA $name $in_ip dev in_$iname 50 | IPA $out_ip dev out_$iname 51 | IPNR $name $out_ip 52 | 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 & 53 | end 54 | 55 | function dropns 56 | set iname $argv[1] 57 | set name n-$iname 58 | IPL del out_$iname 59 | IPN del $name 60 | end 61 | 62 | function doit 63 | nohup sudo ./mycelium --key-file host.bin --api-addr 127.0.0.1:8989 --peers $peers > host.out & 64 | for i in (seq 1 $NUMOFNS) 65 | createns $i 172.16.$i.2/24 172.16.$i.1/24 66 | end 67 | end 68 | 69 | function dropit 70 | sudo pkill -9 mycelium 71 | for i in (seq 1 $NUMOFNS) 72 | dropns $i 73 | end 74 | end 75 | 76 | function cleanit 77 | dropit 78 | sudo rm ./*.bin 79 | sudo rm ./*.out 80 | end 81 | 82 | function showit 83 | sudo killall -USR1 mycelium 84 | end 85 | 86 | function getmycelium 87 | wget https://github.com/threefoldtech/mycelium/releases/latest/download/mycelium-x86_64-unknown-linux-musl.tar.gz \ 88 | -O- | gunzip -c | tar xvf - -C $PWD 89 | end 90 | 91 | -------------------------------------------------------------------------------- /scripts/bigmush.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # 3 | NATNET=172.16.0.0/16 4 | NUMOFNS=32 5 | alias IPN='sudo ip net' 6 | alias IPL='sudo ip link' 7 | alias IPA='sudo ip addr add' 8 | 9 | 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' 10 | 11 | function IPNA() { 12 | local name=$1 13 | shift 14 | sudo ip -n ${name} addr add $@ 15 | } 16 | function IPNL() { 17 | local name=$1 18 | shift 19 | sudo ip -n ${name} link $@ 20 | } 21 | function IPNR() { 22 | local name=$1 23 | shift 24 | local defrtr=${1/\/24/} 25 | shift 26 | sudo ip -n ${name} route add default via ${defrtr} 27 | } 28 | function createns() { 29 | local iname=$1 30 | local in_ip=$2 31 | local out_ip=$3 32 | local name=n-${iname} 33 | IPN add $name 34 | IPL add in_${iname} type veth peer name out_${iname} 35 | IPL set in_${iname} netns ${name} 36 | IPNL ${name} set lo up 37 | IPNL ${name} set in_${iname} up 38 | IPL set out_${iname} up 39 | IPNA ${name} ${in_ip} dev in_${iname} 40 | IPA ${out_ip} dev out_${iname} 41 | IPNR ${name} ${out_ip} 42 | # start mycelium, relying on local discovery 43 | 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 & 44 | } 45 | function dropns() { 46 | local iname=$1 47 | local name=n-${iname} 48 | IPL del out_${iname} 49 | IPN del ${name} 50 | } 51 | 52 | function doit() { 53 | nohup sudo ./mycelium --key-file host.bin --api-addr 127.0.0.1:8989 --peers ${peers} >host.out & 54 | for i in $(seq 1 $NUMOFNS); do 55 | createns ${i} 172.16.${i}.2/24 172.16.${i}.1/24 56 | done 57 | } 58 | function dropit() { 59 | sudo pkill -9 mycelium 60 | for i in $(seq 1 $NUMOFNS); do 61 | dropns ${i} 62 | done 63 | } 64 | 65 | function cleanit() { 66 | dropit 67 | sudo rm ./*.bin 68 | sudo rm ./*.out 69 | } 70 | 71 | function showit() { 72 | sudo killall -USR1 mycelium 73 | } 74 | 75 | function getmycelium() { 76 | wget https://github.com/threefoldtech/mycelium/releases/latest/download/mycelium-x86_64-unknown-linux-musl.tar.gz \ 77 | -O- | gunzip -c | tar xvf - -C ${PWD} 78 | 79 | } 80 | -------------------------------------------------------------------------------- /scripts/setup_network.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # 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. 4 | # 5 | # Note: this script requires root privilege 6 | 7 | set -ex 8 | 9 | # Create veth pairs 10 | ip l add p0 type veth peer p1 11 | ip l add p2 type veth peer p3 12 | ip l add p4 type veth peer p5 13 | ip l add p6 type veth peer p7 14 | 15 | # Create bridge 16 | ip l add mycelium-br type bridge 17 | 18 | # Add 1 part of every veth pair in the bridge 19 | ip l set p1 master mycelium-br 20 | ip l set p3 master mycelium-br 21 | ip l set p5 master mycelium-br 22 | ip l set p7 master mycelium-br 23 | 24 | # Add network namespaces 25 | ip netns add net1 26 | ip netns add net2 27 | ip netns add net3 28 | 29 | # Enable loopback devices in network namespaces 30 | ip -n net1 l set lo up 31 | ip -n net2 l set lo up 32 | ip -n net3 l set lo up 33 | 34 | # Add 1 veth end to every network namespace 35 | ip l set p2 netns net1 36 | ip l set p4 netns net2 37 | ip l set p6 netns net3 38 | 39 | # Set underlay IP addresses on the veth parts which are not in the bridge 40 | ip a add 10.0.2.1/24 dev p0 41 | ip -n net1 a add 10.0.2.2/24 dev p2 42 | ip -n net2 a add 10.0.2.3/24 dev p4 43 | ip -n net3 a add 10.0.2.4/24 dev p6 44 | 45 | # Set all veth interface to up 46 | ip l set p0 up 47 | ip l set p1 up 48 | ip -n net1 l set p2 up 49 | ip l set p3 up 50 | ip -n net2 l set p4 up 51 | ip l set p5 up 52 | ip -n net3 l set p6 up 53 | ip l set p7 up 54 | 55 | # Set bridge as up 56 | ip l set mycelium-br up 57 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | ( 2 | import 3 | ( 4 | let 5 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 6 | in 7 | fetchTarball { 8 | url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 9 | sha256 = lock.nodes.flake-compat.locked.narHash; 10 | } 11 | ) 12 | {src = ./.;} 13 | ) 14 | .shellNix 15 | -------------------------------------------------------------------------------- /systemd/mycelium.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=End-2-end encrypted IPv6 overlay network 3 | Wants=network.target 4 | After=network.target 5 | Documentation=https://github.com/threefoldtech/mycelium 6 | 7 | [Service] 8 | ProtectHome=true 9 | ProtectSystem=true 10 | SyslogIdentifier=mycelium 11 | CapabilityBoundingSet=CAP_NET_ADMIN 12 | StateDirectory=mycelium 13 | StateDirectoryMode=0700 14 | ExecStartPre=+-/sbin/modprobe tun 15 | 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 16 | Restart=always 17 | RestartSec=5 18 | TimeoutStopSec=5 19 | 20 | [Install] 21 | WantedBy=multi-user.target 22 | --------------------------------------------------------------------------------