Skip to content

Commit

Permalink
Expose a summary of socket activity for Intra
Browse files Browse the repository at this point in the history
These metrics are chosen to match Intra's current
set of socket metrics (except for retry-related
metrics, since retry is not yet implemented in
this Handler).

Notably, this change renames one of the two tun2socks
packages (renamed to "tunnel"), and exposes bindings for
additional packages.  This appears to be necessary
because gobind can only expose types from packages that
are explicitly exported.
  • Loading branch information
Ben Schwartz committed Jun 3, 2019
1 parent 97f6f38 commit 16b35df
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 47 deletions.
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -21,7 +21,7 @@ MACOS_ARTIFACT=$(MACOS_BUILDDIR)/Tun2socks.framework
WINDOWS_BUILDDIR=$(BUILDDIR)/windows
LINUX_BUILDDIR=$(BUILDDIR)/linux

ANDROID_BUILD_CMD="GO111MODULE=off $(GOBIND) -a -ldflags $(LDFLAGS) -target=android -tags android -o $(ANDROID_ARTIFACT) $(IMPORT_PATH)/android"
ANDROID_BUILD_CMD="GO111MODULE=off $(GOBIND) -a -ldflags $(LDFLAGS) -target=android -tags android -o $(ANDROID_ARTIFACT) $(IMPORT_PATH)/android $(IMPORT_PATH)/tunnel $(IMPORT_PATH)/tunnel/intra"
IOS_BUILD_CMD="GO111MODULE=off $(GOBIND) -a -ldflags $(LDFLAGS) -bundleid org.outline.tun2socks -target=ios/arm,ios/arm64 -tags ios -o $(IOS_ARTIFACT) $(IMPORT_PATH)/apple"
MACOS_BUILD_CMD="GO111MODULE=off $(GOBIND) -a -ldflags $(LDFLAGS) -bundleid org.outline.tun2socks -target=ios/amd64 -tags ios -o $(MACOS_ARTIFACT) $(IMPORT_PATH)/apple"
WINDOWS_BUILD_CMD="$(XGOCMD) -ldflags $(XGO_LDFLAGS) -tags $(XGO_BUILD_TAGS) --targets=windows/386 -dest $(WINDOWS_BUILDDIR) $(TUN2SOCKS_SRC_PATH)/cmd/tun2socks"
Expand Down
4 changes: 2 additions & 2 deletions android/common.go
Expand Up @@ -19,7 +19,7 @@ import (
"log"
"os"

"github.com/Jigsaw-Code/outline-go-tun2socks/tun2socks"
"github.com/Jigsaw-Code/outline-go-tun2socks/tunnel"
)

const vpnMtu = 1500
Expand All @@ -35,7 +35,7 @@ func makeTunFile(fd int) (*os.File, error) {
return file, nil
}

