用户之声——提建议·赢好礼> HOT

使用场景

SPP (Simple Proxy Protocol Header,以下简称 SPP)协议是一种自定义的协议头格式,用于代理服务器将真实客户端 IP 和其他相关信息传递给后端服务器,用于记录日志、实现访问控制、负载均衡或者故障排除等场景。SPP 协议头固定长度为38字节,相比 Proxy Protocol V2 协议更为简单。
如果您当前现有的后端业务服务为 UDP 服务,已经支持了 SPP 协议或者希望使用更简单的解析方式,您可以使用 SPP 协议来传递客户端真实 IP。EdgeOne 的四层代理支持根据 SPP 协议标准传递真实客户端 IP 至业务服务器,您可以在服务端自行对该协议解析来获取真实客户端 IP 和 Port。

EdgeOne 对 SPP 协议处理流程

请求访问




如上图所示,当您使用 SPP 协议传递客户端 IP 和 Port 时,EdgeOne 的四层代理会自动将客户端的真实 IP 和 Port 以固定 38 字节长度,按照 SPP 协议头格式添加到每个有效载荷之前,您需要在源站服务器解析 SPP 头部字段才能获取客户端的真实 IP 和 Port。

源站响应




如上图所示,源站服务器回包时,需要携带 SPP 协议头一并返回给 EO 四层代理,EO 四层代理会自动卸载 SPP 协议头。
注意:
如果源站服务器没有返回 SPP 协议头,则会导致 EO 四层代理截断有效载荷的业务数据。

操作步骤

步骤1:配置四层代理转发规则

1. 登录 边缘安全加速平台 EO 控制台,在左侧菜单栏中,单击站点列表,在站点列表内单击需配置的站点。
2. 在站点详情页面,单击四层代理
3. 在四层代理页面,选择需要修改的四层代理实例,单击配置
4. 选择需要传递客户端真实 IP 的四层代理规则,单击编辑
5. 填写对应的业务源站地址、源站端口,转发协议选择 UDP,传递客户端 IP 选择 Simple Proxy Protocol,单击保存


步骤2:在源站服务器解析 SPP 字段获取客户端真实 IP

您可以参考 SPP 协议头格式示例代码,在源站服务器上解析 SPP 字段,使用 SPP 协议传输真实客户端 IP 时,服务端获取的业务包数据格式如下:


您可以
参考以下示例代码来对业务数据解析获取到真实客户端 IP。
Go
C
package main

import (
"encoding/binary"
"fmt"
"net"
)

type NetworkConnection struct {
Magic uint16
ClientAddr net.IP
ProxyAddr net.IP
ClientPort uint16
ProxyPort uint16
}

func handleConn(conn *net.UDPConn) {
buf := make([]byte, 1024) // 创建缓冲区
n, addr, err := conn.ReadFromUDP(buf) // 从连接读取数据包

if err != nil {
fmt.Println("Error reading from UDP connection:", err)
return
}

// 将接收到的字节转换为NetworkConnection结构体
nc := NetworkConnection{
Magic: binary.BigEndian.Uint16(buf[0:2]),
ClientAddr: make(net.IP, net.IPv6len),
ProxyAddr: make(net.IP, net.IPv6len),
}
if nc.Magic == 0x56EC {
copy(nc.ClientAddr, buf[2:18])
copy(nc.ProxyAddr, buf[18:34])
nc.ClientPort = binary.BigEndian.Uint16(buf[34:36])
nc.ProxyPort = binary.BigEndian.Uint16(buf[36:38])

// 打印 spp 头信息,包含 magic、客户端真实 ip 和 port、代理的 ip 和 port
fmt.Printf("Received packet:\\n")
fmt.Printf("\\tmagic: %x\\n", nc.Magic)
fmt.Printf("\\tclient address: %s\\n", nc.ClientAddr.String())
fmt.Printf("\\tproxy address: %s\\n", nc.ProxyAddr.String())
fmt.Printf("\\tclient port: %d\\n", nc.ClientPort)
fmt.Printf("\\tproxy port: %d\\n", nc.ProxyPort)
// 打印真实有效的载荷
fmt.Printf("\\tdata: %v\\n\\tcount: %v\\n", string(buf[38:n]), n)
} else {
// 打印真实有效的载荷
fmt.Printf("\\tdata: %v\\n\\tcount: %v\\n", string(buf[0:n]), n)
}

// 回包,注意:需要将 SPP 38字节长度原封不动地返回
response := make([]byte, n)
copy(response, buf[0:n])
_, err = conn.WriteToUDP(response, addr) // 发送数据
if err != nil {
fmt.Println("Write to udp failed, err: ", err)
}
}

