FRP 反代 SSH 后只看到 127.0.0.1?用 go-mmproxy 保留真实客户端 IP
FRP 用来反代 SSH 很方便,但有个典型问题:登录日志里看到的来源地址往往是 127.0.0.1。这样不仅审计不准确,基于真实 IP 的告警和封禁也很难生效。
解决思路并不复杂:让 frpc 在转发时附带 PROXY Protocol v2,再由 go-mmproxy 解析真实客户端地址,并通过透明代理把连接交给 sshd。
数据流
SSH 客户端
↓
公网 frps:12312
↓ FRP 隧道
本机 frpc
↓ PROXY Protocol v2
go-mmproxy:12312
↓ 保留真实源 IP
sshd:22这里的两个 12312 分别位于公网服务器和本地主机,端口号可以不同,只要 frpc 的 localPort 与 mmproxy 的监听端口一致即可。
构建 go-mmproxy 镜像
使用下面的 Dockerfile:
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
RUN apk add --no-cache git
WORKDIR /app
RUN git clone https://github.com/path-network/go-mmproxy .
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w" -o /app/mmproxy .
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/mmproxy .
RUN echo '#!/bin/sh' > /app/start.sh && \
echo '' >> /app/start.sh && \
echo '# 设置路由规则' >> /app/start.sh && \
echo 'if ! ip rule show | grep -Fq "from 127.0.0.0/8 iif lo lookup 123"; then' >> /app/start.sh && \
echo ' ip rule add from 127.0.0.0/8 iif lo table 123' >> /app/start.sh && \
echo 'fi' >> /app/start.sh && \
echo 'ip route replace local 0.0.0.0/0 dev lo table 123' >> /app/start.sh && \
echo '' >> /app/start.sh && \
echo 'if [ -z "$PORT_MAPPINGS" ]; then' >> /app/start.sh && \
echo ' PORT_MAPPINGS="15000:5000"' >> /app/start.sh && \
echo 'fi' >> /app/start.sh && \
echo 'for mapping in $(echo $PORT_MAPPINGS | tr "," " "); do' >> /app/start.sh && \
echo ' listen_port=$(echo $mapping | cut -d: -f1)' >> /app/start.sh && \
echo ' target_port=$(echo $mapping | cut -d: -f2)' >> /app/start.sh && \
echo ' /app/mmproxy -l "0.0.0.0:$listen_port" -4 "127.0.0.1:$target_port" -6 "[::1]:$target_port" &' >> /app/start.sh && \
echo 'done' >> /app/start.sh && \
echo 'wait' >> /app/start.sh && \
chmod +x /app/start.sh
ENTRYPOINT ["/app/start.sh"]构建并推送 AMD64、ARM64 两种架构:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t sstarbucks/go-mmproxy:latest \
--push .使用自己的仓库时,替换镜像名称即可。
Docker Compose 配置
docker-compose.yml:
services:
go-mmproxy:
image: sstarbucks/go-mmproxy:latest
container_name: go-mmproxy
restart: unless-stopped
network_mode: host
cap_add:
- NET_ADMIN
environment:
PORT_MAPPINGS: "12312:22"PORT_MAPPINGS 的格式是“mmproxy 监听端口:sshd 目标端口”。因为使用 host 网络,不需要再配置 ports。NET_ADMIN 用于透明代理和策略路由,不需要把容器扩大为 privileged。
启动容器:
docker compose up -d修改 frpc 配置
本文假设 frpc 容器同样使用 network_mode: host,因此 127.0.0.1:12312 指向宿主机上的 mmproxy。若 frpc 使用 bridge 网络,这个地址会指向 frpc 容器自身,不能直接照搬。
对应的 frpc.toml TCP 代理:
[[proxies]]
name = "ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 12312
remotePort = 12312
transport.proxyProtocolVersion = "v2"关键是 localPort 必须指向 mmproxy,而不是继续直连 sshd 的 22 端口;否则真实 IP 仍然无法传递。
验证结果
先确认策略路由只有一条:
ip rule show | grep 'lookup 123'
ip route show table 123正常输出类似:
32765: from 127.0.0.0/8 iif lo lookup 123
local default dev lo scope host再从外网登录一次并查看日志:
tail -n 20 /var/log/auth.log如果看到下面这样的记录,就说明整条链路已经生效:
Accepted publickey for user from 203.0.113.10 port 27975 ssh2几个注意点
network_mode: host表示容器与宿主机共享网络命名空间,因此容器中的策略路由会直接作用于宿主机。- 启动脚本会先检查规则是否存在,并使用
ip route replace,避免容器重启时不断累积相同规则。 - mmproxy 当前监听
0.0.0.0:12312,应确认本地主机防火墙没有把该端口直接开放给外网。 - 如果要按真实 IP 封禁 SSH 爆破,最直接的位置仍然是公网 frps 的入口端口。
- 公网 SSH 建议使用密钥认证,并关闭 root 密码登录。
参考项目
标签:无
《FRP 反代 SSH 后只看到 127.0.0.1?用 go-mmproxy 保留真实客户端 IP》 © 2026 by 吃蛋的小子 依据 CC BY-SA 4.0 许可协议授权