diff --git a/Dockerfile b/Dockerfile index e94b510..e6cef85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM golang:1.20 as build -RUN apt update -y; apt install -y build-essential clang libbpf-dev bpftool - +RUN apt update -y; apt install -y build-essential clang libbpf-dev bpftool linux-headers-generic gcc-multilib +RUN ln -s /usr/include/x86_64-linux-gnu/asm /usr/include/asm WORKDIR /build ADD . . RUN make build @@ -11,7 +11,6 @@ FROM debian:stable #RUN apt update -y; apt install -y apache2 RUN apt update -y; apt install -y inotify-tools - WORKDIR /dropit COPY scripts/entrypoint.sh /dropit/entrypoint.sh COPY --from=build /build/bpf/vmlinux.h /dropit/vmlinux.h diff --git a/Makefile b/Makefile index dd42615..aa9ac4b 100644 --- a/Makefile +++ b/Makefile @@ -30,4 +30,4 @@ build: generate generate: bpftool btf dump file /sys/kernel/btf/vmlinux format c > $(BPF_DIR)/vmlinux.h - clang -g -O2 -c -target bpf -o $(BPF_DIR)/daemon.o $(BPF_DIR)/daemon.c + clang -g -O2 -I/usr/include/linux -c -target bpf -o $(BPF_DIR)/daemon.o $(BPF_DIR)/daemon.c diff --git a/bpf/daemon.c b/bpf/daemon.c index 322c49a..3bab20b 100644 --- a/bpf/daemon.c +++ b/bpf/daemon.c @@ -1,12 +1,66 @@ //+build ignore - -#include "vmlinux.h" +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +/* #include */ + +typedef __kernel_size_t size_t; + +typedef __kernel_ssize_t ssize_t; + +typedef __u8 u8; + +typedef __s16 s16; + +typedef __u16 u16; + +typedef __s32 s32; + +typedef __u32 u32; + +typedef __s64 s64; + +typedef __u64 u64; +typedef _Bool bool; #define MATCH_ALL 0 -extern int LINUX_KERNEL_VERSION __kconfig; +// These macros represent the direction of traffic in a packet and in a filter rule(to check when to evaluate it). +// Direction will be represented by u8 +#define Ingress 0 +#define Egress 1 + + +// These offsets are defined as per the __sk_buf struct of a particular version. Might break?? +#define OFFSET_PROTOCOL 4 +#define OFFSET_REMOTE_IP 19 +#define OFFSET_LOCAL_IP 20 +#define OFFSET_REMOTE_PORT 23 +#define OFFSET_LOCAL_PORT 24 + + + +extern int LINUX_KERNEL_VERSION __kconfig; +/* copy of 'struct ethhdr' without __packed */ +struct eth_hdr { + unsigned char h_dest[ETH_ALEN]; + unsigned char h_source[ETH_ALEN]; + unsigned short h_proto; +}; struct packet { u32 source_ip; u32 dest_ip; @@ -15,6 +69,7 @@ struct packet { u16 dest_port; u8 protocol; bool is_dropped; + u8 direction; }; struct filter_rule { @@ -22,8 +77,40 @@ struct filter_rule { u16 source_port; u16 dest_port; u8 protocol; + u8 direction; }; +enum { + IPPROTO_IP = 0, + IPPROTO_ICMP = 1, + IPPROTO_IGMP = 2, + IPPROTO_IPIP = 4, + IPPROTO_TCP = 6, + IPPROTO_EGP = 8, + IPPROTO_PUP = 12, + IPPROTO_UDP = 17, + IPPROTO_IDP = 22, + IPPROTO_TP = 29, + IPPROTO_DCCP = 33, + IPPROTO_IPV6 = 41, + IPPROTO_RSVP = 46, + IPPROTO_GRE = 47, + IPPROTO_ESP = 50, + IPPROTO_AH = 51, + IPPROTO_MTP = 92, + IPPROTO_BEETPH = 94, + IPPROTO_ENCAP = 98, + IPPROTO_PIM = 103, + IPPROTO_COMP = 108, + IPPROTO_L2TP = 115, + IPPROTO_SCTP = 132, + IPPROTO_UDPLITE = 136, + IPPROTO_MPLS = 137, + IPPROTO_ETHERNET = 143, + IPPROTO_RAW = 255, + IPPROTO_MPTCP = 262, + IPPROTO_MAX = 263, +}; struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 1 << 24); // 16777216 or 2^24 entries @@ -38,10 +125,12 @@ struct { __uint(max_entries, NO_OF_RULES); // 256 or 2^8 filter rules } filter_rules SEC(".maps"); +// The same lookup_ctx will be used for both XDP and tc related filtering. Different fields will be set depending on filter_rule->direction. struct lookup_ctx { struct packet *pk; struct filter_rule *fr; - int output; + int xdp_output; + int tc_output; }; static void push_log(struct packet *pk) { @@ -58,6 +147,7 @@ static void push_log(struct packet *pk) { packet->protocol = pk->protocol; packet->size = pk->size; packet->is_dropped = pk->is_dropped; + packet->direction = pk->direction; bpf_ringbuf_submit(packet, 0); } @@ -72,19 +162,92 @@ static u64 filter_packet(struct bpf_map *map, u32 *key, pk->source_ip, pk->source_port, pk->dest_port, pk->protocol); */ - if( (value->source_ip == MATCH_ALL || value->source_ip == pk->source_ip) && // match source IP - (value->source_port == MATCH_ALL || value->source_port == pk->source_port) && //match source port - (value->dest_port == MATCH_ALL || value->dest_port == pk->dest_port) && // match destination port - (value->protocol == MATCH_ALL || value->protocol == pk->protocol)){ // match protocol - ctx->output = XDP_DROP; + if((value->direction == Ingress) && // Use XDP filtering on Ingress + (value->source_ip == MATCH_ALL || value->source_ip == pk->source_ip) && // match source IP + (value->source_port == MATCH_ALL || value->source_port == pk->source_port) && //match source port + (value->dest_port == MATCH_ALL || value->dest_port == pk->dest_port) && // match destination port + (value->protocol == MATCH_ALL || value->protocol == pk->protocol)){ // match protocol + ctx->xdp_output = XDP_DROP; ctx->fr = value; return 1; - } + } + + if((value->direction == Egress) && // Use tc filtering on Egress + (value->source_ip == MATCH_ALL || value->source_ip == pk->source_ip) && // match source IP + (value->source_port == MATCH_ALL || value->source_port == pk->source_port) && //match source port + (value->dest_port == MATCH_ALL || value->dest_port == pk->dest_port) && // match destination port + (value->protocol == MATCH_ALL || value->protocol == pk->protocol)){ // match protocol + ctx->tc_output = TC_ACT_SHOT; + ctx->fr = value; + return 1; + } return 0; } + +SEC("tc") +int intercept_packets_tc_egress(struct __sk_buff *ctx){ + //Parse skbuff to create the packet struct and pass that to filter_packets + struct packet pk; + pk.direction = Egress; + // We mark the start and end of our ethernet frame + void *ethernet_start = (void *)(long)ctx->data; + void *ethernet_end = (void *)(long)ctx->data_end; + + struct ethhdr *ethernet_frame = ethernet_start; + // Check if we have the entire ethernet frame + if ((void *)ethernet_frame + sizeof(*ethernet_frame) <= ethernet_end) { + struct iphdr *ip_packet = ethernet_start + sizeof(*ethernet_frame); + + // Check if the IP packet is within the bounds of ethernet frame + if ((void *)ip_packet + sizeof(*ip_packet) <= ethernet_end) { + // extract info from the IP packet + struct packet pk; + pk.source_ip = ip_packet->saddr; + pk.dest_ip = ip_packet->daddr; + pk.protocol = ip_packet->protocol; + pk.size = (ethernet_end - ethernet_start); + pk.dest_port = pk.source_port = 0; + pk.is_dropped = 0; + } + // check the protocol and get port + if (pk.protocol == IPPROTO_TCP) { + struct tcphdr *tcp = (void *)ip_packet + sizeof(*ip_packet); + if ((void *)tcp + sizeof(*tcp) <= ethernet_end) { + // Checking if the destination port matches with the specified port + pk.source_port = tcp->source; + pk.dest_port = tcp->dest; + } + } + + if (pk.protocol == IPPROTO_UDP) { + struct udphdr *udp = (void *)ip_packet + sizeof(*ip_packet); + if ((void *)udp + sizeof(*udp) <= ethernet_end) { + // Checking if the destination port matches with the specified port + pk.source_port = udp->source; + pk.dest_port = udp->dest; + } + } + struct lookup_ctx data = { + .pk = &pk, + .tc_output = 0, + }; + bpf_for_each_map_elem(&filter_rules, filter_packet, &data, 0); + if(data.tc_output == TC_ACT_SHOT){ + pk.is_dropped = 1; + push_log(&pk); + return TC_ACT_SHOT; + } + push_log(&pk); + return TC_ACT_OK; + } + push_log(&pk); + return TC_ACT_SHOT; +} + + SEC("xdp") -int intercept_packets(struct xdp_md *ctx) { +int intercept_packets_xdp_ingress(struct xdp_md *ctx) { // We mark the start and end of our ethernet frame void *ethernet_start = (void *)(long)ctx->data; void *ethernet_end = (void *)(long)ctx->data_end; @@ -106,7 +269,7 @@ int intercept_packets(struct xdp_md *ctx) { pk.size = (ethernet_end - ethernet_start); pk.dest_port = pk.source_port = 0; pk.is_dropped = 0; - + pk.direction = Ingress; // check the protocol and get port if (pk.protocol == IPPROTO_TCP) { struct tcphdr *tcp = (void *)ip_packet + sizeof(*ip_packet); @@ -128,12 +291,12 @@ int intercept_packets(struct xdp_md *ctx) { struct lookup_ctx data = { .pk = &pk, - .output = 0, + .xdp_output = 0, }; bpf_for_each_map_elem(&filter_rules, filter_packet, &data, 0); - if (data.output == XDP_DROP) { + if (data.xdp_output == XDP_DROP) { pk.is_dropped = 1; struct filter_rule *fr = data.fr; // network security event logs only work on kernel 5.16 diff --git a/pkg/config/config.go b/pkg/config/config.go index aac7e5e..c11fd9b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,12 +5,35 @@ import ( "fmt" "net/netip" "strconv" + "strings" "syscall" "github.com/DelusionalOptimist/dropit/pkg/types" "github.com/spf13/viper" ) +func DirectionStringToInt(direction string) uint8 { + switch strings.ToLower(direction) { + case "ingress": + return 0 + case "egress": + return 1 + default: + return 2 + } +} + +func DirectionIntToString(direction uint8) string { + switch direction { + case 0: + return "ingress" + case 1: + return "egress" + default: + return "unknown" + } +} + type Config struct { Rules []struct { ID string `yaml:"id"` @@ -18,6 +41,7 @@ type Config struct { SourcePort string `yaml:"sourcePort"` DestinationPort string `yaml:"destinationPort"` Protocol string `yaml:"protocol"` + Direction string `yaml:"direction"` } `yaml:"rules"` } @@ -108,10 +132,11 @@ func (cfg *Config) parseConfig() (map[string]types.FilterRuleBytes, error) { } byteRule := types.FilterRuleBytes{ - SourceIP: srcIP, - SourcePort: uint16(srcPort << 8 | srcPort >> 8), - DestinationPort: uint16(destPort << 8 | destPort >> 8), + SourceIP: srcIP, + SourcePort: uint16(srcPort<<8 | srcPort>>8), + DestinationPort: uint16(destPort<<8 | destPort>>8), Protocol: protocol, + Direction: DirectionStringToInt(rule.Direction), } fr[rule.ID] = byteRule diff --git a/pkg/monitor/monitor.go b/pkg/monitor/monitor.go index a9d33cf..bf1ee1e 100644 --- a/pkg/monitor/monitor.go +++ b/pkg/monitor/monitor.go @@ -8,6 +8,7 @@ import ( "os" "os/signal" "reflect" + "strings" "syscall" "unsafe" @@ -18,6 +19,11 @@ import ( "github.com/spf13/viper" ) +// TODO: Allow passing these from env variable +var XDP_PROG_NAME = "intercept_packets_xdp_ingress" +var TC_PROG_NAME = "intercept_packets_tc_egress" +var BPF_ELF_NAME = "daemon.o" + type Monitor struct { // userspace filter map FilterRuleBytesMap map[string]types.FilterRuleBytes @@ -50,7 +56,7 @@ func (m *Monitor) StartMonitor(interfaceName, cfgPath *string) error { } } - bpfModule, err := bpf.NewModuleFromFile("daemon.o") + bpfModule, err := bpf.NewModuleFromFile(BPF_ELF_NAME) if err != nil { return err } @@ -104,6 +110,11 @@ func (m *Monitor) loadConfig(cfgPath string) error { if err != nil { return err } + rules := []string{} + for rulename := range m.FilterRuleBytesMap { + rules = append(rules, rulename) + } + log.Printf("Currently active rules: %s\n", strings.Join(rules, ",")) return nil } @@ -120,7 +131,9 @@ func (m *Monitor) InitBPF(interfaceName string, bpfModule *bpf.Module) error { return err } - xdpProg, err := bpfModule.GetProgram("intercept_packets") + //Disabled for testing... + //XDP initialiasation + xdpProg, err := bpfModule.GetProgram(XDP_PROG_NAME) if xdpProg == nil { if err != nil { return fmt.Errorf("Failed to get xdp program %s", err) @@ -133,6 +146,34 @@ func (m *Monitor) InitBPF(interfaceName string, bpfModule *bpf.Module) error { return err } + //TC initialisation + hook := bpfModule.TcHookInit() + err = hook.SetInterfaceByName(interfaceName) + if err != nil { + return fmt.Errorf("failed to set tc hook on interface %s: %v", interfaceName, err) + } + + hook.SetAttachPoint(bpf.BPFTcEgress) + err = hook.Create() + if err != nil { + if errno, ok := err.(syscall.Errno); ok && errno != syscall.EEXIST { + return fmt.Errorf("failed to create tc hook on interface %s: %v", interfaceName, err) + } + } + + tcProg, err := bpfModule.GetProgram(TC_PROG_NAME) + if tcProg == nil { + return fmt.Errorf("could not find program %s: ", TC_PROG_NAME) + } + + var tcOpts bpf.TcOpts + tcOpts.ProgFd = int(tcProg.FileDescriptor()) + err = hook.Attach(&tcOpts) + if err != nil { + return err + } + + // Initialising Map m.FilterMap, err = bpfModule.GetMap("filter_rules") if err != nil { return err @@ -149,8 +190,8 @@ func (m *Monitor) StartLogging(eventsChan chan ([]byte), ringbuf *bpf.RingBuffer packetIdx := 0 log.Printf( - "|%-5s|%-15s|%-8s|%-15s|%-8s|%-5s|%-10s|%-8s|\n", - "No.", "Src IP", "Src Port", "Dest IP", "Dst Port", "Proto", "Size", "Status", + "|%-5s|%-15s|%-8s|%-15s|%-8s|%-5s|%-10s|%-8s|%-15s|\n", + "No.", "Src IP", "Src Port", "Dest IP", "Dst Port", "Proto", "Size", "Status", "Direction", ) for { @@ -160,7 +201,7 @@ func (m *Monitor) StartLogging(eventsChan chan ([]byte), ringbuf *bpf.RingBuffer pk := parseByteData(eventBytes) log.Printf( - "|%-5d|%-15s|%-8d|%-15s|%-8d|%-5s|%-10d|%-8s|\n", + "|%-5d|%-15s|%-8d|%-15s|%-8d|%-5s|%-10d|%-8s|%-15s|\n", packetIdx, pk.SourceIP, pk.SourcePort, @@ -169,6 +210,7 @@ func (m *Monitor) StartLogging(eventsChan chan ([]byte), ringbuf *bpf.RingBuffer pk.Protocol, pk.Size, pk.Status, + pk.Direction, ) case sig := <-m.SigChan: @@ -194,8 +236,11 @@ func (m *Monitor) configChangeHandler(in fsnotify.Event) { log.Println("Filter file updated. No changes to do...") return } - - log.Printf("Filter file updated. Getting new filter rules...\n") + rules := []string{} + for rulename := range frMapNew { + rules = append(rules, rulename) + } + log.Printf("Filter file updated. Currently active rules: %s\n", strings.Join(rules, ",")) for newRule, newVal := range frMapNew { if _, ok := m.FilterRuleBytesMap[newRule]; ok { @@ -306,7 +351,6 @@ func parseByteData(data []byte) types.Packet { DestPort: binary.BigEndian.Uint16(data[14:16]), Protocol: types.GetProtoName(data[16]), Status: "Passed", - // Using gopacket //Protocol: layers.IPProtocol(data[16]).String(), } @@ -321,6 +365,8 @@ func parseByteData(data []byte) types.Packet { pk.DestIP = dstIP.String() } + pk.Direction = config.DirectionIntToString(uint8(data[18])) + isDropped := data[17] if isDropped == 1 { pk.Status = "Dropped" diff --git a/pkg/types/filterRule.go b/pkg/types/filterRule.go index efae2b6..fa85228 100644 --- a/pkg/types/filterRule.go +++ b/pkg/types/filterRule.go @@ -5,4 +5,5 @@ type FilterRuleBytes struct { SourcePort uint16 DestinationPort uint16 Protocol uint8 + Direction uint8 } diff --git a/pkg/types/packet.go b/pkg/types/packet.go index 92fd7d2..c28a176 100644 --- a/pkg/types/packet.go +++ b/pkg/types/packet.go @@ -8,4 +8,5 @@ type Packet struct { DestPort uint16 Protocol string Status string + Direction string } diff --git a/sample/dropit.yaml b/sample/dropit.yaml index 7921ebb..52e0bdb 100644 --- a/sample/dropit.yaml +++ b/sample/dropit.yaml @@ -1,6 +1,13 @@ rules: - - id: "rule1" + - id: "drop-all-ingress" sourceIP: '*' - destinationPort: 8080 + destinationPort: '*' sourcePort: '*' protocol: '*' + direction: ingress + - id: "drop-all-egress" + sourceIP: '*' + destinationPort: '*' + sourcePort: '*' + protocol: '*' + direction: egress