首页 / 资讯中心 / 文章详情

正反向隔离装置下的TCP/UDP穿透实现:单向摆渡如何构建透明隧道

正反向隔离装置下的TCP/UDP穿透实现:单向摆渡如何构建透明隧道 ★ FEATURED ARTICLE
简介面向电力或工控网络中的正反向隔离装置场景这份Demo演示了如何在隔离网关两侧实现TCP与UDP的双向穿透。资源在原有TCP穿透能力基础上新增了UDP透传逻辑适合从事网络隔离、安全网关开发的工程师以及需要验证穿透行为的测试人员参考。压缩包共11个文件、约1.68MB核心包括2个CPP源码文件分别对应代理与测试端、2个INI配置文件、可执行的代理程序与TCP/UDP测试程序另附txt说明文档与pptx整体方案文档便于快速理解模块职责和启动流程。内容预览中可见glzz、xb_gl_proxy、xb_gl_proxy_out等目录源码、配置、可执行文件分层放置结构清晰。已有1250人学习下载对研究隔离装置数据通道或进行二次开发具有直接借鉴价值。1. 正反向隔离装置的tcp/udp穿透_demo先搞懂它解决什么问题在做内外网数据交换时最头痛的场景不是防火墙而是物理隔离装置。两台设备之间没有逻辑通道TCP握手包根本过不去业务却要求内网客户端能访问外网服务。这个demo做的就是一件事把TCP和UDP业务流量翻译成隔离装置认得的单向报文请求走正向通道响应走反向通道模拟出一条应用层透明隧道。对要接隔离装置做集成的开发它是一份可以直接参考的穿透实现对研究tcpip网络安全的人它清楚展示了隔离装置“拦得住连接、拦不住封装”的本质。整个项目不依赖特殊硬件自带模拟层普通电脑上就能跑通。2. 架构设计两条单工通道如何拼成一条全双工链路2.1 为什么TCP直连永远过不去隔离装置的核心逻辑是单向摆渡。正向隔离只允许内网侧的数据单向流向外网侧反向隔离允许外网侧数据单向流向内网侧两条通道物理上互不相通。TCP协议本身是双向的握手要发SYN、收SYNACK数据段要逐包确认这些回程流量在隔离装置里没有返回路径连接自然建立不起来。既然TCP过不去常见的做法是改业务协议把请求和响应拆成两个独立报文分别走正向和反向装置。这个demo的思路更进一步它保留业务的TCP/UDP语义在两端各放一个代理代理负责把双向TCP流量拆成正向和反向两股单向流对业务进程来说连接的仍然是本机端口完全无感知。2.2 整体链路拆解数据流到内网代理后代理按最大包长把TCP字节流切成数据块每块加上自定义包头塞进UDP报文发给正向隔离装置的内网侧接口。正向隔离装置只认这台内网侧主机发给外网侧主机的UDP包会原样转发过去。外网侧代理收到UDP包后按包头里的序号还原TCP字节流再与真正的外网目标服务建立TCP连接。这样一来内网到外网方向就走通了。响应的路径完全对称只是换了装置。外网目标返回的TCP数据在外网侧代理被切成块、封装成UDP包通过反向隔离装置发给内网侧代理内网代理把包还原成TCP字节流写回内网客户端的连接。两个方向的单向通道在应用层拼接对业务呈现的效果和一个普通TCP代理没有区别。2.3 目录结构与模块职责isolation-tcp-udp-penetration-demo/ ├── config.yaml # 全局配置装置地址、监听端口、目标地址 ├── run.py # 一键启动入口负责拉起内/外网代理 ├── core/ │ ├── packet.py # 隔离报文的封装与解析 │ ├── forward_channel.py # 正向通道TCP流切片封装并发往对端 │ ├── reverse_channel.py # 反向通道内网侧轮询拉取数据 │ └── udp_tunnel.py # UDP业务的透传处理 ├── gateway/ │ ├── inner_side.py # 内网侧代理主程序 │ └── outer_side.py # 外网侧代理主程序 ├── sim/ │ └── simulate_gap.py # 模拟隔离装置只管单向转发丢弃反向包 └── test/ ├── tcp_echo_server.py # 外网侧回显服务 ├── tcp_client.py # 内网侧客户端 ├── udp_sender.py └── udp_receiver.pycore目录是穿透的核心逻辑gateway目录是两端入口sim目录提供隔离装置模拟器test目录是验证用的对端程序。这个分层的好处是换真实隔离装置时只改sim层和配置core和gateway不需要动。2.4 隔离报文的包头设计自定义包头是整个穿透协议的关键。包头的设计遵循两个原则足够短以避免浪费UDP有效载荷字段足够全以保证对端能还原数据。下面是core/packet.py里实际使用的字段布局字段长度说明魔数2字节固定0xA5A5用来过滤隔离装置串入的脏包通道号1字节1表示正向2表示反向隔离两条链路序号2字节大端序接收端按它排序还原TCP流分段索引2字节一个TCP数据块被切成多个UDP包时的序号分段总数2字节收到全部段后开始组装数据长度2字节payload实际字节数CRC324字节对包头加payload计算防止中间设备改包总包头加CRC固定为15字节单个UDP报文最大长度控制在1415字节以内比常见UDP的1472字节上限小经过隔离装置时基本不会被IP分片。3. 核心模块实现从包封装到双向穿透的代码走读3.1 包的封装与解析core/packet.py封装函数负责把一段业务数据切成多个隔离报文每个报文带独立的包头和CRC。注意分段索引从1开始因为0留给后续扩展做握手控制包。import struct import zlib MAGIC 0xA5A5 CHANNEL_FORWARD 0x01 CHANNEL_REVERSE 0x02 MAX_PAYLOAD 1400 def build_packet(channel, seq, data): packets [] total (len(data) MAX_PAYLOAD - 1) // MAX_PAYLOAD for idx in range(total): offset idx * MAX_PAYLOAD chunk data[offset:offset MAX_PAYLOAD] header struct.pack(!HBHHHH, MAGIC, channel, seq, idx 1, total, len(chunk)) crc zlib.crc32(header chunk) 0xFFFFFFFF packets.append(header struct.pack(!I, crc) chunk) return packets这里的!HBHHHH是按大端序打包6个字段对应魔数、通道号、序号、分段索引、分段总数、数据长度。之所以强制大端序是因为隔离装置两端可能跑在不同架构的机器上小端序的序号在跨平台传输后会被倒过来读排序直接乱掉。CRC32的计算范围覆盖包头和payload接收端只要发现一个包校验不过就丢弃防止脏包污染还原后的字节流。解析函数是封装的逆过程先校验长度、再校验魔数、最终校验CRC全部通过才返回解析结果。任何一步失败都返回None由上层调用方记录日志并丢弃。def parse_packet(buf): if len(buf) 15: return None magic, channel, seq, seg_idx, seg_total, data_len struct.unpack(!HBHHHH, buf[:11]) if magic ! MAGIC: return None crc_recv struct.unpack(!I, buf[11:15])[0] crc_calc zlib.crc32(buf[:11] buf[15:15 data_len]) 0xFFFFFFFF if crc_recv ! crc_calc: return None return { channel: channel, seq: seq, seg_index: seg_idx, seg_total: seg_total, data: buf[15:15 data_len] }处理脏包时有个细节如果魔数不对就直接丢弃不再做CRC计算因为CRC本身耗CPU在高速转发场景下能省一点是一点。而验证CRC会覆盖包头的通道号通道号不对的包会被拒绝这保证正向通道的包不会串到反向链路里去。3.2 正向通道TCP数据如何打进UDP包正向通道跑在内网侧代理上。它的任务很简单监听本机TCP端口接到客户端连接后把连接上的字节流切成隔离报文发到隔离装置的内网侧接口。接收端在外网侧反向操作把隔离报文还原成TCP字节流。def forward_tcp_to_gap(local_listen_port, gap_inner_addr): server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, local_listen_port)) server.listen(16) while True: conn, _ server.accept() threading.Thread(target_handle_forward_conn, args(conn, gap_inner_addr), daemonTrue).start() def _handle_forward_conn(conn, gap_inner_addr): udp socket.socket(socket.AF_INET, socket.SOCK_DGRAM) seq 0 while True: data conn.recv(4096) if not data: break for pkt in build_packet(CHANNEL_FORWARD, seq, data): udp.sendto(pkt, gap_inner_addr) seq (seq 1) 0xFFFF udp.close() conn.close()每次读取最多4096字节的TCP数据能凑满3个1400字节的分段包。这里把读取和发送放在一个线程里顺序执行避免多线程并发写入同一个UDP socket时出现包顺序乱掉的情况。序号在0到65535之间循环超过就回绕接收端通过CRC和分段总数可以识别乱序和丢包。外网侧代理收到这些UDP包后需要把同一序号下的所有分段拼成一个完整TCP数据块再按序写入与目标服务建立的TCP连接。这个组装逻辑的核心是拿seq做分组键seg_index做排序键攒够seg_total个分段后合并。def _handle_gap_packet(pkt, tcp_conn, buffer): key (pkt[channel], pkt[seq]) if key not in buffer: buffer[key] {segments: {}, total: pkt[seg_total]} buffer[key][segments][pkt[seg_index]] pkt[data] if len(buffer[key][segments]) pkt[seg_total]: ordered [buffer[key][segments][i] for i in range(1, pkt[seg_total] 1)] tcp_conn.sendall(b.join(ordered)) del buffer[key]buffer是每个TCP连接独立维护的字典键是通道号和序号的组合。因为UDP包可能先发后到这里通过等待分段凑齐再写入TCP天然解决了乱序问题。如果某个序号的分段长时间凑不齐说明网络丢包了上层会有一个定时器清理超时的buffer条目同时断开对应的TCP连接让业务层走重连逻辑。3.3 反向通道内网侧主动轮询拉取数据反向隔离的复杂性在于方向。外网侧的响应数据不能主动推到内网只能由内网侧定时发拉取请求隔离装置把排队的数据包送回来。这个demo采用固定间隔轮询内网代理每50毫秒向反向装置的内网侧接口发送一个UDP拉取包外网侧代理收到拉取包后从自己的待发队列里弹出一个包发回。def reverse_poll(poll_interval_ms, gap_inner_addr): udp socket.socket(socket.AF_INET, socket.SOCK_DGRAM) while True: udp.sendto(bPOLL, gap_inner_addr) udp.settimeout(poll_interval_ms / 1000.0 0.01) try: data, _ udp.recvfrom(2048) except socket.timeout: time.sleep(poll_interval_ms / 1000.0) continue pkt parse_packet(data) if pkt and pkt[channel] CHANNEL_REVERSE: dispatch_reverse_packet(pkt)轮询间隔是一个需要权衡的参数。50毫秒在局域网环境能拿到约20RPS的单请求吞吐对验证demo完全够用。如果隔离装置对UDP报文的流量有限速策略比如每秒只允许20个报文这个间隔就要调到200毫秒以上否则拉取响应会被装置静默丢弃客户端表现为长时间收不到响应。外网侧代理只需要把每次TCP写入产生的隔离报文追加到队列尾部拉取包来了就发队列头部的包这样响应的顺序和TCP原始数据的顺序保持一致。3.4 UDP穿透无连接业务的直接透传UDP本身没有连接状态不需要像TCP那样切片和流控。内网侧UDP代理收到一个完整UDP数据报后把它封装成一个隔离报文发给正向装置对端解析后直接发给目标UDP服务。反向路径同理但每个数据报要被当做一个独立消息处理不能拆成多段。def udp_passthrough(local_udp_port, gap_inner_addr): ingress socket.socket(socket.AF_INET, socket.SOCK_DGRAM) ingress.bind((0.0.0.0, local_udp_port)) while True: data, client_addr ingress.recvfrom(2048) seq gen_udp_seq(client_addr) for pkt in build_packet(CHANNEL_FORWARD, seq, data): ingress.sendto(pkt, gap_inner_addr)UDP穿透需要维护一张地址映射表记录每个内网客户端对应的外网目标地址。因为一个客户端的UDP数据报可能来自不同源端口但目标服务和端口是固定的。这里seq由客户端地址哈希生成便于对端代理按客户端身份反向识别响应该发给谁。UDP业务不要求可靠传输所以丢包直接丢掉超时清理映射表条目的任务周期执行。4. 运行起来隔离穿透demo的完整启动流程4.1 准备环境与网络拓扑demo最少需要三台机器或虚拟机模拟三个网络角色。内网侧运行内网代理和客户端外网侧运行外网代理和目标服务中间的模拟机运行sim/simulate_gap.py扮演正反向隔离装置。如果只有一台电脑也可以用三个容器或用loopback接口加不同端口号代替但模拟机必须单独部署否则隔离装置的单向特性无法体现。部署顺序按这个拓扑规划内网客户端 --TCP-- 内网代理 --UDP(正向)-- 模拟隔离机 --UDP-- 外网代理 --TCP-- 目标服务 内网客户端 --TCP-- 内网代理 --UDP(反向)-- 模拟隔离机 --UDP-- 外网代理 --TCP-- 目标服务模拟隔离机的行为最关键它只转发内网侧接口发来的UDP包到外网侧反向同理凡是方向不对的包一律丢弃。真实隔离装置还带物理单向特性模拟机用iptables也能做到类似效果但demo里用纯Python转发就够了。4.2 修改配置文件config.yaml是唯一需要改的配置文件所有网络地址都集中在这里。forward: gap_inner_addr: 192.168.1.2:7101 gap_outer_addr: 192.168.2.2:7102 inner_listen_port: 9000 outer_target_addr: 192.168.2.10 outer_target_port: 8080 reverse: gap_inner_addr: 192.168.1.3:7103 gap_outer_addr: 192.168.2.3:7104 poll_interval_ms: 50 udp: listen_port: 9001 target_addr: 192.168.2.10 target_port: 9002gap_inner_addr和gap_outer_addr分别是隔离装置内网侧和外网侧的接口地址必须和模拟机绑定网卡时用的一致。内网客户端连接的是inner_listen_port9000端口是内网侧代理的TCP监听端口outer_target_addr是外网侧真正要访问的服务地址可以指向外网代理所在机器的回环地址。4.3 启动模拟机与两端代理先在外网侧机器上启动目标服务和外网代理因为内网客户端的数据随时可能到达外网代理需要在收到设备数据的瞬间能够建立到目标的TCP连接。# 外网侧终端1启动回显服务 python test/tcp_echo_server.py --port 8080 # 外网侧终端2启动外网侧代理 python gateway/outer_side.py --config config.yaml # 模拟隔离机终端启动装置模拟 python sim/simulate_gap.py --config config.yaml # 内网侧终端启动内网侧代理 python gateway/inner_side.py --config config.yaml启动顺序有讲究。隔离模拟机必须早于两端代理启动否则代理第一次sendto时没有对端接收UDP包会直接丢在网络上而且不会报错。内网代理可以最晚启动因为它只是监听端口不会主动发包直到有客户端连上来。这个顺序不是死规定但按这个顺序启动时配置错误暴露得最快。4.4 端到端验证内网客户端连接9000端口发一段字符串回显服务会把相同内容返回客户端能在屏幕上看到完整往返。这是最简单的连通性验证。python test/tcp_client.py --server 127.0.0.1 --port 9000 --payload hello gap如果看到返回的hello gap说明正向通道和反向通道都工作正常。再抓包验证协议头是否正确在外网侧代理机器上执行tcpdump抓UDP 7102端口的报文能看到头部前两个字节是a5 a5这就是自定义包头里的魔数。如果魔数不对说明中间环节有人改包排查方向可以直接锁定在隔离装置的数据整形策略上。UDP透传验证同样简单内网侧跑udp_sender发消息外网侧跑udp_receiver监听9002端口能收到即通过。5. 避坑指南隔离穿透最常见的五个问题与排查手段5.1 大包被隔离装置静默丢弃现象客户端发小数据一切正常发超过2KB的报文时服务端完全收不到抓包能看到UDP包发出去了但目标机器上没有出现。原因很多隔离装置的实际处理逻辑不是按UDP长度上限工作而是按装置内部缓冲区大小限制常见值是1400字节左右。超过这个值的包会被装置内部协议栈直接判定为非法报文不进入转发队列。TCP数据一次recv可能读到几千字节build_packet虽然按1400切段了但个别段恰好超过装置上限。解决把MAX_PAYLOAD从1400调到1200留出余量。这个坑的隐蔽之处在于小数据时不会触发只有跑大文件或高带宽业务时流量瞬间中断我在这上面翻过车最终是逐段缩小包长二分定位出来的。5.2 反向轮询频率太高触发限流现象运行几分钟后客户端偶发超时恢复后又能正常工作且超时时间点没有规律。看内网代理日志发现recvfrom频繁超时。原因反向隔离装置对UDP报文的处理速率有限制有些型号对单来源IP限到每秒30个包。默认50毫秒轮询就是每秒20个拉取包表面没超但加上业务反向报文后整体超过阈值装置开始随机丢弃拉取响应丢几次就触发客户端超时。解决把poll_interval_ms调大到200优先保证业务响应包的顺畅同时给急停场景加一个手动触发按钮需要快速拉取时临时降到100毫秒以内用完立即恢复。限速参数每个型号不一样上线前务必做一次满速测试确认实际阈值。5.3 跨平台部署时魔数校验失效现象两端代理分别部署在ARM和x86机器上小数组件功能正常但CRC校验频繁失败日志里全是parse_packet返回None的记录。原因Python的struct.pack在不同架构上默认对齐规则不同如果某处代码用了非!前缀的格式串或者直接用了int.from_bytes没有指字字节序读出来的魔数就会被字节反转0xA5A5变成0xA5A5不变但多字节序号会被倒序。更常见的坑是CRC计算时header里混入了本地字节序打包的字段。解决统一所有pack和unpack的格式串都带!前缀CRC计算前把header用一个bytes对象缓存避免多次打包产生不一致。我在一次跨平台联调时排查了一整天最后发现是zlib.crc32传入的bytes在拼接顺序上出了问题。5.4 隧道空闲后被装置回收连接现象长时间没有业务流量再发起新请求时第一次必然失败第二次重试就成功。原因隔离装置对空闲会话有老化机制两分钟内没有转发流量的UDP流会被删掉。内网侧代理的UDP socket一直没关但装置侧的路由状态已经没了第一个包会触发装置重新建表而这个建表动作来不及影响当前包于是第一包被丢弃。解决加一个应用层心跳每30秒从内网代理发一个空数据包心跳包只带包头没有payload解析端识别到data_len为0就丢弃不写TCP。这样一来装置的空闲会话永远不会超时也避免了为了保活而频繁重建连接。5.5 模拟环境正常、真实装置环境下丢包率极高现象本地模拟器跑通后接入真实隔离装置调试同样的代码丢包率超过30%且集中在固定时间窗口。原因模拟器的转发是纯内存拷贝真实装置带磁盘缓存和硬件加密处理速率受限。大量小UDP包并发到达时装置来不及处理就丢包。代码层面看是内网代理sendto后没有做任何背压控制一个循环里瞬间把几十个包扔给装置。解决在内网代理的发送线程里加入令牌桶限速每发送一个包后sleep一个极小的时间片让装置来得及处理。这个限速值从10微秒起步逐步调大直到丢包率归零。接入生产环境前这种背压机制是必须加上的否则应用一压测就雪崩。6. 二次开发把穿透demo改造成可用的生产通道6.1 协议升级从单连接到多路复用现在的包头结构里通道号只区分正向和反向多个TCP连接的数据包会共享同一个序号空间接收端会拼错。改造思路是增加一个连接ID字段占用2字节放在通道号之后。内网代理为每个客户端TCP连接分配一个唯一ID连接关闭后回收复用。对端代理用(conn_id, seq)作为组装键这样一个UDP通道就能承载多路TCP业务吞吐量能跟着提升一个量级。6.2 可靠性升级补重传与ACK生产环境的隔离链路丢包率普遍在万分之一到百分之一之间TCP流被封装成UDP后丢包就全丢了上层TCP重传机制根本感知不到。最直接的做法是把包头里的分段索引扩展成滑动窗口发送端缓存已经发送但未确认的包接收端每攒齐一个序号就回一个ACK发送端在超时时间后重发未确认包。考虑到隔离装置延迟通常超过20毫秒超时时间建议设成200毫秒太短会导致重传风暴。6.3 上线前的验证方法每次改造完我都会强制做一轮故障演练先拔掉模拟机的网线再插回看两端代理能否在恢复后自动重连再把目标服务杀掉重启确认TCP连接重建不影响后续包序号最后用超大文件压测跑30分钟观察buffer字典有没有堆积。这些场景在demo里都能模拟不需要真装置就能提前暴露大多数问题。做一个能上线的穿透通道难点从来不在协议设计而在边界场景的覆盖。从那以后我每次改完隔离穿透代码都会强制走一遍断线重连、对端重启、持续压测这三板斧宁可多花一小时也不让现场的故障灯亮起来才后悔。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站