Punk

 view release on metacpan or  search on metacpan

include/punk/punk_proxy.h  view on Meta::CPAN

#ifndef PUNK_PROXY_H
#define PUNK_PROXY_H

/* punk_proxy.h - reverse-proxy trust, in C.
 *
 * The `proxy` keyword freezes a trust policy at to_app; pp_resolve runs once
 * at the top of punk_serve, before routing, and OVERWRITES REMOTE_ADDR with
 * the real client. Everything downstream - rate_limit (punk_ratelimit.h),
 * $c->block_ip, the access log, $c->req->address - then reads the right
 * address without any of them being changed. The mount path already rewrites
 * env keys in place (punk_serve.h), so this is an established move.
 *
 * Why it exists: rate_limit keys on REMOTE_ADDR, and because Hyperman's
 * arena makes a counter exact across the whole worker pool rather than per
 * worker, a limiter behind a proxy puts EVERY client in one bucket - a
 * 100/min rule throttles the whole site at 100/min. block_ip, keyed the same
 * way, bans the load balancer.
 *
 * The hop rule, which is what almost every implementation gets wrong:
 * X-Forwarded-For reads `client, proxy1, proxy2` and each hop APPENDS the
 * address it received the connection FROM. The socket peer is the last
 * proxy and never appears in the header it forwarded. So with N trusted
 * proxies the client sits at index N-1 counting from the RIGHT. Taking the
 * leftmost entry is the spoofable version: the client writes that one.
 *
 * Everything here is prefixed pp_ / PP_. Needs nothing but punk_compat.h;
 * included before punk_serve.h, which calls pp_resolve.
 */

#include <string.h>

/* A chain longer than this is pathological. We keep the RIGHTMOST entries,
 * which is the end the hop walk starts from; 'all' mode scans forward for
 * the leftmost separately and is unaffected. */
#define PP_MAX_HOPS 32

/* Addresses are held as 16 bytes, v4 stored v4-mapped (::ffff:a.b.c.d), so
 * one compare serves both families and a v4 CIDR written by an operator
 * still matches the v4-mapped form a dual-stack listener hands us. */
typedef struct {
    unsigned char addr[16];
    int           bits;        /* prefix length in the 128-bit space */
} pp_cidr;

typedef struct {
    int      hops;             /* >0 fixed count; 0 = use cidrs; -1 = all */
    pp_cidr *cidrs;
    int      ncidrs;
    char    *for_key;          /* pre-built env keys: HTTP_X_FORWARDED_FOR */
    STRLEN   for_len;
    char    *proto_key;
    STRLEN   proto_len;
    char    *host_key;
    STRLEN   host_len;
    char    *port_key;
    STRLEN   port_len;
} pp_policy;

static void pp_free(pTHX_ pp_policy *p);   /* the error path in pp_compile */

/* ---- address parsing ----------------------------------------------------
 * Hand-rolled rather than inet_pton: this runs on attacker-controlled bytes
 * that become a shared-memory key, the grammar wanted here is stricter than
 * the libc one (no octal, no shortened v4), and it keeps the Windows story
 * free of ws2tcpip.h. */

/* Strict dotted quad: four decimal octets, no leading zeros (a leading zero
 * is octal to some resolvers and decimal to others, which is exactly the
 * ambiguity an address filter must not inherit), nothing trailing. */
