边缘加速
  • 站点加速
    • 概述
    • Quickly Import and Export Site Configuration
    • 访问控制
      • Token 鉴权
    • 智能加速
    • 文件优化
      • 智能压缩
    • 网络优化
      • HTTP/2
      • HTTP/3(QUIC)
        • 概述
        • 启用 HTTP/3
        • QUIC SDK
          • SDK 概览
          • SDK 下载和集成指引
          • 代码示例
            • Android
            • iOS
          • API 文档
            • Android
            • iOS
      • IPv6 访问
      • 最大上传大小
      • WebSocket
      • 携带客户端 IP 头部回源
      • 携带客户端 IP 地理位置头部回源
      • 开启 gRPC
    • URL 重写
      • 访问 URL 重定向
      • 回源 URL 重写
    • 修改头部
      • 修改 HTTP 节点响应头
      • 修改 HTTP 回源请求头
    • 自定义错误页面
    • 请求与响应行为
      • 请求处理顺序
      • EdgeOne 默认 HTTP 回源请求头
      • EdgeOne 默认 HTTP 响应头
    • Media Services
      • Audio and Video Pre-pulling
      • Just-in-Time Image Processing
      • Just-in-Time Media Processing
      • VOD Media Origin
  • 四层代理
    • 概述
    • 新建四层代理实例
    • 修改四层代理实例配置
    • 停用/删除四层代理实例
    • 批量配置转发规则
    • 获取客户端真实IP
      • 通过 TOA 获取 TCP 协议客户端真实 IP
      • 通过 Proxy Protocol V1/V2 协议获取客户端真实 IP
        • 概述
        • 方式一:通过 Nginx 获取客户端真实 IP
        • 方式二:在业务服务器解析客户端真实 IP
        • Proxy Protocol V1/V2 获取的客户端真实 IP 格式
      • 通过 SPP 协议传递客户端真实 IP
  • 边缘 DNS
    • 托管域名 DNS 解析
      • 修改 DNS 服务器
      • 配置域名 DNS 解析记录
      • DNS 高级配置
    • 接入加速域名
      • 添加加速域名
      • 站点/域名归属权验证
      • 修改 CNAME 解析
    • 别称域名
      • 概述
      • 配置指南
      • 通过别称域名批量接入 SaaS 建站域名
      • 别称域名实现业务的容灾
    • 流量调度
      • 流量调度管理
    • 源站配置
      • 回源配置
        • 配置回源 HTTPS
        • Host Header 重写
        • 回源请求参数设置
        • 回源跟随重定向
        • HTTP/2 回源
        • 分片回源
      • 负载均衡
        • 概述
        • 快速创建负载均衡实例
        • 健康检查策略介绍
        • 查看源站健康状态
        • 相关参考
          • 负载均衡相关概念
          • 请求重试策略介绍
      • 源站组操作指引
      • 相关参考
        • 旧版源站组兼容相关问题
      • 获取 EdgeOne 回源节点 IP
  • 边缘缓存
    • 概述
    • EdgeOne 缓存规则介绍
      • EdgeOne 内容缓存规则
      • 缓存键(Cache Key)介绍
      • Vary 特性
    • 缓存配置
      • 自定义 Cache Key
      • 节点缓存 TTL
      • 状态码缓存 TTL
      • 浏览器缓存 TTL
      • 离线缓存
      • 缓存预刷新
    • 清除和预热缓存
      • 清除缓存
      • 预热缓存
    • 如何提高 EdgeOne 的缓存命中率
  • 规则引擎
    • 概述
    • 规则引擎支持的匹配类型与操作
    • 规则管理
    • 变量

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

使用场景

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);
}
}

步骤3:测试验证

您可以找一台服务器充当客户端,构造客户端请求,以 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

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