|
|
|
package obfssh
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"runtime"
|
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/containerd/console"
|
|
|
|
"github.com/fangdingjun/go-log/v5"
|
|
|
|
"github.com/pkg/sftp"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Server is server connection
|
|
|
|
type Server struct {
|
|
|
|
conn net.Conn
|
|
|
|
sshConn *ssh.ServerConn
|
|
|
|
forwardedPorts map[string]net.Listener
|
|
|
|
exitCh chan struct{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewServer create a new struct for Server
|
|
|
|
//
|
|
|
|
// c is net.Conn
|
|
|
|
//
|
|
|
|
// config is &ssh.ServerConfig
|
|
|
|
//
|
|
|
|
// conf is the server configure
|
|
|
|
//
|
|
|
|
//
|
|
|
|
func NewServer(c net.Conn, config *ssh.ServerConfig, conf *Conf) (*Server, error) {
|
|
|
|
sshConn, ch, req, err := ssh.NewServerConn(&TimedOutConn{c, 15 * 60 * time.Second}, config)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
sc := &Server{
|
|
|
|
conn: c,
|
|
|
|
sshConn: sshConn,
|
|
|
|
forwardedPorts: map[string]net.Listener{},
|
|
|
|
exitCh: make(chan struct{})}
|
|
|
|
go sc.handleGlobalRequest(req)
|
|
|
|
go sc.handleNewChannelRequest(ch)
|
|
|
|
return sc, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run waits for server connection finish
|
|
|
|
func (sc *Server) Run() {
|
|
|
|
sc.sshConn.Wait()
|
|
|
|
log.Debugf("ssh connection closed")
|
|
|
|
sc.close()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sc *Server) close() {
|
|
|
|
log.Debugf("close connection")
|
|
|
|
sc.sshConn.Close()
|
|
|
|
//log.Debugf( "close listener")
|
|
|
|
for _, l := range sc.forwardedPorts {
|
|
|
|
log.Debugf("close listener %s", l.Addr())
|
|
|
|
l.Close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sc *Server) handleNewChannelRequest(ch <-chan ssh.NewChannel) {
|
|
|
|
for newch := range ch {
|
|
|
|
|
|
|
|
log.Debugf("request channel %s", newch.ChannelType())
|
|
|
|
|
|
|
|
switch newch.ChannelType() {
|
|
|
|
case "session":
|
|
|
|
go sc.handleSession(newch)
|
|
|
|
continue
|
|
|
|
|
|
|
|
case "direct-tcpip":
|
|
|
|
go handleDirectTcpip(newch)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Errorf("reject channel request %s", newch.ChannelType())
|
|
|
|
|
|
|
|
newch.Reject(ssh.UnknownChannelType, "unknown channel type")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sc *Server) handleGlobalRequest(req <-chan *ssh.Request) {
|
|
|
|
for r := range req {
|
|
|
|
|
|
|
|
log.Debugf("global request %s", r.Type)
|
|
|
|
|
|
|
|
switch r.Type {
|
|
|
|
case "tcpip-forward":
|
|
|
|
log.Debugf("request port forward")
|
|
|
|
go sc.handleTcpipForward(r)
|
|
|
|
continue
|
|
|
|
case "cancel-tcpip-forward":
|
|
|
|
log.Debugf("request cancel port forward")
|
|
|
|
go sc.handleCancelTcpipForward(r)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.WantReply {
|
|
|
|
r.Reply(false, nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func serveSFTP(ch ssh.Channel) {
|
|
|
|
defer ch.Close()
|
|
|
|
|
|
|
|
server, err := sftp.NewServer(ch)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Debugf("start sftp server failed: %s", err)
|
|
|
|
ch.SendRequest("exit-status", false, ssh.Marshal(exitStatus{Status: 127}))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := server.Serve(); err != nil {
|
|
|
|
log.Debugf("sftp server finished with error: %s", err)
|
|
|
|
ch.SendRequest("exit-status", false, ssh.Marshal(exitStatus{Status: 127}))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ch.SendRequest("exit-status", false, ssh.Marshal(exitStatus{Status: 0}))
|
|
|
|
}
|
|
|
|
|
|
|
|
type directTcpipMsg struct {
|
|
|
|
Raddr string
|
|
|
|
Rport uint32
|
|
|
|
Laddr string
|
|
|
|
Lport uint32
|
|
|
|
}
|
|
|
|
|
|
|
|
type args struct {
|
|
|
|
Arg string
|
|
|
|
}
|
|
|
|
|
|
|
|
type envArgs struct {
|
|
|
|
Name string
|
|
|
|
Value string
|
|
|
|
}
|
|
|
|
|
|
|
|
type exitStatus struct {
|
|
|
|
Status uint32
|
|
|
|
}
|
|
|
|
|
|
|
|
type ptyReq struct {
|
|
|
|
Term string
|
|
|
|
Columns uint32
|
|
|
|
Rows uint32
|
|
|
|
Width uint32
|
|
|
|
Height uint32
|
|
|
|
Mode string
|
|
|
|
}
|
|
|
|
|
|
|
|
type windowChange struct {
|
|
|
|
Columns uint32
|
|
|
|
Rows uint32
|
|
|
|
Width uint32
|
|
|
|
Height uint32
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sc *Server) handleSession(newch ssh.NewChannel) {
|
|
|
|
ch, req, err := newch.Accept()
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("%s", err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ret := false
|
|
|
|
|
|
|
|
var _cmd args
|
|
|
|
var cmd *exec.Cmd
|
|
|
|
var env []string
|
|
|
|
var _ptyReq ptyReq
|
|
|
|
var _windowChange windowChange
|
|
|
|
var _console console.Console
|
|
|
|
var ptsname string
|
|
|
|
|
|
|
|
for r := range req {
|
|
|
|
switch r.Type {
|
|
|
|
case "subsystem":
|
|
|
|
if err := ssh.Unmarshal(r.Payload, &_cmd); err == nil {
|
|
|
|
if _cmd.Arg == "sftp" { // only support sftp
|
|
|
|
ret = true
|
|
|
|
log.Debugf("handle sftp request")
|
|
|
|
go serveSFTP(ch)
|
|
|
|
} else {
|
|
|
|
ret = false
|
|
|
|
log.Debugln("subsystem", _cmd.Arg, "not support")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ret = false
|
|
|
|
log.Debugln("get subsystem arg error", err)
|
|
|
|
}
|
|
|
|
case "shell":
|
|
|
|
ret = true
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
cmd = exec.Command("powershell")
|
|
|
|
} else {
|
|
|
|
cmd = exec.Command("bash", "-l")
|
|
|
|
}
|
|
|
|
cmd.Env = env
|
|
|
|
go handleShell(cmd, ch, _console, ptsname)
|
|
|
|
case "signal":
|
|
|
|
log.Debugln("got signal")
|
|
|
|
ret = true
|
|
|
|
case "exec":
|
|
|
|
ret = true
|
|
|
|
if err = ssh.Unmarshal(r.Payload, &_cmd); err == nil {
|
|
|
|
log.Infoln("execute command", _cmd.Arg)
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
cmd = exec.Command("powershell", "-Command", _cmd.Arg)
|
|
|
|
} else {
|
|
|
|
cmd = exec.Command("bash", "-c", _cmd.Arg)
|
|
|
|
}
|
|
|
|
cmd.Env = env
|
|
|
|
//cmd.Stdin = ch
|
|
|
|
go handleCommand(cmd, ch)
|
|
|
|
} else {
|
|
|
|
log.Debugln(err)
|
|
|
|
ret = false
|
|
|
|
}
|
|
|
|
case "pty-req":
|
|
|
|
ret = true
|
|
|
|
err = ssh.Unmarshal(r.Payload, &_ptyReq)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorln(err)
|
|
|
|
ret = false
|
|
|
|
}
|
|
|
|
log.Debugf("pty req %+v", _ptyReq)
|
|
|
|
if err == nil && (runtime.GOOS == "unix" || runtime.GOOS == "linux") {
|
|
|
|
_console, ptsname, err = newPty()
|
|
|
|
if err == nil {
|
|
|
|
log.Debugf("allocate pty %s", ptsname)
|
|
|
|
env = append(env, fmt.Sprintf("SSH_TTY=%s", ptsname))
|
|
|
|
ws, err := _console.Size()
|
|
|
|
if err != nil {
|
|
|
|
log.Errorln(err)
|
|
|
|
} else {
|
|
|
|
log.Debugf("current console %+v", ws)
|
|
|
|
ws.Height = uint16(_ptyReq.Rows)
|
|
|
|
ws.Width = uint16(_ptyReq.Columns)
|
|
|
|
if err = _console.Resize(ws); err != nil {
|
|
|
|
log.Errorln(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log.Errorln(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case "env":
|
|
|
|
var arg envArgs
|
|
|
|
ret = true
|
|
|
|
if err = ssh.Unmarshal(r.Payload, &arg); err == nil {
|
|
|
|
log.Debugf("got env %s=%s", arg.Name, arg.Value)
|
|
|
|
env = append(env, fmt.Sprintf("%s=%s", arg.Name, arg.Value))
|
|
|
|
} else {
|
|
|
|
log.Debugln("parse env failed", err)
|
|
|
|
ret = false
|
|
|
|
}
|
|
|
|
case "window-change":
|
|
|
|
ret = true
|
|
|
|
err = ssh.Unmarshal(r.Payload, &_windowChange)
|
|
|
|
if err != nil {
|
|
|
|
ret = false
|
|
|
|
log.Errorln(err)
|
|
|
|
}
|
|
|
|
if err == nil && _console != nil {
|
|
|
|
ws, err := _console.Size()
|
|
|
|
if err != nil {
|
|
|
|
log.Errorln(err)
|
|
|
|
} else {
|
|
|
|
log.Debugf("current console %+v", ws)
|
|
|
|
ws.Height = uint16(_windowChange.Rows)
|
|
|
|
ws.Width = uint16(_windowChange.Columns)
|
|
|
|
if err = _console.Resize(ws); err != nil {
|
|
|
|
log.Errorln(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log.Debugf("window change %+v", _windowChange)
|
|
|
|
default:
|
|
|
|
ret = false
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("session request %s, reply %v", r.Type, ret)
|
|
|
|
|
|
|
|
if r.WantReply {
|
|
|
|
r.Reply(ret, nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleShell(cmd *exec.Cmd, ch ssh.Channel, _console console.Console, ptsname string) {
|
|
|
|
defer func() {
|
|
|
|
ch.Close()
|
|
|
|
if _console != nil {
|
|
|
|
_console.Close()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
log.Infoln("start shell")
|
|
|
|
|
|
|
|
if _console != nil {
|
|
|
|
_tty, err := os.OpenFile(ptsname, syscall.O_RDWR|syscall.O_NOCTTY, 0600)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorln(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer _tty.Close()
|
|
|
|
cmd.Stderr = _tty
|
|
|
|
cmd.Stdout = _tty
|
|
|
|
cmd.Stdin = _tty
|
|
|
|
if cmd.SysProcAttr == nil {
|
|
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
|
|
}
|
|
|
|
|
|
|
|
setProcAttr(cmd.SysProcAttr)
|
|
|
|
|
|
|
|
go io.Copy(ch, _console)
|
|
|
|
go io.Copy(_console, ch)
|
|
|
|
} else {
|
|
|
|
cmd.Stderr = ch
|
|
|
|
cmd.Stdout = ch
|
|
|
|
|
|
|
|
in, err := cmd.StdinPipe()
|
|
|
|
if err != nil {
|
|
|
|
ch.SendRequest("exit-status", false,
|
|
|
|
ssh.Marshal(exitStatus{Status: 127}))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
defer in.Close()
|
|
|
|
io.Copy(in, ch)
|
|
|
|
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
|
|
log.Debugln("start command ", err)
|
|
|
|
ch.SendRequest("exit-status", false,
|
|
|
|
ssh.Marshal(exitStatus{Status: 126}))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
code := 0
|
|
|
|
if err = cmd.Wait(); err != nil {
|
|
|
|
log.Debugln(err)
|
|
|
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
|
|
|
if s, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
|
|
|
code = s.ExitStatus()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ch.SendRequest("exit-status", false,
|
|
|
|
ssh.Marshal(exitStatus{Status: uint32(code)}))
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleCommand(cmd *exec.Cmd, ch ssh.Channel) {
|
|
|
|
defer ch.Close()
|
|
|
|
|
|
|
|
cmd.Stdout = ch
|
|
|
|
cmd.Stderr = ch
|
|
|
|
//log.Debugln("execute command", cmd)
|
|
|
|
in, err := cmd.StdinPipe()
|
|
|
|
if err != nil {
|
|
|
|
log.Debugln(err)
|
|
|
|
ch.SendRequest("exit-status", false,
|
|
|
|
ssh.Marshal(exitStatus{Status: 127}))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
defer in.Close()
|
|
|
|
io.Copy(in, ch)
|
|
|
|
}()
|
|
|
|
code := 0
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
log.Debugln(err)
|
|
|
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
|
|
|
if s, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
|
|
|
code = s.ExitStatus()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ch.SendRequest("exit-status", false,
|
|
|
|
ssh.Marshal(exitStatus{Status: uint32(code)}))
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleDirectTcpip(newch ssh.NewChannel) {
|
|
|
|
var r directTcpipMsg
|
|
|
|
|
|
|
|
data := newch.ExtraData()
|
|
|
|
|
|
|
|
err := ssh.Unmarshal(data, &r)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorln("invalid ssh parameter")
|
|
|
|
newch.Reject(ssh.ConnectionFailed, "invalid argument")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("create connection to %s:%d", r.Raddr, r.Rport)
|
|
|
|
|
|
|
|
rconn, err := dialer.Dial("tcp", net.JoinHostPort(r.Raddr, fmt.Sprintf("%d", r.Rport)))
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("%s", err.Error())
|
|
|
|
newch.Reject(ssh.ConnectionFailed, "invalid argument")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
channel, requests, err := newch.Accept()
|
|
|
|
if err != nil {
|
|
|
|
rconn.Close()
|
|
|
|
log.Errorf("%s", err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
//log.Println("forward")
|
|
|
|
go ssh.DiscardRequests(requests)
|
|
|
|
|
|
|
|
PipeAndClose(channel, rconn)
|
|
|
|
}
|
|
|
|
|
|
|
|
type tcpipForwardAddr struct {
|
|
|
|
Addr string
|
|
|
|
Port uint32
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sc *Server) handleCancelTcpipForward(req *ssh.Request) {
|
|
|
|
var a tcpipForwardAddr
|
|
|
|
|
|
|
|
if err := ssh.Unmarshal(req.Payload, &a); err != nil {
|
|
|
|
log.Errorf("invalid ssh parameter for cancel port forward")
|
|
|
|
if req.WantReply {
|
|
|
|
req.Reply(false, nil)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
k := fmt.Sprintf("%s:%d", a.Addr, a.Port)
|
|
|
|
if l, ok := sc.forwardedPorts[k]; ok {
|
|
|
|
l.Close()
|
|
|
|
delete(sc.forwardedPorts, k)
|
|
|
|
}
|
|
|
|
|
|
|
|
if req.WantReply {
|
|
|
|
req.Reply(true, nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sc *Server) handleTcpipForward(req *ssh.Request) {
|
|
|
|
var addr tcpipForwardAddr
|
|
|
|
if err := ssh.Unmarshal(req.Payload, &addr); err != nil {
|
|
|
|
log.Errorf("parse ssh data error: %s", err)
|
|
|
|
if req.WantReply {
|
|
|
|
req.Reply(false, nil)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if addr.Port > 65535 || addr.Port < 0 {
|
|
|
|
log.Errorf("invalid port %d", addr.Port)
|
|
|
|
if req.WantReply {
|
|
|
|
req.Reply(false, nil)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ip := net.ParseIP(addr.Addr)
|
|
|
|
if ip == nil {
|
|
|
|
log.Errorf("invalid ip %d", addr.Port)
|
|
|
|
if req.WantReply {
|
|
|
|
req.Reply(false, nil)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
k := fmt.Sprintf("%s:%d", addr.Addr, addr.Port)
|
|
|
|
|
|
|
|
if _, ok := sc.forwardedPorts[k]; ok {
|
|
|
|
// port in use
|
|
|
|
log.Errorf("port in use: %s", k)
|
|
|
|
if req.WantReply {
|
|
|
|
req.Reply(false, nil)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
//log.Debugf( "get request for addr: %s, port: %d", addr.Addr, addr.Port)
|
|
|
|
|
|
|
|
l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: ip, Port: int(addr.Port)})
|
|
|
|
if err != nil {
|
|
|
|
// listen failed
|
|
|
|
log.Errorf("%s", err.Error())
|
|
|
|
if req.WantReply {
|
|
|
|
req.Reply(false, nil)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
a1 := l.Addr()
|
|
|
|
log.Infof("Listening port %s", a1)
|
|
|
|
p := struct {
|
|
|
|
Port uint32
|
|
|
|
}{
|
|
|
|
uint32(a1.(*net.TCPAddr).Port),
|
|
|
|
}
|
|
|
|
|
|
|
|
sc.forwardedPorts[k] = l
|
|
|
|
|
|
|
|
if req.WantReply {
|
|
|
|
req.Reply(true, ssh.Marshal(p))
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
c, err := l.Accept()
|
|
|
|
if err != nil {
|
|
|
|
log.Debugf("%s", err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
log.Infof("accept connection from %s", c.RemoteAddr())
|
|
|
|
go func(c net.Conn) {
|
|
|
|
laddr := c.LocalAddr()
|
|
|
|
raddr := c.RemoteAddr()
|
|
|
|
a2 := struct {
|
|
|
|
laddr string
|
|
|
|
lport uint32
|
|
|
|
raddr string
|
|
|
|
rport uint32
|
|
|
|
}{
|
|
|
|
addr.Addr,
|
|
|
|
uint32(laddr.(*net.TCPAddr).Port),
|
|
|
|
raddr.(*net.TCPAddr).IP.String(),
|
|
|
|
uint32(raddr.(*net.TCPAddr).Port),
|
|
|
|
}
|
|
|
|
ch, r, err := sc.sshConn.OpenChannel("forwarded-tcpip", ssh.Marshal(a2))
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("forward port failed: %s", err.Error())
|
|
|
|
c.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
go ssh.DiscardRequests(r)
|
|
|
|
PipeAndClose(c, ch)
|
|
|
|
}(c)
|
|
|
|
}
|
|
|
|
}
|