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 )