tencent cloud

边缘安全加速平台 EO

动态与公告
产品动态
安全公告
产品公告
产品简介
产品概述
产品优势
应用场景
EdgeOne 与 CDN 等产品功能对比
使用限制
购买指南
试用套餐体验权益说明
免费版套餐使用说明
计费概述
计费项目
购买指引
续费指引
欠费与退款说明
套餐选型对比
关于“干净流量”计费说明
DDoS 防护容量说明
快速入门
选择业务场景
快速接入网站安全加速
通过 Pages 快速部署网站
域名服务与源站配置
域名服务
HTTPS 证书
源站配置
站点加速
概述
访问控制
智能加速
缓存配置
文件优化
网络优化
URL 重写
修改头部
修改应答内容
规则引擎
图片与视频处理
单连接下载限速
DDoS 与 Web 防护
概述
DDoS 防护
Web 防护
Bot 管理
API 资产识别(Beta)
边缘函数
概述
快速指引
操作指引
Runtime APIs
示例函数
实践教程
Pages
四层代理
概述
新建四层代理实例
修改四层代理实例配置
停用/删除四层代理实例
批量配置转发规则
获取客户端真实IP
数据分析与日志服务
日志服务
数据分析
告警服务
站点与计费管理
计费管理
站点管理
版本管理
通用策略
通用参考
配置语法
请求与响应行为
国家/地区及对应代码枚举
Terraform
Terraform 简介
安装和配置 Terraform
实践教程
自动预热/清除缓存
防盗刷/盗链实践
HTTPS 相关实践
加速优化
流量调度
数据分析与告警
第三方日志平台集成实践
对象存储类源站(例如:COS)配置实践
跨域响应配置
API 文档
History
Introduction
API Category
Making API Requests
Site APIs
Acceleration Domain Management APIs
Site Acceleration Configuration APIs
Edge Function APIs
Alias Domain APIs
Security Configuration APIs
Layer 4 Application Proxy APIs
Content Management APIs
Data Analysis APIs
Log Service APIs
Billing APIs
Certificate APIs
Origin Protection APIs
Load Balancing APIs
Diagnostic Tool APIs
Custom Response Page APIs
API Security APIs
DNS Record APIs
Content Identifier APIs
Legacy APIs
Ownership APIs
Image and Video Processing APIs
Multi-Channel Security Gateway APIs
Version Management APIs
Data Types
Error Codes
常见问题
产品特性相关问题
DNS 记录相关问题
域名配置相关问题
站点加速相关问题
数据与日志相关问题
安全防护相关问题
源站配置相关问题
排障指南
异常状态码参考
EdgeOne 4XX/5XX 状态码排障指南
520/524状态码排障指南
521/522 状态码排障指南
工具指南
相关协议
Service Level Agreement
源站防护启用特别约定
TEO 政策
隐私协议
数据处理和安全协议
联系我们
词汇表
文档边缘安全加速平台 EO四层代理获取客户端真实IP通过 TOA 传递 TCP 协议客户端真实 IP

通过 TOA 传递 TCP 协议客户端真实 IP

PDF
聚焦模式
字号
最后更新时间: 2025-03-10 17:38:45
本文介绍了使用四层代理加速时,如何通过 TOA 获取 TCP 协议的客户端真实 IP。

使用场景

当数据报文通过四层加速通道进行加速时,数据报文的源 IP 地址和源 Port 均会发生修改,导致源站无法直接获取到真实客户端的 IP 和 Port 信息。为了将客户端真实 IP 和 Port 信息可传递给源站服务器,在创建加速通道时,您可选择通过 TOA 来传递客户端 IP 和 Port 信息。四层加速通道会将真实客户端的 IP 和 Port 信息放入自定义的 tcp option 字段中。您需要在源站服务器上通过安装 TOA 模块来获取真实客户端地址信息。
说明:
四层代理仅企业版套餐可用。

操作步骤

步骤一:传递客户端 IP 方式选择为 TOA

