croc:点对点安全传输文件

最近偶然发现一个有趣的项目:https://github.com/schollz/croc。它可以实现点对点(通过中继)发送文件的功能,短小精悍且可靠。

我们经常为发文件发愁:要么文件太大,微信、QQ邮箱超大附件经常放不下;要么文件特别私密,用第三方IM软件发送总是不放心。我之前的blog曾经提过两种解决方案:一是阅后即焚的对称密码传递方案,但是操作起来稍显繁琐;二是用RSA非对称加密,使用者理解起来有一定的门槛。而且两者都只能解决隐私问题,不能解决大文件问题。

croc可以解决上述痛点。当然,前提是我们有一台流量和带宽足够、最好是不限流量的云服务器作为自建中继,以确保传输速度和隐私性。

1 服务器端设置

对于Windows Server:

第一步,先开放服务器的9009-9013的TCP端口。如果国内的云服务,通常是在网页控制台的防火墙标签页中进行设置。

接着到这里下载nssm工具:https://nssm.cc/download/。将nssm.exe放在某个目录下,并将该目录加入系统环境变量(或干脆将它放到c:\windows\system32文件夹)。

然后下载croc:https://github.com/schollz/croc/releases,找到最新版本中的windows-64bit版本,下载后,将croc.exe放到某个目录下,假设为c:\croc,然后将该目录加入系统环境变量。

管理员模式启动powershell,依次执行以下命令,以安装、设置crocrelay服务,设置为自动启动,并启动该服务:

nssm install crocrelay "c:\croc\croc.exe"
nssm set CrocRelay AppParameters 'relay --host 0.0.0.0 --ports 9009,9010,9011,9012,9013'
nssm set CrocRelay AppDirectory "c:\croc"
nssm set CrocRelay AppEnvironmentExtra "CROC_PASS=YOUR_RELAY_KEY"
nssm set CrocRelay AppThrottle 10000
nssm set CrocRelay Start SERVICE_AUTO_START
nssm start crocrelay

注意,上面把YOUR_RELAY_KEY换成你自己想要设置的密码,虽然不设置也行,但为了防止你的中继被滥用,建议还是设置一下。

不出意外的话,系统中已经添加了一个crocrelay服务,并已经启动。

过程中,如果需要反复调整设置,可以用nssm停止并删除该服务,以便重新安装服务:

nssm stop crocrelay
nssm remove crocrelay confirm

在前面nssm start crocrelay之后,可以“开始菜单-服务”中查看crocrelay服务是否已经处于运行状态。如果是,则windows server上的crocrelay服务已经安装成功。

对于ubuntu:

第一步也是开放端口。开放服务器的9009-9013的TCP端口。如果国内的云服务,通常是在网页控制台的防火墙标签页中进行设置。

ubuntu服务器系统内如果启用了防火墙,也需要放开:

ufw allow 9009
ufw allow 9010
ufw allow 9011
ufw allow 9012
ufw allow 9013

然后下载croc:https://github.com/schollz/croc/releases,找到最新版本中的linux-64bit版本(注意对应你的实际服务器系统),下载后,将croc二进制文件放到某个目录下,假设为/var/croc/。

为二进制文件赋予执行权限:

chmod +x /var/croc/croc

接下来,在/etc/systemd/system中创建一个文件:croc-relay.service,内容如下:

[Unit]
Description=Croc Relay Service
After=network.target

[Service]
ExecStart=/var/croc/croc relay --host 0.0.0.0 --ports 9009,9010,9011,9012,9013
WorkingDirectory=/var/croc
Environment=CROC_PASS=YOUR_RELAY_KEY
Restart=always
RestartSec=3
User=nobody
Group=nogroup

[Install]
WantedBy=multi-user.target

同样,上面把YOUR_RELAY_KEY换成你自己想要设置的密码。虽然不设置也行,但为了防止你的中继被滥用,建议还是设置一下。

接着更新daemon,设置为开机自启动,并启动服务:

systemctl daemon-reexec
systemctl daemon-reload
systemctl enable croc-relay
systemctl start croc-relay

查看服务状态:

systemctl status croc-relay

查看状态时,应该可以看到服务已经正常运行,并开始监听9009-9013这几个端口。

好了,那么ubuntu上的服务已经安装并启动成功。

2 客户端设置

相对来说,客户端设置就简单多了。

对于Windows:

第一步是下载croc:https://github.com/schollz/croc/releases,找到最新版本中的windows-64bit版本,下载后,将croc.exe放到某个目录下,假设为c:\croc,然后将该目录加入系统环境变量。

为了方便使用,强烈建议在croc同目录下,创建两个bat文件:

send.bat:

@echo off
:: 用法:
::   send <文件或目录> <自定义口令>
::   例: send "D:\iso\ubuntu.iso" my-code

