Skip to content

Commit 92f82af

Browse files
authored
Merge pull request #114 from davrodpin/tunnel/remote
Add support for ssh remote port forwarding
2 parents b3396a3 + b4239ad commit 92f82af

18 files changed

+492
-243
lines changed

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ mole-http: rm-mole-http
4848
--detach \
4949
--network mole \
5050
--ip 192.168.33.11 \
51+
--publish 8080:8080 \
5152
--name mole_http mole_http:latest
5253

5354
rm-mole-ssh:

alias/alias.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const (
3434
// TunnelFlags is a struct that holds all flags required to establish a ssh
3535
// port forwarding tunnel.
3636
type TunnelFlags struct {
37+
TunnelType string
3738
Verbose bool
3839
Insecure bool
3940
Detach bool
@@ -49,10 +50,10 @@ type TunnelFlags struct {
4950
}
5051

5152
// ParseAlias translates a TunnelFlags object to an Alias object
52-
func (tf TunnelFlags) ParseAlias(name, tunnelType string) *Alias {
53+
func (tf TunnelFlags) ParseAlias(name string) *Alias {
5354
return &Alias{
5455
Name: name,
55-
TunnelType: tunnelType,
56+
TunnelType: tf.TunnelType,
5657
Verbose: tf.Verbose,
5758
Insecure: tf.Insecure,
5859
Detach: tf.Detach,
@@ -109,6 +110,7 @@ func (a Alias) ParseTunnelFlags() (*TunnelFlags, error) {
109110

110111
tf := &TunnelFlags{}
111112

113+
tf.TunnelType = a.TunnelType
112114
tf.Verbose = a.Verbose
113115
tf.Insecure = a.Insecure
114116
tf.Detach = a.Detach

alias/alias_test.go

+20-12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
func TestParseTunnelFlags(t *testing.T) {
1818

1919
tests := []struct {
20+
tunnelType string
2021
verbose bool
2122
insecure bool
2223
detach bool
@@ -31,6 +32,7 @@ func TestParseTunnelFlags(t *testing.T) {
3132
timeout string
3233
}{
3334
{
35+
"local",
3436
true,
3537
true,
3638
true,
@@ -45,6 +47,7 @@ func TestParseTunnelFlags(t *testing.T) {
4547
"1m0s",
4648
},
4749
{
50+
"local",
4851
true,
4952
false,
5053
true,
@@ -62,6 +65,7 @@ func TestParseTunnelFlags(t *testing.T) {
6265

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

88+
if test.tunnelType != tf.TunnelType {
89+
t.Errorf("tunnelType doesn't match on test %d: expected: %s, value: %s", id, test.tunnelType, tf.TunnelType)
90+
}
91+
8492
if test.verbose != tf.Verbose {
85-
t.Errorf("verbose doesn't match for test %d: expected: %t, value: %t", id, test.verbose, tf.Verbose)
93+
t.Errorf("verbose doesn't match on test %d: expected: %t, value: %t", id, test.verbose, tf.Verbose)
8694
}
8795

8896
if test.insecure != tf.Insecure {
89-
t.Errorf("insecure doesn't match for test %d: expected: %t, value: %t", id, test.insecure, tf.Insecure)
97+
t.Errorf("insecure doesn't match on test %d: expected: %t, value: %t", id, test.insecure, tf.Insecure)
9098
}
9199

92100
if test.detach != tf.Detach {
93-
t.Errorf("detach doesn't match for test %d: expected: %t, value: %t", id, test.detach, tf.Detach)
101+
t.Errorf("detach doesn't match on test %d: expected: %t, value: %t", id, test.detach, tf.Detach)
94102
}
95103

96104
for i, tsrc := range test.source {
97105
src := tf.Source[i].String()
98106
if tsrc != src {
99-
t.Errorf("source %d doesn't match for test %d: expected: %s, value: %s", id, i, tsrc, src)
107+
t.Errorf("source %d doesn't match on test %d: expected: %s, value: %s", id, i, tsrc, src)
100108
}
101109
}
102110

103111
for i, tdst := range test.destination {
104112
dst := tf.Destination[i].String()
105113
if tdst != dst {
106-
t.Errorf("destination %d doesn't match for test %d: expected: %s, value: %s", id, i, tdst, dst)
114+
t.Errorf("destination %d doesn't match on test %d: expected: %s, value: %s", id, i, tdst, dst)
107115
}
108116
}
109117

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

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

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

122130
if test.connectionRetries != tf.ConnectionRetries {
123-
t.Errorf("connectionRetries doesn't match for test %d: expected: %d, value: %d", id, test.connectionRetries, tf.ConnectionRetries)
131+
t.Errorf("connectionRetries doesn't match on test %d: expected: %d, value: %d", id, test.connectionRetries, tf.ConnectionRetries)
124132
}
125133

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

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

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

138146
}

cmd/add_alias.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ The alias configuration file is saved to ".mole", under your home directory.
2626
return errors.New("alias name not provided")
2727
}
2828

29-
tunnelType = args[0]
29+
tunnelFlags.TunnelType = args[0]
3030
aliasName = args[1]
3131

3232
return nil
3333
},
3434
Run: func(cmd *cobra.Command, arg []string) {
35-
if err := alias.Add(tunnelFlags.ParseAlias(aliasName, "local")); err != nil {
35+
if err := alias.Add(tunnelFlags.ParseAlias(aliasName)); err != nil {
3636
log.WithError(err).Error("failed to add tunnel alias")
3737
os.Exit(1)
3838
}

cmd/root.go

+6-15
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
)
1919

2020
var (
21-
tunnelType string
2221
aliasName string
2322
tunnelFlags = &alias.TunnelFlags{}
2423

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

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

117-
// TODO: rename local to source
118-
local := make([]string, len(tunnelFlags.Source))
116+
source := make([]string, len(tunnelFlags.Source))
119117
for i, r := range tunnelFlags.Source {
120-
local[i] = r.String()
118+
source[i] = r.String()
121119
}
122120

123-
// TODO: rename remote to destination
124-
remote := make([]string, len(tunnelFlags.Destination))
121+
destination := make([]string, len(tunnelFlags.Destination))
125122
for i, r := range tunnelFlags.Destination {
126123
if r.Port == "" {
127-
err := fmt.Errorf("missing port in remote address: %s", r.String())
124+
err := fmt.Errorf("missing port in destination address: %s", r.String())
128125
log.Error(err)
129126
os.Exit(1)
130127
}
131128

132-
remote[i] = r.String()
129+
destination[i] = r.String()
133130
}
134131

135-
channels, err := tunnel.BuildSSHChannels(s.Name, local, remote)
136-
if err != nil {
137-
log.Error(err)
138-
os.Exit(1)
139-
}
140-
141-
t, err := tunnel.New(s, channels)
132+
t, err := tunnel.New(tunnelFlags.TunnelType, s, source, destination)
142133
if err != nil {
143134
log.Error(err)
144135
os.Exit(1)

cmd/start_local.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@ import (
1010
var localCmd = &cobra.Command{
1111
Use: "local",
1212
Short: "Starts a ssh local port forwarding tunnel",
13-
Long: "Starts a ssh local port forwarding tunnel",
13+
Long: `Local Forwarding allows anyone to access outside services like they were
14+
running locally on the source machine.
15+
16+
This could be particular useful for accesing web sites, databases or any kind of
17+
service the source machine does not have direct access to.
18+
19+
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.
20+
Destination endpoints are adrresess that can be reached from the jump server.
21+
`,
22+
Args: func(cmd *cobra.Command, args []string) error {
23+
tunnelFlags.TunnelType = "local"
24+
return nil
25+
},
1426
Run: func(cmd *cobra.Command, arg []string) {
1527
start("", tunnelFlags)
1628
},

cmd/start_remote.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
6+
log "github.com/sirupsen/logrus"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var remoteCmd = &cobra.Command{
11+
Use: "remote",
12+
Short: "Starts a ssh remote port forwarding tunnel",
13+
Long: `Remote Forwarding allows anyone to expose a service running locally to a remote machine.
14+
15+
This could be particular useful for giving someone on the outside access to an internal web application, for example.
16+
17+
Source endpoints are addresses on the jump server where clients can connect to access services running on the corresponding destination endpoints.
18+
Destination endpoints are addresses of services running on the same machine where mole is getting executed.
19+
`,
20+
Args: func(cmd *cobra.Command, args []string) error {
21+
tunnelFlags.TunnelType = "remote"
22+
return nil
23+
},
24+
Run: func(cmd *cobra.Command, arg []string) {
25+
start("", tunnelFlags)
26+
},
27+
}
28+
29+
func init() {
30+
err := bindFlags(tunnelFlags, remoteCmd)
31+
if err != nil {
32+
log.WithError(err).Error("error parsing command line arguments")
33+
os.Exit(1)
34+
}
35+
36+
startCmd.AddCommand(remoteCmd)
37+
}

go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ require (
1010
github.com/kevinburke/ssh_config v0.0.0-20190630040420-2e50c441276c
1111
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
1212
github.com/pelletier/go-buffruneio v0.2.0 // indirect
13+
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
1314
github.com/prometheus/common v0.10.0
1415
github.com/sevlyar/go-daemon v0.1.5
1516
github.com/sirupsen/logrus v1.4.2
1617
github.com/spf13/cobra v0.0.5
1718
github.com/spf13/pflag v1.0.5
1819
github.com/spf13/viper v1.3.2
1920
github.com/stretchr/testify v1.3.0 // indirect
20-
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
21+
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
2122
)

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
5252
github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA=
5353
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
5454
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
55+
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
56+
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
5557
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
5658
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
5759
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -94,6 +96,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
9496
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
9597
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
9698
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
99+
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
100+
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
97101
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
98102
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
99103
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=

test-env/README.md

+34-10
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,23 @@ The ssh authentication key files, `test-env/key` and `test-env/key,pub` will
8686
```sh
8787
$ make test-env
8888
<lots of output messages here>
89-
$ mole --verbose --insecure --local :21112 --local :21113 --remote 192.168.33.11:80 --remote 192.168.33.11:8080 --server [email protected]:22122 --key test-env/ssh-server/keys/key --keep-alive-interval 2s
90-
INFO[0000] tunnel is ready local="127.0.0.1:21113" remote="192.168.33.11:8080"
91-
INFO[0000] tunnel is ready local="127.0.0.1:21112" remote="192.168.33.11:80"
92-
$ curl 127.0.0.1:21112
93-
:)
94-
$ curl 127.0.0.1:21113
95-
:)
89+
mole start local \
90+
--verbose \
91+
--insecure \
92+
--source :21112 \
93+
--source :21113 \
94+
--destination 192.168.33.11:80 \
95+
--destination 192.168.33.11:8080 \
96+
--server [email protected]:22122 \
97+
--key test-env/ssh-server/keys/key \
98+
--keep-alive-interval 2s
99+
DEBU[0000] using ssh config file from: /home/mole/.ssh/config
100+
DEBU[0000] server: [name=127.0.0.1, address=127.0.0.1:22122, user=mole]
101+
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]
102+
DEBU[0000] connection to the ssh server is established server="[name=127.0.0.1, address=127.0.0.1:22122, user=mole]"
103+
DEBU[0000] start sending keep alive packets
104+
INFO[0000] tunnel channel is waiting for connection destination="192.168.33.11:8080" source="127.0.0.1:21113"
105+
INFO[0000] tunnel channel is waiting for connection destination="192.168.33.11:80" source="127.0.0.1:21112"
96106
```
97107

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

118128
```sh
119-
$ mole --verbose --insecure --local :21112 --local :21113 --remote 192.168.33.11:80 --remote 192.168.33.11:8080 --server [email protected]:22122 --key test-env/ssh-server/keys/key --keep-alive-interval 2s
120-
INFO[0000] tunnel is ready local="127.0.0.1:21113" remote="192.168.33.11:8080"
121-
INFO[0000] tunnel is ready local="127.0.0.1:21112" remote="192.168.33.11:80"
129+
mole start local \
130+
--verbose \
131+
--insecure \
132+
--source :21112 \
133+
--source :21113 \
134+
--destination 192.168.33.11:80 \
135+
--destination 192.168.33.11:8080 \
136+
--server [email protected]:22122 \
137+
--key test-env/ssh-server/keys/key \
138+
--keep-alive-interval 2s
139+
DEBU[0000] using ssh config file from: /home/mole/.ssh/config
140+
DEBU[0000] server: [name=127.0.0.1, address=127.0.0.1:22122, user=mole]
141+
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]
142+
DEBU[0000] connection to the ssh server is established server="[name=127.0.0.1, address=127.0.0.1:22122, user=mole]"
143+
DEBU[0000] start sending keep alive packets
144+
INFO[0000] tunnel channel is waiting for connection destination="192.168.33.11:8080" source="127.0.0.1:21113"
145+
INFO[0000] tunnel channel is waiting for connection destination="192.168.33.11:80" source="127.0.0.1:21112"
122146
```
123147

124148
3. Kill all ssh processes running on the container holding the ssh server

test-env/http-server/Dockerfile

+2
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ RUN mkdir -p /data/www
77
COPY default.conf /etc/nginx/conf.d/
88
COPY index.html /data/www
99

10+
EXPOSE 8080
11+
1012
CMD nginx -g 'daemon off;'

test-env/ssh-server/Dockerfile

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ RUN apk update && apk add \
55
libcap \
66
openssh \
77
tcpdump \
8-
supervisor
8+
supervisor \
9+
curl
910

1011
COPY sshd_config /etc/ssh/sshd_config
1112
COPY motd /etc/motd

0 commit comments

Comments
 (0)