Merge branch 'release/0.2.0'

This commit is contained in:
Ryo Ota 2023-08-10 03:49:36 +09:00
commit 58d3afaaee
11 changed files with 188 additions and 18 deletions

View file

@ -10,3 +10,11 @@ updates:
- nwtgck
assignees:
- nwtgck
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
timezone: Asia/Tokyo
open-pull-requests-limit: 99
reviewers: [ nwtgck ]
assignees: [ nwtgck ]

View file

@ -6,9 +6,9 @@ jobs:
build_multi_platform:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go 1.x
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: "1.20"
- name: Build for multi-platform

View file

@ -9,15 +9,15 @@ jobs:
goreleaser:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: "1.20"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v4
with:
version: v1.3.1
args: release --rm-dist

View file

@ -5,8 +5,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
## [Unreleased]
## [0.2.0] - 2023-08-09
### Added
* Add permissions
### Changed
* Require one user at least
## 0.1.0 - 2023-08-08
### Added
* Initial release
[Unreleased]: https://github.com/nwtgck/handy-sshd/compare/v0.1.0...HEAD
[Unreleased]: https://github.com/nwtgck/handy-sshd/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/nwtgck/handy-sshd/compare/v0.1.0...v0.2.0

View file

@ -1,5 +1,22 @@
# handy-sshd
Portable SSH Server
[![CI](https://github.com/nwtgck/handy-sshd/actions/workflows/ci.yml/badge.svg)](https://github.com/nwtgck/handy-sshd/actions/workflows/ci.yml)
Portable SSH Server
## Install on Ubuntu/Debian
```bash
wget https://github.com/nwtgck/handy-sshd/releases/download/v0.1.0/handy-sshd-0.1.0-linux-amd64.deb
sudo dpkg -i handy-sshd-0.1.0-linux-amd64.deb
```
## Install on Mac
```bash
brew install nwtgck/handy-sshd/handy-sshd
```
Get more executables in [the releases](https://github.com/nwtgck/handy-sshd/releases).
## Examples
@ -22,3 +39,42 @@ handy-sshd -p 2222 --user "john:" --user "alice:"
# Listen on unix domain socket
handy-sshd --unix-socket /tmp/my-unix-socket --user "john:"
```
## Permissions
**All permissions are allowed when nothing is specified.** There are some permissions.
* --allow-direct-tcpip
* --allow-execute
* --allow-sftp
* --allow-tcpip-forward
Specifying `--allow-direct-tcpip` and `--allow-execute` for example allows only them.
The log shows "allowed: " and "NOT allowed: " permissions as follows.
```console
$ handy-sshd --user "john:" --allow-direct-tcpip --allow-execute
2023/08/09 20:49:35 INFO listening on :2222...
2023/08/09 20:49:35 INFO allowed: "direct-tcpip", "execute"
2023/08/09 20:49:35 INFO NOT allowed: "tcpip-forward", "sftp"
```
## --help
```
Portable SSH server
Usage:
handy-sshd [flags]
Flags:
--allow-direct-tcpip client can use local forwarding and SOCKS proxy
--allow-execute client can use shell/interactive shell
--allow-sftp client can use SFTP and SSHFS
--allow-tcpip-forward client can use remote forwarding
-h, --help help for handy-sshd
--host string SSH server host (e.g. 127.0.0.1)
-p, --port uint16 SSH server port (default 2222)
--shell string Shell
--unix-socket string Unix-domain socket
--user stringArray SSH user name (e.g. "john:mypassword")
-v, --version show version
```

View file

@ -21,6 +21,21 @@ var flag struct {
sshUnixSocket string
sshShell string
sshUsers []string
allowTcpipForward bool
allowDirectTcpip bool
allowExecute bool
allowSftp bool
}
var allPermissionFlags = []struct {
name string
flagPtr *bool
}{
{name: "tcpip-forward", flagPtr: &flag.allowTcpipForward},
{name: "direct-tcpip", flagPtr: &flag.allowDirectTcpip},
{name: "execute", flagPtr: &flag.allowExecute},
{name: "sftp", flagPtr: &flag.allowSftp},
}
type sshUser struct {
@ -38,6 +53,12 @@ func init() {
RootCmd.PersistentFlags().StringVarP(&flag.sshShell, "shell", "", "", "Shell")
//RootCmd.PersistentFlags().StringVar(&flag.dnsServer, "dns-server", "", "DNS server (e.g. 1.1.1.1:53)")
RootCmd.PersistentFlags().StringArrayVarP(&flag.sshUsers, "user", "", nil, `SSH user name (e.g. "john:mypassword")`)
// Permission flags
RootCmd.PersistentFlags().BoolVarP(&flag.allowTcpipForward, "allow-tcpip-forward", "", false, "client can use remote forwarding")
RootCmd.PersistentFlags().BoolVarP(&flag.allowDirectTcpip, "allow-direct-tcpip", "", false, "client can use local forwarding and SOCKS proxy")
RootCmd.PersistentFlags().BoolVarP(&flag.allowExecute, "allow-execute", "", false, "client can use shell/interactive shell")
RootCmd.PersistentFlags().BoolVarP(&flag.allowSftp, "allow-sftp", "", false, "client can use SFTP and SSHFS")
}
var RootCmd = &cobra.Command{
@ -51,8 +72,26 @@ var RootCmd = &cobra.Command{
return nil
}
logger := slog.Default()
// Allow all permissions if all permission is not set
{
allPermissionFalse := true
for _, permissionFlag := range allPermissionFlags {
allPermissionFalse = allPermissionFalse && !*permissionFlag.flagPtr
}
if allPermissionFalse {
for _, permissionFlag := range allPermissionFlags {
*permissionFlag.flagPtr = true
}
}
}
sshServer := &handy_sshd.Server{
Logger: logger,
Logger: logger,
AllowTcpipForward: flag.allowTcpipForward,
AllowDirectTcpip: flag.allowDirectTcpip,
AllowExecute: flag.allowExecute,
AllowSftp: flag.allowSftp,
}
var sshUsers []sshUser
for _, u := range flag.sshUsers {
@ -62,7 +101,11 @@ var RootCmd = &cobra.Command{
}
sshUsers = append(sshUsers, sshUser{name: splits[0], password: splits[1]})
}
if len(sshUsers) == 0 {
return fmt.Errorf(`No user specified
e.g. --user "john:mypassword"
e.g. --user "john:"`)
}
// (base: https://gist.github.com/jpillora/b480fde82bff51a06238)
sshConfig := &ssh.ServerConfig{
//Define a function to run when a client attempts a password login
@ -109,6 +152,9 @@ var RootCmd = &cobra.Command{
logger.Info(fmt.Sprintf("listening on %s...", flag.sshUnixSocket))
}
defer ln.Close()
showPermissions(logger)
for {
conn, err := ln.Accept()
if err != nil {
@ -121,9 +167,29 @@ var RootCmd = &cobra.Command{
conn.Close()
continue
}
logger.Info("new SSH connection", "client_version", string(sshConn.ClientVersion()))
logger.Info("new SSH connection", "remote_address", sshConn.RemoteAddr(), "client_version", string(sshConn.ClientVersion()))
go sshServer.HandleGlobalRequests(sshConn, reqs)
go sshServer.HandleChannels(flag.sshShell, chans)
}
},
}
func showPermissions(logger *slog.Logger) {
var allowedList []string
var notAllowedList []string
for _, permissionFlag := range allPermissionFlags {
if *permissionFlag.flagPtr {
allowedList = append(allowedList, `"`+permissionFlag.name+`"`)
} else {
notAllowedList = append(notAllowedList, `"`+permissionFlag.name+`"`)
}
}
showList := func(l []string) string {
if len(l) == 0 {
return "none"
}
return strings.Join(l, ", ")
}
logger.Info(fmt.Sprintf("allowed: %s", showList(allowedList)))
logger.Info(fmt.Sprintf("NOT allowed: %s", showList(notAllowedList)))
}

2
go.mod
View file

@ -9,7 +9,7 @@ require (
github.com/pkg/sftp v1.13.5
github.com/spf13/cobra v1.7.0
golang.org/x/crypto v0.12.0
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de
golang.org/x/exp v0.0.0-20230809094429-853ea248256d
)
require (

4
go.sum
View file

@ -27,8 +27,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de h1:l5Za6utMv/HsBWWqzt4S8X17j+kt1uVETUX5UFhn2rE=
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20230809094429-853ea248256d h1:wu5bD43Ana/nF1ZmaLr3lW/FQeJU8CcI+Ln7yWHViXE=
golang.org/x/exp v0.0.0-20230809094429-853ea248256d/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View file

@ -1,14 +1,12 @@
package main
import (
"fmt"
"github.com/nwtgck/handy-sshd/cmd"
"os"
)
func main() {
if err := cmd.RootCmd.Execute(); err != nil {
_, _ = fmt.Fprintf(os.Stderr, err.Error())
os.Exit(-1)
}
}

View file

@ -28,6 +28,13 @@ import (
type Server struct {
Logger *slog.Logger
// Permissions
AllowTcpipForward bool
AllowDirectTcpip bool
AllowExecute bool // this should not be split into "allow-exec" and "allow-pty-req" for now because "pty-req" can be used not for shell execution.
AllowSftp bool
// TODO: DNS server ?
}
@ -47,6 +54,10 @@ func (s *Server) handleChannel(shell string, newChannel ssh.NewChannel) {
case "session":
s.handleSession(shell, newChannel)
case "direct-tcpip":
if !s.AllowDirectTcpip {
newChannel.Reject(ssh.Prohibited, "direct-tcpip not allowed")
break
}
s.handleDirectTcpip(newChannel)
default:
newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", newChannel.ChannelType()))
@ -67,6 +78,11 @@ func (s *Server) handleSession(shell string, newChannel ssh.NewChannel) {
for req := range requests {
switch req.Type {
case "exec":
if !s.AllowExecute {
s.Logger.Info("execution not allowed (exec)")
req.Reply(false, nil)
break
}
s.handleExecRequest(req, connection)
case "shell":
// We only accept the default shell
@ -75,6 +91,11 @@ func (s *Server) handleSession(shell string, newChannel ssh.NewChannel) {
req.Reply(true, nil)
}
case "pty-req":
if !s.AllowExecute {
s.Logger.Info("execution not allowed (pty-req)")
req.Reply(false, nil)
break
}
termLen := req.Payload[3]
w, h := parseDims(req.Payload[termLen+4:])
shf, err = s.createPty(shell, connection)
@ -140,9 +161,17 @@ func (s *Server) handleExecRequest(req *ssh.Request, connection ssh.Channel) {
func (s *Server) handleSessionSubSystem(req *ssh.Request, connection ssh.Channel) {
// https://github.com/pkg/sftp/blob/42e9800606febe03f9cdf1d1283719af4a5e6456/examples/go-sftp-server/main.go#L111
ok := string(req.Payload[4:]) == "sftp"
req.Reply(ok, nil)
if string(req.Payload[4:]) != "sftp" {
req.Reply(false, nil)
return
}
if !s.AllowSftp {
s.Logger.Info("sftp not allowed")
req.Reply(false, nil)
return
}
req.Reply(true, nil)
serverOptions := []sftp.ServerOption{
sftp.WithDebug(os.Stderr),
}
@ -232,6 +261,11 @@ func (s *Server) HandleGlobalRequests(sshConn *ssh.ServerConn, reqs <-chan *ssh.
for req := range reqs {
switch req.Type {
case "tcpip-forward":
if !s.AllowTcpipForward {
s.Logger.Info("tcpip-forward not allowed")
req.Reply(false, nil)
break
}
s.handleTcpipForward(sshConn, req)
default:
// discard

View file

@ -1,3 +1,3 @@
package version
const Version = "0.1.0"
const Version = "0.2.0"