使用 TOA 获取 TCP 协议客户端真实 IP,需在控制台内将四层代理转发规则的传递客户端 IP 方式配置为 TOA,如何修改四层代理规则详见:修改四层代理实例配置


步骤二:后端服务加载 TOA 模块

您可以通过以下两种方式加载 TOA 模块:
方法一(推荐):根据源站 Linux 版本,下载对应版本已编译好的 toa.ko 文件直接进行加载。
方法二:如果方法一中没有找到您当前的源站 Linux 版本,您可以通过下载 TOA 源码文件自行编译并加载。该源码仅支持 x86_64 版本,如果您需要支持 arm64 版本请 联系我们
注意:
因不同安装环境的差异,如果您使用方法一加载过程中遇到问题,请尝试使用方法二,自行安装编译环境后加载。
方法一:下载已编译的 TOA 模块并加载
方法二:自行编译并加载 TOA 模块
1. 根据腾讯云上 Linux 的版本,下载对应的 TOA 包并解压。
centos
TencentOS
debian
suse linux
ubuntu
2. 解压完成后,执行 cd 命令进入刚解压的文件夹后,按照以下方法执行加载 TOA 模块:
脚本一键执行
手工配置加载
/bin/bash -c "$(curl -fsSL https://edgeone-document-file-1258344699.cos.ap-guangzhou.myqcloud.com/TOA/install_toa.sh)"
加载成功后显示如下:

# 解压tar包
tar -zxvf CentOS-7.2-x86_64.tar.gz
# 进入解压后的包目录
cd CentOS-7.2-x86_64
# 加载toa模块
insmod toa.ko
# 拷贝到内核模块目录下
cp toa.ko /lib/modules/`uname -r`/kernel/net/netfilter/ipvs/toa.ko
# 设置系统启动时自动加载toa模块
echo "insmod /lib/modules/`uname -r`/kernel/net/netfilter/ipvs/toa.ko" >> /etc/rc.local
可通过下面命令确认是否已加载成功:
lsmod | grep toa
出现 TOA 时表示已加载成功,如下图所示:

1. 安装编译环境。
1.1 查看当前内核版本号,确认 kernel-devel ,kernel-headers 已安装,并保证版本号与内核版本保持一致。
1.2 确认已安装 gcc 和 make。
1.3 如果以上环境依赖没有安装,可参考如下命令进行安装:
Centos
Ubuntu/Debian
yum install -y gcc
yum install -y make
yum install -y kernel-headers kernel-devel
apt-get install -y gcc
apt-get install -y make
apt-get install -y linux-headers-$(uname -r)

2. 安装完编译环境后,执行以下命令完成源码下载,编译和加载。
脚本一键编译并加载
手工编译并加载
/bin/bash -c "$(curl -fsSL https://edgeone-document-file-1258344699.cos.ap-guangzhou.myqcloud.com/TOA/compile_install_toa.sh)"
# 创建并进入编译目录
mkdir toa_compile && cd toa_compile
# 下载源代码tar包
curl -o toa.tar.gz https://edgeone-document-file-1258344699.cos.ap-guangzhou.myqcloud.com/TOA/toa.tar.gz
# 解压tar包
tar -zxvf toa.tar.gz
# 编译toa.ko文件,编译成功后会在当前目录下生成toa.ko文件
make
# 加载toa模块
insmod toa.ko
# 拷贝到内核模块目录下
cp toa.ko /lib/modules/`uname -r`/kernel/net/netfilter/ipvs/toa.ko
# 设置系统启动时自动加载toa模块
echo "insmod /lib/modules/`uname -r`/kernel/net/netfilter/ipvs/toa.ko" >> /etc/rc.local
3. 执行下面指令确认是否已加载成功:
lsmod | grep toa
出现 toa 则表示已加载成功,如下图所示:




步骤三:验证获取客户端 IP 信息

您可以通过搭建 TCP 服务,并通过另外一台服务器模拟客户端请求进行验证,示例如下:
1. 在当前服务器上,可以通过 Python 创建一个 HTTP 服务来模拟 TCP 服务,如下所示:
# 基于python2
python2 -m SimpleHTTPServer 10000

