如何用TIdMappedPortTCP实现按客户端IP分流至不同目标?
解决方案:基于TIdMappedPortTCP的动态IP映射代理
核心问题分析
你之前的尝试都没命中关键:修改AContext.Binding是操作客户端的连接参数,而非TIdMappedPortTCP要转发到的后端服务器参数。TIdMappedPortTCP的工作逻辑是:接受客户端连接后,主动创建一个到后端服务器的新连接,你需要修改的是这个新连接的目标地址,而非客户端侧的连接信息。
正确实现方式
方法1:利用OnCreateMappedClient事件(推荐)
新版本Indy的TIdMappedPortTCP提供了OnCreateMappedClient事件,该事件在创建后端转发连接之前触发,正好可以在这里根据客户端IP动态设置目标地址。
代码示例:
procedure TMapperApplication.MapperCreateMappedClient(Sender: TIdMappedPortTCP; var VDestIP: string; var VDestPort: Integer; AClientContext: TIdContext); var IP: string; I: Integer; begin IP := AClientContext.Binding.PeerIP; // 根据客户端IP匹配目标服务器 for I := 0 to High(Servers) do begin if IP = Servers[I].Source then begin VDestIP := Servers[I].Host; VDestPort := Servers[I].Port; // 输出日志 TConsole.TextColor := TConsoleColor.Yellow; TConsole.Write('Mapped connection from ' + IP + ' to '); TConsole.ResetStyle; TConsole.WriteLn(Format('%S:%D', [VDestIP, VDestPort])); Exit; end; end; // 处理默认映射 if EnableDefault then begin // 建议将数组最后一项设为默认服务,避免越界 VDestIP := Servers[High(Servers)].Host; VDestPort := Servers[High(Servers)].Port; TConsole.WriteLn('Mapped connection from ' + IP + ' to default host.'); Exit; end; // 无匹配时拒绝连接 VDestIP := ''; // 设置为空则TIdMappedPortTCP会自动断开客户端连接 TConsole.TextColor := TConsoleColor.Red; TConsole.WriteLn('Connection from ' + IP + ' dropped: No mapping found.'); TConsole.ResetStyle; end;
方法2:自定义继承TIdMappedPortTCP
如果你的Indy版本没有OnCreateMappedClient事件,可以自定义组件重写CreateMappedClient方法:
type TCustomMappedPortTCP = class(TIdMappedPortTCP) protected function CreateMappedClient(AClientContext: TIdContext): TIdTCPConnection; override; end; function TCustomMappedPortTCP.CreateMappedClient(AClientContext: TIdContext): TIdTCPConnection; var IP: string; I: Integer; DestIP: string; DestPort: Integer; begin IP := AClientContext.Binding.PeerIP; DestIP := ''; DestPort := 0; // 匹配目标服务器逻辑 for I := 0 to High(Servers) do begin if IP = Servers[I].Source then begin DestIP := Servers[I].Host; DestPort := Servers[I].Port; Break; end; end; // 应用默认映射 if (DestIP = '') and EnableDefault then begin DestIP := Servers[High(Servers)].Host; DestPort := Servers[High(Servers)].Port; end; // 无匹配则拒绝连接 if DestIP = '' then begin Result := nil; TConsole.TextColor := TConsoleColor.Red; TConsole.WriteLn('Connection from ' + IP + ' dropped: No mapping found.'); TConsole.ResetStyle; Exit; end; // 创建并配置后端连接 Result := inherited CreateMappedClient(AClientContext); Result.Host := DestIP; Result.Port := DestPort; end;
原代码的问题修正
- 默认映射部分的
Servers[I].Host存在越界风险:循环结束后I的值为High(Servers)+1,直接访问会触发数组越界,建议单独指定默认项(比如数组最后一个元素)。 Mapper.Active:=true;无需在事件中重复设置,组件启动时已激活,重复调用无意义。
后续域名检测的实现思路
如果要实现基于域名(如server1.site.com)的映射,分两种场景处理:
- HTTP代理:改用
TIdHTTPProxyServer组件,在OnHTTPBeforeCommand事件中解析HTTP请求的Host头,再动态转发到对应后端。 - HTTPS代理(SNI):借助
TIdSSLIOHandlerSocketOpenSSL,在OnGetSSLContext事件中通过OpenSSL API提取TLS握手包中的SNI字段,以此判断目标域名。
GStack.HostByAddress返回空的解决办法
反向DNS解析失败通常是因为客户端IP无反向记录、系统DNS配置错误,可直接调用Linux系统函数getnameinfo实现解析:
function ReverseDNS(const AIP: string): string; var hints: addrinfo; res: Paddrinfo; err: Integer; HostName: array[0..NI_MAXHOST] of Char; begin Result := ''; FillChar(hints, SizeOf(hints), 0); hints.ai_family := AF_UNSPEC; hints.ai_socktype := SOCK_STREAM; hints.ai_flags := AI_NUMERICHOST; err := getaddrinfo(PChar(AIP), nil, @hints, @res); if err = 0 then begin err := getnameinfo(res^.ai_addr, res^.ai_addrlen, HostName, SizeOf(HostName), nil, 0, NI_NAMEREQD); if err = 0 then Result := StrPas(HostName); freeaddrinfo(res); end; end;
内容的提问来源于stack exchange,提问作者Codrut
相关产品推荐
相关产品推荐

