2017-03-04 11:09:10 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2018-07-22 23:04:18 +02:00
|
|
|
"context"
|
2017-03-04 11:09:10 +01:00
|
|
|
"encoding/binary"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"log"
|
2021-01-18 09:46:20 +01:00
|
|
|
"net"
|
2017-03-04 11:09:10 +01:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
2021-01-18 09:46:20 +01:00
|
|
|
"strconv"
|
2018-06-23 15:45:50 +02:00
|
|
|
"strings"
|
2017-03-04 11:09:10 +01:00
|
|
|
"sync"
|
|
|
|
"syscall"
|
|
|
|
"unsafe"
|
|
|
|
|
2021-01-18 09:46:20 +01:00
|
|
|
"github.com/gokrazy/gokrazy"
|
2017-03-04 11:09:10 +01:00
|
|
|
"github.com/google/shlex"
|
|
|
|
"github.com/kr/pty"
|
2021-06-06 13:54:30 +02:00
|
|
|
"github.com/pkg/sftp"
|
2017-03-04 11:09:10 +01:00
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
)
|
|
|
|
|
2021-01-18 09:46:20 +01:00
|
|
|
func handleChannel(newChan ssh.NewChannel) {
|
|
|
|
switch t := newChan.ChannelType(); t {
|
|
|
|
case "session":
|
|
|
|
handleSession(newChan)
|
|
|
|
case "direct-tcpip":
|
|
|
|
handleTCPIP(newChan)
|
|
|
|
default:
|
|
|
|
newChan.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %q", t))
|
2017-03-04 11:09:10 +01:00
|
|
|
return
|
|
|
|
}
|
2021-01-18 09:46:20 +01:00
|
|
|
}
|
2017-03-04 11:09:10 +01:00
|
|
|
|
2021-01-18 09:46:20 +01:00
|
|
|
func parseAddr(addr string) net.IP {
|
|
|
|
ip := net.ParseIP(addr)
|
|
|
|
if ip == nil {
|
|
|
|
if ips, err := net.LookupIP(addr); err == nil {
|
|
|
|
ip = ips[0] // use first address found
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ip
|
|
|
|
}
|
|
|
|
|
|
|
|
// Forwarding ported from https://github.com/gliderlabs/ssh (BSD3 License)
|
|
|
|
|
|
|
|
// direct-tcpip data struct as specified in RFC4254, Section 7.2
|
|
|
|
type localForwardChannelData struct {
|
|
|
|
DestAddr string
|
|
|
|
DestPort uint32
|
|
|
|
|
|
|
|
OriginAddr string
|
|
|
|
OriginPort uint32
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleTCPIP(newChan ssh.NewChannel) {
|
|
|
|
d := localForwardChannelData{}
|
|
|
|
if err := ssh.Unmarshal(newChan.ExtraData(), &d); err != nil {
|
|
|
|
newChan.Reject(ssh.ConnectionFailed, "error parsing forward data: "+err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var ip net.IP
|
|
|
|
switch *forwarding {
|
|
|
|
case "loopback":
|
|
|
|
if ip = parseAddr(d.DestAddr); ip != nil && !ip.IsLoopback() {
|
|
|
|
newChan.Reject(ssh.Prohibited, "port forwarding not allowed for address")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
case "private-network":
|
|
|
|
if ip = parseAddr(d.DestAddr); ip != nil && !gokrazy.IsInPrivateNet(ip) {
|
|
|
|
newChan.Reject(ssh.Prohibited, "port forwarding not allowed for address")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
newChan.Reject(ssh.Prohibited, "port forwarding is disabled")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// fallthrough for forwarding enabled, validate ip != nil once
|
|
|
|
if ip == nil {
|
|
|
|
newChan.Reject(ssh.Prohibited, "host not reachable")
|
|
|
|
}
|
|
|
|
|
|
|
|
dest := net.JoinHostPort(ip.String(), strconv.Itoa(int(d.DestPort)))
|
|
|
|
|
|
|
|
var dialer net.Dialer
|
|
|
|
dconn, err := dialer.DialContext(context.Background(), "tcp", dest)
|
|
|
|
if err != nil {
|
|
|
|
newChan.Reject(ssh.ConnectionFailed, err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ch, reqs, err := newChan.Accept()
|
|
|
|
if err != nil {
|
|
|
|
dconn.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
go ssh.DiscardRequests(reqs)
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
defer ch.Close()
|
|
|
|
defer dconn.Close()
|
|
|
|
io.Copy(ch, dconn)
|
|
|
|
}()
|
|
|
|
go func() {
|
|
|
|
defer ch.Close()
|
|
|
|
defer dconn.Close()
|
|
|
|
io.Copy(dconn, ch)
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleSession(newChannel ssh.NewChannel) {
|
2017-03-04 11:09:10 +01:00
|
|
|
channel, requests, err := newChannel.Accept()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Could not accept channel (%s)", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sessions have out-of-band requests such as "shell", "pty-req" and "env"
|
|
|
|
go func(channel ssh.Channel, requests <-chan *ssh.Request) {
|
2018-07-22 23:04:18 +02:00
|
|
|
ctx, canc := context.WithCancel(context.Background())
|
|
|
|
defer canc()
|
2017-03-04 11:09:10 +01:00
|
|
|
s := session{channel: channel}
|
|
|
|
for req := range requests {
|
2018-07-22 23:04:18 +02:00
|
|
|
if err := s.request(ctx, req); err != nil {
|
|
|
|
log.Printf("request(%q): %v", req.Type, err)
|
2017-03-04 11:09:10 +01:00
|
|
|
errmsg := []byte(err.Error())
|
|
|
|
// Append a trailing newline; the error message is
|
|
|
|
// displayed as-is by ssh(1).
|
|
|
|
if errmsg[len(errmsg)-1] != '\n' {
|
|
|
|
errmsg = append(errmsg, '\n')
|
|
|
|
}
|
|
|
|
req.Reply(false, errmsg)
|
|
|
|
channel.Write(errmsg)
|
|
|
|
channel.Close()
|
|
|
|
}
|
|
|
|
}
|
2018-07-22 23:04:18 +02:00
|
|
|
log.Printf("requests exhausted")
|
2017-03-04 11:09:10 +01:00
|
|
|
}(channel, requests)
|
|
|
|
}
|
|
|
|
|
2018-06-23 15:45:50 +02:00
|
|
|
func expandPath(env []string) []string {
|
|
|
|
pwd, err := os.Getwd()
|
|
|
|
if err != nil {
|
|
|
|
return env
|
|
|
|
}
|
|
|
|
found := false
|
|
|
|
for idx, val := range env {
|
|
|
|
parts := strings.Split(val, "=")
|
|
|
|
if len(parts) < 2 {
|
|
|
|
continue // malformed entry
|
|
|
|
}
|
|
|
|
key := parts[0]
|
|
|
|
if key != "PATH" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
val := strings.Join(parts[1:], "=")
|
|
|
|
env[idx] = fmt.Sprintf("%s=%s:%s", key, pwd, val)
|
|
|
|
found = true
|
|
|
|
}
|
|
|
|
if !found {
|
|
|
|
const busyboxDefaultPATH = "/sbin:/usr/sbin:/bin:/usr/bin"
|
|
|
|
env = append(env, fmt.Sprintf("PATH=%s:%s", pwd, busyboxDefaultPATH))
|
|
|
|
}
|
|
|
|
return env
|
|
|
|
}
|
|
|
|
|
2017-03-04 11:09:10 +01:00
|
|
|
type session struct {
|
|
|
|
env []string
|
|
|
|
ptyf *os.File
|
|
|
|
ttyf *os.File
|
|
|
|
channel ssh.Channel
|
|
|
|
}
|
|
|
|
|
2018-10-29 18:42:56 +01:00
|
|
|
// ptyreq is a Pseudo-Terminal request as per RFC4254 6.2.
|
|
|
|
type ptyreq struct {
|
|
|
|
TERM string // e.g. vt100
|
|
|
|
WidthCharacters uint32
|
|
|
|
HeightRows uint32
|
|
|
|
WidthPixels uint32
|
|
|
|
HeightPixels uint32
|
|
|
|
Modes string
|
|
|
|
}
|
|
|
|
|
|
|
|
// windowchange is a Window Dimension Change as per RFC4254 6.7.
|
|
|
|
type windowchange struct {
|
|
|
|
WidthColumns uint32
|
|
|
|
HeightRows uint32
|
|
|
|
WidthPixels uint32
|
|
|
|
HeightPixels uint32
|
|
|
|
}
|
|
|
|
|
|
|
|
// env is a Environment Variable request as per RFC4254 6.4.
|
|
|
|
type env struct {
|
|
|
|
VariableName string
|
|
|
|
VariableValue string
|
|
|
|
}
|
|
|
|
|
|
|
|
// execR is a Command request as per RFC4254 6.5.
|
|
|
|
type execR struct {
|
|
|
|
Command string
|
2017-03-04 11:09:10 +01:00
|
|
|
}
|
|
|
|
|
2021-06-06 13:54:30 +02:00
|
|
|
// subsystem is a channel request as specified in RFC4254, Section 6.5
|
|
|
|
type subsystem struct {
|
|
|
|
SubsystemName string
|
|
|
|
}
|
|
|
|
|
2020-05-25 08:58:37 +02:00
|
|
|
func findShell() string {
|
|
|
|
if path, err := exec.LookPath("sh"); err == nil {
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
const wellKnownSerialShell = "/tmp/serial-busybox/ash"
|
|
|
|
if _, err := os.Stat(wellKnownSerialShell); err == nil {
|
|
|
|
return wellKnownSerialShell
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2018-07-22 23:04:18 +02:00
|
|
|
func (s *session) request(ctx context.Context, req *ssh.Request) error {
|
2017-03-04 11:09:10 +01:00
|
|
|
switch req.Type {
|
|
|
|
case "pty-req":
|
2018-10-29 18:42:56 +01:00
|
|
|
var r ptyreq
|
|
|
|
if err := ssh.Unmarshal(req.Payload, &r); err != nil {
|
2017-03-04 11:09:10 +01:00
|
|
|
return err
|
|
|
|
}
|
2018-10-29 18:42:56 +01:00
|
|
|
|
|
|
|
var err error
|
|
|
|
s.ptyf, s.ttyf, err = pty.Open()
|
2017-03-04 11:09:10 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-10-29 18:42:56 +01:00
|
|
|
SetWinsize(s.ptyf.Fd(), r.WidthCharacters, r.HeightRows)
|
2017-03-04 11:09:10 +01:00
|
|
|
// Responding true (OK) here will let the client
|
|
|
|
// know we have a pty ready for input
|
|
|
|
req.Reply(true, nil)
|
|
|
|
|
|
|
|
case "window-change":
|
2018-10-29 18:42:56 +01:00
|
|
|
var r windowchange
|
|
|
|
if err := ssh.Unmarshal(req.Payload, &r); err != nil {
|
2017-03-04 11:09:10 +01:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-10-29 18:42:56 +01:00
|
|
|
SetWinsize(s.ptyf.Fd(), r.WidthColumns, r.HeightRows)
|
|
|
|
|
|
|
|
case "env":
|
|
|
|
var r env
|
|
|
|
if err := ssh.Unmarshal(req.Payload, &r); err != nil {
|
2017-03-04 11:09:10 +01:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-10-29 18:42:56 +01:00
|
|
|
s.env = append(s.env, fmt.Sprintf("%s=%s", r.VariableName, r.VariableValue))
|
2017-03-04 11:09:10 +01:00
|
|
|
|
2021-06-06 13:54:30 +02:00
|
|
|
case "subsystem":
|
|
|
|
var sr subsystem
|
|
|
|
if err := ssh.Unmarshal(req.Payload, &sr); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("client requests subsystem %q", sr.SubsystemName)
|
|
|
|
|
|
|
|
if sr.SubsystemName != "sftp" {
|
|
|
|
return fmt.Errorf("subsystem %q not yet implemented", sr.SubsystemName)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("starting SFTP subsystem")
|
|
|
|
|
|
|
|
srv, err := sftp.NewServer(s.channel, sftp.WithDebug(os.Stderr))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
err := srv.Serve()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("(sftp.Server).Serve(): %v", err)
|
|
|
|
if err == io.EOF {
|
|
|
|
srv.Close()
|
|
|
|
log.Printf("sftp client exited session")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
req.Reply(true, nil)
|
|
|
|
|
2017-03-04 11:09:10 +01:00
|
|
|
case "shell":
|
2018-12-28 16:20:43 +01:00
|
|
|
req.Payload = []byte("\x00\x00\x00\x02sh")
|
2018-06-23 15:43:08 +02:00
|
|
|
fallthrough
|
2017-03-04 11:09:10 +01:00
|
|
|
|
|
|
|
case "exec":
|
2018-10-29 18:42:56 +01:00
|
|
|
var r execR
|
|
|
|
if err := ssh.Unmarshal(req.Payload, &r); err != nil {
|
|
|
|
return err
|
2017-03-04 11:09:10 +01:00
|
|
|
}
|
|
|
|
|
2018-10-29 18:42:56 +01:00
|
|
|
cmdline, err := shlex.Split(r.Command)
|
2017-03-04 11:09:10 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if cmdline[0] == "scp" {
|
|
|
|
return scpSink(s.channel, req, cmdline)
|
|
|
|
}
|
|
|
|
|
2018-06-23 16:14:33 +02:00
|
|
|
var cmd *exec.Cmd
|
2020-05-25 08:58:37 +02:00
|
|
|
if shell := findShell(); shell != "" {
|
|
|
|
cmd = exec.CommandContext(ctx, shell, "-c", r.Command)
|
2018-06-23 16:14:33 +02:00
|
|
|
} else {
|
2018-07-22 23:04:18 +02:00
|
|
|
cmd = exec.CommandContext(ctx, cmdline[0], cmdline[1:]...)
|
2018-06-23 16:14:33 +02:00
|
|
|
}
|
|
|
|
log.Printf("Starting cmd %q", cmd.Args)
|
2018-06-23 15:45:50 +02:00
|
|
|
cmd.Env = expandPath(s.env)
|
2017-03-04 11:09:10 +01:00
|
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
|
|
|
|
|
|
if s.ttyf == nil {
|
|
|
|
stdout, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
stdin, err := cmd.StdinPipe()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
stderr, err := cmd.StderrPipe()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
cmd.SysProcAttr.Setsid = true
|
|
|
|
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-06-11 23:18:11 +02:00
|
|
|
req.Reply(true, nil)
|
|
|
|
|
2017-03-04 11:09:10 +01:00
|
|
|
go io.Copy(s.channel, stdout)
|
2018-06-11 23:17:46 +02:00
|
|
|
go io.Copy(s.channel.Stderr(), stderr)
|
2017-03-04 11:09:10 +01:00
|
|
|
go func() {
|
|
|
|
io.Copy(stdin, s.channel)
|
|
|
|
stdin.Close()
|
|
|
|
}()
|
|
|
|
|
2018-07-22 23:04:18 +02:00
|
|
|
go func() {
|
|
|
|
if err := cmd.Wait(); err != nil {
|
|
|
|
log.Printf("err: %v", err)
|
|
|
|
}
|
2018-10-25 12:49:32 +02:00
|
|
|
status := make([]byte, 4)
|
|
|
|
if ws, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok {
|
|
|
|
binary.BigEndian.PutUint32(status, uint32(ws.ExitStatus()))
|
|
|
|
}
|
2017-03-04 11:09:10 +01:00
|
|
|
|
2018-07-22 23:04:18 +02:00
|
|
|
// See https://tools.ietf.org/html/rfc4254#section-6.10
|
2018-10-25 12:49:32 +02:00
|
|
|
if _, err := s.channel.SendRequest("exit-status", false /* wantReply */, status); err != nil {
|
2018-07-22 23:04:18 +02:00
|
|
|
log.Printf("err2: %v", err)
|
|
|
|
}
|
|
|
|
s.channel.Close()
|
|
|
|
}()
|
2017-03-04 11:09:10 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
s.ttyf.Close()
|
|
|
|
s.ttyf = nil
|
|
|
|
}()
|
|
|
|
|
|
|
|
cmd.Stdout = s.ttyf
|
|
|
|
cmd.Stdin = s.ttyf
|
|
|
|
cmd.Stderr = s.ttyf
|
|
|
|
cmd.SysProcAttr.Setctty = true
|
|
|
|
cmd.SysProcAttr.Setsid = true
|
|
|
|
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
|
|
s.ptyf.Close()
|
|
|
|
s.ptyf = nil
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
close := func() {
|
|
|
|
s.channel.Close()
|
|
|
|
cmd.Process.Wait()
|
|
|
|
}
|
|
|
|
|
|
|
|
// pipe session to cmd and vice-versa
|
|
|
|
var once sync.Once
|
|
|
|
go func() {
|
|
|
|
io.Copy(s.channel, s.ptyf)
|
|
|
|
once.Do(close)
|
|
|
|
}()
|
|
|
|
go func() {
|
|
|
|
io.Copy(s.ptyf, s.channel)
|
|
|
|
once.Do(close)
|
|
|
|
}()
|
|
|
|
|
|
|
|
req.Reply(true, nil)
|
|
|
|
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unknown request type: %q", req.Type)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Winsize stores the Height and Width of a terminal.
|
|
|
|
type Winsize struct {
|
|
|
|
Height uint16
|
|
|
|
Width uint16
|
|
|
|
x uint16 // unused
|
|
|
|
y uint16 // unused
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetWinsize sets the size of the given pty.
|
|
|
|
func SetWinsize(fd uintptr, w, h uint32) {
|
|
|
|
ws := &Winsize{Width: uint16(w), Height: uint16(h)}
|
|
|
|
syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws)))
|
|
|
|
}
|