#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/types.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>

#define SSH_PROTOCOL_VERSION "SSH-2.0-"
#define SSH_PROTOCOL_VERSION_LEN 7

#define SSH_KEX_METHOD_DH_GROUP1_SHA1 "diffie-hellman-group1-sha1"
#define SSH_KEX_METHOD_DH_GROUP1_SHA1_LEN 26

struct ssh_payload {
    uint8_t protocol_version[SSH_PROTOCOL_VERSION_LEN];
    uint8_t kex_method_len;
    uint8_t kex_method[SSH_KEX_METHOD_DH_GROUP1_SHA1_LEN];
};

struct bpf_map_def SEC("maps") payload_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(struct conn_key),
    .value_size = sizeof(struct ssh_payload),
    .max_entries = 1024,
};

struct bpf_map_def SEC("maps") conn_state_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(struct conn_key),
    .value_size = sizeof(struct conn_state),
    .max_entries = 1024,
};

struct bpf_map_def SEC("maps") conn_attempts_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(struct in_addr),
    .value_size = sizeof(uint32_t),
    .max_entries = 1024,
};

struct bpf_map_def SEC("maps") active_conns_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(struct in_addr),
    .value_size = sizeof(uint32_t),
    .max_entries = 1024,
};

struct bpf_map_def SEC("maps") xdp_drops_map = {
    .type = BPF_MAP_TYPE_RINGBUF,
    .key_size = sizeof(uint32_t),
    .value_size = sizeof(struct xdp_drop_info),
    .max_entries = 1024,
};

struct conn_key {
    struct in_addr src_ip;
    struct in_addr dst_ip;
    uint16_t src_port;
    uint16_t dst_port;
};

struct conn_state {
    uint8_t state;
};

struct xdp_drop_info {
    struct in_addr src_ip;
    struct in_addr dst_ip;
    uint16_t src_port;
    uint16_t dst_port;
    uint8_t reason;
};

enum conn_state {
    SYN_SENT,
    SYN_RECV,
    ESTABLISHED,
};

enum drop_reason {
    INVALID_PACKET,
    NON_SSH_TRAFFIC,
    RATE_LIMIT_EXCEEDED,
    ACTIVE_CONN_LIMIT_EXCEEDED,
};

static inline void load_ip_header(struct xdp_md *ctx, struct iphdr *iphdr)
{
    bpf_skb_load_bytes(ctx, 0, sizeof(*iphdr), iphdr);
}

static inline void load_tcp_header(struct xdp_md *ctx, struct tcphdr *tcphdr)
{
    bpf_skb_load_bytes(ctx, sizeof(struct ethhdr) + sizeof(struct iphdr), sizeof(*tcphdr), tcphdr);
}

static inline void load_ssh_payload(struct xdp_md *ctx, void *payload, uint16_t payload_len)
{
    bpf_skb_load_bytes(ctx, sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct tcphdr), payload_len, payload);
}

static inline uint16_t get_dst_port(struct xdp_md *ctx)
{
    struct iphdr iphdr;
    struct tcphdr tcphdr;

    load_ip_header(ctx, &iphdr);
    load_tcp_header(ctx, &tcphdr);

    return bpf_ntohs(tcphdr.dest);
}

static inline uint8_t get_tcp_flags(struct xdp_md *ctx)
{
    struct iphdr iphdr;
    struct tcphdr tcphdr;

    load_ip_header(ctx, &iphdr);
    load_tcp_header(ctx, &tcphdr);

    return tcphdr.syn | tcphdr.ack | tcphdr.fin | tcphdr.rst | tcphdr.psh | tcphdr.urg;
}

static inline uint32_t get_src_ip(struct xdp_md *ctx)
{
    struct iphdr iphdr;

    load_ip_header(ctx, &iphdr);

    return iphdr.saddr;
}

static inline uint32_t get_dst_ip(struct xdp_md *ctx)
{
    struct iphdr iphdr;

    load_ip_header(ctx, &iphdr);

    return iphdr.daddr;
}

static inline uint16_t get_src_port(struct xdp_md *ctx)
{
    struct iphdr iphdr;
    struct tcphdr tcphdr;

    load_ip_header(ctx, &iphdr);
    load_tcp_header(ctx, &tcphdr);

    return bpf_ntohs(tcphdr.source);
}

static inline uint16_t get_payload_len(struct xdp_md *ctx)
{
    struct iphdr iphdr;

    load_ip_header(ctx, &iphdr);

    return bpf_ntohs(iphdr.tot_len) - (iphdr.ihl * 4) - (sizeof(struct tcphdr));
}