static int pp_parse_v4(const char *s, STRLEN len, unsigned char out[4]) {
    STRLEN i = 0;
    int oct;
    for (oct = 0; oct < 4; oct++) {
        int v = 0, nd = 0;
        if (oct) {
            if (i >= len || s[i] != '.') return 0;
            i++;
        }
        while (i < len && s[i] >= '0' && s[i] <= '9') {
            if (nd == 0 && s[i] == '0' && i + 1 < len

include/punk/punk_proxy.h  view on Meta::CPAN

static char *pp_key_opt(pTHX_ HV *cfg, const char *opt, const char *dflt,
                        STRLEN *outlen) {
    SV **v = hv_fetch(cfg, opt, (I32)strlen(opt), 0);
    STRLEN nl;
    const char *n;
    if (v && *v && !SvOK(*v)) return NULL;      /* undef switches it off */
    n = (v && *v) ? SvPV_const(*v, nl) : (nl = strlen(dflt), dflt);
    if (!nl) return NULL;
    return pp_env_key(aTHX_ n, nl, outlen);
}

/* Freeze the keyword's config into the policy a request reads. Croaks on
 * anything malformed - a mistyped CIDR or a nonsense hop count must fail at
 * boot, where it is one line of output, not per request where it is a
 * silently wrong address in a rate-limit key. `appenv` is $app->env. */
static pp_policy *pp_compile(pTHX_ HV *cfg, const char *appenv) {
    pp_policy *p;
    SV **t = hv_fetchs(cfg, "trust", 0);

    Newxz(p, 1, pp_policy);
    p->hops = 1;                                /* bare `proxy` = one hop */

    if (t && *t && SvOK(*t)) {
        if (SvROK(*t) && SvTYPE(SvRV(*t)) == SVt_PVAV) {
            AV *av = (AV *)SvRV(*t);
            SSize_t i, n = av_len(av) + 1;
            if (n <= 0) {
                pp_free(aTHX_ p);
                croak("Punk: proxy: trust => [] trusts nothing - drop the "
                      "keyword instead, or name the proxy networks");
            }
            Newxz(p->cidrs, (int)n, pp_cidr);
            for (i = 0; i < n; i++) {
                SV **e = av_fetch(av, i, 0);
                STRLEN sl;
                const char *s;
                if (!(e && *e && SvOK(*e))) {
                    pp_free(aTHX_ p);
                    croak("Punk: proxy: trust list entry %d is undef",
                          (int)i);
                }
                s = SvPV_const(*e, sl);
                if (!pp_parse_cidr(s, sl, &p->cidrs[i])) {
                    SV *msg = sv_2mortal(newSVpvf(
                        "Punk: proxy: '%.*s' is not an address or CIDR",
                        (int)sl, s));
                    pp_free(aTHX_ p);
                    croak("%s", SvPV_nolen(msg));
                }
            }
            p->ncidrs = (int)n;
            p->hops = 0;
        }
        else if (!SvROK(*t) && !looks_like_number(*t)) {
            STRLEN sl;
            const char *s = SvPV_const(*t, sl);
            if (sl == 3 && memEQ(s, "all", 3)) {
                if (!(appenv && strEQ(appenv, "development"))) {
                    pp_free(aTHX_ p);
                    croak("Punk: proxy: trust => 'all' believes any "
                          "X-Forwarded-For from anyone, so with no proxy in "
                          "front it is a total bypass - it is allowed only "
                          "under PUNK_ENV=development. Name a hop count or "
                          "the proxy networks instead");
                }
                p->hops = -1;
            } else {
                SV *msg = sv_2mortal(newSVpvf(
                    "Punk: proxy: trust must be a hop count, an arrayref of "
                    "CIDRs, or 'all' - not '%.*s'", (int)sl, s));
                pp_free(aTHX_ p);
                croak("%s", SvPV_nolen(msg));
            }
        }
        else if (SvROK(*t)) {
            pp_free(aTHX_ p);
            croak("Punk: proxy: trust must be a hop count, an arrayref of "
                  "CIDRs, or 'all'");
        }
        else {
            IV n = SvIV(*t);
            if (n < 1 || n > PP_MAX_HOPS) {
                pp_free(aTHX_ p);
                croak("Punk: proxy: trust => %" IVdf " is not a usable hop "
                      "count (1..%d)", n, PP_MAX_HOPS);
            }
            p->hops = (int)n;
        }
    }

    p->for_key   = pp_key_opt(aTHX_ cfg, "for_header",
                              "X-Forwarded-For",   &p->for_len);
    p->proto_key = pp_key_opt(aTHX_ cfg, "proto_header",
                              "X-Forwarded-Proto", &p->proto_len);
    p->host_key  = pp_key_opt(aTHX_ cfg, "host_header",
                              "X-Forwarded-Host",  &p->host_len);
    p->port_key  = pp_key_opt(aTHX_ cfg, "port_header",
                              "X-Forwarded-Port",  &p->port_len);
    return p;
}

/* ---- the per-request resolution ----------------------------------------- */

static void pp_free(pTHX_ pp_policy *p) {
    if (!p) return;
    Safefree(p->cidrs);
    Safefree(p->for_key);
    Safefree(p->proto_key);
    Safefree(p->host_key);
    Safefree(p->port_key);
    Safefree(p);
}

/* Called once per request from the top of punk_serve, and only when a
 * policy was declared - an app without `proxy` pays one predictable
 * branch and nothing else. */
static void pp_resolve(pTHX_ const pp_policy *p, HV *env) {
    SV **e;
    SV *peersv;
    const char *peer;
    STRLEN plen = 0;

    e = hv_fetchs(env, "REMOTE_ADDR", 0);
    peersv = (e && *e && SvOK(*e)) ? *e : NULL;
    peer = peersv ? SvPV_const(peersv, plen) : "";

    /* the socket peer, kept before anything is rewritten, so an edge ban
     * still has the address the connection actually came from */
    (void)hv_stores(env, "punk.peer_addr",
                    peersv ? newSVsv(peersv) : newSVpvs(""));

    e = p->for_key ? hv_fetch(env, p->for_key, (I32)p->for_len, 0) : NULL;
    if (e && *e && SvOK(*e)) {
        STRLEN xl, clen;
        const char *x = SvPV_const(*e, xl);
        const char *client = pp_xff_client(x, xl, p, peer, plen, &clen);
        if (clen != plen || (clen && memNE(client, peer, clen))) {
            /* copy before the store: `peer` points into the SV it frees */
            SV *newsv = newSVpvn(client, clen);
            (void)hv_stores(env, "REMOTE_ADDR", newsv);
            /* the port described the proxy's socket and is now a lie */
            (void)hv_delete(env, "REMOTE_PORT", 11, G_DISCARD);
        }
    }

    if (p->proto_key) {
        e = hv_fetch(env, p->proto_key, (I32)p->proto_len, 0);
        if (e && *e && SvOK(*e)) {
            STRLEN vl, l;
            const char *v = SvPV_const(*e, vl);
            const char *t = pp_first_token(v, vl, &l);
            if (l == 5 && foldEQ(t, "https", 5)) {



( run in 0.822 second using v1.01-cache-2.11-cpan-54e63673c56 )