前言
最近在使用 sftp
服务时,被告知发起了海量的连接,直接把服务器搞崩,ip
被封了。
这是啥情况?
golang
写的代码,我就正常的访问 sftp
服务,连接使用过后也都关闭了,咋会出现连接一直连着没关的情况呢?
原因分析
先上代码,主要使用了开源库 github.com/pkg/sftp
package mainimport ("fmt""github.com/pkg/sftp""golang.org/x/crypto/ssh""time"
)func main() {ticker := time.NewTicker(time.Second)for range ticker.C {client, err := createDefaultSftpClient()if err != nil {panic(err)}// 演示用wd, err := client.Getwd()if err != nil {panic(err)}fmt.Println(wd)client.Close()}
}func createSFTPClient(user, pwd, host, port string, pemBytes []byte) (*ssh.Client, *sftp.Client, error) {var authMethods []ssh.AuthMethodif pwd != "" {authMethods = append(authMethods, ssh.Password(pwd))}if len(pemBytes) > 0 {signer, err := ssh.ParsePrivateKey(pemBytes)if err != nil {return nil, nil, err}authMethods = append(authMethods, ssh.PublicKeys(signer))}config := &ssh.ClientConfig{User: user,Auth: authMethods,HostKeyCallback: ssh.InsecureIgnoreHostKey(),}conn, err := ssh.Dial("tcp", host+":"+port, config)if err != nil {return nil, nil, err}client, err := sftp.NewClient(conn)if err != nil {return nil, nil, err}return conn, client, nil
}var defaultSftpPemBytes = []byte(``)func createDefaultSftpClient() (*sftp.Client, error) {_, client, err := createSFTPClient("foo", "test", "127.0.0.1", "2222", defaultSftpPemBytes)return client, err
}
本篇的测试环境是 windows
,当上述程序跑起来后,查看本机 2222
端口的使用情况(netstat -ano | findstr 2222
),这一看,果然是好多连接啊。为什么我的 client
已经 Close
了还是会有这么多连接未关闭呢?
想必细心的朋友们已经发现了问题,我故意给 createSFTPClient
返回了两个连接,一个是 ssh.Client
还有一个是 sftp.Client
,但是 createDefaultSftpClient
只返回了 sftp.Client
。
查看 github.com/pkg/sftp
源码发现:
sftp.NewClient
会调用SSH
连接的NewSession
方法创建一个新会话,但不会持有SSH
连接的所有权。sftpClient.Close()
仅关闭SFTP
会话的Channel
,而SSH
连接的生命周期由调用方(即sshClient
)控制。
原因就是 ssh.Client
创建了没有关闭,必须要显示调用 sshClient.Close()
!!!
搭建测试 sftp 服务
使用 docker
镜像 atmoz/sftp
搭建 sftp
服务。
docker-compose.yaml
version: '3.8'services:sftp:image: atmoz/sftpvolumes:- ./atmoz_data:/home/foo/uploads# - ./atmoz_ssh_keys:/home/foo/.ssh/keys # 支持密钥登录command: foo:test:1001 # 用户名:空密码:UID:GIDports:- "2222:22"
这里我踩了两个坑
linux
环境下,服务启动后,尝试创建目录时报错mkdir /test: permission denied
。 是因为宿主机目录的权限或所有权未与容器内用户的UID/GID
匹配。例如,容器内用户UID
为 1001,但宿主机目录所有者是root
,导致权限冲突。可以调整宿主机目录的权限chown -R 1001:1001 /宿主机目录/atmoz_data
。- 若需支持密钥登录,需将上述的
docker-compose.yaml
中volumes
的注释去掉。注意:宿主机的atmoz_ssh_keys
目录下一定要把自己生成的ssh key
公钥放进去,不然会报错。
[/usr/local/bin/create-sftp-user] Parsing user data: "foo:test:1001"
cat: '/home/foo/.ssh/keys/*': No such file or directory
因为使用 atmoz/sftp
镜像时,若在 command
中定义了用户 foo:test:1001
,镜像会自动执行以下操作:
- 创建用户
foo(UID 1001)
。 - 尝试从
/home/foo/.ssh/keys/
目录加载公钥文件(*.pub
或authorized_keys
)。 - 将公钥写入
/home/foo/.ssh/authorized_keys
。
由于将容器内的 /home/foo/.ssh/keys/
目录映射出去了,但是有没放密钥文件进去,找不到文件,所以就直接报错了。
这里还有一点,atmoz/sftp
搭建的服务应该是不支持 Ed25519
类型的密钥的,可以使用 rsa
。
ssh-keygen -t rsa -b 4096
总结
本文主要分析了使用 go
三方库 github.com/pkg/sftp
访问 sftp
服务出现大量的连接未关闭的异常情况,需要显式调用 sshClient.Close()
。
接着介绍了如何使用 docker
搭建 sftp
服务,一个权限,一个密钥,需要注意。