SEC("xdp")
int xdp_prog(struct xdp_md *ctx)
{
    uint16_t dst_port = get_dst_port(ctx);

    if (dst_port != 22) {
        return XDP_DROP;
    }

    struct conn_key key = {
        .src_ip = { .s_addr = get_src_ip(ctx) },
        .dst_ip = { .s_addr = get_dst_ip(ctx) },
        .src_port = get_src_port(ctx),
        .dst_port = dst_port,
    };

    struct conn_state *state = bpf_map_lookup_elem(&conn_state_map, &key);

    if (!state) {
        state = bpf_map_lookup_elem(&conn_state_map, &key);

        if (!state) {
            struct conn_state new_state = { .state = SYN_SENT };
            bpf_map_update_elem(&conn_state_map, &key, &new_state, BPF_ANY);
        }
    }

    uint8_t tcp_flags = get_tcp_flags(ctx);

    if (tcp_flags & (TCPHDR_SYN | TCPHDR_ACK)) {
        if (state->state == SYN_SENT) {
            state->state = SYN_RECV;
        } else if (state->state == SYN_RECV) {
            state->state = ESTABLISHED;
        } else {
            return XDP_DROP;
        }
    } else if (tcp_flags & TCPHDR_FIN) {
        if (state->state == ESTABLISHED) {
            state->state = SYN_SENT;
        } else {
            return XDP_DROP;
        }
    } else {
        return XDP_DROP;
    }

    uint32_t *conn_attempts = bpf_map_lookup_elem(&conn_attempts_map, &key.src_ip);

    if (!conn_attempts) {
        uint32_t new_conn_attempts = 1;
        bpf_map_update_elem(&conn_attempts_map, &key.src_ip, &new_conn_attempts, BPF_ANY);
    } else {
                (*conn_attempts)++;
        if (*conn_attempts > 10) { // Set the rate limit here
            struct xdp_drop_info drop_info = {
                .src_ip = key.src_ip,
                .dst_ip = key.dst_ip,
                .src_port = key.src_port,
                .dst_port = key.dst_port,
                .reason = RATE_LIMIT_EXCEEDED,
            };
            bpf_ringbuf_output(&xdp_drops_map, &drop_info, sizeof(drop_info), 0);
            return XDP_DROP;
        }
    }

    uint32_t *active_conns = bpf_map_lookup_elem(&active_conns_map, &key.src_ip);

    if (!active_conns) {
        uint32_t new_active_conns = 1;
        bpf_map_update_elem(&active_conns_map, &key.src_ip, &new_active_conns, BPF_ANY);
    } else {
        (*active_conns)++;
        if (*active_conns > 3) { // Set the active connection limit here
            struct xdp_drop_info drop_info = {
                .src_ip = key.src_ip,
                .dst_ip = key.dst_ip,
                .src_port = key.src_port,
                .dst_port = key.dst_port,
                .reason = ACTIVE_CONN_LIMIT_EXCEEDED,
            };
            bpf_ringbuf_output(&xdp_drops_map, &drop_info, sizeof(drop_info), 0);
            return XDP_DROP;
        }
    }

    uint16_t payload_len = get_payload_len(ctx);

    if (payload_len > 0) {
        void *payload = bpf_map_lookup_elem(&payload_map, &key);

        if (!payload) {
            payload = bpf_map_lookup_elem(&payload_map, &key);

            if (!payload) {
                payload = bpf_map_alloc_elem(&payload_map, &key);

                if (!payload) {
                    return XDP_DROP;
                }
            }
        }

        load_ssh_payload(ctx, payload, payload_len);

        // Custom SSH protocol validation
        struct ssh_payload *ssh_payload = payload;

        if (memcmp(ssh_payload->protocol_version, SSH_PROTOCOL_VERSION, SSH_PROTOCOL_VERSION_LEN) != 0) {
            struct xdp_drop_info drop_info = {
                .src_ip = key.src_ip,
                .dst_ip = key.dst_ip,
                .src_port = key.src_port,
                .dst_port = key.dst_port,
                .reason = INVALID_PACKET,
            };
            bpf_ringbuf_output(&xdp_drops_map, &drop_info, sizeof(drop_info), 0);
            return XDP_DROP;
        }

        if (ssh_payload->kex_method_len != SSH_KEX_METHOD_DH_GROUP1_SHA1_LEN ||
            memcmp(ssh_payload->kex_method, SSH_KEX_METHOD_DH_GROUP1_SHA1, SSH_KEX_METHOD_DH_GROUP1_SHA1_LEN) != 0) {
            struct xdp_drop_info drop_info = {
                .src_ip = key.src_ip,
                .dst_ip = key.dst_ip,
                .src_port = key.src_port,
                .dst_port = key.dst_port,
                .reason = INVALID_PACKET,
            };
            bpf_ringbuf_output(&xdp_drops_map, &drop_info, sizeof(drop_info), 0);
            return XDP_DROP;
        }
    }

    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";