当前位置: 首页 记笔 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 网络,不需要再配置 portsNET_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

几个注意点

  1. network_mode: host 表示容器与宿主机共享网络命名空间,因此容器中的策略路由会直接作用于宿主机。
  2. 启动脚本会先检查规则是否存在,并使用 ip route replace,避免容器重启时不断累积相同规则。
  3. mmproxy 当前监听 0.0.0.0:12312,应确认本地主机防火墙没有把该端口直接开放给外网。
  4. 如果要按真实 IP 封禁 SSH 爆破,最直接的位置仍然是公网 frps 的入口端口。
  5. 公网 SSH 建议使用密钥认证,并关闭 root 密码登录。

参考项目

标签:无

FRP 反代 SSH 后只看到 127.0.0.1?用 go-mmproxy 保留真实客户端 IP》 © 2026 by 吃蛋的小子 依据 CC BY-SA 4.0 许可协议授权

你的评论