diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 20a4927e..64a80b44 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -117,6 +117,20 @@ builds: binary: plugins/failtoban tags: - full + - id: plugin_username_router + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 + main: ./plugin/username-router + binary: plugins/username-router + tags: + - full archives: - formats: ['tar.gz'] @@ -141,6 +155,7 @@ archives: - plugin_failtoban - plugin_docker - plugin_kubernetes + - plugin_username_router dockers: - image_templates: - "farmer1992/sshpiperd:v{{ .Version }}-amd64" @@ -248,6 +263,7 @@ snapcrafts: - plugin_yaml - plugin_fixed - plugin_failtoban + - plugin_username_router name: sshpiperd name_template: "sshpiperd_{{ .Version }}_{{ .Os }}_{{ .Arch }}" summary: The missing reverse proxy for ssh scp diff --git a/README.md b/README.md index d222c59c..88fbf30c 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ Plugin list * [kubernetes](plugin/kubernetes/) 🔀: manage pipes via Kubernetes CRD. * [azdevicecode](https://github.com/tg123/sshpiper-plugins/tree/main/azdevicecode) 🔒: ask user to enter [azure device code](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) before login * [fixed](plugin/fixed/) 🔀: fixed targeting the dummy sshd server + * [username-router](plugin/username-router/) 🔀: route based on username, the username format is `target+username`, where `target` is the target host and `username` is the username to use for that target. * [simplemath](plugin/simplemath/) 🔒: ask for very simple math question before login, demo purpose * [githubapp](https://github.com/tg123/sshpiper-gh) 🔀: login ssh with your github account * [restful](https://github.com/11notes/docker-sshpiper) by [@11notes](https://github.com/11notes) 🔀🔒: The rest plugin for sshpiperd is a simple plugin that allows you to use a restful backend for authentication and challenge. diff --git a/plugin/username-router/README.md b/plugin/username-router/README.md new file mode 100644 index 00000000..5ed2dc3c --- /dev/null +++ b/plugin/username-router/README.md @@ -0,0 +1,11 @@ +# username-router plugin for sshpiper + +Supports routing based on username. This plugin allows you to route connections to different targets based on the username provided during the SSH connection. +The username format is `target+username`, where `target` is the target host and `username` is the username to use for that target. +`target` can be an IP address or a hostname, and it can also include a port number in the format `target:port`. + +## Usage + +``` +sshpiperd username-router +``` \ No newline at end of file diff --git a/plugin/username-router/main.go b/plugin/username-router/main.go new file mode 100644 index 00000000..f3092c50 --- /dev/null +++ b/plugin/username-router/main.go @@ -0,0 +1,59 @@ +//go:build full || e2e + +package main + +import ( + "fmt" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/tg123/sshpiper/libplugin" + "github.com/urfave/cli/v2" +) + +func parseTargetUser(raw string) (target string, username string, err error) { + // Expect format: [target:port]+user + parts := strings.SplitN(raw, "+", 2) + if len(parts) != 2 { + err = fmt.Errorf("invalid format (expected target:port+user)") + return + } + + target = parts[0] + username = parts[1] + return +} + +func main() { + + libplugin.CreateAndRunPluginTemplate(&libplugin.PluginTemplate{ + Name: "username-router", + Usage: "routing based on target inside username, format: 'target:port+realuser@sshpiper-host'", + CreateConfig: func(c *cli.Context) (*libplugin.SshPiperPluginConfig, error) { + + return &libplugin.SshPiperPluginConfig{ + PasswordCallback: func(conn libplugin.ConnMetadata, password []byte) (*libplugin.Upstream, error) { + + address, user, err := parseTargetUser(conn.User()) + if err != nil { + return nil, fmt.Errorf("invalid username format %q: %w", conn.User(), err) + } + + host, port, err := libplugin.SplitHostPortForSSH(address) + if err != nil { + return nil, fmt.Errorf("invalid target address %q: %w", address, err) + } + + log.Info("routing to address ", address, " with user ", user) + return &libplugin.Upstream{ + UserName: user, + Host: host, + Port: int32(port), + IgnoreHostKey: true, + Auth: libplugin.CreatePasswordAuth(password), + }, nil + }, + }, nil + }, + }) +}