docs: cookbook recipe for getaddrinfo IPv4+IPv6 resolution + SSRF resolve-pin
ober
dd183c50f677b6bfb464b2108f81e23b915cdc11
--- a/data/cookbooks.sexp +++ b/data/cookbooks.sexp @@ -7054,4 +7054,16 @@ "index" "lookup") ("title" . - "Line-offset index + binary search for O(log n) offset->line/col"))) + "Line-offset index + binary search for O(log n) offset->line/col")) + (("code" + . + "(library (example resolve)\n (export resolve-host-addresses)\n (import (chezscheme) (only (jerboa core) def))\n\n (def _libc-loaded\n (let ((v (getenv \"JERBOA_STATIC\")))\n (if (and v (not (string=? v \"\")) (not (string=? v \"0\")))\n #f (load-shared-object #f))))\n (def c-getaddrinfo (foreign-procedure \"getaddrinfo\" (string string void* void*) int))\n (def c-freeaddrinfo (foreign-procedure \"freeaddrinfo\" (void*) void))\n (def c-inet-ntop (foreign-procedure \"inet_ntop\" (int void* u8* int) void*))\n (def AF_INET 2) (def AF_UNSPEC 0) (def SOCK_STREAM 1)\n (def INET_ADDRSTRLEN 16) (def INET6_ADDRSTRLEN 46)\n\n ;; ai_addr moves by OS; ai_family@4 and ai_next@40 are stable.\n (def ai-addr-offset\n (let* ([mt (symbol->string (machine-type))]\n [has? (lambda (sub)\n (let ([ml (string-length mt)] [sl (string-length sub)])\n (let loop ([i 0])\n (cond [(> (+ i sl) ml) #f]\n [(string=? sub (substring mt i (+ i sl))) #t]\n [else (loop (+ i 1))]))))])\n (cond [(or (has? \"osx\") (has? \"darwin\") (has? \"fb\") (has? \"ob\") (has? \"nb\")) 32]\n [else 24])))\n\n ;; Pass the family straight to inet_ntop so the OS AF_INET6 constant is used.\n ;; in_addr @ sockaddr+4; in6_addr @ sockaddr+8 on every supported OS.\n (def (sockaddr->ip family addr)\n (let-values ([(src len) (if (= family AF_INET)\n (values (+ addr 4) INET_ADDRSTRLEN)\n (values (+ addr 8) INET6_ADDRSTRLEN))])\n (let ([buf (make-bytevector len 0)])\n (and (not (= (c-inet-ntop family src buf len) 0))\n (let loop ([i 0])\n (cond [(= i len) #f]\n [(= (bytevector-u8-ref buf i) 0)\n (utf8->string (let ([b (make-bytevector i)])\n (bytevector-copy! buf 0 b 0 i) b))]\n [else (loop (+ i 1))]))))))\n\n (def (collect node acc)\n (if (= node 0) (reverse acc)\n (let* ([fam (foreign-ref 'int node 4)]\n [ap (foreign-ref 'void* node ai-addr-offset)]\n [ip (and (not (= ap 0)) (sockaddr->ip fam ap))]\n [nx (foreign-ref 'void* node 40)])\n (collect nx (if ip (cons ip acc) acc)))))\n\n (def (resolve-host-addresses host)\n (let ([hints (foreign-alloc 48)])\n (do ([i 0 (+ i 1)]) ((= i 48)) (foreign-set! 'unsigned-8 hints i 0))\n (foreign-set! 'int hints 4 AF_UNSPEC)\n (foreign-set! 'int hints 8 SOCK_STREAM)\n (let ([rp (foreign-alloc 8)])\n (foreign-set! 'void* rp 0 0)\n (let ([rc (c-getaddrinfo host #f hints rp)])\n (foreign-free hints)\n (unless (= rc 0) (foreign-free rp)\n (error 'resolve-host-addresses \"DNS failed\" host rc))\n (let* ([res (foreign-ref 'void* rp 0)] [ips (collect res '())])\n (foreign-free rp)\n (unless (= res 0) (c-freeaddrinfo res))\n ips)))))\n)") ("id" . "getaddrinfo-ipv4-ipv6-resolve-and-ssrf-pin") + ("imports" "(chezscheme)" "(only (jerboa core) def)") + ("notes" + . + "Hard-won offsets: ai_family@4, ai_next@40 stable; ai_addr@24 (glibc) vs 32 (BSD/macOS). sockaddr in_addr@4, in6_addr@8. AF_UNSPEC=0 requests both families. Pass the read ai_family directly to inet_ntop (don't hardcode AF_INET6=10/30). For SSRF rebinding safety: resolve once, validate EVERY returned IP against the blocklist (reject if any is non-public), pin one validated IP, and connect to it (tcp-connect-binary IP / rustls-connect-address host IP) — never re-resolve at connect. See lib/std/net/request.ss ssrf-resolve-safe-ip.") + ("tags" "getaddrinfo" "dns" "ipv6" "inet_ntop" "ssrf" + "std net request") + ("title" + . + "Portable getaddrinfo IPv4+IPv6 resolution and DNS-rebinding-safe SSRF pinning")))