# 基于python3
python3 -m http.server 10000
2. 用另一台服务器充当客户端,构造客户端请求,以 Curl 请求来模拟 TCP 请求:
# 利用curl发起http请求, 其中域名为四层代理域名,10000为四层代理转发端口
curl -i "http://a8b7f59fc8d7e6c9.example.com.edgeonedy1.com:10000/"
3. 如果 TOA 已加载完成,在已加载 TOA 的服务器会看到客户端的真实地址信息,如下图红框所示:

如果您当前的业务是以下两种场景,只需要获取 IPv4 或 IPv6 其中一种类型客户端地址,那么参照上述步骤完成服务端加载 TOA 模块即可获取到客户端真实 IP 地址。
源站是 IPv4,只需要获取 IPV4 客户端地址。
源站是 IPv6,只需要获取 IPV6 客户端地址。
但是,如果您当前的业务源站需要同时获取到 IPv4 和 IPv6 两种类型客户端地址,则需要在加载 TOA 模块的同时修改源站业务代码,请继续参考如下指引:修改源站业务代码,支持同时获取 IPv4/IPv6 客户端真实地址信息

修改源站业务代码,同时获取IPv4/IPv6客户端真实 IP

说明:
本章节操作仅在源站需同时获取 IPv4 和 IPv6 客户端地址信息时参考,该操作将指引您如何修改源站业务代码。
源站在建立服务监听时,可参考采用如下两种方式:
1. 采用 IPv4 的地址结构(struct sockaddr_in)搭建服务,其监听的是 IPv4 格式的地址。
2. 采用 IPv6 的地址结构(struct sockaddr_in6)搭建服务,其监听的是 IPv6 格式的地址。

示例代码

监听 IPv4 地址
监听 IPv6 地址
C
Java
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <memory.h>
#include <arpa/inet.h>

