php方案 Netfilter Hook 联动
大白话 Linux 收发包会经过内核 Netfilter 的5个关卡Hook 点 网卡收包 └─PRE_ROUTING← 路由前可改目标IPDNAT └─ 路由决策 ├─LOCAL_IN← 发给本机的包 ├─FORWARD← 转发包路由器用 └─LOCAL_OUT← 本机发出的包 └─POST_ROUTING← 路由后可改源IPSNAT/MASQUERADE iptables/nftables 就是往这些关卡挂规则。NFQUEUE规则里写-jNFQUEUE内核把包送到用户进程PHP说ACCEPT就放行说DROP就丢掉。这样就能在PHP里做任意复杂的包过滤逻辑。 流程 网卡 → 内核Netfilter →NFQUEUE→[netlink socket]→PHP决策 → 裁决回内核 → 放行/丢弃---代码PHP没有 netfilter 扩展直接FFI调 libc socketAPI走 netlink 协议最底层最直接?phpdeclare(strict_types1);// 需要: root 权限ffi.enabletrue// libnetfilter_queue 的 PHP FFI 版跳过回调限制直接走 netlink 裸协议$cFFI::cdef(Cintsocket(intdomain,inttype,intprotocol);intbind(intfd,constvoid*addr,unsignedintlen);ssize_tsend(intfd,constvoid*buf,size_t n,intflags);ssize_trecv(intfd,void*buf,size_t n,intflags);C,libc.so.6);// ── netlink 消息打包工具 ──────────────────────────────────────────────────$pidgetmypid();$seq1;// nlmsghdr(16字节) body$nlfunction(int$t,string$body)use($pid,$seq):string{returnpack(VSSV V,16strlen($body),$t,1/*NLM_F_REQUEST*/,$seq,$pid).$body;};// nfattr: 4字节头 data4字节对齐$attrfn(int$t,string$d):stringpack(vv,4strlen($d),$t).$d.str_repeat(\0,(4-strlen($d)%4)%4);// ── 打开 NETLINK_NETFILTER socket ─────────────────────────────────────────$fd$c-socket(16/*AF_NETLINK*/,3/*SOCK_RAW*/,12/*NETLINK_NETFILTER*/);// sockaddr_nl: family(2) pad(2) pid(4) groups(4) 12字节$saFFI::new(char[12]);FFI::memcpy($sa,pack(SSVV,16,0,$pid,0),12);$c-bind($fd,$sa,12);$sendfunction(string$msg)use($c,$fd):void{$bFFI::new(char[.strlen($msg).]);FFI::memcpy($b,$msg,strlen($msg));$c-send($fd,$b,strlen($msg),0);};// ── 注册到 NFQUEUE ────────────────────────────────────────────────────────$Q0;// 队列号$gmfn():stringpack(CCn,0/*AF_UNSPEC*/,0,$Q);// nfgenmsg$typefn(int$s,int$m):int($s8)|$m;// subsys|msg// 绑定告诉内核队列$Q的包给我$send($nl($type(3/*NFNL_SUBSYS_QUEUE*/,2/*NFQNL_MSG_CONFIG*/),$gm().$attr(1/*NFQA_CFG_CMD*/,pack(CCn,1/*BIND*/,0,2/*AF_INET*/))));// COPY_PACKET 模式把完整包数据传过来不只是元数据$send($nl($type(3,2),$gm().$attr(2/*NFQA_CFG_PARAMS*/,pack(NC,0xFFFF,2/*NFQNL_COPY_PACKET*/))));echo[nfq] Listening on queue #$Q\n;// ── 主循环 ────────────────────────────────────────────────────────────────$rbufFFI::new(char[65536]);while(true){$n$c-recv($fd,$rbuf,65536,0);if($n20)continue;$rawFFI::string($rbuf,$n);// 解 nlmsghdr只处理 NFQNL_MSG_PACKET (type低字节0)if((unpack(Vlen/Stype,$raw)[type]0xFF)!0)continue;// 扫描 nfattrs跳过 nlmsghdr16 nfgenmsg4 偏移20$off20;$pktId$ipnull;while($off4$n){[alen$alen,atype$atype]unpack(valen/vatype,substr($raw,$off,4));$dsubstr($raw,$off4,$alen-4);if($atype1/* NFQA_PACKET_HDR */)$pktIdunpack(N,$d)[1];// packet_id (be32)if($atype10/* NFQA_PAYLOAD */)$ip$d;// 原始 IP 包$off($alen3)~3;// 4字节对齐}if($pktIdnull)continue;// ── 你的过滤逻辑写这里 ────────────────────────────────────────────────$verdict1;// NF_ACCEPTif($ipstrlen($ip)20){$srcIpinet_ntop(substr($ip,12,4));$dstIpinet_ntop(substr($ip,16,4));$protoord($ip[9]);// 6TCP 17UDP 1ICMP// TCP 时解端口IP头长度可变ihl字段低4位×4$ihl(ord($ip[0])0x0F)*4;$sport$dport0;if($proto6strlen($ip)$ihl4){$sportunpack(n,substr($ip,$ihl,2))[1];$dportunpack(n,substr($ip,$ihl2,2))[1];}printf(%-15s:%-5d → %-15s:%-5d proto%d\n,$srcIp,$sport,$dstIp,$dport,$proto);// 封 IP丢弃来自某源 IP 的所有包// if ($srcIp 1.2.3.4) $verdict 0; // NF_DROP// 封端口丢弃目标端口 9999 的 TCP 包// if ($proto 6 $dport 9999) $verdict 0;}// ─────────────────────────────────────────────────────────────────────// 发裁决告诉内核放行还是丢弃$send($nl($type(3,1/*NFQNL_MSG_VERDICT*/),$gm().$attr(1/*NFQA_VERDICT_HDR*/,pack(NN,$verdict,$pktId))));}---启动步骤# 把 HTTP 流量送到队列 0PHP 来决定iptables-IINPUT-p tcp--dport8080-jNFQUEUE--queue-num0# 启动 PHP 过滤器必须 rootphp nfq.php# 清理规则iptables-DINPUT-p tcp--dport8080-jNFQUEUE--queue-num0nftables 等价写法 nft add rule inet filter input tcp dport8080queue num0---为啥不用 libnetfilter_queue 而直接走 netlink ┌────────────────────────┬─────────────────────────────────────────────┐ │ 方式 │ 问题 │ ├────────────────────────┼─────────────────────────────────────────────┤ │ libnetfilter_queueC库 │ 需要回调函数指针PHPFFI不支持C→PHP回调 │ ├────────────────────────┼─────────────────────────────────────────────┤ │ ext-netfilterPHP扩展 │ 不存在 │ ├────────────────────────┼─────────────────────────────────────────────┤ │ 直接 netlink 裸协议 │ 只需 send/recv无回调PHP完全能做 │ └────────────────────────┴─────────────────────────────────────────────┘---5个 Hook 点对应 iptables 表PRE_ROUTING→ nat表DNAT改目标IP/端口做端口映射LOCAL_IN→ filter表INPUT防火墙入站规则FORWARD→ filter表FORWARD路由器转发规则LOCAL_OUT→ filter表OUTPUT本机发出规则POST_ROUTING→ nat表MASQUERADE/SNAT改源IP共享上网