From e5205ef81f5eaf64cbfc3b1e16efb3df5d23a485 Mon Sep 17 00:00:00 2001 From: Malte Swart Date: Wed, 8 Nov 2023 23:14:18 +0100 Subject: [PATCH 1/3] ssh: support multiple builtin SSH server listener Optimized for setups that prevent uses of global listeners (like 0.0.0.0) but still want to serve via multiple addresses. This could be listening on both a IPv4 and IPv6 address while another SSH server is used for server administration. Fixes #12418 --- custom/conf/app.example.ini | 2 +- .../administration/config-cheat-sheet.en-us.md | 2 +- modules/graceful/manager.go | 6 ++++++ modules/setting/ssh.go | 2 +- modules/ssh/init.go | 16 +++++++++++----- .../git_helper_for_declarative_test.go | 2 +- 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index a4e777fa12894..c4fa77246e196 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -150,7 +150,7 @@ RUN_USER = ; git ;; SSH username displayed in clone URLs. ;SSH_USER = %(BUILTIN_SSH_SERVER_USER)s ;; -;; The network interface the builtin SSH server should listen on +;; The network interface(s) the builtin SSH server should listen on ;SSH_LISTEN_HOST = ;; ;; Port number to be exposed in clone URL diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 401da352c126f..2b68d4fd101ee 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -332,7 +332,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `SSH_USER`: **%(BUILTIN_SSH_SERVER_USER)s**: SSH username displayed in clone URLs. This is only for people who configure the SSH server themselves; in most cases, you want to leave this blank and modify the `BUILTIN_SSH_SERVER_USER`. - `SSH_DOMAIN`: **%(DOMAIN)s**: Domain name of this server, used for displayed clone URL. - `SSH_PORT`: **22**: SSH port displayed in clone URL. -- `SSH_LISTEN_HOST`: **0.0.0.0**: Listen address for the built-in SSH server. +- `SSH_LISTEN_HOST`: **0.0.0.0**: Listen address(es) for the built-in SSH server; multiple addresses can be separated by comma. - `SSH_LISTEN_PORT`: **%(SSH\_PORT)s**: Port for the built-in SSH server. - `SSH_ROOT_PATH`: **~/.ssh**: Root path of SSH directory. - `SSH_CREATE_AUTHORIZED_KEYS_FILE`: **true**: Gitea will create a authorized_keys file by default when it is not using the internal ssh server. If you intend to use the AuthorizedKeysCommand functionality then you should turn this off. diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go index 068de21076fc7..1e1a8239e8c99 100644 --- a/modules/graceful/manager.go +++ b/modules/graceful/manager.go @@ -272,6 +272,12 @@ func (g *Manager) InformCleanup() { g.createServerWaitGroup.Done() } +// Should we need to create multile listener for one type (e.g. SSH built-in server), +// the number of expected routines needs to be increased accordingly. +func (g *Manager) IncreaseListenerCountBy(extraNumberOfServersToCreate int) { + g.createServerWaitGroup.Add(extraNumberOfServersToCreate) +} + // Done allows the manager to be viewed as a context.Context, it returns a channel that is closed when the server is finished terminating func (g *Manager) Done() <-chan struct{} { return g.managerCtx.Done() diff --git a/modules/setting/ssh.go b/modules/setting/ssh.go index ea387e521fad5..9cb5810b37f5b 100644 --- a/modules/setting/ssh.go +++ b/modules/setting/ssh.go @@ -25,7 +25,7 @@ var SSH = struct { Domain string `ini:"SSH_DOMAIN"` Port int `ini:"SSH_PORT"` User string `ini:"SSH_USER"` - ListenHost string `ini:"SSH_LISTEN_HOST"` + ListenHost []string `ini:"SSH_LISTEN_HOST"` ListenPort int `ini:"SSH_LISTEN_PORT"` RootPath string `ini:"SSH_ROOT_PATH"` ServerCiphers []string `ini:"SSH_SERVER_CIPHERS"` diff --git a/modules/ssh/init.go b/modules/ssh/init.go index 21d4f8993611f..ee9367664dc2d 100644 --- a/modules/ssh/init.go +++ b/modules/ssh/init.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -22,11 +23,16 @@ func Init() error { } if setting.SSH.StartBuiltinServer { - Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs) - log.Info("SSH server started on %s. Cipher list (%v), key exchange algorithms (%v), MACs (%v)", - net.JoinHostPort(setting.SSH.ListenHost, strconv.Itoa(setting.SSH.ListenPort)), - setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs, - ) + if len(setting.SSH.ListenHost) > 1 { + graceful.GetManager().IncreaseListenerCountBy(len(setting.SSH.ListenHost) - 1) + } + for _, listenHost := range setting.SSH.ListenHost { + Listen(listenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs) + log.Info("SSH server started on %s. Cipher list (%v), key exchange algorithms (%v), MACs (%v)", + net.JoinHostPort(listenHost, strconv.Itoa(setting.SSH.ListenPort)), + setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs, + ) + } return nil } diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go index 10cf79b9fd8b2..7e9cf3e8fe550 100644 --- a/tests/integration/git_helper_for_declarative_test.go +++ b/tests/integration/git_helper_for_declarative_test.go @@ -52,7 +52,7 @@ func createSSHUrl(gitPath string, u *url.URL) *url.URL { u2 := *u u2.Scheme = "ssh" u2.User = url.User("git") - u2.Host = net.JoinHostPort(setting.SSH.ListenHost, strconv.Itoa(setting.SSH.ListenPort)) + u2.Host = net.JoinHostPort(setting.SSH.ListenHost[0], strconv.Itoa(setting.SSH.ListenPort)) u2.Path = gitPath return &u2 } From 056ad0c75ae251db53f0f8f003b25db9322de5d5 Mon Sep 17 00:00:00 2001 From: Malte Swart Date: Fri, 10 Nov 2023 22:45:19 +0100 Subject: [PATCH 2/3] ssh: restore default value for SSH_LISTEN_HOST The documented default value was previously not encoded by just was the system would do on an empty string. --- modules/setting/ssh.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/setting/ssh.go b/modules/setting/ssh.go index 9cb5810b37f5b..79f3e54128a75 100644 --- a/modules/setting/ssh.go +++ b/modules/setting/ssh.go @@ -54,6 +54,7 @@ var SSH = struct { Disabled: false, StartBuiltinServer: false, Domain: "", + ListenHost: []string{"0.0.0.0"}, Port: 22, ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"}, ServerKeyExchanges: []string{"curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"}, From 4c75e2b43a0c7526c3190e7b925bfdc26a03bd48 Mon Sep 17 00:00:00 2001 From: Malte Swart Date: Fri, 10 Nov 2023 22:36:07 +0100 Subject: [PATCH 3/3] ssh: allow overriding SSH_LISTEN_PORT in SSH_LISTEN_HOST Support listening on multiple address with different ports by overridding the default SSH_LISTEN_ADDRESS like `0.0.0.0,127.0.0.1:2022,[::1]:222,::` --- custom/conf/app.example.ini | 3 ++- .../administration/config-cheat-sheet.en-us.md | 2 +- modules/ssh/init.go | 11 ++++++++--- modules/ssh/ssh.go | 7 +++---- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index c4fa77246e196..d2fbcedf9fb59 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -150,7 +150,8 @@ RUN_USER = ; git ;; SSH username displayed in clone URLs. ;SSH_USER = %(BUILTIN_SSH_SERVER_USER)s ;; -;; The network interface(s) the builtin SSH server should listen on +;; The network interface(s) the builtin SSH server should listen on. +;; Individual addresses can include a port statement to override SSH_LISTEN_PORT value, like 127.0.0.1:2022,0.0.0.0 ;SSH_LISTEN_HOST = ;; ;; Port number to be exposed in clone URL diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 2b68d4fd101ee..e3fbe3934220b 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -332,7 +332,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `SSH_USER`: **%(BUILTIN_SSH_SERVER_USER)s**: SSH username displayed in clone URLs. This is only for people who configure the SSH server themselves; in most cases, you want to leave this blank and modify the `BUILTIN_SSH_SERVER_USER`. - `SSH_DOMAIN`: **%(DOMAIN)s**: Domain name of this server, used for displayed clone URL. - `SSH_PORT`: **22**: SSH port displayed in clone URL. -- `SSH_LISTEN_HOST`: **0.0.0.0**: Listen address(es) for the built-in SSH server; multiple addresses can be separated by comma. +- `SSH_LISTEN_HOST`: **0.0.0.0**: Listen address(es) for the built-in SSH server; multiple addresses can be separated by comma, addresses can include a port statement to override SSH_LISTEN_PORT. - `SSH_LISTEN_PORT`: **%(SSH\_PORT)s**: Port for the built-in SSH server. - `SSH_ROOT_PATH`: **~/.ssh**: Root path of SSH directory. - `SSH_CREATE_AUTHORIZED_KEYS_FILE`: **true**: Gitea will create a authorized_keys file by default when it is not using the internal ssh server. If you intend to use the AuthorizedKeysCommand functionality then you should turn this off. diff --git a/modules/ssh/init.go b/modules/ssh/init.go index ee9367664dc2d..a9132c1a129a0 100644 --- a/modules/ssh/init.go +++ b/modules/ssh/init.go @@ -27,10 +27,15 @@ func Init() error { graceful.GetManager().IncreaseListenerCountBy(len(setting.SSH.ListenHost) - 1) } for _, listenHost := range setting.SSH.ListenHost { - Listen(listenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs) + var addr string + if _, _, err := net.SplitHostPort(listenHost); err == nil { + addr = listenHost + } else { + addr = net.JoinHostPort(listenHost, strconv.Itoa(setting.SSH.ListenPort)) + } + Listen(addr, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs) log.Info("SSH server started on %s. Cipher list (%v), key exchange algorithms (%v), MACs (%v)", - net.JoinHostPort(listenHost, strconv.Itoa(setting.SSH.ListenPort)), - setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs, + addr, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs, ) } return nil diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index f8e4f569b87f4..d958c58b04bba 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -17,7 +17,6 @@ import ( "os" "os/exec" "path/filepath" - "strconv" "strings" "sync" "syscall" @@ -280,10 +279,10 @@ func sshConnectionFailed(conn net.Conn, err error) { log.Warn("Failed authentication attempt from %s", conn.RemoteAddr()) } -// Listen starts a SSH server listens on given port. -func Listen(host string, port int, ciphers, keyExchanges, macs []string) { +// Listen starts a SSH server listens on given addr (host, port combination). +func Listen(addr string, ciphers, keyExchanges, macs []string) { srv := ssh.Server{ - Addr: net.JoinHostPort(host, strconv.Itoa(port)), + Addr: addr, PublicKeyHandler: publicKeyHandler, Handler: sessionHandler, ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {