Git 底层原理:传输协议分析(一)
目前主流的代码托管平台都支持 https
和 ssh
的方式来下载和推送代码,这个主要得力于 Git 有一套自定义的传输协议来保证可靠的传输,
本文通过分析 git 的 http 协议开始,由浅入深循序渐进的展开 Git 传输协议的分析。
git https 传输协议分析
Wireshark 是一个抓包工具,有非常强大的过滤和分析功能,用该工具分析 git 协议流非常方便。
不借助 Wireshark ,设置
GIT_TRACE_CURL
环境变量也能够查看到 Git 的传输协议过程,具体看 GIT_TRACE_CURL 或《 Git 底层原理:传输协议分析(二)》。
准备工作
本文使用阿里云的代码托管平台 Codeup 来分析传输协议。当然,你也可以使用 Github 或者 Gitee 。
github 使用更高效的 http/2 协议。 http/2 协议因为数据帧是二进制格式,对于分析 https 交互并不直观,所以本文使用基于 http/1.1 协议的 Codeup 作为示例。
1. 查看服务器 ip 地址
1 | $ host codeup.aliyun.com |
获取到 codeup.aliyun.com
服务器 ip 地址为 118.31.165.50
。
2. 设置 SSLKEYLOGFILE
环境变量
通过设置 SSLKEYLOGFILE
环境变量,可以保存 TLS 的会话钥匙( Session Key
),wireshark 再读取 Session Key
然后实时解析 https 数据流,具体可以参考这篇文章:Walkthrough: Decrypt SSL/TLS traffic (HTTPS and HTTP/2) in Wireshark。
如下设置:
1 | # 该命令只在当前终端生效。 |
3. 设置 Wireshark
首先让 Wireshark 读取 sslkeylog.log
,打开 Wireshark,点击 菜单
>Performances
,在对话框中选择 Protocol
> TLS
,设置 (Pre)-Master-Secret log filename
为你的 SSLKEYLOGFILE
文件路径:
启动 Wireshark 监听网卡,设置过滤规则为 tls && http && ip.addr == 118.31.165.50
,其中 118.31.165.50
就是获取到的服务器 ip 地址。
git clone
做完上面的准备工作后,就可以开始抓包分析了。运行 git clone
命令:
1 | # 确保设置了 SSLKEYLOGFILE 环境变量 |
Wireshark 抓包得到如下数据包:
一开始有个
Unautherized
的过程,这个是 git 在尝试匿名访问。
点击 菜单
> Analyze
> Follow
> HTTP Stream
可以更直观的查看数据交互流,这些数据是 HTTP 内容(不包括TLS):
接下来一步一步分析一下 git clone
的交互过程。
第一次交互:引用发现
第一次交互主要起到 握手
+ 获取仓库引用信息
的作用。服务端认证用户信息,并告诉客户端对应仓库的所有引用信息,以及相关的功能信息。
1 | 首先客户端发起请求(在`Authorization`字段附上了用户名密码) |
客户端 发起的 GET 请求,URL 为 $GIT_URL/info/refs?service=git-upload-pack
。
服务端 返回的信息按照 pkt-line 格式编排,信息主要是远程仓库的引用信息。pkt-line 格式定义每一行的前四个字节代表这一行的十六进制编码的长度,同时也定义前四个字节 0000
为一点消息的结束。详细说明见文末的 pkt-line 格式。
这次交互里面,根据 0000
出现的位置,可以知道服务端返回的信息里面包含了2部分,第一部分是固定字段,表示这次回复的服务类型是 git-upload-pack
。
第二部分的第一行内容信息比较多:
1 | 01163ab7c8d1c1e2ce5f5e16a17c41f6665686980d12 HEAD\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed no-done symref=HEAD:refs/heads/master object-format=sha1 agent=git/2.28.0.agit.6.0 |
这段信息主要描述 HEAD 指针信息,以及功能列表( capability declarations )信息,比如 symref=HEAD:refs/heads/master
表示默认分支为 master
,object-format=sha1
表示对象使用 sha1
校验对象,agent=git/2.28.0.agit.6.0
服务器 git 版本信息。
第二部分第二行开始的信息则是 info/refs
文件内容。info/refs
文件描述了仓库里面的引用信息,包括分支、 tag ,以及一些自定义引用等。
- 值得一提的是,本示例中服务器回复的
info/refs
文件内容里,除了refs/heads/*
和refs/tags/*
,还存在refs/keep-around/*
和refs/merge-requests/*
等引用,这些是 Codeup 平台特有的引用。- 你可以使用
`git --exec-path`/git-update-server-info
命令来生成info/refs
文件。
实际上这一段的数据流及格式官方文档里面有详细的说明: http-protocol.txt,也可以参考本文末的 引用数据流。
第二次交互:请求数据
第二次交互里,客户端把想要的数据告诉给服务端,服务端然后把 pack 包推送给客户端。
1 | 客户端发送数据到服务器 |
通过第一次交互里,客户端 拿到了远程仓库的引用列表,然后在第二次交互里把想要的 commit-id
(及其提交链)发送给服务端。
客户端发起的是 POST 请求,URL 为 $GIT_URL/git-upload-pack
,POST 内容为想要( "want"
)的 commit-id
和已有( "have"
)的 commit-id
,其实就是 branch 和 tag 对应的 commit-id
。数据格式跟上面的服务端回复的引用列表格式类似,具体见 git-upload-pack 数据流格式。
服务端 会根据 "want"
和 "have"
的情况来决定回复最小可用的数据包给客户端,这也是智能(smart)协议的作用所在,相关的机制见 http-protocol.txt 。
服务端回复的是 HTTP 数据流格式( HTTP stream ),其中包括了进度、pack 二进制数据等。第一行的 NAK
代表数据开始,后面的数据则使用了 side-band 格式(类似于 pkg-line 格式),来描述传输进度、数据包等。 side-band 格式前四个字节也是用于表示长度,第五个字节用于标志消息类型,0x01
代表是packfile 数据,0x02
代表是进度消息,0x03
代表是错误信息。
git fetch
用 Wireshark 抓包看看 git fetch 过程:
可以看到,交互过程和 git clone
是一样的,不同的是,客户端请求数据时,除了想要( "want"
)的 commit-id ,还带上了已有( "have"
)的 commit-id ,服务端回复数据时,也添加了 ACK
字段来告诉客户端我回复的是哪些数据。
1 | 客户端请求数据 |
git push
用 Wireshark 抓包看看 git push
过程:
第一次交互:引用发现
1 | 客户端发起引用发现请求 |
引用发现交互和 git clone
是类似的,不同的地方是服务类型为 git-receive-pack
,请求的 URL 为 $GIT_URL/info/refs?service=git-receive-pack
。
第二次交互:推送数据
1 | 客户端推送数据 |
第二次交互里,客户端发起的 POST 请求 URL 为:$GIT_URL/git-receive-pack
,其内容包括old commit-id
、 new commit-id
、当前分支、功能列表以及需要上传的 PACK 二进制内容。其中新旧 2 个 commit-id
有一个比较有意思的设定:
old commit-id
如果是 40 个0
,代表是创建新分支。new commid-id
如果是 40 个0
, 代表是删除分支。old commit-id
和new commit-id
都不为0
,代表是更新分支。
相关说明可以参考 http-protocol.txt git-receive-pack 。
git ssh 传输协议分析
ssh 是建立在 TCP 上的,使用的非对称加密来握手和认证,ssh的会话key(对称密钥)也不能像文章上面一样通过 SSLKEYLOGFILE
环境变量来获取,所以很难使用 Wireshark 进行抓包分析。不过 git 提供了环境变量 GIT_TRACE_PACKET 来显示传输的过程和数据:
图中也指定了 GIT_TRACE=1 ,这个可以用来显示远程执行的命令。
可以看到,ssh 只是一个通道,ssh 交互的 git 协议和流程跟 http 基本上是一样的。
总结
相对于 TCP 、 HTTP 、 Protobuf 等通用协议,Git 传输协议定义的比较随意,且扩展性和通用性比较差。不过仔细想想也没有问题,只要能保证正常传输 Git 数据包那就已经达到目的了。
上面讲了一堆的细节,实际上 Git 传输协议可以更简单的表述( @jiangxin ):
参考资料
- https://github.com/git/git/blob/master/Documentation/technical/http-protocol.txt
- https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols
- https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
- https://wangdoc.com/ssh/client.html
- https://github.com/gcla/termshark
- Walkthrough: Decrypt SSL/TLS traffic (HTTPS and HTTP/2) in Wireshark