diff --git a/.cirrus.yml b/.cirrus.yml index f655b772..4095a53e 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -5,6 +5,7 @@ task: VERSION: 1.13 VERSION: 1.14 VERSION: 1.15 + VERSION: 1.16 container: image: golang:$VERSION diff --git a/Makefile b/Makefile index 290dc189..bf57add0 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ bashcompdir ?= /etc/bash_completion.d GO ?= go ASCIIDOC_OPTS = -asshproxy_version=$(SSHPROXY_VERSION) -GO_OPTS = $(GO_OPTS_EXTRA) -ldflags "-X main.SshproxyVersion=$(SSHPROXY_VERSION)" +GO_OPTS = $(GO_OPTS_EXTRA) -mod=vendor -ldflags "-X main.SshproxyVersion=$(SSHPROXY_VERSION)" SSHPROXY_SRC = $(wildcard cmd/sshproxy/*.go) SSHPROXY_DUMPD_SRC = $(wildcard cmd/sshproxy-dumpd/*.go) diff --git a/cmd/sshproxy/sshproxy.go b/cmd/sshproxy/sshproxy.go index edd6ebbb..05aecd70 100644 --- a/cmd/sshproxy/sshproxy.go +++ b/cmd/sshproxy/sshproxy.go @@ -93,10 +93,11 @@ func (c *etcdChecker) doCheck(hostport string) utils.State { // findDestination finds a reachable destination for the sshd server according // to the etcd database if available or the routes and route_select algorithm. // It returns a string with the service name, a string with host:port, a string -// containing ForceCommand value and a bool containing CommandMustMatch; a +// containing ForceCommand value, a bool containing CommandMustMatch, an int64 +// with etcd_keyttl and a map of strings with the environment variables; a // string with the service name and an empty string if no destination is found // or an error if any. -func findDestination(cli *utils.Client, username string, routes map[string]*utils.RouteConfig, sshdHostport string, checkInterval utils.Duration) (string, string, string, bool, error) { +func findDestination(cli *utils.Client, username string, routes map[string]*utils.RouteConfig, sshdHostport string, checkInterval utils.Duration) (string, string, string, bool, int64, map[string]string, error) { checker := &etcdChecker{ checkInterval: checkInterval, cli: cli, @@ -104,12 +105,12 @@ func findDestination(cli *utils.Client, username string, routes map[string]*util service, err := findService(routes, sshdHostport) if err != nil { - return "", "", "", false, err + return "", "", "", false, 0, nil, err } key := fmt.Sprintf("%s@%s", username, service) if routes[service].Mode == "sticky" && cli != nil && cli.IsAlive() { - dest, err := cli.GetDestination(key) + dest, err := cli.GetDestination(key, routes[service].EtcdKeyTTL) if err != nil { if err != utils.ErrKeyNotFound { log.Errorf("problem with etcd: %v", err) @@ -118,7 +119,7 @@ func findDestination(cli *utils.Client, username string, routes map[string]*util if utils.IsDestinationInRoutes(dest, routes[service].Dest) { if checker.Check(dest) { log.Debugf("found destination in etcd: %s", dest) - return service, dest, routes[service].ForceCommand, routes[service].CommandMustMatch, nil + return service, dest, routes[service].ForceCommand, routes[service].CommandMustMatch, routes[service].EtcdKeyTTL, routes[service].Environment, nil } log.Infof("cannot connect %s to already existing connection(s) to %s: host %s", key, dest, checker.LastState) } else { @@ -129,10 +130,10 @@ func findDestination(cli *utils.Client, username string, routes map[string]*util if len(routes[service].Dest) > 0 { selected, err := utils.SelectRoute(routes[service].RouteSelect, routes[service].Dest, checker, cli, key) - return service, selected, routes[service].ForceCommand, routes[service].CommandMustMatch, err + return service, selected, routes[service].ForceCommand, routes[service].CommandMustMatch, routes[service].EtcdKeyTTL, routes[service].Environment, err } - return service, "", "", false, fmt.Errorf("no destination set for service %s", service) + return service, "", "", false, 0, nil, fmt.Errorf("no destination set for service %s", service) } // findService finds the first service containing a suitable source in the conf, @@ -326,7 +327,7 @@ func mainExitCode() int { log.Errorf("Cannot contact etcd cluster to update state: %v", err) } - service, hostport, forceCommand, commandMustMatch, err := findDestination(cli, username, config.Routes, sshInfos.Dst(), config.CheckInterval) + service, hostport, forceCommand, commandMustMatch, etcdKeyTTL, environment, err := findDestination(cli, username, config.Routes, sshInfos.Dst(), config.CheckInterval) switch { case err != nil: log.Fatalf("Finding destination: %s", err) @@ -345,6 +346,8 @@ func mainExitCode() int { log.Fatalf("Invalid destination '%s': %s", hostport, err) } + setEnvironment(environment) + // waitgroup and channel to stop our background command when exiting. var wg sync.WaitGroup ctx, cancel := context.WithCancel(context.Background()) @@ -366,7 +369,7 @@ func mainExitCode() int { // Register destination in etcd and keep it alive while running. if cli != nil && cli.IsAlive() { key := fmt.Sprintf("%s@%s", username, service) - keepAliveChan, eP, err := cli.SetDestination(ctx, key, sshInfos.Dst(), hostport) + keepAliveChan, eP, err := cli.SetDestination(ctx, key, sshInfos.Dst(), hostport, etcdKeyTTL) etcdPath = eP if err != nil { log.Warningf("setting destination in etcd: %v", err) diff --git a/config/sshproxy.yaml b/config/sshproxy.yaml index 844ee85f..f7a04b3e 100644 --- a/config/sshproxy.yaml +++ b/config/sshproxy.yaml @@ -129,13 +129,16 @@ # the selection is random. For "bandwidth", it's the same as "connections", but # based on the bandwidth used, with a rollback on connections (which is # frequent for new simultaneous connections). The mode value defines the -# stickiness of a connection. It can be "sticky" or "balanced". If "sticky", -# then all connections of a user will be made on the same destination host. If -# "balanced", the route_select algorithm will be used for every connection. -# Finally, the force_command can be set to override the command asked by the -# user. If command_must_match is set to true, then the connection is closed if -# the original command is not the same as the force_command. command_must_match -# defaults to false. +# stickiness of a connection. It can be "sticky" or "balanced" (defaults to +# sticky). If "sticky", then all connections of a user will be made on the same +# destination host. If "balanced", the route_select algorithm will be used for +# every connection. Finally, the force_command can be set to override the +# command asked by the user. If command_must_match is set to true, then the +# connection is closed if the original command is not the same as the +# force_command. command_must_match defaults to false. etcd_keyttl defauts to +# 0. If a value is set (in seconds), the chosen backen will be remembered for +# this amount of time. Environment variables can be set if needed. The '{user}' +# pattern will be replaced with the user login. #routes: # service1: # source: ["192.168.0.1"] @@ -147,6 +150,9 @@ # mode: balanced # force_command: "internal-sftp" # command_must_match: true +# etcd_keyttl: 3600 +# environment: +# XAUTHORITY: /dev/shm/.Xauthority_{user} # default: # dest: ["host5:4222"] diff --git a/doc/sshproxy.yaml.txt b/doc/sshproxy.yaml.txt index 55408c51..fb9b5f50 100644 --- a/doc/sshproxy.yaml.txt +++ b/doc/sshproxy.yaml.txt @@ -102,11 +102,14 @@ executed by the ssh forked by sshproxy. *translate_commands* is an associative array which keys are strings containing the exact user command. The value is an associative array containing: -*ssh_args*:: - an optional list of options that will be passed to ssh. +An associative array *ssh* specifies the SSH options: + +*exe*:: + path or command to use for the SSH client ('ssh' by default). -*command*:: - a mandatory string, the actual executed command. +*args*:: + a list of arguments for the SSH client. Its default value is: '["-q", + "-Y"]'. *disable_dump*:: false by default. If true, no dumps will be done for this command. @@ -178,6 +181,9 @@ listening IP address of the SSH daemon: mode: balanced force_command: "internal-sftp" command_must_match: true + etcd_keyttl: 3600 + environment: + XAUTHORITY: /dev/shm/.Xauthority_{user} default: dest: ["host5:4222"] @@ -195,28 +201,23 @@ priority, then the hosts with less global connections, and in case of a draw, the selection is random. For 'bandwidth', it's the same as 'connections', but based on the bandwidth used, with a rollback on connections (which is frequent for new simultaneous connections). The mode value defines the -stickiness of a connection. It can be 'sticky' or 'balanced'. If 'sticky', -then all connections of a user will be made on the same destination host. If -'balanced', the route_select algorithm will be used for every connections. -Finally, the force_command can be set to override the command asked by the -user. If command_must_match is set to true, then the connection is closed if -the original command is not the same as the force_command. command_must_match -defaults to false. +stickiness of a connection. It can be 'sticky' or 'balanced' (defaults to +'sticky'). If 'sticky', then all connections of a user will be made on the +same destination host. If 'balanced', the route_select algorithm will be used +for every connections. Finally, the force_command can be set to override the +command asked by the user. If command_must_match is set to true, then the +connection is closed if the original command is not the same as the +force_command. command_must_match defaults to false. etcd_keyttl defauts to 0. +If a value is set (in seconds), the chosen backen will be remembered for this +amount of time. An associative array *environment* can be used to set +environment variables. The pattern '\{user}' will be replaced with the user +login. In the previous example, a client connected to '192.168.0.1' will be proxied to 'host1' and, if the host is not reachable, to 'host2'. If a client does not connect to '192.168.0.1' or '192.168.0.2' it will be proxied to the sshd daemon listening on port 4222 on 'host5'. -An associative array *ssh* specifies the SSH options: - -*exe*:: - path or command to use for the SSH client ('ssh' by default). - -*args*:: - a list of arguments for the SSH client. Its default value is: '["-q", - "-Y"]'. - Each of the previous parameters can be overridden for a group thanks to the *groups* associative array. diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..0e3246f4 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/cea-hpc/sshproxy + +go 1.14 + +require ( + github.com/BurntSushi/toml v1.0.0 // indirect + github.com/coreos/etcd v2.3.8+incompatible // indirect + github.com/docker/docker v20.10.13+incompatible + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/kr/pty v1.1.8 + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/olekukonko/tablewriter v0.0.5 + github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 + go.etcd.io/etcd v2.3.8+incompatible + golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect + golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect + golang.org/x/tools v0.1.10 // indirect + gopkg.in/yaml.v2 v2.4.0 + honnef.co/go/tools v0.2.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..69ec228a --- /dev/null +++ b/go.sum @@ -0,0 +1,105 @@ +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/coreos/etcd v2.3.8+incompatible h1:Lkp5dgqMANTjq0UW74OP1H8yCDQT0In4jrw6xfcNlGE= +github.com/coreos/etcd v2.3.8+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/docker/docker v20.10.13+incompatible h1:5s7uxnKZG+b8hYWlPYUi6x1Sjpq2MSt96d15eLZeHyw= +github.com/docker/docker v20.10.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/etcd v2.3.8+incompatible h1:m5lZwb9yKkh27IFgPQWTdiaG/9waG7AWy0NSHedy3Mk= +go.etcd.io/etcd v2.3.8+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +honnef.co/go/tools v0.2.2 h1:MNh1AVMyVX23VUHE2O27jm6lNj3vjO5DexS4A1xvnzk= +honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= diff --git a/pkg/utils/config.go b/pkg/utils/config.go index e819a156..87d34529 100644 --- a/pkg/utils/config.go +++ b/pkg/utils/config.go @@ -65,6 +65,8 @@ type RouteConfig struct { Mode string ForceCommand string `yaml:"force_command"` CommandMustMatch bool `yaml:"command_must_match"` + EtcdKeyTTL int64 `yaml:"etcd_keyttl"` + Environment map[string]string } type sshConfig struct { @@ -255,6 +257,12 @@ func LoadConfig(filename, currentUsername, sid string, start time.Time, groups m config.Environment[k] = replace(v, patterns["{user}"]) } + for service, opts := range config.Routes { + for k, v := range opts.Environment { + config.Routes[service].Environment[k] = replace(v, patterns["{user}"]) + } + } + // replace sources and destinations (with possible missing port) with host:port. if err := CheckRoutes(config.Routes); err != nil { return nil, fmt.Errorf("invalid value in `routes` option: %s", err) diff --git a/pkg/utils/etcd.go b/pkg/utils/etcd.go index 2cc78914..864803f0 100644 --- a/pkg/utils/etcd.go +++ b/pkg/utils/etcd.go @@ -19,6 +19,7 @@ import ( "net" "regexp" "sort" + "strconv" "strings" "time" @@ -87,6 +88,7 @@ func (s State) MarshalJSON() ([]byte, error) { var ( etcdRootPath = "/sshproxy" etcdConnectionsPath = etcdRootPath + "/connections" + etcdHistoryPath = etcdRootPath + "/history" etcdHostsPath = etcdRootPath + "/hosts" // ErrKeyNotFound is returned when key is not found in etcd. @@ -97,6 +99,10 @@ func toConnectionKey(d string) string { return fmt.Sprintf("%s/%s", etcdConnectionsPath, d) } +func toHistoryKey(d string) string { + return fmt.Sprintf("%s/%s", etcdHistoryPath, d) +} + func toHostKey(h string) string { return fmt.Sprintf("%s/%s", etcdHostsPath, h) } @@ -187,9 +193,10 @@ func (c *Client) Close() { } // GetDestination returns the destination found in etcd for a user connected to -// an SSH daemon (key). If the key is not present the error will be +// an SSH daemon (key). If the key is not present and etcdKeyTTL is defined, +// the key is searched in history. If it's not found, the error will be // etcd.ErrKeyNotFound. -func (c *Client) GetDestination(key string) (string, error) { +func (c *Client) GetDestination(key string, etcdKeyTTL int64) (string, error) { path := toConnectionKey(key) ctx, cancel := context.WithTimeout(context.Background(), c.requestTimeout) resp, err := c.cli.Get(ctx, path, clientv3.WithPrefix(), clientv3.WithKeysOnly(), clientv3.WithSort(clientv3.SortByKey, clientv3.SortDescend)) @@ -199,6 +206,19 @@ func (c *Client) GetDestination(key string) (string, error) { } if len(resp.Kvs) == 0 { + if etcdKeyTTL > 0 { + history := toHistoryKey(key) + ctx, cancel := context.WithTimeout(context.Background(), c.requestTimeout) + resp, err := c.cli.Get(ctx, history, clientv3.WithPrefix()) + cancel() + if err != nil { + return "", err + } + + if len(resp.Kvs) != 0 { + return string(resp.Kvs[0].Value), nil + } + } return "", ErrKeyNotFound } @@ -207,10 +227,42 @@ func (c *Client) GetDestination(key string) (string, error) { return dest[0], nil } +func (c *Client) getExistingLease(key string) (string, error) { + history := toHistoryKey(key) + ctx, cancel := context.WithTimeout(context.Background(), c.requestTimeout) + resp, err := c.cli.Get(ctx, history, clientv3.WithPrefix(), clientv3.WithKeysOnly()) + cancel() + if err != nil { + return "", err + } + + if len(resp.Kvs) == 0 { + return "", ErrKeyNotFound + } + + lease := string(resp.Kvs[0].Key)[len(history)+1:] + return lease, nil +} + // SetDestination set current destination in etcd. -func (c *Client) SetDestination(rootctx context.Context, key, sshdHostport string, dst string) (<-chan *clientv3.LeaseKeepAliveResponse, string, error) { +func (c *Client) SetDestination(rootctx context.Context, key, sshdHostport string, dst string, etcdKeyTTL int64) (<-chan *clientv3.LeaseKeepAliveResponse, string, error) { path := fmt.Sprintf("%s/%s/%s/%s", toConnectionKey(key), dst, sshdHostport, time.Now().Format(time.RFC3339Nano)) ctx, cancel := context.WithTimeout(context.Background(), c.requestTimeout) + var history string + var historyID clientv3.LeaseID + if etcdKeyTTL > 0 { + lease, err := c.getExistingLease(key) + if err == nil { + tmpHistoryID, _ := strconv.Atoi(lease) + historyID = clientv3.LeaseID(tmpHistoryID) + } else { + respHistory, err := c.cli.Grant(ctx, etcdKeyTTL) + if err == nil { + historyID = respHistory.ID + } + } + history = fmt.Sprintf("%s/%d", toHistoryKey(key), int64(historyID)) + } resp, err := c.cli.Grant(ctx, c.keyTTL) cancel() if err != nil { @@ -226,12 +278,18 @@ func (c *Client) SetDestination(rootctx context.Context, key, sshdHostport strin } ctx, cancel = context.WithTimeout(context.Background(), c.requestTimeout) _, err = c.cli.Put(ctx, path, string(bytes), clientv3.WithLease(resp.ID)) + if etcdKeyTTL > 0 { + _, err = c.cli.Put(ctx, history, dst, clientv3.WithLease(historyID)) + } cancel() if err != nil { return nil, "", err } k, e := c.cli.KeepAlive(rootctx, resp.ID) + if etcdKeyTTL > 0 { + c.cli.KeepAlive(rootctx, historyID) + } c.leaseID = resp.ID return k, path, e } diff --git a/test/centos-image/gateway.sh b/test/centos-image/gateway.sh index 22d95be8..3de6b4b9 100755 --- a/test/centos-image/gateway.sh +++ b/test/centos-image/gateway.sh @@ -28,6 +28,10 @@ cat </etc/sshproxy/sshproxy.yaml --- debug: true log: /tmp/sshproxy-{user}.log +environment: + XMODIFIERS: globalEnv_{user} +ssh: + args: ["-q", "-Y", "-o SendEnv=XMODIFIERS"] translate_commands: "/usr/libexec/openssh/sftp-server": @@ -58,12 +62,15 @@ routes: dest: ["server1", "server2"] route_select: ordered mode: sticky + etcd_keyttl: 0 service2: source: ["gateway1:2023"] dest: ["server1"] service3: source: ["gateway1:2024"] dest: ["server2"] + environment: + XMODIFIERS: serviceEnv_{user} sftp: source: ["gateway2:2023"] dest: ["server1"] @@ -81,10 +88,14 @@ groups: users: - unknownuser,user2: + environment: + XMODIFIERS: globalUserEnv_{user} routes: service3: source: ["gateway1:2024"] dest: ["server1"] + environment: + XMODIFIERS: serviceUserEnv_{user} EOF exec "$@" diff --git a/test/centos-image/sshproxy_test.go b/test/centos-image/sshproxy_test.go index a34df6f0..3646787c 100644 --- a/test/centos-image/sshproxy_test.go +++ b/test/centos-image/sshproxy_test.go @@ -237,6 +237,32 @@ func TestSimpleConnect(t *testing.T) { } } +var environmentTests = []struct { + user string + port int + want string +}{ + {"", 2023, "globalEnv_centos"}, + {"", 2024, "serviceEnv_centos"}, + {"user2@", 2023, "globalUserEnv_user2"}, + {"user2@", 2024, "serviceUserEnv_user2"}, +} + +func TestEnvironment(t *testing.T) { + for _, tt := range environmentTests { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + args, cmd := prepareCommand(tt.user+"gateway1", tt.port, "echo $XMODIFIERS") + _, stdout, stderr, err := runCommand(ctx, "ssh", args, nil, nil) + stdoutStr := strings.TrimSpace(string(stdout)) + if err != nil { + t.Errorf("%s unexpected error: %v | stderr = %s", cmd, err, string(stderr)) + } else if stdoutStr != tt.want { + t.Errorf("%s hostname = %s, want %s", cmd, stdoutStr, tt.want) + } + } +} + func TestReturnCode(t *testing.T) { for _, exitCode := range []int{0, 3} { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -348,6 +374,68 @@ func TestStickyConnections(t *testing.T) { } } +func TestNotLongStickyConnections(t *testing.T) { + // remove old connections stored in etcd + time.Sleep(4 * time.Second) + + disableHost("server1") + checkHostState(t, "server1", "disabled") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + args, _ := prepareCommand("gateway1", 2022, "hostname") + _, _, _, err := runCommand(ctx, "ssh", args, nil, nil) + if err != nil { + log.Fatal(err) + } + + time.Sleep(2 * time.Second) + enableHost("server1") + checkHostState(t, "server1", "up") + + args, cmdStr := prepareCommand("gateway2", 2022, "hostname") + _, stdout, _, err := runCommand(ctx, "ssh", args, nil, nil) + if err != nil { + log.Fatal(err) + } + dest := strings.TrimSpace(string(stdout)) + if dest != "server1" { + t.Errorf("%s got %s, expected server1", cmdStr, dest) + } +} + +func TestLongStickyConnections(t *testing.T) { + // remove old connections stored in etcd + time.Sleep(4 * time.Second) + + updateLineSSHProxyConf("etcd_keyttl", "3") + disableHost("server1") + checkHostState(t, "server1", "disabled") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + args, _ := prepareCommand("gateway1", 2022, "hostname") + _, _, _, err := runCommand(ctx, "ssh", args, nil, nil) + if err != nil { + log.Fatal(err) + } + + time.Sleep(2 * time.Second) + enableHost("server1") + checkHostState(t, "server1", "up") + + args, cmdStr := prepareCommand("gateway2", 2022, "hostname") + _, stdout, _, err := runCommand(ctx, "ssh", args, nil, nil) + if err != nil { + log.Fatal(err) + } + updateLineSSHProxyConf("etcd_keyttl", "0") + dest := strings.TrimSpace(string(stdout)) + if dest != "server2" { + t.Errorf("%s got %s, expected server2", cmdStr, dest) + } +} + func TestBalancedConnections(t *testing.T) { // remove old connections stored in etcd time.Sleep(4 * time.Second) diff --git a/test/docker-compose.yaml b/test/docker-compose.yaml index 3d57d007..f02a9665 100644 --- a/test/docker-compose.yaml +++ b/test/docker-compose.yaml @@ -13,7 +13,8 @@ services: - server1 - server2 - server3 - command: ["/usr/bin/go", "test", "-v", "-failfast", "-tags", "docker", "./sshproxy_test.go"] + command: ["/usr/bin/sleep", "1000"] + #command: ["/usr/bin/go", "test", "-v", "-failfast", "-tags", "docker", "./sshproxy_test.go"] gateway1: container_name: gateway1 diff --git a/vendor/modules.txt b/vendor/modules.txt new file mode 100644 index 00000000..9d5dbe07 --- /dev/null +++ b/vendor/modules.txt @@ -0,0 +1,71 @@ +# github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 +github.com/Azure/go-ansiterm +github.com/Azure/go-ansiterm/winterm +# github.com/coreos/etcd v2.3.8+incompatible +## explicit +github.com/coreos/etcd/Godeps/_workspace/src/github.com/gogo/protobuf/proto +github.com/coreos/etcd/Godeps/_workspace/src/github.com/golang/protobuf/proto +github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context +github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/http2 +github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/http2/hpack +github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/internal/timeseries +github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/trace +github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc +github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc/codes +github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc/credentials +github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc/grpclog +github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc/metadata +github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc/naming +github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc/peer +github.com/coreos/etcd/Godeps/_workspace/src/google.golang.org/grpc/transport +github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes +github.com/coreos/etcd/etcdserver/etcdserverpb +github.com/coreos/etcd/storage/storagepb +# github.com/creack/pty v1.1.11 +github.com/creack/pty +# github.com/docker/docker v20.10.13+incompatible +## explicit +github.com/docker/docker/pkg/term +# github.com/gogo/protobuf v1.3.2 +## explicit +# github.com/golang/protobuf v1.5.2 +## explicit +# github.com/kr/pty v1.1.8 +## explicit +github.com/kr/pty +# github.com/mattn/go-runewidth v0.0.9 +github.com/mattn/go-runewidth +# github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 +## explicit +github.com/moby/term +github.com/moby/term/windows +# github.com/olekukonko/tablewriter v0.0.5 +## explicit +github.com/olekukonko/tablewriter +# github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 +## explicit +github.com/op/go-logging +# go.etcd.io/etcd v2.3.8+incompatible +## explicit +go.etcd.io/etcd/clientv3 +go.etcd.io/etcd/pkg/transport +# golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 +## explicit +golang.org/x/sys/internal/unsafeheader +golang.org/x/sys/unix +golang.org/x/sys/windows +# gopkg.in/yaml.v2 v2.4.0 +## explicit +gopkg.in/yaml.v2 +# github.com/BurntSushi/toml v1.0.0 +## explicit +github.com/BurntSushi/toml +# golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 +## explicit +golang.org/x/lint +# golang.org/x/tools v0.1.10 +## explicit +golang.org/x/tools +# honnef.co/go/tools v0.2.2 +## explicit +honnef.co/go/tools