diff options
-rw-r--r-- | .clang-format | 16 | ||||
-rw-r--r-- | cmd/bpf/bpf_bpfel.go | 125 | ||||
-rw-r--r-- | cmd/bpf/bpf_bpfel.o | bin | 0 -> 5312 bytes | |||
-rw-r--r-- | cmd/bpf/bpf_usage.c | 73 | ||||
-rw-r--r-- | cmd/bpf/gen.go | 3 | ||||
-rw-r--r-- | cmd/bpf/main.go | 96 | ||||
-rw-r--r-- | flake.nix | 15 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 14 |
9 files changed, 343 insertions, 1 deletions
diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..f304ad3 --- /dev/null +++ b/.clang-format @@ -0,0 +1,16 @@ +BasedOnStyle: LLVM +BreakBeforeBraces: Linux +IndentCaseLabels: false + +AlignConsecutiveMacros: true +AllowShortIfStatementsOnASingleLine: false + +ContinuationIndentWidth: 8 +IndentWidth: 8 +TabWidth: 8 +UseTab: Always + +ForEachMacros: + - 'LIST_FOREACH' + - 'CIRCLEQ_FOREACH' + - 'cJSON_ArrayForEach' diff --git a/cmd/bpf/bpf_bpfel.go b/cmd/bpf/bpf_bpfel.go new file mode 100644 index 0000000..1efd705 --- /dev/null +++ b/cmd/bpf/bpf_bpfel.go @@ -0,0 +1,125 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 + +package main + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +// loadBpf returns the embedded CollectionSpec for bpf. +func loadBpf() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_BpfBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load bpf: %w", err) + } + + return spec, err +} + +// loadBpfObjects loads bpf and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *bpfObjects +// *bpfPrograms +// *bpfMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadBpf() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// bpfSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfSpecs struct { + bpfProgramSpecs + bpfMapSpecs +} + +// bpfSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfProgramSpecs struct { + EgressFunc *ebpf.ProgramSpec `ebpf:"egress__func"` + IngressFunc *ebpf.ProgramSpec `ebpf:"ingress_func"` +} + +// bpfMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type bpfMapSpecs struct { + EgressIp4UsageMap *ebpf.MapSpec `ebpf:"egress_ip4_usage_map"` + IngressIp4UsageMap *ebpf.MapSpec `ebpf:"ingress_ip4_usage_map"` +} + +// bpfObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfObjects struct { + bpfPrograms + bpfMaps +} + +func (o *bpfObjects) Close() error { + return _BpfClose( + &o.bpfPrograms, + &o.bpfMaps, + ) +} + +// bpfMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfMaps struct { + EgressIp4UsageMap *ebpf.Map `ebpf:"egress_ip4_usage_map"` + IngressIp4UsageMap *ebpf.Map `ebpf:"ingress_ip4_usage_map"` +} + +func (m *bpfMaps) Close() error { + return _BpfClose( + m.EgressIp4UsageMap, + m.IngressIp4UsageMap, + ) +} + +// bpfPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. +type bpfPrograms struct { + EgressFunc *ebpf.Program `ebpf:"egress__func"` + IngressFunc *ebpf.Program `ebpf:"ingress_func"` +} + +func (p *bpfPrograms) Close() error { + return _BpfClose( + p.EgressFunc, + p.IngressFunc, + ) +} + +func _BpfClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed bpf_bpfel.o +var _BpfBytes []byte diff --git a/cmd/bpf/bpf_bpfel.o b/cmd/bpf/bpf_bpfel.o Binary files differnew file mode 100644 index 0000000..789c00f --- /dev/null +++ b/cmd/bpf/bpf_bpfel.o diff --git a/cmd/bpf/bpf_usage.c b/cmd/bpf/bpf_usage.c new file mode 100644 index 0000000..5e505da --- /dev/null +++ b/cmd/bpf/bpf_usage.c @@ -0,0 +1,73 @@ +#include <linux/bpf.h> +#include <linux/if_ether.h> +#include <linux/ip.h> + +#include <bpf/bpf_endian.h> +#include <bpf/bpf_helpers.h> + +#define MAX_MAP_ENTRIES 4096 + +char __license[] SEC("license") = "GPL"; + +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, MAX_MAP_ENTRIES); + __type(key, __u32); // source IPv4 address + __type(value, __u64); // no of bytes +} ingress_ip4_usage_map SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, MAX_MAP_ENTRIES); + __type(key, __u32); // destination IPv4 address + __type(value, __u64); // no of bytes +} egress_ip4_usage_map SEC(".maps"); + +typedef enum { + UPDATE_USAGE_INGRESS, + UPDATE_USAGE_EGRESS, +} update_usage_t; + +static __always_inline int update_usage(void *map, struct __sk_buff *skb, + update_usage_t traffic) +{ + __u32 ip4; + __u64 len, *usage; + + void *data_end = (void *)(long)skb->data_end; + void *data = (void *)(long)skb->data; + struct iphdr *ip = data + sizeof(struct ethhdr); + + if (skb->protocol != bpf_htons(ETH_P_IP)) + return TCX_PASS; + else if ((void *)(ip + 1) > data_end) + return TCX_PASS; + + if (traffic == UPDATE_USAGE_INGRESS) + ip4 = ip->saddr; + else + ip4 = ip->daddr; + len = skb->len - sizeof(struct ethhdr); + + usage = bpf_map_lookup_elem(map, &ip4); + if (!usage) { + /* no entry in the map for this IP address yet. */ + bpf_map_update_elem(map, &ip4, &len, BPF_ANY); + } else { + __sync_fetch_and_add(usage, len); + } + + return TCX_PASS; +} + +SEC("tc") +int ingress_func(struct __sk_buff *skb) +{ + return update_usage(&ingress_ip4_usage_map, skb, UPDATE_USAGE_INGRESS); +} + +SEC("tc") +int egress__func(struct __sk_buff *skb) +{ + return update_usage(&egress_ip4_usage_map, skb, UPDATE_USAGE_EGRESS); +} diff --git a/cmd/bpf/gen.go b/cmd/bpf/gen.go new file mode 100644 index 0000000..c90c0a7 --- /dev/null +++ b/cmd/bpf/gen.go @@ -0,0 +1,3 @@ +package main + +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel bpf bpf_usage.c diff --git a/cmd/bpf/main.go b/cmd/bpf/main.go new file mode 100644 index 0000000..5a5f842 --- /dev/null +++ b/cmd/bpf/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "log" + "net" + "net/netip" + "os" + "time" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" +) + +type Usage struct { + ingress uint64 + egress uint64 +} + +func main() { + if len(os.Args) < 2 { + log.Fatalf("Please specify a network interface") + } + + ifaceName := os.Args[1] + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + log.Fatalf("lookup network iface %q: %s", ifaceName, err) + } + + // Load pre-compiled programs into the kernel. + objs := bpfObjects{} + if err := loadBpfObjects(&objs, nil); err != nil { + log.Fatalf("loading objects: %s", err) + } + defer objs.Close() + + // Attach the program. + ingressLink, err := link.AttachTCX(link.TCXOptions{ + Interface: iface.Index, + Program: objs.IngressFunc, + Attach: ebpf.AttachTCXIngress, + }) + if err != nil { + log.Fatalf("could not attach TCx program: %s", err) + } + defer ingressLink.Close() + + // Attach the program to Egress TC. + egressLink, err := link.AttachTCX(link.TCXOptions{ + Interface: iface.Index, + Program: objs.EgressFunc, + Attach: ebpf.AttachTCXEgress, + }) + if err != nil { + log.Fatalf("could not attach TCx program: %s", err) + } + defer egressLink.Close() + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for range ticker.C { + prettyPrint(objs.IngressIp4UsageMap, objs.EgressIp4UsageMap) + } +} + +func prettyPrint(ingress *ebpf.Map, egress *ebpf.Map) { + ipUsage := make(map[netip.Addr]Usage) + var key netip.Addr + var value uint64 + + iter := ingress.Iterate() + for iter.Next(&key, &value) { + ipUsage[key] = Usage { + ingress: value, + } + } + + iter = egress.Iterate() + for iter.Next(&key, &value) { + usage, ok := ipUsage[key] + if (ok) { + usage.egress = value + } else { + usage = Usage { egress: value } + } + + ipUsage[key] = usage + } + + fmt.Print("\033[H\033[2J") + fmt.Printf("%15s\t%16s\t%16s\n", "ip", "down", "up") + for ip4, usage := range ipUsage { + fmt.Printf("%15s\t%16d\t%16d\n", ip4, usage.ingress, usage.egress) + } +} @@ -16,9 +16,22 @@ default = pkgs.mkShell { name = "dev"; - buildInputs = with pkgs; [ go_1_22 gopls jq sqlite ]; + buildInputs = with pkgs; [ + go + gopls + + jq + sqlite + + libbpf + ccls + clang + libllvm + ]; shellHook = '' export PS1="\033[0;36m[ ]\033[0m $PS1" + # stop littering eBPF C programs with go:build ignore + export CGO_ENABLED=0 ''; }; }); @@ -3,6 +3,7 @@ module sinanmohd.com/redq go 1.22.0 require ( + github.com/cilium/ebpf v0.15.0 github.com/go-playground/validator/v10 v10.19.0 github.com/mattn/go-sqlite3 v1.14.22 lukechampine.com/blake3 v1.2.1 @@ -15,6 +16,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/leodido/go-urn v1.4.0 // indirect golang.org/x/crypto v0.19.0 // indirect + golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect @@ -1,3 +1,5 @@ +github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= +github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -10,18 +12,30 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |