Tailscale Taildrop 部分源码阅读记录
20240923 - main 分支
1. 发送文件
1.1. 命令行入口
命令行入口位于 cmd/tailscale/cli/file.go
的 runCp
。
一开始,对命令行参数进行拆解并判别是否有效。target 参数可以是设备名字也可以是 IP,用 tailscaleIPFromArg
统一把 target 解析成 IP,然后使用 getTargetStableID
解析成节点固定 ID
接下来遍历文件名,对每个文件打开并执行 Stat()
,如果发现是目录报错不支持,如果是文件的话获取文件长度并打开一个 countingReader
,创建一个推送进度的 context,调用 localClient.PushFile
(即使用 localapi)推送文件。
1.2. localClient.PushFile
此方法的参数有 ctx、节点固定 ID、传输长度、文件名、文件 Reader
调用 http.NewRequestWithContext
创建一个目标是 tailscaled.sock 的 PUT 请求,路径 /localapi/v0/file-put/<节点固定ID>/<URL编码过的文件名>
。在 http.NewRequestWithContext
中,直接把 countingReader 塞入 Body 进行发送,并把要发送的文件内容长度设置为 ContentLength
然后通过 *LocalClient.doLocalRequestNiceError -> DoLocalRequest
向本地 Tailscaled 发送请求
1.3. Tailscaled 处理 LocalAPI 请求
请求在 ipn/localapi/localapi.go
的 Handler.serveFilePut
中被处理。此接口除了处理 PUT 的单文件提交之外,还支持处理 multipart/form-data
的 POST 多文件提交。注释中还提到 Windows 客户端目前直接使用 peerapi 发送文件,跳过了这里以简化操作流程。
在 serveFilePut
中,使用 Handler.*LocalBackend.FileTargets
拿到目前可以发送文件的所有节点 *apitype.FileTarget
列表,里面包含了各节点的 PeerAPI URL
。
随后对进入请求的 URL 进行处理,提取出节点 ID、文件名,在前面拿到的列表中找有没有匹配目标 ID 的节点,如果没找到或者目标节点不可用就报错。
接下来,开了个 goroutine 不断循环跟踪 LocalBackend.OutgoingFiles
中文件的传输进度,并根据 PUT/POST 决定是调用单文件处理函数 Handler.singleFilePut
还是多文件处理函数 Handler.multiFilePost
1.3.1. 处理单文件 PUT
Handler.singleFilePut
能拿到当前 Context、progressUpdates
文件进度更新 Channel、HttpResponseWriter、请求的 Body、目标节点 PeerAPI URL、发送的文件元数据 ipn.OutgoingFile。
在一开始把待发送文件的 io.Reader
(Body)套了一层,改成了支持定期跟踪文件读取进度的 Reader。接下来,在开始 PUT 文件之前,首先检查此文件是否需要恢复之前的传输,如果属实的话,根据结果修改文件 Reader 的文件指针,跳过前面已经发送的部分,返回一个可以继续开始进行发送的文件 Reader 和已发送的长度偏移 offset。
1.3.2. 检验是否需要恢复传输进度的方法如下:
localclient 构造了向 <PeerAPI>/v0/put/<待传输文件名>
的 GET 请求并获取结果,将返回结果 Body 传入 jsonDecoder 中,然后把文件 Reader 和 jsonDecoder 传入 taildrop.ResumeReader
。taildrop.ResumeReader
中不断调用 hashNext 获取文件的下一块的哈希,从文件中读取对应长度的部分并对比哈希,如果匹配则继续查询,否则返回计算完毕的 offset 和设置过偏移 Reader
接下来开始发送文件。向 http://peer/v0/put/<目标文件名>
构建了 PUT 请求,Body 传入的是前面修改过偏移的文件 Reader,并针对 offset 和文件总长度设置 Range 与 ContentLength 请求头。设置完毕后,使用 ReverseProxy 工具向对端 PeerAPI 发起传输请求。
1.3.3. 处理多文件 POST
Handler.multiFilePost
中主要还是在解析 multipart/form-data。
其将请求 Body 按 params["boundary"]
分为多块,以两块为一组进行解析。每组第一个是文件元数据信息(ipn.OutgoingFile
的 json),第二个则是具体的文件数据,会被传入至 Handler.singleFilePut
发送出去。
1.4. PeerAPI 处理请求
处理函数是 ipn/ipnlocal/peerapi.go
的 handlePeerPut
方法。其首先检查了权限并处理 URL,取出要发送的文件名。
下面的第一部分 GET 处理的就是之前 Handler.singleFilePut
中检查是否文件是否曾经未完成传输的 GET 请求。
第二部分 PUT 处理文件上传。h.peerNode
是发起请求的节点,根据其节点 ID 构造 taildrop 的 ClientID。然后从 Headers 的 Range 中抽取出已发送的字节数 offset,调用 *taildrop.Manager.PutFile
进行实际的文件发送工作。
1.5. *taildrop.Manager.PutFile
PutFile 负责接收给定的 ClientID 的文件,并储存到 Manager.Dir 中。方法位于 taildrop/send.go
,参数有 ClientID、待传输文件名 baseName、文件 Reader、已发送字节数 offset、文件总长度 length。
首先把 Manager.Dir 和 baseName 拼接起来得到 dstPath,然后检查有无 <dstPath>.<ClientID>.partial
的传输未完成文件,根据文件信息构造 inFileKey 并在 m.incomingFiles
中创建 incomingFile
文件传输记录。在文件传输完毕后,将删除此传输记录条目,并用 deleter 删除.partical 文件
接下来,打开或创建.partical 文件,根据 offset 截断文件到指定位置,然后用 io.Copy
把文件内容复制进去。
传输完毕之后,尝试把.partical 改名为真实文件名,如果改名失败的话尝试用新名字再试。
2. 接收文件
2.1. 命令行入口
命令行入口位于 cmd/tailscale/cli/file.go
的 runFileGet
。从命令行参数获取目标目录,如果是 /dev/null
则调用 wipeInbox
清空 “收件箱”,不然调用 runFileGetOneBatch
接收一次文件
在 runFileGetOneBatch
中,调用 localClient.WaitingFiles(ctx)
获得一批等待接收的文件,然后在下面用 receiveFile(ctx, wf, dir)
接收。接收完成后,调用 localClient.DeleteWaitingFile(ctx, wf.Name)
删除” 收件箱 “中的文件。
localClient.WaitingFiles(ctx)
会通过 lc.AwaitWaitingFiles
向 /localapi/v0/files/?waitsec=xx
发起请求。
localClient.DeleteWaitingFile
同样会对 /localapi/v0/files/
发起 Delete 请求
2.1.1. receiveFile(ctx, wf, dir)
receiveFile 方法中,首先调用 localClient.GetWaitingFile
查询 localapi 获取文件的 Reader 和 size,随后用 openFileOrSubstitute
打开指定文件,调用 quarantine.SetOnFile(f)
设置属性,最后 io.Copy 执行文件复制工作,把文件复制到目标目录