Go 实现 SSH-Server

发布时间: 更新时间: 总字数:1691 阅读时间:4m 作者: IP上海 分享 网址

使用 Golang 实现 SSH-Server 服务端

介绍

示例

x/crypto/ssh 示例

ssh-server.go ...
package main

import (
	"encoding/binary"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"os/exec"
	"sync"
	"syscall"
	"unsafe"

	"github.com/creack/pty"
	"golang.org/x/crypto/ssh"
)

func privateDiffie() (b ssh.Signer, err error) {
	private, err := ioutil.ReadFile("~/.ssh/id_rsa")
	if err != nil {
		return
	}
	b, err = ssh.ParsePrivateKey(private)
	return
}

// 开启goroutine, 处理连接的Channel
func handleChannels(chans <-chan ssh.NewChannel) {
	for newChannel := range chans {
		go handleChannel(newChannel)
	}
}

// parseDims extracts two uint32s from the provided buffer.
func parseDims(b []byte) (uint32, uint32) {
	w := binary.BigEndian.Uint32(b)
	h := binary.BigEndian.Uint32(b[4:])
	return w, h
}

// 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) {
	log.Printf("window resize %dx%d", w, h)
	ws := &Winsize{Width: uint16(w), Height: uint16(h)}
	syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws)))
}

func handleChannel(ch ssh.NewChannel) {
	// 仅处理session类型的channel(交互式tty服务端)
	if t := ch.ChannelType(); t != "session" {
		ch.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t))
		return
	}
	// 返回两个队列,connection用于数据交换,requests用户控制指令交互
	connection, requests, err := ch.Accept()
	if err != nil {
		log.Printf("Could not accept channel (%s)", err.Error())
		return
	}

	// 为session启动一个bash
	bash := exec.Command("bash")
	// 关闭连接和session
	close := func() {
		connection.Close()
		_, err := bash.Process.Wait()
		if err != nil {
			log.Printf("Failed to exit bash (%s)", err.Error())
		}
		log.Println("Session closed")
	}

	// 为channel分配一个terminal
	log.Print("Creating pty...")
	tty, err := pty.Start(bash)
	if err != nil {
		log.Printf("Could not start pty (%s)", err)
		close()
		return
	}

	// 管道session到bash和visa-versa
	// 使用 sync.Once 确保close只调用一次
	var once sync.Once
	go func() {
		io.Copy(connection, tty)
		once.Do(close)
	}()
	go func() {
		io.Copy(tty, connection)
		once.Do(close)
	}()

	// session out-of-band请求有"shell"、"pty-req"、"env"等几种
	go func() {
		for req := range requests {
			switch req.Type {
			case "shell":
				if len(req.Payload) == 0 {
					req.Reply(true, nil)
				}
			case "pty-req":
				termLen := req.Payload[3]
				w, h := parseDims(req.Payload[termLen+4:])
				SetWinsize(tty.Fd(), w, h)
				req.Reply(true, nil)
			case "window-change":
				w, h := parseDims(req.Payload)
				SetWinsize(tty.Fd(), w, h)
			}
		}
	}()
}

func main() {
	config := &ssh.ServerConfig{
		// 密码验证回调函数
		PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
			if c.User() == "demo" && string(pass) == "123456" {
				return nil, nil
			}
			return nil, fmt.Errorf("password rejected for %q", c.User())
		},
		// NoClientAuth: true, // 客户端不验证,即任何客户端都可以连接
		// ServerVersion: "SSH-2.0-OWN-SERVER", // "SSH-2.0-",SSH版本
	}
	// 秘钥用于SSH交互双方进行 Diffie-hellman 秘钥交换验证
	if b, err := privateDiffie(); err != nil {
		log.Printf("private diffie host key error: %s", err.Error())
	} else {
		config.AddHostKey(b)
	}

	// 监听地址和端口
	listener, err := net.Listen("tcp", "0.0.0.0:19022")
	if err != nil {
		log.Fatalf("Failed to listen on 1022 (%s)", err.Error())
	}
	log.Println("listen to 0.0.0.0:19022")

	// 接受所有连接
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Printf("Failed to accept incoming connection (%s)", err)
			continue
		}
		// 使用前,必须传入连接进行握手 net.Conn
		sshConn, chans, reqs, err := ssh.NewServerConn(conn, config)
		if err != nil {
			log.Printf("Failed to handshake (%s)", err)
			continue
		}
		log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
		go ssh.DiscardRequests(reqs)
		// 接收所有channels
		go handleChannels(chans)
	}
}

gliderlabs/ssh 示例

gliderlabs-ssh.go ...
package main

import (
	"io"

	"github.com/gliderlabs/ssh"
)

func main() {
	ssh.ListenAndServe(":2222", func(s ssh.Session) {
		io.WriteString(s, "Hello world\n")
	})
}

git ssh server 示例

git-ssh-server.go ...
# 转自 https://forcemz.net/git/2019/03/16/MakeAGitSSHServer/
package main

import (
	"errors"
	"fmt"
	"log"
	"net"
	"os"
	"os/exec"
	"os/user"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/gliderlabs/ssh"
)

// Error
var (
	ErrPublicKeyNotFound  = errors.New("Public key not found")
	ErrEncodeHandshake    = errors.New("Handshake encode error")
	ErrRepositoryNotFound = errors.New("Repository Not Found")
	ErrUnreachablePath    = errors.New("Path is unreachable")
)

// StrSplitSkipEmpty skip empty string suggestcap is suggest cap
func StrSplitSkipEmpty(s string, sep byte, suggestcap int) []string {
	sv := make([]string, 0, suggestcap)
	var first, i int
	for ; i < len(s); i++ {
		if s[i] != sep {
			continue
		}
		if first != i {
			sv = append(sv, s[first:i])
		}
		first = i + 1
	}
	if first < len(s) {
		sv = append(sv, s[first:])
	}
	return sv
}

