Git 底层原理:传输协议分析(一)

Git 底层原理:传输协议分析(一)

目前主流的代码托管平台都支持 httpsssh 的方式来下载和推送代码,这个主要得力于 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
2
$ host codeup.aliyun.com
codeup.aliyun.com has address 118.31.165.50

获取到 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
2
# 该命令只在当前终端生效。
export SSLKEYLOGFILE=~/sslkeylog.log
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
2
3
# 确保设置了 SSLKEYLOGFILE 环境变量
# export SSLKEYLOGFILE=~/sslkeylog.log
$ git clone https://codeup.aliyun.com/5ed5e6f717b522454a36976e/Codeup-Demo.git

Wireshark 抓包得到如下数据包:

一开始有个 Unautherized 的过程,这个是 git 在尝试匿名访问。

点击 菜单 > Analyze > Follow > HTTP Stream 可以更直观的查看数据交互流,这些数据是 HTTP 内容(不包括TLS):

接下来一步一步分析一下 git clone 的交互过程。

第一次交互:引用发现

第一次交互主要起到 握手 + 获取仓库引用信息 的作用。服务端认证用户信息,并告诉客户端对应仓库的所有引用信息,以及相关的功能信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
首先客户端发起请求(在`Authorization`字段附上了用户名密码)
>>>>>>>>>>>>>>>>>>>>>>>>>>>
GET /5ed5e6f717b522454a36976e/Codeup-Demo.git/info/refs?service=git-upload-pack
>>>>>>>>>>>>>>>>>>>>>>>>>>>


服务端认证用户信息,然后返回引用列表
<<<<<<<<<<<<<<<<<<<<<<<<<<<
001e# service=git-upload-pack
000001163ab7c8d1c1e2ce5f5e16a17c41f6665686980d12 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
0040f82d3c440cf02ff2e20d712eaa7ba63a9fbff4ea refs/heads/develop
...省略...
003c3ab7c8d1c1e2ce5f5e16a17c41f6665686980d12 refs/tags/v1.0
0000
<<<<<<<<<<<<<<<<<<<<<<<<<<<

客户端 发起的 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 表示默认分支为 masterobject-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
客户端发送数据到服务器
>>>>>>>>>>>>>>>>>>>>>>>>>>>
POST /5ed5e6f717b522454a36976e/Codeup-Demo.git/git-upload-pack

00a8want f82d3c440cf02ff2e20d712eaa7ba63a9fbff4ea multi_ack_detailed no-done side-band-64k thin-pack ofs-delta deepen-since deepen-not agent=git/2.24.3.(Apple.Git-128)
0032want 61ee902744d1f5a480e607856d44b104602d6b13
0032want ae02248d14bfdc9d4d38b1532cab278d179bc863
0032want 3ab7c8d1c1e2ce5f5e16a17c41f6665686980d12
0032want 3ab7c8d1c1e2ce5f5e16a17c41f6665686980d12
00000009done
>>>>>>>>>>>>>>>>>>>>>>>>>>>


服务器回复 pack 包
<<<<<<<<<<<<<<<<<<<<<<<<<<<
0008NAK
0024\2Enumerating objects: 48, done.
0023\2Counting objects: 2% (1/48)
...省略...
002b\2Counting objects: 100% (48/48), done.
2004\1PACK.......0..x...... ....O.}!81.
KY..OQ.q.)....}..C...>..Et,."..)........O.b :o..2G...uhK.s.. 3.+N. </P..a..L.Y.1...k.r..71..........X...X.......m.B{Z....m.hp......2..u.}.C>/+.Y.j..k...m.>=.M....Z...x...Aj.!.....{.`....B`...m...2.....G.Z..Z.....
...省略...
......,.....$.....x.}Q=k.1...+.MI...,.Ph.PH...OM..,W..A...}M..J5........{.Em....9...a...T.A..G.+..H,.x.)...w6=......I..ay.....7.q......G....1...X.G..s0'H....;..O%.....".....:......d1V....fJ...d....pd..j1JV1o...z..~C_l......%...N..z.L....@.+.V+'...|.=..c:&}'..T....r~...9F+.......7....V(s3....uQ....T7.=F..Gt`i.Z. n..Vn
0006\1.
003b\2Total 48 (delta 0), reused 0 (delta 0), pack-reused 0
0000
<<<<<<<<<<<<<<<<<<<<<<<<<<<

通过第一次交互里,客户端 拿到了远程仓库的引用列表,然后在第二次交互里把想要的 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
客户端请求数据
>>>>>>>>>>>>>>>>>>>>>>>>>>>
POST /5ed5e6f717b522454a36976e/Codeup-Demo.git/git-upload-pack HTTP/1.1

