├── .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