func processInputPackets(tunnel tun2socks.Tunnel, tun *os.File) {
func processInputPackets(tunnel tunnel.Tunnel, tun *os.File) {
buffer := make([]byte, vpnMtu)
for tunnel.IsConnected() {
len, err := tun.Read(buffer)
Expand Down
8 changes: 4 additions & 4 deletions android/intra.go
Expand Up @@ -15,13 +15,13 @@
package tun2socks

import (
"github.com/Jigsaw-Code/outline-go-tun2socks/tun2socks"
"github.com/Jigsaw-Code/outline-go-tun2socks/tunnel"
)

// IntraTunnel embeds the tun2socks.Tunnel interface so it gets exported by gobind.
// Intra does not need any methods beyond the basic Tunnel interface.
type IntraTunnel interface {
tun2socks.Tunnel
tunnel.Tunnel
}

// ConnectIntraTunnel reads packets from a TUN device and applies the Intra routing
Expand All @@ -35,12 +35,12 @@ type IntraTunnel interface {
//
// Throws an exception if the TUN file descriptor cannot be opened, or if the tunnel fails to
// connect.
func ConnectIntraTunnel(fd int, fakedns, udpdns, tcpdns string) (IntraTunnel, error) {
func ConnectIntraTunnel(fd int, fakedns, udpdns, tcpdns string, listener tunnel.IntraListener) (IntraTunnel, error) {
tun, err := makeTunFile(fd)
if err != nil {
return nil, err
}
tunnel, err := tun2socks.NewIntraTunnel(fakedns, udpdns, tcpdns, tun)
tunnel, err := tunnel.NewIntraTunnel(fakedns, udpdns, tcpdns, tun, listener)
if err != nil {
return nil, err
}
Expand Down
6 changes: 3 additions & 3 deletions android/outline.go
Expand Up @@ -17,12 +17,12 @@ package tun2socks
import (
"errors"

"github.com/Jigsaw-Code/outline-go-tun2socks/tun2socks"
"github.com/Jigsaw-Code/outline-go-tun2socks/tunnel"
)

// OutlineTunnel embeds the tun2socks.OutlineTunnel interface so it gets exported by gobind.
type OutlineTunnel interface {
tun2socks.OutlineTunnel
tunnel.OutlineTunnel
}

// ConnectSocksTunnel reads packets from a TUN device and routes it to a SOCKS server. Returns an
Expand All @@ -44,7 +44,7 @@ func ConnectSocksTunnel(fd int, host string, port int, isUDPEnabled bool) (Outli
if err != nil {
return nil, err
}
tunnel, err := tun2socks.NewTunnel(host, uint16(port), isUDPEnabled, tun)
tunnel, err := tunnel.NewTunnel(host, uint16(port), isUDPEnabled, tun)
if err != nil {
return nil, err
}
Expand Down
6 changes: 3 additions & 3 deletions apple/tun2socks.go
Expand Up @@ -20,12 +20,12 @@ import (
"runtime/debug"
"time"

"github.com/Jigsaw-Code/outline-go-tun2socks/tun2socks"
"github.com/Jigsaw-Code/outline-go-tun2socks/tunnel"
)

// AppleTunnel embeds the tun2socks.Tunnel interface so it gets exported by gobind.
type AppleTunnel interface {
tun2socks.Tunnel
tunnel.OutlineTunnel
}

// TunWriter is an interface that allows for outputting packets to the TUN (VPN).
Expand Down Expand Up @@ -58,5 +58,5 @@ func ConnectSocksTunnel(tunWriter TunWriter, host string, port int, isUDPEnabled
if tunWriter == nil || host == "" || port <= 0 || port > 65535 {
return nil, errors.New("Must provide a TunWriter, a valid SOCKS proxy host and port")
}
return tun2socks.NewTunnel(host, uint16(port), isUDPEnabled, tunWriter)
return tunnel.NewTunnel(host, uint16(port), isUDPEnabled, tunWriter)
}
2 changes: 1 addition & 1 deletion tun2socks/common.go → tunnel/common.go
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package tun2socks
package tunnel

import (
"errors"
Expand Down
24 changes: 15 additions & 9 deletions tun2socks/intra.go → tunnel/intra.go
Expand Up @@ -12,23 +12,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package tun2socks
package tunnel

import (
"errors"
"io"
"net"
"time"

"github.com/Jigsaw-Code/outline-go-tun2socks/tun2socks/intra"
"github.com/Jigsaw-Code/outline-go-tun2socks/tunnel/intra"
"github.com/eycorsican/go-tun2socks/core"
)

type IntraListener interface {
intra.UDPListener
intra.TCPListener
}

type intratunnel struct {
*tunnel
fakedns net.Addr
udpdns net.Addr
tcpdns net.Addr
fakedns net.Addr
udpdns net.Addr
tcpdns net.Addr
listener IntraListener
}

// NewIntraTunnel creates a connected Intra session.
Expand All @@ -37,7 +43,7 @@ type intratunnel struct {
// This will normally be a reserved or remote IP address, port 53.
// `udpdns` and `tcpdns` are the actual location of the DNS server in use.
// These will normally be localhost with a high-numbered port.
func NewIntraTunnel(fakedns, udpdns, tcpdns string, tunWriter io.WriteCloser) (Tunnel, error) {
func NewIntraTunnel(fakedns, udpdns, tcpdns string, tunWriter io.WriteCloser, listener IntraListener) (Tunnel, error) {
fakednsipaddr, err := net.ResolveUDPAddr("udp", fakedns)
if err != nil {
return nil, err
Expand All @@ -55,7 +61,7 @@ func NewIntraTunnel(fakedns, udpdns, tcpdns string, tunWriter io.WriteCloser) (T
}
core.RegisterOutputFn(tunWriter.Write)
base := &tunnel{tunWriter, core.NewLWIPStack(), true}
s := &intratunnel{tunnel: base, fakedns: fakednsipaddr, udpdns: udpdnsipaddr, tcpdns: tcpdnsipaddr}
s := &intratunnel{tunnel: base, fakedns: fakednsipaddr, udpdns: udpdnsipaddr, tcpdns: tcpdnsipaddr, listener: listener}
s.registerConnectionHandlers()
return s, nil
}
Expand All @@ -65,6 +71,6 @@ func (t *intratunnel) registerConnectionHandlers() {
// RFC 5382 REQ-5 requires a timeout no shorter than 2 hours and 4 minutes.
timeout, _ := time.ParseDuration("2h4m")

core.RegisterUDPConnHandler(intra.NewUDPHandler(t.fakedns, t.udpdns, timeout))
core.RegisterTCPConnHandler(intra.NewTCPHandler(t.fakedns, t.tcpdns))
core.RegisterUDPConnHandler(intra.NewUDPHandler(t.fakedns, t.udpdns, timeout, t.listener))
core.RegisterTCPConnHandler(intra.NewTCPHandler(t.fakedns, t.tcpdns, t.listener))
}
68 changes: 58 additions & 10 deletions tun2socks/intra/tcp.go → tunnel/intra/tcp.go
Expand Up @@ -19,36 +19,81 @@ package intra
import (
"io"
"net"
"time"

"github.com/eycorsican/go-tun2socks/common/log"
"github.com/eycorsican/go-tun2socks/core"
)

type tcpHandler struct {
fakedns net.Addr
truedns net.Addr
fakedns net.Addr
truedns net.Addr
listener TCPListener
}

// Usage summary for each TCP socket, reported when it is closed.
type TCPSocketSummary struct {
Download int64 // Total bytes downloaded.
Upload int64 // Total bytes uploaded.
Duration int32 // Duration in seconds.
ServerPort int16 // The server port. All values except 80, 443, and 0 are set to -1.
Synack int32 // TCP handshake latency (ms)
}

type TCPListener interface {
OnTCPSocketClosed(*TCPSocketSummary)
}

// NewTCPHandler returns a TCP forwarder with Intra-style behavior.
// Currently this class only redirects DNS traffic to a
// specified server. (This should be rare for TCP.)
// All other traffic is forwarded unmodified.
func NewTCPHandler(fakedns, truedns net.Addr) core.TCPConnHandler {
return &tcpHandler{fakedns: fakedns, truedns: truedns}
func NewTCPHandler(fakedns, truedns net.Addr, listener TCPListener) core.TCPConnHandler {
return &tcpHandler{fakedns: fakedns, truedns: truedns, listener: listener}
}

func (h *tcpHandler) handleUpload(local net.Conn, remote *net.TCPConn) {
func (h *tcpHandler) handleUpload(local net.Conn, remote *net.TCPConn, upload chan int64) {
// TODO: Handle half-closed sockets more correctly if upstream
// changes `local` to a more detailed type than `net.Conn`.
io.Copy(remote, local)
bytes, _ := io.Copy(remote, local)
local.Close()
remote.CloseWrite()
upload <- bytes
}

func (h *tcpHandler) handleDownload(local net.Conn, remote *net.TCPConn) {
io.Copy(local, remote)
func (h *tcpHandler) handleDownload(local net.Conn, remote *net.TCPConn) (bytes int64, err error) {
bytes, err = io.Copy(local, remote)
local.Close()
remote.CloseRead()
return
}

func (h *tcpHandler) forward(local net.Conn, remote *net.TCPConn, summary TCPSocketSummary) {
upload := make(chan int64)
start := time.Now()
go h.handleUpload(local, remote, upload)
download, _ := h.handleDownload(local, remote)
summary.Download = download
summary.Upload = <-upload
summary.Duration = int32(time.Since(start).Seconds())
h.listener.OnTCPSocketClosed(&summary)
}

func filteredPort(addr net.Addr) int16 {
_, port, err := net.SplitHostPort(addr.String())
if err != nil {
return -1
}
if port == "80" {
return 80
}
if port == "443" {
return 443
}
if port == "0" {
return 0
}
return -1
}

// TODO: Request upstream to make `conn` a `core.TCPConn` so we can have finer-
Expand All @@ -64,12 +109,15 @@ func (h *tcpHandler) Handle(conn net.Conn, target net.Addr) error {
if err != nil {
return err
}
var summary TCPSocketSummary
summary.ServerPort = filteredPort(target)
start := time.Now()
c, err := net.DialTCP(target.Network(), nil, tcpaddr)
if err != nil {
return err
}
go h.handleUpload(conn, c)
go h.handleDownload(conn, c)
summary.Synack = int32(time.Since(start).Seconds() * 1000)
go h.forward(conn, c, summary)
log.Infof("new proxy connection for target: %s:%s", target.Network(), target.String())
return nil
}
45 changes: 32 additions & 13 deletions tun2socks/intra/udp.go → tunnel/intra/udp.go
Expand Up @@ -27,16 +27,29 @@ import (
"github.com/eycorsican/go-tun2socks/core"
)

// Summary of a non-DNS UDP association, reported when it is discarded.
type UDPSocketSummary struct {
Upload int64 // Amount uploaded (bytes)
Download int64 // Amount downloaded (bytes)
Duration int32 // How long the socket was open (seconds)
}

type UDPListener interface {
OnUDPSocketClosed(*UDPSocketSummary)
}

type tracker struct {
conn *net.UDPConn
conn *net.UDPConn
start time.Time
upload int64 // bytes
download int64 // bytes
// Parameters used to implement the single-query socket optimization:
fresh bool // True if the socket has not yet been used.
complex bool // True if the socket is not a oneshot DNS query.
queryid uint16 // The DNS query ID for this socket, if there is one.
}

func makeTracker(conn *net.UDPConn) *tracker {
return &tracker{conn, true, false, 0}
return &tracker{conn, time.Now(), 0, 0, false, 0}
}

type udpHandler struct {
Expand All @@ -46,19 +59,21 @@ type udpHandler struct {
udpConns map[core.UDPConn]*tracker
fakedns net.Addr
truedns net.Addr
listener UDPListener
}

// NewUDPHandler makes a UDP handler with Intra-style DNS redirection:
// All packets are routed directly to their destination, except packets whose
// destination is `fakedns`. Those packets are redirected to `truedns`.
// Similarly, packets arriving from `truedns` have the source address replaced
// with `fakedns`.
func NewUDPHandler(fakedns, truedns net.Addr, timeout time.Duration) core.UDPConnHandler {
func NewUDPHandler(fakedns, truedns net.Addr, timeout time.Duration, listener UDPListener) core.UDPConnHandler {
return &udpHandler{
timeout: timeout,
udpConns: make(map[core.UDPConn]*tracker, 8),
fakedns: fakedns,
truedns: truedns,
listener: listener,
}
}

Expand Down Expand Up @@ -88,6 +103,7 @@ func (h *udpHandler) fetchUDPInput(conn core.UDPConn, t *tracker) {
// Pretend that the reply was from the fake DNS server.
addr = h.fakedns
if n < 2 {
// Very short packet, cannot possibly be DNS.
t.complex = true
} else {
responseid := queryid(buf)
Expand All @@ -100,6 +116,7 @@ func (h *udpHandler) fetchUDPInput(conn core.UDPConn, t *tracker) {
// This socket has been used for non-DNS traffic.
t.complex = true
}
t.download += int64(n)
_, err = conn.WriteFrom(buf[:n], addr)
if err != nil {
log.Warnf("failed to write UDP data to TUN")
Expand Down Expand Up @@ -132,7 +149,7 @@ func (h *udpHandler) Connect(conn core.UDPConn, target net.Addr) error {
// TODO: Request upstream to make `addr` a `UDPAddr` for more efficient comparisons.
func (h *udpHandler) DidReceiveTo(conn core.UDPConn, data []byte, addr net.Addr) error {
h.Lock()
tracker, ok1 := h.udpConns[conn]
t, ok1 := h.udpConns[conn]
h.Unlock()

if !ok1 {
Expand All @@ -144,17 +161,17 @@ func (h *udpHandler) DidReceiveTo(conn core.UDPConn, data []byte, addr net.Addr)
addr = h.truedns
id := queryid(data)
if id < 0 {
tracker.complex = true
} else if tracker.fresh {
tracker.queryid = uint16(id)
} else if tracker.queryid != uint16(id) {
tracker.complex = true
t.complex = true
} else if t.upload == 0 {
t.queryid = uint16(id)
} else if t.queryid != uint16(id) {
t.complex = true
}
} else {
tracker.complex = true
t.complex = true
}
tracker.fresh = false
_, err := tracker.conn.WriteTo(data, addr)
t.upload += int64(len(data))
_, err := t.conn.WriteTo(data, addr)
if err != nil {
log.Warnf("failed to forward UDP payload")
return errors.New("failed to write UDP data")
Expand All @@ -170,6 +187,8 @@ func (h *udpHandler) Close(conn core.UDPConn) {

if t, ok := h.udpConns[conn]; ok {
t.conn.Close()
duration := int32(time.Since(t.start).Seconds())
h.listener.OnUDPSocketClosed(&UDPSocketSummary{t.upload, t.download, duration})
delete(h.udpConns, conn)
}
}

0 comments on commit 16b35df

Please sign in to comment.