lanproxy内网穿透实战:纯Java TCP隧道搭建指南
发布时间:2026/9/17 1:24:25 锦皓数字建站

1. 项目概述为什么用 lanproxy 做内网穿透而不是直接抄个 ngrok 教程lanproxy 是一个纯 Java 编写的轻量级内网穿透代理工具核心目标就一件事让没有公网 IP 的设备比如你家里的树莓派、测试服务器、本地开发环境、甚至一台 Windows 笔记本能被外网稳定访问。它不依赖第三方云服务所有流量都走你自己的服务器——这意味着你完全掌控连接路径、日志、权限和数据流向。这和 ngrok、樱花、花生壳这类“开箱即用但黑盒运行”的方案有本质区别前者是工具后者是服务。我第一次接触 lanproxy 是在给客户部署一套工业 Modbus TCP 数据采集系统时。现场 PLC 只能接内网客户想用手机 App 实时看温度曲线又不愿把整个工控网段暴露到公网。当时试过 frp但客户服务器是 CentOS 7 最小化安装glibc 版本太老frp 二进制报错也试过 ngrok但免费版带宽限制严、域名随机、HTTPS 证书要自己配调试半天连不上。最后换 lanproxy从下载到跑通只用了 22 分钟——Java 环境客户服务器上本来就有Tomcat 在跑config.properties 改三行服务端启动客户端一配手机浏览器直接输入http://yourdomain.com:8080就看到实时数据页了。它的底层逻辑非常干净服务端server监听一个公网 TCP 端口比如 49001客户端client通过长连接主动连过去建立隧道当外部请求打到 server 的某个映射端口比如 8080server 就把数据包原样转发给 clientclient 再发给本地服务如 localhost:8080 的 Spring Boot 应用。整个过程不经过任何中间节点没有 DNS 解析跳转也没有 TLS 加密层干扰——这对调试 Modbus TCP、HTTP 接口、甚至 SSH 连接特别友好。你抓包看到的就是纯粹的 TCP 流三次握手、数据传输、四次挥手清清楚楚不像某些代理会在应用层加一层封装导致tcpdump抓出来全是加密乱码。关键词里反复出现的tcp和java不是凑数的。lanproxy 的通信协议就是裸 TCP没用 HTTP 封装也没用 WebSocket 中转所以它天然支持所有基于 TCP 的协议HTTP/HTTPS、SSH、RDP、MySQL、Redis、Modbus TCP、甚至自定义的私有协议。而 Java 实现决定了它跨平台极强——Windows Server、CentOS、Ubuntu、Debian、macOS只要装了 JRE 8扔进去就能跑不用编译不用装额外依赖。config.properties文件就是全部配置入口没有 YAML、JSON 或 TOML没有 CLI 参数嵌套改完保存重启即可生效。这种“少即是多”的设计恰恰是它在嵌入式、工控、教育实验等对稳定性要求高、运维能力弱的场景里活下来的原因。适合谁用不是给只想“5 分钟搞定微信公众号回调调试”的人准备的。它是给那些需要长期稳定运行、要审计每条连接、要对接自有认证系统、或者必须满足等保/工控安全规范的人准备的。比如运维工程师要远程维护几十台分布在不同厂区的 Linux 终端Java 开发者在本地写接口需要让测试同事用真实域名访问自动化集成中Jenkins 要调用内网 Jenkins Slave 的 API学校实验室用树莓派做物联网网关学生用公网地址查传感器数据工业现场用 Modbus TCP 读取 PLC 数据上位机部署在云服务器上。如果你只是临时测个接口ngrok 确实更快但如果你要写进交付文档、放进生产 checklist、或者明年还要维护lanproxy 的可控性、可审计性和零依赖性会省下你至少 3 次深夜救火的时间。2. 核心架构与选型逻辑为什么不是 frp、ngrok 或自建 WebSocket 隧道lanproxy 的架构看似简单但每个设计选择背后都有明确的取舍。理解这些“为什么”才能避开后续踩坑。2.1 服务端-客户端双进程模型不玩单点故障那一套很多新手以为内网穿透就是“一个程序搞定”结果装完 frp server 发现内存飙到 2G再一看日志全是failed to start xray core或app/proxyman/inbound: failed to listen tcp on 108——其实是 frp 的 inbound manager 启动失败而错误信息藏在子模块里排查起来像大海捞针。lanproxy 把服务端proxy-server和客户端proxy-client彻底拆开各自独立进程、独立日志、独立配置。server 只干一件事维持连接池、转发数据包、管理 client 注册client 只干一件事连 server、注册端口映射、收发本地服务流量。两者之间用最简 TCP 协议通信没有状态同步、没有心跳协商、没有重连退避算法——client 断了server 就删掉这个 tunnelclient 重连server 就新建一条通道。这种“无状态”设计让故障隔离变得极其清晰server 日志报错一定是网络或端口问题client 日志报错一定是本地服务没起来或者 config.properties 里填错了本地端口。我见过最典型的误操作是有人把 server 和 client 打包成一个 jar在同一台机器上跑。结果 client 去连127.0.0.1:49001server 监听0.0.0.0:49001看着启动成功实际根本连不上——因为 loopback 地址和通配地址在 Linux socket 层是两回事。后来我把 client 单独部署到另一台虚拟机问题立刻消失。这就是架构分离带来的第一重红利强制你思考网络拓扑。2.2 纯 TCP 协议栈拒绝 HTTP 封装和 TLS 中间层ngrok 默认走 HTTPS 隧道所有流量先被加密、再被封装进 HTTP POST 请求体server 端解密、解包、再转发。好处是能过企业防火墙坏处是你永远看不到原始 TCP 包。比如调试 Modbus TCP你用 Wireshark 抓 server 公网口看到的全是 TLS record根本没法分析00 00 00 06 00 01 00 00 00 01这种标准 ADU而 lanproxy 抓包一眼就能确认三次握手正常、PUSH ACK 流畅、FIN 包有序说明 Modbus 客户端和服务端通信完全 OK问题出在 client 配置的本地端口填成了5020而不是502。更关键的是TCP 长连接支持。HTTP 是短连接默认 keep-alive 时间有限频繁重连对 IoT 设备很不友好。lanproxy 的 client 和 server 之间维持的是真正的 TCP 长连接只要网络不断连接就一直活着。我们实测过client 连上 server 后拔掉网线 30 秒再插回连接自动恢复本地服务完全无感知而 ngrok 免费版超过 30 秒断连就会分配新域名前端必须刷新页面。2.3 Java 实现不是为了炫技而是为了“一次编译处处运行”有人问“为什么不用 Go 写Go 不是更轻量”——确实Go 编译出的二进制更小但 lanproxy 的 Java 选择解决的是另一个维度的问题JVM 生态的成熟度和可观测性。Java 的jstat、jstack、jmap工具链能让你在客户服务器上直接诊断内存泄漏、线程阻塞、GC 飙升。我们曾遇到 client 进程 CPU 占用 99%jstack一导出线程栈发现是SocketInputStream.read()卡死在readBytes根源是本地服务一个老旧的 Java RMI 服务返回了超大响应体client 缓冲区溢出没处理好。如果是 Go你得学 pprof、还得编译带 debug info 的版本而 Java一行命令就定位。另外Java 的 classpath 机制让定制化变得极其简单。比如客户要求 client 启动时读取数据库里的 token而不是写死在 config.properties 里——你只需要写个TokenProvider接口实现类放在lib/下改一行client.classloader配置就行不用动源码、不用重新编译。这种扩展能力在 frp 里得改 C 代码、重新 make在 ngrok 里对不起闭源。2.4 config.properties配置即文档拒绝魔法参数对比 frp 的frpc.iningrok 的ngrok.ymllanproxy 的config.properties就像一张白纸# server 配置 server.port49001 server.sslfalse server.ssl.key-store-path server.ssl.key-store-password server.ssl.key-password # client 配置 client.iddev-web client.server.hostyour-server-ip client.server.port49001 client.server.sslfalse client.local.http.port8080 client.local.https.port8443 client.local.tcp.port22,3306,502 client.local.tcp.namessh,mysql,modbus没有 section、没有 hierarchy、没有 inheritance。所有 key 都是 flat 的命名直白client.local.tcp.port就是本地 TCP 端口列表值类型明确port 是整数name 是字符串用逗号分隔。这种设计带来两个硬性好处一是配置校验简单——启动时读取 properties遇到非法 port 就抛 NumberFormatException错误堆栈直接指向哪一行二是 IDE 友好——IntelliJ 识别.properties文件改 key 时自动提示不会像 YAML 那样缩进错一位就整个文件失效。提示client.local.tcp.port和client.local.tcp.name必须严格一一对应。比如你写了502,3306那name就必须是modbus,mysql顺序不能错。否则 client 注册时会把 Modbus 流量转发到 MySQL 端口导致协议错乱。这是新手最常踩的坑不是 bug是设计使然——lanproxy 把“端口-服务名”映射关系交给用户显式声明避免自动推断带来的歧义。3. 实操全流程从零开始搭建含 CentOS 防火墙、Java 安装、端口冲突排查下面带你完整走一遍一台全新的 CentOS 7 服务器公网 IP203.101.222.100一台本地 Mac内网 IP192.168.1.105目标是让 Mac 上的localhost:8080Spring Boot 应用能被外网通过http://203.101.222.100:8080访问。全程命令可复制粘贴我会标注每一行的意图和潜在雷区。3.1 服务端部署CentOS 7 OpenJDK 11 防火墙放行首先确认 Java 环境。CentOS 7 默认带 OpenJDK 1.8但 lanproxy 推荐 11因用到了HttpClient新 API# 查看当前 Java 版本 java -version # 如果是 1.8升级到 11推荐使用 yum 安装避免手动配置 PATH sudo yum install java-11-openjdk-devel -y # 验证 java -version # 输出应为 openjdk version 11.0.x下载 lanproxy server以 v2.0.1 为例GitHub Release 页面找最新版# 创建工作目录 mkdir -p /opt/lanproxy/server cd /opt/lanproxy/server # 下载注意替换为实际 URL wget https://github.com/ffay/lanproxy/releases/download/v2.0.1/proxy-server-2.0.1.jar # 创建配置文件 touch config.properties编辑config.properties关键项只有 3 行# 监听公网所有地址的 49001 端口不要写 127.0.0.1 server.port49001 # 关闭 SSL测试阶段先关后面再配 server.sslfalse # 日志级别设为 INFO避免 DEBUG 日志刷屏 log.levelINFO启动 server# 后台运行日志输出到 proxy-server.log nohup java -jar proxy-server-2.0.1.jar --config config.properties proxy-server.log 21 # 查看是否启动成功 tail -f proxy-server.log # 正常输出应包含[INFO] [ServerBootstrap] Server started on port 49001现在卡点来了CentOS 7 默认开启 firewalld49001 端口是封死的。很多人到这里就停了ping 得通telnet 49001 却超时。正确打开方式# 永久开放 49001 端口TCP sudo firewall-cmd --permanent --add-port49001/tcp # 重载防火墙规则 sudo firewall-cmd --reload # 验证 sudo firewall-cmd --list-ports # 应看到 49001/tcp注意centos防火墙开放tcp端口配置文件这个热词背后是很多人误改/etc/firewalld/zones/public.xml手动加port标签。这极危险——firewalld 会覆盖该文件下次 reload 就丢失。必须用firewall-cmd命令操作它会自动更新配置并持久化。3.2 客户端配置Mac 上跑 client映射本地 8080 端口在 Mac 上下载 client# 创建目录 mkdir -p ~/lanproxy/client cd ~/lanproxy/client # 下载 client jar同 server 版本 wget https://github.com/ffay/lanproxy/releases/download/v2.0.1/proxy-client-2.0.1.jar # 创建 client 配置 touch config.propertiesconfig.properties内容# client 唯一标识server 用它区分不同 client client.idmy-mac-dev # server 公网 IP 和端口 client.server.host203.101.222.100 client.server.port49001 # 关闭 SSL和服务端保持一致 client.server.sslfalse # 本地要穿透的服务HTTP 服务在 8080 client.local.http.port8080 # 可选如果本地还有 HTTPS 服务放开下面这行 # client.local.https.port8443 # TCP 端口映射这里先空着后面 Modbus 用 client.local.tcp.port client.local.tcp.name启动 client# 后台运行 nohup java -jar proxy-client-2.0.1.jar --config config.properties proxy-client.log 21 # 查看日志 tail -f proxy-client.log # 成功标志[INFO] [ClientBootstrap] Client connected to server successfully此时server 日志会打印[INFO] [ServerHandler] Client my-mac-dev registered, local http port: 8080验证穿透是否生效# 在 Mac 上启动一个测试服务Python 快速起 echo Hello from LAN! index.html python3 -m http.server 8080 # 在任意外网机器比如你的手机浏览器访问 # http://203.101.222.100:8080 # 应看到 Hello from LAN!3.3 处理经典报错error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address这个错误不是 lanproxy 的错而是你本地已有程序占用了目标端口。比如你 Mac 上开了 Docker Desktop它默认监听127.0.0.1:11434Docker 的 Kubernetes API 端口或者 IntelliJ IDEA 的内置 HTTP 服务占了8080。排查步骤# 查看哪个进程在用 8080 lsof -i :8080 # 输出类似 # COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME # java 1234 user 56u IPv6 0x1234567890abcd 0t0 TCP *:http-alt (LISTEN) # 杀掉它 kill -9 1234更彻底的办法改 client 配置把client.local.http.port换成未被占用的端口比如8081然后本地服务也起在8081。记住lanproxy 不帮你找空闲端口它只忠实地把流量转发到你指定的localhost:x。3.4 进阶实战同时穿透 HTTP Modbus TCP502 端口现在把本地 Modbus TCP 服务比如一个模拟 PLC也透出去。假设它运行在localhost:502。修改 client 的config.properties# 启用 TCP 映射 client.local.tcp.port502 client.local.tcp.namemodbus # 注意HTTP 和 TCP 可以共存但端口不能重复 client.local.http.port8080重启 clientkill -9 $(ps aux | grep proxy-client | grep -v grep | awk {print $2}) nohup java -jar proxy-client-2.0.1.jar --config config.properties proxy-client.log 21 server 日志会新增[INFO] [ServerHandler] Client my-mac-dev registered, local tcp ports: [502]外网测试 Modbus TCP# 在另一台 Linux 机器上用 modbus-tcp-test 工具 # 安装Ubuntu sudo apt install python3-pip pip3 install pymodbus # 测试读取寄存器功能码 03地址 0数量 10 python3 -c from pymodbus.client import ModbusTcpClient client ModbusTcpClient(203.101.222.100, port502) result client.read_holding_registers(0, 10, slave1) print(result.registers) client.close() # 如果返回正常数值说明穿透成功实操心得Modbus TCP 对连接稳定性要求极高。我们曾发现 client 日志里频繁出现Connection reset by peer排查后是 client 所在 Mac 的 WiFi 睡眠策略导致 TCP keepalive 被中断。解决方案是在 Mac 终端执行sudo pmset -a tcpkeepalive 1这个命令强制系统发送 TCP keepalive 包间隔 1 秒避免路由器或防火墙主动断连。这是 lanproxy 文档里没写的但工控现场必备。4. 高阶配置与避坑指南SSL 加密、多 client 管理、Java 线程等待优化4.1 为 server 添加 SSL用 Lets Encrypt 免费证书虽然 lanproxy 支持 SSL但它不提供证书申请功能必须你自己搞定。推荐用 CertbotLets Encrypt 官方客户端# 在 server 服务器上安装 certbot sudo yum install certbot -y # 申请证书需域名解析到 server IP sudo certbot certonly --standalone -d your-domain.com # 证书位置通常在 /etc/letsencrypt/live/your-domain.com/ # 生成 keystoreJava 需要 jks 格式 sudo openssl pkcs12 -export -in /etc/letsencrypt/live/your-domain.com/fullchain.pem -inkey /etc/letsencrypt/live/your-domain.com/privkey.pem -out /opt/lanproxy/server/cert.p12 -name lanproxy -CAfile /etc/letsencrypt/live/your-domain.com/chain.pem -caname root # 设置密码记下来后面要用 # 转成 jks sudo keytool -importkeystore -srckeystore /opt/lanproxy/server/cert.p12 -srcstoretype pkcs12 -destkeystore /opt/lanproxy/server/keystore.jks -deststoretype jks -alias lanproxy修改 server 的config.propertiesserver.ssltrue server.ssl.key-store-path/opt/lanproxy/server/keystore.jks server.ssl.key-store-passwordyour-keystore-password server.ssl.key-passwordyour-key-password # SSL 端口建议换一个比如 49002避免和非 SSL 端口混用 server.port49002重启 server。此时 client 必须同步开启 SSLclient.server.ssltrue client.server.hostyour-domain.com # 必须用域名不能用 IP client.server.port49002注意java线程等待都完成这个热词其实关联到 SSL 握手超时问题。如果 client 启动时卡在Connecting to SSL server...大概率是证书域名不匹配或 server 的key-store-password填错了。用openssl s_client -connect your-domain.com:49002测试能打印出证书链就说明 SSL 配置成功。4.2 管理多个 client用 client.id 实现权限隔离一个 server 可以同时服务多个 client靠client.id区分。比如client.idprod-db穿透生产 MySQL3306client.idtest-web穿透测试环境 Web8080client.idfactory-plc穿透工厂 PLC502server 不做鉴权但你可以用client.id做日志过滤和流量监控。比如# 查看 prod-db 的连接日志 grep prod-db /opt/lanproxy/server/proxy-server.log # 统计各 client 的连接次数 awk /Client.*registered/ {print $6} /opt/lanproxy/server/proxy-server.log | sort | uniq -c更进一步可以写个简单的 shell 脚本根据client.id动态生成 iptables 规则限制每个 client 的最大并发连接数防止某个 client 异常占满 server 资源。4.3 Java 性能调优避免OutOfMemoryError和线程阻塞lanproxy server 默认 JVM 参数很保守。在高并发场景比如同时接入 50 client可能触发 GC 频繁或 OOM。启动时加 JVM 参数nohup java -Xms512m -Xmx2g -XX:UseG1GC -XX:MaxGCPauseMillis200 -jar proxy-server-2.0.1.jar --config config.properties proxy-server.log 21 解释-Xms512m -Xmx2g初始堆 512MB最大 2GB避免频繁扩容-XX:UseG1GC启用 G1 垃圾收集器适合大堆内存-XX:MaxGCPauseMillis200目标 GC 暂停时间 200ms平衡吞吐和延迟。client 端如果要穿透大量 TCP 连接比如 100 个 Modbus 设备也要调nohup java -Xms256m -Xmx1g -jar proxy-client-2.0.1.jar --config config.properties proxy-client.log 21 实操心得java面试八股文里常考的“线程池参数怎么设”在这里有直接应用。lanproxy client 内部用Executors.newCachedThreadPool()处理本地连接但这个线程池无上限。我们曾遇到 client 进程创建了 2000 线程最终 OOM。解决方案是 fork 源码把线程池改成newFixedThreadPool(50)并加监控——这不是 bug是设计者留给专业用户的定制入口。4.4 常见问题速查表问题现象可能原因排查命令解决方案client 启动报Connection refusedserver 没启动或防火墙没放行telnet 203.101.222.100 49001检查 server 进程、firewalld、server 日志server 日志显示Client xxx registered但外网访问 8080 超时client 的client.local.http.port填错或本地服务没起来curl -v http://localhost:8080在 client 机器上确认本地服务监听0.0.0.0:8080不是127.0.0.1:8080外网能访问 HTTP但 Modbus TCP 连不上client.local.tcp.port和client.local.tcp.name数量不匹配grep tcp port proxy-client.log严格保证逗号分隔的端口数 服务名数client 日志频繁Read timed out网络不稳定或 server 的server.port被其他程序占用netstat -tuln | grep :49001检查端口占用增加 client 的client.connect.timeout需改源码server 启动后内存持续增长client 连接未正常关闭server 未释放资源jmap -histo:live pid升级到 v2.0.1该版本修复了连接泄漏最后分享一个小技巧把 client 配置做成模板用 Ansible 或 Shell 脚本批量部署。比如# deploy-client.sh CLIENT_ID$1 SERVER_IP$2 LOCAL_PORT$3 sed s/client.id.*/client.id$CLIENT_ID/ s/client.server.host.*/client.server.host$SERVER_IP/ s/client.local.http.port.*/client.local.http.port$LOCAL_PORT/ \ template.properties config.properties java -jar proxy-client.jar --config config.properties 这样给 100 台设备部署只需./deploy-client.sh plc-001 203.101.222.100 502一行命令。我在实际使用中发现lanproxy 最大的价值不是“能穿透”而是“穿透得透明”。当你需要向客户解释“为什么 Modbus 读不到数据”你可以拿出三张图client 抓包显示发出了00 00 00 06 00 01 00 00 00 01、server 抓包显示收到了同样字节、外网抓包显示返回了00 00 00 06 00 01 00 00 00 02 00 00——三张图连起来问题一定在 PLC 侧而不是网络侧。这种确定性是任何黑盒代理工具都无法提供的。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。