├── README.md ├── config.go ├── go.mod ├── go.sum ├── sshtunnel.go └── version ├── linux64.bat ├── main.go └── win64.bat /README.md: -------------------------------------------------------------------------------- 1 | # sshtunnel 2 | go语言实现的一个SSH隧道端口转发程序 3 | 4 | ## 运行命令 5 | ``` 6 | sshtunnel.exe ./config.json 7 | ``` 8 | 9 | ## 配置示例 10 | ```json 11 | [ 12 | { 13 | "addr": "10.0.0.123:22", 14 | "user": "sshuser", 15 | "pass": "sshpass", 16 | "tunnels": [ 17 | { 18 | "isInput": true, 19 | "remote": "0.0.0.0:58000", 20 | "local": "127.0.0.1:8000" 21 | }, 22 | { 23 | "remote": "127.0.0.1:8000", 24 | "local": "0.0.0.0:58000" 25 | } 26 | ] 27 | } 28 | ] 29 | ``` 30 | 31 | ## 配置说明 32 | 配置文件采用JSON文件格式,支持多主机和多转发 33 | - addr: 需要开启SSH隧道的主机地址,格式为【IP地址:端口】 34 | - user: 主机访问用户名 35 | - pass: 主机访问密码,可选配置(未配置时程序将通过控制台输入) 36 | - tunnels: 包含的隧道转发 37 | - isInput: 开启远端输入,需开启配置/etc/ssh/ssh_config:AllowTcpForwarding=yes,GatewayPorts=yes 38 | - remote: 开启隧道的远程主机配置,格式为【IP地址:端口】 39 | - local: 开启隧道映射到本地的配置,格式为【IP地址:端口】 40 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package sshtunnel 2 | 3 | type Tunnel struct { 4 | IsInput bool `json:"isInput"` 5 | Remote string `json:"remote"` 6 | Local string `json:"local"` 7 | } 8 | 9 | type Config struct { 10 | Addr string `json:"addr"` 11 | User string `json:"user"` 12 | Pass string `json:"pass,omitempty"` 13 | Timeout int `json:"timeout"` 14 | Tunnels []Tunnel `json:"tunnels,omitempty"` 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/scchenyong/sshtunnel 2 | 3 | go 1.12 4 | 5 | require golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 6 | 7 | replace golang.org/x/crypto => github.com/golang/crypto v0.0.0-20201002170205-7f63de1d35b0 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:o2dq4P/Rx0JEcYpoC46Z2DIDm/DtEt2u9B1yKkaeAQg= 2 | github.com/golang/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 3 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 4 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 5 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 6 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 7 | -------------------------------------------------------------------------------- /sshtunnel.go: -------------------------------------------------------------------------------- 1 | package sshtunnel 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/crypto/ssh" 6 | "golang.org/x/crypto/ssh/terminal" 7 | "io" 8 | "log" 9 | "math" 10 | "net" 11 | "strings" 12 | "sync" 13 | "syscall" 14 | "time" 15 | ) 16 | 17 | const ( 18 | SSHConnectDefaultTimeout = 5 * time.Second 19 | SSHReConnectTime = 5 * time.Second 20 | ) 21 | 22 | type SSHTunnel struct { 23 | config *Config 24 | sshClient *ssh.Client 25 | closed bool 26 | closeOnce sync.Once 27 | } 28 | 29 | func NewSSHTunnel(config *Config) *SSHTunnel { 30 | st := new(SSHTunnel) 31 | st.config = config 32 | return st 33 | } 34 | 35 | func (t *SSHTunnel) Start() { 36 | if len(t.config.Pass) == 0 { 37 | t.setPass() 38 | } 39 | t.createTunnel() 40 | } 41 | 42 | func (t *SSHTunnel) Close() { 43 | t.closeOnce.Do(func() { 44 | t.closed = true 45 | if nil != t.sshClient { 46 | t.sshClient.Close() 47 | } 48 | }) 49 | } 50 | 51 | func (t *SSHTunnel) sshSessionCheck() { 52 | session, err := t.sshClient.NewSession() 53 | if err != nil { 54 | t.sshClient = nil 55 | return 56 | } 57 | session.Close() 58 | } 59 | 60 | func (t *SSHTunnel) GetSSHClient() (*ssh.Client, error) { 61 | if t.sshClient != nil { 62 | return t.sshClient, nil 63 | } 64 | timeout := SSHConnectDefaultTimeout 65 | if t.config.Timeout > 0 { 66 | timeout = time.Duration(t.config.Timeout) * time.Second 67 | } 68 | sc := &ssh.ClientConfig{ 69 | User: t.config.User, 70 | Auth: []ssh.AuthMethod{ 71 | ssh.Password(t.config.Pass), 72 | }, 73 | Timeout: timeout, 74 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 75 | } 76 | var err error 77 | t.sshClient, err = ssh.Dial("tcp", t.config.Addr, sc) 78 | if err != nil { 79 | return nil, err 80 | } 81 | log.Printf("连接到服务器成功: %s", t.config.Addr) 82 | return t.sshClient, err 83 | } 84 | 85 | func (t *SSHTunnel) createTunnel() { 86 | t.initSSHClient() 87 | for _, tunnel := range t.config.Tunnels { 88 | if !tunnel.IsInput { 89 | go t.createLocalOutput(tunnel) 90 | continue 91 | } 92 | go t.createRemoteInput(tunnel) 93 | } 94 | } 95 | 96 | func (t *SSHTunnel) reconnectRemote(tunnel Tunnel) { 97 | if t.closed { 98 | return 99 | } 100 | t.initSSHClient() 101 | t.createRemoteInput(tunnel) 102 | } 103 | 104 | func (t *SSHTunnel) createRemoteInput(tunnel Tunnel) { 105 | defer t.reconnectRemote(tunnel) 106 | tid := fmt.Sprintf("%s-%s", tunnel.Remote, tunnel.Local) 107 | log.Printf("隧道[%s]远端接收准备开启...", tid) 108 | sc, err := t.GetSSHClient() 109 | if err != nil { 110 | log.Printf("隧道[%s]远端服务接入失败, 错误: %v", tid, err) 111 | return 112 | } 113 | ll, err := sc.Listen("tcp", tunnel.Remote) 114 | if err != nil { 115 | log.Printf("隧道[%s]开启远端接收失败, 错误: %v", tid, err) 116 | t.sshSessionCheck() 117 | return 118 | } 119 | log.Printf("隧道[%s]开启远端接收成功", tid) 120 | defer func() { 121 | ll.Close() 122 | log.Printf("隧道[%s]远端接收关闭!", tid) 123 | }() 124 | log.Printf("隧道[%s]远端接收开启!", tid) 125 | cno := int64(0) 126 | for { 127 | var lc net.Conn 128 | lc, err = ll.Accept() 129 | if err != nil { 130 | log.Printf("隧道[%s]远端接收连接失败, 错误: %v", tid, err) 131 | return 132 | } 133 | if cno >= math.MaxInt64 { 134 | cno = 0 135 | } 136 | cno += 1 137 | go t.handleRemoteConnect(tid, cno, lc, tunnel.Local) 138 | } 139 | } 140 | 141 | func (t *SSHTunnel) createLocalOutput(tunnel Tunnel) { 142 | tid := fmt.Sprintf("%s-%s", tunnel.Local, tunnel.Remote) 143 | log.Printf("隧道[%s]本地接收准备开启...", tid) 144 | ll, err := net.Listen("tcp", tunnel.Local) 145 | if err != nil { 146 | log.Printf("隧道[%s]开启本地接收失败, 错误: %v", tid, err) 147 | return 148 | } 149 | log.Printf("隧道[%s]开启本地接收成功", tid) 150 | defer func() { 151 | ll.Close() 152 | log.Printf("隧道[%s]本地接收关闭!", tid) 153 | }() 154 | log.Printf("隧道[%s]本地接收开启!", tid) 155 | cno := int64(0) 156 | for { 157 | var lc net.Conn 158 | lc, err = ll.Accept() 159 | if err != nil { 160 | log.Printf("隧道[%s]本地接收连接失败, 错误: %v", tid, err) 161 | return 162 | } 163 | if cno >= math.MaxInt64 { 164 | cno = 0 165 | } 166 | cno += 1 167 | go t.handleLocalConnect(tid, cno, lc, tunnel.Remote) 168 | } 169 | } 170 | 171 | func (t *SSHTunnel) handleLocalConnect(tid string, cno int64, lc net.Conn, remote string) { 172 | defer lc.Close() 173 | sc, err := t.GetSSHClient() 174 | if err != nil { 175 | log.Printf("隧道[%s]远端服务接入失败, 错误: %v", tid, err) 176 | return 177 | } 178 | rc, err := sc.Dial("tcp", remote) 179 | if err != nil { 180 | log.Printf("隧道[%s]获取远端服务连接失败, 错误: %v", tid, err) 181 | t.sshSessionCheck() 182 | return 183 | } 184 | cid := fmt.Sprintf("%s:%d", tid, cno) 185 | t.transfer(cid, lc, rc) 186 | } 187 | 188 | func (t *SSHTunnel) handleRemoteConnect(tid string, cno int64, rc net.Conn, local string) { 189 | defer rc.Close() 190 | lc, err := net.Dial("tcp", local) 191 | if err != nil { 192 | log.Printf("隧道[%s]获取本地服务连接失败, 错误: %v", tid, err) 193 | return 194 | } 195 | cid := fmt.Sprintf("%s:%d", tid, cno) 196 | t.transfer(cid, rc, lc) 197 | } 198 | 199 | func (t *SSHTunnel) setPass() { 200 | fmt.Printf("请输入登陆密码[%s@%s]:", t.config.User, t.config.Addr) 201 | bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin)) 202 | t.config.Pass = string(bytePassword) 203 | fmt.Println() 204 | } 205 | 206 | func (t *SSHTunnel) initSSHClient() { 207 | var err error 208 | for { 209 | if t.closed { 210 | return 211 | } 212 | t.sshClient, err = t.GetSSHClient() 213 | if nil != err { 214 | log.Printf("连接到服务器[%s]失败, 错误: %v", t.config.Addr, err) 215 | if strings.Contains(err.Error(), "unable to authenticate") { 216 | t.config.Pass = "" 217 | t.setPass() 218 | continue 219 | } 220 | log.Printf("稍等%.2fs后重试连接", SSHReConnectTime.Seconds()) 221 | time.Sleep(SSHReConnectTime) 222 | } 223 | return 224 | } 225 | } 226 | 227 | func (t *SSHTunnel) transfer(cid string, lc net.Conn, rc net.Conn) { 228 | defer func() { 229 | rc.Close() 230 | lc.Close() 231 | log.Printf("隧道连接[%s]已断开!", cid) 232 | }() 233 | go func() { 234 | defer func() { 235 | rc.Close() 236 | lc.Close() 237 | }() 238 | io.Copy(rc, lc) 239 | }() 240 | log.Printf("隧道连接[%s]已连接!", cid) 241 | io.Copy(lc, rc) 242 | } 243 | -------------------------------------------------------------------------------- /version/linux64.bat: -------------------------------------------------------------------------------- 1 | set GO111MODULE=on 2 | set GOOS=linux 3 | set GOARCH=amd64 4 | go build -ldflags "-w -s" -o sshtunnel_linux_amd64 main.go 5 | 6 | pause -------------------------------------------------------------------------------- /version/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/scchenyong/sshtunnel" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | func main() { 13 | var sts []*sshtunnel.Config 14 | p := "config.json" 15 | if len(os.Args) == 2 { 16 | p = os.Args[1] 17 | } 18 | f, err := os.ReadFile(p) 19 | if err != nil { 20 | log.Printf("载入配置文件出错, 错误: %v", err) 21 | os.Exit(-1) 22 | } 23 | err = json.Unmarshal(f, &sts) 24 | if nil != err { 25 | log.Printf("解析配置文件内容出错, 错误: %v", err) 26 | os.Exit(-1) 27 | } 28 | 29 | var tunnels []*sshtunnel.SSHTunnel 30 | for _, st := range sts { 31 | tunnel := sshtunnel.NewSSHTunnel(st) 32 | tunnel.Start() 33 | tunnels = append(tunnels, tunnel) 34 | } 35 | 36 | signalChan := make(chan os.Signal) 37 | signal.Notify(signalChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 38 | <-signalChan 39 | for _, t := range tunnels { 40 | t.Close() 41 | } 42 | os.Exit(0) 43 | } 44 | -------------------------------------------------------------------------------- /version/win64.bat: -------------------------------------------------------------------------------- 1 | set GO111MODULE=on 2 | set GOOS=windows 3 | set GOARCH=amd64 4 | go build -ldflags "-w -s" -o sshtunnel_windows_amd64.exe main.go 5 | 6 | pause --------------------------------------------------------------------------------