Compare commits
	
		
			6 Commits
		
	
	
		
			792dfdba78
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 70788ba261 | |||
| e7994824a5 | |||
| 1d292ec21a | |||
| ee828c3e24 | |||
| a9fdf0a053 | |||
| ca02d9b609 | 
							
								
								
									
										70
									
								
								.goreleaser.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								.goreleaser.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
before:
 | 
			
		||||
  hooks:
 | 
			
		||||
    - go mod tidy
 | 
			
		||||
builds:
 | 
			
		||||
  - id: seashell
 | 
			
		||||
    env:
 | 
			
		||||
      - CGO_ENABLED=0
 | 
			
		||||
    binary: seashell
 | 
			
		||||
    goos:
 | 
			
		||||
      - linux
 | 
			
		||||
    goarch:
 | 
			
		||||
      - amd64
 | 
			
		||||
      - "386"
 | 
			
		||||
      - arm64
 | 
			
		||||
      - arm
 | 
			
		||||
      - riscv64
 | 
			
		||||
archives:
 | 
			
		||||
  - files:
 | 
			
		||||
      - seashell.service
 | 
			
		||||
nfpms:
 | 
			
		||||
  - id: seashell
 | 
			
		||||
    description: "SSH server with virtual hosts and username-based routing"
 | 
			
		||||
    homepage: 'https://gitea.elara.ws/Elara6331/seashell'
 | 
			
		||||
    maintainer: 'Elara Ivy <elara@elara.ws>'
 | 
			
		||||
    license: AGPLv3
 | 
			
		||||
    formats:
 | 
			
		||||
      - deb
 | 
			
		||||
      - rpm
 | 
			
		||||
      - apk
 | 
			
		||||
      - archlinux
 | 
			
		||||
    provides:
 | 
			
		||||
      - seashell
 | 
			
		||||
    conflicts:
 | 
			
		||||
      - seashell
 | 
			
		||||
    contents:
 | 
			
		||||
      - src: seashell.service
 | 
			
		||||
        dst: /etc/systemd/system/seashell.service
 | 
			
		||||
aurs:
 | 
			
		||||
  - name: seashell-bin
 | 
			
		||||
    description: "SSH server with virtual hosts and username-based routing"
 | 
			
		||||
    homepage: 'https://gitea.elara.ws/Elara6331/seashell'
 | 
			
		||||
    maintainers:
 | 
			
		||||
      - 'Elara Ivy <elara@elara.ws>'
 | 
			
		||||
    license: AGPLv3
 | 
			
		||||
    private_key: '{{ .Env.AUR_KEY }}'
 | 
			
		||||
    git_url: 'ssh://aur@aur.archlinux.org/seashell-bin.git'
 | 
			
		||||
    provides:
 | 
			
		||||
      - seashell
 | 
			
		||||
    conflicts:
 | 
			
		||||
      - seashell
 | 
			
		||||
    package: |-
 | 
			
		||||
      # binaries
 | 
			
		||||
      install -Dm755 ./seashell "${pkgdir}/usr/bin/seashell"
 | 
			
		||||
      
 | 
			
		||||
      # services
 | 
			
		||||
      install -Dm644 ./seashell.service "${pkgdir}/etc/systemd/system/seashell.service"      
 | 
			
		||||
release:
 | 
			
		||||
  gitea:
 | 
			
		||||
    owner: Elara6331
 | 
			
		||||
    name: seashell
 | 
			
		||||
gitea_urls:
 | 
			
		||||
  api: 'https://gitea.elara.ws/api/v1/'
 | 
			
		||||
  download: 'https://gitea.elara.ws'
 | 
			
		||||
  skip_tls_verify: false
 | 
			
		||||
checksum:
 | 
			
		||||
  name_template: 'checksums.txt'
 | 
			
		||||
snapshot:
 | 
			
		||||
  name_template: "{{ incpatch .Version }}-next"
 | 
			
		||||
changelog:
 | 
			
		||||
  sort: asc
 | 
			
		||||
							
								
								
									
										25
									
								
								.woodpecker.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								.woodpecker.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
