Skip to content

Commit

Permalink
Merge pull request #114 from davrodpin/tunnel/remote
Browse files Browse the repository at this point in the history
 Add support for ssh remote port forwarding
  • Loading branch information
davrodpin committed Aug 9, 2020
2 parents b3396a3 + b4239ad commit 92f82af
Show file tree
Hide file tree
Showing 18 changed files with 492 additions and 243 deletions.
1 change: 1 addition & 0 deletions Makefile
Expand Up @@ -48,6 +48,7 @@ mole-http: rm-mole-http
--detach \
--network mole \
--ip 192.168.33.11 \
--publish 8080:8080 \
--name mole_http mole_http:latest

rm-mole-ssh:
Expand Down
6 changes: 4 additions & 2 deletions alias/alias.go
Expand Up @@ -34,6 +34,7 @@ const (
// TunnelFlags is a struct that holds all flags required to establish a ssh
// port forwarding tunnel.
type TunnelFlags struct {
TunnelType string
Verbose bool
Insecure bool
Detach bool
Expand All @@ -49,10 +50,10 @@ type TunnelFlags struct {
}

// ParseAlias translates a TunnelFlags object to an Alias object
func (tf TunnelFlags) ParseAlias(name, tunnelType string) *Alias {
func (tf TunnelFlags) ParseAlias(name string) *Alias {
return &Alias{
Name: name,
TunnelType: tunnelType,
TunnelType: tf.TunnelType,
Verbose: tf.Verbose,
Insecure: tf.Insecure,
Detach: tf.Detach,
Expand Down Expand Up @@ -109,6 +110,7 @@ func (a Alias) ParseTunnelFlags() (*TunnelFlags, error) {

tf := &TunnelFlags{}

tf.TunnelType = a.TunnelType
tf.Verbose = a.Verbose
tf.Insecure = a.Insecure
tf.Detach = a.Detach
Expand Down
32 changes: 20 additions & 12 deletions alias/alias_test.go
Expand Up @@ -17,6 +17,7 @@ import (
func TestParseTunnelFlags(t *testing.T) {

tests := []struct {
tunnelType string
verbose bool
insecure bool
detach bool
Expand All @@ -31,6 +32,7 @@ func TestParseTunnelFlags(t *testing.T) {
timeout string
}{
{
"local",
true,
true,
true,
Expand All @@ -45,6 +47,7 @@ func TestParseTunnelFlags(t *testing.T) {
"1m0s",
},
{
"local",
true,
false,
true,
Expand All @@ -62,6 +65,7 @@ func TestParseTunnelFlags(t *testing.T) {

for id, test := range tests {
ai := &alias.Alias{
TunnelType: test.tunnelType,
Verbose: test.verbose,
Insecure: test.insecure,
Detach: test.detach,
Expand All @@ -81,58 +85,62 @@ func TestParseTunnelFlags(t *testing.T) {
t.Errorf("%v\n", err)
}

if test.tunnelType != tf.TunnelType {
t.Errorf("tunnelType doesn't match on test %d: expected: %s, value: %s", id, test.tunnelType, tf.TunnelType)
}

if test.verbose != tf.Verbose {
t.Errorf("verbose doesn't match for test %d: expected: %t, value: %t", id, test.verbose, tf.Verbose)
t.Errorf("verbose doesn't match on test %d: expected: %t, value: %t", id, test.verbose, tf.Verbose)
}

if test.insecure != tf.Insecure {
t.Errorf("insecure doesn't match for test %d: expected: %t, value: %t", id, test.insecure, tf.Insecure)
t.Errorf("insecure doesn't match on test %d: expected: %t, value: %t", id, test.insecure, tf.Insecure)
}

if test.detach != tf.Detach {
t.Errorf("detach doesn't match for test %d: expected: %t, value: %t", id, test.detach, tf.Detach)
t.Errorf("detach doesn't match on test %d: expected: %t, value: %t", id, test.detach, tf.Detach)
}

for i, tsrc := range test.source {
src := tf.Source[i].String()
if tsrc != src {
t.Errorf("source %d doesn't match for test %d: expected: %s, value: %s", id, i, tsrc, src)
t.Errorf("source %d doesn't match on test %d: expected: %s, value: %s", id, i, tsrc, src)
}
}

for i, tdst := range test.destination {
dst := tf.Destination[i].String()
if tdst != dst {
t.Errorf("destination %d doesn't match for test %d: expected: %s, value: %s", id, i, tdst, dst)
t.Errorf("destination %d doesn't match on test %d: expected: %s, value: %s", id, i, tdst, dst)
}
}

if test.server != tf.Server.String() {
t.Errorf("server doesn't match for test %d: expected: %s, value: %s", id, test.server, tf.Server.String())
t.Errorf("server doesn't match on test %d: expected: %s, value: %s", id, test.server, tf.Server.String())
}

if test.key != tf.Key {
t.Errorf("key doesn't match for test %d: expected: %s, value: %s", id, test.key, tf.Key)
t.Errorf("key doesn't match on test %d: expected: %s, value: %s", id, test.key, tf.Key)
}

if test.keepAliveInterval != tf.KeepAliveInterval.String() {
t.Errorf("keepAliveInterval doesn't match for test %d: expected: %s, value: %s", id, test.keepAliveInterval, tf.KeepAliveInterval.String())
t.Errorf("keepAliveInterval doesn't match on test %d: expected: %s, value: %s", id, test.keepAliveInterval, tf.KeepAliveInterval.String())
}

if test.connectionRetries != tf.ConnectionRetries {
t.Errorf("connectionRetries doesn't match for test %d: expected: %d, value: %d", id, test.connectionRetries, tf.ConnectionRetries)
t.Errorf("connectionRetries doesn't match on test %d: expected: %d, value: %d", id, test.connectionRetries, tf.ConnectionRetries)
}

if test.waitAndRetry != tf.WaitAndRetry.String() {
t.Errorf("waitAndRetry doesn't match for test %d: expected: %s, value: %s", id, test.waitAndRetry, tf.WaitAndRetry.String())
t.Errorf("waitAndRetry doesn't match on test %d: expected: %s, value: %s", id, test.waitAndRetry, tf.WaitAndRetry.String())
}

if test.sshAgent != tf.SshAgent {
t.Errorf("sshAgent doesn't match for test %d: expected: %s, value: %s", id, test.sshAgent, tf.SshAgent)
t.Errorf("sshAgent doesn't match on test %d: expected: %s, value: %s", id, test.sshAgent, tf.SshAgent)
}

if test.timeout != tf.Timeout.String() {
t.Errorf("timeout doesn't match for test %d: expected: %s, value: %s", id, test.timeout, tf.Timeout.String())
t.Errorf("timeout doesn't match on test %d: expected: %s, value: %s", id, test.timeout, tf.Timeout.String())
}

}
Expand Down
4 changes: 2 additions & 2 deletions cmd/add_alias.go
Expand Up @@ -26,13 +26,13 @@ The alias configuration file is saved to ".mole", under your home directory.
return errors.New("alias name not provided")
}

tunnelType = args[0]
tunnelFlags.TunnelType = args[0]
aliasName = args[1]

return nil
},
Run: func(cmd *cobra.Command, arg []string) {
if err := alias.Add(tunnelFlags.ParseAlias(aliasName, "local")); err != nil {
if err := alias.Add(tunnelFlags.ParseAlias(aliasName)); err != nil {
log.WithError(err).Error("failed to add tunnel alias")
os.Exit(1)
}
Expand Down
21 changes: 6 additions & 15 deletions cmd/root.go
Expand Up @@ -18,7 +18,6 @@ import (
)

var (
tunnelType string
aliasName string
tunnelFlags = &alias.TunnelFlags{}

Expand Down Expand Up @@ -114,31 +113,23 @@ func start(alias string, tunnelFlags *alias.TunnelFlags) {

log.Debugf("server: %s", s)

// TODO: rename local to source
local := make([]string, len(tunnelFlags.Source))
source := make([]string, len(tunnelFlags.Source))
for i, r := range tunnelFlags.Source {
local[i] = r.String()
source[i] = r.String()
}

// TODO: rename remote to destination
remote := make([]string, len(tunnelFlags.Destination))
destination := make([]string, len(tunnelFlags.Destination))
for i, r := range tunnelFlags.Destination {
if r.Port == "" {
err := fmt.Errorf("missing port in remote address: %s", r.String())
err := fmt.Errorf("missing port in destination address: %s", r.String())
log.Error(err)
os.Exit(1)
}

remote[i] = r.String()
destination[i] = r.String()
}

channels, err := tunnel.BuildSSHChannels(s.Name, local, remote)
if err != nil {
log.Error(err)
os.Exit(1)
}

t, err := tunnel.New(s, channels)
t, err := tunnel.New(tunnelFlags.TunnelType, s, source, destination)
if err != nil {
log.Error(err)
os.Exit(1)
Expand Down
14 changes: 13 additions & 1 deletion cmd/start_local.go
Expand Up @@ -10,7 +10,19 @@ import (
var localCmd = &cobra.Command{
Use: "local",
Short: "Starts a ssh local port forwarding tunnel",
Long: "Starts a ssh local port forwarding tunnel",
Long: `Local Forwarding allows anyone to access outside services like they were
running locally on the source machine.
This could be particular useful for accesing web sites, databases or any kind of
service the source machine does not have direct access to.
Source endpoints are addresses on the same machine where mole is getting executed where clients can connect to access services on the corresponding destination endpoints.
Destination endpoints are adrresess that can be reached from the jump server.
`,
Args: func(cmd *cobra.Command, args []string) error {
tunnelFlags.TunnelType = "local"
return nil
},
Run: func(cmd *cobra.Command, arg []string) {
start("", tunnelFlags)
},
Expand Down
37 changes: 37 additions & 0 deletions cmd/start_remote.go
@@ -0,0 +1,37 @@
package cmd

import (
"os"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var remoteCmd = &cobra.Command{
Use: "remote",
Short: "Starts a ssh remote port forwarding tunnel",
Long: `Remote Forwarding allows anyone to expose a service running locally to a remote machine.
This could be particular useful for giving someone on the outside access to an internal web application, for example.
Source endpoints are addresses on the jump server where clients can connect to access services running on the corresponding destination endpoints.
Destination endpoints are addresses of services running on the same machine where mole is getting executed.
`,
Args: func(cmd *cobra.Command, args []string) error {
tunnelFlags.TunnelType = "remote"
return nil
},
Run: func(cmd *cobra.Command, arg []string) {
start("", tunnelFlags)
},
}

func init() {
err := bindFlags(tunnelFlags, remoteCmd)
if err != nil {
log.WithError(err).Error("error parsing command line arguments")
os.Exit(1)
}

startCmd.AddCommand(remoteCmd)
}
3 changes: 2 additions & 1 deletion go.mod
Expand Up @@ -10,12 +10,13 @@ require (
github.com/kevinburke/ssh_config v0.0.0-20190630040420-2e50c441276c
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/pelletier/go-buffruneio v0.2.0 // indirect
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/prometheus/common v0.10.0
github.com/sevlyar/go-daemon v0.1.5
github.com/sirupsen/logrus v1.4.2
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.3.2
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
)
4 changes: 4 additions & 0 deletions go.sum
Expand Up @@ -52,6 +52,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down Expand Up @@ -94,6 +96,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
Expand Down
44 changes: 34 additions & 10 deletions test-env/README.md
Expand Up @@ -86,13 +86,23 @@ The ssh authentication key files, `test-env/key` and `test-env/key,pub` will
```sh
$ make test-env
<lots of output messages here>
$ mole --verbose --insecure --local :21112 --local :21113 --remote 192.168.33.11:80 --remote 192.168.33.11:8080 --server mole@127.0.0.1:22122 --key test-env/ssh-server/keys/key --keep-alive-interval 2s
INFO[0000] tunnel is ready local="127.0.0.1:21113" remote="192.168.33.11:8080"
INFO[0000] tunnel is ready local="127.0.0.1:21112" remote="192.168.33.11:80"
$ curl 127.0.0.1:21112
:)
$ curl 127.0.0.1:21113
:)
mole start local \
--verbose \
--insecure \
--source :21112 \
--source :21113 \
--destination 192.168.33.11:80 \
--destination 192.168.33.11:8080 \
--server mole@127.0.0.1:22122 \
--key test-env/ssh-server/keys/key \
--keep-alive-interval 2s
DEBU[0000] using ssh config file from: /home/mole/.ssh/config
DEBU[0000] server: [name=127.0.0.1, address=127.0.0.1:22122, user=mole]
DEBU[0000] tunnel: [channels:[[source=127.0.0.1:21112, destination=192.168.33.11:80] [source=127.0.0.1:21113, destination=192.168.33.11:8080]], server:127.0.0.1:22122]
DEBU[0000] connection to the ssh server is established server="[name=127.0.0.1, address=127.0.0.1:22122, user=mole]"
DEBU[0000] start sending keep alive packets
INFO[0000] tunnel channel is waiting for connection destination="192.168.33.11:8080" source="127.0.0.1:21113"
INFO[0000] tunnel channel is waiting for connection destination="192.168.33.11:80" source="127.0.0.1:21112"
```

NOTE: If you're wondering about the smile face, that is the response from both
Expand All @@ -116,9 +126,23 @@ $ make test-env
2. Start mole

```sh
$ mole --verbose --insecure --local :21112 --local :21113 --remote 192.168.33.11:80 --remote 192.168.33.11:8080 --server mole@127.0.0.1:22122 --key test-env/ssh-server/keys/key --keep-alive-interval 2s
INFO[0000] tunnel is ready local="127.0.0.1:21113" remote="192.168.33.11:8080"
INFO[0000] tunnel is ready local="127.0.0.1:21112" remote="192.168.33.11:80"
mole start local \
--verbose \
--insecure \
--source :21112 \
--source :21113 \
--destination 192.168.33.11:80 \
--destination 192.168.33.11:8080 \
--server mole@127.0.0.1:22122 \
--key test-env/ssh-server/keys/key \
--keep-alive-interval 2s
DEBU[0000] using ssh config file from: /home/mole/.ssh/config
DEBU[0000] server: [name=127.0.0.1, address=127.0.0.1:22122, user=mole]
DEBU[0000] tunnel: [channels:[[source=127.0.0.1:21112, destination=192.168.33.11:80] [source=127.0.0.1:21113, destination=192.168.33.11:8080]], server:127.0.0.1:22122]
DEBU[0000] connection to the ssh server is established server="[name=127.0.0.1, address=127.0.0.1:22122, user=mole]"
DEBU[0000] start sending keep alive packets
INFO[0000] tunnel channel is waiting for connection destination="192.168.33.11:8080" source="127.0.0.1:21113"
INFO[0000] tunnel channel is waiting for connection destination="192.168.33.11:80" source="127.0.0.1:21112"
```

3. Kill all ssh processes running on the container holding the ssh server
Expand Down
2 changes: 2 additions & 0 deletions test-env/http-server/Dockerfile
Expand Up @@ -7,4 +7,6 @@ RUN mkdir -p /data/www
COPY default.conf /etc/nginx/conf.d/
COPY index.html /data/www

EXPOSE 8080

CMD nginx -g 'daemon off;'
3 changes: 2 additions & 1 deletion test-env/ssh-server/Dockerfile
Expand Up @@ -5,7 +5,8 @@ RUN apk update && apk add \
libcap \
openssh \
tcpdump \
supervisor
supervisor \
curl

COPY sshd_config /etc/ssh/sshd_config
COPY motd /etc/motd
Expand Down

0 comments on commit 92f82af

Please sign in to comment.