int main(int argc, char **argv){
int l_sockfd;
// 服务器地址采用v4结构
struct sockaddr_in serveraddr;
// 业务修改点: 客户端地址必须采用v6结构
struct sockaddr_in6 clientAddr;
int server_port = 10000;

memset(&serveraddr, 0, sizeof(serveraddr));
// 创建socket
l_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (l_sockfd == -1){
printf("Failed to create socket.\\n");
return -1;
}
// 初始化服务器地址信息
memset(&serveraddr, 0, sizeof(struct sockaddr_in));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(server_port);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

int isReuse = 1;
setsockopt(l_sockfd, SOL_SOCKET,SO_REUSEADDR,(const char*)&isReuse,sizeof(isReuse));
// 关联socket和服务器地址信息
int nRet = bind(l_sockfd,(struct sockaddr*)&serveraddr, sizeof(serveraddr));
if(-1 == nRet)
{
printf("bind error\\n");
return -1;
}
// 监听socket
listen(l_sockfd, 5);

int clientAddrLen = sizeof(clientAddr);
memset(&clientAddr, 0, sizeof(clientAddr));
// 接受来自客户端的连接
int linkFd = accept(l_sockfd, (struct sockaddr*)&clientAddr, &clientAddrLen);
if(-1 == linkFd)
{
printf("accept error\\n");
return -1;
}
// 业务修改点: 根据客户端sin6_family的类型, 判断客户端是v4地址还是v6地址
// 当为AF_INET时, 表示客户端是IPv4, 将客户端地址指针转换为struct sockaddr_in*进行获取
// 当为AF_INET6时, 表示客户端是IPv6, 使用struct sockaddr_in6*进行获取
if (clientAddr.sin6_family == AF_INET) {
printf("AF_INET accept getpeername %s : %d successful\\n",
inet_ntoa(((struct sockaddr_in*)&clientAddr)->sin_addr),
ntohs(((struct sockaddr_in*)&clientAddr)->sin_port));
}else if (clientAddr.sin6_family == AF_INET6){
char addr_p[128] = {0};
inet_ntop(AF_INET6, (void *)&((struct sockaddr_in6*)&clientAddr)->sin6_addr, addr_p, (socklen_t )sizeof(addr_p));
printf("AF_INET6 accept getpeername %s : %d successful\\n",
addr_p,
ntohs(((struct sockaddr_in6*)&clientAddr)->sin6_port));
}else{
printf("unknow sin_family:%d \\n", clientAddr.sin6_family);
}
close(l_sockfd);
return 0;
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;


public class ServerDemo {


/** 若采用 IPv4 的地址结构搭建服务,使用 IPV4_HOST */
public static final String IPV4_HOST = "0.0.0.0";


/** 若采用 IPv6 的地址结构搭建服务,使用 IPV6_HOST */
public static final String IPV6_HOST = "::";


public static void main(String[] args) {
int serverPort = 10000;
try (ServerSocket serverSocket = new ServerSocket()) {
// 设置地址复用
serverSocket.setReuseAddress(true);
// 绑定服务器地址和端口,这里使用 IPv4
serverSocket.bind(new InetSocketAddress(InetAddress.getByName(IPV4_HOST), serverPort));
System.out.println("Server is listening on port " + serverPort);


while (true) {
// 接受客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("New client connected: " + clientSocket.getRemoteSocketAddress());


// 处理客户端请求
handleClientRequest(clientSocket);
}
} catch (IOException e) {
System.err.println("Failed to create server socket: " + e.getMessage());
}
}


/**
* 处理函数,具体业务具体实现,这里只做为示例
* 此函数的作用是将 client 的输入原封不动的返回给 client
*/
private static void handleClientRequest(Socket clientSocket) {
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {


// 读取客户端发来的数据
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
// 将接收到的数据原样回复给客户端
outputStream.write(buffer, 0, bytesRead);
}


} catch (IOException e) {
// 当客户端断开连接后
System.err.println("Failed to handle client request: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
System.err.println("Failed to close client socket: " + e.getMessage());
}
}
}
}
C
Java
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <memory.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
int l_sockfd;
// 服务器地址采用v6结构
struct sockaddr_in6 serveraddr;
// 客户端地址采用v6结构
struct sockaddr_in6 clientAddr;
int server_port = 10000;

memset(&serveraddr, 0, sizeof(serveraddr));
// 创建socket
l_sockfd = socket(AF_INET6, SOCK_STREAM, 0);
if (l_sockfd == -1){
printf("Failed to create socket.\\n");
return -1;
}
// 设置服务器地址信息
memset(&serveraddr, 0, sizeof(struct sockaddr_in6));
serveraddr.sin6_family = AF_INET6;
serveraddr.sin6_port = htons(server_port);
serveraddr.sin6_addr = in6addr_any;

int isReuse = 1;
setsockopt(l_sockfd, SOL_SOCKET,SO_REUSEADDR,(const char*)&isReuse,sizeof(isReuse));
// 关联socket和服务器地址信息
int nRet = bind(l_sockfd,(struct sockaddr*)&serveraddr, sizeof(serveraddr));
if(-1 == nRet)
{
printf("bind error\\n");
return -1;
}
// 监听socket
listen(l_sockfd, 5);

int clientAddrLen = sizeof(clientAddr);
memset(&clientAddr, 0, sizeof(clientAddr));
// 接受来自客户端的连接请求
int linkFd = accept(l_sockfd, (struct sockaddr*)&clientAddr, &clientAddrLen);
if(-1 == linkFd)
{
printf("accept error\\n");
return -1;
}
// 这里收到的客户端地址信息全部都采用v6的结构进行存储
// 其中,客户端的IPv4地址也被映射成了一个IPv6的地址,例如:::ffff:119.29.1.1
char addr_p[128] = {0};
inet_ntop(AF_INET6, (void *)&clientAddr.sin6_addr, addr_p, (socklen_t )sizeof(addr_p));
printf("accept %s : %d successful\\n", addr_p, ntohs(clientAddr.sin6_port));
// 业务修改点:通过系统宏定义IN6_IS_ADDR_V4MAPPED来判断一个IPv6地址是否是IPv4的映射地址(代表客户端是IPv4)
if(IN6_IS_ADDR_V4MAPPED(&clientAddr.sin6_addr)) {
struct sockaddr_in real_v4_sin;
memset (&real_v4_sin, 0, sizeof (struct sockaddr_in));
real_v4_sin.sin_family = AF_INET;
real_v4_sin.sin_port = clientAddr.sin6_port;
// 读取最后四个字节即为客户端真实IPv4地址
memcpy (&real_v4_sin.sin_addr, ((char *)&clientAddr.sin6_addr) + 12, 4);
printf("connect %s successful\\n", inet_ntoa(real_v4_sin.sin_addr));
}
close(l_sockfd);
return 0;
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;

