mirror of
https://github.com/nwtgck/handy-sshd.git
synced 2025-06-10 08:03:09 +00:00
Merge branch 'release/0.2.0'
This commit is contained in:
commit
58d3afaaee
11 changed files with 188 additions and 18 deletions
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
|
@ -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 ]
|
||||
|
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
@ -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
|
||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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
|
||||
|
|
56
README.md
56
README.md
|
@ -1,6 +1,23 @@
|
|||
# handy-sshd
|
||||
[](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
|
||||
|
||||
```bash
|
||||
|
@ -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
|
||||
```
|
||||
|
|
70
cmd/root.go
70
cmd/root.go
|
@ -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,
|
||||
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
2
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
38
server.go
38
server.go
|
@ -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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
package version
|
||||
|
||||
const Version = "0.1.0"
|
||||
const Version = "0.2.0"
|
||||
|
|
Loading…
Add table
Reference in a new issue