GoAccess 提取 X-Forwarded-For 真实源 IP 踩坑记:%x 不是 XFF

问题背景

用 GoAccess 解析 Nginx access 日志生成流量报表时,发现客户端 IP 字段无法提取 X-Forwarded-For(XFF)中的真实源 IP——报表里全是代理/网关的 IP,攻击溯源和访客统计全废了。

排查了一下午,根因是一个字符:把 %x 当成了 XFF 字段。

根因:%x 是日期时间,不是 XFF

错误命令(第一版):

1
2
3
4
5
goaccess access_xff.log -o report.html \
--log-format="%h - %^ [%d:%t %^] \"%r\" %s %b \"%R\" \"%u\" \"%x\"" \
--date-format=%d/%b/%Y \
--time-format=%T \
--no-ip-validation

查 GoAccess 官方手册确认:

  • %x = Combined date and time(组合日期时间字段),配合 --datetime-format 使用
  • 它不是 X-Forwarded-For。GoAccess 会把日志该位置的内容当日期时间解析,导致解析失败或字段错位 → XFF 提取不出来

正确语法:~h 波浪号

GoAccess 用波浪号 + 大括号分隔符提取 XFF,格式为 ~h{分隔符}。常见格式会提取列表中的第一个有效 IP,但这只是日志解析结果,不自动等于可信的真实客户端地址:

XFF 字段格式 正确 specifier
"192.0.2.3, 198.51.100.17, 203.0.113.2"(带引号,逗号+空格) "~h{, }"
"192.0.2.12", "198.51.100.17"(引号包裹单个 IP) ~h{", }
192.0.2.12, 198.51.100.17(无引号) ~h{, }
192.0.2.14 198.51.100.17(空格分隔) ~h{ }

这些地址为文档示例地址。XFF 可由客户端伪造,只有入口代理清理或重写客户端传入的头、并正确配置可信代理范围时,才能用于溯源。更稳妥的做法是由 Nginx 的 realip 模块按可信代理链解析客户端地址,再记录解析后的 $remote_addr,由 GoAccess 使用 %h 读取;原始 XFF 可作为辅助证据保留。

修复版命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 情况 1:XFF 在日志末尾,带引号、逗号+空格分隔(最常见)
goaccess access_xff.log -o report.html \
--log-format="%h - %^ [%d:%t %^] \"%r\" %s %b \"%R\" \"%u\" \"~h{, }\"" \
--date-format=%d/%b/%Y \
--time-format=%T

# 情况 2:日志中间有额外字段($request_time、$upstream 等),用 %^ 跳过
goaccess access.log -o report.html \
--log-format="%h - %^ [%d:%t %^] \"%r\" %^ %^ %^ %s %b \"%R\" \"%u\" \"~h{, }\"" \
--date-format=%d/%b/%Y \
--time-format=%T

# 情况 3:XFF 无引号,逗号分隔
goaccess access.log -o report.html \
--log-format="%h - %^ [%d:%t %^] \"%r\" %s %b \"%R\" \"%u\" ~h{, }" \
--date-format=%d/%b/%Y \
--time-format=%T

关键参数说明

参数 作用
~h{, } 按 XFF 分隔符提取 IP;地址可信度需另外核查代理链
%^ 跳过不需要的字段(通配符)
--no-ip-validation 关闭 IP 校验,主要用于日志地址已混淆等特殊场景;IPv6 不要求此参数
--date-format=%d/%b/%Y 日期格式(Nginx 默认)
--time-format=%T 时间格式 HH:MM:SS

IPv6 两个注意点

  1. GoAccess 支持 IPv4 和 IPv6,合法 IPv6 地址不需要关闭 IP 校验。解析失败时先核对版本、日志格式与字段分隔符。
  2. XFF 中 IPv6 带端口时(如 [2001:db8::1]:443),应先规范化为纯地址或由可信代理解析后写入日志。关闭校验不等于正确处理端口;用少量 IPv4、IPv6 和多级代理样本核对实际结果。

经验总结

  1. %x 是日期时间组合字段,不是 XFF —— 本次失败的根因。
  2. XFF 提取必须用 ~h 波浪号语法,取逗号分隔列表的第一个 IP。
  3. log-format 必须与日志逐字段对齐,多余字段用 %^ 跳过,否则字段错位导致整体解析失败。
  4. IPv6 本身不要求关闭 IP 校验,XFF 也不自动可信。
  5. 排查方法:先 goaccess --log-format=... -f access.log(不带 -o)直接观察解析是否报错,再生成 HTML 报表。

一个更通用的教训:工具的 format specifier 长得像什么不重要,手册里写它是什么才重要。%x 看起来”像是 extension/extra”的东西,实际是日期时间——这种”望文生义”的坑在日志分析工具里非常常见。

参考资料