From e2f9f853e48667a2ca149b80eb93a676bb288a0a Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Sat, 11 May 2019 14:38:34 -0500 Subject: [PATCH 1/3] Initial cut --- .drone.yml | 22 +++ .gitignore | 2 + LICENSE | 202 +++++++++++++++++++++++++ README.md | 31 ++++ conntrack_linux.go | 56 +++++++ demo/demo.go | 87 +++++++++++ go.mod | 18 +++ go.sum | 95 ++++++++++++ gonat.go | 184 ++++++++++++++++++++++ gonat_linux.go | 353 +++++++++++++++++++++++++++++++++++++++++++ gonat_linux_test.go | 42 +++++ packet.go | 217 ++++++++++++++++++++++++++ packet_test.go | 91 +++++++++++ stats_linux.go | 72 +++++++++ test_helper_linux.go | 220 +++++++++++++++++++++++++++ transport_linux.go | 122 +++++++++++++++ 16 files changed, 1814 insertions(+) create mode 100644 .drone.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 conntrack_linux.go create mode 100644 demo/demo.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gonat.go create mode 100644 gonat_linux.go create mode 100644 gonat_linux_test.go create mode 100644 packet.go create mode 100644 packet_test.go create mode 100644 stats_linux.go create mode 100644 test_helper_linux.go create mode 100644 transport_linux.go diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..a5755cd --- /dev/null +++ b/.drone.yml @@ -0,0 +1,22 @@ +kind: pipeline +name: default + +platform: + os: linux + arch: amd64 + +steps: +- name: test + image: golang + privileged: true + environment: + COVERALLS_TOKEN: + from_secret: coveralls_token + commands: + - apt-get -q -y update + - apt-get -q -y install lsof net-tools iptables + - iptables -D ufw-before-input -m conntrack --ctstate INVALID -j DROP || echo "Rule not found" + - iptables -I OUTPUT -p tcp -m conntrack --ctstate ESTABLISHED --ctdir ORIGINAL --tcp-flags RST RST -j DROP + - go test -covermode=count -coverprofile=profile.cov + - go get github.com/mattn/goveralls + - goveralls -coverprofile=profile.cov \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f22dd6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +demo/demo +gonat.test diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd655e8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Brave New Software Project, Inc. + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c88df9 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +gonat [![Build Status](https://drone.lantern.io/api/badges/getlantern/gonat/status.svg)](https://drone.lantern.io/getlantern/gonat) [![GoDoc](https://godoc.org/github.com/getlantern/gonat?status.png)](http://godoc.org/github.com/getlantern/gonat) +========== + +This library only works on Linux. + +Dependencies are managed using Go modules. If using a version of Go prior to 1.13, use the environment +variable `GO111MODULE=on` to enable use of modules. + +In order to work, this library needs to be able to open raw sockets and update the conntrack table +via netlink. You can give the binary the correct capabilities with: + +`sudo setcap CAP_NET_RAW,CAP_NET_ADMIN+ep ` + +This library requires the nf_conntrack module to be installed at runtime. + +``` +modprobe nf_conntrack +modprobe nf_conntrack_ipv4 +``` + +iptables needs to be configured to drop the outbound RST packets that the kernel would usually create in response to SYN/ACK +packets responding to our raw TCP connections. We do this only for tcp connections that are already in ESTABLISHED in conntrack. +The library manually adds these to conntrack since we're using raw sockets. + +`sudo iptables -I OUTPUT -p tcp -m conntrack --ctstate ESTABLISHED --ctdir ORIGINAL --tcp-flags RST RST -j DROP` + +To run the unit tests, you need to have root permissions. It's also useful to enable tracing while running the tests. + +``` +GO111MODULE=on go test -c && TRACE=true sudo -E ./gonat.test +``` \ No newline at end of file diff --git a/conntrack_linux.go b/conntrack_linux.go new file mode 100644 index 0000000..358baca --- /dev/null +++ b/conntrack_linux.go @@ -0,0 +1,56 @@ +package gonat + +import ( + "syscall" + + "github.com/ti-mo/conntrack" +) + +// Since we're using unconnected raw sockets, the kernel doesn't create ip_conntrack +// entries for us. When we receive a SYNACK packet from the upstream end in response +// to the SYN packet that we forward from the client, the kernel automatically sends +// an RST packet because it doesn't see a connection in the right state. We can't +// actually fake a connection in the right state, however we can manually create an entry +// in ip_conntrack which allows us to use a single iptables rule to safely drop +// all outbound RST packets for tracked tcp connections. The rule can be added like so: +// +// iptables -A OUTPUT -p tcp -m conntrack --ctstate ESTABLISHED --ctdir ORIGINAL --tcp-flags RST RST -j DROP +// +func (s *server) createConntrackEntry(upFT FiveTuple) error { + flow := s.ctFlowFor(true, upFT) + log.Tracef("Creating conntrack entry for %v", upFT) + return s.ctrack.Create(flow) +} + +func (s *server) deleteConntrackEntry(upFT FiveTuple) { + flow := s.ctFlowFor(false, upFT) + if err := s.ctrack.Delete(flow); err != nil { + log.Errorf("Unable to delete conntrack entry for %v: %v", upFT, err) + } +} + +func (s *server) ctFlowFor(create bool, upFT FiveTuple) conntrack.Flow { + var ctTimeout uint32 + var status conntrack.StatusFlag + if create { + // we set the status to ASSURED so that the kernel doesn't destroy the conntrack entry + // prior to its expiration. It would do this because the connection doesn't look right and + // very quickly transitions into a CLOSED state, at which point it would be eligible for + // destruction even before its timeout. + status = conntrack.StatusConfirmed | conntrack.StatusAssured + ctTimeout = s.ctTimeout + } + + flow := conntrack.NewFlow( + upFT.IPProto, status, + upFT.Src.IP(), upFT.Dst.IP(), + upFT.Src.Port, upFT.Dst.Port, + ctTimeout, 0) + if create && upFT.IPProto == syscall.IPPROTO_TCP { + flow.ProtoInfo.TCP = &conntrack.ProtoInfoTCP{ + State: 3, // ESTABLISHED + } + } + + return flow +} diff --git a/demo/demo.go b/demo/demo.go new file mode 100644 index 0000000..47c1d66 --- /dev/null +++ b/demo/demo.go @@ -0,0 +1,87 @@ +package main + +import ( + "flag" + "net/http" + _ "net/http/pprof" + "os" + "os/signal" + "syscall" + "time" + + "github.com/getlantern/golog" + "github.com/getlantern/gonat" + tun "github.com/getlantern/gotun" +) + +var ( + log = golog.LoggerFor("gotun-demo") +) + +var ( + tunDevice = flag.String("tun-device", "tun0", "tun device name") + tunAddr = flag.String("tun-address", "10.0.0.2", "tun device address") + tunMask = flag.String("tun-mask", "255.255.255.0", "tun device netmask") + tunGW = flag.String("tun-gw", "10.0.0.1", "tun device gateway") + mtu = flag.Int("mtu", 1500, "maximum transmission unit for TUN device") + ifOut = flag.String("ifout", "", "name of interface to use for outbound connections") + tcpDest = flag.String("tcpdest", "80.249.99.148", "destination to which to connect all TCP traffic") + udpDest = flag.String("udpdest", "8.8.8.8", "destination to which to connect all UDP traffic") + pprofAddr = flag.String("pprofaddr", "", "pprof address to listen on, not activate pprof if empty") +) + +func main() { + flag.Parse() + + if *pprofAddr != "" { + go func() { + log.Debugf("Starting pprof page at http://%s/debug/pprof", *pprofAddr) + srv := &http.Server{ + Addr: *pprofAddr, + } + if err := srv.ListenAndServe(); err != nil { + log.Error(err) + } + }() + } + + dev, err := tun.OpenTunDevice(*tunDevice, *tunAddr, *tunGW, *tunMask, *mtu) + if err != nil { + log.Fatal(err) + } + defer dev.Close() + + s, err := gonat.NewServer(dev, &gonat.Opts{ + IFName: *ifOut, + IdleTimeout: 5 * time.Second, + BufferDepth: 10000, + OnOutbound: func(pkt *gonat.IPPacket) { + pkt.SetDest(gonat.Addr{*tcpDest, pkt.FT().Dst.Port}) + }, + OnInbound: func(pkt *gonat.IPPacket, ft gonat.FiveTuple) { + pkt.SetSource(gonat.Addr{*tunGW, ft.Dst.Port}) + }, + }) + if err != nil { + log.Fatal(err) + } + defer s.Close() + + ch := make(chan os.Signal, 1) + signal.Notify(ch, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT) + go func() { + <-ch + log.Debug("Closing gonat server") + s.Close() + log.Debug("Closing TUN device") + dev.Close() + log.Debug("Finished closing") + os.Exit(0) + }() + + log.Debugf("Final result: %v", s.Serve()) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..594972e --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/getlantern/gonat + +go 1.12 + +require ( + github.com/florianl/go-conntrack v0.0.0-20190502110113-6d50e184fe3e + github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 + github.com/getlantern/fdcount v0.0.0-20170105153814-6a6cb5839bc5 + github.com/getlantern/golog v0.0.0-20170508214112-cca714f7feb5 + github.com/getlantern/gotun v0.0.0-20190523194503-885514e382d2 + github.com/getlantern/grtrack v0.0.0-20160824195228-cbf67d3fa0fd + github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f + github.com/google/gopacket v1.1.17 + github.com/mdlayher/netlink v0.0.0-20190514163018-336c8d74f4a0 + github.com/oxtoacart/bpool v0.0.0-20190524125616-8c0b41497736 + github.com/stretchr/testify v1.3.0 + github.com/ti-mo/conntrack v0.0.0-20190323132511-733fb77b6071 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..64165ff --- /dev/null +++ b/go.sum @@ -0,0 +1,95 @@ +github.com/aristanetworks/goarista v0.0.0-20190502180301-283422fc1708/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= +github.com/aristanetworks/goarista v0.0.0-20190514202536-8f808a500156 h1:sdAZ4pJ5nD/EzLkcw4AonvhgrU1aBKxx6ga1b7Psr9o= +github.com/aristanetworks/goarista v0.0.0-20190514202536-8f808a500156/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/florianl/go-conntrack v0.0.0-20190502110113-6d50e184fe3e h1:QslAYPskpG2G2DwTIWTyjB490R8Cg6EO6lQ7I3p/gag= +github.com/florianl/go-conntrack v0.0.0-20190502110113-6d50e184fe3e/go.mod h1:bVjd6LQuEVqwX8Mw3sMv22v569+H/3TQWW6itTFrBh8= +github.com/florianl/go-conntrack v0.1.0 h1:suODoW7bfJC7Vo9+sXbLN0Nf8lRJclq2U2+7PSYAM1o= +github.com/florianl/go-conntrack v0.1.0/go.mod h1:a4XwdljLUqLMcGJqq8kbVvVEzfoaA/c4jKwkEWDhAI4= +github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4= +github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= +github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So= +github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= +github.com/getlantern/eventual v0.0.0-20180125201821-84b02499361b/go.mod h1:O8T3zFEcY6+LRXFcVV4q8mEu2tDIixG8edC84DfswBc= +github.com/getlantern/fdcount v0.0.0-20170105153814-6a6cb5839bc5 h1:8Q9iN/V24EG01IgXEKVScth/rTXpplBxCYio/yIKtUw= +github.com/getlantern/fdcount v0.0.0-20170105153814-6a6cb5839bc5/go.mod h1:XZwE+iIlAgr64OFbXKFNCllBwV4wEipPx8Hlo2gZdbM= +github.com/getlantern/framed v0.0.0-20190306221922-7f7919c8cf9b h1:rLHDb6VkSBQZTnY1ttltW1Sxp16XKLYlF06o8ukMg2o= +github.com/getlantern/framed v0.0.0-20190306221922-7f7919c8cf9b/go.mod h1:c6/4xBJDKOXLNKPJH//5z5EEHiFBPt+HpDFAlcvjDWw= +github.com/getlantern/golog v0.0.0-20170508214112-cca714f7feb5 h1:Okd7vkn9CfIgDBj1ST/vtBTCfD/kxIhYD412K+FRKPc= +github.com/getlantern/golog v0.0.0-20170508214112-cca714f7feb5/go.mod h1:Vwx1Cg64gCdIalad44uvQsKZw6LsVczIKZrUBStEjVw= +github.com/getlantern/gonat v0.0.3/go.mod h1:MoO1FFoIiT4nna0uMDyzx8z7TDrpVt7qqeiQMaN7vNU= +github.com/getlantern/gotun v0.0.0-20190422200803-35dee1b197b5 h1:RgknT+sLDtsoJEySPGJHagDVJFyOVIXTkY4Cks8xWTg= +github.com/getlantern/gotun v0.0.0-20190422200803-35dee1b197b5/go.mod h1:xBRkm/iNqMm5ND8ZTgiCoyyPAyfY89vbYgtYFWx66lw= +github.com/getlantern/gotun v0.0.0-20190517161228-5fd9609427d4 h1:xt8Pw0bYiBX6USldD9vH5hS6pLCBup4pedYHDWMV3VI= +github.com/getlantern/gotun v0.0.0-20190517161228-5fd9609427d4/go.mod h1:1tN1bcYuIxD79XpS3RD+3ptxiBEWwR0p+PG2MsC4Nbo= +github.com/getlantern/gotun v0.0.0-20190521133812-51bfa687e26b h1:b/m0EoOnfn9NSKpCaCe1nxC8NLZ/CiqvfPx5v5tbCKk= +github.com/getlantern/gotun v0.0.0-20190521133812-51bfa687e26b/go.mod h1:1tN1bcYuIxD79XpS3RD+3ptxiBEWwR0p+PG2MsC4Nbo= +github.com/getlantern/gotun v0.0.0-20190523194503-885514e382d2 h1:B1Hyjlam0zeV5vgnAlnW666yFPeaj6rEm9AOUuzii1A= +github.com/getlantern/gotun v0.0.0-20190523194503-885514e382d2/go.mod h1:1tN1bcYuIxD79XpS3RD+3ptxiBEWwR0p+PG2MsC4Nbo= +github.com/getlantern/gotun v0.0.0-20190523213226-ef5ea18d56ca h1:/aGhThS16krbbEDGtnJgT2yaaK1YPtGnFNK1vVmjyME= +github.com/getlantern/gotun v0.0.0-20190523213226-ef5ea18d56ca/go.mod h1:1tN1bcYuIxD79XpS3RD+3ptxiBEWwR0p+PG2MsC4Nbo= +github.com/getlantern/grtrack v0.0.0-20160824195228-cbf67d3fa0fd h1:GPrx88jy222gMuRHXxBSViT/3zdNO210nRAaXn+lL6s= +github.com/getlantern/grtrack v0.0.0-20160824195228-cbf67d3fa0fd/go.mod h1:RkQEgBdrJCH5tYJP2D+a/aJ216V3c9q8w/tCJtEiDoY= +github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0= +github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o= +github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc= +github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA= +github.com/getlantern/idletiming v0.0.0-20190331182121-9540d1aeda2b h1:fR5xnj6A0dJ3/H+xyR2QeKy98QwQlcuk0Qczu2hdqHk= +github.com/getlantern/idletiming v0.0.0-20190331182121-9540d1aeda2b/go.mod h1:MGP8kEgZGgAhvHISt0hJGQgxg/VAqGdw3+kSZBnfC/4= +github.com/getlantern/ipproxy v0.0.0-20190502203022-c6564ee6fba1/go.mod h1:AX6L48UflMFs/pkaXppIu4VblJZvT8ES9mTO0nSnpd8= +github.com/getlantern/mtime v0.0.0-20170117193331-ba114e4a82b0 h1:1VNkP55LM/W2IwWN+qi+5X3gZcEQHfj8X9E+FNxVgM4= +github.com/getlantern/mtime v0.0.0-20170117193331-ba114e4a82b0/go.mod h1:u537FS7ld4Whf7h7/0ql/myAudWWBNgeRhgE9XXH4Pk= +github.com/getlantern/netx v0.0.0-20190110220209-9912de6f94fd h1:mn98vs69Kqw56iKhR82mjk16Q1q5aDFFW0E89/QbXkQ= +github.com/getlantern/netx v0.0.0-20190110220209-9912de6f94fd/go.mod h1:wKdY0ikOgzrWSeB9UyBVKPRhjXQ+vTb+BPeJuypUuNE= +github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA= +github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= +github.com/getlantern/uuid v1.1.2-0.20190507182000-5c9436b8c718/go.mod h1:uX10hOzZUUDR+oYNSIks+RcozOEiwTNC/K2rw9SUi1k= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY= +github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= +github.com/google/netstack v0.0.0-20190505230633-4391e4a763ab/go.mod h1:r/rILWg3r1Qy9G1IFMhsqWLq2GjwuYoTuPgG7ckMAjk= +github.com/mdlayher/netlink v0.0.0-20181210160939-e069752bc835/go.mod h1:a3TlQHkJH2m32RF224Z7LhD5N4mpyR8eUbCoYHywrwg= +github.com/mdlayher/netlink v0.0.0-20190313131330-258ea9dff42c/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/netlink v0.0.0-20190411141321-3cae06de9d30/go.mod h1:t19erlH/I/040715SD0/ktZi33scgSEuOlp57AIFe/c= +github.com/mdlayher/netlink v0.0.0-20190514163018-336c8d74f4a0 h1:93Nlj3OMYLLA6QA6baiIeWW8P8CTlwQFA4pAecq7f/8= +github.com/mdlayher/netlink v0.0.0-20190514163018-336c8d74f4a0/go.mod h1:gOrA34zDL0K3RsACQe54bDYLF/CeFspQ9m5DOycycQ8= +github.com/oxtoacart/bpool v0.0.0-20190227141107-8c4636f812cc h1:uhnyuvDwdKbjemAXHKsiEZOPagHim2nRjMcazH1g26A= +github.com/oxtoacart/bpool v0.0.0-20190227141107-8c4636f812cc/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0= +github.com/oxtoacart/bpool v0.0.0-20190524125616-8c0b41497736 h1:C9bEdTfu5QY+TIf4ohXC2oWkT88Qq3/t1yiUxf/Guvs= +github.com/oxtoacart/bpool v0.0.0-20190524125616-8c0b41497736/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/ti-mo/conntrack v0.0.0-20190323132511-733fb77b6071 h1:NnXFjutDwCYD2AoSFSEuUKC1H0f0Y8jeYXapxZSlg84= +github.com/ti-mo/conntrack v0.0.0-20190323132511-733fb77b6071/go.mod h1:4S0aZBgVjqDnOB7vilkmLDNHivyRGwPbDfTak5XQ0no= +github.com/ti-mo/netfilter v0.2.0 h1:mMZ70vvHTlY9y8ElWflp5nVN5kkUDvm6D1JXRgartKI= +github.com/ti-mo/netfilter v0.2.0/go.mod h1:8GbBGsY/8fxtyIdfwy29JiluNcPK4K7wIT+x42ipqUU= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= +golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20181212120007-b05ddf57801d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190410235845-0ad05ae3009d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190415081028-16da32be82c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug= +golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190516110030-61b9204099cb h1:k07iPOt0d6nEnwXF+kHB+iEg+WSuKe/SOQuFM2QoD+E= +golang.org/x/sys v0.0.0-20190516110030-61b9204099cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/gonat.go b/gonat.go new file mode 100644 index 0000000..2e04a44 --- /dev/null +++ b/gonat.go @@ -0,0 +1,184 @@ +package gonat + +import ( + "net" + "time" + + "github.com/getlantern/errors" + "github.com/getlantern/golog" + "github.com/oxtoacart/bpool" +) + +const ( + // DefaultBufferPoolSize is 10 MB + DefaultBufferPoolSize = 10000000 + + // DefaultBufferDepth is 250 packets + DefaultBufferDepth = 250 + + // DefaultIdleTimeout is 65 seconds + DefaultIdleTimeout = 65 * time.Second + + // DefaultStatsInterval is 15 seconds + DefaultStatsInterval = 15 * time.Second + + // MinConntrackTimeout sets a lower bound on how long we'll let conntrack entries persist + MinConntrackTimeout = 1 * time.Minute + + // MaximumIPPacketSize is 65535 bytes + MaximumIPPacketSize = 65535 +) + +const ( + minEphemeralPort = 32768 + maxEphemeralPort = 61000 // consistent with most Linux kernels + numEphemeralPorts = maxEphemeralPort - minEphemeralPort +) + +var ( + log = golog.LoggerFor("gonat") +) + +type Server interface { + // Serve starts processing packets and blocks until finished + Serve() error + + // Count of accepted packets + AcceptedPackets() int + + // Count of invalid packets (unknown destination, wrong IP version, etc.) + InvalidPackets() int + + // Count of packets dropped due to being stalled writing down or upstream, being unable to assign a port + // open a connection, etc. + DroppedPackets() int + + // Number of TCP connections being tracked + NumTCPConns() int + + // Number of UDP connections being tracked + NumUDPConns() int + + // Close stops the server and cleans up resources + Close() error +} + +type Opts struct { + // IFName is the name of the interface to use for connecting upstream. + // If not specified, this will use the default interface for reaching the + // Internet. + IFName string + + // IFAddr is the address to use for outbound packets. Overrides the IFName + // when specified. + IFAddr string + + // BufferPool is a pool for buffers. If not provided, default to a 10MB pool. + // Each []byte in the buffer pool should be bytes. + BufferPool BufferPool + + // BufferDepth specifies the number of outbound packets to buffer between + // stages in the send/receive pipeline. The default is . + BufferDepth int + + // IdleTimeout specifies the amount of time before idle connections are + // automatically closed. The default is . + IdleTimeout time.Duration + + // StatsInterval controls how frequently to display stats. Defaults to + // . + StatsInterval time.Duration + + // OnOutbound allows modifying outbound ip packets. + OnOutbound func(pkt *IPPacket) + + // OnInbound allows modifying inbound ip packets. ft is the 5 tuple to + // which the current connection/UDP port mapping is keyed. + OnInbound func(pkt *IPPacket, downFT FiveTuple) +} + +// ApplyDefaults applies the default values to the given Opts, including making +// a new Opts if opts is nil. +func (opts *Opts) ApplyDefaults() error { + if opts == nil { + opts = &Opts{} + } + if opts.BufferPool == nil { + opts.BufferPool = NewBufferPool(DefaultBufferPoolSize) + } + if opts.BufferDepth <= 0 { + opts.BufferDepth = DefaultBufferDepth + } + if opts.IdleTimeout <= 0 { + opts.IdleTimeout = DefaultIdleTimeout + } + if opts.StatsInterval <= 0 { + opts.StatsInterval = DefaultStatsInterval + } + if opts.OnOutbound == nil { + opts.OnOutbound = func(pkt *IPPacket) {} + } + if opts.OnInbound == nil { + opts.OnInbound = func(pkt *IPPacket, downFT FiveTuple) {} + } + if opts.IFAddr == "" { + var err error + if opts.IFName != "" { + opts.IFAddr, err = firstIPv4AddrFor(opts.IFName) + } else { + opts.IFAddr, err = findDefaultIPv4Addr() + } + if err != nil { + return err + } + } + return nil +} + +func firstIPv4AddrFor(ifName string) (string, error) { + outIF, err := net.InterfaceByName(ifName) + if err != nil { + return "", errors.New("Unable to find interface for interface %v: %v", ifName, err) + } + outIFAddrs, err := outIF.Addrs() + if err != nil { + return "", errors.New("Unable to get addresses for interface %v: %v", ifName, err) + } + for _, outIFAddr := range outIFAddrs { + switch t := outIFAddr.(type) { + case *net.IPNet: + ipv4 := t.IP.To4() + if ipv4 != nil { + return ipv4.String(), nil + } + } + } + return "", errors.New("Unable to find IPv4 address for interface %v", ifName) +} + +// findDefaultIPv4Addr find the default IPv4 address through which the Internet can be reached. +func findDefaultIPv4Addr() (string, error) { + // try to find default interface by dialing an external connection + conn, err := net.Dial("udp4", "lantern.io:80") + if err != nil { + return "", errors.New("Unable to dial lantern.io: %v", err) + } + ip := conn.LocalAddr().(*net.UDPAddr).IP.String() + return ip, nil +} + +// NewBufferPool creates a buffer pool with the given sizeInBytes containing slices +// sized to accomodate our MaximumIPPacketSize. +func NewBufferPool(sizeInBytes int) BufferPool { + return bpool.NewBytePool(sizeInBytes, MaximumIPPacketSize) +} + +// BufferPool is a bool of byte slices +type BufferPool interface { + // Get gets a byte slice from the pool + Get() []byte + // Put returns a byte slice to the pool + Put([]byte) + // NumPooled returns the number of currently pooled items + NumPooled() int +} diff --git a/gonat_linux.go b/gonat_linux.go new file mode 100644 index 0000000..74d423d --- /dev/null +++ b/gonat_linux.go @@ -0,0 +1,353 @@ +package gonat + +import ( + "io" + "math/rand" + "net" + "syscall" + "time" + + "github.com/getlantern/errors" + "github.com/getlantern/ops" + "github.com/ti-mo/conntrack" +) + +type server struct { + acceptedPackets int64 + invalidPackets int64 + droppedPackets int64 + numTCPConns int64 + numUDPConns int64 + + tcpSocket io.ReadWriteCloser + udpSocket io.ReadWriteCloser + downstream io.ReadWriter + opts *Opts + bufferPool BufferPool + ctrack *conntrack.Conn + ctTimeout uint32 + randomPortSequence []uint16 + portIndexes map[uint8]map[Addr]int + connsByDownFT map[FiveTuple]*conn + connsByUpFT map[FiveTuple]*conn + fromDownstream chan *IPPacket + toDownstream chan *IPPacket + fromUpstream chan *IPPacket + closedConns chan *conn + close chan interface{} + closed chan interface{} +} + +// NewServer constructs a new Server that reads packets from downstream +// and writes response packets back to downstream. +func NewServer(downstream io.ReadWriter, opts *Opts) (Server, error) { + err := opts.ApplyDefaults() + if err != nil { + return nil, errors.New("Error applying default options: %v", err) + } + + log.Debugf("Outbound packets will use %v", opts.IFAddr) + + _ctTimeout := opts.IdleTimeout * 2 + if _ctTimeout < MinConntrackTimeout { + _ctTimeout = MinConntrackTimeout + } + ctTimeout := uint32(_ctTimeout.Seconds()) + + // We create a random order for assigning new ports to minimize the chance of colliding + // with other running gonat instances. + randomPortSequence := make([]uint16, numEphemeralPorts) + for i := uint16(0); i < uint16(numEphemeralPorts); i++ { + randomPortSequence[i] = minEphemeralPort + i + } + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + rnd.Shuffle(numEphemeralPorts, func(i int, j int) { + randomPortSequence[i], randomPortSequence[j] = randomPortSequence[j], randomPortSequence[i] + }) + + s := &server{ + downstream: downstream, + opts: opts, + bufferPool: opts.BufferPool, + ctTimeout: ctTimeout, + randomPortSequence: randomPortSequence, + portIndexes: make(map[uint8]map[Addr]int), + connsByDownFT: make(map[FiveTuple]*conn), + connsByUpFT: make(map[FiveTuple]*conn), + fromDownstream: make(chan *IPPacket, opts.BufferDepth), + toDownstream: make(chan *IPPacket, opts.BufferDepth), + fromUpstream: make(chan *IPPacket, opts.BufferDepth), + closedConns: make(chan *conn, opts.BufferDepth), + close: make(chan interface{}), + closed: make(chan interface{}), + } + return s, nil +} + +func (s *server) Serve() error { + var err error + s.tcpSocket, err = createSocket(FiveTuple{IPProto: syscall.IPPROTO_TCP, Src: Addr{s.opts.IFAddr, 0}}) + if err != nil { + return err + } + ops.Go(func() { s.readFromUpstream(s.tcpSocket) }) + + s.udpSocket, err = createSocket(FiveTuple{IPProto: syscall.IPPROTO_UDP, Src: Addr{s.opts.IFAddr, 0}}) + if err != nil { + s.tcpSocket.Close() + return err + } + ops.Go(func() { s.readFromUpstream(s.udpSocket) }) + + s.ctrack, err = conntrack.Dial(nil) + if err != nil { + s.tcpSocket.Close() + s.udpSocket.Close() + return errors.New("Unable to obtain connection for managing conntrack: %v", err) + } + + ops.Go(s.trackStats) + ops.Go(s.dispatch) + ops.Go(s.writeToDownstream) + return s.readFromDownstream() +} + +func (s *server) dispatch() { + defer func() { + for _, c := range s.connsByDownFT { + c.Close() + s.deleteConn(c) + s.deleteConntrackEntry(c.upFT) + } + close(s.toDownstream) + s.tcpSocket.Close() + s.udpSocket.Close() + s.ctrack.Close() + close(s.closed) + }() + + reapTicker := time.NewTicker(1 * time.Second) + defer reapTicker.Stop() + + for { + select { + case pkt := <-s.fromDownstream: + s.onPacketFromDownstream(pkt) + case pkt := <-s.fromUpstream: + s.onPacketFromUpstream(pkt) + case c := <-s.closedConns: + s.deleteConntrackEntry(c.upFT) + case <-reapTicker.C: + s.reapIdleConns() + case <-s.close: + return + } + } +} + +func (s *server) onPacketFromDownstream(pkt *IPPacket) { + switch pkt.IPProto { + case syscall.IPPROTO_TCP, syscall.IPPROTO_UDP: + s.opts.OnOutbound(pkt) + downFT := pkt.FT() + c := s.connsByDownFT[downFT] + + if pkt.HasTCPFlag(TCPFlagRST) { + if c != nil { + c.Close() + } + return + } + + if c == nil { + upFT, err := s.assignPort(downFT) + if err != nil { + log.Errorf("Unable to assign port, dropping packet %v: %v", downFT, err) + s.dropPacket(pkt) + return + } + c, err = s.newConn(downFT, upFT) + if err != nil { + log.Errorf("Unable to create connection, dropping packet %v: %v", downFT, err) + s.dropPacket(pkt) + return + } + s.connsByDownFT[downFT] = c + s.connsByUpFT[upFT] = c + c.markActive() + s.addConn(pkt.IPProto) + } + select { + case c.toUpstream <- pkt: + log.Tracef("Transmit -- %v -> %v", c.downFT, c.upFT) + s.acceptedPacket() + default: + // don't block if we're stalled writing upstream + log.Tracef("Stalled writing packet %v upstream", downFT) + s.dropPacket(pkt) + } + default: + log.Tracef("Unknown IP protocol, ignoring packet %v: %v", pkt.FT(), pkt.IPProto) + s.rejectPacket(pkt.Raw) + } +} + +func (s *server) onPacketFromUpstream(pkt *IPPacket) { + upFT := pkt.FT().Reversed() + c := s.connsByUpFT[upFT] + if c == nil { + log.Tracef("Ignoring packet for unknown upstream FT: %v", upFT) + s.rejectPacket(pkt.Raw) + return + } + + pkt.SetDest(c.downFT.Src) + s.opts.OnInbound(pkt, c.downFT) + pkt.recalcChecksum() + c.markActive() + select { + case s.toDownstream <- pkt: + // okay + log.Tracef("Transmit -- %v <- %v", c.downFT, c.upFT) + s.acceptedPacket() + default: + log.Tracef("Stalled writing packet %v downstream", c.downFT) + s.dropPacket(pkt) + } +} + +// assignPort assigns an ephemeral local port for a new connection. If an existing connection +// with the resulting 5-tuple is already tracked because a different application created it, +// this will fail on createConntrackEntry and then retry until it finds an untracked ephemeral +// port or runs out of ports to try. +func (s *server) assignPort(downFT FiveTuple) (upFT FiveTuple, err error) { + portIndexesByOrigin := s.portIndexes[upFT.IPProto] + if portIndexesByOrigin == nil { + portIndexesByOrigin = make(map[Addr]int) + s.portIndexes[upFT.IPProto] = portIndexesByOrigin + } + + upFT.IPProto = downFT.IPProto + upFT.Dst = downFT.Dst + upFT.Src.IPString = s.opts.IFAddr + + for i := 0; i < numEphemeralPorts; i++ { + portIndex := portIndexesByOrigin[downFT.Dst] + 1 + if portIndex >= numEphemeralPorts { + // loop back around to beginning of random sequence + portIndex = 0 + } + portIndexesByOrigin[upFT.Dst] = portIndex + upFT.Src.Port = s.randomPortSequence[portIndex] + err = s.createConntrackEntry(upFT) + if err != nil { + // this can happen if this 5-tuple is already tracked, ignore and retry + continue + } + return + } + err = errors.New("Gave up looking for ephemeral port, final error from conntrack: %v", err) + return +} + +func (s *server) reapIdleConns() { + var connsToClose []*conn + for _, c := range s.connsByDownFT { + if c.timeSinceLastActive() > s.opts.IdleTimeout { + connsToClose = append(connsToClose, c) + s.deleteConn(c) + } + } + if len(connsToClose) > 0 { + // close conns on a goroutine to avoid tying up main dispatch loop + ops.Go(func() { + for _, c := range connsToClose { + c.Close() + } + }) + } +} + +func (s *server) deleteConn(c *conn) { + delete(s.connsByDownFT, c.downFT) + delete(s.connsByUpFT, c.upFT) +} + +// readFromDownstream reads all IP packets from downstream clients. +func (s *server) readFromDownstream() error { + defer s.Close() + + for { + b := s.bufferPool.Get() + n, err := s.downstream.Read(b) + if err != nil { + if err == io.EOF { + return err + } + return errors.New("Unexpected error reading from downstream: %v", err) + } + raw := b[:n] + pkt, err := parseIPPacket(raw) + if err != nil { + log.Tracef("Error on inbound packet, ignoring: %v", err) + s.rejectPacket(raw) + continue + } + s.fromDownstream <- pkt + } +} + +// writeToDownstream writes all IP packets that we're sending back dowstream. +func (s *server) writeToDownstream() { + for pkt := range s.toDownstream { + _, err := s.downstream.Write(pkt.Raw) + s.bufferPool.Put(pkt.Raw) + if err != nil { + log.Errorf("Unexpected error writing to downstream: %v", err) + return + } + } +} + +func (s *server) readFromUpstream(socket io.ReadWriteCloser) { + defer socket.Close() + + for { + b := s.bufferPool.Get() + n, err := socket.Read(b) + if err != nil { + s.rejectPacket(b) + if netErr, ok := err.(net.Error); ok && netErr.Temporary() { + continue + } + return + } + if pkt, err := parseIPPacket(b[:n]); err != nil { + log.Tracef("Ignoring unparseable packet from upstream: %v", err) + s.rejectPacket(b) + } else { + s.fromUpstream <- pkt + } + } +} + +func (s *server) rejectPacket(b []byte) { + s.invalidPacket() + s.bufferPool.Put(b) +} + +func (s *server) dropPacket(pkt *IPPacket) { + s.droppedPacket() + s.bufferPool.Put(pkt.Raw) +} + +func (s *server) Close() error { + select { + case <-s.close: + // already closed + default: + close(s.close) + } + <-s.closed + return nil +} diff --git a/gonat_linux_test.go b/gonat_linux_test.go new file mode 100644 index 0000000..253dea5 --- /dev/null +++ b/gonat_linux_test.go @@ -0,0 +1,42 @@ +// +build linux + +package gonat + +import ( + "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const ( + tunGW = "10.0.0.9" +) + +// Note - this test has to be run with root permissions to allow setting up the +// TUN device. +func TestEndToEnd(t *testing.T) { + RunTest(t, "tun0", "10.0.0.10", tunGW, "255.255.255.0", 1500, func(ifAddr string, dev io.ReadWriter, origEchoAddr Addr, finishedCh chan interface{}) (func() error, error) { + s, err := NewServer(dev, &Opts{ + StatsInterval: 250 * time.Millisecond, + OnOutbound: func(pkt *IPPacket) { + pkt.SetDest(origEchoAddr) + }, + OnInbound: func(pkt *IPPacket, downFT FiveTuple) { + pkt.SetSource(Addr{tunGW, downFT.Dst.Port}) + }, + }) + if err != nil { + return nil, err + } + + go func() { + assert.Equal(t, io.EOF, s.Serve()) + _s := s.(*server) + assert.True(t, _s.bufferPool.NumPooled() > 0, "buffers should be returned to pool") + close(finishedCh) + }() + return func() error { return nil }, nil + }) +} diff --git a/packet.go b/packet.go new file mode 100644 index 0000000..e332740 --- /dev/null +++ b/packet.go @@ -0,0 +1,217 @@ +// Checksum computations based on logic in github.com/google/gopacket +// +// Copyright 2012 Google, Inc. All rights reserved. +// Copyright 2009-2011 Andreas Krennmair. All rights reserved. + +package gonat + +import ( + "encoding/binary" + "fmt" + "net" + "syscall" + + "github.com/getlantern/errors" +) + +// TCPFlags are the different flags supported in the TCP header +const ( + TCPFlagRST = 0x04 +) + +var ( + networkByteOrder = binary.BigEndian +) + +func parseIPPacket(raw []byte) (*IPPacket, error) { + ipVersion := uint8(raw[0]) >> 4 + if ipVersion != 4 { + return nil, errors.New("Unsupported ip protocol version: %v", ipVersion) + } + + pkt := &IPPacket{Raw: raw, IPVersion: ipVersion} + return pkt.parseV4() +} + +type IPPacket struct { + Raw []byte + IPVersion uint8 + IPProto uint8 + SrcAddr *net.IPAddr + DstAddr *net.IPAddr + Header []byte + Payload []byte +} + +func (pkt *IPPacket) parseV4() (*IPPacket, error) { + ihl := uint8(pkt.Raw[0]) & 0x0F + length := networkByteOrder.Uint16(pkt.Raw[2:4]) + if length < 20 { + return pkt, errors.New("Invalid (too small) IP length (%d < 20)", length) + } else if ihl < 5 { + return pkt, errors.New("Invalid (too small) IP header length (%d < 5)", ihl) + } else if int(ihl*4) > int(length) { + return pkt, errors.New("Invalid IP header length > IP length (%d > %d)", ihl, length) + } else if int(ihl)*4 > len(pkt.Raw) { + return pkt, errors.New("Not all IP header bytes available") + } + + pkt.Header = pkt.Raw[:ihl*4] + pkt.Payload = pkt.Raw[ihl*4:] + pkt.IPProto = uint8(pkt.Header[9]) + pkt.SrcAddr = &net.IPAddr{IP: net.IP(pkt.Header[12:16])} + pkt.DstAddr = &net.IPAddr{IP: net.IP(pkt.Header[16:20])} + + return pkt, nil +} + +// HasTCPFlag returns true if the packet is a TCP packet that has the given flag set. +func (pkt *IPPacket) HasTCPFlag(flag uint8) bool { + return pkt.IPProto == syscall.IPPROTO_TCP && pkt.Payload[13]&flag != 0 +} + +func (pkt *IPPacket) SetSource(addr Addr) { + ip := addr.IP() + copy(pkt.Header[12:16], ip) + networkByteOrder.PutUint16(pkt.Payload[0:2], addr.Port) + pkt.SrcAddr = &net.IPAddr{IP: ip} +} + +func (pkt *IPPacket) SetDest(addr Addr) { + ip := addr.IP() + copy(pkt.Header[16:20], ip) + networkByteOrder.PutUint16(pkt.Payload[2:4], addr.Port) + pkt.DstAddr = &net.IPAddr{IP: ip} +} + +func (pkt *IPPacket) ipChecksum() uint16 { + return networkByteOrder.Uint16(pkt.Header[10:]) +} + +func (pkt *IPPacket) recalcIPChecksum() { + // Clear checksum bytes + pkt.Header[10] = 0 + pkt.Header[11] = 0 + + // Compute checksum + var csum uint32 + for i := 0; i < len(pkt.Header); i += 2 { + csum += uint32(pkt.Header[i]) << 8 + csum += uint32(pkt.Header[i+1]) + } + for { + // Break when sum is less or equals to 0xFFFF + if csum <= 65535 { + break + } + // Add carry to the sum + csum = (csum >> 16) + uint32(uint16(csum)) + } + // Flip all the bits + networkByteOrder.PutUint16(pkt.Header[10:], ^uint16(csum)) +} + +func (pkt *IPPacket) tcpChecksum() uint16 { + return networkByteOrder.Uint16(pkt.Payload[16:]) +} + +func (pkt *IPPacket) udpChecksum() uint16 { + return networkByteOrder.Uint16(pkt.Payload[6:]) +} + +func (pkt *IPPacket) recalcChecksum() { + switch pkt.IPProto { + case syscall.IPPROTO_TCP: + pkt.recalcTCPChecksum() + case syscall.IPPROTO_UDP: + pkt.recalcUDPChecksum() + } + pkt.recalcIPChecksum() +} + +func (pkt *IPPacket) recalcTCPChecksum() { + pkt.recalcTransportChecksum(16) +} + +func (pkt *IPPacket) recalcUDPChecksum() { + pkt.recalcTransportChecksum(6) +} + +func (pkt *IPPacket) recalcTransportChecksum(csumIdx int) { + // Clear checksum bytes + pkt.Payload[csumIdx] = 0 + pkt.Payload[csumIdx+1] = 0 + + csum := pkt.calcIPPseudoHeaderChecksum() + + // to handle odd lengths, we loop to length - 1, incrementing by 2, then + // handle the last byte specifically by checking against the original + // length. + length := len(pkt.Payload) - 1 + for i := 0; i < length; i += 2 { + // For our test packet, doing this manually is about 25% faster + // (740 ns vs. 1000ns) than doing it by calling binary.BigEndian.Uint16. + csum += uint32(pkt.Payload[i]) << 8 + csum += uint32(pkt.Payload[i+1]) + } + if len(pkt.Payload)%2 == 1 { + csum += uint32(pkt.Payload[length]) << 8 + } + for csum > 0xffff { + csum = (csum >> 16) + (csum & 0xffff) + } + networkByteOrder.PutUint16(pkt.Payload[csumIdx:], ^uint16(csum)) +} + +func (pkt *IPPacket) calcIPPseudoHeaderChecksum() (csum uint32) { + csum += (uint32(pkt.Header[12]) + uint32(pkt.Header[14])) << 8 + csum += uint32(pkt.Header[13]) + uint32(pkt.Header[15]) + csum += (uint32(pkt.Header[16]) + uint32(pkt.Header[18])) << 8 + csum += uint32(pkt.Header[17]) + uint32(pkt.Header[19]) + + length := uint32(len(pkt.Payload)) + csum += uint32(pkt.IPProto) + csum += length & 0xffff + csum += length >> 16 + + return csum +} + +func (pkt *IPPacket) FT() FiveTuple { + return FiveTuple{ + IPProto: pkt.IPProto, + Src: Addr{IPString: pkt.SrcAddr.String(), Port: networkByteOrder.Uint16(pkt.Payload[0:2])}, + Dst: Addr{IPString: pkt.DstAddr.String(), Port: networkByteOrder.Uint16(pkt.Payload[2:4])}, + } +} + +type FiveTuple struct { + IPProto uint8 + Src Addr + Dst Addr +} + +func (ft FiveTuple) Reversed() FiveTuple { + return FiveTuple{ + IPProto: ft.IPProto, + Src: ft.Dst, + Dst: ft.Src, + } +} + +func (ft FiveTuple) String() string { + return fmt.Sprintf("[%d] %v -> %v", ft.IPProto, ft.Src, ft.Dst) +} + +type Addr struct { + IPString string + Port uint16 +} + +func (a Addr) String() string { + return fmt.Sprintf("%v:%d", a.IPString, a.Port) +} + +func (a Addr) IP() net.IP { + return net.ParseIP(a.IPString).To4() +} diff --git a/packet_test.go b/packet_test.go new file mode 100644 index 0000000..ee42d5b --- /dev/null +++ b/packet_test.go @@ -0,0 +1,91 @@ +package gonat + +import ( + "encoding/hex" + "strings" + "testing" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/stretchr/testify/assert" +) + +func TestTCP(t *testing.T) { + raw, err := hex.DecodeString("45000088dc9340004006e6c0c0a801e650f96394a8cc0050eeba8cabdde4fcaa801800e5779600000101080a5f691ad1a5fab79d474554202f3147422e7a697020485454502f312e310d0a486f73743a2038302e3234392e39392e3134380d0a557365722d4167656e743a206375726c2f372e35382e300d0a4163636570743a202a2f2a0d0a0d0a") + if !assert.NoError(t, err) { + return + } + + pkt, err := parseIPPacket(raw) + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, "192.168.1.230:43212", pkt.FT().Src.String()) + assert.Equal(t, "80.249.99.148:80", pkt.FT().Dst.String()) + + pkt.recalcIPChecksum() + pkt.recalcTCPChecksum() + expectedIPChecksum, expectedTCPChecksum := checksumsViaGoPacket(raw) + assert.Equal(t, expectedIPChecksum, pkt.ipChecksum()) + assert.Equal(t, expectedTCPChecksum, pkt.tcpChecksum()) +} + +func TestHasRST(t *testing.T) { + withRST, err := hex.DecodeString("450000280000400040063cce7f0000017f0000012710cc98000000003e4e28c15014000057160000") + if !assert.NoError(t, err) { + return + } + pkt, err := parseIPPacket(withRST) + if !assert.NoError(t, err) { + return + } + assert.True(t, pkt.HasTCPFlag(TCPFlagRST)) + + withoutRST, err := hex.DecodeString(strings.Replace("45 00 00 48 4B 3E 40 00 40 06 DB 6F 0A 00 00 02 50 F9 63 94 CE AA 00 50 58 38 DD 0B 4A 59 0E F1 D0 10 60 00 3A EB 00 00 01 01 08 0A 98 0D 1B AA AF 42 43 CD 01 01 05 12 4A 5A FB 09 4A 5B 00 B1 4A 59 6F 19 4A 5A D9 19", " ", "", -1)) + if !assert.NoError(t, err) { + return + } + pkt, err = parseIPPacket(withoutRST) + if !assert.NoError(t, err) { + return + } + assert.False(t, pkt.HasTCPFlag(TCPFlagRST)) +} + +// for some reason, the TCP checksum in the test data doesn't match what's calculated by RFC 793, +// so we round-trip through gopacket to calculate the expected TCP checksum +func checksumsViaGoPacket(data []byte) (uint16, uint16) { + packet, ip, tcp := gopacketLayers(data) + if ip == nil || tcp == nil { + return 0, 0 + } + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + ComputeChecksums: true, + } + tcp.SetNetworkLayerForChecksum(ip) + err := gopacket.SerializePacket(buf, opts, packet) + if err != nil { + log.Error(err) + return 0, 0 + } + _, ip, tcp = gopacketLayers(buf.Bytes()) + if ip == nil || tcp == nil { + return 0, 0 + } + return ip.Checksum, tcp.Checksum +} + +func gopacketLayers(data []byte) (gopacket.Packet, *layers.IPv4, *layers.TCP) { + packet := gopacket.NewPacket(data, layers.LayerTypeIPv4, gopacket.Default) + if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer != nil { + ip, _ := ipLayer.(*layers.IPv4) + if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil { + tcp, _ := tcpLayer.(*layers.TCP) + return packet, ip, tcp + } + } + + return nil, nil, nil +} diff --git a/stats_linux.go b/stats_linux.go new file mode 100644 index 0000000..6f8f9a3 --- /dev/null +++ b/stats_linux.go @@ -0,0 +1,72 @@ +package gonat + +import ( + "sync/atomic" + "syscall" + "time" +) + +func (s *server) trackStats() { + ticker := time.NewTicker(s.opts.StatsInterval) + defer ticker.Stop() + + for { + select { + case <-s.close: + return + case <-ticker.C: + log.Debugf("TCP Conns: %v UDP Conns: %v", s.NumTCPConns(), s.NumUDPConns()) + log.Debugf("Invalid Packets: %d Accepted Packets: %d Dropped Packets: %d", s.InvalidPackets(), s.AcceptedPackets(), s.DroppedPackets()) + } + } +} + +func (s *server) acceptedPacket() { + atomic.AddInt64(&s.acceptedPackets, 1) +} + +func (s *server) AcceptedPackets() int { + return int(atomic.LoadInt64(&s.acceptedPackets)) +} + +func (s *server) invalidPacket() { + atomic.AddInt64(&s.invalidPackets, 1) +} + +func (s *server) InvalidPackets() int { + return int(atomic.LoadInt64(&s.invalidPackets)) +} + +func (s *server) droppedPacket() { + atomic.AddInt64(&s.droppedPackets, 1) +} + +func (s *server) DroppedPackets() int { + return int(atomic.LoadInt64(&s.droppedPackets)) +} + +func (s *server) addConn(proto uint8) { + switch proto { + case syscall.IPPROTO_TCP: + atomic.AddInt64(&s.numTCPConns, 1) + case syscall.IPPROTO_UDP: + atomic.AddInt64(&s.numUDPConns, 1) + } +} + +func (s *server) removeConn(proto uint8) { + switch proto { + case syscall.IPPROTO_TCP: + atomic.AddInt64(&s.numTCPConns, -1) + case syscall.IPPROTO_UDP: + atomic.AddInt64(&s.numUDPConns, -1) + } +} + +func (s *server) NumTCPConns() int { + return int(atomic.LoadInt64(&s.numTCPConns)) +} + +func (s *server) NumUDPConns() int { + return int(atomic.LoadInt64(&s.numUDPConns)) +} diff --git a/test_helper_linux.go b/test_helper_linux.go new file mode 100644 index 0000000..dfb9a0d --- /dev/null +++ b/test_helper_linux.go @@ -0,0 +1,220 @@ +package gonat + +import ( + "io" + "net" + "strconv" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/getlantern/fdcount" + tun "github.com/getlantern/gotun" + "github.com/getlantern/grtrack" + + "github.com/stretchr/testify/assert" +) + +var ( + serverTCPConnections int64 +) + +// Note - this test has to be run with root permissions to allow setting up the +// TUN device. +func RunTest(t *testing.T, tunDeviceName, tunAddr, tunGW, tunMask string, mtu int, doTest func(ifAddr string, dev io.ReadWriter, origEchoAddr Addr, finishedCh chan interface{}) (func() error, error)) { + _, tcpConnCount, err := fdcount.Matching("TCP") + if !assert.NoError(t, err, "unable to get initial TCP socket count") { + return + } + _, udpConnCount, err := fdcount.Matching("UDP") + if !assert.NoError(t, err, "unable to get initial UDP socket count") { + return + } + _, rawConnCount, err := fdcount.Matching("raw") + if !assert.NoError(t, err, "unable to get initial raw socket count") { + return + } + + // Open a TUN device + dev, err := tun.OpenTunDevice(tunDeviceName, tunAddr, tunGW, tunMask, mtu) + if err != nil { + if err != nil { + if strings.HasSuffix(err.Error(), "operation not permitted") { + t.Log("This test requires root access. Compile, then run with root privileges. See the README for more details.") + } + t.Fatal(err) + } + } + // for some reason, on some Linux installs, reading hangs even after the device is closed. + // withTimeout is a hack that allows to return an EOF to the reader before our test ends. + dev = withTimeout(dev, 5*time.Second) + + grtracker := grtrack.Start() + + opts := &Opts{} + if err := opts.ApplyDefaults(); !assert.NoError(t, err) { + return + } + + // Start echo servers + closeCh := make(chan interface{}) + echoAddr := tcpEcho(t, closeCh, opts.IFAddr) + udpEcho(t, closeCh, echoAddr) + host, _port, _ := net.SplitHostPort(echoAddr) + port, _ := strconv.Atoi(_port) + origEchoAddr := Addr{host, uint16(port)} + echoAddr = tunGW + ":" + _port + + finishedCh := make(chan interface{}) + beforeClose, err := doTest(opts.IFAddr, dev, origEchoAddr, finishedCh) + if !assert.NoError(t, err) { + return + } + + b := make([]byte, 8) + log.Debugf("Dialing echo server with UDP at: %v", echoAddr) + uconn, err := net.Dial("udp", echoAddr) + if !assert.NoError(t, err, "Unable to get UDP connection to TUN device") { + return + } + + _, err = uconn.Write([]byte("helloudp")) + if !assert.NoError(t, err) { + return + } + + _, err = io.ReadFull(uconn, b) + if !assert.NoError(t, err) { + return + } + uconn.Close() + + log.Debugf("Dialing echo server with TCP at: %v", echoAddr) + conn, err := net.DialTimeout("tcp", echoAddr, 5*time.Second) + if !assert.NoError(t, err) { + return + } + + _, err = conn.Write([]byte("hellotcp")) + if !assert.NoError(t, err) { + return + } + + _, err = io.ReadFull(conn, b) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, "hellotcp", string(b)) + conn.Close() + time.Sleep(50 * time.Millisecond) + assert.Zero(t, atomic.LoadInt64(&serverTCPConnections), "Server-side TCP connection should have been closed") + + close(closeCh) + beforeClose() + if err := dev.Close(); !assert.NoError(t, err) { + return + } + + select { + case <-finishedCh: + tcpConnCount.AssertDelta(0) + udpConnCount.AssertDelta(0) + rawConnCount.AssertDelta(0) + time.Sleep(2 * time.Second) + grtracker.Check(t) + case <-time.After(15 * time.Second): + t.Error("gonat failed to terminate in a reasonable amount of time") + } +} + +func tcpEcho(t *testing.T, closeCh <-chan interface{}, ip string) string { + l, err := net.Listen("tcp", ip+":0") + if err != nil { + t.Fatal(err) + } + go func() { + <-closeCh + l.Close() + }() + log.Debugf("TCP echo server listening at: %v", l.Addr()) + + go func() { + for { + conn, err := l.Accept() + if err != nil { + return + } + atomic.AddInt64(&serverTCPConnections, 1) + go func() { + io.Copy(conn, conn) + conn.Close() + atomic.AddInt64(&serverTCPConnections, -1) + }() + } + }() + + return l.Addr().String() +} + +func udpEcho(t *testing.T, closeCh <-chan interface{}, echoAddr string) { + conn, err := net.ListenPacket("udp", echoAddr) + if err != nil { + t.Fatal(err) + } + go func() { + <-closeCh + conn.Close() + }() + log.Debugf("UDP echo server listening at: %v", conn.LocalAddr()) + + go func() { + b := make([]byte, 20480) + for { + n, addr, err := conn.ReadFrom(b) + if err != nil { + return + } + conn.WriteTo(b[:n], addr) + } + }() +} + +type read struct { + b []byte + err error +} + +func withTimeout(dev io.ReadWriteCloser, timeout time.Duration) io.ReadWriteCloser { + result := &timingOutReader{ + ReadWriteCloser: dev, + timeout: timeout, + reads: make(chan *read), + } + go result.process() + return result +} + +type timingOutReader struct { + io.ReadWriteCloser + timeout time.Duration + reads chan *read +} + +func (r *timingOutReader) process() { + for { + b := make([]byte, MaximumIPPacketSize) + n, err := r.ReadWriteCloser.Read(b) + r.reads <- &read{b[:n], err} + } +} + +func (r *timingOutReader) Read(b []byte) (n int, err error) { + select { + case read := <-r.reads: + copy(b, read.b) + return len(read.b), read.err + case <-time.After(r.timeout): + return 0, io.EOF + } +} diff --git a/transport_linux.go b/transport_linux.go new file mode 100644 index 0000000..475821d --- /dev/null +++ b/transport_linux.go @@ -0,0 +1,122 @@ +package gonat + +import ( + "fmt" + "io" + "os" + "sync/atomic" + "syscall" + "time" + + "github.com/getlantern/errors" + "github.com/getlantern/ops" +) + +// newConn creates a connection built around a raw socket for either TCP or UDP +// (depending no the specified proto). Being a raw socket, it allows us to send our +// own IP packets. +func (s *server) newConn(downFT FiveTuple, upFT FiveTuple) (*conn, error) { + socket, err := createSocket(upFT) + if err != nil { + return nil, err + } + c := &conn{ + ReadWriteCloser: socket, + downFT: downFT, + upFT: upFT, + toUpstream: make(chan *IPPacket, s.opts.BufferDepth), + s: s, + close: make(chan interface{}), + } + ops.Go(c.writeToUpstream) + return c, nil +} + +func createSocket(upFT FiveTuple) (io.ReadWriteCloser, error) { + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, int(upFT.IPProto)) + if err != nil { + return nil, errors.New("Unable to create transport: %v", err) + } + if err := syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1); err != nil { + syscall.Close(fd) + return nil, errors.New("Unable to set IP_HDRINCL: %v", err) + } + bindAddr := sockAddrFor(upFT.Src) + if err := syscall.Bind(fd, bindAddr); err != nil { + syscall.Close(fd) + return nil, errors.New("Unable to bind raw socket: %v", err) + } + if upFT.Dst.Port > 0 { + connectAddr := sockAddrFor(upFT.Dst) + if err := syscall.Connect(fd, connectAddr); err != nil { + syscall.Close(fd) + return nil, errors.New("Unable to connect raw socket: %v", err) + } + } + if err := syscall.SetNonblock(fd, true); err != nil { + syscall.Close(fd) + return nil, errors.New("Unable to set raw socket to non-blocking: %v", err) + } + return os.NewFile(uintptr(fd), fmt.Sprintf("fd %d", fd)), nil +} + +func sockAddrFor(addr Addr) syscall.Sockaddr { + var ad [4]byte + copy(ad[:], addr.IP()) + return &syscall.SockaddrInet4{ + Addr: ad, + Port: int(addr.Port), + } +} + +type conn struct { + io.ReadWriteCloser + downFT FiveTuple + upFT FiveTuple + toUpstream chan *IPPacket + s *server + lastActive int64 + close chan interface{} +} + +func (c *conn) writeToUpstream() { + defer func() { + c.s.closedConns <- c + }() + defer c.ReadWriteCloser.Close() + + for { + select { + case pkt := <-c.toUpstream: + pkt.SetSource(c.upFT.Src) + pkt.recalcChecksum() + _, err := c.Write(pkt.Raw) + c.s.bufferPool.Put(pkt.Raw) + if err != nil { + log.Errorf("Error writing upstream: %v", err) + return + } + c.markActive() + case <-c.close: + return + } + } +} + +func (c *conn) markActive() { + atomic.StoreInt64(&c.lastActive, time.Now().UnixNano()) +} + +func (c *conn) timeSinceLastActive() time.Duration { + return time.Duration(time.Now().UnixNano() - atomic.LoadInt64(&c.lastActive)) +} + +func (c *conn) Close() error { + select { + case <-c.close: + return nil + default: + close(c.close) + return nil + } +} From 0160b17b618d04bf00e1b08b49e5bdc412aed667 Mon Sep 17 00:00:00 2001 From: Harry Harpham Date: Fri, 24 May 2019 01:11:46 -0500 Subject: [PATCH 2/3] Add instructions for undoing iptables rule --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9c88df9..51770ae 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ The library manually adds these to conntrack since we're using raw sockets. `sudo iptables -I OUTPUT -p tcp -m conntrack --ctstate ESTABLISHED --ctdir ORIGINAL --tcp-flags RST RST -j DROP` +To undo this, run the same command, but replace the `-I` flag with the `-D` flag. + To run the unit tests, you need to have root permissions. It's also useful to enable tracing while running the tests. ``` From 11789f26c5a310cfba1d04b8b81f16a8330c08fd Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Fri, 24 May 2019 08:14:39 -0500 Subject: [PATCH 3/3] Added coverage badge to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51770ae..7a0ef39 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -gonat [![Build Status](https://drone.lantern.io/api/badges/getlantern/gonat/status.svg)](https://drone.lantern.io/getlantern/gonat) [![GoDoc](https://godoc.org/github.com/getlantern/gonat?status.png)](http://godoc.org/github.com/getlantern/gonat) +gonat [![GoDoc](https://godoc.org/github.com/getlantern/gonat?status.png)](http://godoc.org/github.com/getlantern/gonat) [![Build Status](https://drone.lantern.io/api/badges/getlantern/gonat/status.svg)](https://drone.lantern.io/getlantern/gonat) [![Coverage Status](https://coveralls.io/repos/github/getlantern/gonat/badge.svg?branch=init)](https://coveralls.io/github/getlantern/gonat) ========== This library only works on Linux.