tencent cloud

Tencent Cloud EdgeOne

TOAによるTCPプロトコルのクライアント実IP伝送

ダウンロード
フォーカスモード
フォントサイズ
最終更新日: 2026-10-15 16:51:02
AI翻訳
本文では、レイヤー4プロキシ加速を使用する際に、TOAを通じてTCPプロトコルのクライアントの実際のIPを取得する方法について説明します。

使用シナリオ

データパケットがレイヤー4加速チャネルを通じて加速される際に、データパケットの送信元IPアドレスと送信元Portは変更されます。これにより、オリジンサーバーは実際のクライアントのIPおよびPort情報を直接取得できなくなります。クライアントの実際のIPおよびPort情報をオリジンサーバーに渡すために、加速チャネルを作成する際に、TOAを使用してクライアントIPおよびPort情報を転送することを選択できます。レイヤー4加速チャネルは、実際のクライアントのIPおよびPort情報をカスタムtcp optionフィールドに格納します。オリジンサーバーにTOAモジュールをインストールして、実際のクライアントアドレス情報を取得する必要があります。
説明:
レイヤー4プロキシはエンタープライズ版プランでのみ利用可能です。

操作手順

ステップ1:クライアントIP伝送方式としてTOAを選択する

TOAを使用してTCPプロトコルのクライアントの実際のIPを取得するには、コンソール内でレイヤー4プロキシ転送ルールのクライアントIP転送方法をTOAに設定する必要があります。レイヤー4プロキシルールの変更方法の詳細については、レイヤー4プロキシインスタンス設定の変更を参照してください。


ステップ2:バックエンドサービスによるTOAモジュールのロード

TOAモジュールは、以下の2つの方法でロードできます:
方法1(推奨):オリジンサーバーのLinuxバージョンに応じて、対応するバージョンのコンパイル済みtoa.koファイルをダウンロードして直接ロードします。
方法2:方法1で現在のオリジンサーバーのLinuxバージョンが見つからない場合は、TOAソースコードファイルをダウンロードして自身でコンパイルし、ロードすることができます。このソースコードはx86_64バージョンのみをサポートしています。arm64バージョンのサポートが必要な場合は、お問い合わせください。
注意:
インストール環境の違いにより、方法1を使用してロードする過程で問題が発生した場合は、方法2を試して、自身でコンパイル環境をインストールしてからロードしてください。
方法1:コンパイル済みTOAモジュールのダウンロードとロード
方法2:TOAモジュールの自己コンパイルとロード
1. Tencent Cloud上の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が表示された場合は、正常にロードされたことを示します。下図の通りです:




ステップ3:クライアントIP情報取得の検証

TCPサービスを構築し、別のサーバーを使用してクライアントリクエストをシミュレートすることで検証できます。例は以下の通りです:
1. 現在のサーバー上で、Pythonを使用してHTTPサービスを作成し、TCPサービスをシミュレートできます。以下に示します:
# Python 2に基づく
python2 -m SimpleHTTPServer 10000

# Python 3に基づく
python3 -m http.server 10000
2. 別のサーバーをクライアントとして使用し、クライアントリクエストを構築し、CurlリクエストでTCPリクエストをシミュレートします:
# curlを使用してhttpリクエストを送信します。ここで、ドメイン名は四層プロキシドメイン名であり、10000は四層プロキシ転送ポートです。
curl -i "http://a8b7f59fc8d7e6c9.example.com.edgeonedy1.com:10000/"
3. TOAが既にロードされている場合、TOAがロードされたサーバーでクライアントの実際のアドレス情報を確認できます。下図の赤枠で示す通りです:

現在のビジネスが以下の2つのシナリオのいずれかであり、IPv4またはIPv6のいずれか一方のタイプのクライアントアドレスのみを取得すればよい場合は、前述の手順に従ってサーバー側でTOAモジュールをロードするだけで、クライアントの実際のIPアドレスを取得できます。
オリジンサーバーがIPv4の場合、IPv4クライアントアドレスのみを取得すれば十分です。
オリジンサーバーがIPv6の場合、IPv6クライアントアドレスのみを取得すれば十分です。
ただし、現在のビジネスオリジンサーバーでIPv4とIPv6の両方のタイプのクライアントアドレスを同時に取得する必要がある場合は、TOAモジュールをロードすると同時にオリジンサーバーのビジネスコードを変更する必要があります。以下の手順を参照してください:オリジンサーバーのビジネスコードを変更し、IPv4/IPv6クライアントの実際のアドレス情報を同時に取得できるようにします。

オリジンサーバーの業務コードを変更し、IPv4/IPv6クライアントの実際のIPを同時に取得する

説明:
本章節の操作は、オリジンサーバーがIPv4とIPv6のクライアントアドレス情報を同時に取得する必要がある場合にのみ参照してください。この操作は、オリジンサーバーのビジネスコードを変更する方法を案内します。
オリジンサーバーがサービスリスナーを確立する際には、以下の2つの方法を参考に採用できます:
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));
// ソケットを作成します。
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));
// ソケットとサーバーアドレス情報を関連付けます。
int nRet = bind(l_sockfd,(struct sockaddr*)&serveraddr, sizeof(serveraddr));
if(-1 == nRet)
{
printf("bind error\\n");
return -1;
}
// ソケットをリッスンします。
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));
// ソケットを作成します。
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));
// ソケットとサーバーアドレス情報を関連付けます。
int nRet = bind(l_sockfd,(struct sockaddr*)&serveraddr, sizeof(serveraddr));
if(-1 == nRet)
{
printf("bind error\\n");
return -1;
}
// ソケットをリッスンします。
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;
// 最後の4バイトを読み取ると、それがクライアントの実際の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を取得する際、タイプが一致しない場合にこのカウントが増加します。例えば、あるクライアント接続に保存されているソースIPがIPv4であり、IPv6アドレスではない場合にこのカウントが増加します。
getname_toa_empty
TOAを含まないクライアントファイルディスクリプタに対してgetsockopt関数を呼び出すと、このカウントが増加します。
ip6_address_alloc
TOAカーネルモジュールがTCPデータパケットに保存された送信元IP、送信元ポートを取得する際、情報を保存するための領域を確保します。
ip6_address_free
接続が解放されると、toaカーネルモジュールは以前に送信元IP、送信元ポートを保存するために使用したメモリを解放します。すべての接続が閉じられている場合、全CPUのこのカウントの合計はip6_address_allocのカウントと等しくなるはずです。


ヘルプとサポート

この記事はお役に立ちましたか?

フィードバック