labels:
 | 
			
		||||
  platform: linux/amd64
 | 
			
		||||
 | 
			
		||||
steps:
 | 
			
		||||
  docker:
 | 
			
		||||
    image: gitea.elara.ws/elara6331/builder
 | 
			
		||||
    environment:
 | 
			
		||||
      - REGISTRY=gitea.elara.ws
 | 
			
		||||
      - REGISTRY_USERNAME=Elara6331
 | 
			
		||||
      - KO_DOCKER_REPO=gitea.elara.ws/elara6331
 | 
			
		||||
      - KO_DEFAULTBASEIMAGE=gitea.elara.ws/elara6331/static
 | 
			
		||||
    secrets: [ registry_password ]
 | 
			
		||||
    commands:
 | 
			
		||||
      - registry-login
 | 
			
		||||
      - ko build -B --platform=linux/amd64,linux/arm64,linux/riscv64 -t latest,${CI_COMMIT_TAG} --sbom=none
 | 
			
		||||
    when:
 | 
			
		||||
      event: tag
 | 
			
		||||
  
 | 
			
		||||
  release:
 | 
			
		||||
    image: goreleaser/goreleaser
 | 
			
		||||
    commands:
 | 
			
		||||
      - goreleaser release
 | 
			
		||||
    secrets: [ gitea_token, aur_key ]
 | 
			
		||||
    when:
 | 
			
		||||
      event: tag
 | 
			
		||||