set "FILE=%~1"
set "CODE=%~2"
croc --relay "YOUR_SERVER_IP:9009" --pass YOUR_RELAY_KEY send --code "%CODE%" "%FILE%"

dl.bat:

@echo off
:: 用法:dl <code-phrase>
:: 例:   dl my-code
croc --yes --relay "YOUR_SERVER_IP:9009" --pass YOUR_RELAY_KEY %*

同样注意,里面的YOUR_SERVER_IP改成你自己服务器的IP,YOUR_RELAY_KEY改成你自己前面设置的RELAY密码。

接下来,就可以使用send命令来发送文件了:

send <文件或目录> <口令>

我们假设文件名是file.doc,口令是hello123456,那么发送方的命令就是:

send file.doc hello123456

发送方可以将croc.exe、send.bat、dl.bat一起打包发给接收方,接收方解压缩到某个目录后,即可在该目录中单击路径栏,输入“cmd”进入对应该目录的命令行窗口,然后输入:

dl hello123456

即刻开始下载进程。

对于安卓:

可以到这里下载croc的安卓端apk:https://f-droid.org/packages/com.github.howeyc.crocgui/。安装apk后,打开app,界面中进入“设定”页,可以切换到中文。然后在下面的地址、密码中,分别输入你的服务器IP、RELAY密码。

然后就可以愉快地发送和接收文件了。

3 进阶:让使用更容易

以上方法仍然需要用户操作命令行,这会让不少人望而却步。那么可以通过构造一个powershell脚本来创建需要的窗口,包括文件选择、文件夹选择和code的输入窗口。

由于默认情况下,ps1并不能双击运行(其实右键选择“使用 powershell 运行”就行),可能还是会有人不习惯,那么可以创建一个vbs脚本来调用它,这样用户只需双击vbs即可实现对应的功能。

创建 SendWithCroc.ps1,注意保存为ANSI格式,或UTF-8带BOM格式,内容如下:

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName PresentationFramework

# 获取当前目录
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$CrocExe = Join-Path $ScriptDir 'croc.exe'

if (-not (Test-Path $CrocExe)) {
    [System.Windows.Forms.MessageBox]::Show("同目录下未找到 croc.exe!", "错误")
    exit
}

# 弹出选择类型对话框
# 创建选择窗口
$win = New-Object System.Windows.Window
$win.Title = "请选择发送类型"
$win.Width = 360
$win.Height = 160
$win.WindowStartupLocation = "CenterScreen"
$stack = New-Object System.Windows.Controls.StackPanel
$stack.Margin = '20'

$tip = New-Object System.Windows.Controls.TextBlock
$tip.Text = "请选择你要发送的内容:"
$tip.FontSize = 16
$tip.Margin = '0,0,0,18'
[void]$stack.Children.Add($tip)

$btnPanel = New-Object System.Windows.Controls.StackPanel
$btnPanel.Orientation = "Horizontal"
$btnPanel.HorizontalAlignment = "Center"
$btnPanel.Margin = '0,0,0,0'

$btnFile = New-Object System.Windows.Controls.Button
$btnFile.Content = "发送文件"
$btnFile.Width = 80
$btnFile.Margin = '0,0,12,0'
$btnFile.Padding = '0,4,0,4'
$btnFile.Add_Click({
    $win.Tag = "file"
    $win.DialogResult = $true
    $win.Close()
})
[void]$btnPanel.Children.Add($btnFile)

$btnFolder = New-Object System.Windows.Controls.Button
$btnFolder.Content = "发送文件夹"
$btnFolder.Width = 80
$btnFolder.Margin = '0,0,12,0'
$btnFolder.Padding = '0,4,0,4'
$btnFolder.Add_Click({
    $win.Tag = "folder"
    $win.DialogResult = $true
    $win.Close()
})
[void]$btnPanel.Children.Add($btnFolder)

$btnCancel = New-Object System.Windows.Controls.Button
$btnCancel.Content = "取消"
$btnCancel.Width = 50
$btnCancel.Add_Click({
    $win.Tag = "cancel"
    $win.DialogResult = $false
    $win.Close()
})
[void]$btnPanel.Children.Add($btnCancel)

[void]$stack.Children.Add($btnPanel)
$win.Content = $stack

# 展示窗口并获取选择结果
$result = $win.ShowDialog()
$type = $win.Tag

if ($type -eq "cancel" -or $result -ne $true) {
    [System.Windows.Forms.MessageBox]::Show("操作已取消。", "croc 文件发送")
    exit
}

