diff options
author | Lukasz Wojciechowski <l.wojciechow@partner.samsung.com> | 2017-10-31 09:59:38 +0100 |
---|---|---|
committer | Lukasz Wojciechowski <l.wojciechow@partner.samsung.com> | 2018-04-27 17:34:20 +0200 |
commit | 38ef549139849d62ffe88af54af14ab7d456ebf8 (patch) | |
tree | b5dbd56c116f5ab8cd9c83cc1f1b703dbbef88fc /tunnels | |
parent | 9eec2c2c3f17322be8d9eab30ff174f9173a2120 (diff) | |
download | boruta-38ef549139849d62ffe88af54af14ab7d456ebf8.tar.gz boruta-38ef549139849d62ffe88af54af14ab7d456ebf8.tar.bz2 boruta-38ef549139849d62ffe88af54af14ab7d456ebf8.zip |
Move tunnels from workers to separate package
Tunnels package provides implementation of simple data forwarding
tunnels between IP addresses pairs.
The implementation has been moved from workers package and adjusted
to newly defined Tunneler interface. The interface defines basic
operations on tunnels (creation, getting address, closing).
It provides additional layer of abstraction allowing mockuping tests
of parts of the code using tunnels.
The mock implementation of interface is provided in matcher package.
It is generated using mockgen command:
mockgen -package matcher \
-destination=matcher/tunneler_mock_test.go \
-write_package_comment=false \
git.tizen.org/tools/boruta/tunnels Tunneler
Change-Id: Ida42f0134f0c365c8f1ffe772b859a0218c301ed
Signed-off-by: Lukasz Wojciechowski <l.wojciechow@partner.samsung.com>
Signed-off-by: Maciej Wereski <m.wereski@partner.samsung.com>
Diffstat (limited to 'tunnels')
-rw-r--r-- | tunnels/tunneler.go | 35 | ||||
-rw-r--r-- | tunnels/tunnels.go | 124 | ||||
-rw-r--r-- | tunnels/tunnels_suite_test.go | 29 | ||||
-rw-r--r-- | tunnels/tunnels_test.go | 120 |
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()) + }) +}) |