public class ServerDemo {

/** 若采用 IPv4 的地址结构搭建服务,使用 IPV4_HOST */
public static final String IPV4_HOST = "0.0.0.0";

/** 若采用 IPv6 的地址结构搭建服务,使用 IPV6_HOST */
public static final String IPV6_HOST = "::";

public static void main(String[] args) {
int serverPort = 10000;
try (ServerSocket serverSocket = new ServerSocket()) {
// 设置地址复用
serverSocket.setReuseAddress(true);
// 绑定服务器地址和端口,这里使用 IPv4
serverSocket.bind(new InetSocketAddress(InetAddress.getByName(IPV6_HOST), serverPort));
System.out.println("Server is listening on port " + serverPort);

while (true) {
// 接受客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("New client connected: " + clientSocket.getRemoteSocketAddress());

// 处理客户端请求
handleClientRequest(clientSocket);
}
} catch (IOException e) {
System.err.println("Failed to create server socket: " + e.getMessage());
}
}

/**
* 处理函数,具体业务具体实现,这里只做为示例
* 此函数的作用是将 client 的输入原封不动的返回给 client
*/
private static void handleClientRequest(Socket clientSocket) {
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {

// 读取客户端发来的数据
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
// 将接收到的数据原样回复给客户端
outputStream.write(buffer, 0, bytesRead);
}

} catch (IOException e) {
// 当客户端断开连接后
System.err.println("Failed to handle client request: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
System.err.println("Failed to close client socket: " + e.getMessage());
}
}
}
}

控制台输出结果

Server is listening on port 10000
New client connected: /127.0.0.1:50680
New client connected: /0:0:0:0:0:0:0:1:51124
New client connected: /127.0.0.1:51136

相关参考

监控 TOA 运行状态

为保障 TOA 内核模块运行的稳定性,TOA 内核模块还提供了监控功能。在插入 toa.ko 内核模块后,可以通过执行以下命令方式监控 TOA 模块的工作状态。
cat /proc/net/toa_stats
TOA 运行状态如下:

其中主要的监控指标对应的含义如下所示:
指标名称
说明
syn_recv_sock_toa
接收带有 TOA 信息的连接个数。
syn_recv_sock_no_toa
接收并不带有 TOA 信息的连接个数。
getname_toa_ok
调用 getsockopt 获取源 IP 成功即会增加此计数,另外调用 accept 函数接收客户端请求时也会增加此计数。
getname_toa_mismatch
调用 getsockopt 获取源 IP 时,当类型不匹配时,此计数增加。例如某条客户端连接内存放的是 IPv4 源 IP,并非为 IPv6 地址时,此计数便会增加。
getname_toa_empty
对某一个不含有 TOA 的客户端文件描述符调用 getsockopt 函数时,此计数便会增加。
ip6_address_alloc
当 TOA 内核模块获取 TCP 数据包中保存的源 IP、源 Port 时,会申请空间保存信息。
ip6_address_free
当连接释放时,toa 内核模块会释放先前用于保存源 IP、源 port 的内存,在所有连接都关闭的情况下,所有 CPU 的此计数相加应等于 ip6_address_alloc 的计数。


帮助和支持

本页内容是否解决了您的问题?

填写满意度调查问卷,共创更好文档体验。

文档反馈