有奖捉虫:办公协同&微信生态&物联网文档专题 HOT
文档中心 > DDoS 防护 > 最佳实践 > 获取客户端真实 IP(端口接入)
本文将介绍如何通过 TOA 模块获取客户端 IP,以代替 DDoS API 文档中错误的域名接入方式和缺少其他发行版编译环境依赖包安装命令的端口接入方式。

TOA 模块是一款高性能、轻量级的 TCP 连接信息采集工具,它可以在应用层获取客户端真实 IP,并将其转发给后端服务器。本文将分别介绍域名接入和端口接入两种方式下如何使用 TOA 模块获取客户端 IP。

域名接入方式

在 Nginx 反向代理的相应 location 位置配置如下内容,获取客户端 IP 的信息:
log_format real_ip '$http_x_forwarded_for - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';

server {
listen 80;
server_name yourdomain.com;

location / {
proxy_pass http://backend_server;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
access_log /var/log/nginx/access.log real_ip;
}
}
其中 proxy_set_header 配置项用于设置 HTTP 请求头,并将客户端真实 IP 添加到请求头中;access_log 配置项则用于设置 Nginx 的日志格式,从而在日志中记录客户端真实 IP。

端口接入方式

后端服务加载 TOA 模块

您可以通过以下三种方式加载 TOA 模块:

方法一:使用 modprobe 加载 TOA 模块

下图表示使用 modprobe 加载 TOA 模块失败,可以参考 处理方法 修复。

处理方法
1. 检查内核配置文件,是否支持 TOA ,可以看到 TOA 是以模块形式编译。

2. 通过 modprobe toa 加载模块。
3. 再次检查 TOA 是否已经支持,如下图所示,则代表已经支持。


方法二:下载已编译的 TOA 模块并加载

1. 根据腾讯云上 Linux 的版本,下载对应的 TOA 包并解压。
centos
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 时表示已加载成功,如下图所示:


方法三:自行编译并加载 TOA 模块

1. 安装编译环境。
查看当前内核版本号,确认 kernel-devel ,kernel-headers 已安装,并保证版本号与内核版本保持一致。
确认已安装 gcc 和 make。
如果以上环境依赖没有安装,可参考如下命令进行安装:
centos
yum install -y gcc
yum install -y make
yum install -y kernel-headers kernel-devel
Ubuntu/Debian
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 信息

如果您当前的业务是以下两种场景,只需要获取 IPv4 或 IPv6 其中一种类型客户端地址,可以按照如下步骤完成服务端加载 TOA 模块即可获取到客户端真实 IP 地址。
源站是 IPv4,只需要获取 IPV4 客户端地址。
源站是 IPv6,只需要获取 IPV6 客户端地址。

您可以通过搭建 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/"

C 语言示例代码

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

如果您需要同时获取到 IPv4 和 IPv6 两种类型客户端地址,则需要在加载 TOA 模块的同时修改源站业务代码。
源站在建立服务监听时,可参考采用如下两种方式:
采用 IPv4 的地址结构 struct sockaddr_in 搭建服务,其监听的是 IPv4 格式的地址。
#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;
}

采用 IPv6 的地址结构 struct sockaddr_in6 搭建服务,其监听的是 IPv6 格式的地址。
#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;
}


Java 代码示例

源站在建立服务监听时,可参考采用如下两种方式:
1. 采用 IPv4 的地址结构搭建服务,IPv4、IPv6 格式的地址均可监听。
2. 采用 IPv6 的地址结构搭建服务,IPv4、IPv6 格式的地址均可监听。

采用 IPv4 的地址结构搭建服务

需要注意的是:示例代码采用 IPv4 的地址结构搭建服务,若需要采用 IPv6 的地址结构搭建服务,则需要将示例代码中的 serverSocket.bind(new InetSocketAddress(InetAddress.getByName(IPV4_HOST), serverPort)); 更改为 serverSocket.bind(new InetSocketAddress(InetAddress.getByName(IPV6_HOST), serverPort));
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());
}
}
}
}

采用 IPv6 的地址结构搭建服务

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 的计数。