summaryrefslogtreecommitdiff
path: root/tunnels
diff options
context:
space:
mode:
Diffstat (limited to 'tunnels')
-rw-r--r--tunnels/tunneler.go35
-rw-r--r--tunnels/tunnels.go124
-rw-r--r--tunnels/tunnels_suite_test.go29
-rw-r--r--tunnels/tunnels_test.go120
4 files changed, 308 insertions, 0 deletions
diff --git a/tunnels/tunneler.go b/tunnels/tunneler.go
new file mode 100644
index 0000000..8f369b1
--- /dev/null
+++ b/tunnels/tunneler.go
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+// File tunnels/tunneler.go defines Tunneler interface with API
+// for basic operations on tunnels.
+
+package tunnels
+
+import (
+ "net"
+)
+
+// Tunneler defines API for basic operations on tunnels.
+type Tunneler interface {
+ // Create sets up a new tunnel.
+ Create(net.IP, net.IP) error
+ // Close shuts down tunnel.
+ Close() error
+ // Addr returns the address of the tunnel to be used by a user
+ // for a connection to Dryad.
+ Addr() net.Addr
+}
diff --git a/tunnels/tunnels.go b/tunnels/tunnels.go
new file mode 100644
index 0000000..d8409db
--- /dev/null
+++ b/tunnels/tunnels.go
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+// Package tunnels allows creation of simple forwarding tunnels
+// between IP addresses pairs.
+package tunnels
+
+import (
+ "io"
+ "net"
+)
+
+// defaultSSHPort is a default port used for connection to dest by Tunnel.
+const defaultSSHPort = 22
+
+// Tunnel forwards data between source and destination addresses.
+type Tunnel struct {
+ Tunneler
+ listener *net.TCPListener
+ dest *net.TCPAddr
+ done chan struct{}
+}
+
+// Create sets up data forwarding tunnel between src and dest IP addresses.
+// It will listen on random port on src and forward to SSH port (22) of dest.
+//
+// When connection to src is made a corresponding one is created to dest
+// and data is copied between them.
+//
+// Close should be called to clean up this function and terminate connections.
+func (t *Tunnel) Create(src, dest net.IP) (err error) {
+ return t.create(src, dest, defaultSSHPort)
+}
+
+// create is a helper function for Create method, which allows to setup any
+// port for testing purposes.
+func (t *Tunnel) create(src, dest net.IP, portSSH int) (err error) {
+ t.dest = &net.TCPAddr{IP: dest, Port: portSSH}
+ t.done = make(chan struct{})
+ // It will listen on a random port.
+ t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: src})
+ if err != nil {
+ return err
+ }
+ go t.listenAndForward()
+ return nil
+}
+
+// Close stops listening on the port for new connections and terminates exisiting ones.
+func (t *Tunnel) Close() error {
+ close(t.done)
+ return t.listener.Close()
+}
+
+// listenAndForward accepts connection, creates a corresponding one to dest
+// and starts goroutines copying data in both directions.
+//
+// Connection close does not guarantee that the other one is terminated.
+// It does however stop copying data from the closed end.
+func (t *Tunnel) listenAndForward() {
+ for {
+ select {
+ case <-t.done:
+ // Stop listening if the tunnel is no longer active.
+ return
+ default:
+ }
+ srcConn, err := t.listener.Accept()
+ if err != nil {
+ // TODO(amistewicz): log an error.
+ continue
+ }
+ destConn, err := net.DialTCP("tcp", nil, t.dest)
+ if err != nil {
+ // TODO(amistewicz): log an error.
+ srcConn.Close()
+ continue
+ }
+ // Close connections when we are done.
+ defer srcConn.Close()
+ defer destConn.Close()
+ // Forward traffic in both directions.
+ // These goroutines will stop when Close() is called on srcConn and destConn.
+ go t.forward(srcConn, destConn)
+ go t.forward(destConn, srcConn)
+ }
+}
+
+// Addr returns address on which it listens for connection.
+//
+// It should be used to make a connection to the Tunnel.
+func (t *Tunnel) Addr() net.Addr {
+ return t.listener.Addr()
+}
+
+// forward copies data from src to dest.
+//
+// It is the simplest and most portable solution.
+// Properly an iptables entries should be made as follows:
+// * -t nat -A PREROUTING -i src_interface -p tcp --dport src_port -j DNAT --to-destination dest_address
+// * -A FORWARD -i src_interface -o dest_interface -p tcp --syn --dport src_port -m conntrack --ctstate NEW -j ACCEPT
+// * -A FORWARD -i src_interface -o dest_interface -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
+// * -A FORWARD -i dest_interface -o src_interface -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
+// * -t nat -A POSTROUTING -o dest_interface -p tcp --dport dest_port -d dest_ip -j SNAT --to-source src_ip
+func (t *Tunnel) forward(src io.Reader, dest io.Writer) {
+ _, err := io.Copy(dest, src)
+ // TODO(amistewicz): save statistics about usage in each direction.
+ if err != nil {
+ // TODO(amistewicz): log an error. It will occur every time a dest end is closed before src.
+ }
+}
diff --git a/tunnels/tunnels_suite_test.go b/tunnels/tunnels_suite_test.go
new file mode 100644
index 0000000..32d4c7e
--- /dev/null
+++ b/tunnels/tunnels_suite_test.go
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package tunnels
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ "testing"
+)
+
+func TestTunnels(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Tunnels Suite")
+}
diff --git a/tunnels/tunnels_test.go b/tunnels/tunnels_test.go
new file mode 100644
index 0000000..99af08f
--- /dev/null
+++ b/tunnels/tunnels_test.go
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package tunnels
+
+import (
+ "io"
+ "net"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Tunnels", func() {
+ invalidIP := net.IPv4(255, 8, 8, 8)
+
+ listen := func(done chan struct{}, in, out string) *net.TCPAddr {
+ addr := new(net.TCPAddr)
+ ln, err := net.ListenTCP("tcp", addr)
+ go func() {
+ defer close(done)
+ defer GinkgoRecover()
+ // Accept a single connection
+ defer ln.Close()
+ conn, err := ln.Accept()
+ Expect(err).ToNot(HaveOccurred())
+ defer conn.Close()
+ if in != "" {
+ buf := make([]byte, len(in))
+ _, err := io.ReadFull(conn, buf)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(string(buf)).To(Equal(in))
+ }
+ if out != "" {
+ _, err := io.WriteString(conn, out)
+ Expect(err).ToNot(HaveOccurred())
+ }
+ }()
+ Expect(err).ToNot(HaveOccurred())
+ return ln.Addr().(*net.TCPAddr)
+ }
+
+ var t *Tunnel
+
+ BeforeEach(func() {
+ t = new(Tunnel)
+ Expect(t).NotTo(BeNil())
+ })
+
+ It("should make a connection", func() {
+ done := make(chan struct{})
+ lAddr := listen(done, "", "")
+ err := t.create(nil, nil, lAddr.Port)
+ Expect(err).ToNot(HaveOccurred())
+
+ conn, err := net.DialTCP("tcp", nil, lAddr)
+ Expect(err).ToNot(HaveOccurred())
+
+ defer conn.Close()
+ Eventually(done).Should(BeClosed())
+ Expect(t.Close()).To(Succeed())
+ })
+
+ It("should pass data through", func() {
+ done := make(chan struct{})
+ testIn := "input test string"
+ testOut := "output test string"
+ lAddr := listen(done, testIn, testOut)
+ err := t.create(nil, nil, lAddr.Port)
+ Expect(err).ToNot(HaveOccurred())
+ conn, err := net.DialTCP("tcp", nil, t.Addr().(*net.TCPAddr))
+ Expect(err).ToNot(HaveOccurred())
+ defer conn.Close()
+
+ _, err = io.WriteString(conn, testIn)
+ Expect(err).ToNot(HaveOccurred())
+ buf := make([]byte, len(testOut))
+ _, err = io.ReadFull(conn, buf)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(string(buf)).To(Equal(testOut))
+
+ Eventually(done).Should(BeClosed())
+ Expect(t.Close()).To(Succeed())
+ })
+
+ It("should fail to listen on invalid address", func() {
+ err := t.Create(invalidIP, nil)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("should fail to connect to invalid address", func() {
+ err := t.create(nil, nil, 0)
+ Expect(err).ToNot(HaveOccurred())
+
+ conn, err := net.DialTCP("tcp", nil, t.Addr().(*net.TCPAddr))
+ Expect(err).ToNot(HaveOccurred())
+ defer conn.Close()
+
+ // There is no good way to check for closed connection
+ // so we wait for some error to occur:
+ // write tcp [::1]:36454->[::1]:42173: write: broken pipe
+ // It usually takes 2 attempts.
+ Eventually(func() error {
+ _, err = io.WriteString(conn, "test")
+ return err
+ }).Should(HaveOccurred())
+ })
+})