if ($type -eq "file") {
    $FileDialog = New-Object System.Windows.Forms.OpenFileDialog
    $FileDialog.Title = "请选择要发送的文件"
    $FileDialog.Multiselect = $false
    $FileDialog.Filter = "所有文件 (*.*)|*.*"
    $Result = $FileDialog.ShowDialog()
    if ($Result -ne [System.Windows.Forms.DialogResult]::OK) {
        [System.Windows.Forms.MessageBox]::Show("你没有选择文件,操作已取消。", "croc 文件发送")
        exit
    }
    $SendPath = $FileDialog.FileName
} elseif ($type -eq "folder") {
    $FolderDialog = New-Object System.Windows.Forms.FolderBrowserDialog
    $FolderDialog.Description = "请选择要发送的文件夹"
    $Result = $FolderDialog.ShowDialog()
    if ($Result -ne [System.Windows.Forms.DialogResult]::OK) {
        [System.Windows.Forms.MessageBox]::Show("你没有选择文件夹,操作已取消。", "croc 文件发送")
        exit
    }
    $SendPath = $FolderDialog.SelectedPath
}

# 输入 code
# 自定义简单输入对话框
Add-Type -AssemblyName PresentationFramework

$window = New-Object System.Windows.Window
$window.Title = "输入 code"
$window.Width = 400
$window.Height = 150
$window.WindowStartupLocation = "CenterScreen"

$panel = New-Object System.Windows.Controls.StackPanel
$panel.Margin = '10'

$label = New-Object System.Windows.Controls.Label
$label.Content = "请输入你希望使用的 code ,注意至少6个字符:"
[void]$panel.Children.Add($label)

$textbox = New-Object System.Windows.Controls.TextBox
$textbox.Margin = '0,5,0,5'
$textbox.Height = 28
$textbox.FontSize = 14
$textbox.Padding = '0,4,0,4'
[void]$panel.Children.Add($textbox)

$buttonPanel = New-Object System.Windows.Controls.StackPanel
$buttonPanel.Orientation = "Horizontal"
$buttonPanel.HorizontalAlignment = "Right"

$okButton = New-Object System.Windows.Controls.Button
$okButton.Content = "确定"
$okButton.Width = 80
$okButton.Margin = "0,0,5,0"
$okButton.Padding = '0,4,0,4'
$okButton.Add_Click({
    $window.DialogResult = $true
    $window.Close()
})
[void]$buttonPanel.Children.Add($okButton)

$cancelButton = New-Object System.Windows.Controls.Button
$cancelButton.Content = "取消"
$cancelButton.Width = 80
$cancelButton.Add_Click({
    $window.DialogResult = $false
    $window.Close()
})
[void]$buttonPanel.Children.Add($cancelButton)

[void]$panel.Children.Add($buttonPanel)
$window.Content = $panel

if ($window.ShowDialog() -eq $true) {
    $Code = $textbox.Text
    if ([string]::IsNullOrWhiteSpace($Code)) {
        [System.Windows.Forms.MessageBox]::Show("你没有输入 code,操作已取消。", "croc 文件发送")
        exit
    }
} else {
    [System.Windows.Forms.MessageBox]::Show("你取消了输入,操作已取消。", "croc 文件发送")
    exit
}

[System.Windows.Forms.MessageBox]::Show(
    "请将 code 提供给接收方。`r`n`r`n点确定后开始等待对方连接,注意不要关闭弹出来的 Powershell 窗口。`r`n`r`n对方开始传输后,Powershell 窗口中会显示进度。请等待文件传输完成。`r`n`r`n好,现在点击确定。",
    "croc 文件发送"
)

# 发送,注意把YOUR_SERVER_IP、YOUR_RELAY_KEY改成你自己的
$Output = & $CrocExe --relay "YOUR_SERVER_IP:9009" --pass YOUR_RELAY_KEY send --code=$Code "$SendPath" | Out-String

if ($LASTEXITCODE -eq 0) {
    [System.Windows.Forms.MessageBox]::Show("发送成功!", "croc 文件发送")
} else {
    [System.Windows.Forms.MessageBox]::Show("发送失败:`r`n$Output", "croc 文件发送")
}

然后创建 发送.vbs,注意保存为ANSI格式,内容如下:

Set fso = CreateObject("Scripting.FileSystemObject")
Set shell = CreateObject("WScript.Shell")
currdir = fso.GetParentFolderName(WScript.ScriptFullName)
ps1 = currdir & "\SendWithCroc.ps1"
croc = currdir & "\croc.exe"

If Not fso.FileExists(croc) Then
    MsgBox "同目录下没有 croc.exe,请确认文件完整!", 48, "错误"
    WScript.Quit
End If

