├── bpf ├── .gitignore ├── Makefile └── tc-prog.c ├── GeoIP.dat ├── docs ├── app-xRunning.png ├── demoOverview.png ├── app3-submitWord.png └── requestsByCountry.png ├── go.mod ├── app-x ├── app-1 │ └── main.go ├── app-2 │ └── main.go ├── main.go └── app-3 │ └── main.go ├── go.sum ├── tracing ├── README.md └── main.go ├── main.go ├── static └── index.html └── README.md /bpf/.gitignore: -------------------------------------------------------------------------------- 1 | tc-prog.d 2 | tc-prog.o 3 | -------------------------------------------------------------------------------- /GeoIP.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b3a-dev/ebpf-geoip-demo/HEAD/GeoIP.dat -------------------------------------------------------------------------------- /docs/app-xRunning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b3a-dev/ebpf-geoip-demo/HEAD/docs/app-xRunning.png -------------------------------------------------------------------------------- /docs/demoOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b3a-dev/ebpf-geoip-demo/HEAD/docs/demoOverview.png -------------------------------------------------------------------------------- /docs/app3-submitWord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b3a-dev/ebpf-geoip-demo/HEAD/docs/app3-submitWord.png -------------------------------------------------------------------------------- /docs/requestsByCountry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b3a-dev/ebpf-geoip-demo/HEAD/docs/requestsByCountry.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/b3a-dev/http-map-ws 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/abh/geoip v0.0.0-20160510155516-07cea4480daa 7 | github.com/cilium/ebpf v0.0.0-20201005075717-edc4db4deb5b 8 | github.com/iovisor/gobpf v0.0.0-20200614202714-e6b321d32103 9 | ) 10 | -------------------------------------------------------------------------------- /app-x/app-1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | http.HandleFunc("/", HelloServer) 10 | http.ListenAndServe(":8081", nil) 11 | } 12 | 13 | func HelloServer(w http.ResponseWriter, r *http.Request) { 14 | fmt.Fprintf(w, "Hello World!\n") 15 | } -------------------------------------------------------------------------------- /app-x/app-2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | http.HandleFunc("/", HelloServer) 10 | http.ListenAndServe(":8082", nil) 11 | } 12 | 13 | func HelloServer(w http.ResponseWriter, r *http.Request) { 14 | fmt.Fprintf(w, "eBPF Summit\n") 15 | } -------------------------------------------------------------------------------- /app-x/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | http.HandleFunc("/", HelloServer) 10 | http.ListenAndServe(":8081", nil) 11 | } 12 | 13 | func HelloServer(w http.ResponseWriter, r *http.Request) { 14 | fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:]) 15 | } -------------------------------------------------------------------------------- /app-x/app-3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func main() { 10 | http.HandleFunc("/", helloServer) 11 | http.ListenAndServe(":8083", nil) 12 | } 13 | 14 | func helloServer(w http.ResponseWriter, r *http.Request) { 15 | fmt.Fprintf(w, "Submitted word: %s!\n", r.URL.Path[1:]) 16 | word := strings.TrimSpace(r.URL.Path[1:]) 17 | postWord(word) 18 | } 19 | 20 | func postWord(word string) { 21 | fmt.Println("New request with word:", word) 22 | } 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/abh/geoip v0.0.0-20160510155516-07cea4480daa h1:o7+BnQZpdqHPCc9F2fTWPCM9Y9AyUHBWbTL+pCrCdb0= 2 | github.com/abh/geoip v0.0.0-20160510155516-07cea4480daa/go.mod h1:N2q9pP3q4thAewFqmOB/DL8EsWimMuDOx4KduwXMT5A= 3 | github.com/cilium/ebpf v0.0.0-20201005075717-edc4db4deb5b h1:8gHt/Rv262jGvuAlkW2buuyo9HjdwQFmeGQySL9Oocs= 4 | github.com/cilium/ebpf v0.0.0-20201005075717-edc4db4deb5b/go.mod h1:44MooKJJW1eGX0M0Ne/tB1uxHmzizXDaLREv3WIHrJ0= 5 | github.com/iovisor/gobpf v0.0.0-20200614202714-e6b321d32103 h1:U2+owsfuUe5xehsVEmLZW91zY1ce7z3/YCQjMLVb6Q4= 6 | github.com/iovisor/gobpf v0.0.0-20200614202714-e6b321d32103/go.mod h1:+5U5qu5UOu8YJ5oHVLvWKH7/Dr5QNHU7mZ2RfPEeXg8= 7 | golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 8 | -------------------------------------------------------------------------------- /bpf/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean all 2 | .SUFFIXES: 3 | 4 | CILIUM_DIR ?= $(GOPATH)/src/github.com/cilium/cilium/ 5 | NCPUS ?= $(shell nproc --all) 6 | 7 | BPFCC = clang 8 | BPFCFLAGS = -O2 -nostdinc -Wall -Wextra -Wshadow -target bpf -emit-llvm -g 9 | BPFCFLAGS += -I$(CILIUM_DIR)/bpf/include -I$(CILIUM_DIR)/bpf/ 10 | BPFCFLAGS += -DNCPUS=$(NCPUS) 11 | BPFLLC = llc 12 | BPFLLCFLAGS = -march=bpf -mcpu=probe -mattr=dwarfris 13 | 14 | ifneq ($(V),1) 15 | Q ?= @ 16 | endif 17 | 18 | all: tc-prog.o 19 | 20 | %.ll: %.c 21 | @echo " BPFC $@" 22 | $(Q) $(BPFCC) $(BPFCFLAGS) -c $< -o $@ 23 | $(Q) $(BPFCC) $(BPFCFLAGS) -MM $< -MT $@ -MF $*.d 24 | 25 | %.o: %.ll 26 | @echo " LLC $@" 27 | $(Q) $(BPFLLC) $(BPFLLCFLAGS) -filetype=obj $< -o $@ 28 | 29 | read-perf: FORCE 30 | go build -o read-perf 31 | 32 | clean: 33 | rm -f *.o *.ll *.d 34 | 35 | FORCE: 36 | -------------------------------------------------------------------------------- /tracing/README.md: -------------------------------------------------------------------------------- 1 | # Tracing Go function with eBPF 2 | We are going to be using the simple Go server [app-3](https://github.com/b3a-dev/ebpf-geoip-demo/tree/master/app-x/app-3) that will be accepting http requests: 3 | ``` 4 | GET http://pubIP:8083/`my_word` -> "Submitted word: `my_word`" 5 | ``` 6 | * When you go build it, the function that receives the posted word as parameter is created as a symbol in the created binary. 7 | * Uprobes let you create a hook at a memory address anywhere in userspace. 8 | * You can attach a uprobe to the function symbol, which you can have trigger an eBPF program. 9 | * The uprobe is copied into memory anytime the binary is executed, meaning it will trigger anytime any process runs that function. 10 | * Then using the gobpf library, we can write a small eBPF program that will be triggered anytime the function in the simple Go server is executed, with every post with a word. 11 | * By using an eBPF map, we can then read the words written by the ebpf side, and build a words-cloud that will be served and exposed with a public IP. 12 | 13 | 14 | ## Demo Steps 15 | ### Setup the server 16 | * The server used is an Ubuntu VM from GCP (or other cloud provider). 17 | * Don't forget to allow http traffic for this instance. 18 | * Install dependencies: bcc from sources: https://github.com/iovisor/bcc/blob/master/INSTALL.md#ubuntu---source 19 | 20 | *Note: the listed steps didn't word for me and in order to fix an error I had to add this step: 21 | ``` 22 | sudo apt-get install python3-distutils --reinstall 23 | ``` 24 | 25 | ### Run the simple Go server 26 | We need the binary of the server [app-3](https://github.com/b3a-dev/ebpf-geoip-demo/tree/master/app-x/app-3) as we will use it as parameter. 27 | 28 | ``` 29 | cd app-3 30 | go build 31 | ./ 32 | ``` 33 | 34 | ### Run tracing 35 | Right now this app is taking the name of the binary that contains the function we are going to be tracing as parameter. 36 | 37 | ``` 38 | go run main.go /path/to/app-3/binary/ 39 | ``` 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tracing/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/iovisor/gobpf/bcc" 14 | ) 15 | 16 | const eBPF_Program = ` 17 | #include 18 | #include 19 | 20 | 21 | #define SP_OFFSET(offset) (void *)PT_REGS_SP(ctx) + offset * 8 22 | 23 | struct str_arg { 24 | unsigned short len; 25 | char parameter_value[256]; 26 | }__attribute__((packed)); 27 | 28 | BPF_PERF_OUTPUT(events); 29 | 30 | int get_arguments(struct pt_regs *ctx) { 31 | struct str_arg arg; 32 | char *parameter_value; 33 | 34 | bpf_probe_read(¶meter_value, sizeof(parameter_value), SP_OFFSET(1)); 35 | bpf_probe_read(&arg.len, sizeof(arg.len), SP_OFFSET(2)); 36 | bpf_probe_read_str(&arg.parameter_value, sizeof(arg.parameter_value), (void *)parameter_value); 37 | 38 | events.perf_submit(ctx, &arg, sizeof(arg)); 39 | 40 | return 0; 41 | } 42 | ` 43 | 44 | func main() { 45 | 46 | bpfModule := bcc.NewModule(eBPF_Program, []string{}) 47 | 48 | uprobeFd, err := bpfModule.LoadUprobe("get_arguments") 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | // AttachUprobe attaches a uprobe fd to the symbol in the library or binary 'name' 54 | // The 'name' argument can be given as either a full library path (/usr/lib/..), a library without the lib prefix, 55 | // or as a binary with full path (/bin/bash) A pid can be given to attach to, or -1 to attach to all processes 56 | err = bpfModule.AttachUprobe(os.Args[1], "main.postWord", uprobeFd, -1) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | lostChan := make(chan uint64) 62 | table := bcc.NewTable(bpfModule.TableId("events"), bpfModule) 63 | channel := make(chan []byte) 64 | 65 | // InitPerfMap initializes a perf map with a receiver channel, with a default page_cnt. 66 | // func InitPerfMap(table *Table, receiverChan chan []byte, lostChan chan uint64) (*PerfMap, error) 67 | perfMap, err := bcc.InitPerfMap(table, channel, lostChan) 68 | 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | 73 | c := make(chan os.Signal, 1) 74 | signal.Notify(c, os.Interrupt) 75 | 76 | go func() { 77 | for { 78 | value := <-channel 79 | argument := strings.Split(string(value), " ") 80 | word := argument[0] 81 | fmt.Println(word) 82 | } 83 | }() 84 | 85 | perfMap.Start() 86 | <-c 87 | perfMap.Stop() 88 | 89 | // Serve static files (index.html) for client side in browser 90 | http.Handle("/", http.FileServer(http.Dir("./static"))) 91 | 92 | // Word Cloud chart in index.html will call /data.json to read data 93 | http.HandleFunc("/wordCloudData.json", wordcloudDataHandler) 94 | 95 | log.Println("Listening on :80...") 96 | err = http.ListenAndServe(":80", nil) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | } 101 | 102 | // wordcloudDataHandler writes the json body needed to draw in the word cloud (index.html) 103 | 104 | // The returned object needs to be an array of: 105 | // name: Name of the country 106 | // id: country ID 107 | // percent: percent of the total requests in that country 108 | // amount: number requests in that country 109 | func wordcloudDataHandler(w http.ResponseWriter, req *http.Request) { 110 | w.Header().Set("Content-Type", "application/json") 111 | 112 | points := []mapPoint{} 113 | for country, amount := range requestsByCountry { 114 | point := mapPoint{ 115 | Name: country, 116 | ID: country, 117 | Amount: strconv.Itoa(amount), 118 | Percent: fmt.Sprintf("%f", (float64(amount)/float64(requestsTotal))*100), 119 | } 120 | points = append(points, point) 121 | 122 | } 123 | str, _ := json.Marshal(points) 124 | fmt.Fprintf(w, string(str)) 125 | } 126 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | "net" 10 | 11 | "github.com/cilium/ebpf" 12 | "github.com/cilium/ebpf/perf" 13 | 14 | "github.com/abh/geoip" 15 | ) 16 | 17 | // File containing GeoIP Lite database (Maxmind) 18 | const geoIPFile = "GeoIP.dat" 19 | 20 | // mapPoint is used to create a point in the Javascript map 21 | type mapPoint struct { 22 | 23 | // Name of the country 24 | Name string `json:"name"` 25 | 26 | // ID is the country code of the country 27 | ID string `json:"id"` 28 | 29 | // Percentage of the requests for this point 30 | Percent string `json:"percent"` 31 | 32 | // Number of requests for this point 33 | Amount string `json:"amount"` 34 | } 35 | 36 | // For GeoIP to work you need to have GeoIP lib installed in the system 37 | // as this Go lib is just a wrapper 38 | var gi *geoip.GeoIP 39 | 40 | // requestsByCountry contains request counter for each country 41 | var requestsByCountry map[string]int 42 | 43 | // requestsTotal contains counter of total requests 44 | var requestsTotal int 45 | 46 | // A request is described as a string with the source IPv4 Address 47 | var requests chan string 48 | 49 | func main() { 50 | 51 | var err error 52 | 53 | requests = make(chan string) 54 | requestsByCountry = make(map[string]int) 55 | go readRequests() 56 | 57 | gi, err = geoip.Open(geoIPFile) 58 | if err != nil { 59 | log.Fatalln("Could not open GeoIP database") 60 | } 61 | 62 | // eBPF map 63 | path := "/sys/fs/bpf/tc/globals/xevents" 64 | eventsMap, err := ebpf.LoadPinnedMap(path) 65 | if err != nil { 66 | log.Fatal("failed to load xevents: ", err) 67 | } 68 | 69 | fmt.Printf("MaxEntries=%d\n", eventsMap.ABI().MaxEntries) 70 | bufferSize := int(4096 * eventsMap.ABI().MaxEntries) 71 | eventsRd, err := perf.NewReader(eventsMap, bufferSize) 72 | if err != nil { 73 | log.Fatal("Failed to initialize perf ring buffer:", err) 74 | } 75 | defer eventsRd.Close() 76 | 77 | go func() { 78 | for { 79 | rec, err := eventsRd.Read() 80 | if err != nil { 81 | break 82 | } 83 | ip4 := net.IPv4( 84 | rec.RawSample[0], 85 | rec.RawSample[1], 86 | rec.RawSample[2], 87 | rec.RawSample[3], 88 | ) 89 | //fmt.Printf("->%s\n", ip4.String()) 90 | requests <- ip4.String() 91 | } 92 | 93 | }() 94 | 95 | // Serve static files (index.html) for client side in browser 96 | http.Handle("/", http.FileServer(http.Dir("./static"))) 97 | 98 | // Map chart in index.html will call /data.json to read data 99 | http.HandleFunc("/data.json", mapDataHandler) 100 | 101 | log.Println("Listening on :80...") 102 | err = http.ListenAndServe(":80", nil) 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | 107 | } 108 | 109 | // readRequests reads requests from channel 110 | // request must be a string containing the ipv4 address 111 | // example: 8.8.8.8 112 | func readRequests() { 113 | for { 114 | select { 115 | case ip := <-requests: 116 | fmt.Println("New request from: ", ip) 117 | go processRequest(ip) 118 | } 119 | } 120 | 121 | } 122 | 123 | // processRequest invokes geoIP database to get country code 124 | // then increments 2 counters: 125 | // - specific counter for country code 126 | // - global counter 127 | func processRequest(ip string) { 128 | countryCode := getCountryByIP(ip) 129 | requestsByCountry[countryCode]++ 130 | requestsTotal++ 131 | } 132 | 133 | // getCountryByIP given a IPv4 address it returns 134 | // a country code (I.E. "ES") 135 | func getCountryByIP(ip string) string { 136 | country, _ := gi.GetCountry(ip) 137 | return country 138 | 139 | } 140 | 141 | // mapDataHandler writes the json body needed to draw in the map (index.html) 142 | // The returned object needs to be an array of: 143 | // name: Name of the country 144 | // id: country ID 145 | // percent: percent of the total requests in that country 146 | // amount: number requests in that country 147 | func mapDataHandler(w http.ResponseWriter, req *http.Request) { 148 | w.Header().Set("Content-Type", "application/json") 149 | 150 | points := []mapPoint{} 151 | for country, amount := range requestsByCountry { 152 | point := mapPoint{ 153 | Name: country, 154 | ID: country, 155 | Amount: strconv.Itoa(amount), 156 | Percent: fmt.Sprintf("%f", (float64(amount)/float64(requestsTotal))*100), 157 | } 158 | points = append(points, point) 159 | 160 | } 161 | str, _ := json.Marshal(points) 162 | fmt.Fprintf(w, string(str)) 163 | } 164 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eBPF geoip demo 2 | This repository contains the resources to be used during the eBPF Summit demo. 3 | * **app-x**: contains 1 directory per independent app that would be running in the server. 4 | * **bpf**: contains ebpf program that inspect incoming TCP packets with the configured destination port, get source IP addresses and write those in a eBPF map, used by the go userspace application. 5 | * **static**: static files to show a map in the browser with the representation of the places in the world form where different app-x are accessed. 6 | * **docs**: documentation screenshots. 7 | * **root dir**: contains a simple userspace Go server that reads from the eBPF map and shows those updating the map that is serving. 8 | * **tracing**: contains an independent second part of the demo, consist on tracing a Go function with eBPF. 9 | 10 | ## Demo overview 11 | **The high level idea for the demo:**
12 | Having x services running in a server exposed in different ports, without having to edit or make any changes at all to them, it is possible to monitor from which locations of the world are those being consumed.
13 | The attendees could try making http GET request to any of the services, and in real time we will be able to see in the map the points from where the Summit attendees are joining.
14 | 15 | 16 | demoOverviewDiagram 17 | 18 |
19 | 20 | **Events flow:** 21 | * The server used is an Ubuntu VM from GCP (or other cloud provider). 22 | * There would be services running in the VM, exposed using different ports, in the diagram: 8081, 8082 and 8083. 23 | * A eBPF program will gather the source IPs from the requests and share those with userspace Go app by using an eBPF map. 24 | * The Go app will get the location with a geoip service. 25 | * Depending on the number of requests from the same country a json structure will be formed. 26 | * Reading from the json structure the map will represent points with different sized in those countries with attendees. 27 | * Additionally more info could be gathered such: number of requests from same IP, number of different IPs (should be the same of number of attendees to the session), which services are accessing.. 28 | 29 | 30 | ## Demo Steps 31 | ### Setup the server 32 | * The server used is an Ubuntu VM from GCP (or other cloud provider). 33 | * Don't forget to allow http traffic for this instance. 34 | * TODO: Configure fw rules to permit traffic to ports: 8081, 8082 and 8083. 35 | * Install dependencies 36 | 37 | ``` 38 | $ sudo apt update 39 | $ sudo apt install make llvm clang golang-go 40 | $ git clone git@github.com:cilium/cilium.git 41 | ``` 42 | 43 | For GeoIP to work you need to have GeoIP lib installed in the system, as the used Go lib is just a wrapper. 44 | ``` 45 | $ sudo apt-get install -y geoip-database 46 | $ libgeoip-dev 47 | ``` 48 | 49 | ### Deploy the independent applications 50 | * `app-x`: contains 1 directory per independent app that would be running in the server. 51 | - app-1: running in port 8081, GET http://pubIP:8081/ -> "Hello World!" 52 | - app-2: running in port 8082, GET http://pubIP:8082/ -> "eBPF Summit" 53 | - app-3: running in port 8083, GET http://pubIP:8083/`my_word` -> "Submitted word: `my_word`" 54 | 55 | 56 | * Run the servers by running in each app-x directory: 57 | ``` 58 | $ go run main.go& 59 | ``` 60 | app-xRunning 61 | 62 | * You can test the above behavior by using both curl or a browser. 63 | 64 | app3-submitWord 65 | 66 | 67 | ### Compile and install the eBPF program 68 | Inspects incoming TCP packets with the configured destination port (initially 80), get source IP addresses and write those in a eBPF map, used by the go userspace application. 69 | 70 | #### Compile: 71 | ``` 72 | $ CILIUM_DIR=~/cilium/ make -C bpf/ 73 | ``` 74 | 75 | #### Attach the eBPF program: 76 | This is a tc (traffic control) subsystem program. The [tc(8)](http://man7.org/linux/man-pages/man8/tc-bpf.8.html) command has eBPF support, so we can directly load BPF programs as classifiers. tc programs can classify, modify, redirect or drop packets. 77 | The basics are: create a "clsact" qdisc classifier for a network device `ens4`, and then add an ingress classifier/filter by specifying the BPF object and relevant ELF section. Example, to add an ingress classifier to ens4 in ELF section `bpf-prog` from tc-prog.o (a bpf-bytecode-compiled object file): 78 | 79 | ``` 80 | $ sudo tc qdisc add dev ens4 clsact 81 | $ sudo tc filter add dev ens4 ingress bpf da obj bpf/tc-prog.o sec bpf-prog 82 | $ sudo tc filter show dev ens4 ingress 83 | ``` 84 | 85 | Expected output from last previous command: 86 | ``` 87 | filter protocol all pref 49152 bpf chain 0 88 | filter protocol all pref 49152 bpf chain 0 handle 0x1 tc-prog.o:[bpf-prog] direct-action not_in_hw id 77 tag 1e2a2ca8a73223a5 jited 89 | ``` 90 | 91 | In the case of ingress, we do classification via the core network interface receive function, so we are getting the packet after the driver has processed it but before IP etc 92 | 93 | 94 | To cleanup: 95 | ``` 96 | $ sudo sudo tc filter delete dev ens4 ingress pref 49152 handle 0x1 bpf 97 | $ sudo rm /sys/fs/bpf/tc/globals/xevents 98 | ``` 99 | 100 | ### Run the userpace Go server 101 | Simple userspace Go server that reads from the eBPF map the source IP addresses written by the eBPF program and shows those updating the map that is serving. 102 | 103 | ``` 104 | $ sudo go run main.go 105 | ``` 106 | 107 | And access http://pubIP to check the map: 108 | 109 | requestsByCountry 110 | 111 | -------------------------------------------------------------------------------- /bpf/tc-prog.c: -------------------------------------------------------------------------------- 1 | /* Include dependencies, as if we were importing needed libraries? */ 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /* tc, much more than just QoS nowadays 11 | There are hooks available in tc (Linux Traffic Control subsystem), that allow to run eBPF programs as both: filters and actions */ 12 | #ifndef TC_ACT_PIPE /* I understand this checks if the flag "direct-action" is set for tc, if set then the return value from the filter should be considered as the one of an action instead. */ 13 | #define TC_ACT_PIPE 3 /* this is a possible return value as action, means: Iterate to the next action, if available. */ 14 | #endif 15 | 16 | /* Used later */ 17 | #if !defined(NCPUS) 18 | #error "Please define NCPUS macro" 19 | #endif 20 | 21 | /* Target port for requests we want to get source IPs from. 22 | */ 23 | #define TCP_PORT 80 24 | 25 | /* kernel data types, u32: unsigned 32-bit value 26 | prefix with a dou-ble underscore as a user-space program needs to use these types 27 | */ 28 | struct event { 29 | __u32 ip_addr; /* source IP addr */ 30 | }; 31 | 32 | /* 33 | A struct bpf_elf_map entry defines a map in the program and contains all relevant information needed to generate a map which is used from BPF programs & userspace app. 34 | The structure must be placed into the maps section, so that the loader can find it. 35 | 36 | When we define this type of map (PF_MAP_TYPE_PERF_EVENT_ARRAY), for performance reasons there is one copy/instance of the datastructure per CPU. 37 | 38 | */ 39 | struct bpf_elf_map __section_maps xevents = { 40 | .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY, /* These type of Array maps are used by the kernel to associate tracing output with a specific key. User-space programs associate fds with each key, and can poll() those fds to receive notification that data has been traced. */ 41 | .size_key = sizeof(__u32), 42 | .size_value = sizeof(struct event), 43 | .pinning = PIN_GLOBAL_NS, /* Pinning options determine how the map's file descriptor is exported via the filesystem, maps which specify PIN_GLOBAL_NS are found in /sys/fs/bpf/tc/globals/ */ 44 | .max_elem = NCPUS, 45 | }; 46 | 47 | /* Type of bpf program: tc (traffic control) subsystem program 48 | tc_cls_act allows to use BPF programs as classifiers and actions in tc, the Linux QoS subsystem. 49 | tc programs can classify, modify, redirect or drop packets. 50 | 51 | we mark to the loader (tc in this case), which part is the map and the bpf program. 52 | */ 53 | __section("bpf-prog") 54 | /* Using direct packet access to read packet data, meaning we can use the __sk_buff "data" pointer to access packet data like a normal pointer. For safety BPF requires we first test we have not reached the end of the linear portion of the packet (data_end). */ 55 | int collect_ips(struct __sk_buff *skb) { /* We pass as context a pointer to the struct __sk_buff containing packet metadata/data. This structure is defined in include/linux/bpf.h*/ 56 | struct event xev; 57 | void *data, *data_end; /* end of the linear portion of the packet */ 58 | int l4_off; 59 | __u16 proto; 60 | __u8 nexthdr; 61 | 62 | if (!validate_ethertype(skb, &proto)) { /* if unknown traffic */ 63 | goto end; 64 | } 65 | 66 | switch (proto) { 67 | case bpf_htons(ETH_P_IP): { /* ETH_P_IP instead of ETH_P_ALL as we only want to listen for incoming IP packets, ingress. */ 68 | struct iphdr *ip4; /* use an ip hdr because we only want ip/check */ 69 | /* only eth packet:*/ 70 | if (!revalidate_data(skb, &data, &data_end, &ip4)) { /* we wrote all IP heaadaer (function from Cilium) make sure all data we need is in this structure, we are not accessing any other mem position, we are only accesing only packet info We do this for th eip header ,test we have not reached the end of the linear portion of the packet (data_end) */ 71 | goto end; 72 | } 73 | nexthdr = ip4->protocol; /* GET protocol, in each header field says which is the next protoco, in this case we want only thoe packet next is TCP */ 74 | if (nexthdr != IPPROTO_TCP) /* continue only if TCP */ 75 | goto end; 76 | if (ipv4_is_fragment(ip4)) /* If we have fragmented headers we may not have the port info is in other packet, IP heaer has flag metnion is fragmented, so if it is a fragment we don't do anytign, no handle that ,Check if IPv4 fragment matches fragment reassembly buffer. */ 77 | goto end; 78 | l4_off = ETH_HLEN + ipv4_hdrlen(ip4); /* packet size, offset = ETH_HLEN (Total octets in header) + ipv4 header length ?*/ 79 | 80 | __u16 dport; /* 16 bits, 2Bytes, different ways to write this, a-b-c .. or c-b-a, which order do we restore ip addresses...? no matter which machine reviwe netwrok orer, host order, machine oder */ 81 | ctx_load_bytes(skb, l4_off + TCP_DPORT_OFF, &dport, sizeof(dport)); /* convce verifier we are only looking at data we are allowe dto, an dthis how we do it, same with revalidate_data but nwo we onlly bring port.*/ 82 | if (bpf_ntohs(dport) != TCP_PORT) { /* want only those accessing defined port TCP_PORT (80) , this oncversta the host to use the host ordr: network to host sort, */ 83 | goto end; 84 | } 85 | xev.ip_addr = ip4->saddr; /* get the source ip from iphdr struct and set the value in the event struct */ 86 | break; 87 | } 88 | 89 | default: 90 | goto end; 91 | } 92 | 93 | /* helper function, push the even in the datasturcture, map, in the instance of the datastructure we are currently running */ 94 | skb_event_output(skb, &xevents, BPF_F_CURRENT_CPU, &xev, sizeof(xev)); 95 | end: 96 | return TC_ACT_PIPE; /* this bpf tc classifier programm returns directly an action, means: Iterate to the next action. */ 97 | } 98 | 99 | BPF_LICENSE("GPL"); /* some bpf helper functions are only available if you set the license of your bpf program to GPL. */ 100 | --------------------------------------------------------------------------------