├── .cargo └── config.sample.toml ├── .clang-format ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── cliff.toml ├── config.sample.toml ├── docs ├── assets │ ├── einat.excalidraw │ └── einat.png ├── example │ ├── config.toml │ └── systemd │ │ ├── system │ │ └── einat.service │ │ └── sysusers.d │ │ └── einat.conf ├── guide │ ├── cross.md │ ├── openwrt.md │ └── use-case.md └── reference │ ├── packet-flow.md │ └── rfc-compliance.md ├── flake.lock ├── flake.nix ├── nix ├── cross-package.nix ├── module.nix └── package.nix ├── src ├── bpf │ ├── bpf_log.h │ ├── einat.bpf.c │ ├── einat.h │ └── kernel │ │ ├── _vmlinux.h │ │ └── vmlinux.h ├── config.rs ├── instance │ ├── config.rs │ └── mod.rs ├── macros.rs ├── main.rs ├── route.rs ├── skel │ ├── aya.rs │ ├── einat │ │ ├── aya.rs │ │ ├── libbpf.rs │ │ ├── libbpf_common.rs │ │ ├── libbpf_skel.rs │ │ ├── mod.rs │ │ ├── obj_data.rs │ │ └── types.rs │ ├── libbpf.rs │ └── mod.rs └── utils.rs └── tests └── e2e.sh /.cargo/config.sample.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(unix)'] 2 | runner = "sudo -E" 3 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | IndentWidth: 4 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/bpf/kernel/_vmlinux.h linguist-generated 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://afdian.com/a/EHfive"] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | 10 | 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | **Configuration** 19 | Command-line options: 20 | 21 | ``` 22 | einat 23 | ``` 24 | 25 | Configuration file: 26 | 27 | ```toml 28 | # content of einat configuration file 29 | ``` 30 | 31 | **Target Machine (please complete the following information):** 32 | 33 | - Architecture: [e.g. x86-64, aarch64] 34 | - Linux distribution and version: [e.g. Arch Linux (rolling), OpenWrt (v23.05.5)] 35 | - Kernel version: [e.g. 5.15, 6.7.1] 36 | - einat version(`einat -v`): [e.g. `version: 0.1.5 features: aya build_features: pkg-config`] 37 | 38 | **Additional context** 39 | Add any other context about the problem here, e.g. network interface information, firewall(iptables/nftables) configuration. 40 | And connection test results of `nslookup aliyun.com 223.5.5.5`, `traceroute -T 223.5.5.5`, `ping -M do -s 1464 223.5.5.5`, etc. . 41 | 42 | Please elaborate what you have changed in detail for unchecked options below. 43 | 44 | - [ ] I have read **README** and notes in **config.sample.toml**. 45 | - [ ] I don't have any (hardware) offload/acceleration solutions enabled. 46 | - [ ] I have a clean firewall or with only firewall rule of TCP MSS Clamping. 47 | - [ ] I don't have any special/advanced routing rules other than the basic default routing. 48 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | nix_build_static: 13 | strategy: 14 | max-parallel: 6 15 | matrix: 16 | preset: [static, ipv6_static, verify_msrv_ipv6_static] 17 | arch: [x86_64, aarch64] 18 | 19 | runs-on: ubuntu-latest 20 | env: 21 | package_name: ${{ matrix.preset }}-${{ matrix.arch }}-unknown-linux-musl 22 | filename: einat-${{ matrix.preset }}-${{ matrix.arch }}-unknown-linux-musl 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Install Nix 28 | uses: cachix/install-nix-action@v30 29 | with: 30 | nix_path: nixpkgs=channel:nixos-unstable 31 | extra_nix_config: | 32 | sandbox = false # can't install in act without this 33 | 34 | - name: Nix build 35 | run: nix build .#${{ env.package_name }} && cp ./result/bin/einat ${{ env.filename }} 36 | 37 | - if: ${{ !env.ACT }} 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: ${{ env.filename }} 41 | path: ${{ env.filename }} 42 | 43 | release: 44 | if: startsWith(github.ref, 'refs/tags/') 45 | needs: nix_build_static 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/download-artifact@v4 49 | with: 50 | path: builds 51 | merge-multiple: true 52 | 53 | - name: Release with builds 54 | uses: softprops/action-gh-release@v2 55 | with: 56 | files: builds/* 57 | fail_on_unmatched_files: true 58 | draft: true 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | result 13 | 14 | .cargo/config.toml 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.1.8] - 2025-04-14 6 | 7 | ### 🐛 Bug Fixes 8 | 9 | - Fix a crash caused by IP address type conversion 10 | 11 | ### Improve 12 | 13 | - Update deps & migrate to rtnetlink 0.16.0 14 | 15 | ## [0.1.7] - 2025-02-25 16 | 17 | This is a hotfix that bring the minimum required Linux kernel version back to 5.15 . 18 | 19 | ### 🐛 Bug Fixes 20 | 21 | - Initialize struct bpf_timer without accessing its opaque fields 22 | 23 | ## [0.1.6] - 2025-02-22 24 | 25 | ### Highlights 26 | 27 | - Fixed hairpinning, it was broken since v0.1.3.. 28 | - `bpf_fib_lookup_external` now respect `ip rule` selectors `ipproto`, `sport`, `dport` and `fwmark` in addition to previously working `from`, `to` and `oif`. 29 | This is useful for balancing traffic to multiple external source addresses in a static manner, see . 30 | 31 | ### 🚀 Features 32 | 33 | - Add features info to cli version info 34 | - _(bpf)_ Lookup external source address with fwmark if possible 35 | - _(bpf)_ Fib lookup route with layer 4 ports passed 36 | 37 | ### 🐛 Bug Fixes 38 | 39 | - Fix setting of hairpinning flag and route table 40 | 41 | ### Improve 42 | 43 | - Hide developer facing option --bpf-log from the help message 44 | - _(bpf)_ Update the bpf log tag to [einat] 45 | - Avoid converting OsString to String for config file path 46 | - Explicitly specify encap type for IP tunnel link types 47 | 48 | ## [0.1.5] - 2024-12-09 49 | 50 | ### 🐛 Bug Fixes 51 | 52 | - Prevent pkg_config from emitting Cargo linking instructions 53 | - Fix CLI arg --internal not being applied 54 | 55 | ### Improve 56 | 57 | - Re-enable libbpf logging 58 | - TCX attach before all other links 59 | - Prefix match binding & ct addresses with external network CIDR 60 | - _(build)_ Error out if build commands not exit with success 61 | - Allow using bpftool for stripping 62 | 63 | ## [0.1.4] - 2024-11-20 64 | 65 | This is a hotfix addressing build error on Rust 1.80 on which is the minimal version that einat requires to build. 66 | 67 | ### 🐛 Bug Fixes 68 | 69 | - Elided lifetimes in associated constant 70 | 71 | ### 🧪 Testing 72 | 73 | - Add tests for einat skel 74 | 75 | ## [0.1.3] - 2024-11-19 76 | 77 | ### Highlights 78 | 79 | - Fix a bug that might cause silent packet drop, which has been observed on PPPoE interface for large packets. 80 | - Use pure-rust Aya loader by default, einat now has zero native dependency except libc on target platform. 81 | This should make einat be built more easily especially for cross-compilation. 82 | - Allow attaching eBPF programs with new TCX interface, aya loader only. 83 | - Allow do SNAT for specified internal network only 84 | 85 | ```bash 86 | # do SNAT for internal packets with source of 192.168.1.0/24 only 87 | einat -i extern0 --hairpin-if intern0 lo --internal 192.168.1.0/24 88 | ``` 89 | 90 | ### 🚀 Features 91 | 92 | - Add pure-Rust aya loading backend support 93 | - Add config option to toggle TCX interface usage 94 | - Allow do NAT for specified internal network only 95 | - Add CLI options for snat_internals and bpf_loader 96 | 97 | ### 🐛 Bug Fixes 98 | 99 | - Workaround an unroll failure 100 | - _(ebpf)_ Always pull first header bytes 101 | - Split EINAT_BPF_CFLAGS args 102 | 103 | ### Improve 104 | 105 | - Increase log level of libbpf netlink error to DEBUG 106 | - Describe NAT44 enabling more specifically 107 | - Log eBPF loader used 108 | - Enable bpf_fib_lookup_external by default on kernel>=6.7 109 | 110 | ## [0.1.2] - 2024-04-13 111 | 112 | ### 🚀 Features 113 | 114 | - Implement interface monitoring and dynamic attaching 115 | - Add CLI option to print einat version 116 | 117 | ### 🐛 Bug Fixes 118 | 119 | - Filter out link address of all zero 120 | - Fix checksums calculation of IPv6 packets 121 | - Passthrough unsupported types of IPv6 packet 122 | 123 | ### Improve 124 | 125 | - Change the default UDP/ICMP timeout to 2 mins 126 | - [**breaking**] Disallow user supplied if_index 127 | 128 | ## [0.1.1] - 2024-04-07 129 | 130 | ### 🚀 Features 131 | 132 | - Add more CLI options 133 | 134 | ### 🐛 Bug Fixes 135 | 136 | - Guard against division by zero in libbpf-rs 137 | - Do not use unspecified IP address as external address 138 | - Fix port range merging algorithm 139 | - Prefer local address over prefix address 140 | 141 | ## [0.1.0] - 2024-04-05 142 | 143 | Initial release. 144 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "einat" 3 | version = "0.1.8" 4 | edition = "2021" 5 | license = "GPL-2.0" 6 | authors = ["Huang-Huang Bao "] 7 | repository = "https://github.com/EHfive/einat-ebpf" 8 | rust-version = "1.80" 9 | publish = false 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [profile.release] 14 | codegen-units = 1 15 | lto = true 16 | opt-level = "z" 17 | strip = true 18 | 19 | [features] 20 | default = ["aya", "pkg-config"] 21 | # Enable IPv6 NAPT 22 | ipv6 = [] 23 | # Enable Aya BPF loader 24 | aya = [ 25 | # the dep:aya is also used for determining kernel version so it's already included 26 | ] 27 | # Enable libbpf BPF loader 28 | libbpf = ["dep:libbpf-rs", "dep:libbpf-sys"] 29 | # Enable libbpf skeleton-wrapped BPF loader 30 | libbpf-skel = [ 31 | "dep:libbpf-rs", 32 | "dep:libbpf-sys", 33 | "dep:libbpf-cargo", 34 | "dep:self_cell", 35 | ] 36 | # Bindgen for libbpf headers, required on 32-bit platforms 37 | bindgen = ["libbpf-sys?/bindgen"] 38 | # Link against static `libelf` and `zlib` for libbpf loader. 39 | static = ["libbpf-sys?/static"] 40 | # 41 | # libbpf is vendored and static in any case. 42 | # 43 | 44 | [dependencies] 45 | anyhow = "1.0.98" 46 | async-stream = "0.3.6" 47 | aya = "0.13.1" 48 | aya-obj = "0.2.1" 49 | bitflags = { version = "2.9.0", features = ["bytemuck"] } 50 | bytemuck = { version = "1.22.0", features = ["derive"] } 51 | cfg-if = "1.0.0" 52 | enum_dispatch = "0.3.13" 53 | fundu = "2.0.1" 54 | futures-util = { version = "0.3.31", default-features = false, features = [ 55 | "std", 56 | ] } 57 | ipnet = { version = "2.11.0", features = ["serde"] } 58 | lexopt = "0.3.1" 59 | libbpf-rs = { version = "0.24.8", optional = true } 60 | libbpf-sys = { version = "1.5.0", optional = true } 61 | libc = "0.2.171" 62 | netlink-packet-core = "0.7.0" 63 | netlink-packet-route = "0.22.0" 64 | netlink-proto = "0.11.5" 65 | netlink-sys = "0.8.7" 66 | prefix-trie = "0.7.0" 67 | rtnetlink = "0.16.0" 68 | self_cell = { version = "1.2.0", optional = true } 69 | serde = { version = "1.0.219", features = ["derive"] } 70 | tokio = { version = "1.44.2", features = [ 71 | "macros", 72 | "rt", 73 | "signal", 74 | "sync", 75 | "time", 76 | ] } 77 | toml = { version = "0.8.20", default-features = false, features = ["parse"] } 78 | tracing = { version = "0.1.41", default-features = false, features = ["std"] } 79 | tracing-subscriber = { version = "0.3.19", default-features = false, features = [ 80 | "std", 81 | "fmt", 82 | "ansi", 83 | ] } 84 | 85 | [target.'cfg(not(target_arch="x86_64"))'.dependencies] 86 | libbpf-sys = { version = "1.5.0", features = ["bindgen"], optional = true } 87 | 88 | [build-dependencies] 89 | libbpf-cargo = { version = "0.24.8", optional = true } 90 | pkg-config = { version = "0.3.32", optional = true } 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # einat 2 | 3 | einat is an eBPF-based Endpoint-Independent NAT(Network Address Translation). 4 | 5 | The eBPF part of einat implements an "Endpoint-Independent Mapping" and "Endpoint-Independent Filtering" NAT on TC egress and ingress hooks. 6 | 7 | ### Features 8 | 9 | - **eBPF**: IPv4 to IPv4 NAPT(Network Address Port Translation) 10 | - **eBPF**: IPv6 to IPv6 NAPT 11 | - **eBPF**: Endpoint-Independent(Full Cone) NAT for TCP, UDP and ICMP 12 | - **eBPF**: Partial external port range usage, allows reserving external ports for other usage 13 | - **Frontend**: Automatic reconfiguration on interface address changes 14 | - **Frontend**: Automatic IP rule and route setup for hairpinning, see https://github.com/EHfive/einat-ebpf/issues/4 15 | 16 | See example [use cases](./docs/guide/use-case.md) for what can be achieved with EIM + EIF and other features `einat` provides. 17 | 18 | For implementation details, see documentations under [reference](./docs/reference/). 19 | 20 | ## Requirement 21 | 22 | - Linux kernel >= 5.15 (compiled with BPF and BTF support) on target machine 23 | - Rust toolchain (`cargo` etc.) 24 | - `clang` for compiling BPF C code 25 | - `bpftool` or `llvm-strip` for stripping compiled BPF object 26 | - `libbpf` headers 27 | - (optional) `pkg-config` to locate `libbpf` headers 28 | 29 | Additional dependencies for `"libbpf"` loader: 30 | 31 | - `rustfmt` for formatting generated code 32 | - `clang` libs for bindgen 33 | - `libelf` from elfutils and `zlib` on target platform 34 | 35 | Currently we support `"aya"`, `"libbpf"` and `"libbpf-skel"` eBPF loaders, only the `"aya"` is enabled by default as it requires no native dependencies on target platform except libc. 36 | 37 | The `"libbpf-skel"` loader is served as reference purpose and you should just use `aya` or `libbpf` instead. 38 | 39 | It's also required the eBPF JIT implementation for target architecture in kernel has implemented support for BPF-to-BPF calls, which is not the case for MIPS and other architectures have less interests. This application is only tested to work on x86-64 or aarch64. 40 | 41 | See also [OpenWrt guide](./docs/guide/openwrt.md) for pitfalls running this on OpenWrt. 42 | 43 | ## Installation 44 | 45 | ```shell 46 | cargo install --locked --git https://github.com/EHfive/einat-ebpf.git 47 | ``` 48 | 49 | You can also enable IPv6 NAT66 feature with `--features ipv6` flag, however it would increase load time of eBPF programs to about 4 times. 50 | 51 | Or build static binaries with Nix flakes we provide, run `nix flake show` to list all available packages. 52 | 53 | ```shell 54 | nix build "github:EHfive/einat-ebpf#static-x86_64-unknown-linux-musl" 55 | nix build "github:EHfive/einat-ebpf#ipv6_static-x86_64-unknown-linux-musl" 56 | # Cross compile for aarch64 57 | nix build "github:EHfive/einat-ebpf#static-aarch64-unknown-linux-musl" 58 | ``` 59 | 60 | For NixOS, you can use module [`github:EHfive/einat-ebpf#nixosModules.default`](./nix/module.nix). 61 | 62 | For OpenWrt, there are [openwrt-einat-ebpf](https://github.com/muink/openwrt-einat-ebpf) and [luci-app-einat](https://github.com/muink/luci-app-einat) by @muink. 63 | 64 | See also [cross-compilation guide](./docs/guide/cross.md) for cross-compilation on Debian/Debian-based distros. 65 | 66 | You can also download pre-built static binaries from [Releases](https://github.com/EHfive/einat-ebpf/releases) or our [Actions](https://github.com/EHfive/einat-ebpf/actions) artifacts(zip archived). 67 | 68 | For persisting einat in systemd-powered system manually, reference [example](./docs/example/) config and unit file, and see [Systemd](https://wiki.archlinux.org/title/Systemd) in famous‌ ArchWiki. 69 | 70 | ### Build Environment Variables 71 | 72 | | Name | Example Value | Note | 73 | | ---------------------- | -------------------------- | --------------------------------------------------------------------------- | 74 | | `EINAT_BPF_CFLAGS` | `-I/usr/include/` | Specify extra CFLAGS for BPF object compilation | 75 | | `EINAT_BPF_STRIP_CMD` | `llvm-strip -g -o` | BPF object stripping command, would followed by target path and source path | 76 | | `LIBBPF_NO_PKG_CONFIG` | `1` | Disable [pkg_config lookup] of libbpf. | 77 | 78 | [pkg_config lookup]: https://docs.rs/pkg-config/0.3.31/pkg_config/index.html#environment-variables 79 | 80 | You can combine `LIBBPF_NO_PKG_CONFIG` and `EINAT_BPF_CFLAGS` to specify include flag of libbpf headers manually. 81 | 82 | See also [build.rs](./build.rs) for reference. 83 | 84 | ## Usage 85 | 86 | ``` 87 | einat - An eBPF-based Endpoint-Independent NAT 88 | 89 | USAGE: 90 | einat [OPTIONS] 91 | 92 | OPTIONS: 93 | -h, --help Print this message 94 | -c, --config Path to configuration file 95 | -i, --ifname External network interface name, e.g. eth0 96 | --nat44 Enable NAT44/NAPT44 for specified network interface, enabled by 97 | default if neither --nat44 nor --nat66 are specified 98 | --nat66 Enable NAT66/NAPT66 for specified network interface 99 | --ports ... External TCP/UDP port ranges, defaults to 20000-29999 100 | --hairpin-if ... Hairpin internal network interface names, e.g. lo, lan0 101 | --internal ... Perform source NAT for these internal networks only 102 | --bpf-loader BPF loading backend used, one of aya or libbpf 103 | -v, --version Print einat version 104 | ``` 105 | 106 | You would only need to specify external interface name in a minimal setup, and `einat` would select an external IP address on specified interface and reconfigures automatically. 107 | 108 | ```shell 109 | # Enable IP forwarding if not already 110 | sudo sysctl net.ipv4.ip_forward=1 111 | # With simplified CLI options, 112 | # this setup NAT for traffic forwarding to and from wan0 and setup hairpin 113 | # routing for traffic forwarding from lo or lan0 to wan0 114 | sudo einat --ifname wan0 --hairpin-if lo lan0 115 | # With config file 116 | sudo einat --config /path/to/config.toml 117 | ``` 118 | 119 | See [config.sample.toml](./config.sample.toml) for more configuration options. This program requires `cap_sys_admin` for passing eBPF verification and `cap_net_admin` for attaching eBPF program to TC hooks on network interface. 120 | 121 | Also make sure nftables/iptables masquerading rule is not set and forwarding of inbound traffic from external interface to internal interfaces for port ranges `einat` uses is allowed. 122 | 123 | If you attach einat to tunnel interfaces(e.g. PPPoE, WireGuard) with MTU less than 1500 bytes, 124 | you might also want to setup [TCP MSS clamping] in case there is ICMP black hole which prevent PMTUD(Path MTU Discovery) from functioning on either internal or remote side, 125 | see . Though this only works for TCP. 126 | 127 | [TCP MSS clamping]: https://wiki.nftables.org/wiki-nftables/index.php/Mangling_packet_headers#Mangling_TCP_options 128 | 129 | > [!IMPORTANT] 130 | > Disable any hardware offload/acceleration solutions before trying out einat, especially on OpenWrt where "acceleration" solutions are commonly abused. 131 | > As hardware offload solution can't recognize NAT bindings created in einat, the incoming packets flowing over hardware firmware could be dropped due to some internal firewall policies. 132 | 133 | To test if this works, you can use tools below on internal network behind NAT. Notice you could only got "Full Cone" NAT if your external network is already "Full Cone" NAT or has a public IP. 134 | 135 | - `stunclient` from [stuntman](https://github.com/jselbie/stunserver) 136 | - [stun-nat-behaviour](https://github.com/pion/stun/tree/master/cmd/stun-nat-behaviour) 137 | - [go-stun](https://github.com/ccding/go-stun) 138 | - [NatTypeTester](https://github.com/HMBSbige/NatTypeTester) on Windows 139 | 140 | ## Alternatives 141 | 142 | - [netfilter-full-cone-nat](https://github.com/Chion82/netfilter-full-cone-nat) 143 | - [nft-fullcone](https://github.com/fullcone-nat-nftables) 144 | 145 | Instead of relying on existing Netfilter conntrack system like these out-of-tree kernel modules did, we implement a fully functional Endpoint Independent NAT engine on eBPF TC hook from scratch thus avoiding hassles dealing with "Address and Port-Dependent" Netfilter conntrack system and being slim and efficient. 146 | 147 | And `einat` utilizes aya's CO-RE(Compile Once – Run Everywhere) capabilities that hugely simplifies distribution and deployment. 148 | 149 | ## Recommended Reading 150 | 151 | - How NAT traversal works, by David Anderson 152 | - RFC 4787, Network Address Translation (NAT) Behavioral Requirements for Unicast UDP, 153 | - [Chinese] einat-ebpf,用 eBPF 从头写一个 Full Cone NAT, 154 | 155 | ## COPYING 156 | 157 | Sources under ./src/bpf/kernel are derived from Linux kernel, hence they are GPL-2.0-only licensed. 158 | For other files under this project, unless specified, they are GPL-2.0-or-later licensed. 159 | 160 | Notice our BPF program calls into GPL-licensed kernel functions so you need to choose GPL-2.0-only license to distribute it. 161 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | 4 | const SRC: &str = "src/bpf/einat.bpf.c"; 5 | const SRC_DIR: &str = "src/bpf/"; 6 | 7 | fn out_path(file: &str) -> PathBuf { 8 | let mut out = 9 | PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR must be set in build script")); 10 | out.push(file); 11 | out 12 | } 13 | 14 | fn c_args() -> Vec { 15 | let mut c_args: Vec = [ 16 | "-Wall", 17 | "-mcpu=v3", 18 | "-fno-stack-protector", 19 | // error out if loop is not unrolled 20 | "-Werror=pass-failed", 21 | // "-Werror" 22 | ] 23 | .iter() 24 | .map(|s| s.to_string()) 25 | .collect(); 26 | 27 | if cfg!(feature = "ipv6") { 28 | c_args.push("-DFEAT_IPV6".to_string()); 29 | } 30 | 31 | c_args 32 | } 33 | 34 | #[cfg(any(feature = "aya", feature = "libbpf"))] 35 | fn einat_obj_build() { 36 | use std::ffi::OsStr; 37 | use std::process::Command; 38 | 39 | let bpf_obj_tmp = &out_path("einat.bpf.tmp.o"); 40 | let bpf_obj = &out_path("einat.bpf.o"); 41 | 42 | // compile BPF C code 43 | let mut cmd = Command::new("clang"); 44 | 45 | cmd.args(c_args()); 46 | 47 | if let Some(cflags) = option_env!("EINAT_BPF_CFLAGS") { 48 | cmd.args(cflags.split_ascii_whitespace()); 49 | } 50 | 51 | // Specify environment variable LIBBPF_NO_PKG_CONFIG=1 to disable pkg-config lookup. 52 | // Or just disable the "pkg-config" feature. 53 | #[cfg(feature = "pkg-config")] 54 | match pkg_config::Config::new() 55 | .cargo_metadata(false) 56 | .probe("libbpf") 57 | { 58 | Ok(libbpf) => { 59 | let includes = libbpf 60 | .include_paths 61 | .into_iter() 62 | .map(|i| format!("-I{}", i.to_string_lossy())); 63 | cmd.args(includes); 64 | } 65 | Err(e) => { 66 | eprintln!("Can not locate libbpf with pkg-config: {}", e) 67 | } 68 | } 69 | 70 | let target = if env::var("CARGO_CFG_TARGET_ENDIAN").unwrap() == "little" { 71 | "bpfel" 72 | } else { 73 | "bpfeb" 74 | }; 75 | 76 | let res = cmd 77 | .arg("-target") 78 | .arg(target) 79 | .arg("-g") 80 | .arg("-O2") 81 | .arg("-c") 82 | .arg(SRC) 83 | .arg("-o") 84 | .arg(bpf_obj_tmp) 85 | .status() 86 | .expect("compile BPF object failed"); 87 | if !res.success() { 88 | panic!("{}", res); 89 | } 90 | 91 | fn strip_obj>(strip_cmd: &str, target: S, source: S) -> Result<(), String> { 92 | let mut args = strip_cmd.split_ascii_whitespace(); 93 | let cmd = args.next().unwrap(); 94 | let res = Command::new(cmd) 95 | .args(args) 96 | .arg(target) 97 | .arg(source) 98 | .status(); 99 | 100 | match res { 101 | Ok(res) => { 102 | if res.success() { 103 | return Ok(()); 104 | } 105 | Err(format!("{}: {}", strip_cmd, res)) 106 | } 107 | Err(err) => Err(format!("{}: {}", strip_cmd, err)), 108 | } 109 | } 110 | 111 | // strip the DWARF debug information 112 | let strip_bpf_obj = || -> Result<(), String> { 113 | if let Some(strip_cmd) = option_env!("EINAT_BPF_STRIP_CMD") { 114 | return strip_obj(strip_cmd, bpf_obj, bpf_obj_tmp); 115 | } 116 | 117 | let res = strip_obj("bpftool gen object", bpf_obj, bpf_obj_tmp); 118 | if res.is_ok() { 119 | return res; 120 | } 121 | eprintln!("strip with bpftool failed, fallback to llvm-strip"); 122 | 123 | let res = strip_obj("llvm-strip -g -o", bpf_obj, bpf_obj_tmp); 124 | if res.is_ok() { 125 | return res; 126 | } 127 | eprintln!("strip with llvm-strip failed, skip stripping"); 128 | 129 | std::fs::rename(bpf_obj_tmp, bpf_obj).unwrap(); 130 | 131 | Ok(()) 132 | }; 133 | 134 | strip_bpf_obj().expect("strip BPF object file failed"); 135 | 136 | println!("cargo:rerun-if-env-changed=EINAT_BPF_CFLAGS"); 137 | println!("cargo:rerun-if-env-changed=EINAT_BPF_STRIP_CMD"); 138 | } 139 | 140 | #[cfg(feature = "libbpf-skel")] 141 | fn libbpf_skel_build() { 142 | use libbpf_cargo::SkeletonBuilder; 143 | 144 | let out = out_path("einat.skel.rs"); 145 | 146 | SkeletonBuilder::new() 147 | .source(SRC) 148 | .clang_args(c_args()) 149 | .debug(true) 150 | .build_and_generate(&out) 151 | .unwrap(); 152 | } 153 | 154 | fn gen_build_info() { 155 | macro_rules! enabled_features { 156 | ($($feat:literal),+) => { 157 | &[ $(#[cfg(feature = $feat)] $feat),+ ] 158 | }; 159 | } 160 | 161 | let features: &[&str] = enabled_features!("ipv6", "aya", "libbpf", "libbpf-skel"); 162 | let build_features: &[&str] = enabled_features!("pkg-config", "bindgen", "static"); 163 | let features = features.join(","); 164 | let build_features = build_features.join(","); 165 | 166 | // IMPORTANT: 167 | // use format `: [ : ]+`, and should not contains any whitespace, 168 | // so the printed build info text can be parsed. 169 | let build_info = [ 170 | ("version", env!("CARGO_PKG_VERSION").to_string()), 171 | ("features", features), 172 | ("build_features", build_features), 173 | ]; 174 | 175 | let build_info = build_info 176 | .iter() 177 | .map(|(k, v)| { 178 | if v.is_empty() { 179 | format!("{k}: ") 180 | } else { 181 | format!("{k}: {v}") 182 | } 183 | }) 184 | .collect::>() 185 | .join(" "); 186 | 187 | println!("cargo:rustc-env=EINAT_BUILD_INFO={}", build_info); 188 | } 189 | 190 | fn main() { 191 | #[cfg(any(feature = "aya", feature = "libbpf"))] 192 | einat_obj_build(); 193 | 194 | #[cfg(feature = "libbpf-skel")] 195 | libbpf_skel_build(); 196 | 197 | gen_build_info(); 198 | 199 | println!("cargo:rerun-if-changed={}", SRC_DIR); 200 | println!("cargo:rerun-if-changed=build.rs"); 201 | } 202 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://keats.github.io/tera/docs/#introduction 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | striptags | trim | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 26 | {% if commit.breaking %}[**breaking**] {% endif %}\ 27 | {{ commit.message | upper_first }}\ 28 | {% endfor %} 29 | {% endfor %}\n 30 | """ 31 | # template for the changelog footer 32 | footer = """ 33 | 34 | """ 35 | # remove the leading and trailing s 36 | trim = true 37 | # postprocessors 38 | postprocessors = [ 39 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL 40 | ] 41 | 42 | [git] 43 | # parse the commits based on https://www.conventionalcommits.org 44 | conventional_commits = true 45 | # filter out the commits that are not conventional 46 | filter_unconventional = true 47 | # process each line of a commit as an individual commit 48 | split_commits = false 49 | # regex for preprocessing the commit messages 50 | commit_preprocessors = [ 51 | # Replace issue numbers 52 | #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, 53 | # Check spelling of the commit with https://github.com/crate-ci/typos 54 | # If the spelling is incorrect, it will be automatically fixed. 55 | #{ pattern = '.*', replace_command = 'typos --write-changes -' }, 56 | ] 57 | # regex for parsing and grouping commits 58 | commit_parsers = [ 59 | { message = "^feat", group = "🚀 Features" }, 60 | { message = "^fix", group = "🐛 Bug Fixes" }, 61 | { message = "^doc", group = "📚 Documentation", skip = true }, 62 | { message = "^perf", group = "⚡ Performance" }, 63 | { message = "^refactor", group = "🚜 Refactor" }, 64 | { message = "^style", group = "🎨 Styling" }, 65 | { message = "^test", group = "🧪 Testing" }, 66 | { message = "^chore\\(release\\): prepare for", skip = true }, 67 | { message = "^chore\\(deps.*\\)", skip = true }, 68 | { message = "^chore\\(pr\\)", skip = true }, 69 | { message = "^chore\\(pull\\)", skip = true }, 70 | { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks", skip = true }, 71 | { body = ".*security", group = "🛡️ Security" }, 72 | { message = "^revert", group = "◀️ Revert" }, 73 | ] 74 | # protect breaking changes from being skipped due to matching a skipping commit_parser 75 | protect_breaking_commits = true 76 | # filter out the commits that are not matched by commit parsers 77 | filter_commits = false 78 | # regex for matching git tags 79 | # tag_pattern = "v[0-9].*" 80 | # regex for skipping tags 81 | # skip_tags = "" 82 | # regex for ignoring tags 83 | # ignore_tags = "" 84 | # sort the tags topologically 85 | topo_order = false 86 | # sort the commits inside sections by oldest/newest order 87 | sort_commits = "oldest" 88 | # limit the number of commits included in the changelog. 89 | # limit_commits = 42 90 | -------------------------------------------------------------------------------- /config.sample.toml: -------------------------------------------------------------------------------- 1 | # einat Configuration File 2 | 3 | [defaults] 4 | ipv4_local_rule_pref = 200 5 | ipv6_local_rule_pref = 200 6 | ipv4_hairpin_rule_pref = 100 7 | ipv6_hairpin_rule_pref = 100 8 | ipv4_hairpin_table_id = 4787 9 | ipv6_hairpin_table_id = 4787 10 | 11 | # For ports not in specified ranges, einat would passthrough NAT if the traffic 12 | # is on interface's external address. You should exclude ports of services ( 13 | # e.g. SSH, HTTP server) serving on NAT host's external address and expecting 14 | # inbound initiated traffic from NAT port ranges specified here. 15 | tcp_ranges = ["20000-29999"] 16 | udp_ranges = ["20000-29999"] 17 | # Combined ICMP query ID ranges, must include `icmp_in_ranges` and `icmp_out_ranges`. 18 | icmp_ranges = ["0-65535"] 19 | # Inbound ICMP query ID ranges 20 | icmp_in_ranges = ["0-9999"] 21 | # Outbound ICMP query ID ranges 22 | icmp_out_ranges = ["1000-65535"] 23 | 24 | # Minimal NAT44 configuration with hairpin routing 25 | [[interfaces]] 26 | if_name = "eth0" 27 | nat44 = true 28 | ipv4_hairpin_route.internal_if_names = ["lo", "internal"] 29 | 30 | [[interfaces]] 31 | # External(outbound) interface on which NAT would be performed. 32 | if_name = "eth2" 33 | 34 | # Unlike NAT flags in CLI options, `nat44` and `nat66` are both disabled by default. 35 | # Explicitly set either `nat44` or `nat66` to enable NAT. 36 | # 37 | # Enable NAPT44 38 | nat44 = false 39 | # Enable NAPT66, this requires that einat was built with "ipv6" feature flag. 40 | nat66 = false 41 | 42 | # Set max BPF log level, developer facing debugging option, 43 | # would increases eBPF verification time if enabled. 44 | # You can also set `--bpf-log ` for CLI. 45 | # 0: disable, 1: error, 2: warn, 3: info, 4: debug, 5: trace 46 | # View logs with `cat /sys/kernel/debug/tracing/trace_pipe` 47 | bpf_log_level = 0 48 | 49 | # Enable external address(preferred source) lookup from Linux routing table and rules. 50 | # Only works on Linux kernel>=6.7, it's a no-op for kernel on lower version. 51 | # 52 | # Note the Linux routing table & rules are only served as database purpose. 53 | # For policy-based routing, it only matches `ip rule` selector `from`, `to`, 54 | # `fwmark`, `ipproto`, `sport`, `dport`, and/or `oif` of attached interface, 55 | # And it requires Linux kernel>=6.10 to match with `fwmark` selector. 56 | # 57 | # If you enable this option, be careful to have external configs and SNAT 58 | # internals properly setting up for all possible preferred sources configured 59 | # in route table otherwise SNAT skipping or packet dropping could happen. 60 | # 61 | # Enabled by default on Linux kernel>=6.7. 62 | bpf_fib_lookup_external = true 63 | 64 | # Set this to `false` for early disabling inbound ICMP binding initiation, 65 | # similar to set `icmp_in_ranges = []`. 66 | allow_inbound_icmpx = true 67 | 68 | # NAT records lifetimes, see . 69 | # See available time units in . 70 | timeout_fragment = "2s" 71 | timeout_pkt_min = "1m" 72 | timeout_pkt_default = "5m" 73 | timeout_tcp_trans = "4m" 74 | timeout_tcp_est = "124m" 75 | 76 | # Max fragment tracking records allowed. 77 | frag_track_max_records = 65536 78 | 79 | # Max NAT binding records allowed, defaults to 65536 * 3(TCP, UDP and ICMP). 80 | # 81 | # Note the default value is already the upper limit for a single external 82 | # address. If your network is under high pressure that taken up all ports, 83 | # try lowering timeout_pkt_* and/or timeout_tcp_* so unused port bindings can 84 | # be cleaned quicker thus can be reused. 85 | binding_max_records = 196608 86 | 87 | # Max connection tracking records allowed, this should be at least the same as 88 | # `binding_max_records`, defaults to `binding_max_records` * 2. Increase this 89 | # if you have more connections than the default value. 90 | ct_max_records = 393216 91 | 92 | # BPF loading backend used, one of "aya", "libbpf" or "libbpf-skel". 93 | # Requires respetive Cargo feature flag to be enabled on build, fallback to 94 | # other enabled loading backend if not available. 95 | bpf_loader = "aya" 96 | 97 | # Use TCX interface if available, only supported by aya loader. 98 | # Enabled by default on Linux kernel>=6.6 99 | prefer_tcx = true 100 | 101 | # Disable source NAT for specified destination networks. 102 | no_snat_dests = [ 103 | # "192.168.0.0/16" 104 | ] 105 | 106 | # This appends external config(s) with `match_address = "0.0.0.0/0` 107 | # and/or `match_address = "::/0` to match all IP addresses on interface. 108 | default_externals = true 109 | 110 | # Enable source NAT for internal source networks specified here only, all other 111 | # addresses would be seen as external routable address. Note traffic with 112 | # SNAT-able(no_snat=false) external source address(traffic from NAT host itself) 113 | # would do SNAT regardless. 114 | # 115 | # This is useful only if you have external(public) addresses assigned to internal 116 | # hosts and you want to have traffic from those external addresses going trough 117 | # the interface that einat attaches without being SNATed. 118 | # 119 | # This prepends the following external configs if the internals is not empty for 120 | # IPv4 or IPv6 respectively. Be careful to not include possible external 121 | # addresses here otherwise traffic would be blocked. So if it's not required, 122 | # don't set this option. 123 | # 124 | # 125 | # [[interfaces.externals]] 126 | # network = "0.0.0.0/0" # "::/0" for IPv6 127 | # no_snat = true 128 | # no_hairpin = true 129 | # 130 | # [[interfaces.externals]] 131 | # network = "" 132 | # is_internal = true 133 | # #... 134 | # 135 | snat_internals = [ 136 | # "192.168.1.0/24", 137 | # "fc00::1:0/64" 138 | ] 139 | 140 | # Automatically configure hairpinning routes 141 | [interfaces.ipv4_hairpin_route] 142 | # Enable the hairpin routing configuration, defaults to true if 143 | # `internal_if_names` is not empty, otherwise defaults to false. 144 | enable = false 145 | internal_if_names = [ 146 | # "lo", 147 | # "internal" 148 | ] 149 | # Hairpinning IP protocols. You can also add "icmp" here, however it would 150 | # results ICMP query packet to be send back to the sender due to combinated 151 | # effect of the "Endpoint-Independent Mapping" behavior and ICMP does not 152 | # distinguish between source query ID and destination query ID, this is not 153 | # very useful thus not including "icmp" by default. 154 | ip_protocols = ["tcp", "udp"] 155 | # Defaults to `defaults.ipv4_local_rule_pref`. 156 | ip_rule_pref = 200 157 | # Defaults to `defaults.ipv4_hairpin_table_id`. 158 | table_id = 4787 159 | 160 | [interfaces.ipv6_hairpin_route] 161 | enable = false 162 | internal_if_names = [] 163 | ip_protocols = ["tcp", "udp"] 164 | # Defaults to `defaults.ipv6_local_rule_pref`. 165 | ip_rule_pref = 200 166 | # Defaults to `defaults.ipv6_hairpin_table_id`. 167 | table_id = 4787 168 | 169 | # Configure external source addresses that NAT or not NAT to it. 170 | # External configs are saved in prefix trie, hence config with more specific 171 | # address prefix would be used. In the case of configs with same evaluated 172 | # address prefix, config item defined earilier has higher priority. The first 173 | # static or matching address would be used as the default NAT external source. 174 | [[interfaces.externals]] 175 | # Specify a static external source for NAT 176 | address = "192.168.4.2" 177 | # Or specify a static network as NAT external source, the address part(e.g. 178 | # 10.0.0.5) of this CIDR would be used as default NAT external source 179 | # if this config item got selected. 180 | network = "10.0.0.5/24" 181 | # If true, the specified address/network is marked as internal source instead 182 | # of NAT external source. And such internal source would do NAT explicitly. 183 | is_internal = false 184 | # If `true`, the address would not be used as target NAT source address, 185 | # and packet flow from or to this source address would pass through NAT. 186 | no_snat = false 187 | # Disable hairpinning for the address. 188 | no_hairpin = false 189 | # Defaults to ranges in [defaults] if not specified. 190 | #tcp_ranges = ["10000-65535"] 191 | #udp_ranges = ["10000-65535"] 192 | #icmp_ranges = ["0-65535"] 193 | #icmp_in_ranges = ["0-9999"] 194 | #icmp_out_ranges = ["1000-65535"] 195 | 196 | # You can set ranges to empty `[]` to disable NAT for respective protocol. 197 | # For example disable NAT for TCP, you can than combine with Netfilter 198 | # masquerading for TCP to form a mixed NAT. 199 | #tcp_ranges = [] 200 | 201 | # Use `match_address` to match addresses on external interface specified. 202 | [[interfaces.externals]] 203 | # Match a CIDR network, would evaluate to matched address(es) on the interface. 204 | match_address = "192.168.4.0/24" 205 | # This is equivalent to format above. 206 | match_address = { network = "192.168.4.0/24" } 207 | # Match an address range. 208 | match_address = { start = "192.168.4.100", end = "192.168.4.200" } 209 | 210 | # You might want to exclude some address from being selected as 211 | # NAT external address. 212 | # Example that exclude IPv6 link-local addresses. 213 | [[interfaces.externals]] 214 | match_address = "fe80::/10" 215 | no_snat = true 216 | no_hairpin = true 217 | -------------------------------------------------------------------------------- /docs/assets/einat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EHfive/einat-ebpf/a6b0395a1fc855c3b0968d9d53dd726607b3e5ba/docs/assets/einat.png -------------------------------------------------------------------------------- /docs/example/config.toml: -------------------------------------------------------------------------------- 1 | # See "config.sample.toml" for more options. 2 | 3 | [[interfaces]] 4 | if_name = "eth0" 5 | # NAT44 and NAT66 are disabled by default, set `nat44` and `nat66` to enable. 6 | #nat44 = true 7 | #nat66 = true 8 | ipv4_hairpin_route.internal_if_names = [] 9 | -------------------------------------------------------------------------------- /docs/example/systemd/system/einat.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=einat-ebpf service 3 | After=network.target 4 | 5 | [Service] 6 | User=einat 7 | CapabilityBoundingSet=CAP_SYS_ADMIN CAP_NET_ADMIN 8 | AmbientCapabilities=CAP_SYS_ADMIN CAP_NET_ADMIN 9 | ExecStart=/usr/bin/einat -c /etc/einat/config.toml 10 | Restart=on-failure 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /docs/example/systemd/sysusers.d/einat.conf: -------------------------------------------------------------------------------- 1 | u einat - "einat-ebpf user" - - 2 | -------------------------------------------------------------------------------- /docs/guide/cross.md: -------------------------------------------------------------------------------- 1 | # Cross-Compilation on Debian 2 | 3 | This guide gives example for cross-compiling for aarch64, replace "aarch64" and "arm64" with respective architecture identifier tokens for cross-compiling for other architectures. 4 | 5 | The "aya" loader is used by default. 6 | To enable only the "libbpf" loader, specify Cargo flags `--no-default-features --features aya,pkg-config`. 7 | 8 | ### Build Dependencies 9 | 10 | Install `gcc-aarch64-linux-gnu` for cross linking. Install `clang` for bindgen and compile eBPF C code in this project. 11 | 12 | ``` 13 | apt install gcc-aarch64-linux-gnu clang 14 | # bpftool for BPF object stripping 15 | apt install linux-tools-common 16 | # Or use llvm-strip 17 | apt install llvm 18 | ``` 19 | 20 | Install `rustup` to get Rust>=1.74, see https://www.rust-lang.org/tools/install. Also make sure `rustfmt` is installed as it's used by `libbpf-cargo`. 21 | 22 | Add required target to Rust toolchain: 23 | 24 | ``` 25 | rustup target add aarch64-unknown-linux-gnu 26 | ``` 27 | 28 | ### Target Dependencies 29 | 30 | For "libbpf" loader, you would also need to install `libelf` and `zlib1g` as it's required by `libbpf`. 31 | 32 | ``` 33 | dpkg --add-architecture arm64 34 | apt update 35 | # We only need libbpf headers 36 | apt install libbpf-dev:arm64 37 | # For "libbpf" loader only 38 | apt install libelf-dev:arm64 zlib1g-dev:arm64 39 | ``` 40 | 41 | ### Environment Variables 42 | 43 | ``` 44 | export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER="/usr/bin/aarch64-linux-gnu-gcc" 45 | export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C target-feature=+crt-static -L /usr/lib/aarch64-linux-gnu" 46 | export BINDGEN_EXTRA_CLANG_ARGS_aarch64_unknown_linux_gnu="-I /usr/include/aarch64-linux-gnu" 47 | 48 | # you may additionally set `CC_` and `CFLAGS_` which read by Rust `cc` crate 49 | export CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc" 50 | export CFLAGS_aarch64_unknown_linux_gnu="-I /usr/include/aarch64-linux-gnu -L /usr/lib/aarch64-linux-gnu" 51 | ``` 52 | 53 | Specify `EINAT_BPF_CFLAGS` if einat build script failed to locate libbpf headers. 54 | 55 | ``` 56 | export EINAT_BPF_CFLAGS="-I /usr/include/aarch64-linux-gnu" 57 | ``` 58 | 59 | ### Build static binary 60 | 61 | ``` 62 | cd einat-ebpf 63 | cargo build --target aarch64-unknown-linux-gnu --features static --release 64 | stat ./target/aarch64-unknown-linux-gnu/release/einat 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/guide/openwrt.md: -------------------------------------------------------------------------------- 1 | # OpenWrt 2 | 3 | As said in [README](../../README.md), `einat` requires a kernel with BPF and BTF support enabled which is not the default in OpenWrt. 4 | And `einat` requires running kernel for target architecture has implemented support for BPF-to-BPF calls, which is not the case for MIPS and some other architectures with less maintenance in BPF codebase. 5 | 6 | So if the architecture of your router is not x86-64 or aarch64 or other actively maintained architecture in kernel, your router would mostly not be able to have `einat` working. Unless someone has implemented BPF features `einat` required for the architecture. 7 | 8 | The following is OpenWrt build configs required for `einat` to work. 9 | 10 | > [!IMPORTANT] 11 | > Disable any hardware offload/acceleration solutions before trying out einat. 12 | 13 | ## Build Configs 14 | 15 | Make sure to use latest OpenWrt release or OpenWrt on main branch. 16 | 17 | ``` 18 | CONFIG_KERNEL_DEBUG_KERNEL=y 19 | CONFIG_KERNEL_DEBUG_INFO=y 20 | CONFIG_KERNEL_DEBUG_INFO_REDUCED=n 21 | CONFIG_KERNEL_DEBUG_INFO_BTF=y 22 | ``` 23 | 24 | These translate to kernel configs below 25 | 26 | ``` 27 | CONFIG_DEBUG_KERNEL=y 28 | CONFIG_DEBUG_INFO=y 29 | CONFIG_DEBUG_INFO_REDUCED=n 30 | CONFIG_DEBUG_INFO_BTF=y 31 | ``` 32 | 33 | ## Kernel Configs 34 | 35 | Additional kernel configs required, you might need to add these to kernel config file manually, see https://openwrt.org/docs/guide-developer/toolchain/use-buildsystem#kernel_configuration_optional . 36 | 37 | ``` 38 | CONFIG_BPF_SYSCALL=y 39 | CONFIG_BPF_JIT=y 40 | CONFIG_NET_ACT_BPF=y 41 | 42 | # needed if using bpf_log_level >= 1 43 | CONFIG_BPF_EVENTS=y 44 | ``` 45 | 46 | See https://github.com/iovisor/bcc/blob/master/docs/kernel_config.md for explanation on these BPF options. 47 | 48 | ## Setup einat 49 | 50 | > [!NOTE] 51 | > Alternatively, you can use [openwrt-einat-ebpf](https://github.com/muink/openwrt-einat-ebpf) and [luci-app-einat](https://github.com/muink/luci-app-einat) instead of setting up `einat` manually. 52 | 53 | Find out interface names for your router with `ip addr`, it would be `pppoe-wan` or `wan` for external interface and `br-lan` for internal interface in a common OpenWrt setup. 54 | 55 | Download pre-built binaries for x86_64 or aarch64 from [release page](https://github.com/EHfive/einat-ebpf/releases/latest) or [actions snapshot build](https://github.com/EHfive/einat-ebpf/actions/workflows/build.yml). 56 | 57 | ```shell 58 | # Download build for aarch64 59 | wget -O einat https://github.com/EHfive/einat-ebpf/releases/latest/download/einat-static-aarch64-unknown-linux-musl 60 | chmod +x einat 61 | 62 | # replace pppoe-wan with wan if you are not using PPPoE 63 | einat -i pppoe-wan --hairpin-if lo br-lan 64 | ``` 65 | 66 | You would also need to **disable IP masquerading** for WAN firewall zone and **allow inbound traffic forwarding from WAN to LAN**, that can be done in Luci - Firewall page. 67 | 68 | If this works, you can add an init script to run `einat` as a service, see https://openwrt.org/docs/techref/initscripts. 69 | -------------------------------------------------------------------------------- /docs/guide/use-case.md: -------------------------------------------------------------------------------- 1 | # Use Cases 2 | 3 | The following use cases assume: 4 | 5 | - A Linux network setup with loopback interface `lo`, an internal network interface `lan` and an external network interface `wan` on router. 6 | - An EIM + EIF(Full Cone) external network (like an EIM + EIF CGNAT) or a external network with public IPv4 address on `wan`. 7 | - The firewall is not blocking traffic of interfaces or ports that `einat` interacts. 8 | - `einat` NAPT44 is setup on `wan` and have hairpin routing setup for traffic from `lo` and `lan`, i.e. `einat -i wan --hairpin-if lo lan`. 9 | 10 | And we give the following example network configuration: 11 | 12 | | Host | interface | network | address | note | 13 | | -------- | --------- | ---------------- | --------------- | ---------------------------- | 14 | | Router | `wan` | `233.252.0.0/24` | `233.252.0.200` | Have default route setup | 15 | | Router | `lan` | `192.168.1.1/24` | `192.168.1.1` | Forwarding to lo or wan | 16 | | Device 1 | `eth0` | `192.168.1.1/24` | `192.168.1.100` | Connected with lan on router | 17 | | | | | `192.168.1.200` | Secondary address | 18 | 19 | We call the address of `wan`(`233.252.0.200` here) on router as "external address". 20 | 21 | ## STUN NAT behavior test 22 | 23 | Install [stuntman](https://github.com/jselbie/stunserver) which contains `stunclient` and `stunserver`. 24 | 25 | Test NAT behavior on device 1: 26 | 27 | ```shell 28 | $ stunclient stunserver.stunprotocol.org --mode full --verbosity 1 --protocol udp --localaddr 192.168.1.100 --localport 20000 29 | # or test for TCP 30 | $ stunclient stunserver.stunprotocol.org --mode full --verbosity 1 --protocol tcp --localport 192.168.1.200 --localport 20000 31 | ``` 32 | 33 | It should gives 34 | 35 | ``` 36 | Local address: 192.168.1.100:20000 37 | Mapped address: 233.252.0.200:20000 38 | Behavior test: success 39 | Nat behavior: Endpoint Independent Mapping 40 | Filtering test: success 41 | Nat filtering: Endpoint Independent Filtering 42 | ``` 43 | 44 | Also if you perform NAT behavior test with the same local source port but from a different address in a short gap, the result should also be EIM + EIF and the resulting external port should not be the same with previous test. Otherwise, for a network setup with `einat` behind an external NAT, the external NAT has a fake EIM behavior. 45 | 46 | ```shell 47 | $ stunclient stunserver.stunprotocol.org --mode full --verbosity 1 --protocol udp --localaddr 192.168.1.200 --localport 20000 48 | Local address: 192.168.1.200:20000 49 | Mapped address: 233.252.0.200:[20001] <- this should changes 50 | Behavior test: success 51 | Nat behavior: Endpoint Independent Mapping 52 | Filtering test: success 53 | Nat filtering: Endpoint Independent Filtering 54 | ``` 55 | 56 | ## STUN-based port mapping with Natter 57 | 58 | [Natter](https://github.com/MikeWang000000/Natter) is a STUN-based port mapping daemon, you can use this tool to hold an external TCP/UDP port and forwarding the traffic to a specified target(e.g. a local TCP listening service). 59 | 60 | Run Natter on device 1 with test forwarder. 61 | 62 | ```shell 63 | $ python natter.py -b 20000 -m test 64 | [I] Natter v2.0.0 65 | [I] 66 | [I] tcp://192.168.1.100:20000 <--Natter--> tcp://233.252.0.200:20000 67 | [I] 68 | [I] Test mode in on. 69 | [I] Please check [ http://233.252.0.200:20000 ] 70 | ... 71 | 72 | # curl on test HTTP service served on the external port should gives: 73 | $ curl http://233.252.0.200:20000 74 |

