Skip to content

Commit f156ac9

Browse files
committed
Add an option to force the command executed by sshproxy.
Add an option to close the connection when original command does not match de force command. Add an option to translate the command to something else.
1 parent 9278896 commit f156ac9

File tree

8 files changed

+192
-44
lines changed

8 files changed

+192
-44
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SSHPROXY_VERSION ?= 1.4.0
1+
SSHPROXY_VERSION ?= 1.5.0
22
SSHPROXY_GIT_URL ?= github.com/cea-hpc/sshproxy
33

44
prefix ?= /usr

cmd/sshproxy/sshproxy.go

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -92,18 +92,19 @@ func (c *etcdChecker) doCheck(hostport string) utils.State {
9292

9393
// findDestination finds a reachable destination for the sshd server according
9494
// to the etcd database if available or the routes and route_select algorithm.
95-
// It returns a string with the service name and a string with host:port, a
95+
// It returns a string with the service name, a string with host:port, a string
96+
// containing ForceCommand value and a bool containing CommandMustMatch; a
9697
// string with the service name and an empty string if no destination is found
9798
// or an error if any.
98-
func findDestination(cli *utils.Client, username string, routes map[string]*utils.RouteConfig, sshdHostport string, checkInterval utils.Duration) (string, string, error) {
99+
func findDestination(cli *utils.Client, username string, routes map[string]*utils.RouteConfig, sshdHostport string, checkInterval utils.Duration) (string, string, string, bool, error) {
99100
checker := &etcdChecker{
100101
checkInterval: checkInterval,
101102
cli: cli,
102103
}
103104

104105
service, err := findService(routes, sshdHostport)
105106
if err != nil {
106-
return "", "", err
107+
return "", "", "", false, err
107108
}
108109
key := fmt.Sprintf("%s@%s", username, service)
109110

@@ -117,7 +118,7 @@ func findDestination(cli *utils.Client, username string, routes map[string]*util
117118
if utils.IsDestinationInRoutes(dest, routes[service].Dest) {
118119
if checker.Check(dest) {
119120
log.Debugf("found destination in etcd: %s", dest)
120-
return service, dest, nil
121+
return service, dest, routes[service].ForceCommand, routes[service].CommandMustMatch, nil
121122
}
122123
log.Infof("cannot connect %s to already existing connection(s) to %s: host %s", key, dest, checker.LastState)
123124
} else {
@@ -128,10 +129,10 @@ func findDestination(cli *utils.Client, username string, routes map[string]*util
128129

129130
if len(routes[service].Dest) > 0 {
130131
selected, err := utils.SelectRoute(routes[service].RouteSelect, routes[service].Dest, checker, cli, key)
131-
return service, selected, err
132+
return service, selected, routes[service].ForceCommand, routes[service].CommandMustMatch, err
132133
}
133134

134-
return service, "", fmt.Errorf("no destination set for service %s", service)
135+
return service, "", "", false, fmt.Errorf("no destination set for service %s", service)
135136
}
136137

137138
// findService finds the first service containing a suitable source in the conf,
@@ -309,6 +310,7 @@ func mainExitCode() int {
309310
log.Debugf("config.log_stats_interval = %s", config.LogStatsInterval.Duration())
310311
log.Debugf("config.etcd = %+v", config.Etcd)
311312
log.Debugf("config.bg_command = %s", config.BgCommand)
313+
log.Debugf("config.translate_commands = %v", config.TranslateCommands)
312314
log.Debugf("config.environment = %v", config.Environment)
313315
log.Debugf("config.routes = %v", config.Routes)
314316
log.Debugf("config.ssh.exe = %s", config.SSH.Exe)
@@ -324,7 +326,7 @@ func mainExitCode() int {
324326
log.Errorf("Cannot contact etcd cluster to update state: %v", err)
325327
}
326328

327-
service, hostport, err := findDestination(cli, username, config.Routes, sshInfos.Dst(), config.CheckInterval)
329+
service, hostport, forceCommand, commandMustMatch, err := findDestination(cli, username, config.Routes, sshInfos.Dst(), config.CheckInterval)
328330
switch {
329331
case err != nil:
330332
log.Fatalf("Finding destination: %s", err)
@@ -442,24 +444,39 @@ func mainExitCode() int {
442444
if port != utils.DefaultSSHPort {
443445
sshArgs = append(sshArgs, "-p", port)
444446
}
445-
if originalCmd != "" {
446-
if strings.Contains(originalCmd, "sftp-server") {
447-
// Ask for sftp subsystem on destination (arguments are
448-
// the same used by sftp client command).
449-
sshArgs = append(sshArgs, "-oForwardX11=no",
450-
"-oForwardAgent=no", "-oPermitLocalCommand=no",
451-
"-oClearAllForwardings=yes", "-oProtocol=2",
452-
"-s", "--", host, "sftp")
453-
if config.Dump != "" {
454-
// We don't want to dump sftp connections
455-
config.Dump = "etcd"
447+
doCmd := ""
448+
if forceCommand != "" {
449+
log.Debugf("forceCommand = %s", forceCommand)
450+
doCmd = forceCommand
451+
} else if originalCmd != "" {
452+
doCmd = originalCmd
453+
}
454+
commandTranslated := false
455+
if doCmd != "" {
456+
if commandMustMatch && originalCmd != doCmd {
457+
log.Errorf("error executing proxied ssh command: originalCmd \"%s\" does not match forceCommand \"%s\"", originalCmd, forceCommand)
458+
return 1
459+
}
460+
for fromCmd, translateCmdConf := range config.TranslateCommands {
461+
if doCmd == fromCmd {
462+
log.Debugf("translateCmdConf = %+v", translateCmdConf)
463+
for _, sshArg := range translateCmdConf.SSHArgs {
464+
sshArgs = append(sshArgs, sshArg)
465+
}
466+
sshArgs = append(sshArgs, "--", host, translateCmdConf.Command)
467+
if config.Dump != "" && translateCmdConf.DisableDump {
468+
config.Dump = "etcd"
469+
}
470+
commandTranslated = true
471+
break
456472
}
457-
} else {
473+
}
474+
if !commandTranslated {
458475
if interactiveCommand {
459476
// Force TTY allocation because the user probably asked for it.
460477
sshArgs = append(sshArgs, "-t")
461478
}
462-
sshArgs = append(sshArgs, host, originalCmd)
479+
sshArgs = append(sshArgs, host, doCmd)
463480
}
464481
} else {
465482
sshArgs = append(sshArgs, host)
@@ -469,7 +486,7 @@ func mainExitCode() int {
469486

470487
var recorder *Recorder
471488
if config.Dump != "" {
472-
recorder = NewRecorder(conninfo, config.Dump, originalCmd, config.EtcdStatsInterval.Duration(), config.LogStatsInterval.Duration(), config.DumpLimitSize, config.DumpLimitWindow.Duration())
489+
recorder = NewRecorder(conninfo, config.Dump, doCmd, config.EtcdStatsInterval.Duration(), config.LogStatsInterval.Duration(), config.DumpLimitSize, config.DumpLimitWindow.Duration())
473490

474491
wg.Add(1)
475492
go func() {

config/sshproxy.yaml

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,24 @@
6161
# set.
6262
#etcd_stats_interval: "0"
6363

64+
# Commands can be translated between what is received by sshproxy and what is
65+
# executed by the ssh forked by sshproxy. The keys are strings containing the
66+
# exact user command. ssh_args contains an optional list of options that will
67+
# be passed to ssh. command is a mandatory string, the actual executed command.
68+
# disable_dump is false by default. If true, no dumps will be done for this
69+
# command.
70+
#translate_commands:
71+
# "internal-sftp":
72+
# ssh_args:
73+
# - "-oForwardX11=no"
74+
# - "-oForwardAgent=no"
75+
# - "-oPermitLocalCommand=no"
76+
# - "-oClearAllForwardings=yes"
77+
# - "-oProtocol=2"
78+
# - "-s"
79+
# command: "sftp"
80+
# disable_dump: true
81+
6482
# A command can be launched in the background for the session duration.
6583
# The standard and error outputs are only logged in debug mode.
6684
#bg_command: ""
@@ -110,11 +128,14 @@
110128
# priority, then the hosts with less global connections, and in case of a draw,
111129
# the selection is random. For "bandwidth", it's the same as "connections", but
112130
# based on the bandwidth used, with a rollback on connections (which is
113-
# frequent for new simultaneous connections). Finally, the mode value defines
114-
# the stickiness of a connection. It can be "sticky" or "balanced". If
115-
# "sticky", then all connections of a user will be made on the same destination
116-
# host. If "balanced", the route_select algorithm will be used for every
117-
# connection.
131+
# frequent for new simultaneous connections). The mode value defines the
132+
# stickiness of a connection. It can be "sticky" or "balanced". If "sticky",
133+
# then all connections of a user will be made on the same destination host. If
134+
# "balanced", the route_select algorithm will be used for every connection.
135+
# Finally, the force_command can be set to override the command asked by the
136+
# user. If command_must_match is set to true, then the connection is closed if
137+
# the original command is not the same as the force_command. command_must_match
138+
# defaults to false.
118139
#routes:
119140
# service1:
120141
# source: ["192.168.0.1"]
@@ -124,6 +145,8 @@
124145
# dest: [host3, host4]
125146
# route_select: bandwidth
126147
# mode: balanced
148+
# force_command: "internal-sftp"
149+
# command_must_match: true
127150
# default:
128151
# dest: ["host5:4222"]
129152

doc/sshproxy.yaml.txt

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,34 @@ It can also be a network address where to send dumps if specified as
9797
suffix such as 'h', 'm' and 's' (e.g. '2m30s'). These statistics are
9898
only available when the 'dump' option is set.
9999

100+
Commands can be translated between what is received by sshproxy and what is
101+
executed by the ssh forked by sshproxy. *translate_commands* is an associative
102+
array which keys are strings containing the exact user command. The value is
103+
an associative array containing:
104+
105+
*ssh_args*::
106+
an optional list of options that will be passed to ssh.
107+
108+
*command*::
109+
a mandatory string, the actual executed command.
110+
111+
*disable_dump*::
112+
false by default. If true, no dumps will be done for this command.
113+
114+
For example, we can have the following:
115+
116+
translate_commands:
117+
"internal-sftp":
118+
ssh_args:
119+
- "-oForwardX11=no"
120+
- "-oForwardAgent=no"
121+
- "-oPermitLocalCommand=no"
122+
- "-oClearAllForwardings=yes"
123+
- "-oProtocol=2"
124+
- "-s"
125+
command: "sftp"
126+
disable_dump: true
127+
100128
etcd configuration is provided in an associative array *etcd* whose keys are:
101129

102130
*endpoints*::
@@ -148,6 +176,8 @@ listening IP address of the SSH daemon:
148176
dest: [host3, host4]
149177
route_select: bandwidth
150178
mode: balanced
179+
force_command: "internal-sftp"
180+
command_must_match: true
151181
default:
152182
dest: ["host5:4222"]
153183

@@ -164,11 +194,14 @@ If 'connections', the hosts with less connections from the user have
164194
priority, then the hosts with less global connections, and in case of a draw,
165195
the selection is random. For 'bandwidth', it's the same as 'connections', but
166196
based on the bandwidth used, with a rollback on connections (which is
167-
frequent for new simultaneous connections). Finally, the mode value defines
168-
the stickiness of a connection. It can be 'sticky' or 'balanced'. If
169-
'sticky', then all connections of a user will be made on the same destination
170-
host. If 'balanced', the route_select algorithm will be used for every
171-
connections.
197+
frequent for new simultaneous connections). The mode value defines the
198+
stickiness of a connection. It can be 'sticky' or 'balanced'. If 'sticky',
199+
then all connections of a user will be made on the same destination host. If
200+
'balanced', the route_select algorithm will be used for every connections.
201+
Finally, the force_command can be set to override the command asked by the
202+
user. If command_must_match is set to true, then the connection is closed if
203+
the original command is not the same as the force_command. command_must_match
204+
defaults to false.
172205

173206
In the previous example, a client connected to '192.168.0.1' will be proxied
174207
to 'host1' and, if the host is not reachable, to 'host2'. If a client does not

misc/sshproxy.spec

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
%global debug_package %{nil}
44

55
Name: sshproxy
6-
Version: 1.4.0
6+
Version: 1.5.0
77
Release: 1%{?dist}
88
Summary: SSH proxy
99
License: CeCILL-B
@@ -51,6 +51,9 @@ install -p -m 0644 config/sshproxy.yaml %{buildroot}%{_sysconfdir}/sshproxy
5151
%{_mandir}/man8/sshproxy-replay.8*
5252

5353
%changelog
54+
* Tue Oct 26 2021 Cyril Servant <[email protected]> - 1.5.0-1
55+
- sshproxy 1.5.0
56+
5457
* Mon Aug 16 2021 Cyril Servant <[email protected]> - 1.4.0-1
5558
- sshproxy 1.4.0
5659

pkg/utils/config.go

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ var (
2929
type Config struct {
3030
Debug bool
3131
Log string
32-
CheckInterval Duration `yaml:"check_interval"` // Minimum interval between host checks
32+
CheckInterval Duration `yaml:"check_interval"`
3333
ErrorBanner string `yaml:"error_banner"`
3434
Dump string
3535
DumpLimitSize uint64 `yaml:"dump_limit_size"`
@@ -39,20 +39,32 @@ type Config struct {
3939
LogStatsInterval Duration `yaml:"log_stats_interval"`
4040
BgCommand string `yaml:"bg_command"`
4141
SSH sshConfig
42+
TranslateCommands map[string]*TranslateCommandConfig `yaml:"translate_commands"`
4243
Environment map[string]string
4344
Routes map[string]*RouteConfig
4445
Users []map[string]subConfig
4546
Groups []map[string]subConfig
4647
}
4748

49+
// TranslateCommandConfig represents the configuration of a translate_command.
50+
// SSHArgs is optional. Command is mandatory. DisableDump defaults to false
51+
type TranslateCommandConfig struct {
52+
SSHArgs []string `yaml:"ssh_args"`
53+
Command string
54+
DisableDump bool `yaml:"disable_dump"`
55+
}
56+
4857
// RouteConfig represents the configuration of a route. Dest is mandatory,
4958
// Source is mandatory if the associated service name is not the default one.
50-
// RouteSelect defaults to "ordered", Mode defaults to "stiky".
59+
// RouteSelect defaults to "ordered", Mode defaults to "stiky", ForceCommand is
60+
// optional, CommandMustMatch defaults to false
5161
type RouteConfig struct {
52-
Source []string
53-
Dest []string
54-
RouteSelect string `yaml:"route_select"`
55-
Mode string
62+
Source []string
63+
Dest []string
64+
RouteSelect string `yaml:"route_select"`
65+
Mode string
66+
ForceCommand string `yaml:"force_command"`
67+
CommandMustMatch bool `yaml:"command_must_match"`
5668
}
5769

5870
type sshConfig struct {
@@ -81,11 +93,12 @@ type subConfig struct {
8193
Log interface{}
8294
ErrorBanner interface{} `yaml:"error_banner"`
8395
Dump interface{}
84-
DumpLimitSize interface{} `yaml:"dump_limit_size"`
85-
DumpLimitWindow interface{} `yaml:"dump_limit_window"`
86-
EtcdStatsInterval interface{} `yaml:"etcd_stats_interval"`
87-
LogStatsInterval interface{} `yaml:"log_stats_interval"`
88-
BgCommand interface{} `yaml:"bg_command"`
96+
DumpLimitSize interface{} `yaml:"dump_limit_size"`
97+
DumpLimitWindow interface{} `yaml:"dump_limit_window"`
98+
EtcdStatsInterval interface{} `yaml:"etcd_stats_interval"`
99+
LogStatsInterval interface{} `yaml:"log_stats_interval"`
100+
BgCommand interface{} `yaml:"bg_command"`
101+
TranslateCommands map[string]*TranslateCommandConfig `yaml:"translate_commands"`
89102
Environment map[string]string
90103
Routes map[string]*RouteConfig
91104
SSH sshConfig
@@ -153,6 +166,11 @@ func parseSubConfig(config *Config, subconfig *subConfig) error {
153166
config.Routes[service] = opts
154167
}
155168

169+
// merge translate_commands
170+
for k, v := range subconfig.TranslateCommands {
171+
config.TranslateCommands[k] = v
172+
}
173+
156174
// merge environment
157175
for k, v := range subconfig.Environment {
158176
config.Environment[k] = v

test/centos-image/gateway.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ cat <<EOF >/etc/sshproxy/sshproxy.yaml
2929
debug: true
3030
log: /tmp/sshproxy-{user}.log
3131
32+
translate_commands:
33+
"/usr/libexec/openssh/sftp-server":
34+
ssh_args:
35+
- "-oForwardX11=no"
36+
- "-oForwardAgent=no"
37+
- "-oPermitLocalCommand=no"
38+
- "-oClearAllForwardings=yes"
39+
- "-oProtocol=2"
40+
- "-s"
41+
command: "sftp"
42+
disable_dump: true
43+
3244
etcd:
3345
endpoints:
3446
- "https://etcd:2379"
@@ -52,6 +64,11 @@ routes:
5264
service3:
5365
source: ["gateway1:2024"]
5466
dest: ["server2"]
67+
sftp:
68+
source: ["gateway2:2023"]
69+
dest: ["server1"]
70+
force_command: "/usr/libexec/openssh/sftp-server"
71+
command_must_match: true
5572
default:
5673
dest: ["server3"]
5774

0 commit comments

Comments
 (0)