├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── config.yaml ├── ebpf ├── Makefile ├── bpf_helpers.h └── rdns.c ├── go.mod ├── go.sum ├── main.go └── src ├── config.go ├── container_info.go ├── ebpf.go ├── egrets.go └── ip_tags.go /.gitignore: -------------------------------------------------------------------------------- 1 | trash 2 | egrets 3 | .*.sw[a-z] 4 | *.o 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fedora:26 2 | RUN dnf install -y llvm clang kernel-devel make binutils git 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_IMAGE=egrets_builder 2 | CFLAGS:= 3 | ifdef DEBUG 4 | CFLAGS=-DDEBUG 5 | endif 6 | 7 | all: egrets-main build-docker-image build-ebpf-object 8 | 9 | build-docker-image: 10 | sudo docker build -t $(DOCKER_IMAGE) -f Dockerfile . 11 | 12 | build-shell: 13 | sudo docker run -it --rm \ 14 | -v $(PWD)/ebpf:/dist/ \ 15 | --workdir=/dist/ \ 16 | $(DOCKER_IMAGE) \ 17 | bash -i 18 | 19 | build-ebpf-object: 20 | sudo docker run --rm \ 21 | -v $(PWD)/ebpf:/dist/ \ 22 | --workdir=/dist/ \ 23 | $(DOCKER_IMAGE) \ 24 | make CFLAGS=$(CFLAGS) 25 | 26 | egrets-main: 27 | go build 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egrets 2 | Egrets is a proof of concept tool that uses eBPF, raw sockets, and kprobes to monitor egress traffic. Raw sockets are used to sniff for DNS queries and responses which are used to build a mapping of IP addresses to hostnames. Kprobes are used to watch syscalls that make network connections. Traditionally, these syscalls would at the most only have IP address information, but because we also have a mapping of hostnames to IP addresses, we can get a better understanding of what hosts processes are connecting to. This is much more reliable than say, whitelisting massive lists of IP addresses or relying on PTR records (which no one uses anyway.) 3 | 4 | Egrets is compatible with Docker and will tag TCP connections with container metadata. 5 | 6 | There is currently support for printing out events only. A future version will include support for killing processes and taking coredumps (see [Meatball](https://github.com/ancat/meatball)) I'm still learning go and ebpf, so there's still a lot of basic improvements that could be made. There are also some concurrency/synchronization issues so if you leave this program running long enough, it'll eventually crash. Use this at your own risk! :~) 7 | 8 | ## Getting Started 9 | 10 | There are a few prerequisites. First, you need to be running a "relatively" recent kernel; this was tested on 4.15+ kernels. You will also need to ensure that `debugfs` is mounted; `debug -t debugfs none /sys/kernel/debug` if it's not. And finally, you (maybe unsurprisingly) will need root. 11 | 12 | ``` 13 | # grab a copy of this repo 14 | $ git clone https://github.com/ancat/egrets.git 15 | $ cd egrets 16 | 17 | # build 18 | $ apt-get install libpcap0.8-dev 19 | $ make 20 | 21 | # run 22 | sudo ./egrets 23 | ``` 24 | 25 | ## Examples 26 | 27 | In the examples below each type of event has its own tag. `dns.query` and `dns.answer` are DNS queries and responses respectively; `process.tcp_v4` are the network connections that tie together syscall + DNS information. 28 | 29 | Running `curl facebook.com`: 30 | ``` 31 | INFO[0001] process.tcp_v4 comm=curl pid=2074 connection=67.207.67.3:53 dns.entry= container.image=none container.hostname=none container.ip=none 32 | INFO[0001] dns.query hostname=facebook.com type=A 33 | INFO[0001] dns.answer hostname=facebook.com response=31.13.71.36 34 | INFO[0001] dns.query hostname=facebook.com type=AAAA 35 | INFO[0001] dns.answer hostname=facebook.com response=2a03:2880:f112:83:face:b00c:0:25de 36 | INFO[0001] process.tcp_v4 comm=curl pid=2074 connection=31.13.71.36:80 dns.entry=facebook.com container.image=none container.hostname=none container.ip=none 37 | INFO[0001] process.tcp_v4 comm=curl pid=2073 connection=31.13.71.36:80 dns.entry=facebook.com container.image=none container.hostname=none container.ip=none 38 | ``` 39 | 40 | Running `apt update` from within a container: 41 | ``` 42 | INFO[0001] process.tcp_v4 comm=http pid=2735 connection=67.207.67.3:53 dns.entry= container.image=gremlinweb container.hostname=0b1c6a890623 container.ip=172.17.0.2 43 | INFO[0001] dns.query hostname=security.debian.org type=A 44 | INFO[0001] dns.answer hostname=security.debian.org response=149.20.4.14 45 | INFO[0001] dns.answer hostname=security.debian.org response=128.61.240.73 46 | INFO[0001] dns.answer hostname=security.debian.org response=128.31.0.63 47 | INFO[0001] dns.answer hostname=security.debian.org response=128.101.240.215 48 | INFO[0001] process.tcp_v4 comm=http pid=2735 connection=149.20.4.14:80 dns.entry=security.debian.org container.image=gremlinweb container.hostname=0b1c6a890623 container.ip=172.17.0.2 49 | INFO[0001] process.tcp_v4 comm=http pid=2735 connection=128.61.240.73:80 dns.entry=security.debian.org container.image=gremlinweb container.hostname=0b1c6a890623 container.ip=172.17.0.2 50 | INFO[0001] process.tcp_v4 comm=http pid=2735 connection=128.31.0.63:80 dns.entry=security.debian.org container.image=gremlinweb container.hostname=0b1c6a890623 container.ip=172.17.0.2 51 | INFO[0001] process.tcp_v4 comm=http pid=2735 connection=128.101.240.215:80 dns.entry=security.debian.org container.image=gremlinweb container.hostname=0b1c6a890623 container.ip=172.17.0.2 52 | INFO[0001] process.tcp_v4 comm=http pid=2735 connection=149.20.4.14:80 dns.entry=security.debian.org container.image=gremlinweb container.hostname=0b1c6a890623 container.ip=172.17.0.2 53 | INFO[0001] dns.query hostname=deb.debian.org type=A 54 | INFO[0001] process.tcp_v4 comm=http pid=2736 connection=67.207.67.3:53 dns.entry= container.image=gremlinweb container.hostname=0b1c6a890623 container.ip=172.17.0.2 55 | INFO[0001] dns.answer hostname=deb.debian.org response=debian.map.fastly.net 56 | INFO[0001] dns.answer hostname=deb.debian.org response=199.232.38.133 57 | INFO[0001] process.tcp_v4 comm=http pid=2736 connection=199.232.38.133:80 dns.entry=deb.debian.org container.image=gremlinweb container.hostname=0b1c6a890623 container.ip=172.17.0.2 58 | ``` 59 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # if you don't use containers, save the lookup time 2 | container_metadata: true 3 | async_container_metadata: true 4 | log_blocked: false 5 | log_dns: false 6 | 7 | cache_http: true 8 | 9 | # classic bpf is borked 10 | use_classic_bpf: false 11 | 12 | # manually decode packets (experimental, unused) 13 | manual_packet_decode: true 14 | 15 | trusted_dns: 67.207.67.3 16 | # explicit blocks (threat intel maybe?) 17 | # not implemented yet 18 | block: 19 | # don't let containers talk to the host 20 | - 172.17.0.1 21 | allow: 22 | - deb.debian.org 23 | - repo.iovisor.org 24 | - apt.llvm.org 25 | - ppa.launchpad.net 26 | - mirrors.digitalocean.com 27 | - packagecloud.io 28 | - d28dx6y1hfq314.cloudfront.net 29 | - deb.nodesource.com 30 | - security.debian.org 31 | - security.ubuntu.com 32 | - download.docker.com 33 | - security-cdn.debian.org 34 | container_allow: 35 | gremlinweb: 36 | - security.debian.org 37 | - deb.debian.org 38 | - security-cdn.debian.org 39 | -------------------------------------------------------------------------------- /ebpf/Makefile: -------------------------------------------------------------------------------- 1 | LINUX_HEADERS=$(shell rpm -ql kernel-devel | head -1) 2 | CFLAGS=$(CFLAGS) 3 | 4 | all: 5 | clang $(CFLAGS) -D__KERNEL__ -D__ASM_SYSREG_H \ 6 | -Wno-unused-value \ 7 | -Wno-pointer-sign \ 8 | -Wno-compare-distinct-pointer-types \ 9 | -Wunused \ 10 | -Wall \ 11 | -Werror \ 12 | -I $(LINUX_HEADERS)/include/linux \ 13 | -I $(LINUX_HEADERS)/arch/x86/include \ 14 | -I $(LINUX_HEADERS)/arch/x86/include/generated \ 15 | -I $(LINUX_HEADERS)/include \ 16 | -I $(LINUX_HEADERS)/include/generated/uapi \ 17 | -I $(LINUX_HEADERS)/arch/x86/include/uapi \ 18 | -I $(LINUX_HEADERS)/include/uapi \ 19 | -O2 -emit-llvm -c /dist/rdns.c \ 20 | -o - | llc -march=bpf -filetype=obj -o "/dist/rdns.o" 21 | -------------------------------------------------------------------------------- /ebpf/bpf_helpers.h: -------------------------------------------------------------------------------- 1 | #ifndef __BPF_HELPERS_H 2 | #define __BPF_HELPERS_H 3 | 4 | /* helper macro to place programs, maps, license in 5 | * different sections in elf_bpf file. Section names 6 | * are interpreted by elf_bpf loader 7 | */ 8 | #define SEC(NAME) __attribute__((section(NAME), used)) 9 | 10 | /* helper functions called from eBPF programs written in C */ 11 | static void *(*bpf_map_lookup_elem)(void *map, void *key) = 12 | (void *) BPF_FUNC_map_lookup_elem; 13 | static int (*bpf_map_update_elem)(void *map, void *key, void *value, 14 | unsigned long long flags) = 15 | (void *) BPF_FUNC_map_update_elem; 16 | static int (*bpf_map_delete_elem)(void *map, void *key) = 17 | (void *) BPF_FUNC_map_delete_elem; 18 | static int (*bpf_probe_read)(void *dst, int size, void *unsafe_ptr) = 19 | (void *) BPF_FUNC_probe_read; 20 | static unsigned long long (*bpf_ktime_get_ns)(void) = 21 | (void *) BPF_FUNC_ktime_get_ns; 22 | static int (*bpf_trace_printk)(const char *fmt, int fmt_size, ...) = 23 | (void *) BPF_FUNC_trace_printk; 24 | static unsigned long long (*bpf_get_smp_processor_id)(void) = 25 | (void *) BPF_FUNC_get_smp_processor_id; 26 | static unsigned long long (*bpf_get_current_pid_tgid)(void) = 27 | (void *) BPF_FUNC_get_current_pid_tgid; 28 | static unsigned long long (*bpf_get_current_uid_gid)(void) = 29 | (void *) BPF_FUNC_get_current_uid_gid; 30 | static int (*bpf_get_current_comm)(void *buf, int buf_size) = 31 | (void *) BPF_FUNC_get_current_comm; 32 | static int (*bpf_perf_event_read)(void *map, int index) = 33 | (void *) BPF_FUNC_perf_event_read; 34 | static int (*bpf_clone_redirect)(void *ctx, int ifindex, int flags) = 35 | (void *) BPF_FUNC_clone_redirect; 36 | static int (*bpf_redirect)(int ifindex, int flags) = 37 | (void *) BPF_FUNC_redirect; 38 | static int (*bpf_perf_event_output)(void *ctx, void *map, 39 | unsigned long long flags, void *data, 40 | int size) = 41 | (void *) BPF_FUNC_perf_event_output; 42 | static int (*bpf_skb_get_tunnel_key)(void *ctx, void *key, int size, int flags) = 43 | (void *) BPF_FUNC_skb_get_tunnel_key; 44 | static int (*bpf_skb_set_tunnel_key)(void *ctx, void *key, int size, int flags) = 45 | (void *) BPF_FUNC_skb_set_tunnel_key; 46 | static unsigned long long (*bpf_get_prandom_u32)(void) = 47 | (void *) BPF_FUNC_get_prandom_u32; 48 | 49 | static unsigned long long (*bpf_get_current_task)(void) = 50 | (void *) BPF_FUNC_get_current_task; 51 | 52 | /* llvm builtin functions that eBPF C program may use to 53 | * emit BPF_LD_ABS and BPF_LD_IND instructions 54 | */ 55 | struct sk_buff; 56 | unsigned long long load_byte(void *skb, 57 | unsigned long long off) asm("llvm.bpf.load.byte"); 58 | unsigned long long load_half(void *skb, 59 | unsigned long long off) asm("llvm.bpf.load.half"); 60 | unsigned long long load_word(void *skb, 61 | unsigned long long off) asm("llvm.bpf.load.word"); 62 | 63 | /* a helper structure used by eBPF C program 64 | * to describe map attributes to elf_bpf loader 65 | */ 66 | #define BUF_SIZE_MAP_NS 256 67 | 68 | struct bpf_map_def { 69 | unsigned int type; 70 | unsigned int key_size; 71 | unsigned int value_size; 72 | unsigned int max_entries; 73 | unsigned int map_flags; 74 | unsigned int pinning; 75 | char namespace[BUF_SIZE_MAP_NS]; 76 | }; 77 | 78 | static int (*bpf_skb_store_bytes)(void *ctx, int off, void *from, int len, int flags) = 79 | (void *) BPF_FUNC_skb_store_bytes; 80 | static int (*bpf_l3_csum_replace)(void *ctx, int off, int from, int to, int flags) = 81 | (void *) BPF_FUNC_l3_csum_replace; 82 | static int (*bpf_l4_csum_replace)(void *ctx, int off, int from, int to, int flags) = 83 | (void *) BPF_FUNC_l4_csum_replace; 84 | 85 | #if defined(__x86_64__) 86 | 87 | #define PT_REGS_PARM1(x) ((x)->di) 88 | #define PT_REGS_PARM2(x) ((x)->si) 89 | #define PT_REGS_PARM3(x) ((x)->dx) 90 | #define PT_REGS_PARM4(x) ((x)->cx) 91 | #define PT_REGS_PARM5(x) ((x)->r8) 92 | #define PT_REGS_RET(x) ((x)->sp) 93 | #define PT_REGS_FP(x) ((x)->bp) 94 | #define PT_REGS_RC(x) ((x)->ax) 95 | #define PT_REGS_SP(x) ((x)->sp) 96 | #define PT_REGS_IP(x) ((x)->ip) 97 | 98 | #elif defined(__s390x__) 99 | 100 | #define PT_REGS_PARM1(x) ((x)->gprs[2]) 101 | #define PT_REGS_PARM2(x) ((x)->gprs[3]) 102 | #define PT_REGS_PARM3(x) ((x)->gprs[4]) 103 | #define PT_REGS_PARM4(x) ((x)->gprs[5]) 104 | #define PT_REGS_PARM5(x) ((x)->gprs[6]) 105 | #define PT_REGS_RET(x) ((x)->gprs[14]) 106 | #define PT_REGS_FP(x) ((x)->gprs[11]) /* Works only with CONFIG_FRAME_POINTER */ 107 | #define PT_REGS_RC(x) ((x)->gprs[2]) 108 | #define PT_REGS_SP(x) ((x)->gprs[15]) 109 | #define PT_REGS_IP(x) ((x)->ip) 110 | 111 | #elif defined(__aarch64__) 112 | 113 | #define PT_REGS_PARM1(x) ((x)->regs[0]) 114 | #define PT_REGS_PARM2(x) ((x)->regs[1]) 115 | #define PT_REGS_PARM3(x) ((x)->regs[2]) 116 | #define PT_REGS_PARM4(x) ((x)->regs[3]) 117 | #define PT_REGS_PARM5(x) ((x)->regs[4]) 118 | #define PT_REGS_RET(x) ((x)->regs[30]) 119 | #define PT_REGS_FP(x) ((x)->regs[29]) /* Works only with CONFIG_FRAME_POINTER */ 120 | #define PT_REGS_RC(x) ((x)->regs[0]) 121 | #define PT_REGS_SP(x) ((x)->sp) 122 | #define PT_REGS_IP(x) ((x)->pc) 123 | 124 | #elif defined(__powerpc__) 125 | 126 | #define PT_REGS_PARM1(x) ((x)->gpr[3]) 127 | #define PT_REGS_PARM2(x) ((x)->gpr[4]) 128 | #define PT_REGS_PARM3(x) ((x)->gpr[5]) 129 | #define PT_REGS_PARM4(x) ((x)->gpr[6]) 130 | #define PT_REGS_PARM5(x) ((x)->gpr[7]) 131 | #define PT_REGS_RC(x) ((x)->gpr[3]) 132 | #define PT_REGS_SP(x) ((x)->sp) 133 | #define PT_REGS_IP(x) ((x)->nip) 134 | 135 | #endif 136 | 137 | #ifdef __powerpc__ 138 | #define BPF_KPROBE_READ_RET_IP(ip, ctx) ({ (ip) = (ctx)->link; }) 139 | #define BPF_KRETPROBE_READ_RET_IP BPF_KPROBE_READ_RET_IP 140 | #else 141 | #define BPF_KPROBE_READ_RET_IP(ip, ctx) ({ \ 142 | bpf_probe_read(&(ip), sizeof(ip), (void *)PT_REGS_RET(ctx)); }) 143 | #define BPF_KRETPROBE_READ_RET_IP(ip, ctx) ({ \ 144 | bpf_probe_read(&(ip), sizeof(ip), \ 145 | (void *)(PT_REGS_FP(ctx) + sizeof(ip))); }) 146 | #endif 147 | 148 | #endif 149 | -------------------------------------------------------------------------------- /ebpf/rdns.c: -------------------------------------------------------------------------------- 1 | #define randomized_struct_fields_start struct { 2 | #define randomized_struct_fields_end }; 3 | #include 4 | 5 | #pragma clang diagnostic push 6 | #pragma clang diagnostic ignored "-Waddress-of-packed-member" 7 | #pragma clang diagnostic ignored "-Wgnu-variable-sized-type-not-at-end" 8 | #include 9 | #pragma clang diagnostic pop 10 | #include 11 | #include 12 | #include "bpf_helpers.h" 13 | 14 | #pragma clang diagnostic push 15 | #pragma clang diagnostic ignored "-Wtautological-compare" 16 | #pragma clang diagnostic ignored "-Wgnu-variable-sized-type-not-at-end" 17 | #include 18 | #pragma clang diagnostic pop 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | #define ETH_LEN 14 25 | 26 | // packet parsing state machine helpers 27 | #define cursor_advance(_cursor, _len) \ 28 | ({ void *_tmp = _cursor; _cursor += _len; _tmp; }) 29 | 30 | unsigned long long load_byte(void *skb, 31 | unsigned long long off) asm("llvm.bpf.load.byte"); 32 | unsigned long long load_half(void *skb, 33 | unsigned long long off) asm("llvm.bpf.load.half"); 34 | unsigned long long load_word(void *skb, 35 | unsigned long long off) asm("llvm.bpf.load.word"); 36 | unsigned long long load_dword(void *skb, 37 | unsigned long long off) asm("llvm.bpf.load.dword"); 38 | 39 | #define MASK(_n) ((_n) < 64 ? (1ull << (_n)) - 1 : ((uint64_t)-1LL)) 40 | #define MASK128(_n) ((_n) < 128 ? ((unsigned __int128)1 << (_n)) - 1 : ((unsigned __int128)-1)) 41 | uint64_t bpf_dext_pkt(void *pkt, uint64_t off, uint64_t bofs, uint64_t bsz) { 42 | if (bofs == 0 && bsz == 8) { 43 | return load_byte(pkt, off); 44 | } else if (bofs + bsz <= 8) { 45 | return load_byte(pkt, off) >> (8 - (bofs + bsz)) & MASK(bsz); 46 | } else if (bofs == 0 && bsz == 16) { 47 | return load_half(pkt, off); 48 | } else if (bofs + bsz <= 16) { 49 | return load_half(pkt, off) >> (16 - (bofs + bsz)) & MASK(bsz); 50 | } else if (bofs == 0 && bsz == 32) { 51 | return load_word(pkt, off); 52 | } else if (bofs + bsz <= 32) { 53 | return load_word(pkt, off) >> (32 - (bofs + bsz)) & MASK(bsz); 54 | } else if (bofs == 0 && bsz == 64) { 55 | return load_dword(pkt, off); 56 | } else if (bofs + bsz <= 64) { 57 | return load_dword(pkt, off) >> (64 - (bofs + bsz)) & MASK(bsz); 58 | } 59 | return 0; 60 | } 61 | struct udp_t { 62 | unsigned short sport; 63 | unsigned short dport; 64 | unsigned short length; 65 | unsigned short crc; 66 | } __attribute__((packed));; 67 | 68 | struct ethernet_t { 69 | unsigned long long dst:48; 70 | unsigned long long src:48; 71 | unsigned int type:16; 72 | } __attribute__((packed));; 73 | 74 | struct ip_t { 75 | unsigned char ver:4; // byte 0 76 | unsigned char hlen:4; 77 | unsigned char tos; 78 | unsigned short tlen; 79 | unsigned short identification; // byte 4 80 | unsigned short ffo_unused:1; 81 | unsigned short df:1; 82 | unsigned short mf:1; 83 | unsigned short foffset:13; 84 | unsigned char ttl; // byte 8 85 | unsigned char nextp; 86 | unsigned short hchecksum; 87 | unsigned int src; // byte 12 88 | unsigned int dst; // byte 16 89 | } __attribute__((packed));; 90 | 91 | // pulled from /sys/kernel/debug/tracing/events/sched/sched_process_fork/format 92 | struct sched_process_fork { 93 | unsigned short common_type; 94 | unsigned char common_flags; 95 | unsigned char common_preempt_count; 96 | uint32_t common_pid; 97 | char parent_comm[16]; 98 | uint32_t parent_pid; 99 | char child_comm[16]; 100 | uint32_t child_pid; 101 | }; 102 | 103 | # define printk(fmt, ...) \ 104 | ({ \ 105 | char ____fmt[] = fmt; \ 106 | bpf_trace_printk(____fmt, sizeof(____fmt), \ 107 | ##__VA_ARGS__); \ 108 | }) 109 | 110 | #ifndef memset 111 | # define memset(dest, chr, n) __builtin_memset((dest), (chr), (n)) 112 | #endif 113 | 114 | #ifndef memcpy 115 | # define memcpy(dest, src, n) __builtin_memcpy((dest), (src), (n)) 116 | #endif 117 | 118 | #ifndef memmove 119 | # define memmove(dest, src, n) __builtin_memmove((dest), (src), (n)) 120 | #endif 121 | 122 | /* 123 | netevent.port = ({ typeof(__be16) _val; __builtin_memset(&_val, 0, sizeof(_val)); bpf_probe_read(&_val, sizeof(_val), (u64)&poop->sin_port); _val; }); 124 | netevent.address = ({ typeof(__be32) _val; __builtin_memset(&_val, 0, sizeof(_val)); bpf_probe_read(&_val, sizeof(_val), (u64)&poop->sin_addr.s_addr); _val; }); 125 | bpf_get_current_comm(&netevent.comm, sizeof(netevent.comm)); 126 | bpf_perf_event_output(ctx, bpf_pseudo_fd(1, -1), CUR_CPU_IDENTIFIER, &netevent, sizeof(netevent)); 127 | */ 128 | 129 | struct bpf_map_def SEC("maps/events") tcp_v4 = { 130 | .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY, 131 | .key_size = sizeof(int), 132 | .value_size = sizeof(__u32), 133 | .max_entries = 1024, 134 | .pinning = 0, 135 | .namespace = "", 136 | }; 137 | 138 | struct bpf_map_def SEC("maps/exec_events") exec_events = { 139 | .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY, 140 | .key_size = sizeof(int), 141 | .value_size = sizeof(__u32), 142 | .max_entries = 1024, 143 | .pinning = 0, 144 | .namespace = "", 145 | }; 146 | 147 | struct netevent_t { 148 | uint64_t pid; 149 | uint64_t ts; 150 | char comm[TASK_COMM_LEN]; 151 | uint64_t fd; 152 | uint64_t uid; 153 | uint16_t port; 154 | uint32_t address; 155 | uint32_t inet_family; 156 | }; 157 | 158 | struct execevent_t { 159 | uint64_t pid; 160 | uint64_t type; 161 | }; 162 | 163 | SEC("tracepoint/sched/sched_process_fork") 164 | int tracepoint__sched_process_fork(struct sched_process_fork *ctx) { 165 | u32 cpu = bpf_get_smp_processor_id(); 166 | 167 | struct execevent_t event = {0}; 168 | event.pid = ctx->child_pid; 169 | event.type = 2; 170 | bpf_perf_event_output(ctx, &exec_events, cpu, &event, sizeof(event)); 171 | 172 | #ifdef DEBUG 173 | printk("tracepoint :~) %d %d\n", ctx->child_pid, ctx->parent_pid); 174 | #endif 175 | return 0; 176 | } 177 | 178 | SEC("kprobe/sys_execve") 179 | int kprobe__sys_execve(struct pt_regs *ctx) { 180 | u32 cpu = bpf_get_smp_processor_id(); 181 | 182 | struct execevent_t event = {0}; 183 | event.pid = bpf_get_current_pid_tgid(); 184 | event.type = 1; 185 | bpf_perf_event_output(ctx, &exec_events, cpu, &event, sizeof(event)); 186 | 187 | #ifdef DEBUG 188 | printk("execve! @ [%d]\n", event.pid); 189 | #endif 190 | return 0; 191 | } 192 | 193 | SEC("kprobe/sys_connect") 194 | int kprobe__sys_connect(struct pt_regs *ctx) { 195 | // this handles all types of sockets 196 | // https://github.com/ancat/meatball/blob/master/meatball.c#L47 197 | u32 cpu = bpf_get_smp_processor_id(); 198 | struct netevent_t event = {0}; 199 | event.ts = 69; 200 | bpf_get_current_comm(&event.comm, sizeof(event.comm)); 201 | 202 | //int kprobe__sys_connect(struct pt_regs *ctx, int sockfd, struct sockaddr* addr, int addrlen) { 203 | int sockfd = ctx->di; 204 | struct sockaddr* addr = (void*) ctx->si;//int addrlen = ctx->dx; 205 | struct sockaddr_in* poop = (struct sockaddr_in*) addr; 206 | 207 | sa_family_t family; 208 | bpf_probe_read(&family, sizeof(family), &poop->sin_family); 209 | if (family != AF_INET) { 210 | return 0; 211 | } 212 | 213 | uint16_t port; 214 | bpf_probe_read(&port, sizeof(port), &poop->sin_port); 215 | port = htons(port); 216 | 217 | uint32_t in_addr; 218 | bpf_probe_read(&in_addr, sizeof(in_addr), &poop->sin_addr); 219 | in_addr = __constant_ntohl(in_addr); 220 | 221 | event.fd = sockfd; 222 | event.port = port; 223 | event.pid = bpf_get_current_pid_tgid(); 224 | event.address = in_addr; 225 | event.inet_family = family; 226 | bpf_perf_event_output(ctx, &tcp_v4, cpu, &event, sizeof(event)); 227 | 228 | return 0; 229 | } 230 | 231 | SEC("socket/filter_udp") 232 | int filter_udp(struct __sk_buff *skb) { 233 | uint8_t *cursor = 0; 234 | struct ethernet_t *ethernet = cursor_advance(cursor, sizeof(*ethernet)); 235 | unsigned int p_type = bpf_dext_pkt(skb, (uint64_t)ethernet+12, 0, 16); 236 | 237 | if (p_type == 0x800) { 238 | struct ip_t *ip = cursor_advance(cursor, sizeof(*ip)); 239 | int proto = load_byte(skb, ETH_HLEN + offsetof(struct ip_t, nextp)); 240 | if (proto == IPPROTO_UDP) { 241 | struct udp_t *udp = cursor_advance(cursor, sizeof(*udp)); 242 | unsigned short dport = bpf_dext_pkt(skb, (uint64_t)udp + offsetof(struct udp_t, dport), 0, 16); 243 | unsigned short sport = bpf_dext_pkt(skb, (uint64_t)udp + offsetof(struct udp_t, sport), 0, 16); 244 | 245 | if (sport == 53) { 246 | return -1; 247 | } else if (dport == 53) { 248 | return -1; 249 | } 250 | } 251 | } 252 | 253 | return 0; 254 | } 255 | 256 | char _license[] SEC("license") = "GPL"; 257 | unsigned int _version SEC("version") = 0xFFFFFFFE; 258 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ancat/egrets 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/google/gopacket v1.1.17 7 | github.com/iovisor/gobpf v0.0.0-20190827113749-5e47cda1484e 8 | github.com/sirupsen/logrus v1.4.2 9 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 10 | gopkg.in/yaml.v2 v2.2.8 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY= 3 | github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= 4 | github.com/iovisor/gobpf v0.0.0-20190827113749-5e47cda1484e h1:yElUy1h1s76Ld2usPb26W2SQ1GydhfIIsxoHIVkREyQ= 5 | github.com/iovisor/gobpf v0.0.0-20190827113749-5e47cda1484e/go.mod h1:+5U5qu5UOu8YJ5oHVLvWKH7/Dr5QNHU7mZ2RfPEeXg8= 6 | github.com/iovisor/gobpf v0.0.0-20191219090757-e72091e3c5e6 h1:iFG10/KLpx/+XAWkOp7cCg4cHC4ZJ1NfYyhy+ti6GMA= 7 | github.com/iovisor/gobpf v0.0.0-20191219090757-e72091e3c5e6/go.mod h1:+5U5qu5UOu8YJ5oHVLvWKH7/Dr5QNHU7mZ2RfPEeXg8= 8 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 11 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 12 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 14 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 15 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 16 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 17 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 18 | golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 20 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 21 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 24 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | egrets "github.com/ancat/egrets/src" 5 | ) 6 | 7 | func main() { 8 | ebpf_binary := "ebpf/rdns.o" 9 | egrets.Main(ebpf_binary) 10 | } 11 | -------------------------------------------------------------------------------- /src/config.go: -------------------------------------------------------------------------------- 1 | package egrets 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | type EgretsStats struct { 11 | PacketsSeen int 12 | QueriesSeen int 13 | MissingDns int 14 | MissingContainer int 15 | } 16 | 17 | type EgretsConfig struct { 18 | Container_Metadata bool 19 | Async_Container_Metadata bool 20 | Use_Classic_BPF bool 21 | Log_Blocked bool 22 | Log_Dns bool 23 | Allow []string 24 | Trusted_Dns string 25 | Container_Allow map[string][]string 26 | Cache_Http bool 27 | Manual_Packet_Decode bool 28 | } 29 | 30 | func LoadEgretsConfig(filename string) *EgretsConfig { 31 | config := EgretsConfig{} 32 | config_bytes, err := ioutil.ReadFile(filename) 33 | 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | err = yaml.Unmarshal([]byte(config_bytes), &config) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | return &config 44 | } 45 | 46 | func DumpConfig(config *EgretsConfig) { 47 | if config.Container_Metadata { 48 | fmt.Printf("we will fetch container metadata\n") 49 | } else { 50 | fmt.Printf("we will not fetch container metadata\n") 51 | } 52 | 53 | if config.Log_Blocked { 54 | fmt.Printf("we will only log blocked requests\n") 55 | } 56 | 57 | for _, hostname := range config.Allow { 58 | fmt.Printf("allowed hosts: %s\n", hostname) 59 | } 60 | 61 | for key, hostnames := range config.Container_Allow { 62 | fmt.Printf("key=%s\n", key) 63 | for _, container_hostname := range hostnames { 64 | fmt.Printf("%s => %s\n", key, container_hostname) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/container_info.go: -------------------------------------------------------------------------------- 1 | package egrets 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "strconv" 13 | ) 14 | 15 | type ContainerInfo struct { 16 | Hostname string 17 | IpAddress string 18 | Image string 19 | } 20 | 21 | // FIXME: what if we pulled this in the kprobe, and not here? 22 | // it would mean less io/less chance to bork 23 | // https://github.com/ntop/libebpfflow/blob/322329d/ebpflow_code.ebpf#L77 24 | func GetContainerInfo(pid int) *ContainerInfo { 25 | cgroup_name, err := GetCpusetCgroup(pid) 26 | 27 | // this makes spotting failures more obvious without having to crash 28 | // or worse, assume that there's no container info 29 | if err != nil { 30 | cinfo := ContainerInfo{} 31 | cinfo.Hostname = "fail" 32 | cinfo.IpAddress = "fail" 33 | cinfo.Image = "fail" 34 | return &cinfo 35 | } 36 | 37 | if cgroup_name == nil || bytes.Equal(cgroup_name, []byte("/")) { 38 | return nil 39 | } 40 | 41 | if bytes.Contains(cgroup_name, []byte("/docker")) { 42 | container_id := bytes.TrimPrefix(cgroup_name, []byte("/docker/")) 43 | return GetDockerMetadata(container_id) 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func GetCpusetCgroup(pid int) ([]byte, error) { 50 | cgroup_path := fmt.Sprintf("/proc/%d/cgroup", pid) 51 | cgroup_data, err := ioutil.ReadFile(cgroup_path) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | lines := bytes.Split(cgroup_data, []byte("\n")) 57 | var cgroup_id int 58 | var cgroup_controller []byte 59 | var cgroup_name []byte 60 | for _, line := range lines { 61 | if len(line) < 2 { 62 | break 63 | } 64 | 65 | parts := bytes.Split(line, []byte(":")) 66 | cgroup_id, _ = strconv.Atoi(string(parts[0])) 67 | cgroup_controller = parts[1] 68 | cgroup_name = parts[2] 69 | if cgroup_id == 1 || bytes.Equal(cgroup_controller, []byte("cpuset")) { 70 | return cgroup_name, nil 71 | } 72 | } 73 | 74 | return nil, fmt.Errorf("could not pull cgroup name from /proc/$$/cgroup") 75 | } 76 | 77 | var httpc *http.Client 78 | func GetDockerMetadata(docker_id []byte) *ContainerInfo { 79 | unix_path := fmt.Sprintf("http://unix/v1.24/containers/%s/json", string(docker_id)) 80 | 81 | // FIXME: turn httpc into a parameter so we don't have to check globals 82 | if httpc == nil || config.Cache_Http == false { 83 | httpc = &http.Client{ 84 | Transport: &http.Transport{ 85 | DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { 86 | return net.Dial("unix", "/var/run/docker.sock") 87 | }, 88 | }, 89 | } 90 | } 91 | 92 | response, err := httpc.Get(unix_path) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | var infoblob map[string]interface{} 98 | ploop := &bytes.Buffer{} 99 | io.Copy(ploop, response.Body) 100 | if err := json.Unmarshal(ploop.Bytes(), &infoblob); err != nil { 101 | panic(err) 102 | } 103 | 104 | info := ContainerInfo{} 105 | info.Hostname = infoblob["Config"].(map[string]interface{})["Hostname"].(string) 106 | info.Image = infoblob["Config"].(map[string]interface{})["Image"].(string) 107 | info.IpAddress = infoblob["NetworkSettings"].(map[string]interface{})["IPAddress"].(string) 108 | 109 | return &info 110 | } 111 | -------------------------------------------------------------------------------- /src/ebpf.go: -------------------------------------------------------------------------------- 1 | package egrets 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "syscall" 7 | "unsafe" 8 | 9 | "github.com/google/gopacket/afpacket" 10 | "github.com/google/gopacket/layers" 11 | "github.com/google/gopacket/pcap" 12 | "golang.org/x/net/bpf" 13 | "github.com/iovisor/gobpf/elf" 14 | ) 15 | 16 | func LoadModuleElf(path string) (*elf.Module, error) { 17 | mod := elf.NewModule(path) 18 | if mod == nil { 19 | return nil, fmt.Errorf("failed to load elf at %s", path) 20 | } 21 | 22 | var secParams = map[string]elf.SectionParams{} 23 | 24 | if err := mod.Load(secParams); err != nil { 25 | return nil, err 26 | } 27 | 28 | return mod, nil 29 | } 30 | 31 | func GetSocketFilter(module *elf.Module, filter_name string) (int, *elf.SocketFilter, error) { 32 | socketFilter := module.SocketFilter(filter_name) 33 | if socketFilter == nil { 34 | return -1, nil, fmt.Errorf("failed to find socket filter %s", filter_name) 35 | } 36 | 37 | fd, err := syscall.Socket( 38 | syscall.AF_PACKET, 39 | syscall.SOCK_RAW|syscall.SOCK_CLOEXEC|syscall.SOCK_NONBLOCK, 40 | 0x300, 41 | ) 42 | 43 | if err != nil { 44 | return -1, nil, err 45 | } 46 | 47 | sockaddr := syscall.RawSockaddrLinklayer{ 48 | Family: syscall.AF_PACKET, 49 | Protocol: 0x0300, 50 | Ifindex: 2, 51 | Pkttype: syscall.PACKET_HOST, 52 | } 53 | 54 | _, _, e := syscall.Syscall( 55 | syscall.SYS_BIND, 56 | uintptr(fd), 57 | uintptr(unsafe.Pointer(&sockaddr)), 58 | unsafe.Sizeof(sockaddr)); 59 | 60 | if e > 0 { 61 | return -1, nil, errors.New("failed to bind") 62 | } 63 | 64 | if err := elf.AttachSocketFilter(socketFilter, fd); err != nil { 65 | return -1, nil, err 66 | } 67 | 68 | return fd, socketFilter, nil 69 | } 70 | 71 | func GetMap( 72 | module *elf.Module, 73 | map_name string, 74 | map_chan chan []byte, 75 | lost_chan chan uint64, 76 | ) error { 77 | event_table := module.Map(map_name) 78 | if event_table == nil { 79 | return fmt.Errorf("couldn't find map %s", map_name) 80 | } 81 | 82 | pm, _ := elf.InitPerfMap(module, map_name, map_chan, lost_chan) 83 | pm.PollStart() 84 | 85 | return nil 86 | } 87 | 88 | func GetPacketHandle(bpf_filter string) (*afpacket.TPacket, error) { 89 | var afpacketHandle *afpacket.TPacket 90 | var err error 91 | 92 | afpacketHandle, err = afpacket.NewTPacket( 93 | afpacket.OptFrameSize(65536), 94 | afpacket.OptBlockSize(8388608), 95 | afpacket.OptNumBlocks(1), 96 | afpacket.OptAddVLANHeader(false), 97 | afpacket.OptPollTimeout(-10000000), 98 | afpacket.SocketRaw, 99 | afpacket.TPacketVersion3) 100 | 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | pcapBPF, err := pcap.CompileBPFFilter(layers.LinkTypeEthernet, 65536, bpf_filter) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | bpfIns := []bpf.RawInstruction{} 111 | for _, ins := range pcapBPF { 112 | bpfIns2 := bpf.RawInstruction{ 113 | Op: ins.Code, 114 | Jt: ins.Jt, 115 | Jf: ins.Jf, 116 | K: ins.K, 117 | } 118 | bpfIns = append(bpfIns, bpfIns2) 119 | } 120 | 121 | if afpacketHandle.SetBPF(bpfIns); err != nil { 122 | return nil, err 123 | } 124 | 125 | return afpacketHandle, nil 126 | } 127 | -------------------------------------------------------------------------------- /src/egrets.go: -------------------------------------------------------------------------------- 1 | package egrets 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "flag" 7 | "fmt" 8 | "net" 9 | "os" 10 | "syscall" 11 | 12 | "github.com/google/gopacket" 13 | "github.com/google/gopacket/afpacket" 14 | "github.com/google/gopacket/layers" 15 | "github.com/iovisor/gobpf/elf" 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | type TextFormatter struct { 20 | PadLevelText bool 21 | ForceColors bool 22 | LoggerName string 23 | log.TextFormatter 24 | } 25 | 26 | type tcp_v4 struct { 27 | Pid uint64 28 | Ts uint64 29 | Comm [16]byte 30 | Fd uint64 31 | Uid uint64 32 | Port uint32 33 | Addr uint32 34 | Family uint64 35 | } 36 | 37 | type exec_event struct { 38 | Pid uint64 39 | Type uint64 40 | } 41 | 42 | type EventConnectV4 struct { 43 | Comm [16]byte 44 | Pid uint 45 | Hostname string 46 | Addr uint32 47 | Port uint32 48 | ContainerMetadata *ContainerInfo 49 | } 50 | 51 | func Long2ip(ipLong uint32) string { 52 | ipByte := make([]byte, 4) 53 | binary.BigEndian.PutUint32(ipByte, ipLong) 54 | ip := net.IP(ipByte) 55 | return ip.String() 56 | } 57 | 58 | var ip_to_hostname map[string]string 59 | // consider caching namespace -> container info 60 | var pid_to_namespace map[int]*ContainerInfo 61 | 62 | var dump_config = flag.Bool("d", false, "dump the parsed config and exit") 63 | var config_file = flag.String("f", "config.yaml", "config file (default `config.yaml`") 64 | var config *EgretsConfig 65 | var ipv4map = IpNode{} 66 | var event_channel chan string 67 | var stats = EgretsStats{} 68 | 69 | func Main(ebpf_binary string) { 70 | flag.Parse() 71 | config = LoadEgretsConfig(*config_file) 72 | if *dump_config { 73 | DumpConfig(config) 74 | return 75 | } 76 | 77 | event_channel = make(chan string) 78 | go func() { 79 | for { 80 | event_string := <-event_channel 81 | fmt.Printf("%s", event_string) 82 | } 83 | }() 84 | 85 | ip_to_hostname = make(map[string]string) 86 | pid_to_namespace = make(map[int]*ContainerInfo) 87 | 88 | mod, err := LoadModuleElf(ebpf_binary) 89 | if err != nil { 90 | log.Fatalf("Failed to load ebpf binary: %s", err) 91 | } 92 | 93 | fd, socketFilter, err := GetSocketFilter(mod, "socket/filter_udp") 94 | if err != nil { 95 | log.Fatalf("Failed loading UDP socket filter: %s", err) 96 | } 97 | 98 | defer elf.DetachSocketFilter(socketFilter, fd) 99 | defer syscall.Close(fd) 100 | 101 | err = syscall.SetNonblock(fd, false) 102 | if err != nil { 103 | log.Fatalf("failed to set AF_PACKET to blocking: %s", err) 104 | } 105 | 106 | dns_stream := os.NewFile(uintptr(fd), "hehe") 107 | if dns_stream == nil { 108 | log.Fatalf("couldn't turn fd into a file") 109 | } 110 | 111 | // we only need these if we want container metadata asynchronously 112 | if config.Container_Metadata && config.Async_Container_Metadata { 113 | if err := mod.EnableTracepoint("tracepoint/sched/sched_process_fork"); err != nil { 114 | log.Fatalf("Failed to enable tracepoint: %s\nMake sure you are running as root and that debugfs is mounted!", err) 115 | } 116 | 117 | if err := mod.EnableKprobe("kprobe/sys_execve", 1); err != nil { 118 | log.Fatalf("Failed to set up kprobes: %s\nMake sure you are running as root and that debugfs is mounted!", err) 119 | } 120 | 121 | // this is used by both the sched_process_fork tp and the execve kp 122 | exec_channel := make(chan []byte) 123 | exec_lost_chan := make(chan uint64) 124 | err = GetMap(mod, "exec_events", exec_channel, exec_lost_chan) 125 | if err != nil { 126 | log.Fatalf("Failed to load exec events map: %s", err) 127 | } 128 | 129 | go process_execs(exec_channel, exec_lost_chan) 130 | } 131 | 132 | if err := mod.EnableKprobe("kprobe/sys_connect", 1); err != nil { 133 | log.Fatalf("Failed to set up kprobes: %s\nMake sure you are running as root and that debugfs is mounted!", err) 134 | } 135 | 136 | channel := make(chan []byte) 137 | lost_chan := make(chan uint64) 138 | err = GetMap(mod, "events", channel, lost_chan) 139 | if err != nil { 140 | log.Fatalf("Failed to load events map: %s", err) 141 | } 142 | 143 | 144 | go process_tcp(channel, lost_chan) 145 | 146 | if config.Use_Classic_BPF { 147 | afpacketHandle, err := GetPacketHandle("udp and port 53") 148 | if err != nil { 149 | panic(err) 150 | } 151 | 152 | defer afpacketHandle.Close() 153 | process_alt_dns(afpacketHandle) 154 | } else { 155 | process_dns(dns_stream) 156 | } 157 | } 158 | 159 | func process_dns(dns_stream *os.File) { 160 | log.Printf("starting dns boye") 161 | // we should be using channels for this 162 | // https://www.openwall.com/lists/musl/2018/10/11/2 163 | for { 164 | byte_chunk := make([]byte, 2048) 165 | length, _ := dns_stream.Read(byte_chunk) 166 | if length == 0 { 167 | continue 168 | } 169 | 170 | go process_packet(byte_chunk, length) 171 | } 172 | } 173 | 174 | func process_alt_dns(afpacketHandle *afpacket.TPacket) { 175 | log.Printf("starting alt dns boye") 176 | 177 | for { 178 | data, _, err := afpacketHandle.ZeroCopyReadPacketData() 179 | if err != nil { 180 | panic(err) 181 | } 182 | 183 | go process_packet(data, len(data)) 184 | continue 185 | 186 | packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.NoCopy) 187 | dns_packet := packet.Layer(layers.LayerTypeDNS) 188 | if dns_packet == nil { 189 | continue 190 | } 191 | 192 | dns := dns_packet.(*layers.DNS) 193 | log_dns_event(dns) 194 | } 195 | } 196 | 197 | func process_execs(receiver chan []byte, lost chan uint64) { 198 | log.Printf("execves processing starting") 199 | 200 | for { 201 | select { 202 | case data, ok := <-receiver: 203 | if !ok { 204 | return 205 | } 206 | 207 | buffer := bytes.NewBuffer(data) 208 | var event exec_event 209 | err := binary.Read(buffer, binary.LittleEndian, &event) 210 | if err != nil { 211 | panic(err) 212 | } 213 | 214 | go func() { 215 | pid := int(event.Pid & 0xFFFFFFFF) 216 | var container_info *ContainerInfo 217 | container_info = GetContainerInfo(pid) 218 | // FIXME data race 219 | pid_to_namespace[pid] = container_info 220 | }() 221 | case _, ok := <-lost: 222 | if !ok { 223 | return 224 | } 225 | } 226 | } 227 | } 228 | 229 | func process_tcp(receiver chan []byte, lost chan uint64) { 230 | log.Printf("tcp processing starting") 231 | 232 | for { 233 | select { 234 | case data, ok := <-receiver: 235 | if !ok { 236 | return 237 | } 238 | 239 | buffer := bytes.NewBuffer(data) 240 | var event tcp_v4 241 | err := binary.Read(buffer, binary.LittleEndian, &event) 242 | if err != nil { 243 | panic(err) 244 | } 245 | 246 | stats.PacketsSeen++ 247 | go parse_tcp_event(event) 248 | case _, ok := <-lost: 249 | if !ok { 250 | return 251 | } 252 | } 253 | } 254 | } 255 | 256 | func parse_tcp_event(event tcp_v4) { 257 | // exclude stuff bound to 0.0.0.0 258 | // FIXME: remember that these include udp too 259 | if event.Addr == 0 { 260 | return 261 | } 262 | 263 | pid := int(event.Pid & 0xFFFFFFFF) 264 | var container_info *ContainerInfo 265 | container_hostname := "none" 266 | container_image := "none" 267 | container_ip := "none" 268 | 269 | if config.Container_Metadata { 270 | container_info = pid_to_namespace[pid] 271 | if container_info == nil && !config.Async_Container_Metadata { 272 | container_info = GetContainerInfo(pid) 273 | pid_to_namespace[pid] = container_info 274 | } 275 | } 276 | 277 | if container_info != nil { 278 | container_image = container_info.Image 279 | container_ip = container_info.IpAddress 280 | container_hostname = container_info.Hostname 281 | } 282 | 283 | if container_image == "fail" { 284 | event_channel <- fmt.Sprintf("container.missing comm=%s pid=%d\n", event.Comm, pid) 285 | stats.MissingContainer++ 286 | } 287 | 288 | ip_address := Long2ip(event.Addr) 289 | if ip_address == config.Trusted_Dns { 290 | return 291 | } 292 | 293 | // FIXME: data race in ip_to_hostname 294 | dns_name := ip_to_hostname[ip_address] 295 | 296 | ip_tags := ipv4map.get_tags(int(event.Addr)) 297 | dns_name_alt := "" 298 | if ip_tags != nil { 299 | dns_name_alt = ip_tags[0] 300 | } 301 | 302 | // FIXME: this checks only the hash table, which assumes ip:host is 1:1 303 | // the bst assumes ip:host is 1:many (more realistic) but I don't 304 | // feel like implementing something to check if two arrays overlap 305 | // it also doesn't check the container specific allow list 306 | 307 | // FIXME: we should consider caching the decision too 308 | // if an IP is allowed/blocked now, it will not change later 309 | // what if one dns entry for an ip is allowed, and another dns entry is not? 310 | allowed := tag_exists(config.Allow, dns_name) 311 | if dns_name == "" { 312 | // FIXME: oddly enough, this only happens when container metadata is enabled 313 | event_channel <- fmt.Sprintf("process.missing comm=%s pid=%d connection=%s:%d\n", event.Comm, pid, ip_address, event.Port) 314 | stats.MissingDns++ 315 | } 316 | 317 | if config.Log_Blocked { 318 | if !allowed { 319 | // syscall.Kill(pid, 9) 320 | event_channel <- fmt.Sprintf( 321 | "process.blocked comm=%s pid=%d connection=%s:%d dns.entry=%s container.image=%s container.hostname=%s container.ip=%s\n", 322 | event.Comm, 323 | pid, 324 | ip_address, 325 | event.Port, 326 | dns_name, 327 | container_image, 328 | container_hostname, 329 | container_ip, 330 | ) 331 | } 332 | } else { 333 | event_channel <- fmt.Sprintf( 334 | "process.tcp_v4 comm=%s pid=%d connection=%s:%d allowed=%t dns.entry=%s dns.entry2=%s container.image=%s container.hostname=%s container.ip=%s\n", 335 | event.Comm, 336 | pid, 337 | ip_address, 338 | event.Port, 339 | allowed, 340 | dns_name, 341 | dns_name_alt, 342 | container_image, 343 | container_hostname, 344 | container_ip, 345 | ) 346 | } 347 | } 348 | 349 | func extract_dns(chunk []byte, size int) gopacket.Packet { 350 | packet := gopacket.NewPacket( 351 | chunk[:size], 352 | layers.LayerTypeEthernet, 353 | gopacket.NoCopy, 354 | ) 355 | 356 | dns_packet := packet.Layer(layers.LayerTypeDNS) 357 | if dns_packet == nil { 358 | return nil 359 | } 360 | 361 | return packet 362 | } 363 | 364 | func log_dns_event(dns *layers.DNS) { 365 | var dns_event string 366 | if len(dns.Answers) > 0 { 367 | for _, answer := range dns.Answers { 368 | switch answer.Type { 369 | default: 370 | fmt.Printf("unknown dns type: %s\n", answer.Type.String()) 371 | case layers.DNSTypeAAAA: 372 | ip_to_hostname[string(answer.IP.String())] = string(dns.Questions[0].Name) 373 | dns_event = fmt.Sprintf("dns.answer hostname=%s response=%s\n", dns.Questions[0].Name, answer.IP) 374 | case layers.DNSTypeA: 375 | ipv4map.insert(ipv4toint(answer.IP), []string{string(dns.Questions[0].Name)}) 376 | ip_to_hostname[string(answer.IP.String())] = string(dns.Questions[0].Name) 377 | dns_event = fmt.Sprintf("dns.answer hostname=%s response=%s\n", dns.Questions[0].Name, answer.IP) 378 | case layers.DNSTypeCNAME: 379 | ip_to_hostname[string(answer.CNAME)] = string(dns.Questions[0].Name) 380 | dns_event = fmt.Sprintf("dns.answer hostname=%s response=%s\n", dns.Questions[0].Name, answer.CNAME) 381 | } 382 | 383 | if config.Log_Dns { 384 | event_channel <- dns_event 385 | } 386 | } 387 | } else if len(dns.Questions) > 0 { 388 | if config.Log_Dns { 389 | dns_event = fmt.Sprintf("dns.query hostname=%s type=%s\n", dns.Questions[0].Name, dns.Questions[0].Type.String()) 390 | event_channel <- dns_event 391 | } 392 | } else { 393 | fmt.Printf("we have no answers fuq\n") 394 | } 395 | 396 | if dns.QR && len(dns.Questions) > 0 && string(dns.Questions[0].Name) == "reset" { 397 | ip_to_hostname = make(map[string]string) 398 | stats.MissingDns = 0; stats.MissingContainer = 0; stats.PacketsSeen = 0; stats.QueriesSeen = 0; 399 | } else if dns.QR && len(dns.Questions) > 0 && string(dns.Questions[0].Name) == "dump" { 400 | fmt.Printf("Stats:\n%+v\n", stats) 401 | } 402 | } 403 | 404 | // experimental, nothing calls it yet 405 | func process_packet_manual(chunk[]byte, length int) { 406 | var err error 407 | ether := layers.Ethernet{} 408 | if err = ether.DecodeFromBytes(chunk, gopacket.NilDecodeFeedback); err != nil { 409 | log.Println(err) 410 | } 411 | if ether.EthernetType != layers.EthernetTypeIPv4 { 412 | return// no ipv6 yet 413 | } 414 | ipv44 := layers.IPv4{} 415 | if err = ipv44.DecodeFromBytes(ether.Payload, gopacket.NilDecodeFeedback); err != nil { 416 | panic("wat") 417 | } 418 | udpp := layers.UDP{} 419 | if err = udpp.DecodeFromBytes(ipv44.Payload, gopacket.NilDecodeFeedback); err != nil { 420 | panic("no udp") 421 | } 422 | dns_packet := layers.DNS{} 423 | err = dns_packet.DecodeFromBytes(udpp.Payload, gopacket.NilDecodeFeedback); 424 | if err == nil { 425 | log_dns_event(&dns_packet) 426 | } else { 427 | panic(err) 428 | } 429 | } 430 | 431 | func process_packet(chunk []byte, length int) { 432 | packet := gopacket.NewPacket(chunk[:length], layers.LayerTypeEthernet, gopacket.NoCopy) 433 | 434 | dns_packet := packet.Layer(layers.LayerTypeDNS) 435 | if dns_packet == nil { 436 | return 437 | } 438 | 439 | dns := dns_packet.(*layers.DNS) 440 | log_dns_event(dns) 441 | stats.QueriesSeen++ 442 | } 443 | -------------------------------------------------------------------------------- /src/ip_tags.go: -------------------------------------------------------------------------------- 1 | package egrets 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | /* 9 | Use a binary search tree to quickly find an IP address in the list of seen 10 | DNS responses. This is faster than the built in hash table for the first few 11 | thousand inserts but then becomes slower. The difference between the two is 12 | measured in the nanoseconds the whole time, so I'm going to stop pretending 13 | like I'm a computer scientist for now 14 | */ 15 | type IpNode struct { 16 | left *IpNode 17 | right *IpNode 18 | Data int 19 | Tags []string 20 | } 21 | 22 | func ipv4toint(ip_address net.IP) int { 23 | var a,b,c,d int 24 | fmt.Sscanf(ip_address.String(), "%d.%d.%d.%d", &a,&b,&c,&d) 25 | return (a<<24) | (b<<16) | (c<<8) | (d<<0) 26 | } 27 | 28 | // computer scientist roleplay time 29 | // use a trie 30 | func tag_exists(haystack []string, needle string) bool { 31 | for _, candidate := range haystack { 32 | if candidate == needle { 33 | return true 34 | } 35 | } 36 | 37 | return false 38 | } 39 | 40 | func (t* IpNode) insert(data int, tags []string) { 41 | if data == t.Data { 42 | if len(t.Tags) == 0 { 43 | t.Tags = make([]string, 0) 44 | t.Tags = append(t.Tags,tags...) 45 | } else if !tag_exists(t.Tags, tags[0]) { 46 | // `tags []string` should really be just a string 47 | t.Tags = append(t.Tags,tags...) 48 | } 49 | } else if data > t.Data { 50 | if t.right != nil { 51 | t.right.insert(data, tags) 52 | } else { 53 | t.right = &IpNode{} 54 | t.right.Data = data 55 | t.right.Tags = tags 56 | } 57 | } else if data < t.Data { 58 | if t.left != nil { 59 | t.left.insert(data, tags) 60 | } else { 61 | t.left = &IpNode{} 62 | t.left.Data = data 63 | t.left.Tags = tags 64 | } 65 | } 66 | } 67 | 68 | func (t* IpNode) dump() { 69 | left_int := 0 70 | right_int := 0 71 | 72 | if t.left != nil { 73 | left_int = t.left.Data 74 | } 75 | 76 | if t.right != nil { 77 | right_int = t.right.Data 78 | } 79 | 80 | if left_int == 0 && right_int == 0 { 81 | } else { 82 | fmt.Printf(" %d [%+v]\n", t.Data, t.Tags) 83 | fmt.Printf("%d <--^--> %d\n", left_int, right_int) 84 | } 85 | 86 | if t.left != nil { 87 | t.left.dump() 88 | } 89 | 90 | if t.right != nil { 91 | t.right.dump() 92 | } 93 | } 94 | 95 | func (t* IpNode) get_tags(data int) []string { 96 | var tags_left []string 97 | var tags_right []string 98 | 99 | if t.Data == data { 100 | return t.Tags 101 | } 102 | 103 | if t.left != nil { 104 | tags_left = t.left.get_tags(data) 105 | } 106 | 107 | if t.right != nil { 108 | tags_right = t.right.get_tags(data) 109 | } 110 | 111 | if tags_left != nil { 112 | return tags_left 113 | } else if tags_right != nil { 114 | return tags_right 115 | } 116 | 117 | return nil 118 | } 119 | --------------------------------------------------------------------------------