cmd = "powershell -ExecutionPolicy Bypass -File """ & ps1 & """"
shell.Run cmd, 1, False

接下来是 ReceiveWithCroc.ps1,注意保存为ANSI格式,或UTF-8带BOM格式,内容如下:

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName PresentationFramework

# 获取当前目录
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$CrocExe = Join-Path $ScriptDir 'croc.exe'

if (-not (Test-Path $CrocExe)) {
    [System.Windows.Forms.MessageBox]::Show("同目录下未找到 croc.exe!", "错误")
    exit
}

# 输入 code,弹窗对话框
$window = New-Object System.Windows.Window
$window.Title = "输入 code"
$window.Width = 400
$window.Height = 150
$window.WindowStartupLocation = "CenterScreen"

$panel = New-Object System.Windows.Controls.StackPanel
$panel.Margin = '10'

$label = New-Object System.Windows.Controls.Label
$label.Content = "请输入对方给你的 code:"
[void]$panel.Children.Add($label)

$textbox = New-Object System.Windows.Controls.TextBox
$textbox.Margin = '0,5,0,5'
$textbox.Height = 28
$textbox.FontSize = 14
$textbox.Padding = '0,4,0,4'
[void]$panel.Children.Add($textbox)

$buttonPanel = New-Object System.Windows.Controls.StackPanel
$buttonPanel.Orientation = "Horizontal"
$buttonPanel.HorizontalAlignment = "Right"

$okButton = New-Object System.Windows.Controls.Button
$okButton.Content = "确定"
$okButton.Width = 80
$okButton.Margin = "0,0,5,0"
$okButton.Padding = '0,4,0,4'
$okButton.Add_Click({
    $window.DialogResult = $true
    $window.Close()
})
[void]$buttonPanel.Children.Add($okButton)

$cancelButton = New-Object System.Windows.Controls.Button
$cancelButton.Content = "取消"
$cancelButton.Width = 80
$cancelButton.Add_Click({
    $window.DialogResult = $false
    $window.Close()
})
[void]$buttonPanel.Children.Add($cancelButton)

[void]$panel.Children.Add($buttonPanel)
$window.Content = $panel

if ($window.ShowDialog() -eq $true) {
    $Code = $textbox.Text
    if ([string]::IsNullOrWhiteSpace($Code)) {
        [System.Windows.Forms.MessageBox]::Show("你没有输入 code,操作已取消。", "croc 文件接收")
        exit
    }
} else {
    [System.Windows.Forms.MessageBox]::Show("你取消了输入,操作已取消。", "croc 文件接收")
    exit
}

# 创建接收文件夹
$SaveDir = Join-Path $ScriptDir '接收文件'
if (-not (Test-Path $SaveDir)) {
    New-Item -ItemType Directory -Path $SaveDir | Out-Null
}
# 开始接收,注意把YOUR_SERVER_IP、YOUR_RELAY_KEY改成你自己的
$Output = & $CrocExe --yes --out "$SaveDir" --relay "YOUR_SERVER_IP:9009" --pass YOUR_RELAY_KEY $Code | Out-String

if ($LASTEXITCODE -eq 0) {
    [System.Windows.Forms.MessageBox]::Show("文件接收完成!`r`n全部已保存到:$SaveDir", "croc 文件接收")
} else {
    [System.Windows.Forms.MessageBox]::Show("接收失败:`r`n$Output", "croc 文件接收")
}

然后创建 接收.vbs,注意保存为ANSI格式,内容如下:

Set fso = CreateObject("Scripting.FileSystemObject")
Set shell = CreateObject("WScript.Shell")
currdir = fso.GetParentFolderName(WScript.ScriptFullName)
ps1 = currdir & "\ReceiveWithCroc.ps1"
croc = currdir & "\croc.exe"

If Not fso.FileExists(croc) Then
    MsgBox "同目录下没有 croc.exe,请确认文件完整!", 48, "错误"
    WScript.Quit
End If

cmd = "powershell -ExecutionPolicy Bypass -File """ & ps1 & """"
shell.Run cmd, 1, False

把以下5个文件都放在一个目录下:

croc.exe
ReceiveWithCroc.ps1
SendWithCroc.ps1
发送.vbs
接收.vbs

把它们打包并分发,让发送方和接收方人手一份。解压缩到任意文件夹就行,也无需设置环境变量。

现在用起来就方便多了:发送方只需双击“发送.vbs”,即可选择文件或文件夹,并输入code;接收方则双击“接收.vbs”,输入同样的code即可。接收的文件将保存到该文件夹的子目录“接收文件”中。

4 补充说明

1、croc的发送和接收是一次性的,接收端一旦完成了接收,发送端会即刻停止发送。

2、使用命令行方式时,send的第一个参数,如果是命令行的当前文件夹下的文件,则直接用文件名或文件夹名即可。如果要发送其他目录下的文件,可以使用全路径名称。

3、使用命令行方式时,dl下载后的文件,会在当前命令行所在的目录。

4、虽然文件的传输是通过中继服务器,但中继服务器从原理上不可能得知传输的具体内容。

5、以上所有用到的工具,可以直接在这里下载:点击下载