00b4want 113cc207f5a226e066f1119b51e57e2b8fbd8e28 multi_ack_detailed no-done side-band-64k thin-pack include-tag ofs-delta deepen-since deepen-not agent=git/2.24.3.(Apple.Git-128)
00000032have 61ee902744d1f5a480e607856d44b104602d6b13
0032have ae02248d14bfdc9d4d38b1532cab278d179bc863
0032have f82d3c440cf02ff2e20d712eaa7ba63a9fbff4ea
0009done
>>>>>>>>>>>>>>>>>>>>>>>>>>>

服务端返回数据
<<<<<<<<<<<<<<<<<<<<<<<<<<<
0038ACK 61ee902744d1f5a480e607856d44b104602d6b13 common
0038ACK ae02248d14bfdc9d4d38b1532cab278d179bc863 common
0038ACK f82d3c440cf02ff2e20d712eaa7ba63a9fbff4ea common
0031ACK f82d3c440cf02ff2e20d712eaa7ba63a9fbff4ea
0023\x02Enumerating objects: 3, done.
0022\x02Counting objects: 33% (1/3)
0029\x02Counting objects: 100% (3/3), done.
01d4\x01PACK..........x...K.. ...=.{c..<...U^.UH,5....x..z.=.=...d..D.\.i:. 8..h..CT.(.h
...省略...
.MM.r...../.k ......^.........P.@.z.@3Z.q.1.-....a.s..+g.6k@.....U..0..Sd._.H..lU.Q.=f....&.@.P\......b..7./..?{..t.V).7..Ndz5x.+I-.....L..6.B......./m...X.A.003a\x02Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
0006\x01.
0000
<<<<<<<<<<<<<<<<<<<<<<<<<<<

git push

用 Wireshark 抓包看看 git push 过程:

第一次交互:引用发现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
客户端发起引用发现请求
>>>>>>>>>>>>>>>>>>>>>>>>>>>
GET /5ed5e6f717b522454a36976e/Codeup-Demo.git/info/refs?service=git-receive-pack HTTP/1.1
>>>>>>>>>>>>>>>>>>>>>>>>>>>


服务端返回引用信息列表
<<<<<<<<<<<<<<<<<<<<<<<<<<<
001f# service=git-receive-pack
000000caf82d3c440cf02ff2e20d712eaa7ba63a9fbff4ea refs/heads/develop.report-status report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta push-options object-format=sha1 agent=git/2.28.0.agit.6.0
004961ee902744d1f5a480e607856d44b104602d6b13 refs/heads/feature/p3c_scan
...省略...
003c3ab7c8d1c1e2ce5f5e16a17c41f6665686980d12 refs/tags/v1.0
0000
<<<<<<<<<<<<<<<<<<<<<<<<<<<

引用发现交互和 git clone 是类似的,不同的地方是服务类型为 git-receive-pack,请求的 URL 为 $GIT_URL/info/refs?service=git-receive-pack

第二次交互:推送数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
客户端推送数据
>>>>>>>>>>>>>>>>>>>>>>>>>>>
POST /5ed5e6f717b522454a36976e/Codeup-Demo.git/git-receive-pack HTTP/1.1

00a5113cc207f5a226e066f1119b51e57e2b8fbd8e28 b4a87307c6b56064d623851fe018b94d25e68e13 refs/heads/master. report-status side-band-64k agent=git/2.24.3.(Apple.Git-128)0000PACK.........
x...K
.0.@.9E...L&3).x.|&.E[..=.......o.f.LJm.P.....(1..M$...s.P.#.....j%..3...tD.JD.jTR/-.%......;..c.!.Q.......\...Q0$.B.O...Q.y..{t......pj<0....'.....R....2....J.x...........`...v....m.f..9..R.R. .t..g....x.+I-..K.,...+.LI5...;*....1.:.I.;.9...p.v.Bg
>>>>>>>>>>>>>>>>>>>>>>>>>>>


服务端返回成功
<<<<<<<<<<<<<<<<<<<<<<<<<<<
0030.000eunpack ok
0019ok refs/heads/master
00000000
<<<<<<<<<<<<<<<<<<<<<<<<<<<

第二次交互里,客户端发起的 POST 请求 URL 为:$GIT_URL/git-receive-pack ,其内容包括old commit-idnew commit-id 、当前分支、功能列表以及需要上传的 PACK 二进制内容。其中新旧 2 个 commit-id 有一个比较有意思的设定:

  • old commit-id 如果是 40 个 0 ,代表是创建新分支。
  • new commid-id 如果是 40 个 0 , 代表是删除分支。
  • old commit-idnew 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 ):

参考资料