├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitignore ├── LICENSE ├── README.txt ├── SPONSORS.md ├── api ├── api.$progname.h.in ├── api.h.in └── xmake.lua ├── bpf ├── block_ip.bpf.c ├── block_port.bpf.c ├── block_private_ipv4.bpf.c ├── commons.bpf.h ├── nop.bpf.c └── xmake.lua ├── test ├── cli.bats ├── cli.flags.bats ├── cni.bats ├── fixtures │ └── cni │ │ ├── attach_in.json │ │ └── attach_out.json ├── helpers.bash └── net.bash ├── tools ├── .gitignore └── xmake.lua ├── traffico-cni.c ├── traffico.c ├── vmlinux ├── .gitignore └── xmake.lua ├── xmake.lua └── xmake ├── modules └── api.lua └── repos.lua /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/archlinux:latest 2 | 3 | RUN pacman -Syy --noconfirm 4 | RUN pacman -S --noconfirm nodejs clang llvm gcc linux-headers bpf unzip docker vi vim 5 | 6 | RUN pacman -S --noconfirm --needed git base-devel 7 | 8 | 9 | 10 | # user setup 11 | WORKDIR /tmp 12 | RUN useradd -m -r -u 1000 vscode -s /bin/bash 13 | RUN echo '%vscode ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers 14 | USER vscode 15 | 16 | RUN git clone https://aur.archlinux.org/yay.git && cd yay && makepkg --noconfirm -si 17 | RUN yay --noconfirm -S xmake 18 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "Dockerfile" 4 | }, 5 | "customizations": { 6 | "vscode": { 7 | "extensions": [ 8 | "ms-vscode.cpptools", 9 | "tboox.xmake-vscode" 10 | ] 11 | } 12 | }, 13 | "runArgs": [ 14 | "--privileged" 15 | ], 16 | "forwardPorts": [] 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .xmake/ 3 | build/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Leonardo Di Donato & Lorenzo Fontana 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | _ _ 2 | | | | | o 3 | _|_ ,_ __, | | | | __ __ 4 | | / | / | |/ |/ | / / \_ 5 | |_/ |_/\_/|_/|__/|__/|_/\___/\__/ 6 | |\ |\ 7 | |/ |/ 8 | 9 | README 10 | 11 | traffico is a collection of tools to shape traffic on a network using traffic control tc(8). 12 | It can be used via a CLI tool (traffico) or as a CNI plugin (traffico-cni). 13 | For a list of the available programs and what they do see the BUILT-IN PROGRAMS section. 14 | 15 | The BUILT-IN PROGRAMS are very opinionated and made for the needs of the authors but the framework 16 | is flexible enough to be used for other purposes. You can add programs to the bpf/ directory 17 | to extend it to other use cases. 18 | 19 | CONTACT 20 | 21 | If you have problems, question, ideas or suggestions, please contact us by 22 | posting to https://github.com/leodido/traffico/issues. 23 | 24 | DOWNLOAD 25 | 26 | To download the very latest source do this: 27 | 28 | git clone https://github.com/leodido/traffico.git 29 | 30 | AUTHORS 31 | 32 | Leonardo Di Donato 33 | Lorenzo Fontana 34 | 35 | USAGE 36 | 37 | Traffico can be either used standalone or as a CNI plugin. 38 | 39 | traffico 40 | traffico is a CLI tool that can be used to load and unload the programs. 41 | You can choose an interface and choose whether the program will be loaded in 42 | "INGRESS" or "EGRESS". 43 | 44 | Example usage: 45 | traffico --ifname=eth0 --at=INGRESS block_private_ipv4 46 | 47 | traffico-cni 48 | traffico-cni is a meta CNI plugin that allows the traffico programs to be used in CNI. 49 | 50 | Meta means that traffic-cni does not create any interface for you, 51 | it is intended to be used as a chained CNI plugin. 52 | 53 | The plugin block to use traffico-cni is very similar to how traffico is 54 | used as a CLI tool. 55 | 56 | { 57 | "type": "traffico-cni", 58 | "program": "block_private_ipv4", 59 | "attachPoint": "ingress" 60 | } 61 | 62 | Here's an example CNI config file featuring traffico-cni. 63 | 64 | { 65 | "name": "mynetwork", 66 | "cniVersion": "0.4.0", 67 | "plugins": [ 68 | { 69 | "type": "ptp", 70 | "ipMasq": true, 71 | "ipam": { 72 | "type": "host-local", 73 | "subnet": "10.10.10.0/24", 74 | "resolvConf": "/etc/resolv.conf", 75 | "routes": [ 76 | { "dst": "0.0.0.0/0" } 77 | ] 78 | }, 79 | "dns": { 80 | "nameservers": ["1.1.1.1", "1.0.0.1"] 81 | } 82 | }, 83 | { 84 | "type": "firewall" 85 | }, 86 | { 87 | "type": "traffico-cni", 88 | "program": "block_private_ipv4", 89 | "attachPoint": "ingress" 90 | }, 91 | { 92 | "type": "tc-redirect-tap" 93 | } 94 | ] 95 | } 96 | 97 | BUILT-IN PROGRAMS 98 | 99 | block_private_ipv4 100 | block_private_ipv4 is a program that can be used to block 101 | private IPv4 addresses subnets allowing only SSH access on port 22. 102 | 103 | block_ip 104 | block_ip is a program that drops packets with destination equal to the 105 | input IPv4 address. 106 | 107 | block_port 108 | block_port is a program that drops packets with the destination port 109 | equal to the input port number. 110 | 111 | nop 112 | nop is a simple program that does nothing. 113 | 114 | BUILD 115 | 116 | To compile traffico from source you either provide your `vmlinux.h` in the 117 | `vmlinux/` directory (default option) or you configure the project to 118 | generate one from your current Linux kernel: 119 | 120 | xmake f --generate-vmlinux=y 121 | 122 | Now you will be able to build traffico from source by running: 123 | 124 | xmake 125 | 126 | In case you only want to compile the BPF programs you can do this: 127 | 128 | xmake -b bpf 129 | 130 | TEST 131 | 132 | To run the test suite you can do this: 133 | 134 | xmake -b test 135 | xmake run test 136 | -------------------------------------------------------------------------------- /SPONSORS.md: -------------------------------------------------------------------------------- 1 | # Community Sponsorship 2 | 3 | ## Sponsors 4 | 5 | Support this project by [becoming a sponsor](https://github.com/sponsors/leodido). 6 | 7 | Depending on the sponsorship tier, your logo will show up here with a link to your website. 8 | 9 | [![Garnet Labs Inc](https://user-images.githubusercontent.com/120051/190853826-c9465539-7f98-4a61-901c-efd1e739c936.png)](https://garnet.ai) 10 | -------------------------------------------------------------------------------- /api/api.$progname.h.in: -------------------------------------------------------------------------------- 1 | #include "${PROGNAME}.skel.h" 2 | 3 | int ${OPERATION}${PROGNAME}(struct config *conf, after_attach_fn_t cb, bpf_obj_fn_t obj_cb) 4 | { 5 | int err; 6 | char buf[100]; 7 | buf[sizeof(buf) - 1] = '\0'; 8 | 9 | // Skeleton 10 | struct ${PROGNAME}_bpf *obj = NULL; 11 | obj = ${PROGNAME}_bpf__open(); 12 | if (!obj) 13 | { 14 | log_err(conf, "fail: opening the eBPF skeleton\n"); 15 | return 1; 16 | } 17 | 18 | if (obj_cb) { 19 | err = obj_cb(obj); 20 | if (err) 21 | { 22 | fprintf(stderr, "traffico: fail calling obj callback\n"); 23 | goto destroy_${PROGNAME}; 24 | } 25 | } 26 | 27 | err = ${PROGNAME}_bpf__load(obj); 28 | if (err) 29 | { 30 | libbpf_strerror(err, buf, sizeof(buf)); 31 | log_err(conf, "fail: loading the eBPF skeleton: %s\n", buf); 32 | goto destroy_${PROGNAME}; 33 | } 34 | 35 | DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, .ifindex = conf->ifindex, .attach_point = conf->attach_point); 36 | err = bpf_tc_hook_create(&hook); 37 | if (err) 38 | { 39 | // Moving on in case the hook file already exists 40 | // TODO ? make this behavior configurable from arguments 41 | if (err != -EEXIST) 42 | { 43 | libbpf_strerror(err, buf, sizeof(buf)); 44 | log_err(conf, "fail: creating the qdisc: %s\n", buf); 45 | goto destroy_${PROGNAME}; 46 | } 47 | log_out(conf, "done: hook already existing, using it\n"); 48 | } 49 | 50 | // Attach the TC eBPF program to the qdisc 51 | int fd = bpf_program__fd(obj->progs.${PROGNAME}); 52 | DECLARE_LIBBPF_OPTS(bpf_tc_opts, opts, .prog_fd = fd, .flags = BPF_TC_F_REPLACE); 53 | err = bpf_tc_attach(&hook, &opts); 54 | if (err) 55 | { 56 | libbpf_strerror(err, buf, sizeof(buf)); 57 | log_err(conf, "fail: attaching the TC eBPF program: %s\n", buf); 58 | goto cleanup_${PROGNAME}; 59 | } 60 | log_out(conf, "done: attaching the TC eBPF program\n"); 61 | log_out(conf, "opts: handle: 0x%x\n", opts.handle); 62 | log_out(conf, "opts: priority: %d\n", opts.priority); 63 | log_out(conf, "opts: program ID: %d\n", opts.prog_id); 64 | 65 | err = cb(hook, opts); 66 | 67 | if (conf->cleanup_on_exit) 68 | { 69 | opts.prog_fd = opts.prog_id = 0; 70 | opts.flags = 0; 71 | err = bpf_tc_detach(&hook, &opts); 72 | if (err) 73 | { 74 | libbpf_strerror(err, buf, sizeof(buf)); 75 | log_err(conf, "fail: detaching the TC eBPF program: %s\n", buf); 76 | } 77 | log_out(conf, "done: detaching the TC eBPF program\n"); 78 | } 79 | 80 | cleanup_${PROGNAME}: 81 | if (conf->cleanup_on_exit) 82 | { 83 | // Force the cleanup of the qdisc as well 84 | hook.attach_point |= BPF_TC_INGRESS; 85 | err = bpf_tc_hook_destroy(&hook); 86 | if (err) 87 | { 88 | libbpf_strerror(err, buf, sizeof(buf)); 89 | log_err(conf, "fail: destroying the qdisc: %s\n", buf); 90 | } 91 | log_out(conf, "done: destroying the qdisc\n"); 92 | } 93 | 94 | destroy_${PROGNAME}: 95 | if (conf->cleanup_on_exit) 96 | { 97 | ${PROGNAME}_bpf__destroy(obj); 98 | } 99 | 100 | return err < 0 ? -err : err; 101 | } 102 | -------------------------------------------------------------------------------- /api/api.h.in: -------------------------------------------------------------------------------- 1 | #ifndef TRAFFICO_API_H 2 | #define TRAFFICO_API_H 3 | 4 | #include 5 | #include 6 | 7 | /// defines 8 | #define TOOL_NAME "traffico" 9 | #define NUM_PROGRAMS ${PROGRAMS_COUNT} 10 | #define PROGRAMS_DESCRIPTION ${PROGRAMS_DESCRIPTION} 11 | 12 | /// globals 13 | static const char *g_programs_name[NUM_PROGRAMS] = {${PROGRAMS_AS_STRINGS}}; 14 | 15 | /// types 16 | enum program {${PROGRAMS_AS_SYMBOLS}}; 17 | 18 | typedef enum program program_t; 19 | 20 | struct config 21 | { 22 | bool verbose; 23 | char ifname[IF_NAMESIZE]; 24 | int ifindex; 25 | enum bpf_tc_attach_point attach_point; 26 | 27 | bool cleanup_on_exit; 28 | program_t program; 29 | char *program_arg; 30 | FILE *err_stream; 31 | FILE *out_stream; 32 | }; 33 | 34 | typedef int (*bpf_obj_fn_t)(void *obj); 35 | 36 | typedef int (*after_attach_fn_t)(struct bpf_tc_hook hook, struct bpf_tc_opts opts); 37 | 38 | typedef int (*attach_fn_t)(struct config *conf, after_attach_fn_t cb, bpf_obj_fn_t obj_cb); 39 | 40 | /// logging 41 | int print_log(FILE *f, bool verbosity, bool prefix, const char *fmt, va_list argptr) 42 | { 43 | int res; 44 | if (!verbosity) 45 | { 46 | return 0; 47 | } 48 | 49 | if (prefix) 50 | { 51 | fprintf(f, TOOL_NAME ": "); 52 | } 53 | res = vfprintf(f, fmt, argptr); 54 | 55 | return res; 56 | } 57 | 58 | void log_err(struct config *cfg, const char *fmt, ...) 59 | { 60 | va_list argptr; 61 | va_start(argptr, fmt); 62 | print_log(cfg->err_stream, cfg->verbose, true, fmt, argptr); 63 | va_end(argptr); 64 | } 65 | 66 | void log_out(struct config *cfg, const char *fmt, ...) 67 | { 68 | va_list argptr; 69 | va_start(argptr, fmt); 70 | print_log(cfg->out_stream, cfg->verbose, true, fmt, argptr); 71 | va_end(argptr); 72 | } 73 | 74 | /// do nothing after attach 75 | int exit_after_attach(struct bpf_tc_hook hook, struct bpf_tc_opts opts) 76 | { 77 | return 0; 78 | } 79 | 80 | /// non-existing programs 81 | int ${OPERATION}0(struct config *conf, after_attach_fn_t cb, bpf_obj_fn_t obj_cb) 82 | { 83 | return 1; 84 | } 85 | 86 | ${API} 87 | 88 | /// dispatch 89 | attach_fn_t attach_fn[NUM_PROGRAMS] = { ${PROGRAMS_OPS_AS_SYMBOLS} }; 90 | 91 | int attach(struct config *conf, after_attach_fn_t cb, bpf_obj_fn_t obj_cb) 92 | { 93 | attach_fn_t fn = attach_fn[conf->program]; 94 | if (fn) { 95 | return fn(conf, cb, obj_cb); 96 | } 97 | return ${OPERATION}0(conf, cb, obj_cb); 98 | } 99 | 100 | 101 | 102 | #endif // TRAFFICO_API_H -------------------------------------------------------------------------------- /api/xmake.lua: -------------------------------------------------------------------------------- 1 | 2 | set_xmakever("2.6.1") -- Minimum version to compile BPF source correctly 3 | 4 | -- includes 5 | includes("../xmake/repos.lua") 6 | 7 | -- rules 8 | add_rules("mode.release", "mode.debug") 9 | 10 | -- target to generate API components for every BPF program 11 | target("every.api") 12 | set_kind("headeronly") 13 | includes("../bpf") 14 | add_deps("bpf") 15 | on_config(function(target) 16 | import("xmake.modules.api", { rootdir = os.projectdir() }) 17 | api.gen(target, "bpf") 18 | 19 | import("actions.config.configfiles", { alias = "gen_configfiles", rootdir = os.programdir() }) 20 | gen_configfiles() 21 | end) 22 | 23 | -- target to generate the API 24 | target("api") 25 | set_kind("headeronly") 26 | add_deps("every.api") 27 | on_config(function(target) 28 | import("xmake.modules.api", { rootdir = os.projectdir() }) 29 | api(target, "every.api", true) 30 | 31 | import("actions.config.configfiles", { alias = "gen_configfiles", rootdir = os.programdir() }) 32 | gen_configfiles() 33 | end) 34 | -------------------------------------------------------------------------------- /bpf/block_ip.bpf.c: -------------------------------------------------------------------------------- 1 | #include "vmlinux.h" 2 | #include "commons.bpf.h" 3 | 4 | #include 5 | 6 | char LICENSE[] SEC("license") = "Dual BSD/GPL"; 7 | 8 | // TODO > make this easy to configure via config struct 9 | const volatile __u32 input = 0; // address to block (host byte order) 10 | 11 | SEC("tc") 12 | int block_ip(struct __sk_buff *skb) 13 | { 14 | void *data_end = (void *)(unsigned long long)skb->data_end; 15 | void *data = (void *)(unsigned long long)skb->data; 16 | 17 | struct ethhdr *eth = data; 18 | const int l3_offset = sizeof(*eth); 19 | 20 | if (data + l3_offset > data_end) 21 | { 22 | bpf_printk("block_ip: [eth] size lenght check hit: continue"); 23 | return TC_ACT_OK; 24 | } 25 | 26 | if (eth->h_proto != bpf_htons(ETH_P_IP)) 27 | { 28 | bpf_printk("block_ip: [eth] protocol is %d: continue", eth->h_proto); 29 | return TC_ACT_OK; 30 | } 31 | 32 | struct iphdr *ip_header = data + l3_offset; 33 | const int l4_offset = l3_offset + sizeof(*ip_header); 34 | if (data + l4_offset > data_end) 35 | { 36 | bpf_printk("block_ip: [iph] size lenght check hit: continue"); 37 | return TC_ACT_OK; 38 | } 39 | 40 | if (ip_is_fragment(skb, l3_offset)) 41 | { 42 | bpf_printk("block_ip: [iph] is fragment: continue"); 43 | return TC_ACT_OK; 44 | } 45 | 46 | u32 dest = bpf_ntohl(ip_header->daddr); 47 | if (dest == input) 48 | { 49 | bpf_printk("block_ip: [iph] destination address is %pI4: block", ip_header->daddr); 50 | return TC_ACT_SHOT; 51 | } 52 | 53 | return TC_ACT_OK; 54 | } 55 | -------------------------------------------------------------------------------- /bpf/block_port.bpf.c: -------------------------------------------------------------------------------- 1 | #include "vmlinux.h" 2 | #include "commons.bpf.h" 3 | 4 | #include 5 | 6 | char LICENSE[] SEC("license") = "Dual BSD/GPL"; 7 | 8 | // TODO > make this easy to configure via config struct 9 | const volatile __u32 input = 0; 10 | 11 | SEC("tc") 12 | int block_port(struct __sk_buff *skb) 13 | { 14 | bpf_printk("TBD"); 15 | 16 | return TC_ACT_OK; 17 | } 18 | -------------------------------------------------------------------------------- /bpf/block_private_ipv4.bpf.c: -------------------------------------------------------------------------------- 1 | #include "vmlinux.h" 2 | #include "commons.bpf.h" 3 | 4 | #include 5 | 6 | char LICENSE[] SEC("license") = "Dual BSD/GPL"; 7 | 8 | struct subnet 9 | { 10 | __u32 subnet; 11 | __u32 netmask; 12 | }; 13 | 14 | static struct subnet blocked_subnets[] = { 15 | // 10.0.0.0/8 16 | { 17 | .subnet = 0x0A000000, // 10.0.0.0 18 | .netmask = 0xFF000000, // 255.0.0.0 19 | }, 20 | // 172.16.0.0/12 21 | { 22 | .subnet = 0xAC100000, // 172.16.0.0 23 | .netmask = 0xFFF00000, // 255.240.0.0 24 | }, 25 | // 192.168.0.0/16 26 | { 27 | .subnet = 0xC0A80000, // 192.168.0.0 28 | .netmask = 0xFFF00000, // 255.240.0.0 29 | }, 30 | }; 31 | 32 | SEC("tc") 33 | int block_private_ipv4(struct __sk_buff *skb) 34 | { 35 | bpf_printk("============================================="); 36 | void *data_end = (void *)(unsigned long long)skb->data_end; 37 | void *data = (void *)(unsigned long long)skb->data; 38 | 39 | struct ethhdr *eth = data; 40 | const int l3_offset = sizeof(*eth); 41 | 42 | if (data + l3_offset > data_end) 43 | { 44 | bpf_printk("classifier: [eth] size lenght check hit: continue"); 45 | return TC_ACT_OK; 46 | } 47 | 48 | if (eth->h_proto != bpf_htons(ETH_P_IP)) 49 | { 50 | bpf_printk("classifier: [eth] protocol is %d: continue", eth->h_proto); 51 | return TC_ACT_OK; 52 | } 53 | 54 | struct iphdr *ip_header = data + l3_offset; 55 | const int l4_offset = l3_offset + sizeof(*ip_header); 56 | 57 | if (data + l4_offset > data_end) 58 | { 59 | bpf_printk("classifier: [iph] size lenght check hit: continue"); 60 | return TC_ACT_OK; 61 | } 62 | 63 | if (ip_header->protocol == IPPROTO_ICMP) 64 | { 65 | bpf_printk("classifier: [iph] is icmp, shot"); 66 | return TC_ACT_SHOT; 67 | } 68 | 69 | if (ip_is_fragment(skb, l3_offset)) 70 | { 71 | bpf_printk("classifier: [iph] is fragment: continue"); 72 | return TC_ACT_OK; 73 | } 74 | 75 | struct tcphdr *tcp = (struct tcphdr *)(data + l4_offset); 76 | const int l7_offset = l4_offset + sizeof(*tcp); 77 | 78 | if (data + l7_offset > data_end) 79 | { 80 | bpf_printk("classifier: [tcph] size lenght check hit: continue"); 81 | return TC_ACT_OK; 82 | } 83 | 84 | bpf_printk("daddr: %d", ip_header->daddr); 85 | bpf_printk("saddr: %d", ip_header->saddr); 86 | 87 | u16 tcp_dest_nl = bpf_ntohs(tcp->dest); 88 | u16 tcp_source_nl = bpf_ntohs(tcp->source); 89 | 90 | for (int i = 0; i < sizeof(blocked_subnets) / sizeof(struct subnet); i++) 91 | { 92 | u32 netmask = bpf_htonl(blocked_subnets[i].netmask); 93 | u32 subnetip = bpf_htonl(blocked_subnets[i].subnet); 94 | 95 | bpf_printk("ip_header->daddr & netmask: %d", ip_header->daddr & netmask); 96 | bpf_printk("subnetip & netmask: %d", subnetip & netmask); 97 | bpf_printk("tcp dest port: %d", tcp_dest_nl); 98 | bpf_printk("tcp source port: %d", tcp_source_nl); 99 | 100 | if ((ip_header->daddr & netmask) == (subnetip & netmask)) 101 | { 102 | if (tcp_source_nl == 22) 103 | { 104 | bpf_printk("even though it matched, the source port is 22, so we will allow it"); 105 | return TC_ACT_OK; 106 | } 107 | 108 | bpf_printk("daddr is on a blocked subnet, shot"); 109 | return TC_ACT_SHOT; 110 | } 111 | } 112 | 113 | return TC_ACT_OK; 114 | } 115 | -------------------------------------------------------------------------------- /bpf/commons.bpf.h: -------------------------------------------------------------------------------- 1 | #ifndef TRAFFICO_BPF_COMMONS_H 2 | #define TRAFFICO_BPF_COMMONS_H 3 | #include 4 | #include 5 | 6 | #ifndef TC_ACT_OK 7 | #define TC_ACT_OK 0 8 | #endif 9 | 10 | #ifndef TC_ACT_SHOT 11 | #define TC_ACT_SHOT 2 12 | #endif 13 | 14 | #ifndef ETH_P_IP 15 | #define ETH_P_IP 0x0800 16 | #endif 17 | 18 | #ifndef IP_MF 19 | #define IP_MF 0x2000 20 | #endif 21 | 22 | #ifndef IP_OFFSET 23 | #define IP_OFFSET 0x1FFF 24 | #endif 25 | 26 | unsigned long long load_half(void *skb, unsigned long long off) asm("llvm.bpf.load.half"); 27 | 28 | static inline int ip_is_fragment(struct __sk_buff *skb, u64 nhoff) 29 | { 30 | return load_half(skb, nhoff + offsetof(struct iphdr, frag_off)) & (IP_MF | IP_OFFSET); 31 | } 32 | 33 | /// \brief Our own definition of the bpf_trace_printk tracepoint struct. 34 | /// 35 | /// Defining it we avoid depending on the latest vmlinux.h file. 36 | /// Notice that suffic __x ensures it does not collides with the vmlinux.h of kernels >= 5.9. 37 | struct trace_event_raw_bpf_trace_printk___x 38 | { 39 | }; 40 | 41 | /// \brief Redefine bpf_printk to support automatic new lines and clamp. 42 | /// 43 | /// It needs a kernel >= 5.2 because of eBPF global and static variables. 44 | #undef bpf_printk 45 | #ifndef NDEBUG 46 | #define bpf_printk(fmt, ...) \ 47 | ({ \ 48 | static char ____fmt[] = fmt "\0"; \ 49 | if (bpf_core_type_exists(struct trace_event_raw_bpf_trace_printk___x)) \ 50 | { \ 51 | bpf_trace_printk(____fmt, sizeof(____fmt) - 1, ##__VA_ARGS__); \ 52 | } \ 53 | else \ 54 | { \ 55 | ____fmt[sizeof(____fmt) - 2] = '\n'; \ 56 | bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \ 57 | } \ 58 | }) 59 | #else 60 | #define bpf_printk(fmt, ...) \ 61 | { \ 62 | } \ 63 | while (0) 64 | #endif 65 | 66 | #endif // TRAFFICO_BPF_COMMONS_H 67 | -------------------------------------------------------------------------------- /bpf/nop.bpf.c: -------------------------------------------------------------------------------- 1 | #include "vmlinux.h" 2 | #include "commons.bpf.h" 3 | 4 | char LICENSE[] SEC("license") = "Dual BSD/GPL"; 5 | 6 | SEC("tc") 7 | int nop(struct __sk_buff *skb) 8 | { 9 | bpf_printk("nop"); 10 | 11 | return TC_ACT_OK; 12 | } 13 | -------------------------------------------------------------------------------- /bpf/xmake.lua: -------------------------------------------------------------------------------- 1 | set_xmakever("2.6.1") -- Minimum version to compile BPF source correctly 2 | 3 | -- includes 4 | includes("../xmake/repos.lua") 5 | 6 | -- rules 7 | rule("bpf") 8 | set_extensions(".bpf.c") 9 | on_config(function (target) 10 | assert(is_host("linux"), 'rule("bpf"): only supported on linux!') 11 | local headerdir = path.join(target:autogendir(), "rules", "bpf") 12 | if not os.isdir(headerdir) then 13 | os.mkdir(headerdir) 14 | end 15 | target:add("includedirs", headerdir, { interface = true }) 16 | end) 17 | before_buildcmd_file(function (target, batchcmds, sourcefile, opt) 18 | 19 | local filecfg = target:fileconfig(sourcefile) 20 | local bpftool = "bpftool" 21 | if filecfg and filecfg.bpftool then 22 | bpftool = filecfg.bpftool 23 | end 24 | 25 | local headerfile = path.join(target:autogendir(), "rules", "bpf", (path.filename(sourcefile):gsub("%.bpf%.c", ".skel.h"))) 26 | local objectfile = path.join(target:autogendir(), "rules", "bpf", (path.filename(sourcefile):gsub("%.bpf%.c", ".bpf.o"))) 27 | local targetarch 28 | if target:is_arch("x86_64", "i386") then 29 | targetarch = "__TARGET_ARCH_x86" 30 | elseif target:is_arch("arm64", "arm64-v8a") then 31 | targetarch = "__TARGET_ARCH_arm64" 32 | elseif target:is_arch("arm.*") then 33 | targetarch = "__TARGET_ARCH_arm" 34 | elseif target:is_arch("mips64", "mips") then 35 | targetarch = "__TARGET_ARCH_mips" 36 | elseif target:is_arch("ppc64", "ppc") then 37 | targetarch = "__TARGET_ARCH_powerpc" 38 | end 39 | target:add("includedirs", path.directory(headerfile), { interface = true }) 40 | 41 | target:set("optimize", "faster") 42 | batchcmds:show_progress(opt.progress, "${color.build.object}compiling.bpf %s", sourcefile) 43 | batchcmds:mkdir(path.directory(objectfile)) 44 | batchcmds:compile(sourcefile, objectfile, {configs = {force = {cxflags = {"-target bpf", "-g"}}, defines = targetarch}}) 45 | batchcmds:mkdir(path.directory(headerfile)) 46 | batchcmds:show_progress(opt.progress, "${color.build.object}compiling.bpf.o %s", objectfile) 47 | batchcmds:execv(bpftool, {"gen", "skeleton", objectfile}, {stdout = headerfile}) 48 | batchcmds:show_progress(opt.progress, "${color.build.object}generating.skel.h %s", headerfile) 49 | batchcmds:add_depfiles(sourcefile) 50 | batchcmds:set_depmtime(os.mtime(headerfile)) 51 | batchcmds:set_depcache(target:dependfile(objectfile)) 52 | end) 53 | rule_end() 54 | 55 | -- rules 56 | add_rules("mode.release", "mode.debug") 57 | 58 | -- toolchain 59 | add_requires("llvm") 60 | set_toolchains("@llvm") 61 | 62 | -- requirements 63 | add_requires("linux-headers") 64 | add_requires("libbpf v0.8.0", { system = false }) 65 | 66 | -- probe 67 | target("bpf") 68 | set_kind("object") 69 | set_policy("build.across_targets_in_parallel", false) 70 | includes("../tools") 71 | add_deps("bpftool") 72 | includes("../vmlinux") 73 | add_deps("vmlinux") 74 | add_packages("libbpf") 75 | add_files("*.bpf.c", { rules = { "bpf", override = true}, bpftool = path.join("tools", "bpftool") }) 76 | -------------------------------------------------------------------------------- /test/cli.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load helpers 4 | export BATS_TEST_NAME_PREFIX=$(setsuite) 5 | bats_require_minimum_version 1.7.0 6 | 7 | setup() { 8 | echo "# setup:" >&3 9 | 10 | load net 11 | 12 | NETNS="ns$((RANDOM % 10))" 13 | 14 | new_netns "${NETNS}" 15 | setup_net "${NETNS}" 16 | } 17 | 18 | teardown() { 19 | echo "# teardown:" >&3 20 | 21 | killall traffico &>/dev/null || true 22 | del_netdev 23 | del_netns "${NETNS}" 24 | } 25 | 26 | @test "install nop program at egress" { 27 | run ip netns exec "${NETNS}" traffico -i "${PEER}" nop >/dev/null 3>&- & 28 | sleep 1 29 | run ip netns exec "${NETNS}" tc qdisc show dev "${PEER}" clsact 30 | [ "$(echo $output | xargs)" == "qdisc clsact ffff: parent ffff:fff1" ] 31 | } 32 | 33 | @test "install nop program at ingress" { 34 | run ip netns exec "${NETNS}" traffico -i "${PEER}" --at ingress nop >/dev/null 3>&- & 35 | sleep 1 36 | run ip netns exec "${NETNS}" tc qdisc show dev "${PEER}" clsact 37 | [ "$(echo $output | xargs)" == "qdisc clsact ffff: parent ffff:fff1" ] 38 | } 39 | 40 | @test "--no-cleanup works" { 41 | echo "# traffico up at interface ${PEER}" >&3 42 | run ip netns exec "${NETNS}" traffico --no-cleanup -i "${PEER}" --at ingress nop >/dev/null 3>&- & 43 | sleep 1 44 | run ip netns exec "${NETNS}" tc qdisc show dev "${PEER}" clsact 45 | [ "$(echo $output | xargs)" == "qdisc clsact ffff: parent ffff:fff1" ] 46 | killall traffico 47 | echo "# traffico down" >&3 48 | sleep 1 49 | run ip netns exec "${NETNS}" tc qdisc show dev "${PEER}" clsact 50 | [ "$(echo $output | xargs)" == "qdisc clsact ffff: parent ffff:fff1" ] 51 | echo "# tc qdisc still there" >&3 52 | run bpftool prog show name nop 53 | OUT=${lines[0]##*: } >&3 54 | [ "${OUT%% *}" == "sched_cls" ] 55 | echo "# BPF program still there" >&3 56 | run ip netns exec "${NETNS}" tc qdisc del dev "${PEER}" clsact 57 | } 58 | 59 | @test "block_private_ipv4 blocks ICMP packets" { 60 | run ip netns exec "${NETNS}" ping -W1 -4 -c1 10.22.1.2 61 | [ $status -eq 0 ] 62 | run ip netns exec "${NETNS}" traffico -i lo --at egress block_private_ipv4 >/dev/null 3>&- & 63 | sleep 1 64 | run ip netns exec "${NETNS}" tc qdisc show dev lo clsact 65 | [ "$(echo $output | xargs)" == "qdisc clsact ffff: parent ffff:fff1" ] 66 | run ip netns exec "${NETNS}" ping -W1 -4 -c1 10.22.1.2 67 | [ $status -eq 1 ] 68 | } -------------------------------------------------------------------------------- /test/cli.flags.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load helpers 4 | export BATS_TEST_NAME_PREFIX=$(setsuite) 5 | bats_require_minimum_version 1.7.0 6 | 7 | @test "help" { 8 | run traffico --help 9 | [ $status -eq 0 ] 10 | [ "${lines[0]}" == 'Usage: traffico [OPTION...] PROGRAM' ] 11 | } 12 | 13 | @test "usage" { 14 | run traffico --usage 15 | [ $status -eq 0 ] 16 | [ "${lines[0]%% *}" == 'Usage:' ] 17 | } 18 | 19 | @test "invalid option" { 20 | run traffico -x 21 | [ ! $status -eq 0 ] 22 | [ $status -eq 64 ] 23 | [ "${lines[0]##*: }" == "invalid option -- 'x'" ] 24 | } 25 | 26 | @test "unrecognized option" { 27 | run traffico --xxxx 28 | [ ! $status -eq 0 ] 29 | [ $status -eq 64 ] 30 | [ "${lines[0]##*: }" == "unrecognized option '--xxxx'" ] 31 | } 32 | 33 | @test "missing program" { 34 | run traffico 35 | [ ! $status -eq 0 ] 36 | [ $status -eq 64 ] 37 | [ "${lines[0]}" == 'traffico: program name is mandatory' ] 38 | } 39 | 40 | @test "unavailable program" { 41 | run traffico xxx 42 | [ ! $status -eq 0 ] 43 | [ $status -eq 64 ] 44 | [ "${lines[0]}" == "traffico: argument 'xxx' is not a traffico program" ] 45 | } 46 | 47 | @test "unavailable network interface" { 48 | run traffico -i ciao 49 | [ ! $status -eq 0 ] 50 | [ $status -eq 64 ] 51 | [ "${lines[0]}" == "traffico: option '--ifname' requires an existing interface: got 'ciao'" ] 52 | } 53 | 54 | @test "unavailable network interface (long)" { 55 | run traffico --ifname ciao 56 | [ ! $status -eq 0 ] 57 | [ $status -eq 64 ] 58 | [ "${lines[0]}" == "traffico: option '--ifname' requires an existing interface: got 'ciao'" ] 59 | } 60 | 61 | @test "unsupported attach point" { 62 | run traffico --at wrong 63 | [ ! $status -eq 0 ] 64 | [ $status -eq 64 ] 65 | [ "${lines[0]}" == "traffico: option '--at' requires one of the following values: INGRESS|EGRESS" ] 66 | } -------------------------------------------------------------------------------- /test/cni.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load helpers 4 | fixtures cni 5 | export BATS_TEST_NAME_PREFIX=$(setsuite) 6 | bats_require_minimum_version 1.7.0 7 | 8 | setup() { 9 | echo "# setup:" >&3 10 | 11 | load net 12 | 13 | NETNS="ns$((RANDOM % 10))" 14 | 15 | new_netns "${NETNS}" 16 | setup_net "${NETNS}" 17 | new_server 18 | } 19 | 20 | teardown() { 21 | echo "# teardown:" >&3 22 | 23 | del_netdev 24 | del_netns "${NETNS}" 25 | del_server 26 | } 27 | 28 | @test "block_private_ipv4" { 29 | run curl --max-time 1 --silent "${VETH_ADDR}:${SERVER_PORT}" >/dev/null 30 | [ $status -eq 0 ] 31 | echo "# can reach ${VETH_ADDR}:${SERVER_PORT}" >&3 32 | run ip netns exec "${NETNS}" curl --max-time 1 --silent "${VETH_ADDR}:${SERVER_PORT}" >/dev/null 33 | [ $status -eq 0 ] 34 | echo "# can reach ${VETH_ADDR}:${SERVER_PORT} from the namespace" >&3 35 | echo "# installing block_private_ipv4 in the namespace" >&3 36 | run ip netns exec "${NETNS}" bash -c "cat "$FIXTURE_ROOT/attach_in.json" | CNI_COMMAND=ADD traffico-cni" 37 | [ $status -eq 0 ] 38 | OUT=$(cat "$FIXTURE_ROOT/attach_out.json") 39 | run diff -u <(echo "$OUT") <(echo "$output") 40 | [ $status -eq 0 ] 41 | echo "# attach ok" >&3 42 | run ip netns exec "${NETNS}" tc qdisc show dev peer0 clsact 43 | [ "$(echo $output | xargs)" == "qdisc clsact ffff: parent ffff:fff1" ] 44 | echo "# qdisc ok" >&3 45 | run ip netns exec "${NETNS}" curl --max-time 1 --silent "${VETH_ADDR}:${SERVER_PORT}" >/dev/null 46 | [ ! $status -eq 0 ] 47 | echo "# cannot reach ${VETH_ADDR}:${SERVER_PORT} from the namespace" >&3 48 | } -------------------------------------------------------------------------------- /test/fixtures/cni/attach_in.json: -------------------------------------------------------------------------------- 1 | { 2 | "cniVersion": "0.4.0", 3 | "name": "dummy-network", 4 | "prevResult": { 5 | "cniVersion": "1.0.0", 6 | "interfaces": [ 7 | { 8 | "name": "peer0" 9 | } 10 | ], 11 | "ips": [ 12 | ], 13 | "routes": [ 14 | ], 15 | "dns": { 16 | } 17 | }, 18 | "type": "traffico-cni", 19 | "program": "block_private_ipv4", 20 | "attachPoint": "ingress" 21 | } -------------------------------------------------------------------------------- /test/fixtures/cni/attach_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "cniVersion": "1.0.0", 3 | "interfaces": [{ 4 | "name": "peer0" 5 | }], 6 | "ips": [], 7 | "routes": [], 8 | "dns": { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/helpers.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | fixtures() { 4 | FIXTURE_ROOT="$BATS_TEST_DIRNAME/fixtures/$1" 5 | # shellcheck disable=SC2034 6 | RELATIVE_FIXTURE_ROOT="${FIXTURE_ROOT#"$BATS_CWD"/}" 7 | } 8 | 9 | setsuite() { 10 | local PREFIX 11 | PREFIX="$(basename "$BATS_TEST_FILENAME" .bats)" 12 | echo "${PREFIX%%.*}: " 13 | } -------------------------------------------------------------------------------- /test/net.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | del_netns() { 4 | echo "# remove namespace $1 (if it exists)" >&3 5 | ip netns delete "$1" &>/dev/null || true 6 | } 7 | 8 | new_netns() { 9 | del_netns "$1" 10 | echo "# create namespace $1" >&3 11 | ip netns add "$1" &>/dev/null 12 | } 13 | 14 | setup_net() { 15 | export VETH="veth0" 16 | export PEER="peer0" 17 | export VETH_ADDR="10.22.1.1" 18 | export PEER_ADDR="10.22.1.2" 19 | 20 | echo "# create veth link $VETH <-> $PEER" >&3 21 | ip link add "$VETH" type veth peer name "$PEER" &>/dev/null 22 | 23 | echo "# assign device $PEER to namespace $1" >&3 24 | ip link set "$PEER" netns "$1" &>/dev/null 25 | 26 | echo "# setup address of $VETH" >&3 27 | ip addr add "$VETH_ADDR/24" dev "$VETH" &>/dev/null 28 | ip link set "$VETH" up &>/dev/null 29 | 30 | echo "# setup address of $PEER" >&3 31 | ip netns exec "$1" ip addr add "$PEER_ADDR"/24 dev "$PEER" &>/dev/null 32 | ip netns exec "$1" ip link set "$PEER" up &>/dev/null 33 | ip netns exec "$1" ip link set lo up &>/dev/null 34 | ip netns exec "$1" ip route add default via "$VETH_ADDR" &>/dev/null 35 | } 36 | 37 | del_netdev() { 38 | echo "# delete device $VETH" >&3 39 | ip link delete "$VETH" &>/dev/null 40 | } 41 | 42 | new_server() { 43 | export SERVER_PORT=8787 44 | echo "# serving $BATS_TMPDIR at $SERVER_PORT" >&3 45 | mini_httpd -d "$BATS_TMPDIR" -p "$SERVER_PORT" -i "$BATS_RUN_TMPDIR/mini_httpd.pid" &>/dev/null 46 | } 47 | 48 | del_server() { 49 | echo "# shutdown server" >&3 50 | pkill -F "$BATS_RUN_TMPDIR/mini_httpd.pid" &>/dev/null 51 | } 52 | -------------------------------------------------------------------------------- /tools/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !xmake.lua 3 | !.gitignore -------------------------------------------------------------------------------- /tools/xmake.lua: -------------------------------------------------------------------------------- 1 | set_xmakever("2.6.1") -- Minimum version to compile BPF source correctly 2 | 3 | -- options 4 | option("require-bpftool", {showmenu = true, default = false, description = "require bpftool package"}) 5 | 6 | --- run `xmake f --require-bpftool=y` to pull bpftool from xmake-repo repo rather than using the system one 7 | if has_config("require-bpftool") then 8 | add_requires("linux-tools", {configs = {bpftool = true}}) 9 | add_packages("linux-tools") 10 | end 11 | 12 | -- bpftool 13 | local bpftool = path.join("tools", "bpftool") 14 | target("bpftool") 15 | set_kind("phony") 16 | on_config(function (target) 17 | if os.tryrm(bpftool) then 18 | if has_config("require-bpftool") then 19 | local target = path.join(target:pkg("linux-tools"):installdir(), "sbin", "bpftool") 20 | os.ln(target, bpftool) 21 | else 22 | import("lib.detect.find_program") 23 | local sys_bpftool = find_program("bpftool") 24 | if sys_bpftool == nil then 25 | os.raise("cannot find bpftool in system") 26 | end 27 | os.ln(sys_bpftool, bpftool) 28 | end 29 | import("core.base.option") 30 | if option.get("verbose") then 31 | import("core.base.json") 32 | local vers = os.iorunv(bpftool, {"version", "-j"}) 33 | print(json.decode(vers)) 34 | end 35 | end 36 | end) 37 | before_build(function(target) 38 | import("utils.progress") 39 | progress.show(10, "${color.build.object}providing.bpftool %s", bpftool) 40 | end) 41 | on_clean(function (target) 42 | os.tryrm(bpftool) 43 | end) 44 | -------------------------------------------------------------------------------- /traffico-cni.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "api.h" 11 | 12 | enum cni_error_codes 13 | { 14 | CNI_INCOMPATIBLE = 1, 15 | CNI_UNSUPPORTED_FIELD, 16 | CNI_CONTAINER_NOT_EXISTING, 17 | CNI_INVALID_ENV_VARS, 18 | CNI_IO_FAILURE, 19 | CNI_FAILED_DECODE_CONTENT, 20 | CNI_INVALID_NETWORK_CONFIG, 21 | CNI_TRY_AGAIN_LATER, 22 | }; 23 | 24 | struct cni_error 25 | { 26 | char *cni_version; 27 | int code; 28 | char *msg; 29 | char *details; 30 | }; 31 | 32 | void print_cni_error(struct cni_error *err) 33 | { 34 | cJSON *error_obj = cJSON_CreateObject(); 35 | cJSON_AddStringToObject(error_obj, "cniVersion", err->cni_version); 36 | cJSON_AddNumberToObject(error_obj, "code", err->code); 37 | cJSON_AddStringToObject(error_obj, "msg", err->msg); 38 | if (strlen(err->details) > 0) 39 | cJSON_AddStringToObject(error_obj, "details", err->details); 40 | else 41 | cJSON_AddStringToObject(error_obj, "details", err->msg); 42 | 43 | printf("%s\n", cJSON_Print(error_obj)); 44 | } 45 | 46 | #define BUFFERSIZE 10 47 | int get_stdin(char **text) 48 | { 49 | *text = calloc(1, 1); 50 | char *buffer[BUFFERSIZE]; 51 | while (fgets(buffer, BUFFERSIZE, stdin)) 52 | { 53 | *text = realloc(*text, strlen(*text) + 1 + strlen(buffer)); 54 | if (*text == NULL) 55 | { 56 | return 1; 57 | } 58 | strncat(*text, buffer, strlen(buffer)); 59 | } 60 | return 0; 61 | } 62 | 63 | unsigned int string_to_ip_int(const char *ip) 64 | { 65 | unsigned int ret = 0; 66 | int i; 67 | const char *start; 68 | 69 | start = ip; 70 | for (i = 0; i < 4; i++) 71 | { 72 | char c; 73 | int n = 0; 74 | while (1) 75 | { 76 | c = *start; 77 | start++; 78 | if (c >= '0' && c <= '9') 79 | { 80 | n *= 10; 81 | n += c - '0'; 82 | continue; 83 | } 84 | 85 | if ((i < 3 && c == '.') || i == 3) 86 | { 87 | break; 88 | } 89 | return -1; 90 | } 91 | if (n >= 256) 92 | { 93 | return -1; 94 | } 95 | ret *= 256; 96 | ret += n; 97 | } 98 | return ret; 99 | } 100 | 101 | int add_command() 102 | { 103 | struct cni_error err; 104 | err.cni_version = "1.0.0"; 105 | err.msg = ""; 106 | err.details = ""; 107 | 108 | struct config config = { 109 | .verbose = false, 110 | .cleanup_on_exit = false, 111 | }; 112 | 113 | config.verbose = false; 114 | 115 | char *stdin_text; 116 | if (get_stdin(&stdin_text) != 0) 117 | { 118 | err.code = CNI_IO_FAILURE; 119 | err.msg = "Error reading stdin"; 120 | print_cni_error(&err); 121 | return -1; 122 | } 123 | 124 | cJSON *jsonobj = cJSON_Parse(stdin_text); 125 | 126 | if (jsonobj == NULL) 127 | { 128 | err.msg = "Error parsing JSON"; 129 | err.code = CNI_FAILED_DECODE_CONTENT; 130 | print_cni_error(&err); 131 | return -1; 132 | } 133 | 134 | const cJSON *cniVersion = NULL; 135 | cniVersion = cJSON_GetObjectItemCaseSensitive(jsonobj, "cniVersion"); 136 | 137 | if (cniVersion != NULL && cJSON_IsString(cniVersion)) 138 | { 139 | err.cni_version = cniVersion->valuestring; 140 | } 141 | 142 | const cJSON *programName = NULL; 143 | programName = cJSON_GetObjectItemCaseSensitive(jsonobj, "program"); 144 | char *programName_str = NULL; 145 | 146 | if (programName == NULL || !cJSON_IsString(programName)) 147 | { 148 | err.code = CNI_INVALID_NETWORK_CONFIG; 149 | err.msg = "Missing or invalid field 'program'"; 150 | print_cni_error(&err); 151 | return -1; 152 | } 153 | 154 | programName_str = programName->valuestring; 155 | 156 | const cJSON *attachPoint = NULL; 157 | attachPoint = cJSON_GetObjectItemCaseSensitive(jsonobj, "attachPoint"); 158 | char *attachPoint_str = "EGRESS"; 159 | 160 | if (attachPoint != NULL && cJSON_IsString(attachPoint)) 161 | { 162 | attachPoint_str = attachPoint->valuestring; 163 | } 164 | 165 | if (strcasecmp(attachPoint_str, "INGRESS") == 0) 166 | { 167 | config.attach_point = BPF_TC_INGRESS; 168 | } 169 | else if (strcasecmp(attachPoint_str, "EGRESS") == 0) 170 | { 171 | config.attach_point = BPF_TC_EGRESS; 172 | } 173 | 174 | const cJSON *prevResult = NULL; 175 | prevResult = cJSON_GetObjectItemCaseSensitive(jsonobj, "prevResult"); 176 | 177 | if (prevResult == NULL) 178 | { 179 | err.code = CNI_INVALID_NETWORK_CONFIG; 180 | err.msg = "Could not find prevResult in JSON"; 181 | print_cni_error(&err); 182 | return -1; 183 | } 184 | 185 | const cJSON *interfaces = NULL; 186 | interfaces = cJSON_GetObjectItemCaseSensitive(prevResult, "interfaces"); 187 | 188 | if (interfaces == NULL) 189 | { 190 | err.code = CNI_INVALID_NETWORK_CONFIG; 191 | err.msg = "Failed to get interfaces"; 192 | print_cni_error(&err); 193 | return -1; 194 | } 195 | 196 | const cJSON *interface = NULL; 197 | interface = cJSON_GetArrayItem(interfaces, 0); 198 | 199 | if (interface == NULL) 200 | { 201 | err.code = CNI_INVALID_NETWORK_CONFIG; 202 | err.msg = "Failed to get default interface"; 203 | print_cni_error(&err); 204 | return -1; 205 | } 206 | 207 | const cJSON *ifname = NULL; 208 | ifname = cJSON_GetObjectItemCaseSensitive(interface, "name"); 209 | 210 | if (!cJSON_IsString(ifname)) 211 | { 212 | err.code = CNI_INVALID_NETWORK_CONFIG; 213 | err.msg = "Failed to get ifname"; 214 | print_cni_error(&err); 215 | return -1; 216 | } 217 | 218 | int ifindex = if_nametoindex(ifname->valuestring); 219 | 220 | if (ifindex == 0) 221 | { 222 | err.code = CNI_INVALID_NETWORK_CONFIG; 223 | err.msg = "Failed to retrieve ifindex"; 224 | print_cni_error(&err); 225 | return -1; 226 | } 227 | config.ifindex = ifindex; 228 | 229 | int p; 230 | for (p = 0; p < NUM_PROGRAMS; p++) 231 | { 232 | if (strcasecmp(programName_str, g_programs_name[p]) == 0) 233 | { 234 | config.program = (program_t)p; 235 | break; 236 | } 237 | } 238 | if (config.program == NULL) 239 | { 240 | err.code = CNI_INVALID_NETWORK_CONFIG; 241 | err.msg = "Unknwon program"; 242 | err.details = programName_str; 243 | print_cni_error(&err); 244 | return -1; 245 | } 246 | 247 | strncpy(config.ifname, ifname->valuestring, strlen(ifname->valuestring)); 248 | 249 | if (attach(&config, exit_after_attach, NULL) != 0) 250 | { 251 | err.code = CNI_INVALID_NETWORK_CONFIG; 252 | err.msg = "Failed to attach BPF program"; 253 | print_cni_error(&err); 254 | return -1; 255 | } 256 | 257 | char *string = NULL; 258 | string = cJSON_Print(prevResult); 259 | if (string == NULL) 260 | { 261 | err.code = CNI_IO_FAILURE; 262 | err.msg = "Failed to prepare result for printing"; 263 | print_cni_error(&err); 264 | return -1; 265 | } 266 | 267 | printf("%s\n", string); 268 | return 0; 269 | } 270 | 271 | int plugin_main() 272 | { 273 | char *cni_command = getenv("CNI_COMMAND"); 274 | if (cni_command == NULL) 275 | { 276 | struct cni_error err; 277 | err.cni_version = "1.0.0"; 278 | err.code = CNI_INVALID_ENV_VARS; 279 | err.msg = "CNI_COMMAND not set"; 280 | err.details = "CNI_COMMAND not set"; 281 | print_cni_error(&err); 282 | return -1; 283 | } 284 | if (strcmp(cni_command, "ADD") == 0) 285 | { 286 | return add_command(); 287 | } 288 | else 289 | { 290 | return 0; 291 | } 292 | } 293 | 294 | int main(int argc, char const *argv[]) 295 | { 296 | return plugin_main(); 297 | } 298 | -------------------------------------------------------------------------------- /traffico.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "api.h" 10 | 11 | const char *argp_program_version = TOOL_NAME " 0.0"; 12 | const char *argp_program_bug_address = "https://github.com/leodido/traffico/issues"; 13 | error_t argp_err_exit_status = 1; 14 | const char argp_program_doc[] = 15 | "\n" 16 | "Isolate your host the eBPF way.\n" 17 | "\v" 18 | " PROGRAMS\n" PROGRAMS_DESCRIPTION; 19 | 20 | const char OPT_VERBOSE_LONG[] = "verbose"; 21 | #define OPT_VERBOSE_KEY 'v' 22 | const char OPT_IFNAME_LONG[] = "ifname"; 23 | #define OPT_IFNAME_KEY 'i' 24 | const char OPT_IFNAME_ARG[] = "IFNAME"; 25 | const char OPT_ATTACH_LONG[] = "at"; 26 | #define OPT_ATTACH_KEY 0x80 27 | const char OPT_ATTACH_ARG[] = "INGRESS|EGRESS"; 28 | #define OPT_NO_CLEANUP_KEY 0x81 29 | const char OPT_NO_CLEANUP_LONG[] = "no-cleanup"; 30 | 31 | const struct argp_option argp_opts[] = { 32 | 33 | {"OPTIONS", 0, 0, OPTION_DOC, 0, 0}, 34 | {OPT_VERBOSE_LONG, OPT_VERBOSE_KEY, NULL, 0, "Verbose debug output", -1}, 35 | {OPT_IFNAME_LONG, OPT_IFNAME_KEY, OPT_IFNAME_ARG, 0, "Interface to which to attach the filter\n(defaults to the default gateway interface)", 1}, 36 | {OPT_ATTACH_LONG, OPT_ATTACH_KEY, OPT_ATTACH_ARG, 0, "Where to attach the filter (defaults to egress)", 1}, 37 | {OPT_NO_CLEANUP_LONG, OPT_NO_CLEANUP_KEY, NULL, 0, "Do not detach the TC hook and filter at the exit", 1}, 38 | {"", 0, 0, OPTION_DOC, 0, 0}, 39 | {0} // . 40 | 41 | }; 42 | 43 | static struct config g_config; 44 | 45 | #define log_erro(fmt, ...) \ 46 | log_err(&g_config, fmt, ##__VA_ARGS__); 47 | 48 | #define log_info(fmt, ...) \ 49 | log_out(&g_config, fmt, ##__VA_ARGS__); 50 | 51 | static error_t parse_cli(int key, char *arg, struct argp_state *state) 52 | { 53 | int ifindex; 54 | int p; 55 | switch (key) 56 | { 57 | 58 | // Initializations 59 | case ARGP_KEY_INIT: 60 | g_config.attach_point = BPF_TC_EGRESS; 61 | g_config.ifindex = 0; 62 | g_config.cleanup_on_exit = true; 63 | g_config.verbose = false; 64 | g_config.err_stream = state->err_stream = stderr; 65 | g_config.out_stream = state->out_stream = stdout; 66 | break; 67 | 68 | // Options 69 | case OPT_VERBOSE_KEY: 70 | g_config.verbose = true; 71 | break; 72 | case OPT_IFNAME_KEY: 73 | ifindex = if_nametoindex(arg); 74 | if (ifindex == 0) 75 | { 76 | argp_error(state, "option '--%s' requires an existing interface: got '%s'\n", OPT_IFNAME_LONG, arg); 77 | } 78 | g_config.ifindex = ifindex; 79 | strcpy(g_config.ifname, arg); 80 | break; 81 | case OPT_ATTACH_KEY: 82 | /**/ if (strncasecmp(arg, "egress", 6) == 0) 83 | { 84 | g_config.attach_point = BPF_TC_EGRESS; 85 | } 86 | else if (strncasecmp(arg, "ingress", 7) == 0) 87 | { 88 | g_config.attach_point = BPF_TC_INGRESS; 89 | } 90 | else 91 | { 92 | argp_error(state, "option '--%s' requires one of the following values: %s", OPT_ATTACH_LONG, OPT_ATTACH_ARG); 93 | } 94 | break; 95 | case OPT_NO_CLEANUP_KEY: 96 | g_config.cleanup_on_exit = false; 97 | break; 98 | 99 | // Arguments 100 | case ARGP_KEY_ARG: 101 | assert(arg); 102 | for (p = 0; p < NUM_PROGRAMS; p++) 103 | { 104 | if (strcasecmp(arg, g_programs_name[p]) == 0) 105 | { 106 | g_config.program = (program_t)p; 107 | break; 108 | } 109 | } 110 | g_config.program_arg = arg; 111 | break; 112 | 113 | case ARGP_KEY_END: 114 | if (state->arg_num == 0) 115 | { 116 | print_log(state->err_stream, true, true, "program name is mandatory\n\n", NULL); 117 | argp_state_help(state, state->err_stream, ARGP_HELP_STD_HELP | ARGP_HELP_EXIT_ERR); 118 | } 119 | if (g_config.program == program_0) 120 | { 121 | argp_error(state, "argument '%s' is not a " TOOL_NAME " program", g_config.program_arg); 122 | } 123 | break; 124 | 125 | // Final settings, validations 126 | case ARGP_KEY_FINI: 127 | // Fallback to the default gateway interface by default 128 | if (g_config.ifindex == 0) 129 | { 130 | if (get_gateway_iface(g_config.ifname)) 131 | { 132 | argp_error(state, "could not get the default gateway interface\n"); 133 | } 134 | g_config.ifindex = if_nametoindex(g_config.ifname); 135 | assert(g_config.ifindex != 0); 136 | } 137 | break; 138 | 139 | default: 140 | return ARGP_ERR_UNKNOWN; 141 | } 142 | return 0; 143 | } 144 | 145 | static const struct argp argp = { 146 | .options = argp_opts, 147 | .parser = parse_cli, 148 | .args_doc = "PROGRAM", 149 | .doc = argp_program_doc, 150 | }; 151 | 152 | int get_gateway_iface(char *interface) 153 | { 154 | long dest, gateway; 155 | char iface[IF_NAMESIZE]; 156 | char buf[4096]; 157 | FILE *file; 158 | 159 | memset(iface, 0, sizeof(iface)); 160 | memset(buf, 0, sizeof(buf)); 161 | 162 | file = fopen("/proc/net/route", "r"); 163 | if (!file) 164 | { 165 | return -1; 166 | } 167 | 168 | while (fgets(buf, sizeof(buf), file)) 169 | { 170 | if (sscanf(buf, "%s %lx %lx", iface, &dest, &gateway) == 3) 171 | { 172 | // default route 173 | if (dest == 0) 174 | { 175 | // note > gateway variable contains the address of the gateway 176 | strcpy(interface, iface); 177 | fclose(file); 178 | return 0; 179 | } 180 | } 181 | } 182 | 183 | // default route not found 184 | if (file) 185 | { 186 | fclose(file); 187 | } 188 | return -1; 189 | } 190 | 191 | static volatile sig_atomic_t g_stop; 192 | 193 | void sig_handler(int signo) 194 | { 195 | g_stop = 1; 196 | } 197 | 198 | static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) 199 | { 200 | return print_log(g_config.err_stream, level == LIBBPF_DEBUG && g_config.verbose, false, format, args); 201 | } 202 | 203 | int await(struct bpf_tc_hook hook, struct bpf_tc_opts opts) 204 | { 205 | // Block until user signal 206 | while (!g_stop) 207 | { 208 | fprintf(stdout, "."); 209 | fflush(stdout); 210 | sleep(1); 211 | } 212 | fprintf(stdout, "\n"); 213 | 214 | return 0; 215 | } 216 | 217 | int main(int argc, char **argv) 218 | { 219 | int err; 220 | char buf[100]; 221 | buf[sizeof(buf) - 1] = '\0'; 222 | 223 | // CLI 224 | err = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, NULL, NULL); 225 | if (err) 226 | { 227 | return 1; 228 | } 229 | 230 | // Setup signal handling 231 | if (signal(SIGINT, sig_handler) == SIG_ERR || signal(SIGTERM, sig_handler) == SIG_ERR) 232 | { 233 | log_erro("can't handle signal: %s\n", strerror(errno)); 234 | return 1; 235 | } 236 | 237 | // Setup libbpf 238 | libbpf_set_strict_mode(LIBBPF_STRICT_ALL); 239 | libbpf_set_print(libbpf_print_fn); 240 | 241 | // Execute 242 | log_info("prog: %s\n", g_programs_name[g_config.program]); 243 | return attach(&g_config, &await, NULL); 244 | } 245 | -------------------------------------------------------------------------------- /vmlinux/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !xmake.lua 3 | !.gitignore -------------------------------------------------------------------------------- /vmlinux/xmake.lua: -------------------------------------------------------------------------------- 1 | set_xmakever("2.6.1") -- Minimum version to compile BPF source correctly 2 | 3 | -- options 4 | --- run `xmake f --generate-vmlinux=y` to always generate vmlinux.h rather than assuming one exists in vmlinux/ 5 | option("generate-vmlinux", {showmenu = true, default = false, description = "always generate vmlinux.h"}) 6 | 7 | -- vmlinux 8 | local vmlinuxh = path.join("vmlinux", "vmlinux.h") 9 | target("vmlinux") 10 | set_kind("headeronly") 11 | add_includedirs(path.join("$(projectdir)", "vmlinux"), { public = true }) 12 | 13 | local missing = false 14 | if has_config("generate-vmlinux") then 15 | -- We're gonna need bpftool to generate the vmlinux.h 16 | includes("../tools") 17 | add_deps("bpftool") 18 | else 19 | if not os.exists(path.absolute(vmlinuxh, os.projectdir())) then 20 | -- We're assuming you provide a vmlinux.h 21 | missing = true 22 | end 23 | end 24 | 25 | before_build(function(target) 26 | -- Ensure vmlinux.h exists 27 | if missing then 28 | os.raise("missing " .. vmlinuxh .. ": provide it or configure xmake to generate one for you with xmake f --generate-vmlinux=y") 29 | end 30 | 31 | import("utils.progress") 32 | if has_config("generate-vmlinux") then 33 | -- Generate vmlinux.h 34 | progress.show(20, "${color.build.object}generating.vmlinux %s", vmlinuxh) 35 | local bpftool = path.join("tools", "bpftool") 36 | os.execv(bpftool, { "btf", "dump", "file", "/sys/kernel/btf/vmlinux", "format", "c" }, { stdout = vmlinuxh }) 37 | end 38 | end) 39 | on_clean(function(target) 40 | -- Delete vmlinux.h if we always generate it 41 | if has_config("generate-vmlinux") then 42 | os.tryrm(vmlinuxh) 43 | end 44 | -- Otherwise do nothing to keep the vmlinux.h the user provided 45 | end) -------------------------------------------------------------------------------- /xmake.lua: -------------------------------------------------------------------------------- 1 | set_xmakever("2.6.1") -- Minimum version to compile BPF source correctly 2 | 3 | -- includes 4 | includes("xmake/repos.lua") 5 | 6 | -- rules 7 | add_rules("mode.release", "mode.debug") 8 | 9 | -- traffico 10 | target("traffico") 11 | set_kind("binary") 12 | set_default(true) 13 | includes("api") 14 | add_deps("api") 15 | add_deps("bpf") 16 | add_packages("libbpf") 17 | add_files({ "traffico.c" }, { languages = { "c11" }}) 18 | target_end() 19 | 20 | -- traffico-cni 21 | add_requires("cjson") 22 | target("traffico-cni") 23 | set_kind("binary") 24 | add_packages("cjson") 25 | includes("api") 26 | add_deps("api") 27 | add_deps("bpf") 28 | add_packages("libbpf") 29 | add_files({ "traffico-cni.c" }, { languages = { "c11" }}) 30 | target_end() 31 | 32 | -- test 33 | add_requires("bats v1.7.0", { system = false }) 34 | add_requires("mini_httpd", { system = false }) 35 | target("test") 36 | set_kind("phony") 37 | add_deps("traffico", "traffico-cni") 38 | add_packages("bats", "mini_httpd") 39 | on_run(function (target) 40 | for _, name in ipairs(target:get("deps")) do 41 | os.addenv("PATH", path.absolute(target:dep(name):targetdir())) 42 | end 43 | import("privilege.sudo") 44 | sudo.execv("bats", { "-t", "test/" }) 45 | end) 46 | target_end() 47 | -------------------------------------------------------------------------------- /xmake/modules/api.lua: -------------------------------------------------------------------------------- 1 | import("core.project.project") 2 | 3 | -- get sourcefiles 4 | function _get_programs(target_name) 5 | local programs = {} 6 | for _, target in pairs(project.targets()) do 7 | if target:is_enabled() and target:name() == target_name then 8 | for _, src in pairs(target:sourcebatches()) do 9 | table.join2(programs, src.sourcefiles) 10 | end 11 | end 12 | end 13 | return programs 14 | end 15 | 16 | function gen(target, source_target) 17 | if not target then 18 | raise("could not configure target") 19 | end 20 | 21 | target:set("configdir", target:autogendir()) 22 | 23 | local configfile_template = "api.$progname.h.in" 24 | local configfile_template_path = path.join(target:scriptdir(), configfile_template) 25 | 26 | local programs = _get_programs(source_target) 27 | for _, p in ipairs(programs) do 28 | local progname = string.match(path.basename(p), "(.+)%..+$") 29 | local confname = string.gsub(configfile_template, "%$(%w+)", { progname = progname }) 30 | local tempconf = path.join(os.tmpdir(), confname) 31 | os.tryrm(tempconf) 32 | os.cp(configfile_template_path, tempconf) 33 | target:add("configfiles", tempconf, { variables = { PROGNAME = progname, OPERATION = "attach__" } }) 34 | end 35 | end 36 | 37 | function main(target, components_target, banner) 38 | if not target then 39 | raise("could not configure target") 40 | end 41 | 42 | local gendir = target:autogendir() 43 | target:add("includedirs", gendir, { public = true }) 44 | target:set("configdir", gendir) 45 | 46 | local v = {} 47 | 48 | local _, components, vars = project.target(components_target):configfiles() 49 | local num_components = #components 50 | v["PROGRAMS_COUNT"] = num_components + 1 51 | 52 | local op = vars[1].variables.OPERATION 53 | v["OPERATION"] = op 54 | 55 | local programs = {} 56 | table.insert(programs, "0") 57 | for _, v in ipairs(vars) do 58 | table.insert(programs, v.variables.PROGNAME) 59 | end 60 | table.sort(programs) 61 | v["PROGRAMS_AS_SYMBOLS"] = 'program_' .. table.concat(programs, ", program_") 62 | v["PROGRAMS_AS_STRINGS"] = '"' .. table.concat(programs, '", "') .. '"' 63 | v["PROGRAMS_OPS_AS_SYMBOLS"] = op .. table.concat(programs, ', ' .. op) 64 | table.remove(programs, 1) 65 | v["PROGRAMS_DESCRIPTION"] = '" - ' .. table.concat(programs, '\\n - ') .. '"' 66 | 67 | local content = "" 68 | for i, c in ipairs(components) do 69 | if banner then 70 | content = content .. "/// " .. c .. "\n" 71 | end 72 | content = content .. io.readfile(c) 73 | if i < num_components then 74 | content = content .. "\n\n" 75 | end 76 | end 77 | v["API"] = content 78 | 79 | local configfile = path.join(target:scriptdir(), "api.h.in") 80 | target:add("configfiles", configfile, { variables = v }) 81 | end 82 | -------------------------------------------------------------------------------- /xmake/repos.lua: -------------------------------------------------------------------------------- 1 | add_repositories("l13o https://github.com/l13o/xmake-repo.git main") 2 | --------------------------------------------------------------------------------