Files
2026-07-23 12:38:13 +03:00

97 lines
3.9 KiB
Go

package keymgr
import (
"fmt"
"strings"
"ssh-access-tui/internal/model"
)
// RenderClientConfig produces an OpenSSH client config (works identically on
// Windows' built-in OpenSSH client, macOS, and Linux). It defines exactly
// one connection, "jump", carrying one LocalForward per allowed service —
// the user opens it once (see INSTRUCTIONS.txt) and every service becomes
// reachable at once on the listed local ports, for as long as that one
// connection stays open.
//
// This file is convenience only, not a security boundary: nothing stops a
// user from editing it or invoking ssh by hand. The actual restriction to
// these destinations is enforced server-side via authorized_keys
// permitopen= (see internal/sysadm/authorizedkeys.go) independent of what
// this file says.
func RenderClientConfig(jumpHost string, jumpPort int, username, keyFileName string, forwards []AllocatedForward) string {
var b strings.Builder
fmt.Fprintf(&b, "# ssh-access-tui client config for %s\n", username)
fmt.Fprintf(&b, "# Generated %s. Regenerated whenever your allowed services change.\n\n", genTimestamp())
fmt.Fprintf(&b, "Host jump\n")
fmt.Fprintf(&b, " HostName %s\n", jumpHost)
fmt.Fprintf(&b, " Port %d\n", jumpPort)
fmt.Fprintf(&b, " User %s\n", username)
fmt.Fprintf(&b, " IdentityFile ~/.ssh/%s\n", keyFileName)
fmt.Fprintf(&b, " IdentitiesOnly yes\n")
for _, f := range forwards {
comment := f.HostName + "/" + f.ServiceName
if f.Description != "" {
comment += ": " + f.Description
}
fmt.Fprintf(&b, " LocalForward %d %s:%d # %s\n", f.LocalPort, f.Address, f.Port, comment)
}
return b.String()
}
// RenderClientConfigProxyJump is the ProxyJump-mode counterpart of
// RenderClientConfig: instead of one "jump" connection carrying a
// LocalForward per service, each allowed service gets its own "Host <alias>"
// block that reaches it by proxying through "jump" (ProxyJump), so the user
// runs "ssh <alias>" directly per service rather than opening one
// always-open tunnel and pointing local tools at 127.0.0.1:<port>.
//
// Same non-boundary caveat as RenderClientConfig: nothing here is enforced —
// the actual restriction to these destinations is authorized_keys
// permitopen= (see internal/sysadm/authorizedkeys.go), unaffected by which
// client config style is handed out.
func RenderClientConfigProxyJump(jumpHost string, jumpPort int, username, keyFileName string, forwards []model.Forward) string {
var b strings.Builder
fmt.Fprintf(&b, "# ssh-access-tui client config for %s\n", username)
fmt.Fprintf(&b, "# Generated %s. Regenerated whenever your allowed services change.\n\n", genTimestamp())
fmt.Fprintf(&b, "Host jump\n")
fmt.Fprintf(&b, " HostName %s\n", jumpHost)
fmt.Fprintf(&b, " Port %d\n", jumpPort)
fmt.Fprintf(&b, " User %s\n", username)
fmt.Fprintf(&b, " IdentityFile ~/.ssh/%s\n", keyFileName)
fmt.Fprintf(&b, " IdentitiesOnly yes\n\n")
aliases := hostAliases(forwards)
for i, f := range forwards {
fmt.Fprintf(&b, "Host %s\n", aliases[i])
fmt.Fprintf(&b, " HostName %s\n", f.Address)
fmt.Fprintf(&b, " Port %d\n", f.Port)
fmt.Fprintf(&b, " User %s\n", username)
fmt.Fprintf(&b, " ProxyJump jump\n")
fmt.Fprintf(&b, " IdentityFile ~/.ssh/%s\n", keyFileName)
fmt.Fprintf(&b, " IdentitiesOnly yes\n\n")
}
return b.String()
}
// hostAliases assigns each forward a unique ssh_config "Host" alias: just
// the host name when it's the only service of that host present, otherwise
// "<host>-<service>" so multiple services on one host don't collide.
func hostAliases(forwards []model.Forward) []string {
countByHost := map[string]int{}
for _, f := range forwards {
countByHost[f.HostName]++
}
out := make([]string, len(forwards))
for i, f := range forwards {
if countByHost[f.HostName] > 1 {
out[i] = f.HostName + "-" + f.ServiceName
} else {
out[i] = f.HostName
}
}
return out
}