func main() {
localAddr, _ := net.ResolveUDPAddr("udp", ":6666") // 使用本地地址和端口创建 UDP 地址
conn, err := net.ListenUDP("udp", localAddr) // 创建监听器
if err != nil {
panic("Failed to listen for UDP connections:" + err.Error())
}

defer conn.Close() // 结束时关闭连接
for {
handleConn(conn) // 处理传入的连接
}
}

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
struct NetworkConnection {
uint16_t magic;
struct in6_addr clientAddr;
struct in6_addr proxyAddr;
uint16_t clientPort;
uint16_t proxyPort;
};
void handleConn(int sockfd) {
struct sockaddr_in clientAddr;
socklen_t addrLen = sizeof(clientAddr);
unsigned char buf[BUF_SIZE];
ssize_t n = recvfrom(sockfd, buf, BUF_SIZE, 0, (struct sockaddr *)&clientAddr, &addrLen);
if (n < 0) {
perror("Error reading from UDP connection");
return;
}
// 将接收到的字节转换为 NetworkConnection 结构体
struct NetworkConnection nc;
nc.magic = ntohs(*(uint16_t *)buf);
if (nc.magic == 0x56EC) { // Magic 为 0x56EC 表示 SPP 头
memcpy(&nc.clientAddr, buf + 2, 16);
memcpy(&nc.proxyAddr, buf + 18, 16);
nc.clientPort = ntohs(*(uint16_t *)(buf + 34));
nc.proxyPort = ntohs(*(uint16_t *)(buf + 36));
printf("Received packet:\\n");
printf("\\tmagic: %x\\n", nc.magic);
char clientIp[INET6_ADDRSTRLEN];
char proxyIp[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &nc.clientAddr, clientIp, INET6_ADDRSTRLEN);
inet_ntop(AF_INET6, &nc.proxyAddr, proxyIp, INET6_ADDRSTRLEN);
// 打印 spp 头信息,包含 magic、客户端真实 ip 和 port、代理的 ip 和 port
printf("\\tclient address: %s\\n", clientIp);
printf("\\tproxy address: %s\\n", proxyIp);
printf("\\tclient port: %d\\n", nc.clientPort);
printf("\\tproxy port: %d\\n", nc.proxyPort);
// 打印真实有效的载荷
printf("\\tdata: %.*s\\n\\tcount: %zd\\n", (int)(n - 38), buf + 38, n);
} else {
printf("\\tdata: %.*s\\n\\tcount: %zd\\n", (int)n, buf, n);
}
// 回包,注意:需要将 SPP 38字节长度原封不动的返回
sendto(sockfd, buf, n, 0, (struct sockaddr *)&clientAddr, addrLen);
}
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Failed to create socket");
exit(EXIT_FAILURE);
}
// 使用本地地址和端口创建 UDP 地址
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(6666);
if (bind(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("Failed to bind");
exit(EXIT_FAILURE);
}
while (1) {
handleConn(sockfd);
}
}

步骤2:测试验证

您可以找一台服务器充当客户端,构造客户端请求,以 nc 命令来模拟 UDP 请求,命令详情如下:
echo "Hello Server" | nc -w 1 -u <IP/DOMAIN> <PORT>
其中,IP/Domain 即为您的四层代理实例接入 IP 或者域名,您可以在 EdgeOne 控制台内查看对应的四层代理实例信息。Port 即为您在 步骤1 内为该规则所配置的转发端口。

服务端收到请求并解析客户端 IP 地址如下:


相关参考

SPP 协议头格式



Magic Number

在 SPP 协议格式中,Magic Number 为 16 位 ,且固定值为 0x56EC,主要用于识别 SPP 协议,并定义了 SPP 协议头是固定 38 字节长度。

Client Address

客户端发起请求的 IP 地址,长度为 128 位,如果是 IPV4 客户端发起,则该值表示 IPV4;如果是 IPV6 客户端发起,则该值表示 IPV6。

Proxy Address

代理服务器的 IP 地址,长度为 128 位,可以和 Client Address 相同的解析方式。

Client Port

客户端发送 UDP 数据包的端口,长度为 16 位。

Proxy Port

代理服务器接收 UDP 数据包的端口,长度为 16 位。

payload

有效载荷,数据包携带的标头后面的数据。