From 05a484ea5e094763af19a2153c4bf1f0da971bd2 Mon Sep 17 00:00:00 2001 From: Sahilb315 Date: Mon, 27 Jul 2026 13:51:09 +0530 Subject: [PATCH] implement decision logic --- ebpf-poc/bpf_bpfeb.go | 48 ++++++---- ebpf-poc/bpf_bpfeb.o | Bin 3736 -> 5600 bytes ebpf-poc/bpf_bpfel.go | 48 ++++++---- ebpf-poc/bpf_bpfel.o | Bin 3744 -> 5560 bytes ebpf-poc/connect.c | 93 ++++++++++++++++--- ebpf-poc/go.mod | 7 +- ebpf-poc/go.sum | 22 +++++ ebpf-poc/main.go | 202 +++++++++++++++++++++++++++++++++--------- 8 files changed, 335 insertions(+), 85 deletions(-) diff --git a/ebpf-poc/bpf_bpfeb.go b/ebpf-poc/bpf_bpfeb.go index 92442e0..d18708b 100644 --- a/ebpf-poc/bpf_bpfeb.go +++ b/ebpf-poc/bpf_bpfeb.go @@ -14,23 +14,33 @@ import ( ) type bpfEvent struct { - _ structs.HostLayout - Pid uint32 - Uid uint32 - Daddr uint32 - Dport uint16 - Proto uint8 - Comm [16]uint8 - _ [1]byte + _ structs.HostLayout + Pid uint32 + Uid uint32 + Daddr uint32 + Dport uint16 + Proto uint8 + Action uint8 + Comm [16]uint8 +} + +type bpfTarget struct { + _ structs.HostLayout + Ip uint32 + Port uint16 + _ [2]byte } // Names of all BPF objects in the ELF. // // Used for safe lookups in a Collection or CollectionSpec. const ( - bpfMapEvents = "events" - bpfProgConnect4 = "connect4" - bpfVarUnusedEvent = "unused_event" + bpfMapEvents = "events" + bpfMapExemptMap = "exempt_map" + bpfMapTargetMap = "target_map" + bpfProgConnect4 = "connect4" + bpfVarUnusedEvent = "unused_event" + bpfVarUnusedTarget = "unused_target" ) // loadBpf returns the embedded CollectionSpec for bpf. @@ -82,14 +92,17 @@ type bpfProgramSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type bpfMapSpecs struct { - Events *ebpf.MapSpec `ebpf:"events"` + Events *ebpf.MapSpec `ebpf:"events"` + ExemptMap *ebpf.MapSpec `ebpf:"exempt_map"` + TargetMap *ebpf.MapSpec `ebpf:"target_map"` } // bpfVariableSpecs contains global variables before they are loaded into the kernel. // // It can be passed ebpf.CollectionSpec.Assign. type bpfVariableSpecs struct { - UnusedEvent *ebpf.VariableSpec `ebpf:"unused_event"` + UnusedEvent *ebpf.VariableSpec `ebpf:"unused_event"` + UnusedTarget *ebpf.VariableSpec `ebpf:"unused_target"` } // bpfObjects contains all objects after they have been loaded into the kernel. @@ -112,12 +125,16 @@ func (o *bpfObjects) Close() error { // // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. type bpfMaps struct { - Events *ebpf.Map `ebpf:"events"` + Events *ebpf.Map `ebpf:"events"` + ExemptMap *ebpf.Map `ebpf:"exempt_map"` + TargetMap *ebpf.Map `ebpf:"target_map"` } func (m *bpfMaps) Close() error { return _BpfClose( m.Events, + m.ExemptMap, + m.TargetMap, ) } @@ -125,7 +142,8 @@ func (m *bpfMaps) Close() error { // // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. type bpfVariables struct { - UnusedEvent *ebpf.Variable `ebpf:"unused_event"` + UnusedEvent *ebpf.Variable `ebpf:"unused_event"` + UnusedTarget *ebpf.Variable `ebpf:"unused_target"` } // bpfPrograms contains all programs after they have been loaded into the kernel. diff --git a/ebpf-poc/bpf_bpfeb.o b/ebpf-poc/bpf_bpfeb.o index 8cc7507b9bfbed985f96cd0e59320220a77e3126..72c57f3f3ba6aa6507db4863c4e97be3bb3a299a 100644 GIT binary patch literal 5600 zcmb_gU2I%O6+U;fNn1*jO=#Ls{>G37+aw!1t&*TL*5=2h7KW@`mk?CxdcD537w@0; z-ZeH2szZw?6%s~*ssbcj;Gw{aUn(Pku;0pKc%qNttr8&R1sbGE=KJQJS>H?&`T!%H zo%5YJbN*)T%xGU69e+BP3#8Wqc@KO>&>I1BV9KJB{_d4vvtoJKShaodofPh?EXpCW zFRX3fWM5s|exH5i5jo8Mxq^HG-5%gP@gXC!O-5UIAZ65U?qES4eG#I~!Bl>qf&TXP z`-ciCcE9RB(0RL_6R{eK^P7*RnwPC~dmC|l80r3gL+w=G*uF&#&(Ez7@U#1%$S%&S z71DeVudSi_6@UJQp|+TfA=`__{nF-2iVv=ct$@n+EzMI%7&`O=6Zt)w_~XE|XAPq3$5;VM+}j2`Yptqzt`m96SxfRI@U+XlHU*w1Hfz%c zKcxH;_*38qKr5i^H$q*;ggupiN*#R`7%}C?6m!23U&djkr4YRtY>uD9I2-cKKMx(3 zvdtfI&c~5$o5AnLQ`Y7&4Sm?Q?TGfQ&(~Q1+iJ~3!tmV0#JMZsvtwTy4JWUZN5fE( z<#r^E%2F6LlTIy)C0Ya*L$~|H3F$WD+FUc5F`S-MI&)DXwYIcd9a`%SpWM-i=P6u> zmSwS0??$Q}HY#mup59Tc&R04TEk%uXVn?Ujvtit-E`*htnT~Yhs1wds8nyaz%GcUY zItJc2YQ3vUyVFWq)mB{^@mv^psv*^P_>&SZNHtlKYOC3ds>zd5o$Iu^?c+UMbZoII zNYc20{y6D$tBJWSIx`4c1Lg%W#@5GlYcuAo)j?&#YPZut@j^($x!O$sz?oEW_@tR( z(5l)@TJv!~EFBx^+9cAZ$aCr)st zRZmwHzdgwe_a@@lSu~V;XH*y0maZqYi|FRK743~ys!6TY#Bj=_a;{W%BUM`sF4eIS zGQC6<57<b$ad)~=OZp>Mh)$GrJ54iuM(kG?hV@!CYQ|Cel?y5$NKU-3KG0?;xWm5bsw|4(C>mXDS=cwt#oF zJG(vu9_O5u^-%#l&eq^@33z4OG2%-Sv~lF|y>7>sBuMyWYW_XE#oF!gcfTYLYKV2F6l!|RHj|E(0~4omP9^{*?wAi*=t^IgT05{%+#M#b`Pp&U3lO6n|E&Zs<1Pp^{@mXsc!lFrI{uyn-^aBRXg<4+ zNU(}}1e#Bdit7^mjQVe8Fvr&wzY0}e zQNL6C3kiOOyEph#2J_kQj^e+-6!A^P|CHbl?mk2r?`#;n+xQQlsdCn?*N|sV4j;Yt z1|9&O9eY)u!{*wfT0`0!^y3+MAtOJYk*nC=-x1`<>$Zk_S{{a+`z71%2k6VliP>Dg z0-m9u6S3JDx;WZDhdw9#k*FAj0!#8iowrrVr`E6$74~&P0JY4c{*~3#F zZhLse!>b-%^YFTdw>*5^!#6y9^LA|aeJ1~0!5cRo9`bO>;k^g;7xqapiIx&6cA|Rm z{N&R`g8y={=qogHzwA6%vQNumy}sD6FGoC7@z8X)P!Vs4q%tkVc)4MF^(*7&&ku#4 zPrFk#9REd#u>a@Qf(7p!aW?nArQC0vM0>m8GQ7j?+;Qq9$9TQ_?kN5V=mX~4_05`a z=L%)7fp}K1mF#cthu5xcTyLITE>Aa~`(zH}{JU|8*m5QOuyJm-Iep5`fE@Pcmvxfs z752_W;*%g3k3WBn3TTWo+nhdSFM*u@tPc3xS}J&eXJxi!>(9Ua?p41boiv4>HE@1i z&$#$M!9Dyx_$J2Ca(@0<|BUTk{#WgE$r<$Y&pUuE>;H0Vd)(G-#SUeE_OC1Q@3CyW Z{HyPd4VKP(=vLbE?PGIvaXbHq{sYL8e?kBN literal 3736 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 akyD5;PhmTj%9<(kjj=M0xorI%#{M51HK9}h diff --git a/ebpf-poc/connect.c b/ebpf-poc/connect.c index 1a4f878..b32256f 100644 --- a/ebpf-poc/connect.c +++ b/ebpf-poc/connect.c @@ -5,35 +5,108 @@ char __license[] SEC("license") = "Dual MIT/GPL"; +#define IPPROTO_TCP 6 + +// Only HTTPS is a candidate for redirect. Every other destination port is left +// alone, otherwise ssh, dns, postgres and everything else on the host would be +// steered into a proxy that cannot speak their protocol. +#define REDIRECT_DPORT 443 + +// What the hook decided. Recorded on every event so the ladder can be checked +// against real traffic before any rewrite is switched on. +#define ACTION_REDIRECT 0 +#define ACTION_SKIP_PROTO 1 +#define ACTION_SKIP_LOOPBACK 2 +#define ACTION_SKIP_EXEMPT 3 +#define ACTION_SKIP_DPORT 4 +#define ACTION_SKIP_NOTARGET 5 + struct event { __u32 pid; __u32 uid; __u32 daddr; __u16 dport; __u8 proto; + __u8 action; __u8 comm[16]; // command }; struct event *unused_event __attribute__((unused)); +// target is the redirect destination, written from userspace once the proxy is +// listening. Keeping it in a map means a proxy restart on a different port is +// one map update rather than a reload of the program. +struct target { + __u32 ip; // network byte order, same layout as ctx->user_ip4 + __u16 port; // host byte order, converted when written back to ctx +}; + +struct target *unused_target __attribute__((unused)); + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, struct target); +} target_map SEC(".maps"); + +// Uids whose traffic is never redirected. The proxy's own upstream fetches live +// here. Without this the rewrite sends the proxy back into itself forever. +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 8); + __type(key, __u32); + __type(value, __u8); +} exempt_map SEC(".maps"); + struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 1 << 24); } events SEC(".maps"); +static __always_inline __u8 decide(struct bpf_sock_addr *ctx, __u32 uid, __u16 dport) { + if (ctx->protocol != IPPROTO_TCP) + return ACTION_SKIP_PROTO; + + // Loopback is never registry traffic, and this also covers clients already + // configured to reach the proxy directly. + if ((bpf_ntohl(ctx->user_ip4) >> 24) == 127) + return ACTION_SKIP_LOOPBACK; + + if (bpf_map_lookup_elem(&exempt_map, &uid)) + return ACTION_SKIP_EXEMPT; + + if (dport != REDIRECT_DPORT) + return ACTION_SKIP_DPORT; + + __u32 key = 0; + struct target *t = bpf_map_lookup_elem(&target_map, &key); + if (!t || t->port == 0) + return ACTION_SKIP_NOTARGET; + + return ACTION_REDIRECT; +} + SEC("cgroup/connect4") int connect4(struct bpf_sock_addr *ctx) { + __u32 uid = bpf_get_current_uid_gid(); + __u16 dport = bpf_ntohs(ctx->user_port); + __u8 action = decide(ctx, uid, dport); + + // A full ring buffer costs an event, never a change in behaviour. struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0); - if (!e) - return 1; + if (e) { + e->pid = bpf_get_current_pid_tgid() >> 32; + e->uid = uid; + e->daddr = ctx->user_ip4; + e->dport = dport; + e->proto = ctx->protocol; + e->action = action; + bpf_get_current_comm(&e->comm, sizeof(e->comm)); + bpf_ringbuf_submit(e, 0); + } - e->pid = bpf_get_current_pid_tgid() >> 32; - e->uid = bpf_get_current_uid_gid(); - e->daddr = ctx->user_ip4; - e->dport = bpf_ntohs(ctx->user_port); - e-> proto = ctx->protocol; - bpf_get_current_comm(&e->comm, sizeof(e->comm)); - - bpf_ringbuf_submit(e, 0); + // Shadow mode. The decision is recorded and never acted on. Turning + // ACTION_REDIRECT into a rewrite of user_ip4 and user_port is the next step. return 1; } diff --git a/ebpf-poc/go.mod b/ebpf-poc/go.mod index 11a65f5..5f87e06 100644 --- a/ebpf-poc/go.mod +++ b/ebpf-poc/go.mod @@ -2,7 +2,6 @@ module ebpf-poc go 1.25.0 -require ( - github.com/cilium/ebpf v0.22.0 // indirect - golang.org/x/sys v0.43.0 // indirect -) +require github.com/cilium/ebpf v0.22.0 + +require golang.org/x/sys v0.43.0 // indirect diff --git a/ebpf-poc/go.sum b/ebpf-poc/go.sum index 02d7cd8..bc23b6c 100644 --- a/ebpf-poc/go.sum +++ b/ebpf-poc/go.sum @@ -1,4 +1,26 @@ github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY= github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= +github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= +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/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/ebpf-poc/main.go b/ebpf-poc/main.go index 922edf2..a661ae4 100644 --- a/ebpf-poc/main.go +++ b/ebpf-poc/main.go @@ -1,4 +1,4 @@ -//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type event bpf connect.c -- -I/usr/include/aarch64-linux-gnu +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type event -type target bpf connect.c -- -I/usr/include/aarch64-linux-gnu package main @@ -7,10 +7,13 @@ import ( "context" "encoding/binary" "errors" + "flag" "fmt" "net" "os" "os/signal" + "strconv" + "strings" "syscall" "github.com/cilium/ebpf" @@ -19,46 +22,65 @@ import ( "github.com/cilium/ebpf/rlimit" ) +// Mirrors the ACTION_* constants in connect.c. +var actionNames = map[uint8]string{ + 0: "REDIRECT", + 1: "skip/proto", + 2: "skip/loopback", + 3: "skip/exempt", + 4: "skip/dport", + 5: "skip/no-target", +} + func main() { - err := rlimit.RemoveMemlock() - if err != nil { - fmt.Printf("Failed to remove mem lock: %v\n", err) - return + proxyFlag := flag.String("proxy", "", "redirect target as host:port, e.g. 127.0.0.1:8443") + exemptFlag := flag.String("exempt-uid", "", "comma separated uids that are never redirected") + tcpOnlyFlag := flag.Bool("tcp-only", false, "only print TCP events, hiding DNS and route probe noise") + flag.Parse() + + if err := run(*proxyFlag, *exemptFlag, *tcpOnlyFlag); err != nil { + fmt.Fprintf(os.Stderr, "pmgwatch: %v\n", err) + os.Exit(1) + } +} + +func run(proxyAddr, exemptUIDs string, tcpOnly bool) error { + if err := rlimit.RemoveMemlock(); err != nil { + return fmt.Errorf("remove memlock: %w", err) } var objs bpfObjects - err = loadBpfObjects(&objs, nil) - if err != nil { - fmt.Printf("Failed to load bpf objects: %v\n", err) - return + if err := loadBpfObjects(&objs, nil); err != nil { + return fmt.Errorf("load bpf objects: %w", err) } - defer objs.Close() - rootCgroup := "/sys/fs/cgroup" + if err := configureTarget(&objs, proxyAddr); err != nil { + return err + } + + if err := configureExempt(&objs, exemptUIDs); err != nil { + return err + } l, err := link.AttachCgroup(link.CgroupOptions{ - Path: rootCgroup, + Path: "/sys/fs/cgroup", Attach: ebpf.AttachCGroupInet4Connect, Program: objs.Connect4, }) if err != nil { - fmt.Printf("Failed to attach cgroup: %v\n", err) - return + return fmt.Errorf("attach cgroup: %w", err) } defer l.Close() rd, err := ringbuf.NewReader(objs.Events) if err != nil { - fmt.Printf("Failed to create ringbuf reader: %v\n", err) - return + return fmt.Errorf("open ringbuf: %w", err) } defer rd.Close() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - defer stop() // Cleans up resources allocated by the signal package - - fmt.Println("Application started. Press Ctrl+C to exit.") + defer stop() go func() { <-ctx.Done() @@ -66,41 +88,139 @@ func main() { rd.Close() }() + fmt.Println("Shadow mode: decisions are recorded, nothing is redirected. Ctrl+C to exit.") + fmt.Printf("%-15s %-17s %-7s %-8s %-22s %s\n", "ACTION", "COMMAND", "UID", "PID", "DESTINATION", "PROTO") + + return drain(rd, tcpOnly) +} + +func drain(rd *ringbuf.Reader, tcpOnly bool) error { var e bpfEvent + for { rec, err := rd.Read() if err != nil { if errors.Is(err, ringbuf.ErrClosed) { - return + return nil } - fmt.Printf("failed to read raw event: %v\n", err) + + fmt.Fprintf(os.Stderr, "read event: %v\n", err) continue } - err = binary.Read(bytes.NewReader(rec.RawSample), binary.LittleEndian, &e) - if err != nil { - fmt.Printf("failed to read event: %v\n", err) + if err := binary.Read(bytes.NewReader(rec.RawSample), binary.LittleEndian, &e); err != nil { + fmt.Fprintf(os.Stderr, "decode event: %v\n", err) continue } - ip := make(net.IP, 4) - binary.LittleEndian.PutUint32(ip, e.Daddr) - - name := e.Comm[:] - if i := bytes.IndexByte(name, 0); i != -1 { - name = name[:i] // keep bytes before the first NUL - } - comm := string(name) - - proto := "" - switch e.Proto { - case syscall.IPPROTO_TCP: - proto = "TCP" - case syscall.IPPROTO_UDP: - proto = "UDP" + if tcpOnly && e.Proto != syscall.IPPROTO_TCP { + continue } - fmt.Println() - fmt.Printf("PID: %v\nUID: %v\ndAddr: %v\ndPort: %v\nProto: %v\nCommand: %v\n", e.Pid, e.Uid, ip, e.Dport, proto, comm) + printEvent(&e) } } + +func printEvent(e *bpfEvent) { + ip := make(net.IP, 4) + binary.LittleEndian.PutUint32(ip, e.Daddr) + + action, ok := actionNames[e.Action] + if !ok { + action = fmt.Sprintf("action/%d", e.Action) + } + + fmt.Printf("%-15s %-17s %-7d %-8d %-22s %s\n", + action, comm(e.Comm[:]), e.Uid, e.Pid, + net.JoinHostPort(ip.String(), strconv.Itoa(int(e.Dport))), proto(e.Proto)) +} + +// comm is a fixed 16 byte buffer, NUL terminated when the name is shorter. +func comm(raw []byte) string { + if i := bytes.IndexByte(raw, 0); i != -1 { + raw = raw[:i] + } + + return string(raw) +} + +func proto(p uint8) string { + switch p { + case syscall.IPPROTO_TCP: + return "TCP" + case syscall.IPPROTO_UDP: + return "UDP" + default: + return strconv.Itoa(int(p)) + } +} + +// configureTarget writes the redirect destination the hook will use. Leaving it +// unset is valid: every otherwise eligible connection then reports +// skip/no-target, which is how shadow mode runs before a proxy exists. +func configureTarget(objs *bpfObjects, addr string) error { + if addr == "" { + fmt.Println("No redirect target set, eligible connections will report skip/no-target") + return nil + } + + host, portStr, err := net.SplitHostPort(addr) + if err != nil { + return fmt.Errorf("parse proxy address %q: %w", addr, err) + } + + ip := net.ParseIP(host) + if ip == nil || ip.To4() == nil { + return fmt.Errorf("proxy address %q must be an IPv4 address", addr) + } + + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return fmt.Errorf("parse proxy port %q: %w", portStr, err) + } + + // The kernel holds the address in network byte order, which is what reading + // the four octets little endian reproduces. + target := bpfTarget{ + Ip: binary.LittleEndian.Uint32(ip.To4()), + Port: uint16(port), + } + + if err := objs.TargetMap.Put(uint32(0), target); err != nil { + return fmt.Errorf("write redirect target: %w", err) + } + + fmt.Printf("Redirect target: %s\n", addr) + + return nil +} + +func configureExempt(objs *bpfObjects, list string) error { + if list == "" { + fmt.Println("No exempt uids set, nothing bypasses the ladder") + return nil + } + + var applied []string + for _, field := range strings.Split(list, ",") { + field = strings.TrimSpace(field) + if field == "" { + continue + } + + uid, err := strconv.ParseUint(field, 10, 32) + if err != nil { + return fmt.Errorf("parse exempt uid %q: %w", field, err) + } + + if err := objs.ExemptMap.Put(uint32(uid), uint8(1)); err != nil { + return fmt.Errorf("write exempt uid %d: %w", uid, err) + } + + applied = append(applied, field) + } + + fmt.Printf("Exempt uids: %s\n", strings.Join(applied, ", ")) + + return nil +}