Tailscale Taildrop 部分源码阅读记录

20240923 - main 分支

1. 发送文件

1.1. 命令行入口

命令行入口位于 cmd/tailscale/cli/file.gorunCp

一开始,对命令行参数进行拆解并判别是否有效。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.goHandler.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.ResumeReadertaildrop.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.gohandlePeerPut 方法。其首先检查了权限并处理 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.gorunFileGet。从命令行参数获取目标目录,如果是 /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 执行文件复制工作,把文件复制到目标目录


Tailscale Taildrop 部分源码阅读记录
https://blog.openyq.top/posts/22700/
作者
yqs112358
许可协议