// RepoPathClean todo
func RepoPathClean(p string) (string, error) {
	xp := path.Clean(p)
	pv := StrSplitSkipEmpty(xp, '/', 4)
	if len(pv) != 2 || len(pv[0]) < 2 {
		return "", ErrUnreachablePath
	}
	return pv[0] + "/" + strings.TrimSuffix(pv[1], ".git"), nil
}

// GetSessionEnv env
func GetSessionEnv(s ssh.Session, key string) string {
	prefix := key + "="
	kl := len(prefix)
	for _, str := range s.Environ() {
		if kl >= len(str) {
			continue
		}
		if strings.HasPrefix(str, prefix) {
			return str[kl:]
		}
	}
	return ""
}

// RepoPathStat stat repo
func RepoPathStat(repo string) (string, error) {
	r := filepath.Join("/home/git/root", repo[0:2], repo)
	if !strings.HasSuffix(r, ".git") {
		r += ".git"
	}
	if _, err := os.Stat(r); err != nil {
		return "", ErrRepositoryNotFound
	}
	return r, nil
}

// GitCommand exe git-*
func GitCommand(s ssh.Session, scmd string, repo string) int {
	if s.User() != "git" {
		// filter unallowed user
		fmt.Fprintf(s.Stderr(), "Permission denied, user: \x1b[31m'%s'\x1b[0m\n", s.User())
		return 127
	}

	pwn, err := RepoPathClean(repo)
	if err != nil {
		fmt.Fprintf(s.Stderr(), "Permission denied: \x1b[31m%v\x1b[0m\n", err)
		return 127
	}
	// TODO AUTH
	diskrepo, err := RepoPathStat(pwn)
	if err != nil {
		fmt.Fprintf(s.Stderr(), "Access deined: \x1b[31m%v\x1b[0m\n", err)
		return 127
	}
	version := GetSessionEnv(s, "GIT_PROTOCOL")
	cmd := exec.Command("git", scmd, diskrepo)
	ProcAttr(cmd)
	cmd.Env = append(environ, "GL_ID=key-"+strconv.FormatInt(1111, 10)) /// to set envid
	if len(version) > 0 {
		cmd.Env = append(environ, "GIT_PROTOCOL="+version) /// to set envid
	}
	cmd.Env = append(environ, s.Environ()...) /// include other
	cmd.Stderr = s.Stderr()
	cmd.Stdin = s
	cmd.Stdout = s
	// FIXME: check timeout
	if err = cmd.Start(); err != nil {
		fmt.Fprintln(s.Stderr(), "Server internal error, unable to run git-", scmd)
		log.Printf("Server internal error, unable to run git-%s error: %v", scmd, err)
		return 127
	}
	var exitcode int
	exitChain := make(chan bool, 1)
	go func() {
		if err = cmd.Wait(); err != nil {
			exitcode = 127
		}
		exitChain <- true
	}()
	for {
		select {
		case <-exitChain:
			return exitcode
		case <-s.Context().Done():
			cmd.Process.Kill()
			return 0
		}
	}
}

func sshAuth(ctx ssh.Context, key ssh.PublicKey) bool {
	/// TODO auth

	return true
}

func sessionHandler(s ssh.Session) {
	cmd := s.Command()
	if len(cmd) < 2 || !strings.HasPrefix(cmd[0], "git-") {
		s.Stderr().Write([]byte("bad command " + cmd[0] + "\n"))
		return
	}
	GitCommand(s, strings.TrimPrefix(cmd[0], "git-"), cmd[1])
}

// Userid get
var Userid uint32

// Groupid in
var Groupid uint32

// NeedSetsid is
var NeedSetsid bool

// ProcAttr is
func ProcAttr(cmd *exec.Cmd) {
	if NeedSetsid {
		cmd.SysProcAttr = &syscall.SysProcAttr{
			Credential: &syscall.Credential{
				Uid: Userid,
				Gid: Groupid,
			},
			Setsid: true,
		}
	}
}

// InitializeUtils ini
func InitializeUtils() error {
	if syscall.Getuid() != 0 {
		NeedSetsid = false
		Userid = uint32(syscall.Getuid())
		Groupid = uint32(syscall.Getgid())
		return nil
	}
	//
	user, err := user.Lookup("git")
	if err != nil {
		return err
	}
	xid, err := strconv.ParseUint(user.Uid, 10, 32)
	if err != nil {
		return err
	}
	environ = os.Environ()
	for i, s := range environ {
		if strings.HasPrefix(s, "HOME=") {
			environ[i] = fmt.Sprintf("HOME=%s", user.HomeDir)
		}
	}

	Userid = uint32(xid)
	zid, err := strconv.ParseUint(user.Gid, 10, 32)
	if err != nil {
		return err
	}
	Groupid = uint32(zid)
	NeedSetsid = true
	return nil
}

var environ []string

func main() {
	srv := &ssh.Server{
		Handler:          sessionHandler,
		PublicKeyHandler: sshAuth,
		MaxTimeout:       time.Second * 180,
		IdleTimeout:      time.Second * 3600,
		Version:          "Basalt-2.0-Single",
	}
	InitializeUtils()
	log.Println("starting ssh server on port 2222...")
	ln, err := net.Listen("tcp", ":2222")
	if err != nil {
		return
	}
	srv.Serve(ln)
}

参考

  1. https://mritd.com/2018/11/09/go-interactive-shell
  2. https://blog.gopheracademy.com/advent-2015/ssh-server-in-go/
  3. https://blog.ywfuns.com/article/details/51731560/
Home Archives Categories Tags Statistics
本文总阅读量 次 本站总访问量 次 本站总访客数