eCapture开源项目三周年, v1.0.0 稳定版发布 eCapture旁观者是一款基于 eBPF 技术的高级网络工具,专注于 SSL/TLS 协议的明文数据捕获与解密。凭借其强大的功能和丰富的特性,eCapture 已成为网络调试和安全分析领域的重要工具。自2022年3月发布至今三年,累计1.4万颗星。 核心功能 1. 多协议支持 加密库支持:兼容 OpenSSL、GnuTLS、BoringSSL 等多种加密库,支持 HTTP/2 和 QUIC 协议捕获。 协议兼容性:涵盖 TLS 1.2 和 TLS 1.3,确保对最新协议的全面支持。 2. 智能数据捕获 eBPF 引擎:基于 eBPF 技术实现高效的网络数据捕获和协议解析。 双栈支持:支持 IPv4 和 IPv6 的双栈模式,确保在不同网络环境中的灵活应用。 四元组过滤:提供灵活的过滤功能,精准捕获目标流量。 3. 主密钥捕获 自动化捕获:自动检测并捕获 TLS 1.2 和 TLS 1.3 的主密钥,实现加密流量的实时解密。 Wireshark 集成:生成与 Wireshark 兼容的流量文件,支持离线解密和分析。 4. 多种捕获模式 Pcap 模式:生成 Pcapng 格式的流量文件,便于后续分析。 Keylog 模式:捕获通信密钥,生成与 Wireshark 兼容的密钥日志文件。 Text 模式:实时输出明文数据,方便开发者快速调试和分析。 5. 跨平台支持 操作系统:支持 Linux 和 Android 系统,确保在不同操作环境中的稳定运行。 架构兼容:兼容 x86_64 和 ARM64 架构,适用于多种设备和服务器环境。 6. 自动化与智能检测 智能模式选择:自动识别 CO-RE 和非 CO-RE 模式,优化运行性能。 内存优化:提供灵活的内存配置选项,提升资源利用率和系统稳定性。 7. 高性能与兼容性 高效处理:基于 eBPF 的高效数据处理,支持大规模并发和长时间抓包。 稳定性提升:经过多次版本迭代和优化,v1.0.0 版本具有更高的稳定性和兼容性。 8. 灵活配置 自定义过滤:支持灵活的数据过滤配置,精准捕获目标流量。 多种输出格式:支持多种数据输出格式,满足不同场景的需求。 日志管理:提供详细的日志记录,方便后续分析和审计。 9. 命令审计 Shell 审计:捕获 Bash 和 Zsh 命令行的输入和输出,支持实时监控和记录。 数据库监控:监控 MySQL 和 PostgreSQL 的 SQL 查询,提供数据库操作审计功能。 10. Wireshark 插件支持 流量分析:生成兼容 Wireshark 的流量文件,方便使用 Wireshark 进行深入分析。 密钥日志集成:支持在 Wireshark 中加载密钥日志,实现加密流量的实时解密。 技术特点 1. 模块化架构 功能扩展:采用模块化设计,便于功能扩展和维护。 灵活配置:支持多种配置选项,满足不同用户的需求。 2. 多语言开发 内核空间:使用 C 语言实现内核空间功能,确保高效和稳定。 用户空间:使用 Golang 和 CGO 实现用户空间功能,提升开发效率和代码维护性。 3. 交叉编译支持 多架构支持:支持在 amd64 和 ARM64 两种架构下进行交叉编译,生成适用于不同平台的二进制文件。 工具链支持:提供丰富的交叉编译工具链,方便用户根据需求进行编译和配置。 版本更新 v1.0.0 版本 新增功能 支持 Docker 镜像,简化部署和使用。 新增对 OpenSSL 3.4.0 和 GnuTLS 的密钥捕获支持。 支持 IPv6 数据包捕获,扩展网络协议支持。 增加 Zsh 命令审计功能,提升安全监控能力。 关键修复 修复 Ubuntu 24.04 系统上初始化脚本的兼容性问题。 解决 ARM64 版本无法正常工作的问题。 优化构建过程,提升整体稳定性和性能。 社区与开发 开源社区 项目星数:自 2022 年 3 月创建以来,累计获得 14,000+ 颗星,社区活跃度高。 贡献者:吸引了众多开发者参与贡献,共同推动项目发展。 作者信息 开发者:由陈驰(CFC4N)开发,详见其个人博客 www.cnxct.com。 开源精神:秉持开源精神,欢迎更多开发者加入社区,共同改进和完善项目。 部署与使用 易用部署 二进制文件:提供预编译的二进制文件,支持 Linux 和 Android 系统。 Docker 镜像:发布 Docker 镜像,简化部署和使用过程。 文档支持:提供详细的使用文档和配置指南,帮助用户快速上手。 总结 eCapture v1.0.0 是一款功能强大、技术先进的网络工具,凭借其多协议支持、智能数据捕获、跨平台兼容性和高性能,成为开发者和安全研究人员的得力助手。通过三年的不断开发和优化,eCapture 在网络调试和安全分析领域树立了良好的口碑。 未来,随着社区的不断贡献,eCapture 将继续保持其技术领先,服务于更多用户。 2025-08-22 0 0
eCapture旁观者支持Golang tls/https加密明文捕获 前言 云原生生态中,golang语言开发的项目越来越多,例如Docker和K8s、etcd等。作为SRE、RD,偶尔需要在生产环境抓网络通讯包,用来分析排查故障。很多时候,都是tls/https加密协议,如何在不重启业务保留现场,不改为自定义CA证书的情况下,分析明文通讯内容呢? 适用场景 eCapture 0.5.0版本在2023年3月12日发布,支持了go语言编写的软件的tls/https明文抓包。只需要root权限,即可捕获并保存为pcapng格式,使用wireshark即可打开查看。 使用方法 gotls模块的e参数用来设定golang编译的可执行文件路径,可以通过ecapture gotls -h来查看使用说明。 bin/ecapture gotls -h NAME: gotls - capture golang tls/https text content without CA cert for ELF compile by Golang toolchain USAGE: ecapture gotls [flags] DESCRIPTION: use eBPF uprobe/TC to capture process event data and network data. also support pcap-NG format. ecapture gotls ecapture gotls --elfpath=/home/cfc4n/go_https_client --hex --pid=3423 ecapture gotls --elfpath=/home/cfc4n/go_https_client -l save.log --pid=3423 ecapture gotls -w save_android.pcapng -i wlan0 --port 443 --elfpath=/home/cfc4n/go_https_client OPTIONS: -e, --elfpath="" ELF path to binary built with Go toolchain. -h, --help[=false] help for gotls -i, --ifname="" (TC Classifier) Interface name on which the probe will be attached. --port=443 port number to capture, default:443. -w, --write="" write the raw packets to file as pcapng format. GLOBAL OPTIONS: -d, --debug[=false] enable debug logging --hex[=false] print byte strings as hex encoded strings -l, --log-file="" -l save the packets to file --nosearch[=false] no lib search -p, --pid=0 if pid is 0 then we target all pids -u, --uid=0 if uid is 0 then we target all users 举个例子 比如/path/elf_filepath_compiled_by_go是一个go写的web服务,并且开启了https加密,代码如下: package main import ( "crypto/tls" "fmt" "io" "net/http" "os" ) func main() { b, e := GetHttp("https://github.com") if e == nil { fmt.Printf("response body: %s\n\n", b) } else { fmt.Printf("error :%v", e) } } func GetHttp(url string) (body []byte, err error) { // 开启TLS密钥记录,用于跟eCpature捕获的密钥对比。 f, err := os.OpenFile("/tmp/go_master_secret.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) if err != nil { panic(err) } defer f.Close() c := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true, KeyLogWriter: f}, }} resp, e := c.Get(url) if e != nil { return nil, e } defer resp.Body.Close() body, err = io.ReadAll(resp.Body) return body, err } 可以使用如下命令,捕获明文通讯。 不用重启这个服务进程,也不需要做其他任何配置,就跟使用tcpdump一样。 ./ecapture gotls -e=/path/elf_filepath_compiled_by_go -w a.pcapng -i eth0 Wireshark打开网络包 下载地址 eCapture Github仓库:https://github.com/gojue/ecapture/releases/tag/v0.5.0 韩国GitHub镜像:https://ghproxy.com/https://github.com/gojue/ecapture/releases/tag/v0.5.0 以下内容,为功能实现原理,若你只是使用,可跳过。 技术原理 Probe参数获取 Golang的ABI不同于C,自定义了ABI机制。并且在go 1.17之前,使用的是栈方式传递调用参数;1.17以以后使用了寄存器方式传递调用参数。你可以阅读Register-based Go calling convention 了解更多知识。 这里有一张Golang的函数参数、返回值的寄存器传递布局,供参考。更多内容可以在线阅读《Go语言高级编程》中文版 eCapture的参数获取实现,可以阅读kern/go_argument.h Probe参数选择 笔者这里hook的是Golang源码目录下crypto/tls/common.go文件中的writeKeyLog函数。用来捕获tls的master secret的label类别、clientRandom、密钥值等。 Golang函数参数传递 有个需要注意的地方,比如writeKeyLog函数的第一个参数是string类型,第二、三个参数是slice类型。在Golang里,也都是一个结构体,如下代码: // runtime/string.go type stringStruct struct { str unsafe.Pointer len int } // runtime/slice.go type slice struct { array unsafe.Pointer len int cap int } 以string类型为例,在Go 的参数传递时,不光传递字符串的str unsafe.Pointer指针地址,也还会传递len int到寄存器上。所以,在获取参数时,需要注意参数所在位置。 eCapture的实现: lab_ptr = (void *)go_get_argument(ctx, is_register_abi, 2); lab_len_ptr = (void *)go_get_argument(ctx, is_register_abi, 3); cr_ptr = (void *)go_get_argument(ctx, is_register_abi, 4); cr_len_ptr = (void *)go_get_argument(ctx, is_register_abi, 5); secret_ptr = (void *)go_get_argument(ctx, is_register_abi, 7); secret_len_ptr = (void *)go_get_argument(ctx, is_register_abi, 8); bpf_probe_read_kernel(&lab_len, sizeof(lab_len), (void *)&lab_len_ptr); bpf_probe_read_kernel(&cr_len, sizeof(lab_len), (void *)&cr_len_ptr); bpf_probe_read_kernel(&secret_len, sizeof(lab_len), (void *)&secret_len_ptr); Golang uretprobe 在eCapture的文本模式中,需要在加密之前、解密之后拿到明文,对应的两个函数分别是crypto/tls.(*Conn).writeRecordLocked和 crypto/tls.(*Conn).Read。加密之前的获取只需要使用eBPF uprobe HOOK即可实现。而解密之后,则需要uretprobe,但Golang里,uretprobe的实现机制,会破坏他的堆栈,导致Golang程序进程崩溃。 这个问题,在iovisor/bcc社区也有讨论:BCC issue: Go crash with uretprobe #1320,包括火焰图、eBPF的领导者Brendan Gregg,对这个问题也没有太好的办法。Gianluca Borello给了间接的解决方案,相对来说还是比较繁琐的,也有一定的crash风险,有兴趣的同学可以去看看。 eCapture里,加密之前的uprobe 已经完成hook,实现https/tls的请求内容明文捕获。但解密后的内容,暂时无法实现。笔者也在尝试其他思路,比如找到Read返回值的调用函数,在哪里使用uprobe实现,但这点逻辑比较偏业务层,调用者比较多,不太方便过滤。如果你有更好的办法,也欢迎提出来。 扩展阅读 Hooking Go from Rust - Hitchhiker’s Guide to the Go-laxy BCC issue: Go crash with uretprobe #1320 Interface method calls with the Go register ABI eCapture旁观者官网 eCapture旁观者 Github仓库 2023-03-12 4 0
eCapture旁观者:Android HTTPS明文抓包,无需CA证书 前言 Android上抓包HTTPS是不是越来越难了?高版本无法添加CA证书,抓包软件依赖太多,VPN模式、或HOOK程序时,会被APP检测到。对抗成本愈加增高。有什么万能的工具吗? 是的,eCapture for Android来了。以后在Android上抓HTTPS通讯包,再也不用安装CA证书了,再也不用下载一堆python依赖环境了,再也不用重打包ssl类库了,再也不用改一堆手机参数了,一键启用,简单明了。 eCapture简介 eCapture是一款无需CA证书即可抓获HTTPS明文的软件。支持pcapng格式,支持Wireshark直接查看。基于eBPF技术,仅需root权限,即可一键抓包。 eCapture中文名旁观者,即 当局者迷,旁观者清。 2022年年初上海疫情期间,笔者开始编写并开源,至今已经半年,GitHub上已经 4200个星星。 eCapture是基于eBPF技术实现的抓包软件,依赖系统内核是否支持eBPF。目前支持在操作系统上,支持了X86_64\ARM64的Linux kernel 4.18以上内核,支持了ARM64 Android(Linux) kernel 5.4以上版本。最新版是在2022年9月9日发布的v0.4.3版本。 演示视频 下载后,一条命令启动,干净利索。 ./ecapture tls -w ecapture.pcapng 先看演示视频,演示环境为Ubuntu 21.04、Android 5.4 (Pixel 6)。 模块功能 eCapture支持tls、bash、mysqld、postgres等模块的信息提取与捕获。本文仅讨论tls这个HTTPS/TLS明文捕获模块。 加密通讯明文捕获--tls模块 tls模块在加密通讯类库上,支持了openssl、gnutls、nspr/nss、boringssl等类库。 但在Android上,pcapng模式只支持boringssl,文本模式则都支持。 如何使用eCapture 环境依赖 操作系统 Linux kernel 4.18以上,Android kernel 5.4以上。 支持BPF,可选支持BTF(eCapture版本不同) root权限 版本选择 BPF CO-RE特性为BTF通用的格式,用于做跨内核版本兼容。有的Android手机是没开启BTF。可以查看系统配置确认,CONFIG_DEBUG_INFO_BTF=y为开启;CONFIG_DEBUG_INFO_BTF=n为关闭;其他为不支持BPF,无法使用eCapture。 cfc4n@vm-server:~$# cat /boot/config-`uname -r` | grep CONFIG_DEBUG_INFO_BTF CONFIG_DEBUG_INFO_BTF=y Android系统上,config是gzip压缩的,且配置文件目录也变了。可使用zcat /proc/config.gz命令代替。 eCapture默认发行了支持CO-RE的ELF程序。Android版会发行一个5.4内核不支持BTF(即没有CO-RE)的版本。 下载后,可以通过./ecapture -v确认。 非CO-RE版本的version信息中包含编译时的内核版本。 # no CO-RE eCapture version: linux_aarch64:0.4.2-20220906-fb34467:5.4.0-104-generic # CO-RE eCapture version: linux_aarch64:0.4.2-20220906-fb34467:[CORE] 若版本不符合自己需求,可以自行编译,步骤见文末。 全局参数介绍 全局参数重点看如下几个 root@vm-server:/home/cfc4n/# ecapture -h --hex[=false] print byte strings as hex encoded strings -l, --log-file="" -l save the packets to file -p, --pid=0 if pid is 0 then we target all pids -u, --uid=0 if uid is 0 then we target all users --hex 用于stdout输出场景,展示结果的十六进制,用于查看非ASCII字符,在内容加密、编码的场景特别有必要。 -l, --log-file= 保存结果的文件路径。 -p, --pid=0 捕获的目标进程,默认为0,则捕获所有进程。 -u, --uid=0 捕获的目标用户,默认为0,则捕获所有用户,对Android来说,是很需要的参数。 模块参数 root@vm-server:/home/cfc4n/project/ssldump# bin/ecapture tls -h OPTIONS: --curl="" curl or wget file path, use to dectet openssl.so path, default:/usr/bin/curl --firefox="" firefox file path, default: /usr/lib/firefox/firefox. --gnutls="" libgnutls.so file path, will automatically find it from curl default. --gobin="" path to binary built with Go toolchain. -h, --help[=false] help for tls -i, --ifname="" (TC Classifier) Interface name on which the probe will be attached. --libssl="" libssl.so file path, will automatically find it from curl default. --nspr="" libnspr44.so file path, will automatically find it from curl default. --port=443 port number to capture, default:443. --pthread="" libpthread.so file path, use to hook connect to capture socket FD.will automatically find it from curl. --wget="" wget file path, default: /usr/bin/wget. -w, --write="" write the raw packets to file as pcapng format. -i参数 -i参数为网卡的名字,Linux上默认为eth0,Android上默认为wlan0,你可以用这个参数自行指定。 输出模式 输出格式支持两种格式,文本跟pcapng文件。有三个参数, 默认,全局参数,输出文本结果到stdout -l 全局参数,保存文本结果的文件路径 -w 仅tls模块参数,保存pcapng结果的文件路径 类库路径 Linux上支持多种类库,不同类库的路径也不一样。 类库 参数路径 默认值 openssl/boringssl --libssl Linux自动查找,Android为/apex/com.android.conscrypt/lib64/libssl.so gnutls --gnutls Linux自动查找,Android pcapng模式暂未支持 nspr/nss --nspr Linux自动查找,Android pcapng模式暂未支持 文本模式 -l 或者不加 -w 参数将启用该模式。 支持openssl、boringssl、gnutls、nspr/nss等多种TLS加密类库。 支持DTLS、TLS1.0至TLS1.3等所有版本的加密协议。 支持-p、-u等所有全局过滤参数。 pcapng模式 -w 参数启用该模式,并用-i选择网卡名,Linux系统默认为eth0,Android系统默认为wlan0, 仅支持openssl、boringssl两个类库的数据捕获。暂不支持TLS 1.3协议。 类库与参数支持 在Linux系统上,大部分类库与参数都是可以支持的。但在Android系统上,因为内核与ARM架构的原因,支持的参数上,有一定的差异。 不同模式的参数支持 -p、-u两个全局参数,支持文本模式,不支持pcapng模式。这是因为pcapng模式是使用eBPF TC技术实现。 模式 -p -u --libsso --port 文本 ✅ ✅ ✅ ✅ pcapng ❌ ❌ ✅ ✅ 不同模式的类库以协议支持 模式 openssl(类库) boringssl(类库) TLS 1.0/1.1/1.2(协议) TLS 1.3(协议) 文本 ✅ ✅ ✅ ✅ pcapng ✅ ✅ ✅ ❌ pcapng模式暂时不支持TLS 1.3,TLS 1.3密钥捕获功能已经开发完成,只是遇到一些BUG,还在解决中。 笔者不是openssl的专家,对TLS 协议也不太熟。需要补充这两块的知识,解决起来成本比较高,也欢迎对这块擅长的朋友一起来解决。 与tcpdump联合使用 eCapture基于eBPF TC,实现了流量捕获,并保存到pcapng文件中。基于eBPF Uprobe实现了TLS Master Secret的捕获。并基于Wireshark的Decryption Secrets Block (DSB)标准,实现了gopacket的DSB功能,合并网络包与密钥,保存到pcapng中。 eCapture在网络包捕获上,没有tcpdump强大,不支持丰富的参数。你可以用eCapture捕获master secrets,用tcpdump捕获网络包,然后使用wiresahrk自定义设置密钥文件,配合使用。 网络包捕获 tcpdump 的常规用法,不再赘述。 密钥捕获 同时启用ecapture ,模式可以选文本或者pcapng,都会保存TLS的master secrets密钥数据到ecapture_masterkey.log中。 网络包查看 用Wireshark打开网络包文件,设置这个master key文件,之后就可以看到TLS解密后的明文了。 配置路径:Wireshark --> Preferences --> Protocols --> TLS --> (Pre)-Master-Secret log filename 参数 指定路径 默认路径 在Android上,Google使用了boring ssl类库,也就是C++语言在libssl基础上的包装。默认情况下,会使用/apex/com.android.conscrypt/lib64/libssl.so路径。 APP的类库确认 你可以使用lsof -p {APP PID}|grep libssl来确认。 若不是默认路径,则可以使用--libssl参数来指定。 高级用法 如果你需要查看的APP是自定义SSL类库,那么你可以自助修改eCapture来实现。 自定义函数名与offset 首先,需要确定HOOK函数的函数名或者符号表地址。 没有源码 如果你没有类库源码可以通过IDA等软件静态分析、动态调试,确定SSL Write的地址offset。在配置填写在user/module/probe_openssl.go文件中,对应的probe配置部分。 { Section: "uprobe/SSL_write", EbpfFuncName: "probe_entry_SSL_write", AttachToFuncName: "SSL_write", UprobeOffset: 0xFFFF00, // TODO BinaryPath: binaryPath, }, offset自动计算 如果你有源码,则可以自行阅读源码确定函数名或者符号表的地址。对于结构体的成员属性读取,则可以通过offsetof宏来自动计算。 // g++ -I include/ -I src/ ./src/offset.c -o off #include <stdio.h> #include <stddef.h> #include <ssl/internal.h> #include <openssl/base.h> #include <openssl/crypto.h> #define SSL_STRUCT_OFFSETS \ X(ssl_st, session) \ X(ssl_st, s3) \ X(ssl_session_st, secret) \ X(ssl_session_st, secret_length) \ X(bssl::SSL3_STATE, client_random) \ X(bssl::SSL_HANDSHAKE, new_session) \ X(bssl::SSL_HANDSHAKE, early_session) \ X(bssl::SSL3_STATE, hs) \ X(bssl::SSL3_STATE, established_session) \ X(bssl::SSL_HANDSHAKE, expected_client_finished_) struct offset_test { /* data */ int t1; bssl::UniquePtr<SSL_SESSION> session; }; int main() { printf("typedef struct ssl_offsets { // DEF \n"); #define X(struct_name, field_name) \ printf(" int " #struct_name "_" #field_name "; // DEF\n"); SSL_STRUCT_OFFSETS #undef X printf("} ssl_offsets; // DEF\n\n"); printf("/* %s */\nssl_offsets openssl_offset_%d = { \n", OPENSSL_VERSION_TEXT, OPENSSL_VERSION_NUMBER); #define X(struct_name, field_name) \ printf(" ." #struct_name "_" #field_name " = %ld,\n", \ offsetof(struct struct_name, field_name)); SSL_STRUCT_OFFSETS #undef X printf("};\n"); return 0; } 参数提取 对于参数,你需要确认被HOOK函数的参数类型,以便确认读取方式,可以参考kern/openssl_kern.c内的SSL_write函数实现。 编译 ARM Linux 编译 公有云厂商大部分都提供了ARM64 CPU服务器,笔者选择了腾讯云的。在广州六区中,名字叫标准型SR1(SR1即ARM 64CPU),最低配的SR1.MEDIUM2 2核2G即满足编译环境。可以按照按量计费方式购买,随时释放,比较划算。 操作系统选择ubuntu 20.04 arm64。 ubuntu@VM-0-5-ubuntu:~$sudo apt-get update ubuntu@VM-0-5-ubuntu:~$sudo apt-get install --yes wget git build-essential pkgconf libelf-dev llvm-12 clang-12 linux-tools-generic linux-tools-common ubuntu@VM-0-5-ubuntu:~$wget https://golang.google.cn/dl/go1.18.linux-arm64.tar.gz ubuntu@VM-0-5-ubuntu:~$sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.18.linux-arm64.tar.gz ubuntu@VM-0-5-ubuntu:~$for tool in "clang" "llc" "llvm-strip" do sudo rm -f /usr/bin/$tool sudo ln -s /usr/bin/$tool-12 /usr/bin/$tool done ubuntu@VM-0-5-ubuntu:~$export GOPROXY=https://goproxy.cn ubuntu@VM-0-5-ubuntu:~$export PATH=$PATH:/usr/local/go/bin 编译方法 ANDROID=1 make 命令编译支持core版本的二进制程序。 ANDROID=1 make nocore命令编译仅支持当前内核版本的二进制程序。 当Wireshark被eBPF增强 Wireshark + eBPF = eCapture 贡献者 感谢chriskaliX 、chenhengqi、vincentmli、huzai9527、yihong0618、sfx(4ft35t)等朋友的贡献。 感谢tiann Weishu在Android这个需求上的推动。 招聘 Leader直招,没有中间商赚差价。面向RASP、HIDS产品,JVM、Linux内核入侵检测等安全研发职位 JAVA资深研发工程师 golang高级工程师 更多职位见 https://www.cnxct.com/jobs/ 2022-09-13 4 0
eCapture:无需CA证书抓https网络明文通讯 eCapture介绍 eCapture是一款基于eBPF技术实现的用户态数据捕获工具。不需要CA证书,即可捕获https/tls的通讯明文。 项目在2022年3月中旬创建,一经发布,广受大家喜爱,至今不到两周已经1200多个Star。 作用 不需要CA证书,即可捕获HTTPS/TLS通信数据的明文。 在bash审计场景,可以捕获bash命令。 数据库审计场景,可以捕获mysqld/mariadDB的SQL查询。 官网 代码仓库见:https://github.com/ehids/ecapture ,也可以关注微信公众号「榫卯江湖」获取最新动态。 产品架构 eCapture系统用户态程序使用Golang语言开发,具有良好的系统兼容性,无依赖快速部署,更适合云原生场景。 内核态代码使用C编写,使用clang/llvm编译,生产bpf字节码后,采用go-bindata转化为golang语法文件,之后采用ehids/ebpfmanager类库,调用bpf syscall进行加载、HOOK、map读取。 golang编译后,无其他任何依赖即可运行,兼容linux kernel 4.18以上所有版本。 eBPF加载机制 关于eBPF详细加载机制,可到https://ebpf.io/ 查阅相关原理。 实现原理 如工作原理的图所示,在用户态的加密解密函数中下钩子。 tcpdump(libpcap)是在数据包接收到,XDP处理后,进行clone packet,进行包的复制,发送给用户态进程。二者工作的所在层不一样。 功能介绍 eCapture有三个模块 tls/ssl明文数据捕获 bash命令审计 mysqld数据库审计 第一个功能适用于基于tls/ssl解密需求的运维监控、故障排查、抽样分析场景。 第二个功能适用于安全领域的bash入侵发现场景,这里只是简单的功能,可以在此基础上增加其他功能。 第三个功能适用于数据库审计场景,尤其是做数据安全、数据防泄漏,甚至入侵检测等。同样,可以在此基础上扩充其他功能。 查看其使用说明,如下 cfc4n@vm-desktop:~/ehids/ecapture$ ./bin/ecapture NAME: ecapture - capture text SSL content without CA cert by ebpf hook. USAGE: ecapture [flags] VERSION: 0.1.5-20220325-47edbed COMMANDS: bash capture bash command help Help about any command mysqld56 capture sql queries from mysqld >5.6 . tls alias name:openssl , use to capture tls/ssl text content without CA cert. DESCRIPTION: ecapture是一款无需安装CA证书,即可抓去HTTPS、TLS等明文数据包的工具。 也可以捕获bash的命令,适用于安全审计场景。包括mysqld的数据库审计等。 仓库地址: https://github.com/ehids/ecapture OPTIONS: --debug[=false] enable debug logging -h, --help[=false] help for ecapture --hex[=false] print byte strings as hex encoded strings -p, --pid=0 if target_pid is 0 then we target all pids 其中,有四个全局参数,分别是 --debug , 用于启动调试日志 --help , 查看帮助 --hex ,按照hex模式打印字符,用与查看不可见字符 --pid ,用于针对特定进程进行数据捕获 HOOK机制 eCapture采用eBPF uprobe相关函数进行HOOK,故需要目标用户态函数信息,包含函数符号表(symbol table),函数偏移地址(offset)。 在大部分linux发行版中,使用的二进制可执行文件(ELF)都是包含符号表的;少部分发行版,会去掉ELF中的符号表。那么针对这种场景,就需要用户自行定位目标函数所在ELF/SO中的偏移地址,通过工具的参数来指定。 对于ELF文件,可以将目标类库静态编译到自身,也可以通过动态链接库的方式引用。那么对于这两种形式,eCapture根据不同场景进行自动查找。若查找不到,用户可以通过命令行参数指定。 故eCapture支持HOOK ELF,以及HOOK SO两种模式。会自动分析ELF文件,读取.dynamic和.dynsym等段信息,查找相关链接库名以及函数名、偏移地址。 查找原理如下图: tls/ssl ecapture tls命令用于启动tls/ssl模块,支持了三类tls/ssl加密类库,分别是 openssl ,动态链接库名字为libssl.so gnutls ,动态链接库名字为libgnutls.so nss/nspr ,动态链接库名字为libnspr4.so 在不同的linux发行版中,因为各种原因,会选择不同的类库。比如wget程序,在ubuntu跟centos中就会使用不同的类库。有的是openssl,有的是gnutls,甚至两个库都引入了。 具体情况,你可以使用ldd $ELF_PATH | grep -E "tls|ssl|nspr|nss"来查看一个ELF文件使用类库情况。 cfc4n@vm-desktop:~$ ldd `which wget` |grep -E "tls|ssl|nspr|nss" libssl.so.1.1 => /lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007f50699f6000) 对于firefox、chrome这种进程,需要在程序启动后才能看到tls类库依赖情况,那么,你可以使用sudo pldd $PID | grep -E "tls|ssl|nspr|nss" 来查看 cfc4n@vm-desktop:~$ ps -ef|grep firefox cfc4n 6846 1432 45 17:50 ? 00:00:04 /usr/lib/firefox/firefox -new-window cfc4n@vm-desktop:~$ sudo pldd 6846 |grep -E "tls|ssl|nspr|nss" /usr/lib/firefox/libnspr4.so /usr/lib/firefox/libnssutil3.so /usr/lib/firefox/libnss3.so /usr/lib/firefox/libssl3.so /lib/x86_64-linux-gnu/libnss_files.so.2 /lib/x86_64-linux-gnu/libnss_mdns4_minimal.so.2 /lib/x86_64-linux-gnu/libnss_dns.so.2 /usr/lib/firefox/libnssckbi.so eCapture的tls模块命令行参数如下,用户可以使用默认配置外,也可以根据自己环境自行指定。 OPTIONS: --curl="" curl or wget file path, use to dectet openssl.so path, default:/usr/bin/curl --firefox="" firefox file path, default: /usr/lib/firefox/firefox. --gnutls="" libgnutls.so file path, will automatically find it from curl default. -h, --help[=false] help for tls --libssl="" libssl.so file path, will automatically find it from curl default. --nspr="" libnspr44.so file path, will automatically find it from curl default. --wget="" wget file path, default: /usr/bin/wget. 同时,使用方法也比较简单,./ecapture tls --hex命令即可。 在linux上,firefox程序中,有很多通讯都使用了/usr/lib/firefox/libnspr4.so,但实际上业务请求是可以通过Socket Thread进程来发送的。可以通过这个特点来过滤,对于chrome程序,相信细心的你,也能搞定。 bash 笔者在安全部门工作,接到过bash审计需求,其实现方法无非是修改系统类库、使用内核模块等技术实现,对系统稳定性有一定风险。基于eBPF技术实现,可以避开这些问题。这里的bash命令的监控,是作为eBPF技术在安全审计场景中的一个探索。 eCapture在实现时首先查找ENV的$SHELL值,作为bash的二进制文件路径进行HOOK。对于bash加载了libreadline.so的场景,也会自动分析,进行符号表查找、offset定位,再进行HOOK。 bash模块的参数有三个,用户可以自定义bash、readlineso的路径。 OPTIONS: --bash="" $SHELL file path, eg: /bin/bash , will automatically find it from $ENV default. -h, --help[=false] help for bash --readlineso="" readline.so file path, will automatically find it from $BASH_PATH default. mysql/mariadb 与bash模块一样,也是作为数据库审计的一个探索。笔者环境为ubuntu 21.04,mysqld也因为协议关系,使用了衍生的MariadDB,用户也可以根据自己实际场景,使用命令行参数进行指定。 mysqld模块,核心原理是HOOK了dispatch_command函数, 第一个参数为CMD类型,值为COM_QUERY时,为查询场景,即审计需求的查询类型。 第二个参数是THD的结构体,在这里我们用不到。 第三个是查询的SQL语句 第四个参数是SQL语句的长度, // https://github.com/MariaDB/server/blob/b5852ffbeebc3000982988383daeefb0549e058a/sql/sql_parse.h#L112 dispatch_command_return dispatch_command(enum enum_server_command command, THD *thd, char* packet, uint packet_length, bool blocking = true); mysqld审计模块参数如下: OPTIONS: -f, --funcname="" function name to hook -h, --help[=false] help for mysqld56 -m, --mysqld="/usr/sbin/mariadbd" mysqld binary file path, use to hook --offset=0 0x710410 其中,--mysqld是用来指定mysqld的路径。 mysqld二进制程序符号表里虽然有dispatch_command信息,但dispatch_command这个函数名每次编译都是变化的,故不能写死。 eCapture的查找方式是读取mysqld二进制的.dynamic段信息,正则语法\w+dispatch_command\w+去匹配所有符号信息,找到其函数名、偏移地址,再使用。 你也可以通过objdump命令来查找,再通过命令行参数自行指定funcname。 mariadbd version : 10.5.13-MariaDB-0ubuntu0.21.04.1 objdump -T /usr/sbin/mariadbd |grep dispatch_command 0000000000710410 g DF .text 0000000000002f35 Base _Z16dispatch_command19enum_server_commandP3THDPcjbb 即offset为0x710410,函数名为_Z16dispatch_command19enum_server_commandP3THDPcjbb。 使用 下载二进制包 eCapture发布在https://github.com/ehids/ecapture/releases ,目前最新版为eCapture v0.1.5。 可在linux kernel 4.18以上版本运行。 二进制包地址: https://github.com/ehids/ecapture/releases/download/v0.1.5/ecapture_v0.1.5.zip 国内加速地址: https://github.do/https://github.com/ehids/ecapture/releases/download/v0.1.5/ecapture_v0.1.5.zip 自行编译 代码仓库在https://github.com/ehids/ecapture ,你可以自行修改源码编译。 演示视频 腾讯视频: B站: https://www.bilibili.com/video/BV1si4y1Q74a Github仓库 2022-03-27 +3 7 0
Clash Royale皇室战争的黑盒技术分析 今年开春,Clash Royale《皇室战争》在国内上架,又是SuperCell公司的大作,想必会非常火。在去年年底我层玩过封测版本,掉线比较严重,但仍没影响到我对它的好奇,我很想知道它的网络同步是如何实现的,战斗那块是帧同步吗?全是客户端计算的吗?这些疑问一直困扰着我,这次在国内上线了,网络状况也好,调试起来会更会舒心,更方便。 为此,我在工作之余,开始对Clash Royale《皇室战争》进行了黑盒分析,洋洋洒洒写了80多页的PPT,用于团队内部分享。顺便也发在博客上,让博客看上去有点生机。 由于我逆向基础差,对clash royale的libg.so分析零零散散的将近两周,一直没能搞定sc文件内容的编码规则,没搞定通讯的消息包结构编码规则,直到团队内部分享日期逼近,才决定暂停分析,尽快收尾,先完成分享,以后再继续。 而我又不想就这样放弃,在分享前后的几天里,我内心一直纠结这事,放不下来,抽空就想去再调试调试,为此落下了工作,计划中的功能也没给其他兄弟们做个好开头,没专心把主体框架搭建好,直到另外一个兄弟简介的提醒,列出大纲框架,我才意识到已经耽误很大的进度了,羞愧难当,今天(周六)本没打算去公司加班的,又把周五背回来的电脑又背回去,乖乖的写点东西,才让我内心平静下来。现在想想,今日凌晨3点多的恶梦或许就是因为愧疚吧。现在这里做个了结,不在工作时间去分析这个问题,不能再耽误工作进度。 分享里,更多的是介绍思路,我本身因为逆向功底原因,未能实现协议的解密,是借助coc-proxy这个项目来完成的。同样,关于SC文件解码部分,也是参考COC的SC文件拼接规则进行的。感谢github社区的技术资料。 PDF如下,由于内容较多(127页),加载时间可能较长,请耐心等待...另外,本PDF会持续更新,增加内容以及修正错误,请来本站阅读最新版本。 希望自己可以在空闲时间里把遗漏的知识点补充的扎实点。 2016年5月17日,公开了公司内部版(公司内部应该修复完毕),127页,10M左右,包含公司内两款手游的解密过程,以及手机游戏客户端的资源、协议、加密密钥保护方案,常见反外挂方案等。希望可以帮助到同样做游戏的朋友。 下载地址: clash royale皇室战争静态资源解密过程与常见手游客户端保护方案 2016-04-10 +2 6 0
在mac osx上看lol国服ob录像的技术分析 一天,兴起而至,想在mac osx上看lol ob,要看国服的,没现成方案,实现不了,未遂,便决定研究一番。 在拳头游戏官方的api中SPECTATING GAMES riot games api提到观看ob模式的程序启动参数: "C:\Riot Games\League of Legends\RADS\solutions\lol_game_client_sln\releases\0.0.1.74\ deploy\League of Legends.exe" "8394" "LoLLauncher.exe" "" "spectator spectator.na.lol.riotgames.com:80 P+C3YqI3Mg9oHc6t9eTAKWE4T8prxwzR 1726229459 NA1" 程序启动的第三个参数中: 这里面包括ob server地址、端口,以及本场录像文件加密key,以及游戏id,游戏所属平台id。其中 encryptionKey参数每次都不一样,从数据包分析发现,这个key并不是为了在获取录像时的验证,我觉得是roitgames为了安全,为每场游戏的协议数据包做的加密key,客户端观看ob 录像时,用这个key解密,以提高协议安全度。 在riotgames官方api介绍上,以及github上几个开源的lol项目上,以及本人wireshark抓包分析总结出一场ob游戏观看,一共调用了如下http api接口 GET /observer-mode/rest/consumer/version //获取录像文件版本信息,返回字符串 GET /observer-mode/rest/consumer/getGameMetaData/:region/:id/*ignore //获取游戏的基本信息,包括游戏当前桢,最后一个chunkid,游戏开始时间等信息。返回json格式字符串 GET /observer-mode/rest/consumer/getLastChunkInfo/:region/:id/:end/*ignore //获取录像中最后一个chunk基本信息,包括了当前chunkid,下一个chunkid,当前桢的id之类信息,返回json格式字符串 GET /observer-mode/rest/consumer/getGameDataChunk/:region/:id/:chunkId/*ignore //获取对应chunkid的协议数据,返回字节流 GET /observer-mode/rest/consumer/getKeyFrame/:region/:id/:frame/*ignore //获取对应chunkid的协议数据,返回字节流 lol ob观战模式的实现,是将游戏中所有玩家操作,合并成一个chunk块,来发送到客户端的,以减少通讯量。 GET /record/:region/:gameId/:encryptionKey 这个api在github上有提到过,但我抓包过程中,没有看到被调用过。暂时忽略。 至此已经知道lol 英雄联盟的观战通讯过程,但全球大区中,我们腾讯服务器跟欧美服务器不一样,并没有公开的ob server地址。而是在tgp腾讯游戏助手中实现的。 这是个嵌入的html,真实地址是 http://api.tgp.qq.com/spectator/v1410/watch.shtml ,对应的ob 观战列表文件为 http://api.pallas.tgp.qq.com/core/get_ob_list ,以ob文件http://ob.pallas.tgp.qq.com/ob_data/1_1632443320.ob 来举例子。对于国外其它服务器的观战,是连接到官方的观战服务器进行的。而腾讯lol的观战,却是将录像文件下载到本地的,那是否可说明tgp助手在本地建立了一个http服务器,并解析ob文件,实现ob观战服务器的功能呢? 为此开始确认这个过程,在tgp腾讯游戏助手上观战了这场游戏,查看游戏进程的启动参数中,ob server地址是多少,开Process Monitor抓进程启动参数,开tgp助手观战: 如上图所示,启动参数复合riotgame的api介绍,我们来确认下127.0.0.1: 36722 是否正确,以及是哪个进程开启的。 replay.exe进程启动详情: 腾讯tgp助手,确实在本地开启http ob server来实现的。那么进一步抓包验证。对于回环地址,wireshark抓不到的,要用RawCap来抓。 同时,wireshark 抓取 ob录像文件下载的数据包,查看localhost.pcanpng ,果然是lol ob录像系统调用的几个http请求。 一一查看version\getGameMetaData\getLastChunkInfo\getGameDataChunk\getKeyFrame几个请求。也确实以相应字符串、json字符串、字节流的形式返回,但数据源头来自哪里呢?肯定是最初的ob文件了。 如上图,其中开头的每行分隔的字符串,、json字符串也比较好理解,里面的元素也是lol ob录像系统请求相应的json字符串 LOLOB/1.0 abstract:{"src": 1, "area_id": 1, "score": 5625000, "game_length": 1260, "battle_type": 4, "max_tier": [0, 0], "game_id": 1632443320, "start_time": "2015-10-21 08:13:37", "ob_ver": "1.82.89", "encryption_key": "x83U9UPUuB/+INJyaU2Wz8lUuLn5aXEt"} source:{"gameId": 1632443320, "gameStartTime": 1445386417603, "platformId": "HN1", "gameMode": "CLASSIC", "mapId": 11, "gameType": "MATCHED_GAME", "gameQueueConfigId": 4, "observers": {"encryptionKey": "x83U9UPUuB/+INJyaU2Wz8lUuLn5aXEt"}, "participants": [{"profileIconId": 7, "championId": 60, "summonerName": "\u5fc3\u6001\u597d\u80fd\u4e0a\u5206\u5417", "skinIndex": 2, "bot": false, "spell2Id": 11, "teamId": 100, "spell1Id": 4}, {"profileIconId": 15, "championId": 117, "summonerName": "\u4e5d\u670813\u65e5\u4e36", "skinIndex": 5, "bot": false, "spell2Id": 4, "teamId": 100, "spell1Id": 12}, {"profileIconId": 841, "championId": 58, "summonerName": "\u501a\u7af9\u8f7b\u8bed\u767d\u886b\u5982\u6545", "skinIndex": 2, "bot": false, "spell2Id": 4, "teamId": 100, "spell1Id": 12}, {"profileIconId": 17, "championId": 429, "summonerName": "\u840c\u515c\u515cTcT", "skinIndex": 2, "bot": false, "spell2Id": 7, "teamId": 100, "spell1Id": 4}, {"profileIconId": 914, "championId": 122, "summonerName": "\u91cd\u5e86\u897f\u5357\u5927\u5b66\u6821\u8349", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 100, "spell1Id": 14}, {"profileIconId": 922, "championId": 105, "summonerName": "L4ugh1ng", "skinIndex": 8, "bot": false, "spell2Id": 12, "teamId": 200, "spell1Id": 4}, {"profileIconId": 1, "championId": 28, "summonerName": "\u5170\u4ead\u5e8f", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 200, "spell1Id": 11}, {"profileIconId": 9, "championId": 203, "summonerName": "\u8292\u5e02\u9189\u9999\u56ed\u996d\u5e84", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 200, "spell1Id": 12}, {"profileIconId": 931, "championId": 110, "summonerName": "15\u5c81\u5357\u660c\u4eba\u670d\u4e0d\u670d", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 200, "spell1Id": 7}, {"profileIconId": 28, "championId": 432, "summonerName": "\u6218\u4e36\u65d7TV\u767d\u8272\u98ce\u8f66", "skinIndex": 1, "bot": false, "spell2Id": 14, "teamId": 200, "spell1Id": 4}], "gameTypeConfigId": 2, "gameLength": 5, "bannedChampions": [{"teamId": 100, "championId": 41, "pickTurn": 1}, {"teamId": 200, "championId": 157, "pickTurn": 2}, {"teamId": 100, "championId": 82, "pickTurn": 3}, {"teamId": 200, "championId": 114, "pickTurn": 4}, {"teamId": 100, "championId": 4, "pickTurn": 5}, {"teamId": 200, "championId": 76, "pickTurn": 6}]} obmeta:{"lastChunkId": 14, "gameKey": {"platformId": "HN1", "gameId": 1632443320}, "startTime": "Oct 21, 2015 8:13:37 AM", "endGameKeyFrameId": -1, "gameLength": 0, "keyFrameTimeInterval": 60000, "port": 0, "gameServerAddress": "", "interestScore": 2989, "clientBackFetchingEnabled": false, "clientAddedLag": 30000, "endStartupChunkId": 7, "chunkTimeInterval": 30000, "clientBackFetchingFreq": 1000, "pendingAvailableKeyFrameInfo": [], "decodedEncryptionKey": "", "createTime": "Oct 21, 2015 8:10:24 AM", "featuredGame": true, "gameEnded": false, "encryptionKey": "", "delayTime": 150000, "endGameChunkId": -1, "pendingAvailableChunkInfo": [], "startGameChunkId": 9, "lastKeyFrameId": 3} keyframe_tab:[[1, 9], [2, 11], [3, 13], [4, 15], [5, 17], [6, 19], [7, 21], [8, 23], [9, 25], [10, 27], [11, 29], [12, 31], [13, 33], [14, 35], [15, 37], [16, 39], [17, 41], [18, 43], [19, 45], [20, 47], [21, 49]] chunk_tab:[[1, 0], [2, 0], [3, 0], [4, 0], [5, 0], [6, 0], [7, 0], [8, 0], [9, 30018], [10, 30008], [11, 29986], [12, 30007], [13, 30000], [14, 29985], [15, 30019], [16, 29981], [17, 30000], [18, 30008], [19, 30003], [20, 29982], [21, 30012], [22, 30011], [23, 29980], [24, 29993], [25, 30018], [26, 29982], [27, 30018], [28, 29984], [29, 30020], [30, 29984], [31, 30016], [32, 29980], [33, 29994], [34, 30009], [35, 30016], [36, 29997], [37, 29976], [38, 30006], [39, 30008], [40, 29985], [41, 30000], [42, 30027], [43, 29972], [44, 30006], [45, 30016], [46, 29977], [47, 30024], [48, 29990], [49, 29994], [50, 20015]] 不难看出,其中“LOLOB/1.0”为tgp助手本身识别的版本信息;abstract是ob录像版本,以及录像数据加密key存储的基本信息;obmeta对应着getGameMetaData的数据;keyframe_tab跟chunk_tab暂时不知道是什么意思,可稍后再回来看。 ob文件的剩下其它部分,显然就是录像中,每个chunk包的录像数据的了,那它是如何拼装的呢? 腾讯tgp助手下载的ob文件中,录像部分开头的字节流是01 00 01 00 00 35 88 85 a0 4a 30 1b 30 27 d5 3b eb b2 12 4d 8c 0d 62 52 0b 15 9f d1 7a d4 98 0c 9d 01 7e d7 b6 6b,回头看看lol游戏进程获取ob录像chunk部分时数据包的内容开头部分 开头的字节流是85 a0 4a 30 1b 30 27 d5 3b eb b2 12 4d 8c 0d 62 52 0b 15 9f d1 7a d4 98 0c 9d 01 7e d7 b6 6b 7d,相比之下,重复部分是85 a0 4a 30 1b 30 27 d5 3b eb b2 12 4d 8c 0d 62 52 0b 15 9f d1 7a d4 98...,再多找几个录像获取数据的数据包,核对一下 可以确认,lol的ob录像回放协议中,协议头是85 a0 4a 30 1b 30 27 d5,也就是说,tgp下载的ob文件字节流部分,85 a0 4a 30 1b 30 27 d5前面的都不是lol协议的,这开头的01 00 01 00 00 35 88这7个字节为tgp读取识别的协议头部分。最初replay.exe程序读取理解的部分,本身不是lol客户端读取的协议部分。那么,这01 00 01 00 00 35 887个字节分别代表什么含义呢? 那我们开始找85 a0 4a 30 1b 30 27 d5出现的位置即可(关于在数M数据中,查找几个字节所在位置的地方,你最好有比较简便的方法,千万别告诉我你是一行一行看的。) 可以看出如下结果: 第一个在 00001124 部分,前面7个字节是01 00 01 00 00 35 88; 第二个在 000046B3 部分,前面7个字节是02 00 01 00 00 2b 10; 第三个在 000071CA 部分,前面7个字节是01 00 02 00 00 60 48; (wireshark会告诉你当前行所在的位置编号,记得加上所关注字节的偏移位置) 到了这里,根据编程灵(经)感(验),会这么猜想:第二个位置跟第一个位置相差了46B3-1124=358F个位置,也就是3588+7=358F(tgp协议头中,最后两个字节3588 + 协议头长度 7),用第三个跟第二个最比较以确认猜想对不对。71CA-46B3=2B17,即2B10+7符合猜测。 到此,可确认tgp协议头的7个字节中,最后2个字节表示这后续包的长度。但这后续包长度是否仅仅是后面2个字节所标示的,是不是后面3字节,还是4字节?还不能确定。能确定的是,开头的3个字节肯定不会表示后续包长度作用。如果你还是没发现这规律,那可不能怪我了,谁让你当时没有灵(jing)感(yan)呢? 如此一来,可以用更快的方法验证这个猜想,写一段程序,从ob录像文件中,分析tgp协议头,根据后2个字节对应长度的数据包,打印出前面tgp协议头的7字节,再打印后面8个字节(确认是否为85 a0 4a 30 1b 30 27 d5) [1 0 1 0 0 53 136] [133 160 74 48 27 48 39 213 59 235 178 18 77 140 13 98 82 11 21 159 209 122 212] [2 0 1 0 0 43 16] [133 160 74 48 27 48 39 213 54 53 74 75 93 254 127 91 102 147 88 122 160 207 84] [1 0 2 0 0 96 72] [133 160 74 48 27 48 39 213 240 224 50 221 162 250 17 161 77 240 23 76 184 68 252] [2 0 2 0 0 78 80] [133 160 74 48 27 48 39 213 137 33 26 130 55 190 142 64 241 79 180 111 211 4 8] [1 0 3 0 0 125 208] [133 160 74 48 27 48 39 213 156 71 54 190 104 32 173 195 78 15 247 95 154 20 4] [2 0 3 0 0 124 192] [133 160 74 48 27 48 39 213 71 93 96 97 131 246 103 242 117 117 242 122 102 57 252] [1 0 4 0 0 153 128] [133 160 74 48 27 48 39 213 199 1 236 10 13 19 41 186 138 120 76 94 198 235 231] [2 0 4 0 0 126 48] [133 160 74 48 27 48 39 213 161 190 218 133 34 189 202 6 4 210 38 115 68 138 93] [1 0 5 0 0 145 56] [133 160 74 48 27 48 39 213 181 160 40 167 228 7 106 250 239 177 147 143 223 218 189] [2 0 5 0 0 124 208] [133 160 74 48 27 48 39 213 133 89 166 5 196 161 242 20 46 198 139 26 18 52 142] [1 0 6 0 0 180 152] [133 160 74 48 27 48 39 213 199 1 236 10 13 19 41 186 60 43 67 53 87 214 163] [2 0 6 0 0 130 120] [133 160 74 48 27 48 39 213 158 207 13 38 213 94 203 78 214 51 158 92 104 197 112] [1 0 7 0 0 34 32] [133 160 74 48 27 48 39 213 178 213 133 20 42 35 24 186 160 140 25 92 252 210 242] [2 0 7 0 0 140 112] [133 160 74 48 27 48 39 213 46 67 112 101 89 72 41 205 82 117 228 27 204 109 200] [1 0 8 0 0 82 24] [133 160 74 48 27 48 39 213 142 246 116 70 48 41 45 65 120 25 184 155 91 123 174] [2 0 8 0 0 132 128] [133 160 74 48 27 48 39 213 253 10 27 170 202 136 197 234 239 187 196 5 166 215 182] [1 0 9 0 0 132 32] [133 160 74 48 27 48 39 213 63 45 71 158 103 71 129 113 196 148 131 177 29 128 162] [2 0 9 0 0 131 200] [133 160 74 48 27 48 39 213 52 170 14 71 17 205 63 28 76 253 41 134 241 227 200] [1 0 10 0 0 70 0] [133 160 74 48 27 48 39 213 9 8 197 104 10 153 18 70 24 239 30 230 123 22 102] [2 0 10 0 0 151 72] [133 160 74 48 27 48 39 213 208 112 28 134 21 105 39 113 51 90 157 36 215 94 173] [1 0 11 0 0 69 192] [133 160 74 48 27 48 39 213 207 49 23 198 37 84 251 4 153 255 28 239 177 47 13] [2 0 11 0 0 147 232] [133 160 74 48 27 48 39 213 64 115 149 233 120 244 31 210 191 42 252 38 126 32 27] [1 0 12 0 1 2 80] [133 160 74 48 27 48 39 213 186 139 142 5 35 179 166 152 135 234 102 89 109 33 6] [178 161 62 95 113 100 101] [113 58 47 114 56 216 181 49 171 213 150 80 50 222 228 94 34 110 55 170 212 163 177] [2 213 106 162 142 22 91] [8 155 150 15 27 47 191 79 102 112 16 90 168 160 96 244 195 13 72 185 16 118 29] 当tgp协议头的第五个字节不为0时,开始发生了错乱,截取后续包的内容是113 58 47 114 56 216 181 49,并非133 160 74 48 27 48 39 213(即85 a0 4a 30 1b 30 27 d5的十进制),说明tgp最后2字节表示后续包长度不对,应该继续增加,从目前的错误来看,起码是3个字节,即第5、6、7个字节。但如果是3个字节,而第4个字节也一直是0,显然也不合适。我决定将用后面4个字节表示剩余包的长度来检验一次。如下图: 结果都正确的打印了tgp协议头,以及后续8字节的85 a0 4a 30 1b 30 27 d5(对应十进制为:133 160 74 48 27 48 39 213) 至此,tgp协议头的后4个字节已经确认含义了,那么剩余前3个字节中,第一个字节有两种数值01、02,这两个数值的含义也比较好弄明白,这时,我的灵(jing)感(yan),他们分别对应getGameDataChunk、getKeyFrame的两种类型,可以从tgp录像请求的http中加以确认,如下图: 从本地抓取127.0.0.1的通讯包中,找http请求为getGameDataChunk、getKeyFrame的相应包,对照响应body中,字节流跟ob文件分析结果中,对应的是否一致。请求url为getGameDataChunk的,到tgp协议头第一个字节01的字节流中找。请求为getKeyFrame的,到第一个字节02中查找。这里也可以确认无误了。 至此,tgp协议头中,只剩下第2、3个字节不名含义。在汇总录像播放时,请求的所有url,你会发现getGameDataChunk、getKeyFrame请求中,对应的id参数是递增的,如下图: 再根据最初打印的所有tgp协议头7字节内容中,当第1字节一样时,发现第2字节不变,而第3个字节持续递增,这时有灵(jing)感(yan)了吗?第3个字节肯定是url请求中第n个包的含义。第二字节也可能跟第三字节构成uint16类型的总包数的含义。若表示一场游戏中,录像回放系统中,全部数据包分包数只用1字节表示的话,那一共可存储1<<8 -1 也就是255个,数量可能不够,所以,我假设第二、第三字节一起表示录像回放总包的含义。至此,tgp的分包方式中,包头含义已经全部表示明白了。那么,剩下的,就时写个结构体或者map来存储这些字节流即可。 到这里,ob文件分析完成了,对于lol ob server请求的几个url中,只剩下getLastChunkInfo请求没有处理了,你要知道,ob文件最初的几个json字符串中,还剩下chunk_tab、keyframe_tab 聪明的你,这时应该有灵(jing)感(yan)了吧。你来实现实时?什么?还是没灵(jing)感(yan)?再见...... 程序写好后,可以体验一下mac上lol客户端,看国服ob咯,不联网也可以看哦,只要从tgp助手那下载了ob录像文件。 国服客户端,提供观战功能并不能用,每次都是无法观战,或者观战数据加载错误之类的,不提供实时观战功能。ob录像请求的url返回404。 英雄联盟,游戏分为4个主进程 更新进程:LoLpatcher 启动进程:LoLLauncher 客户端(游戏大厅):LolClient 游戏进程:LeagueofLegends 这里已实现mac osx上观看lol ob录像,故可得知mac lol 游戏主进程 可以解析 国服lol 产生的lol协议文件,可大约证明游戏进程是兼容的,甚至一致的。。 更新进程上关系不大,只要保证版本一致即可。启动进程跟客户端两个进程实现了登陆认证功能,要做的,是实现这国服qq登陆认证绑定,也就是说mac osx上玩国服lol 理论上可行....要不,你来试试? 代码开源: 2015-10-27 +3 7 0
TCP SOCKET中backlog参数的用途是什么? 在前年时,业务中遇到好多次因为PHP-FPM的backlog参数引发的性能问题,一直想去详细研究一番,还特意在2013年总结里提到这事《为何PHP5.5.6中fpm backlog Changed default listen() backlog to 65535》,然而,我稍于懒惰,一拖再拖,直至今日,才动脑去想,动笔去写。 2013年12月14发布的PHP5.5.6中,changelog中有一条变更, FPM: Changed default listen() backlog to 65535. 这条改动,是在10月28日改的,见increase backlog to the highest value everywhere It makes no sense to use -1 for *BSD (which is the highest value there) and still use 128 for Linux. Lets raise it right to up the limit and let the people lower it if they think that 3.5Mb is too much for a process. IMO this is better than silently dropping connections. patch提交者认为,提高backlog数量,哪怕出现timeout之类错误,也比因为backlog满了之后,悄悄的忽略TCP SYN的请求要好。 当我最近开始想好好了解一下backlog的细节问题时,发现fpm的默认backlog已经不是65535了,现在是511了。(没写在changelog中,你没注意到,这不怪你。)我翻阅了github提交记录,找到这次改动,是在2014年7月22日,Set FPM_BACKLOG_DEFAULT to 511 It is too large for php-fpm to set the listen backlog to 65535. It is realy NOT a good idea to clog the accept queue especially when the client or nginx has a timeout for this connection. Assume that the php-fpm qps is 5000. It will take 13s to completely consume the 65535 backloged connections. The connection maybe already have been closed cause of timeout of nginx or clients. So when we accept the 65535th socket, we get a broken pipe. Even worse, if hundreds of php-fpm processes get a closed connection they are just wasting time and resouces to run a heavy task and finally get error when writing to the closed connection(error: Broken Pipe). The really max accept queue size will be backlog+1(ie, 512 here). We take 511 which is the same as nginx and redis. 其中理由是“backlog值为65535太大了。会导致前面的nginx(或者其他客户端)超时”,而且提交者举例计算了一下,假设FPM的QPS为5000,那么65535个请求全部处理完需要13s的样子。但前端的nginx(或其他客户端)已经等待超时,关闭了这个连接。当FPM处理完之后,再往这个SOCKET ID 写数据时,却发现连接已关闭,得到的是“error: Broken Pipe”,在nginx、redis、apache里,默认的backlog值兜是511。故这里也建议改为511。(后来发现,此patch提交者,是360团队「基础架构快报」中《TCP三次握手之backlog》的作者shafreeck) 对于backlog的参数的含义,在linux kernel手册中也给了注释说明listen - listen for connections on a socket #include /* See NOTES */ #include int listen(int sockfd, int backlog); Description listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2). The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET. The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. If a connection request arrives when the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds. Notes To accept connections, the following steps are performed: 1. A socket is created with socket(2). 2. The socket is bound to a local address using bind(2), so that other sockets may be connect(2)ed to it. 3. A willingness to accept incoming connections and a queue limit for incoming connections are specified with listen(). 4. Connections are accepted with accept(2). POSIX.1-2001 does not require the inclusion of , and this header file is not required on Linux. However, some historical (BSD) implementations required this header file, and portable applications are probably wise to include it. The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See tcp(7) for more information. If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128. In kernels before 2.4.25, this limit was a hard coded value, SOMAXCONN, with the value 128. backlog的定义是已连接但未进行accept处理的SOCKET队列大小,已是(并非syn的SOCKET队列)。如果这个队列满了,将会发送一个ECONNREFUSED错误信息给到客户端,即 linux 头文件 /usr/include/asm-generic/errno.h中定义的“Connection refused”,(如果协议不支持重传,该请求会被忽略)如下: 在linux 2.2以前,backlog大小包括了半连接状态和全连接状态两种队列大小。linux 2.2以后,分离为两个backlog来分别限制半连接SYN_RCVD状态的未完成连接队列大小跟全连接ESTABLISHED状态的已完成连接队列大小。互联网上常见的TCP SYN FLOOD恶意DOS攻击方式就是用/proc/sys/net/ipv4/tcp_max_syn_backlog来控制的,可参见《TCP洪水攻击(SYN Flood)的诊断和处理》。 在使用listen函数时,内核会根据传入参数的backlog跟系统配置参数/proc/sys/net/core/somaxconn中,二者取最小值,作为“ESTABLISHED状态之后,完成TCP连接,等待服务程序ACCEPT”的队列大小。在kernel 2.4.25之前,是写死在代码常量SOMAXCONN,默认值是128。在kernel 2.4.25之后,在配置文件/proc/sys/net/core/somaxconn (即 /etc/sysctl.conf 之类 )中可以修改。我稍微整理了流程图,如下: 如图,服务端收到客户端的syn请求后,将这个请求放入syns queue中,然后服务器端回复syn+ack给客户端,等收到客户端的ack后,将此连接放入accept queue。 大约了解其参数代表意义之后,我稍微测试了一番,并抓去了部分数据,首先确认系统默认参数 在FPM的配置中,listen.backlog值默认为511,而如上结果中看到的Send-Q却是128,可见确实是以/proc/sys/net/core/somaxconn跟listen参数的最小值作为backlog的值。 apache ab这边的结果中,非2xx的http响应有271个,在NGINX日志数据如下: 从nginx结果中看出,本次压测总请求数为10000。http 200响应数量9729个;http 502 响应数量186个;http 504响应数量未85个;即非2xx响应总数为502+504总数,为271个。同时,也跟error.log中数据吻合。同时,也跟TCP数据包中的RST包数量吻合。 在nginx error中,错误号为111,错误信息为“Connection refused”的有186条,对应着所有http 502响应错误的请求;错误号为110,错误信息为“Connection timed out”的有85条,对应着所有http 504响应错误的请求。在linux errno.h头文件定义中,错误号111对应着ECONNREFUSED;错误号110对应着ETIMEDOUT。linux man手册里,对listen参数的说明中,也提到,若client连不上server时,会报告ECONNREFUSED的错。 Nginx error日志中的详细错误如下: 在压测的时候,我用tcpdump抓了通讯包,配合通信包的数据,也可以看出,当backlog为某128时,accept queue队列塞满后,TCP建立的三次握手完成,连接进入ESTABLISHED状态,客户端(nginx)发送给PHP-FPM的数据,FPM处理不过来,没有调用accept将其从accept quque队列取出时,那么就没有ACK包返回给客户端nginx,nginx那边根据TCP 重传机制会再次发从尝试...报了“111: Connection refused”错。当SYNS QUEUE满了时,TCPDUMP的结果如下,不停重传SYN包。 对于已经调用accept函数,从accept queue取出,读取其数据的TCP连接,由于FPM本身处理较慢,以至于NGINX等待时间过久,直接终止了该fastcgi请求,返回“110: Connection timed out”。当FPM处理完成后,往FD里写数据时,发现前端的nginx已经断开连接了,就报了“Write broken pipe”。当ACCEPT QUEUE满了时,TCPDUMP的结果如下,不停重传PSH SCK包。(别问我TCP RTO重传的机制,太复杂了,太深奥了 、TCP的定时器系列 — 超时重传定时器) 对于这些结论,我尝试搜了很多资料,后来在360公司的「基础架构快报」中也看到了他们的研究资料《TCP三次握手之backlog》,也验证了我的结论。 关于ACCEPT QUEUE满了之后的表现问题,早上IM鑫爷给我指出几个错误,感谢批评及指导,在这里,我把这个问题再详细描述一下。如上图所示 NO.515 client发SYN到server,我的seq是0,消息包内容长度为0. (这里的seq并非真正的0,而是wireshark为了显示更好阅读,使用了Relative SeqNum相对序号) NO.516 server回SYN ACK给client,我的seq是0,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。(请发后面的) NO.641 client发ACK给server,我是seq 1 ,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。 NO.992 client发PSH给server,我是seq 1 ,消息包内容长度是496,已经收到你发的seq 1 之前的TCP包。 ...........等了一段时间之后(这里约0.2s左右) NO.4796 client没等到对方的ACK包,开始TCP retransmission这个包,我是seq 1,消息包长度496,已经收到你发的seq 1 之前的TCP包。 ..........又...等了一段时间 NO.9669 client还是没等到对方的ACK包,又开始TCP retransmission这个包,我是seq 1,消息包长度496,已经收到你发的seq 1 之前的TCP包。 NO.13434 server发了SYN ACK给client,这里是tcp spurious retransmission 伪重传,我的seq是0,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。距离其上次发包给client是NO.516 已1秒左右了,因为没有收到NO.641 包ACK。这时,client收到过server的SYN,ACK包,将此TCP 连接状态改为ESTABLISHED,而server那边没有收到client的ACK包,则其TCP连接状态是SYN_RCVD状态。(感谢IM鑫爷指正)也可能是因为accept queue满了,暂时不能将此TCP连接从syns queue拉到accept queue,导致这情况,这需要翻阅内核源码才能确认。 NO.13467 client发TCP DUP ACK包给server,其实是重发了N0.641 ,只是seq变化了,因为要包括它之前发送过的seq的序列号总和。即..我的seq 497 ,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。 NO.16573 client继续重新发消息数据给server,包的内容还是NO.992的内容,因为之前发的几次,都没收到确认。 NO.25813 client继续重新发消息数据给server,包的内容还还是NO.992的内容,仍没收到确认。(参见下图中绿色框内标识) NO.29733 server又重复了NO.13434包的流程,原因也一样,参见NO.13434包注释 NO.29765 client只好跟NO.13467一样,重发ACK包给server。 NO.44507 重复NO.16573的步骤 NO.79195 继续重复NO.16573的步骤 NO.79195 server立刻直接回了RST包,结束会话 详细的包内容备注在后面,需要关注的不光是包发送顺序,包的seq重传之类,还有一个重要的,TCP retransmission timeout,即TCP超时重传。对于这里已经抓到的数据包,wireshark可以看下每次超时重传的时间间隔,如下图: RTO的重传次数是系统可配置的,见/proc/sys/net/ipv4/tcp_retries1 ,而重传时间间隔,间隔增长频率等,是比较复杂的方式计算出来的,见《TCP/IP重传超时--RTO》。 backlog大小设置为多少合适? 从上面的结论中可以看出,这跟FPM的处理能力有关,backlog太大了,导致FPM处理不过来,nginx那边等待超时,断开连接,报504 gateway timeout错。同时FPM处理完准备write 数据给nginx时,发现TCP连接断开了,报“Broken pipe”。backlog太小的话,NGINX之类client,根本进入不了FPM的accept queue,报“502 Bad Gateway”错。所以,这还得去根据FPM的QPS来决定backlog的大小。计算方式最好为QPS=backlog。对了这里的QPS是正常业务下的QPS,千万别用echo hello world这种结果的QPS去欺骗自己。当然,backlog的数值,如果指定在FPM中的话,记得把操作系统的net.core.somaxconn设置的起码比它大。另外,ubuntu server 1404上/proc/sys/net/core/somaxconn 跟/proc/sys/net/ipv4/tcp_max_syn_backlog 默认值都是128,这个问题,我为了抓数据,测了好几遍才发现。 对于测试时,TCP数据包已经drop掉的未进入syns queue,以及未进入accept queue的数据包,可以用netstat -s来查看: 经过相关资料查阅,技术点研究,再做一番测试之后,又加深了我对TCP通讯知识点的记忆,以及对sync queue、accept queue所处环节知识点薄弱的补充,也是蛮有收获,这些知识,在以后的纯TCP通讯程序研发过程中,包括高性能的互联网通讯中,想必有很大帮助,希望自己能继续找些案例来实践检验一下对这些知识的掌握。 参考资料 TCP三次握手之backlog linux里的backlog详解 tcp-queue-的一些问题 The meaning of listen(2)'s backlog parameter TCP 的那些事儿(上) TCP/IP重传超时--RTO TCP的定时器系列—超时重传定时器 Coping with the TCP TIME-WAIT state on busy Linux servers 2018年10月22日新增How TCP backlog works in Linux 2015-06-02 +8 12 0
YSLOW法则中,为什么yahoo推荐用GET代替POST? 背景: 上上周五,公司前端工程师培训,提到前端优化的一些技巧,当然不能少了yahoo yslow的优化法则。其中有这么一条“Use GET for AJAX Requests”,这些法则从最开始的14条,到现在的35条,一直都时刻关注的。可这么一条的原因我却一点都不清楚。在提问的环节里,我对yahoo WEB前端优化法则推荐AJAX中,使用GET代替POST的原因有疑问,便请教前端工程师。我们的工程师说GET的话,浏览器发送一个包,POST会发两个等等。我对这个解释仍带有疑问,甚至怀疑。培训结束后,我随便搜索了一下,并没有得到理想的结果,可能很少人对Yahoo这么有权威的组织提出的优化法则产生怀疑,也很少人想知道为什么建议这么做,更多的人会唯命是从,墨守成规。之后,我又看了遍优化法则,看到一条是推荐开发者使用AJAX缓存的,这时,一个“伟大”的想法在我脑袋中一闪,莫非是GET请求可以缓存,而POST不可以?接着,我把我这个“伟大”的猜测告诉我的同事们,当初已经是下班时间,好多同事都离开公司,我也匆忙收拾东西下班了,没有仔细查找答案。 周末期间,脑袋中频繁的闪现这个问题,仍对我的想法有怀疑,Yahoo前端这么牛X的团队的想法,岂是我这样的菜鸟能这么容易的猜测推断到的?我对我当初的推测的怀疑就像“小时候就怀疑小JJ绝对不是只用来撒尿那么简单”一样坚定。但向我这么懒惰的同学,实在找不出一点时间来验证我这个想法,空闲的时间宁愿多打几盘CS。一直拖到现在,台风来了,在家宅两天,头都睡扁了,也找不出不写这篇文章的理由。 验证Yahoo推荐的理由: 验证XHR请求中yahoo推荐用GET代替POST做法的理由 POST is implemented in the browsers as a two-step process: sending the headers first, then sending data. So it's best to use GET POST请求分两步:发送http headers,再发送http data HTML+JS代码: 抓包工具:wireshark 提示:wireshark(1.2.5版)在抓http包的时候,会默认合并packet reassembly选项,记得全部去掉。如下图(edit-->Preferences) 我分别发了一个GET、一个POST的XHR(XMLHttpRequest)请求,其数据包如下: 如上图,GET请求发送的数据包为第一个红框内的结果;POST请求发送的数据包为第二个红框内结果,但多了一个第12条数据包(粉红色框内),从10.0.0.108(我的PC)发往98.126.129.106(www.cnxct.com的服务器IP,也就是表单提交的目标服务器IP),wireshark给出的信息是“Continuation or non-HTTP traffic”,这个提示就是说,本次数据包是接着上一次的HTTP请求发的,没有HTTP header,只有http data。 详情如下图 XHR HTTP POST请求的header部分数据: XHR HTTP POST 请求的DATA部分: 结论?: 果然,如伟大的YAhoo前端团队所说,XHR HTTP的POST请求会分为两步,先发HTTP HEADER,再发HTTP DATA部分。 然而,新的疑问又来了。为什么要分为两部?谁(例如W3C这种机构)规定的?每个浏览器都是这样的么?分两次比一次的的效率更高吗? 继续: 带这我新的疑问,又进行了如下尝试:先分浏览器,IE8、Firefox5.0、Chrome13分别发送XHR GET 、XHR POST请求,抓包对比结果。 我惊奇的发现(细心的同学会注意到第三张图中,有椭圆形的框标出那些结果的浏览器是Chrome13),Firefox5发送POST的数据包确是没有像yahoo前端优化法则中提到的那样,分为两次,两个包发送,而是一次完成http headers和 http data的发送。如下图: 大家可以从图中看到line-based text data:application/x-www-from-urlcoded下面就是POST的数据。 这时,又有很多疑问产生了,其他浏览器呢?IE的所有版本都会分两次发么?Firefox的其他版本呢? 当我想一个一个尝试抓包对比的同时,幸运的搜到了关于我这个疑问的PDF(Analysis_of_browser_specific_characteristics.pdf) 其中,提到firefox大部分版本在XP、WIN7、UBUNTU、MAC OS等系统上都是以1个包来实现的,其他常见浏览器都是分为两个包。 相比大家很清楚的知道,HTTP(TCP)完成一次事务,通讯次数越多,越有可能出现故障(网络延迟等因素),开销越大,浏览器(客户端)、服务端都要再进行一次TCP通讯,而且,需要一定的时间。对于我们追求更高的用户体验,需要HTTP通讯都避免到这些缺点,而各大浏览器开发商为何仍这么做呢?firefox的做法是最好的吗? 上面的PDF里,模拟了各种网络环境,比如网络延迟、网络丢包等情况,分别来对比POST请求的1次包和2次包的优缺点。得出的结论是:当网络环境好的情况下,1次包跟2次包的在时间上差别基本可以无视。而在网络环境差的情况下,(2次包)TCP的验证数据包完整性上,有非常大的优点,客户端先告诉服务端即将发送的数据包大小,MD5等标识,当服务端告诉客户端收到(ACK包)的时候,客户端再次向服务端发送POST 的DATA。假如网络环境不好,网络延迟、丢包的时候,服务端会等待(延迟时),客户端重发POST的DATA数据到服务单,来确保本次请求的完整性。 撰写这个PDF的作者在他的博客里详细的描述了写这个博客的起因,以及结果,还有一些关于与yahoo yslow前端团队的一些沟通过程,大家可以在这里阅读下(yahoo 的前端团队好像不太友好,哈)。 结论: 通过参考这个PDF,以及我自己做的抓包测试,让我了解Yahoo YSLOW前端团队的这个推荐(他们没详细的说为啥这么推荐,只是简单的提了下GET请求产生TCP一个包;POST请求,产生2个TCP包。甚至都没告诉我们Firefox的多数版本[可能是所有版本]都是发一个TCP包的。更详细、更深层原因也没说,这里还得感谢下http://loadimpact.com的作者)。 备注: 这里提供下我抓包测试时候的数据包(截图用到的数据包中包含我的一些cookie,没上传。这里的是我新抓的,各位见谅),各位可以参考下,如果我的文字、方向有错,欢迎指出。 IE8、FF5、Chrome13发起XHR请求数据包 当你读到这里时,我承认,我骗了你,文章的内容不光是标题中所写的,为何推荐使用POST代替GET,更多的是抓去TCP、HTTP通讯包来验证各个浏览器是否如YSLOW所述的那样分2次包的过程,以及2次包与1次包的优缺点(PDF中)。希望你看到最后的时候,忘记标题讲的是什么。 更新日期:2014年10月27日 在网上看到一篇文章也是介绍GET跟POST区别的,见:XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST? 2011-08-07 +14 18 0