@@ -66,7 +66,7 @@ See the [serial](https://gitea.elara.ws/Elara6331/seashell/wiki/Backends#serial)
 | 
			
		||||
 | 
			
		||||
Seashell can proxy another SSH server. In this case, your client will authenticate to seashell and then seashell will authenticate to the target server, so you should provide seashell with a private key to use for authentication and encryption. If you don't provide this, seashell will ask the authenticating user for the target server's password.
 | 
			
		||||
 | 
			
		||||
The proxy backend takes no extra arguments, so the `ssh` command only requires your username and the routing path:
 | 
			
		||||
Here's an example command:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
ssh user:myproxy@ssh.example.com
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,8 @@ import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"net"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gliderlabs/ssh"
 | 
			
		||||
@@ -41,7 +43,8 @@ import (
 | 
			
		||||
 | 
			
		||||
// proxySettings represents settings for the proxy backend.
 | 
			
		||||
type proxySettings struct {
 | 
			
		||||
	Server      string     `cty:"server"`
 | 
			
		||||
	Host        *string    `cty:"host"`
 | 
			
		||||
	Hosts       *cty.Value `cty:"hosts"`
 | 
			
		||||
	User        *string    `cty:"user"`
 | 
			
		||||
	PrivkeyPath *string    `cty:"privkey"`
 | 
			
		||||
	UserMap     *cty.Value `cty:"user_map"`
 | 
			
		||||
@@ -52,9 +55,6 @@ type proxySettings struct {
 | 
			
		||||
func Proxy(route config.Route) router.Handler {
 | 
			
		||||
	return func(sess ssh.Session, arg string) error {
 | 
			
		||||
		user, _ := sshctx.GetUser(sess.Context())
 | 
			
		||||
		if !route.Permissions.IsAllowed(user, "*") {
 | 
			
		||||
			return router.ErrUnauthorized
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var opts proxySettings
 | 
			
		||||
		err := gocty.FromCtyValue(route.Settings, &opts)
 | 
			
		||||
@@ -78,8 +78,55 @@ func Proxy(route config.Route) router.Handler {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		matched := false
 | 
			
		||||
		addr := arg
 | 
			
		||||
		var portstr, pattern string
 | 
			
		||||
		if opts.Host == nil {
 | 
			
		||||
			hosts := ctyTupleToStrings(opts.Hosts)
 | 
			
		||||
			if len(hosts) == 0 {
 | 
			
		||||
				return errors.New("no host configuration provided")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for _, hostPattern := range hosts {
 | 
			
		||||
				pattern, portstr, ok = strings.Cut(hostPattern, ":")
 | 
			
		||||
				if !ok {
 | 
			
		||||
					// addr is already set by the above statement, so just set the default port
 | 
			
		||||
					portstr = "22"
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				matched, err = path.Match(pattern, arg)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if matched {
 | 
			
		||||
					addr = arg
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			addr, portstr, ok = strings.Cut(*opts.Host, ":")
 | 
			
		||||
			if !ok {
 | 
			
		||||
				// addr is already set by the above statement, so just set the default port
 | 
			
		||||
				portstr = "22"
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !route.Permissions.IsAllowed(user, addr) {
 | 
			
		||||
			return router.ErrUnauthorized
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !matched {
 | 
			
		||||
			return errors.New("provided argument doesn't match any host patterns in configuration")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		port, err := strconv.ParseUint(portstr, 10, 16)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		auth := goph.Auth{
 | 
			
		||||
			gossh.PasswordCallback(requestPassword(opts, sess)),
 | 
			
		||||
			gossh.PasswordCallback(requestPassword(opts, sess, addr)),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if opts.PrivkeyPath != nil {
 | 
			
		||||
@@ -96,24 +143,26 @@ func Proxy(route config.Route) router.Handler {
 | 
			
		||||
			auth = append(goph.Auth{gossh.PublicKeys(pk)}, auth...)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c, err := goph.New(*opts.User, opts.Server, auth)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
		c, err := goph.NewConn(&goph.Config{
 | 
			
		||||
			Auth: auth,
 | 
			
		||||
			User: *opts.User,
 | 
			
		||||
			Addr: addr,
 | 
			
		||||
			Port: uint(port),
 | 
			
		||||
			Callback: func(host string, remote net.Addr, key gossh.PublicKey) error {
 | 
			
		||||
				found, err := goph.CheckKnownHost(host, remote, key, "")
 | 
			
		||||
				if !found {
 | 
			
		||||
					if err = goph.AddKnownHost(host, remote, key, ""); err != nil {
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
		knownHostHandler, err := goph.DefaultKnownHosts()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
				} else if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
		c.Config.Callback = func(host string, remote net.Addr, key gossh.PublicKey) error {
 | 
			
		||||
			println("hi")
 | 
			
		||||
			err = goph.AddKnownHost(host, remote, key, "")
 | 
			
		||||
				return nil
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
			return knownHostHandler(host, remote, key)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		baseCmd := sess.Command()
 | 
			
		||||
 | 
			
		||||
@@ -165,9 +214,9 @@ func Proxy(route config.Route) router.Handler {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// requestPassword asks the client for the remote server's password
 | 
			
		||||
func requestPassword(opts proxySettings, sess ssh.Session) func() (secret string, err error) {
 | 
			
		||||
func requestPassword(opts proxySettings, sess ssh.Session, addr string) func() (secret string, err error) {
 | 
			
		||||
	return func() (secret string, err error) {
 | 
			
		||||
		_, err = fmt.Fprintf(sess.Stderr(), "Password for %s@%s: ", *opts.User, opts.Server)
 | 
			
		||||
		_, err = fmt.Fprintf(sess.Stderr(), "Password for %s@%s: ", *opts.User, addr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,16 @@ route "srv" {
 | 
			
		||||
    backend = "proxy"
 | 
			
		||||
    match = "srv"
 | 
			
		||||
    settings = {
 | 
			
		||||
        server = "1.2.3.4"
 | 
			
		||||
        host = "1.2.3.4"
 | 
			
		||||
        privkey = "/home/elara/.ssh/id_ed25519"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
route "cluster" {
 | 
			
		||||
    backend = "proxy"
 | 
			
		||||
    match = "cluster\\.(.+)"
 | 
			
		||||
    settings = {
 | 
			
		||||
        hosts = ["node*", "nas", "192.168.1.*"]
 | 
			
		||||
        privkey = "/home/elara/.ssh/id_ed25519"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								seashell.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								seashell.service
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Seashell SSH Server
 | 
			
		||||
After=network.target
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
ExecStart=seashell
 | 
			
		||||
Restart=always
 | 
			
		||||
StandardOutput=journal
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=default.target
 | 
			
		||||
		Reference in New Issue
	
	Block a user