It works!


Natter 75 | ``` 76 | 77 | Similar work can also be done with [natmap](https://github.com/heiher/natmap). 78 | 79 | You can optionally start a STUN server for port mapping on private external address, which is not reachable(i.e. behind an external NAT) by public STUN server. With hairpin routing setup, we can reach the STUN server with external address. Note the STUN server port(default is `3478`) should be excluded from NAT port ranges that `einat` uses. 80 | 81 | Start STUN server on router: 82 | 83 | ```shell 84 | stunserver --protocol tcp 85 | ``` 86 | 87 | Run Natter with router STUN server specified on device 1: 88 | 89 | ```shell 90 | python natter.py -b 20001 -m test -s 233.252.0.200 91 | ``` 92 | 93 | ## WireGuard VPN "server" 94 | 95 | > [!NOTE] 96 | > This section does not follow example network configuration above. 97 | 98 | For WireGuard peer in server role, commonly we setup private IP on WireGuard interface(i.e. `wg0`) and do iptables/nftables masquerading for traffic forwarding between WireGuard interface and default external interface from which can reach public Internet. Example nftables rules below. 99 | 100 | ```nft 101 | table ip nat { 102 | chain postrouting { 103 | type nat hook postrouting priority srcnat; policy accept; 104 | iifname wg0 oifname eth0 masquerade 105 | } 106 | } 107 | ``` 108 | 109 | However this only gives WireGuard "client" connecting this "server" an Address and Port-Dependent Mapping (with Endpoint-Independent Mapping characteristic[^netfilter-behavior]) and Address and Port-Dependent Filtering network environment that provided by Netfilter NAT system. 110 | 111 | [^netfilter-behavior]: https://eh5.me/zh-cn/blog/nat-behavior-of-netfilter/ 112 | 113 | To have Endpoint-Independent Mapping and Endpoint-Independent Filtering NAT behavior for traffic forwarding between WireGuard "client" and external interface on WireGuard "server", you can use `einat` instead of iptables/nftables masquerading. Also make sure iptables/nftables masquerade rules like above are removed if you are migrating from iptables/nftables masquerade to `einat`. 114 | 115 | ``` 116 | $ sudo einat -i eth0 --nat44 --ports 20000-29999 --hairpin-if wg0 --internal 117 | ``` 118 | 119 | You might also want to enable NAT66 for IPv6 with `--nat66` flag if you assign private IPv6 addresses instead of public IPv6 addresses to WireGuard peers. 120 | 121 | Note if you are hosting TCP/UDP services on same server, you might want to tweak external ports that `einat` uses so inbound initiated traffic towards unoccupied external ports are accepted. For example, the listening port of WireGuard service should not in range of external ports that `einat` uses(e.g. the default 20000-29999), tweak the port setting of either `einat` or WireGuard so inbound initiated traffic toward WireGuard service are accepted without filtering by `einat`. 122 | -------------------------------------------------------------------------------- /docs/reference/packet-flow.md: -------------------------------------------------------------------------------- 1 | # Packet Flow 2 | 3 | The diagram below briefly shows how `einat` works. 4 | 5 | ![einat flow](../assets/einat.png) 6 | 7 | ## NAT filtering 8 | 9 | | Config Item | Example | Note | 10 | | ---------------- | ------------------------- | ------------------------------------- | 11 | | External address | 233.252.0.200 | An address on external interface(WAN) | 12 | | External ports | 233.252.0.200:20000-29999 | Ports "managed" by `einat` | 13 | 14 | ### Egress 15 | 16 | For any layer 4 packets going through egress hook of `einat`, if either the source address is not external address or the source ports is managed external ports, `einat` would SNAT the original source address and port to external address and port. And the corresponding NAT binding record and CT record would be added or updated to record tables. 17 | 18 | For any other egress packets not been SNATed, `einat` would either passthrough the packet or redirect to ingress if [hairpinning](#hairpinning) condition matches. 19 | 20 | ### Ingress 21 | 22 | For any layer 4 packets going through ingress hook of `einat`, if there is a previous NAT binding record found, `einat` would DNAT the destination address and port back to the internal source and port per binding record found. 23 | 24 | And if a NAT binding is not found but it's a ICMP packet, an inbound initialed binding record would be created and DNAT would be performed correspondingly. 25 | 26 | Also the corresponding CT record would be added or updated. 27 | 28 | ## NAT records 29 | 30 | There is 2 types of records that `einat` uses for tracking NAT state, binding record and conntrack(CT) record, which aligns "Binding Information Bases" record and "Session Tables" record defined in [RFC 6146](https://datatracker.ietf.org/doc/html/rfc6146#section-3) respectively. 31 | 32 | ### Binding record 33 | 34 | The binding record tracks port mapping of `(internal source address, internal source address) <--> (external source address, external source port)`, which is going to be used for NAT. The binding record also has a `use` counter for tracking outbound CTs referencing it and a `ref` counter for tracking any inbound or outbound CTs referencing it. 35 | 36 | A new inbound CT record can only be created if the binding has outbound CTs referencing it, i.e. the `use` counter is not zero. 37 | 38 | If the `ref` counter becomes zero, the binding would be deleted. 39 | 40 | ### Conntrack(CT) record 41 | 42 | The CT record tracks connection of `(internal source address, internal source address, destination address and port) <--> (external source address, external source port, destination address and port)`, and the state and lifetime of the connection. 43 | 44 | An inbound CT is a CT initialed from ingress and an outbound CT is a CT initiated from egress. An inbound CT upgrades to outbound CT if there is valid traffic for the connection that inbound CT tracks going through egress. 45 | 46 | The CT state and lifetime changes based on traffic direction and packet type(e.g. TCP SYN, TCP RESET ...). 47 | 48 | 49 | 50 | Note that neither binding record or CT record are dependent to Netfilter conntrack system, they are independently maintained by `einat` which is independent to Netfilter conntracks. 51 | 52 | ## Hairpinning 53 | 54 | If the destination address of an outbound packet is an external address that is configured to do hairpinning, the packet would be redirected to ingress hook from egress hook. And the relevant SNAT and DNAT would both be performed hence allowing devices in internal network to reach out others with destination of external address and port. 55 | 56 | By default linux would create local routing entry to route packets towards external address locally without going through external interface, this prevent `einat` from performing `SNAT` to translate the source to the external address so destination can reply back and performing `DNAT` to translate external address to the proper internal address. 57 | 58 | Thus in user land, the `einat` adds additional `ip rule` to bypass the local routing for external address in outbound direction, see https://github.com/EHfive/einat-ebpf/issues/4. 59 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": [ 6 | "systems" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1731533236, 11 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 12 | "owner": "numtide", 13 | "repo": "flake-utils", 14 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "numtide", 19 | "repo": "flake-utils", 20 | "type": "github" 21 | } 22 | }, 23 | "naersk": { 24 | "inputs": { 25 | "nixpkgs": [ 26 | "nixpkgs" 27 | ] 28 | }, 29 | "locked": { 30 | "lastModified": 1743800763, 31 | "narHash": "sha256-YFKV+fxEpMgP5VsUcM6Il28lI0NlpM7+oB1XxbBAYCw=", 32 | "owner": "nix-community", 33 | "repo": "naersk", 34 | "rev": "ed0232117731a4c19d3ee93aa0c382a8fe754b01", 35 | "type": "github" 36 | }, 37 | "original": { 38 | "owner": "nix-community", 39 | "repo": "naersk", 40 | "type": "github" 41 | } 42 | }, 43 | "nixpkgs": { 44 | "locked": { 45 | "lastModified": 1744536153, 46 | "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", 47 | "owner": "NixOS", 48 | "repo": "nixpkgs", 49 | "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "NixOS", 54 | "ref": "nixpkgs-unstable", 55 | "repo": "nixpkgs", 56 | "type": "github" 57 | } 58 | }, 59 | "root": { 60 | "inputs": { 61 | "flake-utils": "flake-utils", 62 | "naersk": "naersk", 63 | "nixpkgs": "nixpkgs", 64 | "rust-overlay": "rust-overlay", 65 | "systems": "systems" 66 | } 67 | }, 68 | "rust-overlay": { 69 | "inputs": { 70 | "nixpkgs": [ 71 | "nixpkgs" 72 | ] 73 | }, 74 | "locked": { 75 | "lastModified": 1744599145, 76 | "narHash": "sha256-yzaDPkJwZdUtRj/dzdOeB74yryWzpngYaD7BedqFKk8=", 77 | "owner": "oxalica", 78 | "repo": "rust-overlay", 79 | "rev": "fd6795d3d28f956de01a0458b6fa7baae5c793b4", 80 | "type": "github" 81 | }, 82 | "original": { 83 | "owner": "oxalica", 84 | "repo": "rust-overlay", 85 | "type": "github" 86 | } 87 | }, 88 | "systems": { 89 | "locked": { 90 | "lastModified": 1689347949, 91 | "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", 92 | "owner": "nix-systems", 93 | "repo": "default-linux", 94 | "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", 95 | "type": "github" 96 | }, 97 | "original": { 98 | "owner": "nix-systems", 99 | "repo": "default-linux", 100 | "type": "github" 101 | } 102 | } 103 | }, 104 | "root": "root", 105 | "version": 7 106 | } 107 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | systems.url = "github:nix-systems/default-linux"; 5 | flake-utils = { 6 | url = "github:numtide/flake-utils"; 7 | inputs.systems.follows = "systems"; 8 | }; 9 | rust-overlay = { 10 | url = "github:oxalica/rust-overlay"; 11 | inputs.nixpkgs.follows = "nixpkgs"; 12 | }; 13 | naersk = { 14 | url = "github:nix-community/naersk"; 15 | inputs.nixpkgs.follows = "nixpkgs"; 16 | }; 17 | }; 18 | 19 | outputs = 20 | { 21 | self, 22 | nixpkgs, 23 | flake-utils, 24 | rust-overlay, 25 | naersk, 26 | ... 27 | }: 28 | let 29 | defaultPackage' = 30 | pkgs: 31 | pkgs.callPackage ./nix/package.nix { 32 | naersk = pkgs.callPackage naersk { }; 33 | }; 34 | crossPackage' = import ./nix/cross-package.nix { inherit naersk; }; 35 | 36 | overlay = final: prev: { 37 | einat = defaultPackage' prev; 38 | }; 39 | 40 | cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); 41 | 42 | verifyMsrvArgs = { 43 | rustVersion = nixpkgs.lib.versions.pad 3 cargoToml."package"."rust-version"; 44 | enableStatic = true; 45 | enableIpv6 = true; 46 | }; 47 | 48 | module = { 49 | imports = [ 50 | (import ./nix/module.nix) 51 | { 52 | nixpkgs.overlays = [ overlay ]; 53 | } 54 | ]; 55 | }; 56 | in 57 | flake-utils.lib.eachDefaultSystem ( 58 | system: 59 | let 60 | pkgs = (import nixpkgs) { 61 | inherit system; 62 | overlays = [ rust-overlay.overlays.default ]; 63 | }; 64 | 65 | defaultPackage = defaultPackage' ( 66 | (import nixpkgs) { 67 | inherit system; 68 | } 69 | ); 70 | crossPackage = { ... }@args: crossPackage' ({ inherit pkgs; } // args); 71 | in 72 | { 73 | legacyPackages = (import nixpkgs) { 74 | inherit system; 75 | overlays = [ overlay ]; 76 | }; 77 | 78 | packages = { 79 | default = defaultPackage; 80 | ipv6 = defaultPackage; 81 | 82 | verify_msrv_ipv6_static-x86_64-unknown-linux-musl = crossPackage ( 83 | { 84 | crossPkgs = pkgs.pkgsCross.musl64; 85 | } 86 | // verifyMsrvArgs 87 | ); 88 | verify_msrv_ipv6_static-aarch64-unknown-linux-musl = crossPackage ( 89 | { 90 | crossPkgs = pkgs.pkgsCross.aarch64-multiplatform-musl; 91 | } 92 | // verifyMsrvArgs 93 | ); 94 | 95 | static-x86_64-unknown-linux-musl = crossPackage { 96 | crossPkgs = pkgs.pkgsCross.musl64; 97 | enableStatic = true; 98 | }; 99 | static-i686-unknown-linux-musl = crossPackage { 100 | crossPkgs = pkgs.pkgsCross.musl32; 101 | enableStatic = true; 102 | }; 103 | static-aarch64-unknown-linux-musl = crossPackage { 104 | crossPkgs = pkgs.pkgsCross.aarch64-multiplatform-musl; 105 | enableStatic = true; 106 | }; 107 | 108 | ipv6_static-x86_64-unknown-linux-musl = crossPackage { 109 | crossPkgs = pkgs.pkgsCross.musl64; 110 | enableStatic = true; 111 | enableIpv6 = true; 112 | }; 113 | ipv6_static-i686-unknown-linux-musl = crossPackage { 114 | crossPkgs = pkgs.pkgsCross.musl32; 115 | enableStatic = true; 116 | enableIpv6 = true; 117 | }; 118 | ipv6_static-aarch64-unknown-linux-musl = crossPackage { 119 | crossPkgs = pkgs.pkgsCross.aarch64-multiplatform-musl; 120 | enableStatic = true; 121 | enableIpv6 = true; 122 | }; 123 | }; 124 | } 125 | ) 126 | // { 127 | overlays.default = overlay; 128 | nixosModules.default = module; 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /nix/cross-package.nix: -------------------------------------------------------------------------------- 1 | { naersk }: 2 | { 3 | pkgs, 4 | crossPkgs ? pkgs, 5 | targetTriple ? crossPkgs.hostPlatform.config, 6 | enableStatic ? false, 7 | enableIpv6 ? false, 8 | rustVersion ? "latest" 9 | }: 10 | let 11 | inherit (pkgs) lib system; 12 | targetUnderscore = lib.replaceStrings [ "-" ] [ "_" ] targetTriple; 13 | targetUnderscoreUpper = lib.toUpper targetUnderscore; 14 | 15 | toolchain = pkgs.rust-bin.stable.${rustVersion}.minimal.override { 16 | targets = [ targetTriple ]; 17 | }; 18 | 19 | naersk' = naersk.lib.${system}.override { 20 | cargo = toolchain; 21 | rustc = toolchain; 22 | }; 23 | 24 | crossCC = "${crossPkgs.stdenv.cc}/bin/${crossPkgs.stdenv.cc.targetPrefix}cc"; 25 | 26 | buildInputs = with crossPkgs; [ 27 | ## runtime dependencies on target platform 28 | stdenv.cc.libc 29 | ]; 30 | 31 | buildInputsSearchFlags = map (dep: "-L${lib.getLib dep}/lib") buildInputs; 32 | in 33 | naersk'.buildPackage { 34 | src = ../.; 35 | gitSubmodules = true; 36 | nativeBuildInputs = with pkgs; [ 37 | pkg-config 38 | 39 | # compile BPF C code 40 | llvmPackages.clang-unwrapped 41 | bpftools 42 | ]; 43 | inherit buildInputs; 44 | strictDeps = true; 45 | 46 | cargoBuildOptions = 47 | orig: 48 | orig 49 | ++ lib.optionals enableStatic [ 50 | "--features static" 51 | ] 52 | ++ lib.optionals enableIpv6 [ 53 | "--features ipv6" 54 | ]; 55 | 56 | CARGO_BUILD_TARGET = targetTriple; 57 | 58 | NIX_CFLAGS_COMPILE = lib.optionals (enableStatic && crossPkgs.hostPlatform.isAarch) [ 59 | "-mno-outline-atomics" 60 | ]; 61 | 62 | LIBBPF_NO_PKG_CONFIG = 1; 63 | EINAT_BPF_CFLAGS = "-I${pkgs.libbpf}/include"; 64 | 65 | "CC_${targetUnderscore}" = crossCC; 66 | "CARGO_TARGET_${targetUnderscoreUpper}_LINKER" = crossCC; 67 | 68 | "CARGO_TARGET_${targetUnderscoreUpper}_RUSTFLAGS" = lib.concatStringsSep " " ( 69 | [ 70 | "-C target-feature=${if enableStatic then "+" else "-"}crt-static" 71 | ] 72 | ++ buildInputsSearchFlags 73 | ); 74 | 75 | preBuild = '' 76 | # Avoid adding host dependencies to CFLAGS and LDFLAGS for build platform 77 | if [[ ${pkgs.stdenv.cc.suffixSalt} != ${crossPkgs.stdenv.cc.suffixSalt} ]]; then 78 | export NIX_CC_WRAPPER_TARGET_HOST_${pkgs.stdenv.cc.suffixSalt}=""; 79 | export NIX_BINTOOLS_WRAPPER_TARGET_HOST_${pkgs.stdenv.cc.suffixSalt}=""; 80 | fi 81 | ''; 82 | 83 | doCheck = crossPkgs.system == pkgs.system; 84 | } 85 | -------------------------------------------------------------------------------- /nix/module.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, ... }: 2 | with lib; 3 | let 4 | cfg = config.services.einat; 5 | configFormat = pkgs.formats.toml { }; 6 | configFile = 7 | if cfg.configFile != null 8 | then cfg.configFile 9 | else configFormat.generate "config.toml" cfg.config; 10 | in 11 | { 12 | options.services.einat = { 13 | enable = mkEnableOption "einat service"; 14 | package = mkPackageOption pkgs "einat" { default = [ "einat" ]; }; 15 | configFile = mkOption { 16 | type = types.nullOr types.path; 17 | default = null; 18 | description = "The absolute path to the configuration file."; 19 | }; 20 | config = mkOption { 21 | type = configFormat.type; 22 | default = { }; 23 | description = "The configuration attribute set."; 24 | }; 25 | }; 26 | 27 | config = mkIf cfg.enable { 28 | assertions = [{ 29 | assertion = (cfg.configFile != null) -> (cfg.config == { }); 30 | message = "Either but not both `configFile` and `config` should be specified for einat."; 31 | }]; 32 | 33 | systemd.services.einat = { 34 | description = "einat service"; 35 | after = [ "network.target" ]; 36 | wantedBy = [ "multi-user.target" ]; 37 | restartTriggers = [ configFile ]; 38 | serviceConfig = { 39 | ExecStart = "${cfg.package}/bin/einat -c ${configFile}"; 40 | }; 41 | }; 42 | 43 | environment.systemPackages = [ cfg.package ]; 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /nix/package.nix: -------------------------------------------------------------------------------- 1 | { 2 | naersk, 3 | lib, 4 | rustPlatform, 5 | pkg-config, 6 | llvmPackages, 7 | bpftools, 8 | libbpf, 9 | elfutils, 10 | zlib, 11 | enableIpv6 ? true, 12 | }: 13 | naersk.buildPackage { 14 | src = ../.; 15 | 16 | nativeBuildInputs = [ 17 | pkg-config 18 | llvmPackages.clang-unwrapped 19 | bpftools 20 | rustPlatform.bindgenHook 21 | ]; 22 | 23 | buildInputs = [ 24 | libbpf 25 | elfutils 26 | zlib 27 | ]; 28 | 29 | buildFeatures = [ 30 | "aya" 31 | "libbpf" 32 | ] ++ lib.optionals enableIpv6 [ "ipv6" ]; 33 | 34 | meta = with lib; { 35 | description = "An eBPF-based Endpoint-Independent(Full Cone) NAT"; 36 | homepage = "https://github.com/EHfive/einat-ebpf"; 37 | license = licenses.gpl2Only; 38 | platforms = platforms.linux; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/bpf/bpf_log.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | #ifndef __LOG_H__ 4 | #define __LOG_H__ 5 | 6 | #include 7 | 8 | enum bpf_log_level { 9 | BPF_LOG_LEVEL_NONE = 0, 10 | BPF_LOG_LEVEL_ERROR, 11 | BPF_LOG_LEVEL_WARN, 12 | BPF_LOG_LEVEL_INFO, 13 | BPF_LOG_LEVEL_DEBUG, 14 | BPF_LOG_LEVEL_TRACE, 15 | BPF_LOG_LEVEL_END, 16 | }; 17 | #define _BPF_LOG_LEVEL_ERROR_TOKEN "ERROR" 18 | #define _BPF_LOG_LEVEL_WARN_TOKEN "WARN " 19 | #define _BPF_LOG_LEVEL_INFO_TOKEN "INFO " 20 | #define _BPF_LOG_LEVEL_DEBUG_TOKEN "DEBUG" 21 | #define _BPF_LOG_LEVEL_TRACE_TOKEN "TRACE" 22 | 23 | // can be overwritten with #undef and re #define on the same name 24 | #define BPF_LOG_LEVEL BPF_LOG_LEVEL_DEBUG; 25 | #define BPF_LOG_TOPIC "default" 26 | 27 | #define _bpf_vprintk_exists \ 28 | bpf_core_enum_value_exists(enum bpf_func_id, BPF_FUNC_trace_vprintk) 29 | 30 | #define _bpf_check_printk(...) \ 31 | ___bpf_nth(_, ##__VA_ARGS__, _bpf_vprintk_exists, _bpf_vprintk_exists, \ 32 | _bpf_vprintk_exists, _bpf_vprintk_exists, _bpf_vprintk_exists, \ 33 | _bpf_vprintk_exists, _bpf_vprintk_exists, _bpf_vprintk_exists, \ 34 | _bpf_vprintk_exists, true /*3*/, true /*2*/, true /*1*/, \ 35 | true /*0*/) 36 | 37 | #define _bpf_log_logv(level, fmt, args...) \ 38 | ({ \ 39 | if (BPF_LOG_LEVEL >= level && _bpf_check_printk(args)) { \ 40 | bpf_printk("[einat][" _##level##_TOKEN "] " BPF_LOG_TOPIC \ 41 | " : " fmt, \ 42 | ##args); \ 43 | } \ 44 | }) 45 | 46 | #define bpf_log_error(args...) _bpf_log_logv(BPF_LOG_LEVEL_ERROR, args) 47 | #define bpf_log_warn(args...) _bpf_log_logv(BPF_LOG_LEVEL_WARN, args) 48 | #define bpf_log_info(args...) _bpf_log_logv(BPF_LOG_LEVEL_INFO, args) 49 | #define bpf_log_debug(args...) _bpf_log_logv(BPF_LOG_LEVEL_DEBUG, args) 50 | #define bpf_log_trace(args...) _bpf_log_logv(BPF_LOG_LEVEL_TRACE, args) 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /src/bpf/einat.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | #ifndef __bpf__ 5 | #define __bpf__ 6 | #endif 7 | 8 | #include "kernel/vmlinux.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "bpf_log.h" 15 | 16 | #ifndef FEAT_IPV6 17 | // #define FEAT_IPV6 18 | #endif 19 | 20 | // #include 21 | #define ETH_P_IP 0x0800 22 | #define ETH_P_IPV6 0x86DD 23 | 24 | #define IP_CE 0x8000 /* Flag: "Congestion" */ 25 | #define IP_DF 0x4000 /* Flag: "Don't Fragment" */ 26 | #define IP_MF 0x2000 /* Flag: "More Fragments" */ 27 | #define IP_OFFSET 0x1FFF /* "Fragment Offset" part */ 28 | 29 | // Maximum number of extension headers with nexthdr field 30 | #define MAX_IPV6_EXT_NUM 5 31 | 32 | /* 33 | * NextHeader field of IPv6 header 34 | */ 35 | 36 | #define NEXTHDR_HOP 0 /* Hop-by-hop option header. */ 37 | #define NEXTHDR_ROUTING 43 /* Routing header. */ 38 | #define NEXTHDR_FRAGMENT 44 /* Fragmentation/reassembly header. */ 39 | #define NEXTHDR_AUTH 51 /* Authentication header. */ 40 | #define NEXTHDR_DEST 60 /* Destination options header. */ 41 | 42 | #define NEXTHDR_TCP 6 /* TCP segment. */ 43 | #define NEXTHDR_UDP 17 /* UDP message. */ 44 | #define NEXTHDR_ICMP 58 /* ICMP for IPv6. */ 45 | #define NEXTHDR_NONE 59 /* No next header */ 46 | #define NEXTHDR_SCTP 132 /* SCTP message. */ 47 | 48 | #define IPV6_FRAG_OFFSET 0xFFF8 49 | #define IPV6_FRAG_MF 0x0001 50 | 51 | // #include 52 | #define ICMP_DEST_UNREACH 3 /* Destination Unreachable */ 53 | #define ICMP_TIME_EXCEEDED 11 /* Time Exceeded */ 54 | #define ICMP_PARAMETERPROB 12 /* Parameter Problem */ 55 | 56 | #define ICMP_ECHOREPLY 0 /* Echo Reply */ 57 | #define ICMP_ECHO 8 /* Echo Request */ 58 | #define ICMP_TIMESTAMP 13 /* Timestamp Request */ 59 | #define ICMP_TIMESTAMPREPLY 14 /* Timestamp Reply */ 60 | 61 | // #include 62 | #define ICMPV6_DEST_UNREACH 1 63 | #define ICMPV6_PKT_TOOBIG 2 64 | #define ICMPV6_TIME_EXCEED 3 65 | #define ICMPV6_PARAMPROB 4 66 | 67 | #define ICMPV6_ECHO_REQUEST 128 68 | #define ICMPV6_ECHO_REPLY 129 69 | 70 | #define AF_INET 2 71 | #define AF_INET6 10 72 | 73 | #define CLOCK_MONOTONIC 1 74 | 75 | // #include 76 | #define TC_ACT_UNSPEC (-1) 77 | #define TC_ACT_OK 0 78 | #define TC_ACT_SHOT 2 79 | 80 | #define BPF_LOOP_RET_CONTINUE 0 81 | #define BPF_LOOP_RET_BREAK 1 82 | 83 | union u_inet_addr { 84 | #ifdef FEAT_IPV6 85 | __u32 all[4]; 86 | __be32 ip; 87 | __be32 ip6[4]; 88 | #else 89 | __u32 all[1]; 90 | __be32 ip; 91 | #endif 92 | }; 93 | 94 | struct inet_tuple { 95 | union u_inet_addr saddr; 96 | union u_inet_addr daddr; 97 | __be16 sport; 98 | __be16 dport; 99 | }; 100 | 101 | struct ipv4_lpm_key { 102 | u32 prefixlen; 103 | __be32 ip; 104 | }; 105 | 106 | struct ipv6_lpm_key { 107 | u32 prefixlen; 108 | __be32 ip6[4]; 109 | }; 110 | 111 | struct port_range { 112 | u16 begin_port; 113 | u16 end_port; 114 | }; 115 | 116 | // Make sure it's 2-powered 117 | #define MAX_PORT_RANGES (1 << 2) 118 | #define MAX_PORT_RANGES_MASK (MAX_PORT_RANGES - 1) 119 | // We do random port collision test in unrolled loop on Linux kernel without 120 | // bpf_loop support 121 | #define MAX_PORT_COLLISION_TRIES 32 122 | 123 | struct external_config { 124 | struct port_range tcp_range[MAX_PORT_RANGES]; 125 | struct port_range udp_range[MAX_PORT_RANGES]; 126 | struct port_range icmp_range[MAX_PORT_RANGES]; 127 | // icmp_in_range and icmp_out_range can overlaps but must both be 128 | // included by icmp_range 129 | struct port_range icmp_in_range[MAX_PORT_RANGES]; 130 | struct port_range icmp_out_range[MAX_PORT_RANGES]; 131 | u8 tcp_range_len; 132 | u8 udp_range_len; 133 | u8 icmp_range_len; 134 | u8 icmp_in_range_len; 135 | u8 icmp_out_range_len; 136 | #define EXTERNAL_IS_INTERNAL_FLAG (1 << 0) 137 | #define EXTERNAL_NO_SNAT_FLAG (1 << 1) 138 | u8 flags; 139 | }; 140 | 141 | struct dest_config { 142 | #define DEST_HAIRPIN_FLAG (1 << 0) 143 | #define DEST_NO_SNAT_FLAG (1 << 1) 144 | u8 flags; 145 | }; 146 | 147 | #define BINDING_ORIG_DIR_FLAG (1 << 0) 148 | #define FRAG_TRACK_EGRESS_FLAG (1 << 0) 149 | #define ADDR_IPV4_FLAG (1 << 1) 150 | #define ADDR_IPV6_FLAG (1 << 2) 151 | 152 | // NOTE: all map key structs have to explicitly padded and the padding fields 153 | // need to be zeroed 154 | 155 | struct map_frag_track_key { 156 | u32 ifindex; 157 | u8 flags; 158 | u8 l4proto; 159 | u16 _pad; 160 | u32 id; 161 | union u_inet_addr saddr; 162 | union u_inet_addr daddr; 163 | }; 164 | 165 | struct map_frag_track_value { 166 | __be16 sport; 167 | __be16 dport; 168 | u32 _pad; 169 | struct bpf_timer timer; 170 | }; 171 | 172 | // If BINDING_ORIG_DIR_FLAG is set, "from" is internal source address and "to" 173 | // is mapped external source address, otherwise the relations are reversed. 174 | // We duplicate binding entries for both direction for looking up from both 175 | // ingress and egress. 176 | struct map_binding_key { 177 | u32 ifindex; 178 | u8 flags; 179 | u8 l4proto; 180 | // ICMP ID in the case of ICMP 181 | __be16 from_port; 182 | union u_inet_addr from_addr; 183 | }; 184 | 185 | struct map_binding_value { 186 | union u_inet_addr to_addr; 187 | __be16 to_port; 188 | u8 flags; 189 | u8 is_static; 190 | // We only do binding ref counting on inbound direction, i.e. no 191 | // BINDING_ORIG_DIR_FLAG on binding key 192 | u32 use; 193 | u32 ref; 194 | u32 seq; 195 | }; 196 | 197 | // Set ref of orig dir binding to this to indicate the binding was ref counted 198 | // by at least one CT so it can be deleted in timer callback, otherwise the ref 199 | // is 0 and the binding is subject to be reused in find_port_cb 200 | #define BINDING_ORIG_REF_COUNTED ((u32)-1) 201 | 202 | struct map_ct_key { 203 | u32 ifindex; 204 | u8 flags; 205 | u8 l4proto; 206 | u16 _pad; 207 | struct inet_tuple external; 208 | }; 209 | 210 | // Adapted from NAT64 TCP state machine per RFC6146 211 | enum ct_state { 212 | // CT_CLOSED, 213 | CT_INIT_IN, 214 | CT_INIT_OUT, 215 | CT_ESTABLISHED, 216 | CT_TRANS, 217 | CT_FIN_IN, 218 | CT_FIN_OUT, 219 | CT_FIN_IN_OUT, 220 | }; 221 | 222 | struct map_ct_value { 223 | struct inet_tuple origin; 224 | u8 flags; 225 | u8 _pad[3]; 226 | u32 state; 227 | u32 seq; 228 | struct bpf_timer timer; 229 | }; 230 | 231 | #define COPY_ADDR6(t, s) (__builtin_memcpy((t), (s), sizeof(t))) 232 | 233 | static __always_inline bool inet_addr_equal(const union u_inet_addr *a, 234 | const union u_inet_addr *b) { 235 | #ifdef FEAT_IPV6 236 | return a->all[0] == b->all[0] && a->all[1] == b->all[1] && 237 | a->all[2] == b->all[2] && a->all[3] == b->all[3]; 238 | #else 239 | return a->ip == b->ip; 240 | #endif 241 | } 242 | 243 | static __always_inline void inet_addr_set_ip(union u_inet_addr *addr, 244 | __be32 ip) { 245 | addr->ip = ip; 246 | #ifdef FEAT_IPV6 247 | addr->all[1] = 0; 248 | addr->all[2] = 0; 249 | addr->all[3] = 0; 250 | #endif 251 | } 252 | 253 | #ifdef FEAT_IPV6 254 | static __always_inline void inet_addr_set_ip6(union u_inet_addr *addr, 255 | __be32 ip6[4]) { 256 | COPY_ADDR6(addr->ip6, ip6); 257 | } 258 | #endif 259 | 260 | static __always_inline void inet_tuple_copy(struct inet_tuple *t1, 261 | const struct inet_tuple *t2) { 262 | 263 | COPY_ADDR6(t1->saddr.all, t2->saddr.all); 264 | COPY_ADDR6(t1->daddr.all, t2->daddr.all); 265 | t1->sport = t2->sport; 266 | t1->dport = t2->dport; 267 | } 268 | 269 | static __always_inline void inet_tuple_rev_copy(struct inet_tuple *t1, 270 | const struct inet_tuple *t2) { 271 | 272 | COPY_ADDR6(t1->saddr.all, t2->daddr.all); 273 | COPY_ADDR6(t1->daddr.all, t2->saddr.all); 274 | t1->sport = t2->dport; 275 | t1->dport = t2->sport; 276 | } 277 | 278 | static __always_inline void 279 | binding_value_to_key(u32 ifindex, u8 flags, u8 l4proto, 280 | const struct map_binding_value *val, 281 | struct map_binding_key *key_rev) { 282 | key_rev->ifindex = ifindex; 283 | key_rev->flags = (val->flags & (~BINDING_ORIG_DIR_FLAG)) | flags; 284 | key_rev->l4proto = l4proto; 285 | key_rev->from_port = val->to_port; 286 | COPY_ADDR6(key_rev->from_addr.all, val->to_addr.all); 287 | } 288 | 289 | static __always_inline void 290 | get_rev_dir_binding_key(const struct map_binding_key *key, 291 | const struct map_binding_value *val, 292 | struct map_binding_key *key_rev) { 293 | binding_value_to_key( 294 | key->ifindex, 295 | ((key->flags & BINDING_ORIG_DIR_FLAG) ^ BINDING_ORIG_DIR_FLAG), 296 | key->l4proto, val, key_rev); 297 | } 298 | 299 | enum { RANGE_ALL, RANGE_INBOUND, RANGE_OUTBOUND }; 300 | 301 | static __always_inline u8 select_port_range(struct external_config *ext_config, 302 | u8 l4proto, u8 range_variant, 303 | struct port_range **proto_range) { 304 | switch (l4proto) { 305 | case IPPROTO_TCP: 306 | *proto_range = ext_config->tcp_range; 307 | return ext_config->tcp_range_len; 308 | case IPPROTO_UDP: 309 | *proto_range = ext_config->udp_range; 310 | return ext_config->udp_range_len; 311 | case IPPROTO_ICMP: 312 | case NEXTHDR_ICMP: 313 | switch (range_variant) { 314 | case RANGE_ALL: 315 | *proto_range = ext_config->icmp_range; 316 | return ext_config->icmp_range_len; 317 | case RANGE_INBOUND: 318 | *proto_range = ext_config->icmp_in_range; 319 | return ext_config->icmp_in_range_len; 320 | case RANGE_OUTBOUND: 321 | *proto_range = ext_config->icmp_out_range; 322 | return ext_config->icmp_out_range_len; 323 | default: 324 | __bpf_unreachable(); 325 | } 326 | } 327 | return 0; 328 | } 329 | 330 | static __always_inline int 331 | find_port_range_idx(u16 port, u8 range_len, 332 | const struct port_range range_list[MAX_PORT_RANGES]) { 333 | #pragma unroll 334 | for (int i = 0; i < MAX_PORT_RANGES; i++) { 335 | // somehow unroll would fail without this barrier 336 | barrier(); 337 | if (i >= range_len) { 338 | break; 339 | } 340 | const struct port_range *range = &range_list[i]; 341 | 342 | if (port >= range->begin_port && port <= range->end_port) { 343 | return i; 344 | } 345 | } 346 | return -1; 347 | } 348 | 349 | static __always_inline int get_l3_to_addr_off(bool is_ipv4, bool is_source) { 350 | return is_source ? (is_ipv4 ? offsetof(struct iphdr, saddr) 351 | : offsetof(struct ipv6hdr, saddr)) 352 | : (is_ipv4 ? offsetof(struct iphdr, daddr) 353 | : offsetof(struct ipv6hdr, daddr)); 354 | } 355 | 356 | static __always_inline int bpf_write_inet_addr(struct __sk_buff *skb, 357 | bool is_ipv4, int addr_off, 358 | union u_inet_addr *to_addr) { 359 | return bpf_skb_store_bytes( 360 | skb, addr_off, is_ipv4 ? &to_addr->ip : to_addr->all, 361 | is_ipv4 ? sizeof(to_addr->ip) : sizeof(to_addr->all), 0); 362 | } 363 | 364 | static __always_inline int bpf_write_port(struct __sk_buff *skb, int port_off, 365 | __be16 to_port) { 366 | return bpf_skb_store_bytes(skb, port_off, &to_port, sizeof(to_port), 0); 367 | } 368 | 369 | // copied from cilium, see 370 | // https://github.com/cilium/cilium/commit/847014aa62f94e5a53178670cad1eacea455b227 371 | #define DEFINE_FUNC_CTX_POINTER(FIELD) \ 372 | static __always_inline void *ctx_##FIELD(const struct __sk_buff *ctx) { \ 373 | u8 *ptr; \ 374 | \ 375 | /* LLVM may generate u32 assignments of \ 376 | * ctx->{data,data_end,data_meta}. With this inline asm, LLVM loses \ 377 | * track of the fact this field is on 32 bits. \ 378 | */ \ 379 | asm volatile("%0 = *(u32 *)(%1 + %2)" \ 380 | : "=r"(ptr) \ 381 | : "r"(ctx), "i"(offsetof(struct __sk_buff, FIELD))); \ 382 | return ptr; \ 383 | } 384 | /* This defines ctx_data(). */ 385 | DEFINE_FUNC_CTX_POINTER(data) 386 | /* This defines ctx_data_end(). */ 387 | DEFINE_FUNC_CTX_POINTER(data_end) 388 | /* This defines ctx_data_meta(). */ 389 | DEFINE_FUNC_CTX_POINTER(data_meta) 390 | #undef DEFINE_FUNC_CTX_POINTER 391 | 392 | // `len` needs to be a constant number 393 | static __always_inline int _validate_pull(struct __sk_buff *skb, void **hdr_, 394 | u32 off, u32 len) { 395 | u8 *data = (u8 *)ctx_data(skb); 396 | u8 *data_end = (u8 *)ctx_data_end(skb); 397 | u8 *hdr = data + off; 398 | 399 | // ensure hdr pointer is on it's own for validation to work 400 | barrier_var(hdr); 401 | if (hdr + len > data_end) { 402 | if (bpf_skb_pull_data(skb, off + len)) { 403 | return 1; 404 | } 405 | 406 | data = (u8 *)ctx_data(skb); 407 | data_end = (u8 *)ctx_data_end(skb); 408 | hdr = data + off; 409 | if (hdr + len > data_end) { 410 | return 1; 411 | } 412 | } 413 | 414 | *hdr_ = hdr; 415 | 416 | return 0; 417 | } 418 | 419 | #define VALIDATE_PULL(skb, hdr, off, len) \ 420 | (_validate_pull(skb, (void **)hdr, off, len)) 421 | -------------------------------------------------------------------------------- /src/bpf/kernel/vmlinux.h: -------------------------------------------------------------------------------- 1 | #ifndef __VMLINUX_PATCH_H__ 2 | #define __VMLINUX_PATCH_H__ 3 | 4 | #include "_vmlinux.h" 5 | 6 | /** 7 | * These types must be properly aligned, otherwise BPF verification would fail. 8 | * They are commented out in "_vmlinux.h", originally generated with bpftool. 9 | */ 10 | 11 | struct bpf_timer { 12 | __u64 __opaque[2]; 13 | } __attribute__((aligned(8))); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | //! User-facing configuration types 4 | 5 | use std::fmt::Display; 6 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 7 | use std::num::NonZeroU32; 8 | use std::ops::RangeInclusive; 9 | use std::str::FromStr; 10 | 11 | use anyhow::{anyhow, Result}; 12 | use ipnet::{IpNet, Ipv4Net, Ipv6Net}; 13 | use serde::de::Error as DeError; 14 | use serde::{de::Visitor, Deserialize}; 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct ProtoRange { 18 | pub inner: RangeInclusive, 19 | } 20 | type ProtoRanges = Vec; 21 | 22 | #[derive(Debug, Clone, Deserialize)] 23 | #[serde(default)] 24 | pub struct ConfigDefaults { 25 | pub ipv4_local_rule_pref: u32, 26 | pub ipv6_local_rule_pref: u32, 27 | pub ipv4_hairpin_rule_pref: u32, 28 | pub ipv6_hairpin_rule_pref: u32, 29 | pub ipv4_hairpin_table_id: NonZeroU32, 30 | pub ipv6_hairpin_table_id: NonZeroU32, 31 | pub tcp_ranges: ProtoRanges, 32 | pub udp_ranges: ProtoRanges, 33 | pub icmp_ranges: ProtoRanges, 34 | pub icmp_in_ranges: ProtoRanges, 35 | pub icmp_out_ranges: ProtoRanges, 36 | } 37 | 38 | #[derive(Debug, Clone, Copy, Deserialize)] 39 | #[serde(untagged)] 40 | pub enum AddressMatcher { 41 | Range4 { start: Ipv4Addr, end: Ipv4Addr }, 42 | Range6 { start: Ipv6Addr, end: Ipv6Addr }, 43 | Network(IpNet), 44 | } 45 | 46 | #[derive(Debug, Clone, Copy, Deserialize)] 47 | #[serde(untagged)] 48 | pub enum AddressOrMatcher { 49 | Static { address: IpAddr }, 50 | Network { network: IpNet }, 51 | Matcher { match_address: AddressMatcher }, 52 | } 53 | 54 | #[derive(Debug, Clone, Deserialize)] 55 | pub struct ConfigExternal { 56 | #[serde(flatten)] 57 | pub address: AddressOrMatcher, 58 | #[serde(default)] 59 | pub is_internal: bool, 60 | #[serde(default)] 61 | pub no_snat: bool, 62 | #[serde(default)] 63 | pub no_hairpin: bool, 64 | #[serde(default)] 65 | pub tcp_ranges: Option, 66 | #[serde(default)] 67 | pub udp_ranges: Option, 68 | #[serde(default)] 69 | pub icmp_ranges: Option, 70 | #[serde(default)] 71 | pub icmp_in_ranges: Option, 72 | #[serde(default)] 73 | pub icmp_out_ranges: Option, 74 | } 75 | 76 | impl ConfigExternal { 77 | pub fn default_from(network: IpNet, is_matcher: bool) -> Self { 78 | let address = if is_matcher { 79 | AddressOrMatcher::Matcher { 80 | match_address: AddressMatcher::Network(network), 81 | } 82 | } else { 83 | AddressOrMatcher::Network { network } 84 | }; 85 | Self { 86 | address, 87 | is_internal: false, 88 | no_snat: false, 89 | no_hairpin: false, 90 | tcp_ranges: None, 91 | udp_ranges: None, 92 | icmp_ranges: None, 93 | icmp_in_ranges: None, 94 | icmp_out_ranges: None, 95 | } 96 | } 97 | 98 | pub fn match_any_ipv4() -> Self { 99 | Self::default_from( 100 | IpNet::V4(Ipv4Net::new(Ipv4Addr::UNSPECIFIED, 0).unwrap()), 101 | true, 102 | ) 103 | } 104 | 105 | pub fn match_any_ipv6() -> Self { 106 | Self::default_from( 107 | IpNet::V6(Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 0).unwrap()), 108 | true, 109 | ) 110 | } 111 | } 112 | 113 | #[derive(Debug, Clone, Copy, Default)] 114 | pub struct Timeout(pub u64); 115 | 116 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 117 | pub enum IpProtocol { 118 | Tcp, 119 | Udp, 120 | Icmp, 121 | } 122 | 123 | #[derive(Debug, Clone, Default, Deserialize)] 124 | pub struct ConfigHairpinRoute { 125 | #[serde(default)] 126 | pub enable: Option, 127 | #[serde(default)] 128 | pub internal_if_names: Vec, 129 | #[serde(default)] 130 | pub ip_rule_pref: Option, 131 | #[serde(default)] 132 | pub table_id: Option, 133 | #[serde(default = "default_ip_protocols")] 134 | pub ip_protocols: Vec, 135 | } 136 | 137 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 138 | pub enum BpfLoader { 139 | Aya, 140 | Libbpf, 141 | LibbpfSkel, 142 | } 143 | 144 | #[derive(Debug, Clone, Default, Deserialize)] 145 | pub struct ConfigNetIf { 146 | pub if_name: String, 147 | #[serde(default)] 148 | pub nat44: bool, 149 | 150 | #[cfg_attr(not(feature = "ipv6"), allow(dead_code))] 151 | #[serde(default)] 152 | pub nat66: bool, 153 | 154 | #[serde(default)] 155 | pub bpf_log_level: Option, 156 | #[serde(default)] 157 | pub bpf_fib_lookup_external: Option, 158 | #[serde(default)] 159 | pub allow_inbound_icmpx: Option, 160 | #[serde(default)] 161 | pub timeout_fragment: Option, 162 | #[serde(default)] 163 | pub timeout_pkt_min: Option, 164 | #[serde(default)] 165 | pub timeout_pkt_default: Option, 166 | #[serde(default)] 167 | pub timeout_tcp_trans: Option, 168 | #[serde(default)] 169 | pub timeout_tcp_est: Option, 170 | #[serde(default)] 171 | pub frag_track_max_records: Option, 172 | #[serde(default)] 173 | pub binding_max_records: Option, 174 | #[serde(default)] 175 | pub ct_max_records: Option, 176 | #[serde(default = "default_true")] 177 | pub default_externals: bool, 178 | #[serde(default)] 179 | pub snat_internals: Vec, 180 | #[serde(default)] 181 | pub no_snat_dests: Vec, 182 | #[serde(default)] 183 | pub externals: Vec, 184 | #[serde(default)] 185 | pub ipv4_hairpin_route: ConfigHairpinRoute, 186 | 187 | #[cfg_attr(not(feature = "ipv6"), allow(dead_code))] 188 | #[serde(default)] 189 | pub ipv6_hairpin_route: ConfigHairpinRoute, 190 | 191 | #[serde(default)] 192 | pub bpf_loader: Option, 193 | #[serde(default = "default_true")] 194 | pub prefer_tcx: bool, 195 | } 196 | 197 | #[derive(Debug, Default, Deserialize)] 198 | #[serde(deny_unknown_fields)] 199 | #[allow(dead_code)] 200 | pub struct Config { 201 | #[serde(default)] 202 | pub version: Option, 203 | #[serde(default)] 204 | pub defaults: ConfigDefaults, 205 | #[serde(default)] 206 | pub interfaces: Vec, 207 | } 208 | 209 | impl ConfigNetIf { 210 | pub const fn nat44(&self) -> bool { 211 | self.nat44 212 | } 213 | 214 | pub const fn nat66(&self) -> bool { 215 | cfg!(feature = "ipv6") && self.nat66 216 | } 217 | 218 | pub const fn nat64(&self) -> bool { 219 | false 220 | } 221 | } 222 | 223 | impl From for u64 { 224 | fn from(value: Timeout) -> Self { 225 | value.0 226 | } 227 | } 228 | 229 | impl From for Timeout { 230 | fn from(value: std::time::Duration) -> Self { 231 | Self(value.as_nanos().min(u64::MAX as _) as _) 232 | } 233 | } 234 | 235 | impl<'de> Deserialize<'de> for Timeout { 236 | fn deserialize(deserializer: D) -> Result 237 | where 238 | D: serde::Deserializer<'de>, 239 | { 240 | struct RangeVisitor; 241 | impl Visitor<'_> for RangeVisitor { 242 | type Value = Timeout; 243 | 244 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 245 | formatter.write_str("timeout seconds") 246 | } 247 | 248 | fn visit_str(self, v: &str) -> Result 249 | where 250 | E: serde::de::Error, 251 | { 252 | let duration = fundu::parse_duration(v).map_err(DeError::custom)?; 253 | 254 | Ok(duration.into()) 255 | } 256 | } 257 | 258 | deserializer.deserialize_str(RangeVisitor) 259 | } 260 | } 261 | 262 | impl<'de> Deserialize<'de> for IpProtocol { 263 | fn deserialize(deserializer: D) -> Result 264 | where 265 | D: serde::Deserializer<'de>, 266 | { 267 | struct IpProtocolVisitor; 268 | impl Visitor<'_> for IpProtocolVisitor { 269 | type Value = IpProtocol; 270 | 271 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 272 | formatter.write_str("IP protocol: tcp, udp or icmp") 273 | } 274 | 275 | fn visit_str(self, v: &str) -> Result 276 | where 277 | E: serde::de::Error, 278 | { 279 | if v.eq_ignore_ascii_case("tcp") { 280 | Ok(IpProtocol::Tcp) 281 | } else if v.eq_ignore_ascii_case("udp") { 282 | Ok(IpProtocol::Udp) 283 | } else if v.eq_ignore_ascii_case("icmp") { 284 | Ok(IpProtocol::Icmp) 285 | } else { 286 | Err(DeError::custom( 287 | "Invalid protocol name, expecting one of \"tcp\", \"udp\" or \"icmp\".", 288 | )) 289 | } 290 | } 291 | } 292 | 293 | deserializer.deserialize_str(IpProtocolVisitor) 294 | } 295 | } 296 | 297 | impl FromStr for BpfLoader { 298 | type Err = anyhow::Error; 299 | fn from_str(s: &str) -> Result { 300 | if s.eq_ignore_ascii_case("aya") { 301 | Ok(BpfLoader::Aya) 302 | } else if s.eq_ignore_ascii_case("libbpf") { 303 | Ok(BpfLoader::Libbpf) 304 | } else if s.eq_ignore_ascii_case("libbpf-skel") { 305 | Ok(BpfLoader::LibbpfSkel) 306 | } else { 307 | Err(anyhow!( 308 | "Invalid BPF loader, expecting one of \"aya\", \"libbpf\" or \"libbpf-skel\".", 309 | )) 310 | } 311 | } 312 | } 313 | 314 | impl<'de> Deserialize<'de> for BpfLoader { 315 | fn deserialize(deserializer: D) -> Result 316 | where 317 | D: serde::Deserializer<'de>, 318 | { 319 | struct BpfLoaderVisitor; 320 | impl Visitor<'_> for BpfLoaderVisitor { 321 | type Value = BpfLoader; 322 | 323 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 324 | formatter.write_str("BPF loader: aya, libbpf or libbpf-skel") 325 | } 326 | 327 | fn visit_str(self, v: &str) -> Result 328 | where 329 | E: serde::de::Error, 330 | { 331 | v.parse().map_err(DeError::custom) 332 | } 333 | } 334 | 335 | deserializer.deserialize_str(BpfLoaderVisitor) 336 | } 337 | } 338 | 339 | impl Display for ProtoRange { 340 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 341 | f.write_fmt(format_args!("{}-{}", self.inner.start(), self.inner.end())) 342 | } 343 | } 344 | 345 | impl FromStr for ProtoRange { 346 | type Err = anyhow::Error; 347 | fn from_str(s: &str) -> Result { 348 | let Some((start, end)) = s.split_once('-') else { 349 | return Err(anyhow!("missing '-' in port range")); 350 | }; 351 | let start: u16 = start.parse()?; 352 | let end: u16 = end.parse()?; 353 | 354 | if start > end { 355 | // empty port range is valid for our bpf program but we explicitly disallow 356 | // it on parsing stage to notify user about potential misconfiguration 357 | return Err(anyhow!("empty port range")); 358 | } 359 | 360 | Ok(ProtoRange { 361 | inner: RangeInclusive::new(start, end), 362 | }) 363 | } 364 | } 365 | 366 | impl<'de> Deserialize<'de> for ProtoRange { 367 | fn deserialize(deserializer: D) -> Result 368 | where 369 | D: serde::Deserializer<'de>, 370 | { 371 | struct RangeVisitor; 372 | impl Visitor<'_> for RangeVisitor { 373 | type Value = ProtoRange; 374 | 375 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 376 | formatter.write_str("L4 protocol port range") 377 | } 378 | 379 | fn visit_str(self, v: &str) -> Result 380 | where 381 | E: serde::de::Error, 382 | { 383 | v.parse().map_err(DeError::custom) 384 | } 385 | } 386 | 387 | deserializer.deserialize_str(RangeVisitor) 388 | } 389 | } 390 | 391 | impl Default for ConfigDefaults { 392 | fn default() -> Self { 393 | fn range(inner: RangeInclusive) -> ProtoRanges { 394 | debug_assert!(!inner.is_empty()); 395 | vec![ProtoRange { inner }] 396 | } 397 | Self { 398 | ipv4_local_rule_pref: 200, 399 | ipv6_local_rule_pref: 200, 400 | ipv4_hairpin_rule_pref: 100, 401 | ipv6_hairpin_rule_pref: 100, 402 | ipv4_hairpin_table_id: NonZeroU32::new(4787).unwrap(), 403 | ipv6_hairpin_table_id: NonZeroU32::new(4787).unwrap(), 404 | tcp_ranges: range(20000..=29999), 405 | udp_ranges: range(20000..=29999), 406 | icmp_ranges: range(0..=u16::MAX), 407 | icmp_in_ranges: range(0..=9999), 408 | icmp_out_ranges: range(1000..=u16::MAX), 409 | } 410 | } 411 | } 412 | 413 | impl AddressMatcher { 414 | pub fn contains(&self, address: &IpAddr) -> bool { 415 | match self { 416 | AddressMatcher::Network(network) => match network { 417 | IpNet::V4(network) => match address { 418 | IpAddr::V4(v4) => network.contains(&Ipv4Net::new(*v4, 32).unwrap()), 419 | _ => false, 420 | }, 421 | IpNet::V6(network) => match address { 422 | IpAddr::V6(v6) => network.contains(&Ipv6Net::new(*v6, 128).unwrap()), 423 | _ => false, 424 | }, 425 | }, 426 | AddressMatcher::Range4 { start, end } => match address { 427 | IpAddr::V4(v4) => v4 >= start && v4 <= end, 428 | _ => false, 429 | }, 430 | AddressMatcher::Range6 { start, end } => match address { 431 | IpAddr::V6(v6) => v6 >= start && v6 <= end, 432 | _ => false, 433 | }, 434 | } 435 | } 436 | } 437 | 438 | const fn default_true() -> bool { 439 | true 440 | } 441 | 442 | fn default_ip_protocols() -> Vec { 443 | vec![IpProtocol::Tcp, IpProtocol::Udp] 444 | } 445 | 446 | #[cfg(test)] 447 | mod tests { 448 | use super::*; 449 | 450 | #[test] 451 | fn test_parse() { 452 | let config_str = r#" 453 | [defaults] 454 | tcp_ranges = ["10000-65535"] 455 | udp_ranges = ["10000-65535"] 456 | icmp_ranges = ["0-65535"] 457 | icmp_in_ranges = ["0-9999"] 458 | icmp_out_ranges = ["1000-65535"] 459 | 460 | [[interfaces]] 461 | if_name = "eth0" 462 | nat44 = true 463 | nat66 = false 464 | bpf_fib_lookup_external = false 465 | default_externals = true 466 | no_snat_dests = ["192.168.0.0/16"] 467 | hairpin_dests = ["192.168.2.0/24"] 468 | 469 | [[interfaces.externals]] 470 | address = "192.168.1.1" 471 | no_snat = false 472 | no_hairpin = false 473 | tcp_ranges = ["10000-65535"] 474 | udp_ranges = ["10000-65535"] 475 | icmp_ranges = ["0-65535"] 476 | icmp_in_ranges = ["0-9999"] 477 | icmp_out_ranges = ["1000-65535"] 478 | 479 | [[interfaces.externals]] 480 | match_address = "192.168.1.1/24" 481 | 482 | [[interfaces.externals]] 483 | match_address = { start = "192.168.1.1", end = "192.168.1.255" } 484 | "#; 485 | let _config: Config = toml::from_str(config_str).unwrap(); 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /src/instance/config.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | use std::fmt::{Debug, Display}; 5 | use std::ops::RangeInclusive; 6 | 7 | use anyhow::{anyhow, Result}; 8 | // avoid reinventing the wheel, this would not increase binary size much 9 | use aya::util::KernelVersion; 10 | #[cfg(feature = "ipv6")] 11 | use ipnet::Ipv6Net; 12 | use ipnet::{IpNet, Ipv4Net}; 13 | use prefix_trie::map::Entry; 14 | use prefix_trie::{Prefix, PrefixMap, PrefixSet}; 15 | use tracing::warn; 16 | 17 | use crate::config::{AddressOrMatcher, ConfigDefaults, ConfigExternal, ConfigNetIf, ProtoRange}; 18 | use crate::route::IfAddresses; 19 | use crate::skel::einat; 20 | use crate::skel::einat::types; 21 | use crate::skel::{DestConfig, ExternalConfig}; 22 | use crate::utils::{IpAddress, IpNetwork}; 23 | 24 | #[derive(Debug, Default)] 25 | pub struct LoadConfig(pub(super) einat::EinatConstConfig); 26 | 27 | pub trait InetPrefix: 28 | IpNetwork 29 | + IpAddress TryFrom<&'a [u8], Error: Debug>> 30 | + Copy 31 | + Prefix 32 | + PartialEq 33 | + Display 34 | { 35 | } 36 | 37 | impl InetPrefix for Ipv4Net {} 38 | #[cfg(feature = "ipv6")] 39 | impl InetPrefix for Ipv6Net {} 40 | 41 | #[derive(Debug, Default)] 42 | pub struct InetConfig { 43 | pub external_addr: P, 44 | pub dest_config: PrefixMap, 45 | pub external_config: PrefixMap, 46 | } 47 | 48 | #[derive(Debug, Default)] 49 | pub struct RuntimeConfig { 50 | pub v4: InetConfig, 51 | #[cfg(feature = "ipv6")] 52 | pub v6: InetConfig, 53 | } 54 | 55 | pub struct RuntimeConfigEval { 56 | v4_no_snat_dests: Vec, 57 | #[cfg(feature = "ipv6")] 58 | v6_no_snat_dests: Vec, 59 | externals: Vec, 60 | } 61 | 62 | #[derive(Debug, PartialEq, Eq)] 63 | struct ExternalRanges(Vec>); 64 | 65 | #[derive(Debug)] 66 | struct External { 67 | address: AddressOrMatcher, 68 | is_internal: bool, 69 | no_snat: bool, 70 | no_hairpin: bool, 71 | tcp_ranges: ExternalRanges, 72 | udp_ranges: ExternalRanges, 73 | icmp_ranges: ExternalRanges, 74 | icmp_in_ranges: ExternalRanges, 75 | icmp_out_ranges: ExternalRanges, 76 | } 77 | 78 | impl LoadConfig { 79 | pub fn from(config: &ConfigNetIf, has_eth_encap: bool) -> Self { 80 | let nat44 = config.nat44(); 81 | let nat66 = config.nat66(); 82 | let nat64 = config.nat64(); 83 | 84 | let mut ro_data = einat::EinatRoData { 85 | HAS_ETH_ENCAP: has_eth_encap as _, 86 | INGRESS_IPV4: (nat44 || nat64) as _, 87 | EGRESS_IPV4: nat44 as _, 88 | INGRESS_IPV6: nat66 as _, 89 | EGRESS_IPV6: (nat66 || nat64) as _, 90 | ..Default::default() 91 | }; 92 | 93 | let bpf_fib_lookup_external = config.bpf_fib_lookup_external.unwrap_or_else(|| { 94 | if let Ok(v) = KernelVersion::current() { 95 | v >= KernelVersion::new(6, 7, 0) 96 | } else { 97 | false 98 | } 99 | }); 100 | ro_data.ENABLE_FIB_LOOKUP_SRC = bpf_fib_lookup_external as _; 101 | 102 | if let Some(v) = config.bpf_log_level { 103 | ro_data.LOG_LEVEL = v; 104 | } 105 | if let Some(v) = config.allow_inbound_icmpx { 106 | ro_data.ALLOW_INBOUND_ICMPX = v as _; 107 | } 108 | if let Some(v) = config.timeout_fragment { 109 | ro_data.TIMEOUT_FRAGMENT = v.0; 110 | } 111 | if let Some(v) = config.timeout_pkt_min { 112 | ro_data.TIMEOUT_PKT_MIN = v.0; 113 | } 114 | if let Some(v) = config.timeout_pkt_default { 115 | ro_data.TIMEOUT_PKT_MIN = v.0; 116 | } 117 | if let Some(v) = config.timeout_tcp_trans { 118 | ro_data.TIMEOUT_TCP_TRANS = v.0; 119 | } 120 | if let Some(v) = config.timeout_tcp_est { 121 | ro_data.TIMEOUT_TCP_EST = v.0; 122 | } 123 | 124 | let mut const_config = einat::EinatConstConfig { 125 | ro_data, 126 | prefer_tcx: config.prefer_tcx, 127 | ..Default::default() 128 | }; 129 | 130 | if let Some(v) = config.frag_track_max_records { 131 | const_config.frag_track_max_entries = v; 132 | } 133 | if let Some(v) = config.binding_max_records { 134 | // each binding requires 2 records, for both direction 135 | const_config.binding_max_entries = v * 2; 136 | } 137 | 138 | const_config.ct_max_entries = if let Some(v) = config.ct_max_records { 139 | v.min(const_config.binding_max_entries / 2) 140 | } else { 141 | const_config.binding_max_entries 142 | }; 143 | 144 | Self(const_config) 145 | } 146 | } 147 | 148 | impl RuntimeConfigEval { 149 | pub fn try_from(if_config: &ConfigNetIf, defaults: &ConfigDefaults) -> Result { 150 | fn unwrap_v4(network: &IpNet) -> Option { 151 | if let IpNet::V4(network) = network { 152 | Some(network.trunc()) 153 | } else { 154 | None 155 | } 156 | } 157 | 158 | let v4_internals = if_config 159 | .snat_internals 160 | .iter() 161 | .filter_map(unwrap_v4) 162 | .collect::>(); 163 | 164 | let v4_no_snat_dests = if_config 165 | .no_snat_dests 166 | .iter() 167 | .filter_map(unwrap_v4) 168 | .collect::>(); 169 | 170 | #[cfg(feature = "ipv6")] 171 | fn unwrap_v6(network: &IpNet) -> Option { 172 | if let IpNet::V6(network) = network { 173 | Some(network.trunc()) 174 | } else { 175 | None 176 | } 177 | } 178 | 179 | #[cfg(feature = "ipv6")] 180 | let v6_internals = if_config 181 | .snat_internals 182 | .iter() 183 | .filter_map(unwrap_v6) 184 | .collect::>(); 185 | 186 | #[cfg(feature = "ipv6")] 187 | let v6_no_snat_dests = if_config 188 | .no_snat_dests 189 | .iter() 190 | .filter_map(unwrap_v6) 191 | .collect::>(); 192 | 193 | fn convert_internals + IpNetwork>(internals: Vec

) -> Vec { 194 | if internals.is_empty() { 195 | return Vec::new(); 196 | } 197 | 198 | // add match all config to bypass SNAT 199 | let mut external = 200 | ConfigExternal::default_from(P::from(P::Addr::unspecified(), 0).into(), false); 201 | external.no_snat = true; 202 | external.no_hairpin = true; 203 | 204 | let mut externals = vec![external]; 205 | 206 | for internal in internals { 207 | if internal.prefix_len() == 0 { 208 | // the whole network is internal, return the default empty so SNAT is implicitly performed 209 | warn!("specify match all network as internal that clear our other internals"); 210 | return Vec::new(); 211 | } 212 | // add more specific internal config to enable SNAT explicitly 213 | let mut external = ConfigExternal::default_from(internal.into(), false); 214 | external.is_internal = true; 215 | 216 | externals.push(external); 217 | } 218 | 219 | externals 220 | } 221 | 222 | let v4_internals = convert_internals(v4_internals); 223 | cfg_if::cfg_if!( 224 | if #[cfg(feature = "ipv6")] { 225 | let v6_internals = convert_internals(v6_internals); 226 | } else { 227 | let v6_internals = Vec::new(); 228 | } 229 | ); 230 | 231 | let mut default_externals = Vec::new(); 232 | if if_config.default_externals { 233 | if if_config.nat44() { 234 | default_externals.push(ConfigExternal::match_any_ipv4()); 235 | } 236 | if if_config.nat66() { 237 | default_externals.push(ConfigExternal::match_any_ipv6()); 238 | } 239 | } 240 | 241 | let externals = v4_internals 242 | .iter() 243 | .chain(&v6_internals) 244 | .chain(&if_config.externals) 245 | .chain(&default_externals) 246 | .map(|external| External::try_from(external, defaults)) 247 | .collect::>>()?; 248 | 249 | Ok(Self { 250 | v4_no_snat_dests, 251 | #[cfg(feature = "ipv6")] 252 | v6_no_snat_dests, 253 | externals, 254 | }) 255 | } 256 | 257 | pub fn eval(&self, addresses: &IfAddresses) -> RuntimeConfig { 258 | let v4_addresses = addresses.ipv4.iter().map(|&addr| Ipv4Net::from_addr(addr)); 259 | let v4 = InetConfig::from(&self.v4_no_snat_dests, &self.externals, v4_addresses); 260 | 261 | #[cfg(feature = "ipv6")] 262 | let v6_addresses = addresses.ipv6.iter().map(|&addr| Ipv6Net::from_addr(addr)); 263 | #[cfg(feature = "ipv6")] 264 | let v6 = InetConfig::from(&self.v6_no_snat_dests, &self.externals, v6_addresses); 265 | 266 | RuntimeConfig { 267 | v4, 268 | #[cfg(feature = "ipv6")] 269 | v6, 270 | } 271 | } 272 | } 273 | 274 | impl InetConfig

{ 275 | pub fn hairpin_dests(&self) -> Vec

{ 276 | use core::cmp::Ordering; 277 | let mut res: Vec<_> = self 278 | .dest_config 279 | .iter() 280 | .filter_map(|(prefix, config)| { 281 | if config.flags.contains(types::DestFlags::HAIRPIN) { 282 | Some(*prefix) 283 | } else { 284 | None 285 | } 286 | }) 287 | .collect(); 288 | 289 | let external = &self.external_addr; 290 | // move external address to first 291 | res.sort_by(|a, b| { 292 | if a == external { 293 | Ordering::Less 294 | } else if b == external { 295 | Ordering::Greater 296 | } else { 297 | Ordering::Equal 298 | } 299 | }); 300 | 301 | res 302 | } 303 | 304 | fn from( 305 | no_snat_dests: &[P], 306 | externals: &[External], 307 | addresses: impl IntoIterator, 308 | ) -> Self { 309 | let mut external_addr: Option

= None; 310 | let mut dest_config = PrefixMap::::new(); 311 | let mut external_config = PrefixMap::::new(); 312 | 313 | for network in no_snat_dests { 314 | let dest_value = dest_config.entry(*network).or_default(); 315 | dest_value.flags.insert(types::DestFlags::NO_SNAT); 316 | } 317 | 318 | let mut addresses_set = PrefixSet::from_iter(addresses); 319 | for external in externals { 320 | let mut matches = Vec::new(); 321 | match external.address { 322 | AddressOrMatcher::Static { address } => { 323 | if let Some(address) = P::from_ip_addr(address) { 324 | if !address.is_unspecified() { 325 | matches.push(address); 326 | } 327 | } 328 | } 329 | AddressOrMatcher::Network { network } => { 330 | if let Some(network) = P::from_ipnet(network) { 331 | if !network.is_unspecified() { 332 | matches.push(network); 333 | } 334 | } 335 | } 336 | AddressOrMatcher::Matcher { match_address } => { 337 | for address in addresses_set.iter() { 338 | if match_address.contains(&address.ip_addr()) && !address.is_unspecified() { 339 | matches.push(*address); 340 | } 341 | } 342 | } 343 | } 344 | 345 | for address in matches.iter() { 346 | addresses_set.remove(address); 347 | } 348 | 349 | if external_addr.is_none() && !external.no_snat && !external.is_internal { 350 | for network in matches.iter() { 351 | if !network.addr().is_unspecified() { 352 | external_addr = Some(*network); 353 | break; 354 | } 355 | } 356 | } 357 | 358 | for network in matches { 359 | let Entry::Vacant(ext_value) = external_config.entry(network) else { 360 | warn!("external config for {} already exists, skipping", network); 361 | continue; 362 | }; 363 | let ext_value = ext_value.default(); 364 | 365 | if external.is_internal { 366 | ext_value.flags.set(types::ExternalFlags::IS_INTERNAL, true); 367 | // configs below are external only and should be inactive for internal 368 | continue; 369 | } 370 | 371 | if !external.no_hairpin { 372 | if IpNetwork::prefix_len(&network) == 0 { 373 | warn!("a match all network {} with hairpinning is mostly wrong, thus disable hairpinning for it.", &network) 374 | } else { 375 | let dest_value = dest_config.entry(network).or_default(); 376 | dest_value.flags.set(types::DestFlags::HAIRPIN, true); 377 | } 378 | } 379 | 380 | if external.no_snat { 381 | ext_value.flags.set(types::ExternalFlags::NO_SNAT, true); 382 | continue; 383 | } 384 | 385 | external 386 | .tcp_ranges 387 | .apply_raw(&mut ext_value.tcp_range, &mut ext_value.tcp_range_len); 388 | external 389 | .udp_ranges 390 | .apply_raw(&mut ext_value.udp_range, &mut ext_value.udp_range_len); 391 | external 392 | .icmp_ranges 393 | .apply_raw(&mut ext_value.icmp_range, &mut ext_value.icmp_range_len); 394 | external.icmp_in_ranges.apply_raw( 395 | &mut ext_value.icmp_in_range, 396 | &mut ext_value.icmp_in_range_len, 397 | ); 398 | external.icmp_out_ranges.apply_raw( 399 | &mut ext_value.icmp_out_range, 400 | &mut ext_value.icmp_out_range_len, 401 | ); 402 | } 403 | } 404 | 405 | Self { 406 | external_addr: external_addr.unwrap_or(P::unspecified()), 407 | dest_config, 408 | external_config, 409 | } 410 | } 411 | } 412 | 413 | impl ExternalRanges { 414 | fn try_from(ranges: &[ProtoRange], allow_zero: bool) -> Result { 415 | let ranges: Vec<_> = ranges 416 | .iter() 417 | .map(|range| { 418 | if !allow_zero && *range.inner.start() == 0 { 419 | Err(anyhow!("port range {} contains zero, which is not allowed in this type of port range", range)) 420 | } else { 421 | Ok(range.inner.clone()) 422 | } 423 | }) 424 | .collect::>()?; 425 | let ranges = sort_and_merge_ranges(&ranges); 426 | 427 | if ranges.len() > types::MAX_PORT_RANGES { 428 | return Err(anyhow!( 429 | "exceed limit of max {} ranges in port ranges list", 430 | types::MAX_PORT_RANGES 431 | )); 432 | } 433 | Ok(Self(ranges)) 434 | } 435 | 436 | fn contains(&self, other: &ExternalRanges) -> bool { 437 | let this = sort_and_merge_ranges(&self.0); 438 | let other = sort_and_merge_ranges(&other.0); 439 | let mut other_it = other.iter().peekable(); 440 | for range in this { 441 | while let Some(other) = other_it.peek() { 442 | if other.start() < range.start() { 443 | return false; 444 | } 445 | if other.start() > range.end() { 446 | // continue outer loop 447 | break; 448 | } 449 | if other.end() > range.end() { 450 | return false; 451 | } 452 | let _ = other_it.next(); 453 | } 454 | } 455 | other_it.peek().is_none() 456 | } 457 | 458 | fn apply_raw(&self, raw_ranges: &mut types::PortRanges, raw_len: &mut u8) { 459 | assert!(self.0.len() <= raw_ranges.len()); 460 | 461 | for (idx, raw_range) in raw_ranges.iter_mut().enumerate() { 462 | if let Some(range) = self.0.get(idx) { 463 | raw_range.start_port = *range.start(); 464 | raw_range.end_port = *range.end(); 465 | } else { 466 | raw_range.start_port = 0; 467 | raw_range.end_port = 0; 468 | } 469 | } 470 | 471 | *raw_len = self.0.len() as _; 472 | } 473 | } 474 | 475 | impl External { 476 | fn try_from(external: &ConfigExternal, defaults: &ConfigDefaults) -> Result { 477 | let tcp_ranges = ExternalRanges::try_from( 478 | external.tcp_ranges.as_ref().unwrap_or(&defaults.tcp_ranges), 479 | false, 480 | )?; 481 | 482 | let udp_ranges = ExternalRanges::try_from( 483 | external.udp_ranges.as_ref().unwrap_or(&defaults.udp_ranges), 484 | false, 485 | )?; 486 | 487 | let icmp_ranges = ExternalRanges::try_from( 488 | external 489 | .icmp_ranges 490 | .as_ref() 491 | .unwrap_or(&defaults.icmp_ranges), 492 | true, 493 | )?; 494 | 495 | let icmp_in_ranges = if icmp_ranges.0.is_empty() { 496 | ExternalRanges(Vec::new()) 497 | } else { 498 | ExternalRanges::try_from( 499 | external 500 | .icmp_in_ranges 501 | .as_ref() 502 | .unwrap_or(&defaults.icmp_in_ranges), 503 | true, 504 | )? 505 | }; 506 | 507 | let icmp_out_ranges = if icmp_ranges.0.is_empty() { 508 | ExternalRanges(Vec::new()) 509 | } else { 510 | ExternalRanges::try_from( 511 | external 512 | .icmp_out_ranges 513 | .as_ref() 514 | .unwrap_or(&defaults.icmp_out_ranges), 515 | true, 516 | )? 517 | }; 518 | 519 | if !icmp_ranges.contains(&icmp_in_ranges) { 520 | return Err(anyhow!( 521 | "ICMP ranges {:?} not fully include ICMP inbound ranges {:?}", 522 | icmp_ranges, 523 | icmp_in_ranges 524 | )); 525 | } 526 | if !icmp_ranges.contains(&icmp_out_ranges) { 527 | return Err(anyhow!( 528 | "ICMP ranges {:?} not fully include ICMP outbound ranges {:?}", 529 | icmp_ranges, 530 | icmp_in_ranges 531 | )); 532 | } 533 | 534 | Ok(Self { 535 | address: external.address, 536 | no_snat: external.no_snat, 537 | no_hairpin: external.no_hairpin, 538 | is_internal: external.is_internal, 539 | tcp_ranges, 540 | udp_ranges, 541 | icmp_ranges, 542 | icmp_in_ranges, 543 | icmp_out_ranges, 544 | }) 545 | } 546 | } 547 | 548 | fn sort_and_merge_ranges(ranges: &[RangeInclusive]) -> Vec> { 549 | let mut ranges: Vec<_> = ranges 550 | .iter() 551 | .filter(|&range| !range.is_empty()) 552 | .cloned() 553 | .collect(); 554 | ranges.sort_by_key(|range| *range.start()); 555 | 556 | if ranges.len() < 2 { 557 | return ranges; 558 | } 559 | 560 | let mut res = Vec::new(); 561 | let mut curr = ranges[0].clone(); 562 | 563 | for next in ranges.iter().skip(1) { 564 | if *next.start() > *curr.end() + 1 { 565 | res.push(core::mem::replace(&mut curr, next.clone())); 566 | } else if next.end() > curr.end() { 567 | curr = *curr.start()..=*next.end(); 568 | } 569 | } 570 | res.push(curr); 571 | 572 | res 573 | } 574 | 575 | #[cfg(test)] 576 | mod tests { 577 | use super::*; 578 | #[test] 579 | fn external_range() { 580 | let ranges_a = vec![ 581 | ProtoRange { inner: 200..=300 }, 582 | ProtoRange { inner: 0..=100 }, 583 | ProtoRange { inner: 50..=150 }, 584 | ProtoRange { inner: 250..=290 }, 585 | ]; 586 | let ranges_a = ExternalRanges::try_from(&ranges_a, true).unwrap(); 587 | assert_eq!(vec![0..=150, 200..=300], ranges_a.0); 588 | assert!(ranges_a.contains(&ranges_a)); 589 | 590 | let ranges_b = vec![ProtoRange { inner: 0..=100 }]; 591 | let ranges_b = ExternalRanges::try_from(&ranges_b, true).unwrap(); 592 | assert!(ranges_a.contains(&ranges_b)); 593 | 594 | let ranges_c = vec![ProtoRange { inner: 120..=220 }]; 595 | let ranges_c = ExternalRanges::try_from(&ranges_c, true).unwrap(); 596 | assert!(!ranges_a.contains(&ranges_c)); 597 | 598 | let ranges_d = vec![ProtoRange { inner: 0..=1 }]; 599 | let ranges_d = ExternalRanges::try_from(&ranges_d, false); 600 | assert!(ranges_d.is_err()) 601 | } 602 | } 603 | -------------------------------------------------------------------------------- /src/instance/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | mod config; 5 | pub use config::*; 6 | 7 | use std::borrow::Borrow; 8 | use std::net::Ipv4Addr; 9 | use std::time::Instant; 10 | 11 | use anyhow::{anyhow, Result}; 12 | use cfg_if::cfg_if; 13 | #[cfg(any(feature = "aya", feature = "libbpf", feature = "libbpf-skel"))] 14 | use enum_dispatch::enum_dispatch; 15 | use tracing::{debug, info}; 16 | 17 | use crate::skel::einat; 18 | use crate::skel::einat::types::{ 19 | ip_address_from_inet_addr, BindingFlags, ExternalConfig, ExternalFlags, InetAddr, 20 | }; 21 | use crate::skel::einat::{EinatEbpf, EinatEbpfInet, EinatEbpfSkel}; 22 | use crate::skel::{ 23 | EbpfHashMap, EbpfHashMapMut, EbpfLpmTrieMut, EbpfMapFlags, MapBindingKey, MapBindingValue, 24 | MapCtKey, 25 | }; 26 | use crate::utils::{IpAddress, IpNetwork, MapChange, PrefixMapDiff}; 27 | 28 | #[allow(clippy::large_enum_variant)] 29 | #[cfg_attr( 30 | any(feature = "aya", feature = "libbpf", feature = "libbpf-skel"), 31 | enum_dispatch(EinatInstanceT) 32 | )] 33 | pub enum EinatInstanceEnum { 34 | #[cfg(feature = "aya")] 35 | Aya(EinatInstance), 36 | #[cfg(feature = "libbpf")] 37 | Libbpf(EinatInstance), 38 | #[cfg(feature = "libbpf-skel")] 39 | LibbpfSkel(EinatInstance), 40 | } 41 | 42 | pub struct EinatInstance { 43 | skel: T, 44 | config: Option, 45 | links: Option, 46 | } 47 | 48 | #[cfg_attr( 49 | any(feature = "aya", feature = "libbpf", feature = "libbpf-skel"), 50 | enum_dispatch 51 | )] 52 | pub trait EinatInstanceT: Sized { 53 | fn config(&self) -> Option<&RuntimeConfig>; 54 | 55 | fn apply_config(&mut self, config: RuntimeConfig) -> Result<()>; 56 | 57 | fn attach(&mut self, if_name: &str, if_index: u32) -> Result<()>; 58 | 59 | fn detach(&mut self) -> Result<()>; 60 | } 61 | 62 | impl EinatInstanceEnum { 63 | pub fn default_load(config: LoadConfig) -> Result { 64 | cfg_if! { 65 | if #[cfg(feature = "aya")] { 66 | Ok(Self::Aya(EinatInstance::load(config)?)) 67 | } else if #[cfg(feature = "libbpf")] { 68 | Ok(Self::Libbpf(EinatInstance::load(config)?)) 69 | } else if #[cfg(feature = "libbpf-skel")] { 70 | Ok(Self::LibbpfSkel(EinatInstance::load(config)?)) 71 | } else { 72 | Err(anyhow!("no available eBPF loading backend")) 73 | } 74 | } 75 | } 76 | } 77 | 78 | impl EinatInstance { 79 | pub fn load(config: LoadConfig) -> Result { 80 | let start = Instant::now(); 81 | 82 | let skel = T::load(config.0)?; 83 | 84 | info!( 85 | "einat eBPF instance loaded in {:?} with {} loader", 86 | start.elapsed(), 87 | T::NAME 88 | ); 89 | 90 | Ok(Self { 91 | skel, 92 | config: None, 93 | links: None, 94 | }) 95 | } 96 | } 97 | 98 | impl EinatInstanceT for EinatInstance { 99 | fn config(&self) -> Option<&RuntimeConfig> { 100 | self.config.as_ref() 101 | } 102 | 103 | fn apply_config(&mut self, config: RuntimeConfig) -> Result<()> { 104 | apply_inet_config( 105 | &mut self.skel, 106 | self.config.as_ref().map(|config| &config.v4), 107 | &config.v4, 108 | )?; 109 | 110 | #[cfg(feature = "ipv6")] 111 | apply_inet_config( 112 | &mut self.skel, 113 | self.config.as_ref().map(|config| &config.v6), 114 | &config.v6, 115 | )?; 116 | 117 | self.config = Some(config); 118 | Ok(()) 119 | } 120 | 121 | fn attach(&mut self, if_name: &str, if_index: u32) -> Result<()> { 122 | if self.links.is_some() { 123 | return Err(anyhow!("already attached")); 124 | } 125 | let links = self.skel.attach(if_name, if_index)?; 126 | self.links = Some(links); 127 | Ok(()) 128 | } 129 | 130 | fn detach(&mut self) -> Result<()> { 131 | if let Some(links) = self.links.take() { 132 | self.skel.detach(links)?; 133 | } 134 | Ok(()) 135 | } 136 | } 137 | 138 | fn apply_inet_config>( 139 | skel: &mut T, 140 | old: Option<&InetConfig

>, 141 | config: &InetConfig

, 142 | ) -> Result<()> { 143 | let default = Default::default(); 144 | let dest_config_diff = PrefixMapDiff::new( 145 | old.map_or(&default, |old| &old.dest_config), 146 | &config.dest_config, 147 | ); 148 | 149 | let default = Default::default(); 150 | let external_config_diff = PrefixMapDiff::new( 151 | old.map_or(&default, |old| &old.external_config), 152 | &config.external_config, 153 | ); 154 | 155 | let set_addr = old.map_or(true, |old| old.external_addr != config.external_addr); 156 | if set_addr { 157 | if !config.external_addr.is_unspecified() { 158 | info!( 159 | "setting default external address to {}", 160 | config.external_addr.addr() 161 | ); 162 | } 163 | 164 | skel.with_updating_wait(|skel| skel.set_external_addr(config.external_addr))??; 165 | } 166 | 167 | for change in dest_config_diff { 168 | match change { 169 | MapChange::Insert { key, value } | MapChange::Update { key, value, .. } => { 170 | debug!("update dest config of {}", key); 171 | skel.map_dest_config() 172 | .update(key, value, EbpfMapFlags::ANY)?; 173 | } 174 | MapChange::Delete { key, .. } => { 175 | debug!("delete dest config of {}", key); 176 | skel.map_dest_config().delete(key)?; 177 | } 178 | } 179 | } 180 | 181 | for change in external_config_diff { 182 | match change { 183 | MapChange::Insert { key, value } => { 184 | debug!("insert external config of {}", key); 185 | skel.map_external_config() 186 | .update(key, value, EbpfMapFlags::NO_EXIST)?; 187 | } 188 | MapChange::Update { key, old, value } => { 189 | debug!("update external config of {}", key); 190 | skel.with_updating_wait(|skel| -> Result<()> { 191 | remove_binding_and_ct_entries(skel, key, old)?; 192 | skel.map_external_config() 193 | .update(key, value, EbpfMapFlags::EXIST) 194 | })??; 195 | } 196 | MapChange::Delete { key, old } => { 197 | debug!("delete external config of {}", key); 198 | skel.with_updating_wait(|skel| -> Result<()> { 199 | skel.map_external_config().delete(key)?; 200 | remove_binding_and_ct_entries(skel, key, old) 201 | })??; 202 | } 203 | } 204 | } 205 | 206 | Ok(()) 207 | } 208 | 209 | fn remove_binding_and_ct_entries( 210 | skel: &mut T, 211 | external_network: &P, 212 | external_config: &ExternalConfig, 213 | ) -> Result<()> { 214 | // there should be no record for internal addresses as external source 215 | if external_config.flags.contains(ExternalFlags::IS_INTERNAL) { 216 | return Ok(()); 217 | } 218 | 219 | let addr_flag = if IpNetwork::prefix_len(external_network) == Ipv4Addr::LEN { 220 | BindingFlags::ADDR_IPV4 221 | } else { 222 | BindingFlags::ADDR_IPV6 223 | }; 224 | 225 | // cleanup NAT binding records 226 | 227 | let mut to_delete = Vec::new(); 228 | 229 | let addr_matches = |flags: &BindingFlags, inet_addr: &InetAddr| { 230 | if flags.contains(addr_flag) { 231 | let addr = ip_address_from_inet_addr::

(inet_addr); 232 | external_network.contains(&addr) 233 | } else { 234 | false 235 | } 236 | }; 237 | 238 | for key in skel.map_binding().keys() { 239 | let key_owned = key?; 240 | let key: &MapBindingKey = key_owned.borrow(); 241 | 242 | if key.flags.contains(BindingFlags::ORIG_DIR) { 243 | if let Some(binding) = skel.map_binding().lookup(key, EbpfMapFlags::ANY)? { 244 | let binding: &MapBindingValue = binding.borrow(); 245 | 246 | if addr_matches(&binding.flags, &binding.to_addr) { 247 | to_delete.push(key_owned); 248 | } 249 | } 250 | } else if addr_matches(&key.flags, &key.from_addr) { 251 | to_delete.push(key_owned); 252 | } 253 | } 254 | 255 | skel.map_binding_mut() 256 | .delete_batch(to_delete.iter().map(|i| i.borrow()), EbpfMapFlags::ANY)?; 257 | 258 | // cleanup CT records 259 | 260 | let mut to_delete = Vec::new(); 261 | 262 | for key in skel.map_ct().keys() { 263 | let key_owned = key?; 264 | let ct: &MapCtKey = key_owned.borrow(); 265 | if addr_matches(&ct.flags, &ct.external.src_addr) { 266 | to_delete.push(key_owned); 267 | } 268 | } 269 | 270 | skel.map_ct_mut() 271 | .delete_batch(to_delete.iter().map(|i| i.borrow()), EbpfMapFlags::ANY)?; 272 | 273 | Ok(()) 274 | } 275 | 276 | #[cfg(not(any(feature = "aya", feature = "libbpf", feature = "libbpf-skel")))] 277 | /// Dummy impl if no any enabled eBPF loading backend 278 | impl EinatInstanceT for EinatInstanceEnum { 279 | fn config(&self) -> Option<&RuntimeConfig> { 280 | unimplemented!() 281 | } 282 | 283 | fn apply_config(&mut self, _config: RuntimeConfig) -> Result<()> { 284 | unimplemented!() 285 | } 286 | 287 | fn attach(&mut self, _if_name: &str, _if_index: u32) -> Result<()> { 288 | unimplemented!() 289 | } 290 | 291 | fn detach(&mut self) -> Result<()> { 292 | unimplemented!() 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | #[macro_export] 5 | macro_rules! derive_pod { 6 | ( 7 | $( #[$attr:meta] )* 8 | $vis:vis struct $name:ident { 9 | $( 10 | $( #[$attr_f:meta] )? 11 | $vis_f:vis $field:ident : $typ:ty 12 | ),* $(,)* 13 | } 14 | ) => { 15 | $( #[$attr] )* 16 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, ::bytemuck::Zeroable, ::bytemuck::Pod)] 17 | $vis struct $name { 18 | $( 19 | $( #[$attr_f] )? 20 | $vis_f $field : $typ, 21 | )* 22 | } 23 | 24 | /// # Safety 25 | /// Only impl aya::Pod for struct where bytemuck::Pod already applies, 26 | /// it's safe as the later is guarded by bytemuck's Pod derive 27 | #[cfg(feature = "aya")] 28 | unsafe impl ::aya::Pod for $name where $name: ::bytemuck::Pod {} 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/skel/aya.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | use std::borrow::BorrowMut; 5 | use std::io; 6 | use std::mem; 7 | use std::os::fd::{AsFd, AsRawFd, BorrowedFd}; 8 | 9 | pub use ::aya::maps::{HashMap, LpmTrie}; 10 | use anyhow::{anyhow, Result}; 11 | use aya::maps::{lpm_trie::Key, IterableMap, MapData, MapError}; 12 | use aya_obj::generated::{bpf_attr, bpf_cmd}; 13 | 14 | use super::{EbpfHashMap, EbpfHashMapMut, EbpfLpmTrie, EbpfLpmTrieMut, EbpfMapFlags}; 15 | use crate::utils::{IpAddress, IpNetwork}; 16 | 17 | /// 'alias' trait for bytemuck::Pod + aya::Pod 18 | trait Pod: bytemuck::Pod + aya::Pod {} 19 | impl Pod for T {} 20 | 21 | unsafe fn map_delete_batch( 22 | map_fd: BorrowedFd<'_>, 23 | keys_raw: &[u8], 24 | count: u32, 25 | elem_flags: u64, 26 | ) -> Result<()> { 27 | let mut attr = mem::zeroed::(); 28 | let batch = &mut attr.batch; 29 | 30 | batch.keys = keys_raw.as_ptr() as _; 31 | batch.count = count; 32 | batch.map_fd = map_fd.as_raw_fd() as _; 33 | batch.elem_flags = elem_flags; 34 | 35 | let ret = libc::syscall( 36 | libc::SYS_bpf, 37 | bpf_cmd::BPF_MAP_DELETE_BATCH, 38 | &mut attr, 39 | mem::size_of::(), 40 | ); 41 | if ret < 0 { 42 | return Err(anyhow!(io::Error::last_os_error()) 43 | .context(format!("bpf(BPF_MAP_DELETE_BATCH) failed with {}", ret))); 44 | } 45 | 46 | // we don't want a partial deletion of batch 47 | assert_eq!(count, attr.batch.count); 48 | Ok(()) 49 | } 50 | 51 | impl EbpfHashMap for HashMap 52 | where 53 | T: BorrowMut, 54 | { 55 | type BorrowK = K; 56 | type BorrowV = V; 57 | 58 | fn keys(&self) -> impl Iterator> { 59 | self.keys().map(|res| match res { 60 | Ok(v) => Ok(v), 61 | Err(e) => Err(e.into()), 62 | }) 63 | } 64 | 65 | fn lookup(&self, k: &K, flags: EbpfMapFlags) -> Result> { 66 | let res = self.get(k, flags.bits()); 67 | let v = match res { 68 | Ok(v) => Some(v), 69 | Err(MapError::KeyNotFound) => None, 70 | Err(e) => return Err(e.into()), 71 | }; 72 | Ok(v) 73 | } 74 | } 75 | 76 | impl EbpfHashMapMut for HashMap 77 | where 78 | T: BorrowMut, 79 | { 80 | fn update(&mut self, k: &K, v: &V, flags: EbpfMapFlags) -> Result<()> { 81 | self.insert(k, v, flags.bits())?; 82 | Ok(()) 83 | } 84 | 85 | fn delete(&mut self, k: &K) -> Result<()> { 86 | self.remove(k)?; 87 | Ok(()) 88 | } 89 | 90 | fn delete_batch<'i>( 91 | &mut self, 92 | keys: impl IntoIterator, 93 | elem_flags: EbpfMapFlags, 94 | ) -> Result<()> { 95 | let mut keys_raw = Vec::::new(); 96 | for key in keys { 97 | keys_raw.extend(bytemuck::bytes_of(key)); 98 | } 99 | let count = (keys_raw.len() / mem::size_of::()) as u32; 100 | 101 | let map_fd = self.map().fd().as_fd(); 102 | 103 | unsafe { map_delete_batch(map_fd, &keys_raw, count, elem_flags.bits()) } 104 | } 105 | } 106 | 107 | fn key_from_ip_net(network: &K) -> Key<::Data> 108 | where 109 | ::Data: Pod, 110 | { 111 | Key::new(network.prefix_len() as _, network.addr().data()) 112 | } 113 | 114 | impl EbpfLpmTrie for LpmTrie::Data, V> 115 | where 116 | T: BorrowMut, 117 | ::Data: Pod, 118 | { 119 | type BorrowV = V; 120 | 121 | fn lookup(&self, k: &K, flags: EbpfMapFlags) -> Result> { 122 | let res = self.get(&key_from_ip_net(k), flags.bits()); 123 | let v = match res { 124 | Ok(v) => Some(v), 125 | Err(MapError::KeyNotFound) => None, 126 | Err(e) => return Err(e.into()), 127 | }; 128 | Ok(v) 129 | } 130 | } 131 | 132 | impl EbpfLpmTrieMut for LpmTrie::Data, V> 133 | where 134 | T: BorrowMut, 135 | ::Data: Pod, 136 | { 137 | fn update(&mut self, k: &K, v: &V, flags: EbpfMapFlags) -> Result<()> { 138 | self.insert(&key_from_ip_net(k), v, flags.bits())?; 139 | Ok(()) 140 | } 141 | 142 | fn delete(&mut self, k: &K) -> Result<()> { 143 | self.remove(&key_from_ip_net(k))?; 144 | Ok(()) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/skel/einat/aya.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | use std::io; 5 | use std::net::Ipv4Addr; 6 | #[cfg(feature = "ipv6")] 7 | use std::net::Ipv6Addr; 8 | 9 | use anyhow::Result; 10 | use aya::maps::{Array, HashMap, LpmTrie, MapData}; 11 | use aya::programs::links::LinkOrder; 12 | use aya::programs::tc::{ 13 | qdisc_add_clsact, qdisc_detach_program, NlOptions, SchedClassifier, SchedClassifierLinkId, 14 | TcAttachOptions, TcAttachType, 15 | }; 16 | use aya::util::KernelVersion; 17 | use aya::{Ebpf, EbpfLoader}; 18 | use ipnet::Ipv4Net; 19 | #[cfg(feature = "ipv6")] 20 | use ipnet::Ipv6Net; 21 | 22 | use super::types::{ 23 | self, DestConfig, EinatData, ExternalConfig, MapBindingKey, MapBindingValue, MapCtKey, 24 | MapCtValue, 25 | }; 26 | use super::{einat_obj_data, EinatConstConfig, EinatEbpf, EinatEbpfInet}; 27 | use crate::utils::{IpAddress, IpNetwork}; 28 | 29 | type MapEinatData = Array; 30 | type MapBinding = HashMap; 31 | type MapCt = HashMap; 32 | type MapIpv4ExternalConfig = LpmTrie::Data, ExternalConfig>; 33 | type MapIpv4DestConfig = LpmTrie::Data, DestConfig>; 34 | #[cfg(feature = "ipv6")] 35 | type MapIpv6ExternalConfig = LpmTrie::Data, ExternalConfig>; 36 | #[cfg(feature = "ipv6")] 37 | type MapIpv6DestConfig = LpmTrie::Data, DestConfig>; 38 | 39 | pub struct EinatAya { 40 | ebpf: Ebpf, 41 | map_data: MapEinatData, 42 | map_binding: MapBinding, 43 | map_ct: MapCt, 44 | map_ipv4_dest_config: MapIpv4DestConfig, 45 | map_ipv4_external_config: MapIpv4ExternalConfig, 46 | #[cfg(feature = "ipv6")] 47 | map_ipv6_dest_config: MapIpv6DestConfig, 48 | #[cfg(feature = "ipv6")] 49 | map_ipv6_external_config: MapIpv6ExternalConfig, 50 | use_tcx: bool, 51 | } 52 | 53 | pub struct EinatAyaLinks { 54 | ingress_link_id: SchedClassifierLinkId, 55 | egress_link_id: SchedClassifierLinkId, 56 | } 57 | 58 | fn prog_ingress_assert(ebpf: &mut Ebpf) -> &mut SchedClassifier { 59 | ebpf.program_mut(types::PROG_INGRESS_REV_SNAT) 60 | .unwrap() 61 | .try_into() 62 | .unwrap() 63 | } 64 | 65 | fn prog_egress_assert(ebpf: &mut Ebpf) -> &mut SchedClassifier { 66 | ebpf.program_mut(types::PROG_EGRESS_SNAT) 67 | .unwrap() 68 | .try_into() 69 | .unwrap() 70 | } 71 | 72 | impl EinatAya { 73 | fn new(mut ebpf: Ebpf, use_tcx: bool) -> Result { 74 | let map_data = MapEinatData::try_from(ebpf.take_map(".data").unwrap()).unwrap(); 75 | 76 | let map_binding = MapBinding::try_from(ebpf.take_map(types::MAP_BINDING).unwrap()).unwrap(); 77 | 78 | let map_ct = MapCt::try_from(ebpf.take_map(types::MAP_CT).unwrap()).unwrap(); 79 | 80 | let map_ipv4_dest_config = 81 | MapIpv4DestConfig::try_from(ebpf.take_map(types::MAP_IPV4_DEST_CONFIG).unwrap()) 82 | .unwrap(); 83 | 84 | let map_ipv4_external_config = MapIpv4ExternalConfig::try_from( 85 | ebpf.take_map(types::MAP_IPV4_EXTERNAL_CONFIG).unwrap(), 86 | ) 87 | .unwrap(); 88 | 89 | #[cfg(feature = "ipv6")] 90 | let map_ipv6_dest_config = 91 | MapIpv6DestConfig::try_from(ebpf.take_map(types::MAP_IPV6_DEST_CONFIG).unwrap()) 92 | .unwrap(); 93 | 94 | #[cfg(feature = "ipv6")] 95 | let map_ipv6_external_config = MapIpv6ExternalConfig::try_from( 96 | ebpf.take_map(types::MAP_IPV6_EXTERNAL_CONFIG).unwrap(), 97 | ) 98 | .unwrap(); 99 | 100 | prog_ingress_assert(&mut ebpf).load()?; 101 | prog_egress_assert(&mut ebpf).load()?; 102 | 103 | Ok(Self { 104 | ebpf, 105 | map_data, 106 | map_binding, 107 | map_ct, 108 | map_ipv4_dest_config, 109 | map_ipv4_external_config, 110 | #[cfg(feature = "ipv6")] 111 | map_ipv6_dest_config, 112 | #[cfg(feature = "ipv6")] 113 | map_ipv6_external_config, 114 | use_tcx, 115 | }) 116 | } 117 | 118 | fn get_data(&self) -> Result { 119 | let data = self.map_data.get(&0, 0)?; 120 | Ok(data) 121 | } 122 | 123 | fn alter_data_with T>(&mut self, f: F) -> Result { 124 | let mut data = self.map_data.get(&0, 0)?; 125 | let r = f(&mut data); 126 | self.map_data.set(0, data, 0)?; 127 | 128 | Ok(r) 129 | } 130 | } 131 | 132 | impl EinatEbpf for EinatAya { 133 | const NAME: &'static str = "Aya"; 134 | 135 | type MapBinding = MapBinding; 136 | type MapCt = MapCt; 137 | type Links = EinatAyaLinks; 138 | 139 | fn load(config: EinatConstConfig) -> Result { 140 | let mut loader = EbpfLoader::new(); 141 | 142 | macro_rules! set_global { 143 | ($($k:ident),*) => { 144 | $( loader.set_global(stringify!($k), &config.ro_data.$k, true); )* 145 | }; 146 | } 147 | 148 | set_global!( 149 | LOG_LEVEL, 150 | HAS_ETH_ENCAP, 151 | INGRESS_IPV4, 152 | EGRESS_IPV4, 153 | INGRESS_IPV6, 154 | EGRESS_IPV6, 155 | ENABLE_FIB_LOOKUP_SRC, 156 | ALLOW_INBOUND_ICMPX, 157 | TIMEOUT_FRAGMENT, 158 | TIMEOUT_PKT_MIN, 159 | TIMEOUT_PKT_DEFAULT, 160 | TIMEOUT_TCP_TRANS, 161 | TIMEOUT_TCP_EST 162 | ); 163 | 164 | loader.set_max_entries(types::MAP_FRAG_TRACK, config.frag_track_max_entries); 165 | loader.set_max_entries(types::MAP_BINDING, config.binding_max_entries); 166 | loader.set_max_entries(types::MAP_CT, config.ct_max_entries); 167 | 168 | let use_tcx = config.prefer_tcx 169 | && KernelVersion::current().is_ok_and(|v| v >= KernelVersion::new(6, 6, 0)); 170 | 171 | Self::new(loader.load(einat_obj_data())?, use_tcx) 172 | } 173 | 174 | fn map_binding(&self) -> &Self::MapBinding { 175 | &self.map_binding 176 | } 177 | 178 | fn map_binding_mut(&mut self) -> &mut Self::MapBinding { 179 | &mut self.map_binding 180 | } 181 | 182 | fn map_ct(&self) -> &Self::MapCt { 183 | &self.map_ct 184 | } 185 | 186 | fn map_ct_mut(&mut self) -> &mut Self::MapCt { 187 | &mut self.map_ct 188 | } 189 | 190 | fn with_updating T>(&mut self, f: F) -> Result { 191 | self.alter_data_with(|data| data.g_deleting_map_entries = 1)?; 192 | let r = f(self); 193 | self.alter_data_with(|data| data.g_deleting_map_entries = 0)?; 194 | Ok(r) 195 | } 196 | 197 | fn attach(&mut self, if_name: &str, _if_index: u32) -> Result { 198 | let (ingress_opt, egress_opt) = if self.use_tcx { 199 | ( 200 | TcAttachOptions::TcxOrder(LinkOrder::first()), 201 | TcAttachOptions::TcxOrder(LinkOrder::first()), 202 | ) 203 | } else { 204 | if let Err(e) = qdisc_add_clsact(if_name) { 205 | if e.kind() != io::ErrorKind::AlreadyExists { 206 | return Err(e.into()); 207 | } 208 | }; 209 | 210 | let _ = 211 | qdisc_detach_program(if_name, TcAttachType::Ingress, types::PROG_INGRESS_REV_SNAT); 212 | let _ = qdisc_detach_program(if_name, TcAttachType::Egress, types::PROG_EGRESS_SNAT); 213 | 214 | ( 215 | TcAttachOptions::Netlink(NlOptions { 216 | handle: 1, 217 | priority: 1, 218 | }), 219 | TcAttachOptions::Netlink(NlOptions { 220 | handle: 1, 221 | priority: 1, 222 | }), 223 | ) 224 | }; 225 | 226 | let ingress_link_id = prog_ingress_assert(&mut self.ebpf).attach_with_options( 227 | if_name, 228 | TcAttachType::Ingress, 229 | ingress_opt, 230 | )?; 231 | let egress_link_id = prog_egress_assert(&mut self.ebpf).attach_with_options( 232 | if_name, 233 | TcAttachType::Egress, 234 | egress_opt, 235 | ); 236 | 237 | let egress_link_id = match egress_link_id { 238 | Ok(v) => v, 239 | Err(e) => { 240 | let _ = prog_ingress_assert(&mut self.ebpf).detach(ingress_link_id); 241 | return Err(e.into()); 242 | } 243 | }; 244 | 245 | Ok(EinatAyaLinks { 246 | ingress_link_id, 247 | egress_link_id, 248 | }) 249 | } 250 | 251 | fn detach(&mut self, links: Self::Links) -> Result<()> { 252 | let res = prog_egress_assert(&mut self.ebpf).detach(links.egress_link_id); 253 | prog_ingress_assert(&mut self.ebpf).detach(links.ingress_link_id)?; 254 | 255 | Ok(res?) 256 | } 257 | } 258 | 259 | impl EinatEbpfInet for EinatAya { 260 | type MapExternalConfig = MapIpv4ExternalConfig; 261 | type MapDestConfigMap = MapIpv4DestConfig; 262 | 263 | fn external_addr(&self) -> Result { 264 | let addr_ne = self.get_data()?.g_ipv4_external_addr; 265 | let octets: [u8; 4] = bytemuck::bytes_of(&addr_ne).try_into().unwrap(); 266 | Ok(Ipv4Net::from_addr(Ipv4Addr::from(octets))) 267 | } 268 | 269 | fn set_external_addr(&mut self, addr: Ipv4Net) -> Result<()> { 270 | self.alter_data_with(|data| { 271 | data.g_ipv4_external_addr = bytemuck::cast(addr.addr().octets()) 272 | }) 273 | } 274 | 275 | fn map_external_config(&mut self) -> &mut Self::MapExternalConfig { 276 | &mut self.map_ipv4_external_config 277 | } 278 | 279 | fn map_dest_config(&mut self) -> &mut Self::MapDestConfigMap { 280 | &mut self.map_ipv4_dest_config 281 | } 282 | } 283 | 284 | #[cfg(feature = "ipv6")] 285 | impl EinatEbpfInet for EinatAya { 286 | type MapExternalConfig = MapIpv6ExternalConfig; 287 | type MapDestConfigMap = MapIpv6DestConfig; 288 | 289 | fn external_addr(&self) -> Result { 290 | let addr_ne = self.get_data()?.g_ipv6_external_addr; 291 | let octets: [u8; 16] = bytemuck::bytes_of(&addr_ne).try_into().unwrap(); 292 | Ok(Ipv6Net::from_addr(Ipv6Addr::from(octets))) 293 | } 294 | 295 | fn set_external_addr(&mut self, addr: Ipv6Net) -> Result<()> { 296 | self.alter_data_with(|data| { 297 | data.g_ipv6_external_addr = bytemuck::cast(addr.addr().octets()) 298 | }) 299 | } 300 | 301 | fn map_external_config(&mut self) -> &mut Self::MapExternalConfig { 302 | &mut self.map_ipv6_external_config 303 | } 304 | 305 | fn map_dest_config(&mut self) -> &mut Self::MapDestConfigMap { 306 | &mut self.map_ipv6_dest_config 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/skel/einat/libbpf.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | use std::net::Ipv4Addr; 5 | #[cfg(feature = "ipv6")] 6 | use std::net::Ipv6Addr; 7 | 8 | use anyhow::Result; 9 | use ipnet::Ipv4Net; 10 | #[cfg(feature = "ipv6")] 11 | use ipnet::Ipv6Net; 12 | use libbpf_rs::{MapCore, MapFlags, MapHandle, Object}; 13 | 14 | use super::super::libbpf::LibbpfMap; 15 | use super::types::{self, EinatData}; 16 | use super::{einat_obj_data, EinatConstConfig, EinatEbpf, EinatEbpfInet}; 17 | use crate::utils::IpNetwork; 18 | 19 | use super::libbpf_common::{attach, detach, EinatLibbpfLinks}; 20 | 21 | type Map = LibbpfMap; 22 | 23 | pub struct EinatLibbpf { 24 | obj: Object, 25 | map_data: MapHandle, 26 | map_binding: Map, 27 | map_ct: Map, 28 | map_ipv4_dest_config: Map, 29 | map_ipv4_external_config: Map, 30 | #[cfg(feature = "ipv6")] 31 | map_ipv6_dest_config: Map, 32 | #[cfg(feature = "ipv6")] 33 | map_ipv6_external_config: Map, 34 | } 35 | 36 | unsafe impl Send for EinatLibbpf {} 37 | unsafe impl Sync for EinatLibbpf {} 38 | 39 | impl EinatLibbpf { 40 | // XXX: mmap data map instead? 41 | fn get_data(&self) -> Result { 42 | let data = self 43 | .map_data 44 | .lookup(&0u32.to_ne_bytes(), MapFlags::ANY)? 45 | .unwrap(); 46 | Ok(*bytemuck::from_bytes(&data)) 47 | } 48 | 49 | fn alter_data_with T>(&mut self, f: F) -> Result { 50 | let mut data_raw = self 51 | .map_data 52 | .lookup(&0u32.to_ne_bytes(), MapFlags::ANY)? 53 | .unwrap(); 54 | let data: &mut EinatData = bytemuck::from_bytes_mut(&mut data_raw); 55 | 56 | let r = f(data); 57 | 58 | self.map_data 59 | .update(&0u32.to_ne_bytes(), &data_raw, MapFlags::ANY)?; 60 | Ok(r) 61 | } 62 | } 63 | 64 | impl EinatEbpf for EinatLibbpf { 65 | const NAME: &'static str = "libbpf"; 66 | 67 | type MapBinding = Map; 68 | type MapCt = Map; 69 | type Links = EinatLibbpfLinks; 70 | 71 | fn load(config: EinatConstConfig) -> Result { 72 | let mut open_obj = libbpf_rs::ObjectBuilder::default() 73 | .name("einat") 74 | .expect("failed to name obj as einat") 75 | .open_memory(einat_obj_data())?; 76 | 77 | let mut map_ro_data = None; 78 | let mut map_frag_track = None; 79 | let mut map_binding = None; 80 | let mut map_ct = None; 81 | 82 | for map in open_obj.maps_mut() { 83 | match map.name().to_string_lossy().as_ref() { 84 | "einat.rodata" => map_ro_data = Some(map), 85 | types::MAP_FRAG_TRACK => map_frag_track = Some(map), 86 | types::MAP_BINDING => map_binding = Some(map), 87 | types::MAP_CT => map_ct = Some(map), 88 | _ => (), 89 | } 90 | } 91 | 92 | let ro_data_bytes = bytemuck::bytes_of(&config.ro_data); 93 | 94 | let mut map_ro_data = map_ro_data.expect("failed to get rodata map"); 95 | let initial_value = map_ro_data.initial_value_mut().expect("rodata not mmaped"); 96 | let initial_value = &mut initial_value[..ro_data_bytes.len()]; 97 | 98 | initial_value.copy_from_slice(ro_data_bytes); 99 | 100 | // set max_entries 101 | 102 | map_frag_track 103 | .unwrap() 104 | .set_max_entries(config.frag_track_max_entries)?; 105 | map_binding 106 | .unwrap() 107 | .set_max_entries(config.binding_max_entries)?; 108 | map_ct.unwrap().set_max_entries(config.ct_max_entries)?; 109 | 110 | let obj = open_obj.load()?; 111 | 112 | // retrieve maps 113 | 114 | let mut map_data = None; 115 | let mut map_binding = None; 116 | let mut map_ct = None; 117 | let mut map_ipv4_dest_config = None; 118 | let mut map_ipv4_external_config = None; 119 | #[cfg(feature = "ipv6")] 120 | let mut map_ipv6_dest_config = None; 121 | #[cfg(feature = "ipv6")] 122 | let mut map_ipv6_external_config = None; 123 | 124 | for map in obj.maps() { 125 | match map.name().to_string_lossy().as_ref() { 126 | "einat.data" => map_data = Some(map), 127 | types::MAP_BINDING => map_binding = Some(map), 128 | types::MAP_CT => map_ct = Some(map), 129 | types::MAP_IPV4_DEST_CONFIG => map_ipv4_dest_config = Some(map), 130 | types::MAP_IPV4_EXTERNAL_CONFIG => map_ipv4_external_config = Some(map), 131 | #[cfg(feature = "ipv6")] 132 | types::MAP_IPV6_DEST_CONFIG => map_ipv6_dest_config = Some(map), 133 | #[cfg(feature = "ipv6")] 134 | types::MAP_IPV6_EXTERNAL_CONFIG => map_ipv6_external_config = Some(map), 135 | _ => (), 136 | } 137 | } 138 | 139 | macro_rules! wrap { 140 | ($n:ident) => { 141 | LibbpfMap(MapHandle::try_from(&$n.unwrap())?) 142 | }; 143 | } 144 | 145 | Ok(Self { 146 | map_data: MapHandle::try_from(&map_data.unwrap())?, 147 | map_binding: wrap!(map_binding), 148 | map_ct: wrap!(map_ct), 149 | map_ipv4_dest_config: wrap!(map_ipv4_dest_config), 150 | map_ipv4_external_config: wrap!(map_ipv4_external_config), 151 | #[cfg(feature = "ipv6")] 152 | map_ipv6_dest_config: wrap!(map_ipv6_dest_config), 153 | #[cfg(feature = "ipv6")] 154 | map_ipv6_external_config: wrap!(map_ipv6_external_config), 155 | obj, 156 | }) 157 | } 158 | 159 | fn map_binding(&self) -> &Self::MapBinding { 160 | &self.map_binding 161 | } 162 | 163 | fn map_binding_mut(&mut self) -> &mut Self::MapBinding { 164 | &mut self.map_binding 165 | } 166 | 167 | fn map_ct(&self) -> &Self::MapCt { 168 | &self.map_ct 169 | } 170 | 171 | fn map_ct_mut(&mut self) -> &mut Self::MapCt { 172 | &mut self.map_ct 173 | } 174 | 175 | fn with_updating T>(&mut self, f: F) -> Result { 176 | self.alter_data_with(|data| data.g_deleting_map_entries = 1)?; 177 | let r = f(self); 178 | self.alter_data_with(|data| data.g_deleting_map_entries = 0)?; 179 | Ok(r) 180 | } 181 | 182 | fn attach(&mut self, _if_name: &str, if_index: u32) -> Result { 183 | let mut prog_ingress = None; 184 | let mut prog_egress = None; 185 | 186 | for prog in self.obj.progs() { 187 | match prog.name().to_string_lossy().as_ref() { 188 | types::PROG_INGRESS_REV_SNAT => prog_ingress = Some(prog), 189 | types::PROG_EGRESS_SNAT => prog_egress = Some(prog), 190 | _ => unreachable!(), 191 | } 192 | } 193 | 194 | attach(&prog_ingress.unwrap(), &prog_egress.unwrap(), if_index) 195 | } 196 | 197 | fn detach(&mut self, links: Self::Links) -> Result<()> { 198 | detach(links) 199 | } 200 | } 201 | 202 | impl EinatEbpfInet for EinatLibbpf { 203 | type MapExternalConfig = Map; 204 | 205 | type MapDestConfigMap = Map; 206 | 207 | fn external_addr(&self) -> Result { 208 | let addr_ne = self.get_data()?.g_ipv4_external_addr; 209 | let octets: [u8; 4] = bytemuck::bytes_of(&addr_ne).try_into().unwrap(); 210 | Ok(Ipv4Net::from_addr(Ipv4Addr::from(octets))) 211 | } 212 | 213 | fn set_external_addr(&mut self, addr: Ipv4Net) -> Result<()> { 214 | let addr_be: u32 = bytemuck::cast(addr.addr().octets()); 215 | self.alter_data_with(|data| data.g_ipv4_external_addr = addr_be) 216 | } 217 | 218 | fn map_external_config(&mut self) -> &mut Self::MapExternalConfig { 219 | &mut self.map_ipv4_external_config 220 | } 221 | 222 | fn map_dest_config(&mut self) -> &mut Self::MapDestConfigMap { 223 | &mut self.map_ipv4_dest_config 224 | } 225 | } 226 | 227 | #[cfg(feature = "ipv6")] 228 | impl EinatEbpfInet for EinatLibbpf { 229 | type MapExternalConfig = Map; 230 | 231 | type MapDestConfigMap = Map; 232 | 233 | fn external_addr(&self) -> Result { 234 | let addr_ne = self.get_data()?.g_ipv6_external_addr; 235 | let octets: [u8; 16] = bytemuck::bytes_of(&addr_ne).try_into().unwrap(); 236 | Ok(Ipv6Net::from_addr(Ipv6Addr::from(octets))) 237 | } 238 | 239 | fn set_external_addr(&mut self, addr: Ipv6Net) -> Result<()> { 240 | self.alter_data_with(|data| { 241 | data.g_ipv6_external_addr = bytemuck::cast(addr.addr().octets()) 242 | }) 243 | } 244 | 245 | fn map_external_config(&mut self) -> &mut Self::MapExternalConfig { 246 | &mut self.map_ipv6_external_config 247 | } 248 | 249 | fn map_dest_config(&mut self) -> &mut Self::MapDestConfigMap { 250 | &mut self.map_ipv6_dest_config 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/skel/einat/libbpf_common.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | use std::os::fd::AsFd; 5 | 6 | use anyhow::Result; 7 | use libbpf_rs::{Program, TcHook, TcHookBuilder, TC_EGRESS, TC_INGRESS}; 8 | 9 | pub struct EinatLibbpfLinks { 10 | ingress_hook: TcHook, 11 | egress_hook: TcHook, 12 | } 13 | 14 | pub(super) fn attach( 15 | prog_ingress: &Program, 16 | prog_egress: &Program, 17 | if_index: u32, 18 | ) -> Result { 19 | let mut ingress_hook = create_ingress_hook(prog_ingress, if_index) 20 | .create()? 21 | .attach()?; 22 | let egress_hook = create_egress_hook(prog_egress, if_index) 23 | .attach() 24 | .map_err(|e| { 25 | let _ = ingress_hook.detach(); 26 | e 27 | })?; 28 | 29 | Ok(EinatLibbpfLinks { 30 | ingress_hook, 31 | egress_hook, 32 | }) 33 | } 34 | 35 | pub(super) fn detach(mut links: EinatLibbpfLinks) -> Result<()> { 36 | let res = links.egress_hook.detach(); 37 | links.ingress_hook.detach()?; 38 | Ok(res?) 39 | } 40 | 41 | fn create_ingress_hook(prog: &Program, if_index: u32) -> TcHook { 42 | TcHookBuilder::new(prog.as_fd()) 43 | .ifindex(if_index as _) 44 | .replace(true) 45 | .handle(1) 46 | .priority(1) 47 | .hook(TC_INGRESS) 48 | } 49 | 50 | fn create_egress_hook(prog: &Program, if_index: u32) -> TcHook { 51 | TcHookBuilder::new(prog.as_fd()) 52 | .ifindex(if_index as _) 53 | .replace(true) 54 | .handle(1) 55 | .priority(1) 56 | .hook(TC_EGRESS) 57 | } 58 | -------------------------------------------------------------------------------- /src/skel/einat/libbpf_skel.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | mod skel_build { 5 | include!(concat!(env!("OUT_DIR"), "/einat.skel.rs")); 6 | } 7 | use skel_build::types as einat_types; 8 | use skel_build::*; 9 | 10 | use std::mem; 11 | use std::net::Ipv4Addr; 12 | #[cfg(feature = "ipv6")] 13 | use std::net::Ipv6Addr; 14 | 15 | use anyhow::Result; 16 | use ipnet::Ipv4Net; 17 | #[cfg(feature = "ipv6")] 18 | use ipnet::Ipv6Net; 19 | use libbpf_rs::skel::{OpenSkel, SkelBuilder}; 20 | use libbpf_rs::{MapHandle, OpenObject}; 21 | use self_cell::{self_cell, MutBorrow}; 22 | 23 | use super::super::libbpf::LibbpfMap; 24 | use super::{EinatConstConfig, EinatEbpf, EinatEbpfInet, EinatRoData}; 25 | use crate::utils::IpNetwork; 26 | 27 | use super::libbpf_common::{attach, detach, EinatLibbpfLinks}; 28 | 29 | self_cell!( 30 | struct OwnedSkel { 31 | owner: MutBorrow>, 32 | 33 | #[covariant] 34 | dependent: EinatSkel, 35 | } 36 | ); 37 | 38 | type Map = LibbpfMap; 39 | 40 | pub struct EinatLibbpfSkel { 41 | skel: OwnedSkel, 42 | map_binding: Map, 43 | map_ct: Map, 44 | map_ipv4_dest_config: Map, 45 | map_ipv4_external_config: Map, 46 | #[cfg(feature = "ipv6")] 47 | map_ipv6_dest_config: Map, 48 | #[cfg(feature = "ipv6")] 49 | map_ipv6_external_config: Map, 50 | } 51 | 52 | unsafe impl Send for EinatLibbpfSkel {} 53 | unsafe impl Sync for EinatLibbpfSkel {} 54 | 55 | impl EinatEbpf for EinatLibbpfSkel { 56 | const NAME: &'static str = "libbpf skeleton"; 57 | 58 | type MapBinding = Map; 59 | type MapCt = Map; 60 | type Links = EinatLibbpfLinks; 61 | 62 | fn load(config: EinatConstConfig) -> Result { 63 | let obj = MutBorrow::new(mem::MaybeUninit::zeroed()); 64 | 65 | let skel = OwnedSkel::try_new(obj, |obj| -> Result<_> { 66 | let mut open_skel = EinatSkelBuilder::default().open(obj.borrow_mut())?; 67 | *open_skel.maps.rodata_data = 68 | unsafe { mem::transmute::(config.ro_data) }; 69 | 70 | open_skel 71 | .maps 72 | .map_frag_track 73 | .set_max_entries(config.frag_track_max_entries)?; 74 | open_skel 75 | .maps 76 | .map_binding 77 | .set_max_entries(config.binding_max_entries)?; 78 | open_skel 79 | .maps 80 | .map_ct 81 | .set_max_entries(config.ct_max_entries)?; 82 | 83 | Ok(open_skel.load()?) 84 | })?; 85 | 86 | let maps = &skel.borrow_dependent().maps; 87 | 88 | macro_rules! wrap { 89 | ($n:ident) => { 90 | LibbpfMap(MapHandle::try_from(&maps.$n)?) 91 | }; 92 | } 93 | 94 | Ok(Self { 95 | map_binding: wrap!(map_binding), 96 | map_ct: wrap!(map_ct), 97 | map_ipv4_dest_config: wrap!(map_ipv4_dest_config), 98 | map_ipv4_external_config: wrap!(map_ipv4_external_config), 99 | #[cfg(feature = "ipv6")] 100 | map_ipv6_dest_config: wrap!(map_ipv6_dest_config), 101 | #[cfg(feature = "ipv6")] 102 | map_ipv6_external_config: wrap!(map_ipv6_external_config), 103 | skel, 104 | }) 105 | } 106 | 107 | fn map_binding(&self) -> &Self::MapBinding { 108 | &self.map_binding 109 | } 110 | 111 | fn map_binding_mut(&mut self) -> &mut Self::MapBinding { 112 | &mut self.map_binding 113 | } 114 | 115 | fn map_ct(&self) -> &Self::MapCt { 116 | &self.map_ct 117 | } 118 | 119 | fn map_ct_mut(&mut self) -> &mut Self::MapCt { 120 | &mut self.map_ct 121 | } 122 | 123 | fn with_updating T>(&mut self, f: F) -> Result { 124 | self.skel.with_dependent_mut(|_, skel| { 125 | skel.maps.data_data.g_deleting_map_entries = 1; 126 | }); 127 | let r = f(self); 128 | self.skel.with_dependent_mut(|_, skel| { 129 | skel.maps.data_data.g_deleting_map_entries = 0; 130 | }); 131 | Ok(r) 132 | } 133 | 134 | fn attach(&mut self, _if_name: &str, if_index: u32) -> Result { 135 | attach( 136 | &self.skel.borrow_dependent().progs.ingress_rev_snat, 137 | &self.skel.borrow_dependent().progs.egress_snat, 138 | if_index, 139 | ) 140 | } 141 | 142 | fn detach(&mut self, links: Self::Links) -> Result<()> { 143 | detach(links) 144 | } 145 | } 146 | 147 | impl EinatEbpfInet for EinatLibbpfSkel { 148 | type MapExternalConfig = Map; 149 | 150 | type MapDestConfigMap = Map; 151 | 152 | fn external_addr(&self) -> Result { 153 | let addr = &self 154 | .skel 155 | .borrow_dependent() 156 | .maps 157 | .data_data 158 | .g_ipv4_external_addr; 159 | let octets: [u8; 4] = bytemuck::bytes_of(addr).try_into().unwrap(); 160 | Ok(Ipv4Net::from_addr(Ipv4Addr::from(octets))) 161 | } 162 | 163 | fn set_external_addr(&mut self, addr: Ipv4Net) -> Result<()> { 164 | self.skel.with_dependent_mut(|_, skel| { 165 | skel.maps.data_data.g_ipv4_external_addr = bytemuck::cast(addr.addr().octets()); 166 | }); 167 | Ok(()) 168 | } 169 | 170 | fn map_external_config(&mut self) -> &mut Self::MapExternalConfig { 171 | &mut self.map_ipv4_external_config 172 | } 173 | 174 | fn map_dest_config(&mut self) -> &mut Self::MapDestConfigMap { 175 | &mut self.map_ipv4_dest_config 176 | } 177 | } 178 | 179 | #[cfg(feature = "ipv6")] 180 | impl EinatEbpfInet for EinatLibbpfSkel { 181 | type MapExternalConfig = Map; 182 | 183 | type MapDestConfigMap = Map; 184 | 185 | fn external_addr(&self) -> Result { 186 | let addr = &self 187 | .skel 188 | .borrow_dependent() 189 | .maps 190 | .data_data 191 | .g_ipv6_external_addr; 192 | let octets: [u8; 16] = bytemuck::bytes_of(addr).try_into().unwrap(); 193 | Ok(Ipv6Net::from_addr(Ipv6Addr::from(octets))) 194 | } 195 | 196 | fn set_external_addr(&mut self, addr: Ipv6Net) -> Result<()> { 197 | self.skel.with_dependent_mut(|_, skel| { 198 | skel.maps.data_data.g_ipv6_external_addr = bytemuck::cast(addr.addr().octets()); 199 | }); 200 | Ok(()) 201 | } 202 | 203 | fn map_external_config(&mut self) -> &mut Self::MapExternalConfig { 204 | &mut self.map_ipv6_external_config 205 | } 206 | 207 | fn map_dest_config(&mut self) -> &mut Self::MapDestConfigMap { 208 | &mut self.map_ipv6_dest_config 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/skel/einat/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | pub mod types; 5 | 6 | #[cfg(feature = "aya")] 7 | pub mod aya; 8 | #[cfg(feature = "libbpf")] 9 | pub mod libbpf; 10 | #[cfg(feature = "libbpf-skel")] 11 | pub mod libbpf_skel; 12 | 13 | #[cfg(any(feature = "libbpf", feature = "libbpf-skel"))] 14 | pub mod libbpf_common; 15 | 16 | #[cfg(any(feature = "aya", feature = "libbpf"))] 17 | mod obj_data; 18 | #[cfg(any(feature = "aya", feature = "libbpf"))] 19 | use obj_data::einat_obj_data; 20 | 21 | use anyhow::Result; 22 | use ipnet::Ipv4Net; 23 | #[cfg(feature = "ipv6")] 24 | use ipnet::Ipv6Net; 25 | 26 | use super::{EbpfHashMapMut, EbpfLpmTrieMut}; 27 | use crate::utils::IpNetwork; 28 | 29 | pub use types::{ 30 | DestConfig, EinatRoData, ExternalConfig, MapBindingKey, MapBindingValue, MapCtKey, MapCtValue, 31 | }; 32 | 33 | #[allow(unused)] 34 | #[derive(Debug)] 35 | pub struct EinatConstConfig { 36 | pub ro_data: types::EinatRoData, 37 | pub frag_track_max_entries: u32, 38 | pub binding_max_entries: u32, 39 | pub ct_max_entries: u32, 40 | pub prefer_tcx: bool, 41 | } 42 | 43 | impl Default for EinatConstConfig { 44 | fn default() -> Self { 45 | Self { 46 | ro_data: Default::default(), 47 | frag_track_max_entries: 0xffff, 48 | binding_max_entries: 0xffff * 3, 49 | ct_max_entries: 0xffff * 3 * 2, 50 | prefer_tcx: true, 51 | } 52 | } 53 | } 54 | 55 | /// Model trait for operations on our einat ebpf resources 56 | pub trait EinatEbpf: Sized { 57 | const NAME: &'static str; 58 | 59 | type MapBinding: EbpfHashMapMut; 60 | 61 | type MapCt: EbpfHashMapMut; 62 | 63 | type Links; 64 | 65 | fn load(config: EinatConstConfig) -> Result; 66 | 67 | fn map_binding(&self) -> &Self::MapBinding; 68 | 69 | fn map_binding_mut(&mut self) -> &mut Self::MapBinding; 70 | 71 | fn map_ct(&self) -> &Self::MapCt; 72 | 73 | fn map_ct_mut(&mut self) -> &mut Self::MapCt; 74 | 75 | fn with_updating T>(&mut self, f: F) -> Result; 76 | 77 | fn with_updating_wait T>(&mut self, f: F) -> Result { 78 | self.with_updating(|this| { 79 | // Wait for 1ms and expecting all previous BPF program calls 80 | // that had not seen g_deleting_map_entries=1 have finished, 81 | // so binding map and CT map become stable. 82 | std::thread::sleep(std::time::Duration::from_millis(1)); 83 | f(this) 84 | }) 85 | } 86 | 87 | fn attach(&mut self, if_name: &str, if_index: u32) -> Result; 88 | 89 | fn detach(&mut self, links: Self::Links) -> Result<()>; 90 | } 91 | 92 | pub trait EinatEbpfInet { 93 | type MapExternalConfig: EbpfLpmTrieMut; 94 | 95 | type MapDestConfigMap: EbpfLpmTrieMut; 96 | 97 | #[allow(unused)] 98 | fn external_addr(&self) -> Result

; 99 | 100 | fn set_external_addr(&mut self, addr: P) -> Result<()>; 101 | 102 | fn map_external_config(&mut self) -> &mut Self::MapExternalConfig; 103 | 104 | fn map_dest_config(&mut self) -> &mut Self::MapDestConfigMap; 105 | } 106 | 107 | #[cfg(not(feature = "ipv6"))] 108 | pub trait EinatEbpfSkel: EinatEbpf + EinatEbpfInet {} 109 | #[cfg(not(feature = "ipv6"))] 110 | impl EinatEbpfSkel for T where T: EinatEbpf + EinatEbpfInet {} 111 | 112 | #[cfg(feature = "ipv6")] 113 | pub trait EinatEbpfSkel: EinatEbpf + EinatEbpfInet + EinatEbpfInet {} 114 | #[cfg(feature = "ipv6")] 115 | impl EinatEbpfSkel for T where T: EinatEbpf + EinatEbpfInet + EinatEbpfInet {} 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | use std::borrow::Borrow; 120 | use std::collections::HashSet; 121 | use std::fmt::Debug; 122 | use std::hash::RandomState; 123 | use std::net; 124 | 125 | use super::*; 126 | use crate::skel::{EbpfHashMap, EbpfLpmTrie, EbpfMapFlags}; 127 | use crate::utils::IpAddress; 128 | 129 | #[cfg(feature = "aya")] 130 | #[test] 131 | #[ignore = "bpf"] 132 | fn test_aya_maps() { 133 | test_skel_maps::(); 134 | test_skel_inet_maps::(); 135 | #[cfg(feature = "ipv6")] 136 | test_skel_inet_maps::(); 137 | 138 | test_skel_attach::(); 139 | } 140 | 141 | #[cfg(feature = "libbpf")] 142 | #[test] 143 | #[ignore = "bpf"] 144 | fn test_libbpf_maps() { 145 | test_skel_maps::(); 146 | test_skel_inet_maps::(); 147 | #[cfg(feature = "ipv6")] 148 | test_skel_inet_maps::(); 149 | 150 | test_skel_attach::(); 151 | } 152 | 153 | #[cfg(feature = "libbpf-skel")] 154 | #[test] 155 | #[ignore = "bpf"] 156 | fn test_libbpf_skel_maps() { 157 | test_skel_maps::(); 158 | test_skel_inet_maps::(); 159 | #[cfg(feature = "ipv6")] 160 | test_skel_inet_maps::(); 161 | 162 | test_skel_attach::(); 163 | } 164 | 165 | fn test_skel_attach() { 166 | let mut skel = T::load(EinatConstConfig::default()).unwrap(); 167 | let links = skel.attach("lo", 1).unwrap(); 168 | skel.detach(links).unwrap(); 169 | } 170 | 171 | // test if key and value struct size matches BTF map and general map operations 172 | fn test_skel_maps() { 173 | let mut skel = T::load(EinatConstConfig::default()).unwrap(); 174 | 175 | skel.with_updating_wait(|_| {}).unwrap(); 176 | 177 | macro_rules! test_map { 178 | ($map:ident, $kt:tt) => {{ 179 | let mut keys: Vec<_> = (0..100) 180 | .map(|i| $kt { 181 | if_index: i, 182 | ..Default::default() 183 | }) 184 | .collect(); 185 | 186 | for k in keys.iter() { 187 | skel.$map() 188 | .update(k, &Default::default(), EbpfMapFlags::NO_EXIST) 189 | .unwrap(); 190 | } 191 | 192 | let mut keys_set = HashSet::<_, RandomState>::from_iter(keys.iter()); 193 | for k in skel.$map().keys() { 194 | let k = k.unwrap(); 195 | assert!(keys_set.remove(k.borrow())); 196 | } 197 | 198 | let key = keys.pop().unwrap(); 199 | 200 | assert!(skel 201 | .$map() 202 | .lookup(&key, EbpfMapFlags::ANY) 203 | .unwrap() 204 | .is_some()); 205 | 206 | skel.$map().delete(&key).unwrap(); 207 | 208 | assert!(skel 209 | .$map() 210 | .lookup(&key, EbpfMapFlags::ANY) 211 | .unwrap() 212 | .is_none()); 213 | 214 | skel.$map().delete_batch(&keys, EbpfMapFlags::ANY).unwrap(); 215 | 216 | for k in keys.iter() { 217 | assert!(skel.$map().lookup(k, EbpfMapFlags::ANY).unwrap().is_none()); 218 | } 219 | }}; 220 | } 221 | 222 | test_map!(map_binding_mut, MapBindingKey); 223 | test_map!(map_ct_mut, MapCtKey); 224 | } 225 | 226 | trait InetKeyGen: Sized + IpAddress { 227 | fn gen_idx(i: u32) -> Self { 228 | Self::gen_idx_len(i, Self::LEN) 229 | } 230 | 231 | fn gen_idx_len(i: u32, len: u8) -> Self; 232 | } 233 | 234 | impl InetKeyGen for Ipv4Net { 235 | fn gen_idx_len(i: u32, len: u8) -> Self { 236 | IpNetwork::from(net::Ipv4Addr::from_bits(i as _), len) 237 | } 238 | } 239 | 240 | #[cfg(feature = "ipv6")] 241 | impl InetKeyGen for Ipv6Net { 242 | fn gen_idx_len(i: u32, len: u8) -> Self { 243 | IpNetwork::from(net::Ipv6Addr::from_bits(i as _), len) 244 | } 245 | } 246 | 247 | fn test_skel_inet_maps< 248 | P: IpNetwork + InetKeyGen + Eq + Debug + Copy, 249 | T: EinatEbpf + EinatEbpfInet

, 250 | >() { 251 | let mut skel = T::load(EinatConstConfig::default()).unwrap(); 252 | let addr = P::gen_idx(1); 253 | skel.set_external_addr(addr).unwrap(); 254 | assert_eq!(addr, skel.external_addr().unwrap()); 255 | 256 | macro_rules! test_map { 257 | ($map:ident) => {{ 258 | let keys: Vec<_> = (0..P::LEN).map(|i| P::gen_idx_len(i as _, i)).collect(); 259 | for k in keys.iter() { 260 | skel.$map() 261 | .update(k, &Default::default(), EbpfMapFlags::NO_EXIST) 262 | .unwrap(); 263 | } 264 | 265 | for k in keys.iter() { 266 | assert!(skel.$map().lookup(k, EbpfMapFlags::ANY).unwrap().is_some()); 267 | } 268 | 269 | for k in keys.iter() { 270 | skel.$map().delete(k).unwrap(); 271 | } 272 | 273 | for k in keys.iter() { 274 | assert!(skel.$map().lookup(k, EbpfMapFlags::ANY).unwrap().is_none()); 275 | } 276 | }}; 277 | } 278 | 279 | test_map!(map_external_config); 280 | test_map!(map_dest_config); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/skel/einat/obj_data.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | macro_rules! einat_obj_bytes { 5 | () => { 6 | include_bytes!(concat!(env!("OUT_DIR"), "/einat.bpf.o")) 7 | }; 8 | } 9 | 10 | #[repr(C, align(32))] 11 | struct Aligned([u8; N]); 12 | const EINAT_OBJ_LEN: usize = einat_obj_bytes!().len(); 13 | const EINAT_OBJ_ALIGNED: Aligned = Aligned(*einat_obj_bytes!()); 14 | 15 | pub const fn einat_obj_data() -> &'static [u8] { 16 | &EINAT_OBJ_ALIGNED.0 17 | } 18 | -------------------------------------------------------------------------------- /src/skel/einat/types.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | #![allow(dead_code)] 4 | 5 | use std::fmt::Debug; 6 | #[cfg(feature = "ipv6")] 7 | use std::net::Ipv6Addr; 8 | use std::net::{IpAddr, Ipv4Addr}; 9 | 10 | use bitflags::bitflags; 11 | use bytemuck::{Pod, Zeroable}; 12 | use ipnet::Ipv4Net; 13 | #[cfg(feature = "ipv6")] 14 | use ipnet::Ipv6Net; 15 | 16 | use crate::derive_pod; 17 | use crate::utils::IpAddress; 18 | 19 | pub const MAP_FRAG_TRACK: &str = "map_frag_track"; 20 | pub const MAP_BINDING: &str = "map_binding"; 21 | pub const MAP_CT: &str = "map_ct"; 22 | pub const MAP_IPV4_EXTERNAL_CONFIG: &str = "map_ipv4_external_config"; 23 | pub const MAP_IPV4_DEST_CONFIG: &str = "map_ipv4_dest_config"; 24 | #[cfg(feature = "ipv6")] 25 | pub const MAP_IPV6_EXTERNAL_CONFIG: &str = "map_ipv6_external_config"; 26 | #[cfg(feature = "ipv6")] 27 | pub const MAP_IPV6_DEST_CONFIG: &str = "map_ipv6_dest_config"; 28 | 29 | pub const PROG_INGRESS_REV_SNAT: &str = "ingress_rev_snat"; 30 | pub const PROG_EGRESS_SNAT: &str = "egress_snat"; 31 | 32 | #[allow(non_snake_case)] 33 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Zeroable, Pod)] 34 | #[repr(C)] 35 | pub struct EinatRoData { 36 | pub LOG_LEVEL: u8, 37 | pub HAS_ETH_ENCAP: u8, 38 | pub INGRESS_IPV4: u8, 39 | pub EGRESS_IPV4: u8, 40 | pub INGRESS_IPV6: u8, 41 | pub EGRESS_IPV6: u8, 42 | pub ENABLE_FIB_LOOKUP_SRC: u8, 43 | pub ALLOW_INBOUND_ICMPX: u8, 44 | pub TIMEOUT_FRAGMENT: u64, 45 | pub TIMEOUT_PKT_MIN: u64, 46 | pub TIMEOUT_PKT_DEFAULT: u64, 47 | pub TIMEOUT_TCP_TRANS: u64, 48 | pub TIMEOUT_TCP_EST: u64, 49 | } 50 | 51 | impl Default for EinatRoData { 52 | fn default() -> Self { 53 | const E9: u64 = 1_000_000_000; 54 | Self { 55 | LOG_LEVEL: 0, 56 | HAS_ETH_ENCAP: 1, 57 | INGRESS_IPV4: 1, 58 | EGRESS_IPV4: 1, 59 | INGRESS_IPV6: 1, 60 | EGRESS_IPV6: 1, 61 | ENABLE_FIB_LOOKUP_SRC: 0, 62 | ALLOW_INBOUND_ICMPX: 1, 63 | TIMEOUT_FRAGMENT: 2 * E9, 64 | TIMEOUT_PKT_MIN: 120 * E9, 65 | TIMEOUT_PKT_DEFAULT: 300 * E9, 66 | TIMEOUT_TCP_TRANS: 240 * E9, 67 | TIMEOUT_TCP_EST: 7440 * E9, 68 | } 69 | } 70 | } 71 | 72 | derive_pod!( 73 | #[repr(C, packed)] 74 | pub struct EinatData { 75 | pub g_ipv4_external_addr: u32, 76 | #[cfg(feature = "ipv6")] 77 | pub g_ipv6_external_addr: [u32; 4], 78 | pub g_deleting_map_entries: u8, 79 | } 80 | ); 81 | 82 | derive_pod!( 83 | #[repr(transparent)] 84 | pub struct InetAddr { 85 | #[cfg(feature = "ipv6")] 86 | pub inner: [u8; 16], 87 | #[cfg(not(feature = "ipv6"))] 88 | pub inner: [u8; 4], 89 | } 90 | ); 91 | derive_pod!( 92 | #[repr(C, align(4))] 93 | pub struct InetTuple { 94 | pub src_addr: InetAddr, 95 | pub dst_addr: InetAddr, 96 | /// Big-endian 97 | pub src_port: u16, 98 | /// Big-endian 99 | pub dst_port: u16, 100 | } 101 | ); 102 | 103 | derive_pod!( 104 | #[repr(C)] 105 | pub struct Ipv4LpmKey { 106 | pub prefix_len: u32, 107 | pub ip: [u8; 4], 108 | } 109 | ); 110 | 111 | #[cfg(feature = "ipv6")] 112 | derive_pod!( 113 | #[repr(C)] 114 | pub struct Ipv6LpmKey { 115 | pub prefix_len: u32, 116 | pub ip: [u8; 16], 117 | } 118 | ); 119 | 120 | derive_pod!( 121 | #[repr(C)] 122 | pub struct PortRange { 123 | pub start_port: u16, 124 | pub end_port: u16, 125 | } 126 | ); 127 | 128 | bitflags! { 129 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default, Zeroable, Pod)] 130 | #[repr(transparent)] 131 | pub struct ExternalFlags: u8 { 132 | const IS_INTERNAL = 0b1; 133 | const NO_SNAT = 0b10; 134 | } 135 | } 136 | 137 | pub const MAX_PORT_RANGES: usize = 4; 138 | 139 | pub type PortRanges = [PortRange; MAX_PORT_RANGES]; 140 | 141 | derive_pod!( 142 | #[repr(C)] 143 | pub struct ExternalConfig { 144 | pub tcp_range: PortRanges, 145 | pub udp_range: PortRanges, 146 | pub icmp_range: PortRanges, 147 | pub icmp_in_range: PortRanges, 148 | pub icmp_out_range: PortRanges, 149 | pub tcp_range_len: u8, 150 | pub udp_range_len: u8, 151 | pub icmp_range_len: u8, 152 | pub icmp_in_range_len: u8, 153 | pub icmp_out_range_len: u8, 154 | pub flags: ExternalFlags, 155 | } 156 | ); 157 | 158 | bitflags! { 159 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default, Zeroable, Pod)] 160 | #[repr(transparent)] 161 | pub struct DestFlags: u8 { 162 | const HAIRPIN = 0b01; 163 | const NO_SNAT = 0b10; 164 | } 165 | } 166 | 167 | derive_pod!( 168 | #[repr(C)] 169 | pub struct DestConfig { 170 | pub flags: DestFlags, 171 | } 172 | ); 173 | 174 | bitflags! { 175 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default, Zeroable, Pod)] 176 | #[repr(transparent)] 177 | pub struct BindingFlags: u8 { 178 | const ORIG_DIR = 0b001; 179 | const ADDR_IPV4 = 0b010; 180 | const ADDR_IPV6 = 0b100; 181 | } 182 | } 183 | derive_pod!( 184 | #[repr(C)] 185 | pub struct MapBindingKey { 186 | pub if_index: u32, 187 | pub flags: BindingFlags, 188 | pub l4proto: u8, 189 | pub from_port: u16, 190 | pub from_addr: InetAddr, 191 | } 192 | ); 193 | derive_pod!( 194 | #[repr(C)] 195 | pub struct MapBindingValue { 196 | pub to_addr: InetAddr, 197 | pub to_port: u16, 198 | pub flags: BindingFlags, 199 | pub is_static: u8, 200 | pub use_: u32, 201 | pub ref_: u32, 202 | pub seq: u32, 203 | } 204 | ); 205 | 206 | derive_pod!( 207 | #[repr(C)] 208 | pub struct MapCtKey { 209 | pub if_index: u32, 210 | pub flags: BindingFlags, 211 | pub l4proto: u8, 212 | pub _pad: u16, 213 | pub external: InetTuple, 214 | } 215 | ); 216 | 217 | derive_pod!( 218 | #[repr(C)] 219 | pub struct MapCtValue { 220 | pub origin: InetTuple, 221 | pub flags: u8, 222 | pub _pad: [u8; 3], 223 | pub state: u32, 224 | pub seq: u32, 225 | pub bpf_timer: [u64; 2], 226 | } 227 | ); 228 | 229 | pub fn ip_address_from_inet_addr<'a, P: IpAddress>(addr: &'a InetAddr) -> P 230 | where 231 | P::Data: TryFrom<&'a [u8], Error: Debug>, 232 | { 233 | P::from_data((&(addr.inner[..(P::LEN as usize) / 8])).try_into().unwrap()) 234 | } 235 | 236 | impl From for InetAddr { 237 | #[cfg(feature = "ipv6")] 238 | fn from(value: Ipv4Addr) -> Self { 239 | let mut res = Self::default(); 240 | let octets = value.octets(); 241 | res.inner[..octets.len()].copy_from_slice(&octets); 242 | res 243 | } 244 | #[cfg(not(feature = "ipv6"))] 245 | fn from(value: Ipv4Addr) -> Self { 246 | Self { 247 | inner: value.octets(), 248 | } 249 | } 250 | } 251 | 252 | #[cfg(feature = "ipv6")] 253 | impl From for InetAddr { 254 | fn from(value: Ipv6Addr) -> Self { 255 | Self { 256 | inner: value.octets(), 257 | } 258 | } 259 | } 260 | 261 | impl From for InetAddr { 262 | fn from(value: IpAddr) -> Self { 263 | match value { 264 | IpAddr::V4(v4) => v4.into(), 265 | #[cfg(feature = "ipv6")] 266 | IpAddr::V6(v6) => v6.into(), 267 | #[cfg(not(feature = "ipv6"))] 268 | IpAddr::V6(_) => { 269 | panic!("unexpected") 270 | } 271 | } 272 | } 273 | } 274 | 275 | impl From for Ipv4LpmKey { 276 | fn from(value: Ipv4Net) -> Self { 277 | Self { 278 | ip: value.addr().octets(), 279 | prefix_len: value.prefix_len() as _, 280 | } 281 | } 282 | } 283 | 284 | #[cfg(feature = "ipv6")] 285 | impl From for Ipv6LpmKey { 286 | fn from(value: Ipv6Net) -> Self { 287 | Self { 288 | ip: value.addr().octets(), 289 | prefix_len: value.prefix_len() as _, 290 | } 291 | } 292 | } 293 | 294 | #[cfg(test)] 295 | mod tests { 296 | use super::*; 297 | 298 | #[test] 299 | fn inet_addr_to_ip_addr() { 300 | let inet_addr = InetAddr { 301 | #[cfg(feature = "ipv6")] 302 | inner: [192, 168, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff], 303 | #[cfg(not(feature = "ipv6"))] 304 | inner: [192, 168, 0, 1], 305 | }; 306 | 307 | assert_eq!( 308 | Ipv4Addr::new(192, 168, 0, 1), 309 | ip_address_from_inet_addr::(&inet_addr) 310 | ); 311 | 312 | #[cfg(feature = "ipv6")] 313 | assert_eq!( 314 | Ipv6Addr::new(0xc0a8, 1, 0, 0, 0, 0, 0, 0xffff), 315 | ip_address_from_inet_addr::(&inet_addr) 316 | ); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/skel/libbpf.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | use std::borrow::{Borrow, BorrowMut}; 5 | use std::fmt::Debug; 6 | use std::mem; 7 | 8 | use anyhow::Result; 9 | use bytemuck::{Pod, Zeroable}; 10 | use libbpf_rs::{MapCore, MapFlags}; 11 | 12 | use super::{EbpfHashMap, EbpfHashMapMut, EbpfLpmTrie, EbpfLpmTrieMut, EbpfMapFlags}; 13 | use crate::utils::{IpAddress, IpNetwork}; 14 | 15 | pub struct LibbpfMap(pub(super) T); 16 | 17 | pub struct ValueVec(Vec); 18 | 19 | impl Borrow for ValueVec { 20 | fn borrow(&self) -> &V { 21 | bytemuck::from_bytes(&self.0) 22 | } 23 | } 24 | 25 | impl BorrowMut for ValueVec { 26 | fn borrow_mut(&mut self) -> &mut V { 27 | bytemuck::from_bytes_mut(&mut self.0) 28 | } 29 | } 30 | 31 | impl From for MapFlags { 32 | fn from(value: EbpfMapFlags) -> Self { 33 | Self::from_bits_retain(value.bits()) 34 | } 35 | } 36 | 37 | impl EbpfHashMap for LibbpfMap { 38 | type BorrowK = ValueVec; 39 | type BorrowV = ValueVec; 40 | 41 | fn keys(&self) -> impl Iterator> { 42 | MapCore::keys(&self.0).map(|item| Ok(ValueVec(item))) 43 | } 44 | 45 | fn lookup(&self, k: &K, flags: EbpfMapFlags) -> Result> { 46 | let v = MapCore::lookup(&self.0, bytemuck::bytes_of(k), Into::into(flags))?.map(ValueVec); 47 | Ok(v) 48 | } 49 | } 50 | 51 | impl EbpfHashMapMut for LibbpfMap { 52 | fn update(&mut self, k: &K, v: &V, flags: EbpfMapFlags) -> Result<()> { 53 | MapCore::update( 54 | &self.0, 55 | bytemuck::bytes_of(k), 56 | bytemuck::bytes_of(v), 57 | Into::into(flags), 58 | )?; 59 | Ok(()) 60 | } 61 | 62 | fn delete(&mut self, k: &K) -> Result<()> { 63 | MapCore::delete(&self.0, bytemuck::bytes_of(k))?; 64 | Ok(()) 65 | } 66 | 67 | fn delete_batch<'i>( 68 | &mut self, 69 | keys: impl IntoIterator, 70 | elem_flags: EbpfMapFlags, 71 | ) -> Result<()> { 72 | let mut keys_raw = Vec::new(); 73 | for key in keys { 74 | keys_raw.extend(bytemuck::bytes_of(key)); 75 | } 76 | let count = (keys_raw.len() / mem::size_of::()) as u32; 77 | 78 | MapCore::delete_batch( 79 | &self.0, 80 | &keys_raw, 81 | count, 82 | Into::into(elem_flags), 83 | MapFlags::ANY, 84 | )?; 85 | Ok(()) 86 | } 87 | } 88 | 89 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Zeroable, Pod)] 90 | #[repr(C, packed)] 91 | struct LpmKey { 92 | pub prefix_len: u32, 93 | pub data: K, 94 | } 95 | 96 | impl From<&T> for LpmKey<::Data> { 97 | fn from(value: &T) -> Self { 98 | Self { 99 | prefix_len: value.prefix_len() as _, 100 | data: value.addr().data(), 101 | } 102 | } 103 | } 104 | 105 | impl EbpfLpmTrie for LibbpfMap 106 | where 107 | ::Data: Pod, 108 | { 109 | type BorrowV = ValueVec; 110 | 111 | fn lookup(&self, k: &K, flags: EbpfMapFlags) -> Result> { 112 | let k = &LpmKey::from(k); 113 | let v = MapCore::lookup(&self.0, bytemuck::bytes_of(k), Into::into(flags))?.map(ValueVec); 114 | Ok(v) 115 | } 116 | } 117 | 118 | impl EbpfLpmTrieMut for LibbpfMap 119 | where 120 | ::Data: Pod, 121 | { 122 | fn update(&mut self, k: &K, v: &V, flags: EbpfMapFlags) -> Result<()> { 123 | let k = &LpmKey::from(k); 124 | MapCore::update( 125 | &self.0, 126 | bytemuck::bytes_of(k), 127 | bytemuck::bytes_of(v), 128 | Into::into(flags), 129 | )?; 130 | Ok(()) 131 | } 132 | 133 | fn delete(&mut self, k: &K) -> Result<()> { 134 | let k = &LpmKey::from(k); 135 | MapCore::delete(&self.0, bytemuck::bytes_of(k))?; 136 | Ok(()) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/skel/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | #[cfg(feature = "aya")] 5 | mod aya; 6 | #[cfg(any(feature = "libbpf", feature = "libbpf-skel"))] 7 | mod libbpf; 8 | 9 | pub mod einat; 10 | 11 | use std::borrow::BorrowMut; 12 | 13 | use anyhow::Result; 14 | use bitflags::bitflags; 15 | 16 | pub use einat::types::*; 17 | 18 | use crate::utils::IpNetwork; 19 | 20 | bitflags! { 21 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] 22 | #[repr(transparent)] 23 | pub struct EbpfMapFlags: u64 { 24 | const ANY = 0; 25 | const NO_EXIST = 0b1; 26 | const EXIST = 0b10; 27 | const F_LOCK = 0b100; 28 | } 29 | } 30 | 31 | pub trait EbpfHashMap { 32 | type BorrowK: BorrowMut; 33 | type BorrowV: BorrowMut; 34 | 35 | fn keys(&self) -> impl Iterator>; 36 | 37 | fn lookup(&self, k: &K, flags: EbpfMapFlags) -> Result>; 38 | } 39 | 40 | pub trait EbpfHashMapMut: EbpfHashMap { 41 | #[allow(unused)] 42 | fn update(&mut self, k: &K, v: &V, flags: EbpfMapFlags) -> Result<()>; 43 | 44 | #[allow(unused)] 45 | fn delete(&mut self, k: &K) -> Result<()>; 46 | 47 | fn delete_batch<'i>( 48 | &mut self, 49 | keys: impl IntoIterator, 50 | elem_flags: EbpfMapFlags, 51 | ) -> Result<()>; 52 | } 53 | 54 | pub trait EbpfLpmTrie { 55 | type BorrowV: BorrowMut; 56 | 57 | #[allow(unused)] 58 | fn lookup(&self, k: &K, flags: EbpfMapFlags) -> Result>; 59 | } 60 | 61 | pub trait EbpfLpmTrieMut: EbpfLpmTrie { 62 | fn update(&mut self, k: &K, v: &V, flags: EbpfMapFlags) -> Result<()>; 63 | 64 | fn delete(&mut self, k: &K) -> Result<()>; 65 | } 66 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Huang-Huang Bao 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | //! Model for configuration variables and maps of our eBPF application 4 | #[cfg(feature = "ipv6")] 5 | use std::net::Ipv6Addr; 6 | use std::net::{IpAddr, Ipv4Addr}; 7 | 8 | #[cfg(feature = "ipv6")] 9 | use ipnet::Ipv6Net; 10 | use ipnet::{IpNet, Ipv4Net}; 11 | use prefix_trie::{map::Iter as PrefixMapIter, Prefix, PrefixMap}; 12 | 13 | pub enum MapChange<'a, P, T> { 14 | Insert { 15 | key: &'a P, 16 | value: &'a T, 17 | }, 18 | Update { 19 | key: &'a P, 20 | old: &'a T, 21 | value: &'a T, 22 | }, 23 | Delete { 24 | key: &'a P, 25 | old: &'a T, 26 | }, 27 | } 28 | 29 | pub struct PrefixMapDiff<'a, P, T> { 30 | map_a: &'a PrefixMap, 31 | map_b: &'a PrefixMap, 32 | map_a_iter: PrefixMapIter<'a, P, T>, 33 | map_b_iter: PrefixMapIter<'a, P, T>, 34 | map_a_finished: bool, 35 | } 36 | 37 | impl<'a, P, T> PrefixMapDiff<'a, P, T> { 38 | pub fn new(map_a: &'a PrefixMap, map_b: &'a PrefixMap) -> Self { 39 | Self { 40 | map_a, 41 | map_b, 42 | map_a_iter: map_a.iter(), 43 | map_b_iter: map_b.iter(), 44 | map_a_finished: false, 45 | } 46 | } 47 | } 48 | impl<'a, P, T> Iterator for PrefixMapDiff<'a, P, T> 49 | where 50 | P: Prefix, 51 | T: PartialEq, 52 | { 53 | type Item = MapChange<'a, P, T>; 54 | 55 | fn next(&mut self) -> Option { 56 | if !self.map_a_finished { 57 | for (key, value) in self.map_a_iter.by_ref() { 58 | if let Some(other_value) = self.map_b.get(key) { 59 | if other_value != value { 60 | return Some(MapChange::Update { 61 | key, 62 | old: value, 63 | value: other_value, 64 | }); 65 | } 66 | } else { 67 | return Some(MapChange::Delete { key, old: value }); 68 | } 69 | } 70 | self.map_a_finished = true 71 | } 72 | 73 | for (key, value) in self.map_b_iter.by_ref() { 74 | if self.map_a.get(key).is_none() { 75 | return Some(MapChange::Insert { key, value }); 76 | } 77 | } 78 | 79 | None 80 | } 81 | } 82 | 83 | pub trait IpAddress: Sized { 84 | type Data; 85 | const LEN: u8; 86 | 87 | fn is_unspecified(&self) -> bool; 88 | 89 | fn ip_addr(&self) -> IpAddr; 90 | 91 | fn data(&self) -> Self::Data; 92 | 93 | fn from_data(data: Self::Data) -> Self; 94 | 95 | fn from_ip_addr(addr: IpAddr) -> Option; 96 | 97 | fn unspecified() -> Self; 98 | } 99 | 100 | #[allow(dead_code)] 101 | pub trait IpNetwork: Sized { 102 | type Addr: IpAddress; 103 | 104 | fn prefix_len(&self) -> u8; 105 | 106 | fn addr(&self) -> Self::Addr; 107 | 108 | fn from(addr: Self::Addr, prefix_len: u8) -> Self; 109 | 110 | fn from_ipnet(net: IpNet) -> Option; 111 | 112 | fn from_addr(addr: Self::Addr) -> Self { 113 | Self::from(addr, Self::Addr::LEN) 114 | } 115 | } 116 | 117 | impl IpAddress for Ipv4Addr { 118 | type Data = [u8; 4]; 119 | const LEN: u8 = 32; 120 | 121 | fn is_unspecified(&self) -> bool { 122 | self.is_unspecified() 123 | } 124 | 125 | fn ip_addr(&self) -> IpAddr { 126 | IpAddr::V4(*self) 127 | } 128 | 129 | fn data(&self) -> Self::Data { 130 | self.octets() 131 | } 132 | 133 | fn from_data(data: Self::Data) -> Self { 134 | From::from(data) 135 | } 136 | 137 | fn from_ip_addr(addr: IpAddr) -> Option { 138 | if let IpAddr::V4(v4) = addr { 139 | Some(v4) 140 | } else { 141 | None 142 | } 143 | } 144 | 145 | fn unspecified() -> Self { 146 | Self::UNSPECIFIED 147 | } 148 | } 149 | 150 | #[cfg(feature = "ipv6")] 151 | impl IpAddress for Ipv6Addr { 152 | type Data = [u8; 16]; 153 | const LEN: u8 = 128; 154 | 155 | fn is_unspecified(&self) -> bool { 156 | self.is_unspecified() 157 | } 158 | 159 | fn ip_addr(&self) -> IpAddr { 160 | IpAddr::V6(*self) 161 | } 162 | 163 | fn data(&self) -> Self::Data { 164 | self.octets() 165 | } 166 | 167 | fn from_data(data: Self::Data) -> Self { 168 | From::from(data) 169 | } 170 | 171 | fn from_ip_addr(addr: IpAddr) -> Option { 172 | if let IpAddr::V6(v6) = addr { 173 | Some(v6) 174 | } else { 175 | None 176 | } 177 | } 178 | 179 | fn unspecified() -> Self { 180 | Self::UNSPECIFIED 181 | } 182 | } 183 | 184 | impl IpNetwork for Ipv4Net { 185 | type Addr = Ipv4Addr; 186 | 187 | fn prefix_len(&self) -> u8 { 188 | Ipv4Net::prefix_len(self) 189 | } 190 | 191 | fn addr(&self) -> Self::Addr { 192 | Ipv4Net::addr(self) 193 | } 194 | 195 | fn from(addr: Self::Addr, prefix_len: u8) -> Self { 196 | Ipv4Net::new_assert(addr, prefix_len) 197 | } 198 | 199 | fn from_ipnet(net: IpNet) -> Option { 200 | if let IpNet::V4(v4) = net { 201 | Some(v4) 202 | } else { 203 | None 204 | } 205 | } 206 | } 207 | 208 | #[cfg(feature = "ipv6")] 209 | impl IpNetwork for Ipv6Net { 210 | type Addr = Ipv6Addr; 211 | 212 | fn prefix_len(&self) -> u8 { 213 | Ipv6Net::prefix_len(self) 214 | } 215 | 216 | fn addr(&self) -> Self::Addr { 217 | Ipv6Net::addr(self) 218 | } 219 | 220 | fn from(addr: Self::Addr, prefix_len: u8) -> Self { 221 | Ipv6Net::new_assert(addr, prefix_len) 222 | } 223 | 224 | fn from_ipnet(net: IpNet) -> Option { 225 | if let IpNet::V6(v6) = net { 226 | Some(v6) 227 | } else { 228 | None 229 | } 230 | } 231 | } 232 | 233 | impl IpAddress for T 234 | where 235 | T: IpNetwork, 236 | T::Addr: IpAddress, 237 | { 238 | type Data = ::Data; 239 | const LEN: u8 = ::LEN; 240 | 241 | fn is_unspecified(&self) -> bool { 242 | self.prefix_len() == Self::LEN && self.addr().is_unspecified() 243 | } 244 | 245 | fn ip_addr(&self) -> IpAddr { 246 | self.addr().ip_addr() 247 | } 248 | 249 | fn data(&self) -> Self::Data { 250 | self.addr().data() 251 | } 252 | 253 | fn from_data(data: Self::Data) -> Self { 254 | Self::from_addr(T::Addr::from_data(data)) 255 | } 256 | 257 | fn from_ip_addr(addr: IpAddr) -> Option { 258 | let addr = T::Addr::from_ip_addr(addr)?; 259 | Some(Self::from_addr(addr)) 260 | } 261 | 262 | fn unspecified() -> Self { 263 | Self::from_addr(T::Addr::unspecified()) 264 | } 265 | } 266 | 267 | #[cfg(test)] 268 | mod tests { 269 | use super::*; 270 | use ipnet::Ipv4Net; 271 | 272 | #[test] 273 | fn map_diff() { 274 | let mut map_a = PrefixMap::::new(); 275 | let mut map_b = PrefixMap::::new(); 276 | 277 | map_a.insert("192.168.0.0/24".parse().unwrap(), "to delete".to_string()); 278 | map_a.insert("192.168.0.0/25".parse().unwrap(), "unchanged".to_string()); 279 | map_a.insert("192.168.0.0/26".parse().unwrap(), "before".to_string()); 280 | 281 | map_b.insert("192.168.0.0/25".parse().unwrap(), "unchanged".to_string()); 282 | map_b.insert("192.168.0.0/26".parse().unwrap(), "to update".to_string()); 283 | map_b.insert("192.168.0.0/27".parse().unwrap(), "to insert".to_string()); 284 | 285 | let mut deleted = Vec::new(); 286 | let mut updated = Vec::new(); 287 | let mut inserted = Vec::new(); 288 | for change in PrefixMapDiff::new(&map_a, &map_b) { 289 | match change { 290 | MapChange::Delete { key, .. } => deleted.push(*key), 291 | MapChange::Update { key, value, .. } => updated.push((*key, value.clone())), 292 | MapChange::Insert { key, value } => inserted.push((*key, value.clone())), 293 | } 294 | } 295 | 296 | assert_eq!(vec!["192.168.0.0/24".parse::().unwrap()], deleted); 297 | assert_eq!( 298 | vec![( 299 | "192.168.0.0/26".parse::().unwrap(), 300 | "to update".to_string() 301 | ),], 302 | updated 303 | ); 304 | assert_eq!( 305 | vec![( 306 | "192.168.0.0/27".parse::().unwrap(), 307 | "to insert".to_string() 308 | )], 309 | inserted 310 | ); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /tests/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | ip -all netns delete 5 | 6 | ip netns add server1 7 | ip netns add server2 8 | ip netns add router 9 | ip netns add device1 10 | ip netns add device2 11 | 12 | ip link add veth_r_s1 netns router type veth peer veth_s1_r netns server1 13 | ip link add veth_r_s2 netns router type veth peer veth_s2_r netns server2 14 | 15 | ip link add br-lan netns router type bridge 16 | ip link add veth_r_d1 netns router type veth peer veth_d1_r netns device1 17 | ip link add veth_r_d2 netns router type veth peer veth_d2_r netns device2 18 | 19 | # server: setup network 20 | ip netns exec server1 ip addr add 10.0.1.1/24 dev veth_s1_r 21 | ip netns exec server1 ip addr add 10.0.1.2/24 dev veth_s1_r 22 | ip netns exec server1 ip link set veth_s1_r up 23 | # server2 24 | ip netns exec server2 ip addr add 10.0.2.1/24 dev veth_s2_r 25 | ip netns exec server2 ip addr add 10.0.2.2/24 dev veth_s2_r 26 | ip netns exec server2 ip link set veth_s2_r up 27 | 28 | # router: setup external networks 29 | ip netns exec router ip addr add 10.0.1.100/24 dev veth_r_s1 30 | ip netns exec router ip link set veth_r_s1 up 31 | ip netns exec router ip addr add 10.0.2.100/24 dev veth_r_s2 32 | ip netns exec router ip link set veth_r_s2 up 33 | 34 | # router: setup LAN 35 | ip netns exec router ip link set veth_r_d1 master br-lan 36 | ip netns exec router ip link set veth_r_d2 master br-lan 37 | ip netns exec router ip addr add 192.168.1.1/24 dev br-lan 38 | ip netns exec router ip link set br-lan up 39 | ip netns exec router ip link set veth_r_d1 up 40 | ip netns exec router ip link set veth_r_d2 up 41 | 42 | # router: enable forwarding 43 | ip netns exec router sysctl net.ipv4.ip_forward=1 44 | 45 | # device: setup network 46 | ip netns exec device1 ip addr add 192.168.1.100/24 dev veth_d1_r 47 | ip netns exec device1 ip link set veth_d1_r up 48 | ip netns exec device1 ip route add default via 192.168.1.1 dev veth_d1_r 49 | # device2 50 | ip netns exec device2 ip addr add 192.168.1.200/24 dev veth_d2_r 51 | ip netns exec device2 ip link set veth_d2_r up 52 | ip netns exec device2 ip route add default via 192.168.1.1 dev veth_d2_r 53 | 54 | # router: show networking info 55 | ip netns exec router ip link show 56 | ip netns exec router ip addr show 57 | ip netns exec router ip route show 58 | 59 | # start our program 60 | ip netns exec router ./target/debug/einat -i veth_r_s1 --bpf-log 5 >/dev/null 2>&1 & 61 | ip netns exec router ./target/debug/einat -i veth_r_s2 >/dev/null 2>&1 & 62 | sleep 1 63 | 64 | # 65 | # test network connectivity 66 | # 67 | # router to servers 68 | ip netns exec router ping -c1 10.0.1.1 69 | ip netns exec router ping -c1 10.0.2.1 70 | 71 | # devices to router 72 | ip netns exec device1 ping -c1 192.168.1.1 73 | ip netns exec device2 ping -c1 192.168.1.1 74 | 75 | # devices to servers, would be SNAT by router 76 | ip netns exec device1 ping -c1 10.0.1.1 77 | ip netns exec device1 ping -c1 10.0.2.1 78 | # device2 79 | ip netns exec device2 ping -c1 10.0.1.1 80 | ip netns exec device2 ping -c1 10.0.2.1 81 | 82 | # Create unreplied conntracks in router. 83 | # Make sure if we add one of these beforehand, the created conntrack would not block connection from server's 3479 to device's 29999. 84 | ip netns exec server1 nc -uq0 -s 10.0.1.1 -p 3479 10.0.1.100 29999 <<<"test" 85 | ip netns exec server1 nc -uq0 -s 10.0.1.2 -p 3479 10.0.1.100 29999 <<<"test" 86 | ip netns exec router nc -uq0 -s 10.0.1.100 -p 29999 10.0.1.1 3479 <<<"test" 87 | ip netns exec router nc -uq0 -s 10.0.1.100 -p 29999 10.0.1.2 3479 <<<"test" 88 | ip netns exec device2 nc -uq0 -p 29999 10.0.1.1 3479 <<<"test" 89 | ip netns exec device1 nc -uq0 -p 29999 10.0.1.1 3479 <<<"test" 90 | ip netns exec device2 nc -uq0 -p 29999 10.0.1.2 3479 <<<"test" 91 | 92 | # start stunserver in servers 93 | ip netns exec server1 stunserver --mode full --primaryinterface 10.0.1.1 --altinterface 10.0.1.2 & 94 | ip netns exec server2 stunserver --mode full --primaryinterface 10.0.2.1 --altinterface 10.0.2.2 & 95 | sleep 1 96 | 97 | # STUN NAT behavior test with our program 98 | ip netns exec device1 stunclient --mode full --localport 29999 10.0.1.1 99 | 100 | ip netns exec device1 stunclient --mode full --localport 29999 10.0.1.1 | grep -z "Endpoint Independent Mapping.*Endpoint Independent Filtering" 101 | ip netns exec device2 stunclient --mode full --localport 29999 10.0.1.1 | grep -z "Endpoint Independent Mapping.*Endpoint Independent Filtering" 102 | ip netns exec device1 stunclient --mode full --localport 29999 10.0.2.1 | grep -z "Endpoint Independent Mapping.*Endpoint Independent Filtering" 103 | ip netns exec device2 stunclient --mode full --localport 29999 10.0.2.1 | grep -z "Endpoint Independent Mapping.*Endpoint Independent Filtering" 104 | 105 | ip netns delete device2 106 | ip netns delete device1 107 | ip netns delete router 108 | ip netns delete server2 109 | ip netns delete server1 110 | 111 | kill -KILL $(jobs -p) 112 | --------------------------------------------------------------------------------