├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ └── release.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── install_service.sh ├── validator-firewall-common ├── Cargo.toml └── src │ └── lib.rs ├── validator-firewall-ebpf ├── .cargo │ └── config.toml ├── Cargo.toml ├── rust-toolchain.toml └── src │ └── main.rs ├── validator-firewall ├── Cargo.toml └── src │ ├── config.rs │ ├── ip_service.rs │ ├── ip_service_http.rs │ ├── ip_service_main.rs │ ├── leader_tracker.rs │ ├── main.rs │ └── stats_service.rs ├── validator_firewall.png └── xtask ├── Cargo.toml └── src ├── build_ebpf.rs ├── main.rs └── run.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --" 3 | -------------------------------------------------------------------------------- /.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 | 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: nightly 17 | 18 | - name: Install bpf-linker 19 | uses: actions-rs/cargo@v1 20 | with: 21 | command: install 22 | args: bpf-linker 23 | 24 | - uses: actions/cache@v2 25 | with: 26 | path: | 27 | ~/.cargo/bin/ 28 | ~/.cargo/registry/index/ 29 | ~/.cargo/registry/cache/ 30 | ~/.cargo/git/db/ 31 | target/ 32 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 33 | 34 | - uses: actions-rs/cargo@v1 35 | with: 36 | command: xtask 37 | args: build-ebpf --release 38 | 39 | - uses: actions-rs/cargo@v1 40 | with: 41 | command: build 42 | args: --release 43 | 44 | - uses: ncipollo/release-action@v1 45 | with: 46 | artifacts: target/release/validator-firewall 47 | omitBody: true 48 | token: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/master/Rust.gitignore 2 | 3 | # Generated by Cargo 4 | # will have compiled files and executables 5 | debug/ 6 | target/ 7 | 8 | # IDEs 9 | .idea 10 | .vim 11 | .vscode 12 | 13 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 14 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 15 | Cargo.lock 16 | 17 | # These are backup files generated by rustfmt 18 | **/*.rs.bk 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["xtask", "validator-firewall", "validator-firewall-common"] 3 | resolver = "2" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Helius Blockchain Technologies, Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | Apache License 17 | Version 2.0, January 2004 18 | http://www.apache.org/licenses/ 19 | 20 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 21 | 22 | 1. Definitions. 23 | 24 | "License" shall mean the terms and conditions for use, reproduction, 25 | and distribution as defined by Sections 1 through 9 of this document. 26 | 27 | "Licensor" shall mean the copyright owner or entity authorized by 28 | the copyright owner that is granting the License. 29 | 30 | "Legal Entity" shall mean the union of the acting entity and all 31 | other entities that control, are controlled by, or are under common 32 | control with that entity. For the purposes of this definition, 33 | "control" means (i) the power, direct or indirect, to cause the 34 | direction or management of such entity, whether by contract or 35 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 36 | outstanding shares, or (iii) beneficial ownership of such entity. 37 | 38 | "You" (or "Your") shall mean an individual or Legal Entity 39 | exercising permissions granted by this License. 40 | 41 | "Source" form shall mean the preferred form for making modifications, 42 | including but not limited to software source code, documentation 43 | source, and configuration files. 44 | 45 | "Object" form shall mean any form resulting from mechanical 46 | transformation or translation of a Source form, including but 47 | not limited to compiled object code, generated documentation, 48 | and conversions to other media types. 49 | 50 | "Work" shall mean the work of authorship, whether in Source or 51 | Object form, made available under the License, as indicated by a 52 | copyright notice that is included in or attached to the work 53 | (an example is provided in the Appendix below). 54 | 55 | "Derivative Works" shall mean any work, whether in Source or Object 56 | form, that is based on (or derived from) the Work and for which the 57 | editorial revisions, annotations, elaborations, or other modifications 58 | represent, as a whole, an original work of authorship. For the purposes 59 | of this License, Derivative Works shall not include works that remain 60 | separable from, or merely link (or bind by name) to the interfaces of, 61 | the Work and Derivative Works thereof. 62 | 63 | "Contribution" shall mean any work of authorship, including 64 | the original version of the Work and any modifications or additions 65 | to that Work or Derivative Works thereof, that is intentionally 66 | submitted to Licensor for inclusion in the Work by the copyright owner 67 | or by an individual or Legal Entity authorized to submit on behalf of 68 | the copyright owner. For the purposes of this definition, "submitted" 69 | means any form of electronic, verbal, or written communication sent 70 | to the Licensor or its representatives, including but not limited to 71 | communication on electronic mailing lists, source code control systems, 72 | and issue tracking systems that are managed by, or on behalf of, the 73 | Licensor for the purpose of discussing and improving the Work, but 74 | excluding communication that is conspicuously marked or otherwise 75 | designated in writing by the copyright owner as "Not a Contribution." 76 | 77 | "Contributor" shall mean Licensor and any individual or Legal Entity 78 | on behalf of whom a Contribution has been received by Licensor and 79 | subsequently incorporated within the Work. 80 | 81 | 2. Grant of Copyright License. Subject to the terms and conditions of 82 | this License, each Contributor hereby grants to You a perpetual, 83 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 84 | copyright license to reproduce, prepare Derivative Works of, 85 | publicly display, publicly perform, sublicense, and distribute the 86 | Work and such Derivative Works in Source or Object form. 87 | 88 | 3. Grant of Patent License. Subject to the terms and conditions of 89 | this License, each Contributor hereby grants to You a perpetual, 90 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 91 | (except as stated in this section) patent license to make, have made, 92 | use, offer to sell, sell, import, and otherwise transfer the Work, 93 | where such license applies only to those patent claims licensable 94 | by such Contributor that are necessarily infringed by their 95 | Contribution(s) alone or by combination of their Contribution(s) 96 | with the Work to which such Contribution(s) was submitted. If You 97 | institute patent litigation against any entity (including a 98 | cross-claim or counterclaim in a lawsuit) alleging that the Work 99 | or a Contribution incorporated within the Work constitutes direct 100 | or contributory patent infringement, then any patent licenses 101 | granted to You under this License for that Work shall terminate 102 | as of the date such litigation is filed. 103 | 104 | 4. Redistribution. You may reproduce and distribute copies of the 105 | Work or Derivative Works thereof in any medium, with or without 106 | modifications, and in Source or Object form, provided that You 107 | meet the following conditions: 108 | 109 | (a) You must give any other recipients of the Work or 110 | Derivative Works a copy of this License; and 111 | 112 | (b) You must cause any modified files to carry prominent notices 113 | stating that You changed the files; and 114 | 115 | (c) You must retain, in the Source form of any Derivative Works 116 | that You distribute, all copyright, patent, trademark, and 117 | attribution notices from the Source form of the Work, 118 | excluding those notices that do not pertain to any part of 119 | the Derivative Works; and 120 | 121 | (d) If the Work includes a "NOTICE" text file as part of its 122 | distribution, then any Derivative Works that You distribute must 123 | include a readable copy of the attribution notices contained 124 | within such NOTICE file, excluding those notices that do not 125 | pertain to any part of the Derivative Works, in at least one 126 | of the following places: within a NOTICE text file distributed 127 | as part of the Derivative Works; within the Source form or 128 | documentation, if provided along with the Derivative Works; or, 129 | within a display generated by the Derivative Works, if and 130 | wherever such third-party notices normally appear. The contents 131 | of the NOTICE file are for informational purposes only and 132 | do not modify the License. You may add Your own attribution 133 | notices within Derivative Works that You distribute, alongside 134 | or as an addendum to the NOTICE text from the Work, provided 135 | that such additional attribution notices cannot be construed 136 | as modifying the License. 137 | 138 | You may add Your own copyright statement to Your modifications and 139 | may provide additional or different license terms and conditions 140 | for use, reproduction, or distribution of Your modifications, or 141 | for any such Derivative Works as a whole, provided Your use, 142 | reproduction, and distribution of the Work otherwise complies with 143 | the conditions stated in this License. 144 | 145 | 5. Submission of Contributions. Unless You explicitly state otherwise, 146 | any Contribution intentionally submitted for inclusion in the Work 147 | by You to the Licensor shall be under the terms and conditions of 148 | this License, without any additional terms or conditions. 149 | Notwithstanding the above, nothing herein shall supersede or modify 150 | the terms of any separate license agreement you may have executed 151 | with Licensor regarding such Contributions. 152 | 153 | 6. Trademarks. This License does not grant permission to use the trade 154 | names, trademarks, service marks, or product names of the Licensor, 155 | except as required for reasonable and customary use in describing the 156 | origin of the Work and reproducing the content of the NOTICE file. 157 | 158 | 7. Disclaimer of Warranty. Unless required by applicable law or 159 | agreed to in writing, Licensor provides the Work (and each 160 | Contributor provides its Contributions) on an "AS IS" BASIS, 161 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 162 | implied, including, without limitation, any warranties or conditions 163 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 164 | PARTICULAR PURPOSE. You are solely responsible for determining the 165 | appropriateness of using or redistributing the Work and assume any 166 | risks associated with Your exercise of permissions under this License. 167 | 168 | 8. Limitation of Liability. In no event and under no legal theory, 169 | whether in tort (including negligence), contract, or otherwise, 170 | unless required by applicable law (such as deliberate and grossly 171 | negligent acts) or agreed to in writing, shall any Contributor be 172 | liable to You for damages, including any direct, indirect, special, 173 | incidental, or consequential damages of any character arising as a 174 | result of this License or out of the use or inability to use the 175 | Work (including but not limited to damages for loss of goodwill, 176 | work stoppage, computer failure or malfunction, or any and all 177 | other commercial damages or losses), even if such Contributor 178 | has been advised of the possibility of such damages. 179 | 180 | 9. Accepting Warranty or Additional Liability. While redistributing 181 | the Work or Derivative Works thereof, You may choose to offer, 182 | and charge a fee for, acceptance of support, warranty, indemnity, 183 | or other liability obligations and/or rights consistent with this 184 | License. However, in accepting such obligations, You may act only 185 | on Your own behalf and on Your sole responsibility, not on behalf 186 | of any other Contributor, and only if You agree to indemnify, 187 | defend, and hold each Contributor harmless for any liability 188 | incurred by, or claims asserted against, such Contributor by reason 189 | of your accepting any such warranty or additional liability. 190 | 191 | END OF TERMS AND CONDITIONS 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Validator Firewall 2 | 3 | Low level blocking for validator nodes. This project is a work in progress and interfaces may change. 4 | 5 | ![arch diagram](./validator_firewall.png) 6 | 7 | 8 | ## Prerequisites 9 | 10 | 1. Install nightly components: `rustup toolchain install nightly --component rust-src` 11 | 2. Install bpf-linker: `cargo install bpf-linker` 12 | 13 | ### Caveats for Ubuntu 20.04 14 | 15 | There is a bug with bpftool and the default kernel installed by the distribution. To avoid running into it, you can install a newer bpftool version that does not include the bug with: 16 | 17 | ``` 18 | sudo apt install linux-tools-5.8.0-63-generic 19 | export PATH=/usr/lib/linux-tools/5.8.0-63-generic:$PATH 20 | ``` 21 | 22 | Bond interfaces are not supported. 23 | 24 | ## General Structure 25 | 26 | The project is split into two main components: an eBPF program that runs in the context of the network driver and a 27 | userspace program that configures it. The raw bytes of the eBPF program are linked into the userspace program and 28 | the executable is loaded into the kernel on start. The userspace program is responsible for setting up the eBPF 29 | maps (shared memory between the eBPF program and the userspace program), pushing in external data, and reporting stats. 30 | 31 | By default, all non-gossip traffic is blocked on protected ports. Additional hosts can be configured to be allowed when 32 | not in gossip, or denied (even when in gossip). This is configured via a static overrides file using the following format: 33 | 34 | ```yaml 35 | allow: 36 | - name: "host1" 37 | ip: 1.2.3.4 38 | - name: "host2" 39 | ip: 4.5.6.7 40 | deny: 41 | - name: "spammer" 42 | ip: 8.9.10.11 43 | ``` 44 | 45 | ## Build eBPF 46 | 47 | ```bash 48 | cargo xtask build-ebpf 49 | ``` 50 | 51 | To perform a release build you can use the `--release` flag. 52 | You may also change the target architecture with the `--target` flag. 53 | 54 | ## Build Userspace 55 | 56 | ```bash 57 | cargo build 58 | ``` 59 | 60 | ## Run 61 | 62 | ```bash 63 | #If -p is not specified, we only act on 8009, 8010 64 | RUST_LOG=info cargo xtask run --release -- --iface --static-overrides -p 8004 -p 8005 -p 8006 65 | ``` 66 | ## Leader Schedule Aware Blocking and External IP Service 67 | 68 | By default, the firewall will attempt to determine the identity of the validator by looking at "getIdentity" from the given 69 | RPC endpoint (default: http://localhost:8899). If an external RPC endpoint is specified, the identity of the validator 70 | being protected *MUST* be provided, or the firewall will not be able to determine if the validator is the leader or not. 71 | 72 | The default source of the allowed list of submitting IPs is based on Gossip. This can also be overridden by using the 73 | `--external-ip-service-url` flag. There is an example implementation of this service in the `external_ip_service` binary. 74 | 75 | 76 | 77 | 78 | ## Production 79 | This should be run under a user with the CAP_NET_ADMIN capability. This is required to load the eBPF program and to set the XDP program on the interface. 80 | 81 | The `install_service.sh` script (uses sudo) can create a basic systemd unit file and overrides config. 82 | 83 | ## Developing on this Project 84 | 85 | In general, the eBPF component should be as lightweight and as fast as possible. It's in the datapath, so we need to do as little work there as we can. 86 | 87 | Some background reading on why XDP: [How to Drop 10M Packets per Second](https://blog.cloudflare.com/how-to-drop-10-million-packets) 88 | 89 | This project heavily uses [Aya](https://aya-rs.dev/book/). 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /install_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to check if a command exists 4 | command_exists () { 5 | command -v "$1" &> /dev/null 6 | } 7 | 8 | # Check if Cargo is installed 9 | if ! command_exists cargo; then 10 | echo "Cargo could not be found. Please install Rust and Cargo." 11 | exit 1 12 | fi 13 | 14 | # Install rust nightly and bpf linker 15 | echo "Installing Rust nightly and BPF linker..." 16 | rustup toolchain install nightly --component rust-src 17 | cargo install bpf-linker 18 | 19 | # Build the eBPF binary 20 | echo "Running 'cargo xtask build-ebpf --release'..." 21 | if ! cargo xtask build-ebpf --release; then 22 | echo "Command 'cargo xtask build-ebpf --release' failed to execute." 23 | exit 1 24 | fi 25 | 26 | # Build userspace 27 | echo "Running 'cargo build --release'..." 28 | if ! cargo build --release; then 29 | echo "Command 'cargo build --release' failed to execute." 30 | exit 1 31 | fi 32 | 33 | # Install binary 34 | BINARY_PATH="target/release/validator-firewall" 35 | if [ -f "$BINARY_PATH" ]; then 36 | echo "Copying the binary to /usr/local/sbin..." 37 | sudo cp "$BINARY_PATH" /usr/local/sbin/ 38 | else 39 | echo "Binary file 'validator-firewall' not found." 40 | exit 1 41 | fi 42 | 43 | # Create config dir 44 | echo "Creating directory /etc/validator-firewall/..." 45 | sudo mkdir -p /etc/validator-firewall/ 46 | 47 | # Create exmaple static overrides file 48 | STATIC_OVERRIDES_FILE="/etc/validator-firewall/static_overrides.yml" 49 | echo "Creating static_overrides.yml file..." 50 | sudo bash -c "cat > $STATIC_OVERRIDES_FILE" < /dev/null; then 63 | echo "Interface $interface found." 64 | break 65 | else 66 | echo "Interface $interface does not exist. Please enter a valid interface." 67 | fi 68 | done 69 | 70 | # Create a template systemd unit file 71 | SYSTEMD_FILE="/etc/systemd/system/validator-firewall.service" 72 | echo "Creating systemd unit file..." 73 | sudo bash -c "cat > $SYSTEMD_FILE" < = HashMap::::with_max_entries(DENY_LIST_SIZE, 0); 26 | #[map(name = "hvf_always_allow")] 27 | static FULL_SCHEDULE_ALLOW_LIST: HashMap = HashMap::::with_max_entries(8192, 0); 28 | #[map(name = "hvf_stats")] 29 | static STATS: PerCpuHashMap = PerCpuHashMap::::with_max_entries(16384, 0); 30 | 31 | #[map(name = "hvf_protected_ports")] 32 | static PROTECTED_PORTS: HashMap = HashMap::::with_max_entries(1024, 0); 33 | 34 | #[map(name = "hvf_cnc")] 35 | static CNC: Array = Array::::with_max_entries(1, 0); 36 | 37 | #[xdp] 38 | pub fn validator_firewall(ctx: XdpContext) -> u32 { 39 | let cnc = match CNC.get(0) { 40 | Some(cnc) => cnc, 41 | None => { 42 | warn!(&ctx, "No CNC data found, using defaults"); 43 | &RuntimeControls{ global_enabled: true, close_to_leader: true } 44 | } 45 | }; 46 | 47 | if cnc.global_enabled { 48 | match try_process_packet(&ctx, cnc.close_to_leader) { 49 | Ok(ret) => ret, 50 | Err(_) => { 51 | error!(&ctx, "Error processing packet!"); 52 | xdp_action::XDP_PASS 53 | } 54 | } 55 | } else { 56 | xdp_action::XDP_PASS 57 | } 58 | } 59 | 60 | #[panic_handler] 61 | fn panic(_info: &core::panic::PanicInfo) -> ! { 62 | unsafe { core::hint::unreachable_unchecked() } 63 | } 64 | 65 | //Hide some unsafe blocks 66 | #[inline(always)] 67 | fn is_allowed(address: u32, close_to_leader: bool) -> bool { 68 | return if close_to_leader { 69 | unsafe { LEADER_SLOT_DENY_LIST.get(&address).is_none() } 70 | } else { 71 | unsafe { FULL_SCHEDULE_ALLOW_LIST.get(&address).is_some() } 72 | } 73 | 74 | } 75 | 76 | #[inline(always)] 77 | fn is_protected_port(dest_port: u16) -> bool { 78 | unsafe { PROTECTED_PORTS.get(&dest_port).is_some() } 79 | } 80 | 81 | #[inline(always)] 82 | fn increment_counter(ctx: &XdpContext, address: u32, stat_type: StatType) { 83 | unsafe { 84 | if let None = STATS.get_ptr(&address) { 85 | let _ = STATS.insert(&address, &ConnectionStats::default(), 0); 86 | } 87 | match STATS.get_ptr_mut(&address) { 88 | Some(stats) => { 89 | match stat_type { 90 | StatType::All => { 91 | (*stats).pkt_count += 1; 92 | }, 93 | StatType::Blocked => { 94 | (*stats).blocked_pkt_count += 1; 95 | }, 96 | StatType::FarFromLeader => { 97 | (*stats).far_from_leader_pkt_count += 1; 98 | }, 99 | StatType::ZeroRtt => { 100 | (*stats).zero_rtt_pkt_count += 1; 101 | } 102 | } 103 | }, 104 | None => { 105 | error!(ctx, "No entry for {} in stats map!", address); 106 | } 107 | } 108 | } 109 | } 110 | 111 | #[inline(always)] 112 | fn ptr_at(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> { 113 | let start = ctx.data(); 114 | let end = ctx.data_end(); 115 | let len = mem::size_of::(); 116 | 117 | if start + offset + len > end { 118 | return Err(()); 119 | } 120 | 121 | Ok((start + offset) as *const T) 122 | } 123 | 124 | #[inline(always)] 125 | fn try_process_packet(ctx: &XdpContext, close_to_leader: bool) -> Result { 126 | let eth_header: *const EthHdr = ptr_at(&ctx, 0)?; 127 | if let EtherType::Ipv6 = unsafe { (*eth_header).ether_type } { 128 | return Ok(xdp_action::XDP_PASS); 129 | } 130 | 131 | let ipv4_header: *const Ipv4Hdr = ptr_at(&ctx, EthHdr::LEN)?; 132 | return if let IpProto::Udp = unsafe { (*ipv4_header).proto } { 133 | let source_addr = u32::from_be(unsafe { (*ipv4_header).src_addr }); 134 | let udp_header: *const UdpHdr = ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN)?; 135 | let dest_port = u16::from_be(unsafe { (*udp_header).dest }); 136 | if !is_protected_port(dest_port) { 137 | return Ok(xdp_action::XDP_PASS); 138 | } 139 | 140 | //Traffic above here is other OS traffic, not counted in our stats 141 | increment_counter(ctx, source_addr, StatType::All); 142 | if !close_to_leader { 143 | increment_counter(ctx, source_addr, StatType::FarFromLeader); 144 | } 145 | if is_quic_zero_rtt(ctx, source_addr) { 146 | increment_counter(ctx, source_addr, StatType::ZeroRtt); 147 | } 148 | let action = if is_allowed(source_addr, close_to_leader) { 149 | debug!( 150 | ctx, 151 | "ALLOW SRC IP: {:i}, DEST PORT: {}", 152 | source_addr, 153 | dest_port 154 | ); 155 | xdp_action::XDP_PASS 156 | } else { 157 | debug!( 158 | ctx, 159 | "DROP SRC IP: {:i}, DEST PORT: {}", 160 | source_addr, 161 | dest_port 162 | ); 163 | increment_counter(ctx, source_addr, StatType::Blocked); 164 | xdp_action::XDP_DROP 165 | }; 166 | 167 | Ok(action) 168 | } else { 169 | Ok(xdp_action::XDP_PASS) 170 | }; 171 | } 172 | 173 | //Placeholder 174 | #[inline(always)] 175 | fn is_quic_zero_rtt(ctx: &XdpContext, source_addr: u32) -> bool { 176 | false 177 | } -------------------------------------------------------------------------------- /validator-firewall/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "validator-firewall" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | anyhow = "1" 9 | async-trait = "0.1.80" 10 | axum = "0.7.5" 11 | tower-http = { version = "0.5.2", features = ["validate-request", "auth"] } 12 | aya = "0.12" 13 | aya-log = "0.2" 14 | cadence = "1.4.0" 15 | cidr = { version = "0.2", features = ["serde"] } 16 | clap = { version = "4.1", features = ["derive"] } 17 | env_logger = "0.11" 18 | libc = "0.2" 19 | log = "0.4" 20 | rangemap = "1.5.1" 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_yaml = { version = "0.9" } 23 | solana-rpc-client = "1.18.15" 24 | solana-sdk = "1.18.15" 25 | tokio = { version = "1.25", features = ["macros", "rt", "rt-multi-thread", "net", "signal"] } 26 | tracing = "0.1.40" 27 | tracing-subscriber = { version = "0.3.18", features = [ 28 | "json", 29 | "env-filter", 30 | "ansi", 31 | ] } 32 | validator-firewall-common = { path = "../validator-firewall-common", features = ["user"] } 33 | serde_json = "1.0.117" 34 | hyper = "0.14.28" 35 | reqwest = "0.11.27" 36 | duckdb = { version="1.0.0", features=["bundled"]} 37 | 38 | [[bin]] 39 | name = "validator-firewall" 40 | path = "src/main.rs" 41 | 42 | [[bin]] 43 | name = "standalone-ip-service" 44 | path = "src/ip_service_main.rs" 45 | -------------------------------------------------------------------------------- /validator-firewall/src/config.rs: -------------------------------------------------------------------------------- 1 | use cidr::Ipv4Cidr; 2 | use serde::Deserialize; 3 | use std::path::PathBuf; 4 | 5 | #[allow(dead_code)] //Used in Debug 6 | #[derive(Deserialize, Debug)] 7 | pub struct NameAddressPair { 8 | pub name: String, 9 | pub ip: Ipv4Cidr, 10 | } 11 | 12 | #[derive(Deserialize, Debug)] 13 | pub struct StaticOverrides { 14 | pub allow: Vec, 15 | pub deny: Vec, 16 | } 17 | 18 | pub fn load_static_overrides(path: PathBuf) -> Result { 19 | let file = std::fs::File::open(path)?; 20 | let reader = std::io::BufReader::new(file); 21 | let overrides = serde_yaml::from_reader(reader)?; 22 | Ok(overrides) 23 | } 24 | -------------------------------------------------------------------------------- /validator-firewall/src/ip_service.rs: -------------------------------------------------------------------------------- 1 | use aya::maps::{HashMap, Map}; 2 | use cidr::Ipv4Cidr; 3 | use duckdb::params; 4 | use log::{debug, error, info}; 5 | use rangemap::RangeInclusiveSet; 6 | use solana_rpc_client::nonblocking::rpc_client::RpcClient; 7 | use std::collections::HashSet; 8 | use std::net::Ipv4Addr; 9 | use std::ops::RangeInclusive; 10 | use std::sync::atomic::{AtomicBool, Ordering}; 11 | use std::sync::Arc; 12 | use std::time::Duration; 13 | use tokio::sync::Mutex; 14 | use tokio::time::sleep; 15 | 16 | const DENY_LIST_SIZE: u32 = 524288; 17 | const DYNAMIC_LIST_BUFFER: u32 = 1024; 18 | 19 | pub struct HttpDenyListClient { 20 | url: String, 21 | } 22 | 23 | impl HttpDenyListClient { 24 | pub fn new(url: String) -> Self { 25 | Self { url } 26 | } 27 | } 28 | 29 | impl DenyListClient for HttpDenyListClient { 30 | async fn get_deny_list(&self) -> Result, ()> { 31 | let client = reqwest::Client::new(); 32 | match client.get(&self.url).send().await { 33 | Ok(resp) => { 34 | if resp.status().is_success() { 35 | let allow_list: Vec = resp.json().await.unwrap(); 36 | info!( 37 | "Retrieved {} IPs from external ip service", 38 | allow_list.len() 39 | ); 40 | Ok(allow_list) 41 | } else { 42 | error!("Failed to decode deny list from external ip service."); 43 | Err(()) 44 | } 45 | } 46 | Err(_) => Err(()), 47 | } 48 | } 49 | } 50 | 51 | pub struct DuckDbDenyListClient { 52 | conn: Arc>, 53 | query: String, 54 | } 55 | 56 | impl DuckDbDenyListClient { 57 | pub fn new(query: String) -> Self { 58 | Self { 59 | conn: Arc::new(Mutex::new(duckdb::Connection::open_in_memory().unwrap())), 60 | query, 61 | } 62 | } 63 | 64 | async fn get_deny_list(&self) -> Result, ()> { 65 | info!("Executing query: {}", self.query); 66 | 67 | let conn = self.conn.lock().await; 68 | let mut stmt = conn.prepare(&self.query).unwrap(); 69 | let mut rows = stmt.query(params![]).unwrap(); 70 | let mut deny_list = Vec::new(); 71 | let mut count = 0; 72 | while let Some(row) = rows.next().unwrap() { 73 | let ip: u32 = row.get(0).unwrap(); 74 | let converted: Ipv4Addr = ip.into(); 75 | 76 | let cidr = Ipv4Cidr::new(converted, 32).unwrap(); 77 | deny_list.push(cidr); 78 | count += 1; 79 | } 80 | 81 | info!("Retrieved {} IPs from query", count); 82 | Ok(deny_list) 83 | } 84 | } 85 | 86 | impl DenyListClient for DuckDbDenyListClient { 87 | async fn get_deny_list(&self) -> Result, ()> { 88 | self.get_deny_list().await 89 | } 90 | } 91 | 92 | pub struct NoOpDenyListClient; 93 | 94 | impl DenyListClient for NoOpDenyListClient { 95 | async fn get_deny_list(&self) -> Result, ()> { 96 | Ok(Vec::new()) 97 | } 98 | } 99 | 100 | pub trait DenyListClient { 101 | async fn get_deny_list(&self) -> Result, ()>; 102 | } 103 | 104 | pub struct DenyListService { 105 | deny_list_client: T, 106 | } 107 | 108 | impl DenyListService { 109 | pub fn new(allow_list_client: T) -> Self { 110 | Self { 111 | deny_list_client: allow_list_client, 112 | } 113 | } 114 | 115 | pub async fn get_deny_list(&self) -> Result, ()> { 116 | self.deny_list_client.get_deny_list().await 117 | } 118 | } 119 | 120 | pub struct DenyListStateUpdater { 121 | exit_flag: Arc, 122 | allow_service: Arc>, 123 | allow_ranges: RangeInclusiveSet, 124 | deny_ranges: RangeInclusiveSet, 125 | deny_cidrs: Arc>, 126 | } 127 | 128 | pub fn to_range(ip: Ipv4Cidr) -> RangeInclusive { 129 | let start_addr: u32 = ip.first_address().try_into().unwrap(); 130 | let end_addr: u32 = ip.last_address().try_into().unwrap(); 131 | start_addr..=end_addr 132 | } 133 | 134 | impl DenyListStateUpdater { 135 | pub fn new( 136 | exit_flag: Arc, 137 | allow_service: Arc>, 138 | static_overrides: Arc<(HashSet, HashSet)>, 139 | ) -> Self { 140 | Self { 141 | exit_flag, 142 | allow_service, 143 | allow_ranges: { 144 | RangeInclusiveSet::from_iter( 145 | static_overrides.clone().0.iter().map(|ip| to_range(*ip)), 146 | ) 147 | }, 148 | deny_ranges: { 149 | RangeInclusiveSet::from_iter(static_overrides.1.iter().map(|ip| to_range(*ip))) 150 | }, 151 | deny_cidrs: Arc::new(static_overrides.1.clone()), 152 | } 153 | } 154 | 155 | pub fn is_denied(&self, addr: &u32) -> bool { 156 | self.deny_ranges.contains(addr) 157 | } 158 | 159 | pub fn is_allowed(&self, addr: &u32) -> bool { 160 | self.allow_ranges.contains(addr) 161 | } 162 | 163 | pub async fn run(&self, allow_list: Map) { 164 | let mut deny_list: HashMap<_, u32, u8> = HashMap::try_from(allow_list).unwrap(); 165 | let mut dynamic_deny_set = HashSet::new(); 166 | { 167 | let ips: Vec = self 168 | .deny_cidrs 169 | .iter() 170 | .flat_map(|ip| ip.into_iter().addresses()) 171 | .collect(); 172 | if ips.len() > (DENY_LIST_SIZE - DYNAMIC_LIST_BUFFER) as usize { 173 | error!("Deny list is too large to fit in map, static overrides will be truncated."); 174 | } 175 | 176 | for ip in ips 177 | .iter() 178 | .take((DENY_LIST_SIZE - DYNAMIC_LIST_BUFFER) as usize) 179 | { 180 | let ip_numeric: u32 = u32::from(*ip); 181 | if !self.is_allowed(&ip_numeric) { 182 | deny_list.insert(ip_numeric, 0, 0).unwrap(); 183 | } 184 | } 185 | } 186 | 187 | while !self.exit_flag.load(Ordering::Relaxed) { 188 | if let Ok(nodes) = self.allow_service.get_deny_list().await { 189 | dynamic_deny_set.clear(); 190 | for ip4addr in nodes.iter().flat_map(|cidr| cidr.into_iter().addresses()) { 191 | let ip_numeric: u32 = ip4addr.try_into().expect("Received invalid ip address"); 192 | if !self.is_allowed(&ip_numeric) { 193 | dynamic_deny_set.insert(ip_numeric); 194 | } 195 | } 196 | 197 | dynamic_deny_set.retain(|x| !self.is_allowed(x)); 198 | let to_remove = { 199 | deny_list 200 | .iter() 201 | .filter_map(|r| r.ok()) 202 | .filter(|(x, _)| !dynamic_deny_set.contains(x)) 203 | .filter(|(x, _)| !self.is_denied(x)) 204 | .map(|x| x.0) 205 | .collect::>() 206 | }; 207 | 208 | debug!("Pruning {} ips from deny list", to_remove.len()); 209 | for ip in to_remove { 210 | deny_list.remove(&ip).unwrap(); 211 | } 212 | 213 | { 214 | for ip in dynamic_deny_set.iter() { 215 | if !deny_list.get(ip, 0).is_ok() { 216 | deny_list.insert(ip, 0, 0).unwrap(); 217 | } 218 | } 219 | } 220 | } else { 221 | error!("Error fetching deny list from RPC"); 222 | } 223 | 224 | sleep(Duration::from_secs(10)).await; 225 | } 226 | } 227 | } 228 | 229 | #[cfg(test)] 230 | mod tests { 231 | use super::*; 232 | use rangemap::RangeInclusiveSet; 233 | use std::str::FromStr; 234 | 235 | #[test] 236 | fn test_inet_ranges() { 237 | let mut deny_range = RangeInclusiveSet::new(); 238 | let single_host = Ipv4Cidr::from_str("1.1.1.1/32").unwrap(); 239 | 240 | let start_addr: u32 = single_host.first().address().try_into().unwrap(); 241 | let end_addr: u32 = single_host.last().address().try_into().unwrap(); 242 | deny_range.insert(start_addr..=end_addr); 243 | 244 | assert!(deny_range.contains(&start_addr)); 245 | } 246 | 247 | #[test] 248 | fn test_range_coalescing() { 249 | let mut deny_range = RangeInclusiveSet::new(); 250 | let first_host = Ipv4Cidr::from_str("1.1.1.1/32").unwrap(); 251 | let adjacent_host = Ipv4Cidr::from_str("1.1.1.2/32").unwrap(); 252 | 253 | let bare_host = Ipv4Cidr::from_str("192.168.1.1").unwrap(); 254 | assert!(bare_host.is_host_address()); 255 | 256 | let start_addr: u32 = first_host.first().address().try_into().unwrap(); 257 | let end_addr: u32 = first_host.last().address().try_into().unwrap(); 258 | deny_range.insert(start_addr..=end_addr); 259 | 260 | let start_addr: u32 = adjacent_host.first().address().try_into().unwrap(); 261 | let end_addr: u32 = adjacent_host.last().address().try_into().unwrap(); 262 | deny_range.insert(start_addr..=end_addr); 263 | assert_eq!(deny_range.len(), 1); 264 | 265 | println!("{:?}", deny_range); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /validator-firewall/src/ip_service_http.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::State; 2 | use axum::http::StatusCode; 3 | use axum::response::IntoResponse; 4 | use axum::routing::{get, post}; 5 | use axum::{Json, Router}; 6 | use cidr::Ipv4Cidr; 7 | use std::collections::HashSet; 8 | use std::sync::Arc; 9 | use tokio::sync::RwLock; 10 | use tower_http::auth::AddAuthorizationLayer; 11 | use tracing::{debug, info, warn}; 12 | 13 | pub struct IPState { 14 | pub gossip_nodes: Arc>>, 15 | pub http_nodes: Arc>>, 16 | pub blocked_nodes: Arc>>, 17 | } 18 | 19 | impl IPState { 20 | pub fn new() -> Self { 21 | IPState { 22 | gossip_nodes: Arc::new(RwLock::new(HashSet::new())), 23 | http_nodes: Arc::new(RwLock::new(HashSet::new())), 24 | blocked_nodes: Arc::new(RwLock::new(HashSet::new())), 25 | } 26 | } 27 | pub async fn set_gossip_nodes(&self, nodes: HashSet) { 28 | *self.gossip_nodes.write().await = nodes; 29 | } 30 | 31 | pub async fn add_http_node(&self, node: Ipv4Cidr) { 32 | self.http_nodes.write().await.insert(node); 33 | } 34 | 35 | pub async fn add_blocked_node(&self, node: Ipv4Cidr) { 36 | self.blocked_nodes.write().await.insert(node); 37 | } 38 | 39 | pub async fn remove_blocked_node(&self, node: Ipv4Cidr) { 40 | self.blocked_nodes.write().await.remove(&node); 41 | } 42 | 43 | pub async fn remove_http_node(&self, node: Ipv4Cidr) { 44 | self.http_nodes.write().await.remove(&node); 45 | } 46 | 47 | pub async fn get_combined_nodes(&self) -> HashSet { 48 | let mut combined_nodes = HashSet::new(); 49 | combined_nodes.extend(self.blocked_nodes.read().await.iter()); 50 | let allow_listed = self.http_nodes.read().await; 51 | combined_nodes.retain(|node| !allow_listed.contains(node)); 52 | 53 | combined_nodes 54 | } 55 | } 56 | 57 | pub fn create_router(state: Arc, token: Option) -> Router { 58 | async fn get_deny_list(state: State>) -> impl IntoResponse { 59 | let nodes = state.get_combined_nodes().await; 60 | let nodes: Vec = nodes.iter().map(|node| node.to_string()).collect(); 61 | let body = serde_json::to_string(&nodes).unwrap(); 62 | (StatusCode::OK, body) 63 | } 64 | 65 | async fn get_http_nodes(state: State>) -> impl IntoResponse { 66 | let nodes = state.http_nodes.read().await; 67 | let nodes: Vec = nodes.iter().map(|node| node.to_string()).collect(); 68 | let body = serde_json::to_string(&nodes).unwrap(); 69 | (StatusCode::OK, body) 70 | } 71 | 72 | async fn add_http_node( 73 | state: State>, 74 | Json(payload): Json, 75 | ) -> impl IntoResponse { 76 | debug!("add_http_node: {:?}", payload); 77 | state.add_http_node(payload).await; 78 | (StatusCode::CREATED, payload.to_string()) 79 | } 80 | 81 | async fn get_blocked_nodes(state: State>) -> impl IntoResponse { 82 | let nodes = state.blocked_nodes.read().await; 83 | let nodes: Vec = nodes.iter().map(|node| node.to_string()).collect(); 84 | let body = serde_json::to_string(&nodes).unwrap(); 85 | (StatusCode::OK, body) 86 | } 87 | 88 | async fn add_blocked_node( 89 | state: State>, 90 | Json(payload): Json, 91 | ) -> impl IntoResponse { 92 | debug!("add_blocked_node: {:?}", payload); 93 | state.add_blocked_node(payload).await; 94 | (StatusCode::CREATED, payload.to_string()) 95 | } 96 | 97 | async fn remove_blocked_node( 98 | state: State>, 99 | Json(payload): Json, 100 | ) -> impl IntoResponse { 101 | debug!("remove_blocked_node: {:?}", payload); 102 | state.remove_blocked_node(payload).await; 103 | (StatusCode::OK, payload.to_string()) 104 | } 105 | 106 | async fn remove_http_node( 107 | state: State>, 108 | Json(payload): Json, 109 | ) -> impl IntoResponse { 110 | debug!("remove_http_node: {:?}", payload); 111 | state.remove_http_node(payload).await; 112 | (StatusCode::OK, payload.to_string()) 113 | } 114 | 115 | let app = Router::new() 116 | .route("/", get(get_deny_list)) 117 | .route("/nodes", get(get_deny_list)) 118 | .with_state(state.clone()); 119 | return if let Some(token) = token { 120 | info!("Adding authentication layer with token: {}", token); 121 | Router::new() 122 | .route( 123 | "/allowed", 124 | post(add_http_node) 125 | .delete(remove_http_node) 126 | .get(get_http_nodes), 127 | ) 128 | .route( 129 | "/blocked", 130 | post(add_blocked_node) 131 | .delete(remove_blocked_node) 132 | .get(get_blocked_nodes), 133 | ) 134 | .route_layer(AddAuthorizationLayer::bearer(&token)) 135 | .with_state(state.clone()) 136 | .merge(app) 137 | } else { 138 | warn!("No authentication configured for write layer."); 139 | Router::new() 140 | .route( 141 | "/allowed", 142 | post(add_http_node) 143 | .delete(remove_http_node) 144 | .get(get_http_nodes), 145 | ) 146 | .route( 147 | "/blocked", 148 | post(add_blocked_node) 149 | .delete(remove_blocked_node) 150 | .get(get_blocked_nodes), 151 | ) 152 | .with_state(state.clone()) 153 | .merge(app) 154 | }; 155 | } 156 | -------------------------------------------------------------------------------- /validator-firewall/src/ip_service_main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod ip_service; 3 | mod ip_service_http; 4 | 5 | use crate::config::{load_static_overrides, NameAddressPair}; 6 | use crate::ip_service::{DenyListService, DenyListStateUpdater}; 7 | use crate::ip_service_http::{create_router, IPState}; 8 | use axum::{ 9 | http::StatusCode, 10 | routing::{get, post}, 11 | Json, Router, 12 | }; 13 | use cidr::Ipv4Cidr; 14 | use clap::Parser; 15 | use log::{error, info}; 16 | use serde::{Deserialize, Serialize}; 17 | use solana_rpc_client::nonblocking::rpc_client::RpcClient; 18 | use solana_sdk::commitment_config::CommitmentConfig; 19 | use std::collections::HashSet; 20 | use std::path::PathBuf; 21 | use std::sync::atomic::AtomicBool; 22 | use std::sync::Arc; 23 | 24 | #[derive(Debug, Parser)] 25 | struct IpServiceConfig { 26 | #[clap(short, long, default_value = "https://api.mainnet-beta.solana.com")] 27 | rpc_endpoint: String, 28 | #[clap(short, long)] 29 | static_overrides: Option, 30 | #[clap(short, long)] 31 | bearer_token: Option, 32 | #[clap(short, long, default_value = "11525")] 33 | port: u16, 34 | } 35 | 36 | #[tokio::main] 37 | async fn main() -> Result<(), Box> { 38 | let exit_flag = Arc::new(AtomicBool::new(false)); 39 | let config = IpServiceConfig::parse(); 40 | tracing_subscriber::fmt().json().init(); 41 | 42 | let rpc_endpoint = config.rpc_endpoint.clone(); 43 | 44 | let static_overrides = { 45 | let mut local_allow = HashSet::new(); 46 | let mut local_deny = HashSet::new(); 47 | 48 | // Load static overrides if provided 49 | if let Some(path) = config.static_overrides { 50 | let overrides = load_static_overrides(path).unwrap(); 51 | let denied: HashSet = overrides.deny.iter().map(|x| x.ip.clone()).collect(); 52 | let intersection: Vec<&NameAddressPair> = overrides 53 | .allow 54 | .iter() 55 | .filter(|x| denied.contains(&x.ip)) 56 | .collect(); 57 | 58 | if !intersection.is_empty() { 59 | error!( 60 | "Static overrides contain overlapping entries for deny and allow: {:?}", 61 | intersection 62 | ); 63 | std::process::exit(1); 64 | } 65 | for node in overrides.allow.iter() { 66 | local_allow.insert(node.ip); 67 | } 68 | for node in overrides.deny.iter() { 69 | local_deny.insert(node.ip); 70 | } 71 | 72 | info!("Loaded static overrides: {:?}", overrides); 73 | }; 74 | Arc::new((local_allow, local_deny)) 75 | }; 76 | 77 | let app_state = Arc::new(IPState::new()); 78 | 79 | for node in static_overrides.0.iter() { 80 | app_state.add_http_node(node.clone()).await; 81 | } 82 | 83 | for node in static_overrides.1.iter() { 84 | app_state.add_blocked_node(node.clone()).await; 85 | } 86 | 87 | info!("Starting IP service on port {}", config.port.clone()); 88 | let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", config.port)) 89 | .await 90 | .unwrap(); 91 | let app = create_router(app_state.clone(), config.bearer_token); 92 | 93 | Ok(axum::serve(listener, app.into_make_service()) 94 | .await 95 | .unwrap()) 96 | } 97 | -------------------------------------------------------------------------------- /validator-firewall/src/leader_tracker.rs: -------------------------------------------------------------------------------- 1 | use aya::maps::{Array, Map}; 2 | use log::{error, info, warn}; 3 | use rangemap::RangeInclusiveSet; 4 | use solana_rpc_client::nonblocking::rpc_client::RpcClient; 5 | use solana_sdk::commitment_config::{CommitmentConfig, CommitmentLevel}; 6 | use solana_sdk::epoch_info::EpochInfo; 7 | use std::ops::Range; 8 | use std::sync::atomic::{AtomicBool, Ordering}; 9 | use std::sync::Arc; 10 | use tokio::sync::RwLock; 11 | use validator_firewall_common::RuntimeControls; 12 | 13 | #[derive(Copy, Clone, Debug)] 14 | enum LeaderDistance { 15 | Close { begin: u64, current: u64, end: u64 }, 16 | Far { current: u64 }, 17 | } 18 | 19 | #[derive(Clone, Debug)] 20 | enum LeaderTrackerState { 21 | NeedIdentity, 22 | NeedLeaderSchedule, 23 | Running { slots: RangeInclusiveSet }, 24 | } 25 | 26 | pub struct RPCLeaderTracker { 27 | exit_flag: Arc, 28 | rpc_client: RpcClient, 29 | slot_buffer: u64, 30 | id_override: Option, 31 | leader_status: Arc>>, 32 | } 33 | 34 | impl RPCLeaderTracker { 35 | pub fn new( 36 | exit_flag: Arc, 37 | rpc_client: RpcClient, 38 | slot_buffer: u64, 39 | id_override: Option, 40 | ) -> Self { 41 | RPCLeaderTracker { 42 | exit_flag, 43 | rpc_client, 44 | slot_buffer, 45 | id_override, 46 | leader_status: Arc::new(RwLock::new(None)), 47 | } 48 | } 49 | 50 | async fn close_to_leader(&self) -> Option { 51 | self.leader_status.read().await.clone() 52 | } 53 | pub async fn run(&self) { 54 | let mut current_epoch = 0u64; 55 | let mut max_slot = 0u64; 56 | 57 | let mut tracker_state = LeaderTrackerState::NeedIdentity; 58 | while !self.exit_flag.load(Ordering::Relaxed) { 59 | match tracker_state { 60 | LeaderTrackerState::NeedIdentity => { 61 | if let Ok(_) = self.get_identity().await { 62 | tracker_state = LeaderTrackerState::NeedLeaderSchedule; 63 | } else { 64 | warn!("Failed to get identity. Retrying in 5 seconds."); 65 | tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; 66 | continue; 67 | } 68 | } 69 | LeaderTrackerState::NeedLeaderSchedule => { 70 | let new_schedule = match self.refresh_leader_schedule().await { 71 | Ok(sched) => sched, 72 | Err(_) => { 73 | warn!("Failed to get leader schedule. Retrying in 5 seconds."); 74 | tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; 75 | continue; 76 | } 77 | }; 78 | 79 | match self.rpc_client.get_epoch_info().await { 80 | Ok(epoch_info) => { 81 | current_epoch = epoch_info.epoch; 82 | max_slot = epoch_info.absolute_slot - epoch_info.slot_index 83 | + epoch_info.slots_in_epoch; 84 | tracker_state = LeaderTrackerState::Running { 85 | slots: new_schedule, 86 | }; 87 | info!("New leader schedule loaded. Epoch {current_epoch} max slot {max_slot}"); 88 | } 89 | Err(_) => { 90 | error!("Failed to get epoch boundaries, leader schedule is incomplete"); 91 | tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; 92 | continue; 93 | } 94 | } 95 | } 96 | 97 | LeaderTrackerState::Running { ref slots } => { 98 | match self 99 | .rpc_client 100 | .get_epoch_info_with_commitment(CommitmentConfig { 101 | commitment: CommitmentLevel::Processed, 102 | }) 103 | .await 104 | { 105 | Ok(epoch_info) => { 106 | if epoch_info.epoch != current_epoch 107 | || epoch_info.absolute_slot > max_slot 108 | { 109 | tracker_state = LeaderTrackerState::NeedLeaderSchedule; 110 | info!("Epoch changed. Getting new leader schedule."); 111 | { 112 | let mut guard = self.leader_status.write().await; 113 | *guard = None; 114 | } 115 | continue; 116 | } 117 | 118 | let idx = epoch_info.slot_index; 119 | let mut guard = self.leader_status.write().await; 120 | if let Some(leader_range) = slots.get(&idx) { 121 | *guard = Some(LeaderDistance::Close { 122 | begin: *leader_range.start(), 123 | current: idx, 124 | end: *leader_range.end(), 125 | }); 126 | } else { 127 | *guard = Some(LeaderDistance::Far { current: idx }); 128 | } 129 | } 130 | Err(_) => { 131 | error!("Failed to get epoch info."); 132 | { 133 | let mut guard = self.leader_status.write().await; 134 | *guard = None; 135 | } 136 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; 137 | continue; 138 | } 139 | } 140 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; 141 | } 142 | } 143 | } 144 | } 145 | 146 | async fn get_identity(&self) -> Result { 147 | match self.id_override.clone() { 148 | Some(id) => Ok(id), 149 | None => match self.rpc_client.get_identity().await { 150 | Ok(id) => Ok(id.to_string()), 151 | Err(e) => { 152 | error!("Failed to get identity: {e}"); 153 | Err(()) 154 | } 155 | }, 156 | } 157 | } 158 | 159 | async fn refresh_leader_schedule(&self) -> Result, ()> { 160 | let my_id = self.get_identity().await?; 161 | return match self 162 | .rpc_client 163 | .get_leader_schedule_with_commitment( 164 | None, 165 | CommitmentConfig { 166 | commitment: CommitmentLevel::Processed, 167 | }, 168 | ) 169 | .await 170 | { 171 | Err(e) => { 172 | error!("Failed to get leader schedule: {e}"); 173 | Err(()) 174 | } 175 | Ok(sched) => { 176 | if sched.is_none() { 177 | error!("Failed to get leader schedule."); 178 | return Err(()); 179 | } 180 | if let Some(my_slots) = sched.unwrap().get(&my_id) { 181 | let mut leader_ranges: RangeInclusiveSet = RangeInclusiveSet::new(); 182 | for slot in my_slots { 183 | let end: u64 = *slot as u64; 184 | 185 | let range = end.saturating_sub(self.slot_buffer)..=end; 186 | leader_ranges.insert(range); 187 | } 188 | leader_ranges.insert(0..=10); 189 | // let rngs: Vec> =leader_ranges.iter().collect(); 190 | info!("Leader ranges: {leader_ranges:?}"); 191 | 192 | Ok(leader_ranges) 193 | } else { 194 | error!("No slots found for: {my_id}"); 195 | Err(()) 196 | } 197 | } 198 | }; 199 | } 200 | } 201 | 202 | pub struct CommandControlService { 203 | exit_flag: Arc, 204 | tracker: Arc, 205 | cnc_array: Map, 206 | } 207 | 208 | impl CommandControlService { 209 | pub fn new(exit_flag: Arc, tracker: Arc, cnc_array: Map) -> Self { 210 | CommandControlService { 211 | exit_flag, 212 | tracker, 213 | cnc_array, 214 | } 215 | } 216 | 217 | pub async fn run(&mut self) { 218 | let mut cnc_array: Array<_, RuntimeControls> = 219 | Array::try_from(&mut self.cnc_array).unwrap(); 220 | let mut close_to_leader_enabled = false; 221 | cnc_array 222 | .set( 223 | 0, 224 | RuntimeControls { 225 | global_enabled: true, 226 | close_to_leader: close_to_leader_enabled, 227 | }, 228 | 0, 229 | ) 230 | .unwrap(); 231 | 232 | while !self.exit_flag.load(Ordering::Relaxed) { 233 | let result = self.tracker.close_to_leader().await; 234 | match (result, close_to_leader_enabled) { 235 | ( 236 | Some(LeaderDistance::Close { 237 | begin, 238 | current, 239 | end, 240 | }), 241 | false, 242 | ) => { 243 | close_to_leader_enabled = true; 244 | info!( 245 | "Entering close to leader mode: Begin {begin} Current {current} End {end}" 246 | ); 247 | let RuntimeControls { 248 | global_enabled, 249 | close_to_leader: _, 250 | } = cnc_array.get(&0, 0).unwrap(); 251 | cnc_array 252 | .set( 253 | 0, 254 | RuntimeControls { 255 | global_enabled, 256 | close_to_leader: true, 257 | }, 258 | 0, 259 | ) 260 | .unwrap(); 261 | } 262 | (Some(LeaderDistance::Far { current }), true) => { 263 | close_to_leader_enabled = false; 264 | info!("Exiting close to leader mode: Current {current}"); 265 | let RuntimeControls { 266 | global_enabled, 267 | close_to_leader: _, 268 | } = cnc_array.get(&0, 0).unwrap(); 269 | cnc_array 270 | .set( 271 | 0, 272 | RuntimeControls { 273 | global_enabled, 274 | close_to_leader: false, 275 | }, 276 | 0, 277 | ) 278 | .unwrap(); 279 | } 280 | (None, false) => { 281 | close_to_leader_enabled = true; 282 | warn!("Entering close to leader mode due to missing leader status"); 283 | let RuntimeControls { 284 | global_enabled, 285 | close_to_leader: _, 286 | } = cnc_array.get(&0, 0).unwrap(); 287 | cnc_array 288 | .set( 289 | 0, 290 | RuntimeControls { 291 | global_enabled, 292 | close_to_leader: true, 293 | }, 294 | 0, 295 | ) 296 | .unwrap(); 297 | } 298 | _ => {} 299 | } 300 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; 301 | } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /validator-firewall/src/main.rs: -------------------------------------------------------------------------------- 1 | mod ip_service; 2 | mod stats_service; 3 | 4 | mod config; 5 | mod leader_tracker; 6 | 7 | use crate::config::{load_static_overrides, NameAddressPair}; 8 | use crate::ip_service::{ 9 | DenyListService, DenyListStateUpdater, DuckDbDenyListClient, HttpDenyListClient, 10 | NoOpDenyListClient, 11 | }; 12 | use crate::leader_tracker::{CommandControlService, RPCLeaderTracker}; 13 | use anyhow::Context; 14 | use aya::{ 15 | include_bytes_aligned, 16 | maps::HashMap, 17 | programs::{Xdp, XdpFlags}, 18 | Bpf, 19 | }; 20 | use aya_log::BpfLogger; 21 | use cidr::Ipv4Cidr; 22 | use clap::Parser; 23 | use log::{debug, error, info, warn}; 24 | use serde::Deserialize; 25 | use solana_rpc_client::nonblocking::rpc_client::RpcClient; 26 | use std::collections::HashSet; 27 | use std::path::PathBuf; 28 | use std::sync::atomic::AtomicBool; 29 | use std::sync::Arc; 30 | use tokio::signal; 31 | 32 | #[derive(Debug, Parser)] 33 | struct HVFConfig { 34 | #[clap(short, long, default_value = "eth0")] 35 | iface: String, 36 | #[clap(short, long)] 37 | static_overrides: Option, 38 | #[clap(short, long, default_value = "http://localhost:8899")] 39 | rpc_endpoint: String, 40 | #[arg(short, long, value_name = "PORT", value_parser = clap::value_parser!(u16), num_args = 0..)] 41 | protected_ports: Vec, 42 | #[clap(short, long)] 43 | leader_id: Option, 44 | #[clap(short, long)] 45 | external_ip_service_url: Option, 46 | #[clap(short, long)] 47 | query_file: Option, 48 | } 49 | 50 | const DENY_LIST_MAP: &str = "hvf_deny_list"; 51 | const PROTECTED_PORTS_MAP: &str = "hvf_protected_ports"; 52 | const CONNECTION_STATS: &str = "hvf_stats"; 53 | const CNC_ARRAY: &str = "hvf_cnc"; 54 | 55 | #[tokio::main] 56 | async fn main() -> Result<(), anyhow::Error> { 57 | let config = HVFConfig::parse(); 58 | 59 | tracing_subscriber::fmt().json().init(); 60 | 61 | let static_overrides = { 62 | let mut local_allow = HashSet::new(); 63 | let mut local_deny = HashSet::new(); 64 | 65 | // Load static overrides if provided 66 | if let Some(path) = config.static_overrides { 67 | let overrides = load_static_overrides(path)?; 68 | let denied: HashSet = overrides.deny.iter().map(|x| x.ip.clone()).collect(); 69 | let intersection: Vec<&NameAddressPair> = overrides 70 | .allow 71 | .iter() 72 | .filter(|x| denied.contains(&x.ip)) 73 | .collect(); 74 | 75 | if !intersection.is_empty() { 76 | error!( 77 | "Static overrides contain overlapping entries for deny and allow: {:?}", 78 | intersection 79 | ); 80 | std::process::exit(1); 81 | } 82 | for node in overrides.allow.iter() { 83 | local_allow.insert(node.ip); 84 | } 85 | for node in overrides.deny.iter() { 86 | local_deny.insert(node.ip); 87 | } 88 | 89 | info!("Loaded static overrides: {:?}", overrides); 90 | }; 91 | Arc::new((local_allow, local_deny)) 92 | }; 93 | 94 | let protected_ports = if config.protected_ports.is_empty() { 95 | warn!("No protected ports provided, defaulting to 8009 and 8010"); 96 | vec![8009, 8010] 97 | } else { 98 | config.protected_ports.clone() 99 | }; 100 | 101 | // Bump the memlock rlimit. This is needed for older kernels that don't use the 102 | // new memcg based accounting, see https://lwn.net/Articles/837122/ 103 | let rlim = libc::rlimit { 104 | rlim_cur: libc::RLIM_INFINITY, 105 | rlim_max: libc::RLIM_INFINITY, 106 | }; 107 | let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim) }; 108 | if ret != 0 { 109 | debug!("remove limit on locked memory failed, ret is: {}", ret); 110 | } 111 | 112 | // This will include your eBPF object file as raw bytes at compile-time and load it at 113 | // runtime. This approach is recommended for most real-world use cases. If you would 114 | // like to specify the eBPF program at runtime rather than at compile-time, you can 115 | // reach for `Bpf::load_file` instead. 116 | #[cfg(debug_assertions)] 117 | let mut bpf = Bpf::load(include_bytes_aligned!( 118 | "../../target/bpfel-unknown-none/debug/validator-firewall" 119 | ))?; 120 | #[cfg(not(debug_assertions))] 121 | let mut bpf = Bpf::load(include_bytes_aligned!( 122 | "../../target/bpfel-unknown-none/release/validator-firewall" 123 | ))?; 124 | if let Err(e) = BpfLogger::init(&mut bpf) { 125 | // This can happen if you remove all log statements from your eBPF program. 126 | warn!("failed to initialize eBPF logger: {}", e); 127 | } 128 | let program: &mut Xdp = bpf.program_mut("validator_firewall").unwrap().try_into()?; 129 | program.load()?; 130 | program.attach(&config.iface, XdpFlags::default()) 131 | .context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE")?; 132 | 133 | info!("Filtering UDP ports: {:?}", protected_ports); 134 | push_ports_to_map(&mut bpf, protected_ports)?; 135 | 136 | let exit = Arc::new(AtomicBool::new(false)); 137 | let gossip_exit = exit.clone(); 138 | 139 | let ip_svc_handle = if let Some(url) = config.external_ip_service_url { 140 | info!("Using external IP service: {}", url); 141 | let ip_service = HttpDenyListClient::new(url); 142 | let state_updater = DenyListStateUpdater::new( 143 | gossip_exit, 144 | Arc::new(DenyListService::new(ip_service)), 145 | static_overrides.clone(), 146 | ); 147 | 148 | let map = bpf.take_map(DENY_LIST_MAP).unwrap(); 149 | let state_updater_handle = tokio::spawn(async move { 150 | state_updater.run(map).await; 151 | }); 152 | 153 | state_updater_handle 154 | } else if let Some(query_file) = config.query_file { 155 | //read contents of file to string 156 | let query = std::fs::read_to_string(query_file)?; 157 | 158 | let s_updater = DenyListStateUpdater::new( 159 | gossip_exit, 160 | Arc::new(DenyListService::new(DuckDbDenyListClient::new(query))), 161 | static_overrides.clone(), 162 | ); 163 | 164 | let map = bpf.take_map(DENY_LIST_MAP).unwrap(); 165 | let gossip_handle = tokio::spawn(async move { 166 | s_updater.run(map).await; 167 | }); 168 | gossip_handle 169 | } else { 170 | //Default to no-op deny list client 171 | 172 | warn!("No deny list client specified, only using static overrides"); 173 | let noop = NoOpDenyListClient {}; 174 | let s_updater = DenyListStateUpdater::new( 175 | gossip_exit, 176 | Arc::new(DenyListService::new(noop)), 177 | static_overrides.clone(), 178 | ); 179 | 180 | let map = bpf.take_map(DENY_LIST_MAP).unwrap(); 181 | let gossip_handle = tokio::spawn(async move { 182 | s_updater.run(map).await; 183 | }); 184 | gossip_handle 185 | }; 186 | 187 | //Start the leader tracker 188 | let tracker = Arc::new(RPCLeaderTracker::new( 189 | exit.clone(), 190 | RpcClient::new(config.rpc_endpoint.clone()), 191 | 12, 192 | config.leader_id, 193 | )); 194 | let bg_tracker = tracker.clone(); 195 | let tracker_handle = tokio::spawn(async move { 196 | bg_tracker.clone().run().await; 197 | }); 198 | 199 | let mut tracker_service = 200 | CommandControlService::new(exit.clone(), tracker, bpf.take_map(CNC_ARRAY).unwrap()); 201 | let tracker_service_handle = tokio::spawn(async move { 202 | tracker_service.run().await; 203 | }); 204 | 205 | //Start the stats service 206 | let stats_exit = exit.clone(); 207 | let stats_service = 208 | stats_service::StatsService::new(stats_exit, 10, bpf.take_map(CONNECTION_STATS).unwrap()); 209 | let stats_handle = tokio::spawn(async move { 210 | stats_service.run().await; 211 | }); 212 | 213 | info!("Waiting for Ctrl-C..."); 214 | signal::ctrl_c().await?; 215 | exit.store(true, std::sync::atomic::Ordering::SeqCst); 216 | 217 | let (_, _, _, _) = tokio::join!( 218 | ip_svc_handle, 219 | stats_handle, 220 | tracker_handle, 221 | tracker_service_handle 222 | ); 223 | info!("Exiting..."); 224 | 225 | Ok(()) 226 | } 227 | 228 | fn push_ports_to_map(bpf: &mut Bpf, ports: Vec) -> Result<(), anyhow::Error> { 229 | let mut protected_ports: HashMap<_, u16, u8> = 230 | HashMap::try_from(bpf.map_mut(PROTECTED_PORTS_MAP).unwrap()).unwrap(); 231 | for port in ports { 232 | protected_ports.insert(&port, &0, 0)?; 233 | } 234 | Ok(()) 235 | } 236 | 237 | #[cfg(test)] 238 | mod tests { 239 | 240 | use cidr::Ipv4Cidr; 241 | use std::str::FromStr; 242 | 243 | #[test] 244 | fn test_scalar_conversion() { 245 | let string_scalar = Ipv4Cidr::from_str("1.3.5.7").unwrap(); 246 | assert_eq!(string_scalar.network_length(), 32); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /validator-firewall/src/stats_service.rs: -------------------------------------------------------------------------------- 1 | use aya::maps::{Map, MapData, MapIter, PerCpuHashMap, PerCpuValues}; 2 | use std::net::Ipv4Addr; 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | use std::sync::Arc; 5 | use tracing::info; 6 | use validator_firewall_common::StatType::{All, Blocked}; 7 | use validator_firewall_common::{ConnectionStats, StatType}; 8 | 9 | pub struct StatsService { 10 | exit: Arc, 11 | interval: u64, 12 | traffic_stats: Map, 13 | } 14 | 15 | impl StatsService { 16 | pub fn new(exit: Arc, interval: u64, traffic_stats: Map) -> Self { 17 | Self { 18 | exit, 19 | interval, 20 | traffic_stats, 21 | } 22 | } 23 | 24 | pub fn prepare_stats( 25 | map: MapIter< 26 | u32, 27 | PerCpuValues, 28 | PerCpuHashMap<&MapData, u32, ConnectionStats>, 29 | >, 30 | stat_type: StatType, 31 | ) -> Vec<(Ipv4Addr, u64)> { 32 | let mut pairs: Vec<(Ipv4Addr, u64)> = map 33 | .filter_map(|res| res.ok()) 34 | .map(|(addr, per_cpu)| { 35 | let parsed_addr = std::net::Ipv4Addr::from(u32::from_ne_bytes(addr.to_ne_bytes())); 36 | 37 | ( 38 | parsed_addr, 39 | per_cpu 40 | .iter() 41 | .map(|x| match stat_type { 42 | StatType::All => x.pkt_count, 43 | StatType::Blocked => x.blocked_pkt_count, 44 | StatType::FarFromLeader => x.far_from_leader_pkt_count, 45 | StatType::ZeroRtt => x.zero_rtt_pkt_count, 46 | }) 47 | .sum::(), 48 | ) 49 | }) 50 | .collect(); 51 | pairs.sort_by(|a, b| b.1.cmp(&a.1)); 52 | pairs 53 | } 54 | 55 | pub async fn run(&self) { 56 | let co_exit = self.exit.clone(); 57 | let traffic_stats: PerCpuHashMap<_, u32, ConnectionStats> = 58 | PerCpuHashMap::try_from(&self.traffic_stats).unwrap(); 59 | let report_interval = tokio::time::Duration::from_secs(self.interval); 60 | let mut blocked_last_sum = 0u64; 61 | let mut blocked_las_eval_time = std::time::Instant::now(); 62 | let mut all_last_sum = 0u64; 63 | let mut all_las_eval_time = std::time::Instant::now(); 64 | 65 | while !co_exit.load(Ordering::Relaxed) { 66 | // Get stats from the maps 67 | let mut all_sum = 0u64; 68 | let mut log_limit = 100; 69 | for (addr, total) in Self::prepare_stats(traffic_stats.iter(), All) { 70 | all_sum += total; 71 | if log_limit > 0 { 72 | info!("total_packets: {:?} = {:?}", addr, total); 73 | log_limit -= 1; 74 | } 75 | } 76 | 77 | let rate = (all_sum - all_last_sum) / all_las_eval_time.elapsed().as_secs().max(1); 78 | let delta = all_sum - all_last_sum; 79 | 80 | info!( 81 | traffic_type = "All", 82 | rate = rate, 83 | delta = delta, 84 | total = all_sum, 85 | "All traffic summary: {} pkts last_interval {} pkts {} pkts/s", 86 | all_sum, 87 | delta, 88 | rate 89 | ); 90 | all_last_sum = all_sum; 91 | all_las_eval_time = std::time::Instant::now(); 92 | 93 | let mut blocked_sum = 0u64; 94 | let mut log_limit = 100; 95 | for (addr, total) in Self::prepare_stats(traffic_stats.iter(), Blocked) { 96 | blocked_sum += total; 97 | if log_limit > 0 { 98 | info!("dropped_packets: {:?} = {:?}", addr, total); 99 | log_limit -= 1; 100 | } 101 | } 102 | 103 | let rate = 104 | (blocked_sum - blocked_last_sum) / blocked_las_eval_time.elapsed().as_secs().max(1); 105 | let delta = blocked_sum - blocked_last_sum; 106 | info!( 107 | traffic_type = "Blocked", 108 | rate = rate, 109 | delta = delta, 110 | total = blocked_sum, 111 | "Blocked traffic summary: {} pkts last_interval {} pkts {} pkts/s", 112 | blocked_sum, 113 | delta, 114 | rate 115 | ); 116 | blocked_last_sum = blocked_sum; 117 | blocked_las_eval_time = std::time::Instant::now(); 118 | 119 | tokio::time::sleep(report_interval).await; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /validator_firewall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helius-labs/validator-firewall/ec43b2a3f9a49d4540258201aa1454d6db381fa7/validator_firewall.png -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1" 8 | clap = { version = "4.1", features = ["derive"] } 9 | -------------------------------------------------------------------------------- /xtask/src/build_ebpf.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, process::Command}; 2 | 3 | use clap::Parser; 4 | 5 | #[derive(Debug, Copy, Clone)] 6 | pub enum Architecture { 7 | BpfEl, 8 | BpfEb, 9 | } 10 | 11 | impl std::str::FromStr for Architecture { 12 | type Err = String; 13 | 14 | fn from_str(s: &str) -> Result { 15 | Ok(match s { 16 | "bpfel-unknown-none" => Architecture::BpfEl, 17 | "bpfeb-unknown-none" => Architecture::BpfEb, 18 | _ => return Err("invalid target".to_owned()), 19 | }) 20 | } 21 | } 22 | 23 | impl std::fmt::Display for Architecture { 24 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 25 | f.write_str(match self { 26 | Architecture::BpfEl => "bpfel-unknown-none", 27 | Architecture::BpfEb => "bpfeb-unknown-none", 28 | }) 29 | } 30 | } 31 | 32 | #[derive(Debug, Parser)] 33 | pub struct Options { 34 | /// Set the endianness of the BPF target 35 | #[clap(default_value = "bpfel-unknown-none", long)] 36 | pub target: Architecture, 37 | /// Build the release target 38 | #[clap(long)] 39 | pub release: bool, 40 | } 41 | 42 | pub fn build_ebpf(opts: Options) -> Result<(), anyhow::Error> { 43 | let dir = PathBuf::from("validator-firewall-ebpf"); 44 | let target = format!("--target={}", opts.target); 45 | let mut args = vec!["build", target.as_str(), "-Z", "build-std=core"]; 46 | if opts.release { 47 | args.push("--release") 48 | } 49 | 50 | // Command::new creates a child process which inherits all env variables. This means env 51 | // vars set by the cargo xtask command are also inherited. RUSTUP_TOOLCHAIN is removed 52 | // so the rust-toolchain.toml file in the -ebpf folder is honored. 53 | 54 | let status = Command::new("cargo") 55 | .current_dir(dir) 56 | .env_remove("RUSTUP_TOOLCHAIN") 57 | .args(&args) 58 | .status() 59 | .expect("failed to build bpf program"); 60 | assert!(status.success()); 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | mod build_ebpf; 2 | mod run; 3 | 4 | use std::process::exit; 5 | 6 | use clap::Parser; 7 | 8 | #[derive(Debug, Parser)] 9 | pub struct Options { 10 | #[clap(subcommand)] 11 | command: Command, 12 | } 13 | 14 | #[derive(Debug, Parser)] 15 | enum Command { 16 | BuildEbpf(build_ebpf::Options), 17 | Run(run::Options), 18 | } 19 | 20 | fn main() { 21 | let opts = Options::parse(); 22 | 23 | use Command::*; 24 | let ret = match opts.command { 25 | BuildEbpf(opts) => build_ebpf::build_ebpf(opts), 26 | Run(opts) => run::run(opts), 27 | }; 28 | 29 | if let Err(e) = ret { 30 | eprintln!("{e:#}"); 31 | exit(1); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /xtask/src/run.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | use anyhow::Context as _; 4 | use clap::Parser; 5 | 6 | use crate::build_ebpf::{build_ebpf, Architecture, Options as BuildOptions}; 7 | 8 | #[derive(Debug, Parser)] 9 | pub struct Options { 10 | /// Set the endianness of the BPF target 11 | #[clap(default_value = "bpfel-unknown-none", long)] 12 | pub bpf_target: Architecture, 13 | /// Build and run the release target 14 | #[clap(long)] 15 | pub release: bool, 16 | /// The command used to wrap your application 17 | #[clap(short, long, default_value = "sudo -E")] 18 | pub runner: String, 19 | /// Arguments to pass to your application 20 | #[clap(name = "args", last = true)] 21 | pub run_args: Vec, 22 | } 23 | 24 | /// Build the project 25 | fn build(opts: &Options) -> Result<(), anyhow::Error> { 26 | let mut args = vec!["build"]; 27 | if opts.release { 28 | args.push("--release") 29 | } 30 | let status = Command::new("cargo") 31 | .args(&args) 32 | .status() 33 | .expect("failed to build userspace"); 34 | assert!(status.success()); 35 | Ok(()) 36 | } 37 | 38 | /// Build and run the project 39 | pub fn run(opts: Options) -> Result<(), anyhow::Error> { 40 | // build our ebpf program followed by our application 41 | build_ebpf(BuildOptions { 42 | target: opts.bpf_target, 43 | release: opts.release, 44 | }) 45 | .context("Error while building eBPF program")?; 46 | build(&opts).context("Error while building userspace application")?; 47 | 48 | // profile we are building (release or debug) 49 | let profile = if opts.release { "release" } else { "debug" }; 50 | let bin_path = format!("target/{profile}/validator-firewall"); 51 | 52 | // arguments to pass to the application 53 | let mut run_args: Vec<_> = opts.run_args.iter().map(String::as_str).collect(); 54 | 55 | // configure args 56 | let mut args: Vec<_> = opts.runner.trim().split_terminator(' ').collect(); 57 | args.push(bin_path.as_str()); 58 | args.append(&mut run_args); 59 | 60 | // run the command 61 | let status = Command::new(args.first().expect("No first argument")) 62 | .args(args.iter().skip(1)) 63 | .status() 64 | .expect("failed to run the command"); 65 | 66 | if !status.success() { 67 | anyhow::bail!("Failed to run `{}`", args.join(" ")); 68 | } 69 | Ok(()) 70 | } 71 | --------------------------------------------------------------------------------