From f837468474d422e53d382e8dfec27ff15c7496ac Mon Sep 17 00:00:00 2001 From: Junyi Yi Date: Thu, 7 Sep 2023 20:31:26 -0400 Subject: [PATCH 1/5] refactor(intra): use outline-sdk as the network stack --- intra/android/init.go | 27 +++++ intra/android/tun.go | 38 +++++++ intra/android/tun2socks.go | 68 +++++++++---- intra/packet_proxy.go | 181 +++++++++++++++++++++++++++++++++ intra/queryconn.go | 122 +++++++++++++++++++++++ intra/stream_dialer.go | 109 ++++++++++++++++++++ intra/tcp.go | 198 ++++++++++++++++--------------------- intra/tunnel.go | 124 +++++++++++------------ intra/udp.go | 161 ++---------------------------- 9 files changed, 672 insertions(+), 356 deletions(-) create mode 100644 intra/android/init.go create mode 100644 intra/android/tun.go create mode 100644 intra/packet_proxy.go create mode 100644 intra/queryconn.go create mode 100644 intra/stream_dialer.go diff --git a/intra/android/init.go b/intra/android/init.go new file mode 100644 index 0000000..b45e921 --- /dev/null +++ b/intra/android/init.go @@ -0,0 +1,27 @@ +package tun2socks + +// Copyright 2019 The Outline Authors +// +// 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. + +import ( + "runtime/debug" + + "github.com/eycorsican/go-tun2socks/common/log" +) + +func init() { + // Conserve memory by increasing garbage collection frequency. + debug.SetGCPercent(10) + log.SetLevel(log.WARN) +} diff --git a/intra/android/tun.go b/intra/android/tun.go new file mode 100644 index 0000000..e3639dc --- /dev/null +++ b/intra/android/tun.go @@ -0,0 +1,38 @@ +// Copyright 2023 Jigsaw Operations LLC +// +// 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 tun2socks + +import ( + "errors" + "os" + + "golang.org/x/sys/unix" +) + +func makeTunFile(fd int) (*os.File, error) { + if fd < 0 { + return nil, errors.New("must provide a valid TUN file descriptor") + } + // Make a copy of `fd` so that os.File's finalizer doesn't close `fd`. + newfd, err := unix.Dup(fd) + if err != nil { + return nil, err + } + file := os.NewFile(uintptr(newfd), "") + if file == nil { + return nil, errors.New("failed to open TUN file descriptor") + } + return file, nil +} diff --git a/intra/android/tun2socks.go b/intra/android/tun2socks.go index b569697..b8bebca 100644 --- a/intra/android/tun2socks.go +++ b/intra/android/tun2socks.go @@ -15,59 +15,65 @@ package tun2socks import ( - "runtime/debug" + "errors" + "io" + "io/fs" + "log" + "os" "strings" "github.com/Jigsaw-Code/outline-go-tun2socks/intra" "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh" "github.com/Jigsaw-Code/outline-go-tun2socks/intra/protect" - "github.com/Jigsaw-Code/outline-go-tun2socks/tunnel" - "github.com/eycorsican/go-tun2socks/common/log" + "github.com/Jigsaw-Code/outline-sdk/network" ) -func init() { - // Conserve memory by increasing garbage collection frequency. - debug.SetGCPercent(10) - log.SetLevel(log.WARN) -} - // ConnectIntraTunnel reads packets from a TUN device and applies the Intra routing // rules. Currently, this only consists of redirecting DNS packets to a specified // server; all other data flows directly to its destination. // // `fd` is the TUN device. The IntraTunnel acquires an additional reference to it, which -// is released by IntraTunnel.Disconnect(), so the caller must close `fd` _and_ call -// Disconnect() in order to close the TUN device. +// +// is released by IntraTunnel.Disconnect(), so the caller must close `fd` _and_ call +// Disconnect() in order to close the TUN device. +// // `fakedns` is the DNS server that the system believes it is using, in "host:port" style. -// The port is normally 53. +// +// The port is normally 53. +// // `udpdns` and `tcpdns` are the location of the actual DNS server being used. For DNS -// tunneling in Intra, these are typically high-numbered ports on localhost. +// +// tunneling in Intra, these are typically high-numbered ports on localhost. +// // `dohdns` is the initial DoH transport. It must not be `nil`. // `protector` is a wrapper for Android's VpnService.protect() method. // `listener` will be provided with a summary of each TCP and UDP socket when it is closed. // // Throws an exception if the TUN file descriptor cannot be opened, or if the tunnel fails to // connect. -func ConnectIntraTunnel(fd int, fakedns string, dohdns doh.Transport, protector protect.Protector, listener intra.Listener) (intra.Tunnel, error) { - tun, err := tunnel.MakeTunFile(fd) +func ConnectIntraTunnel(fd int, fakedns string, dohdns doh.Transport, protector protect.Protector, listener intra.Listener) (*intra.Tunnel, error) { + tun, err := makeTunFile(fd) if err != nil { return nil, err } - dialer := protect.MakeDialer(protector) - config := protect.MakeListenConfig(protector) - t, err := intra.NewTunnel(fakedns, dohdns, tun, dialer, config, listener) + t, err := intra.NewTunnel(fakedns, dohdns, tun, protector, listener) if err != nil { return nil, err } - go tunnel.ProcessInputPackets(t, tun) + go copyUntilEOF(t, tun) + go copyUntilEOF(tun, t) return t, nil } // NewDoHTransport returns a DNSTransport that connects to the specified DoH server. // `url` is the URL of a DoH server (no template, POST-only). If it is nonempty, it -// overrides `udpdns` and `tcpdns`. +// +// overrides `udpdns` and `tcpdns`. +// // `ips` is an optional comma-separated list of IP addresses for the server. (This -// wrapper is required because gomobile can't make bindings for []string.) +// +// wrapper is required because gomobile can't make bindings for []string.) +// // `protector` is the socket protector to use for all external network activity. // `auth` will provide a client certificate if required by the TLS server. // `listener` will be notified after each DNS query succeeds or fails. @@ -79,3 +85,23 @@ func NewDoHTransport(url string, ips string, protector protect.Protector, auth d dialer := protect.MakeDialer(protector) return doh.NewTransport(url, split, dialer, auth, listener) } + +func copyUntilEOF(dst, src io.ReadWriteCloser) { + log.Printf("[debug] start relaying traffic [%s] -> [%s]", src, dst) + defer log.Printf("[debug] stop relaying traffic [%s] -> [%s]", src, dst) + + const commonMTU = 1500 + buf := make([]byte, commonMTU) + defer dst.Close() + for { + _, err := io.CopyBuffer(dst, src, buf) + if err == nil || isErrClosed(err) { + + return + } + } +} + +func isErrClosed(err error) bool { + return errors.Is(err, os.ErrClosed) || errors.Is(err, fs.ErrClosed) || errors.Is(err, network.ErrClosed) +} diff --git a/intra/packet_proxy.go b/intra/packet_proxy.go new file mode 100644 index 0000000..80a3c6a --- /dev/null +++ b/intra/packet_proxy.go @@ -0,0 +1,181 @@ +// Copyright 2023 Jigsaw Operations LLC +// +// 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 intra + +import ( + "errors" + "fmt" + "log" + "net" + "net/netip" + "sync/atomic" + "time" + + "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh" + "github.com/Jigsaw-Code/outline-go-tun2socks/intra/protect" + "github.com/Jigsaw-Code/outline-sdk/network" + "github.com/Jigsaw-Code/outline-sdk/transport" +) + +type intraPacketProxy struct { + fakeDNSAddr netip.AddrPort + dns atomic.Pointer[doh.Transport] + proxy network.PacketProxy + listener UDPListener +} + +var _ network.PacketProxy = (*intraPacketProxy)(nil) + +func makeIntraPacketProxy(fakeDNS netip.AddrPort, dns doh.Transport, protector protect.Protector, listener UDPListener) (*intraPacketProxy, error) { + if dns == nil { + return nil, errors.New("dns is required") + } + + pl := &transport.UDPPacketListener{ + ListenConfig: *protect.MakeListenConfig(protector), + Address: ":0", + } + + // TODO: add timeout option in SDK so we can configure the timeout to 5 minutes + // RFC 4787 REQ-5 requires a timeout no shorter than 5 minutes. + pp, err := network.NewPacketProxyFromPacketListener(pl) + if err != nil { + return nil, fmt.Errorf("failed to create packet proxy from listener: %w", err) + } + + dohpp := &intraPacketProxy{ + fakeDNSAddr: fakeDNS, + proxy: pp, + listener: listener, + } + dohpp.dns.Store(&dns) + + return dohpp, nil +} + +// NewSession implements PacketProxy.NewSession. +func (p *intraPacketProxy) NewSession(resp network.PacketResponseReceiver) (network.PacketRequestSender, error) { + log.Println("[debug] initializing a new UDP session...") + defer log.Println("[info] New UDP session initialized") + + dohResp := &dohPacketRespReceiver{ + PacketResponseReceiver: resp, + stats: makeTracker(), + listener: p.listener, + } + req, err := p.proxy.NewSession(dohResp) + if err != nil { + log.Printf("[error] failed to create UDP session: %v\n", err) + return nil, fmt.Errorf("failed to create new session: %w", err) + } + + return &dohPacketReqSender{ + PacketRequestSender: req, + proxy: p, + response: dohResp, + stats: dohResp.stats, + }, nil +} + +func (p *intraPacketProxy) SetDNS(dns doh.Transport) error { + if dns == nil { + return errors.New("dns is required") + } + p.dns.Store(&dns) + return nil +} + +// DoH PacketRequestSender wrapper +type dohPacketReqSender struct { + network.PacketRequestSender + + response *dohPacketRespReceiver + proxy *intraPacketProxy + stats *tracker +} + +// DoH PacketResponseReceiver wrapper +type dohPacketRespReceiver struct { + network.PacketResponseReceiver + + stats *tracker + listener UDPListener +} + +var _ network.PacketRequestSender = (*dohPacketReqSender)(nil) +var _ network.PacketResponseReceiver = (*dohPacketRespReceiver)(nil) + +// WriteTo implements PacketRequestSender.WriteTo. It will query the DoH server if the packet a DNS packet. +func (req *dohPacketReqSender) WriteTo(p []byte, destination netip.AddrPort) (int, error) { + log.Printf("[debug] Sending raw UDP packet (%v bytes) to %v\n", len(p), destination) + + if destination == req.proxy.fakeDNSAddr { + defer func() { + // conn was only used for this DNS query, so it's unlikely to be used again + if req.stats.download.Load() == 0 && req.stats.upload.Load() == 0 { + log.Println("[debug] DoH dedicated session finished, Closing...") + req.Close() + } + }() + + log.Println("[debug] Doing DNS request over DoH server...") + resp, err := (*req.proxy.dns.Load()).Query(p) + if err != nil { + log.Printf("[error] DoH request failed: %v\n", err) + return 0, fmt.Errorf("DoH request error: %w", err) + } + if len(resp) == 0 { + log.Println("[error] DoH response is empty") + return 0, errors.New("empty DoH response") + } + + log.Printf("[info] Write DoH response (%v bytes) from %v\n", len(resp), req.proxy.fakeDNSAddr) + return req.response.writeFrom(resp, net.UDPAddrFromAddrPort(req.proxy.fakeDNSAddr), false) + } + + log.Printf("[debug] UDP Session: upload %v bytes to %v\n", len(p), destination) + req.stats.upload.Add(int64(len(p))) + return req.PacketRequestSender.WriteTo(p, destination) +} + +// Close terminates the UDP session, and reports session stats to the listener. +func (resp *dohPacketRespReceiver) Close() error { + log.Println("[debug] UDP session terminating...") + defer log.Printf("[info] UDP session terminated: down = %v, up = %v\n", resp.stats.download.Load(), resp.stats.upload.Load()) + if resp.listener != nil { + resp.listener.OnUDPSocketClosed(&UDPSocketSummary{ + Duration: int32(time.Since(resp.stats.start)), + UploadBytes: resp.stats.upload.Load(), + DownloadBytes: resp.stats.download.Load(), + }) + } + return resp.PacketResponseReceiver.Close() +} + +// WriteFrom implements PacketResponseReceiver.WriteFrom. +func (resp *dohPacketRespReceiver) WriteFrom(p []byte, source net.Addr) (int, error) { + log.Printf("[debug] Receiving raw UDP packet (%v bytes) from %v\n", len(p), source) + return resp.writeFrom(p, source, true) +} + +// writeFrom writes to the underlying PacketResponseReceiver. +// It will also add len(p) to downloadBytes if doStat is true. +func (resp *dohPacketRespReceiver) writeFrom(p []byte, source net.Addr, doStat bool) (int, error) { + if doStat { + log.Printf("[debug] UDP Session: download %v bytes from %v\n", len(p), source) + resp.stats.download.Add(int64(len(p))) + } + return resp.PacketResponseReceiver.WriteFrom(p, source) +} diff --git a/intra/queryconn.go b/intra/queryconn.go new file mode 100644 index 0000000..bedf777 --- /dev/null +++ b/intra/queryconn.go @@ -0,0 +1,122 @@ +// Copyright 2023 Jigsaw Operations LLC +// +// 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 intra + +import ( + "errors" + "io" + "log" + "net" + "time" + + "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh" + "github.com/Jigsaw-Code/outline-sdk/transport" +) + +var errUnsupported = errors.New("feature is not supported") + +// twoWayPipe connects two I/O endpoints (source and dest) bidirectionally. +// The type itself also acts as the dest's [io.ReadWriteCloser]. +type dohQueryServerConn struct { + conn *dohQueryStreamConn +} + +// dohQueryStreamConn is an "outbound" [transport.StreamConn] that handles DNS-over-TCP (DoT) traffic. +// "Outbound" means Reading responses from the remote DoT server, and Writing requests to DoT server. +// +// dohQueryStreamConn also contains a corresponding serverConn, which is an "inbound" [io.ReadWriteCloser]. +// "Inbound" means Reading requests from the client, and Writing responses to the client. +type dohQueryStreamConn struct { + reqReader, respReader *io.PipeReader + reqWriter, respWriter *io.PipeWriter + serverConn io.ReadWriteCloser +} + +var _ io.ReadWriteCloser = (*dohQueryServerConn)(nil) +var _ transport.StreamConn = (*dohQueryStreamConn)(nil) + +func makeDoHQueryStreamConn(dns doh.Transport) (conn *dohQueryStreamConn) { + defer log.Println("[info] DoT over DoH session initialized") + conn = &dohQueryStreamConn{} + conn.reqReader, conn.reqWriter = io.Pipe() + conn.respReader, conn.respWriter = io.Pipe() + conn.serverConn = &dohQueryServerConn{conn} + + go doh.Accept(dns, conn) + return +} + +func (p *dohQueryServerConn) Close() error { + return p.conn.Close() +} + +func (p *dohQueryServerConn) Read(data []byte) (int, error) { + log.Printf("[debug] Sending DoH request (%v bytes)\n", len(data)) + return p.conn.reqReader.Read(data) +} + +func (p *dohQueryServerConn) Write(data []byte) (int, error) { + log.Printf("[debug] Got DoH response (%v bytes)\n", len(data)) + return p.conn.respWriter.Write(data) +} + +func (conn *dohQueryStreamConn) Close() error { + return errors.Join(conn.CloseRead(), conn.CloseWrite()) +} + +func (conn *dohQueryStreamConn) CloseRead() error { + defer log.Println("[info] DoT over DoH read session terminated") + return errors.Join(conn.respReader.Close(), conn.respWriter.Close()) +} + +func (conn *dohQueryStreamConn) CloseWrite() error { + defer log.Println("[info] DoT over DoH write session terminated") + return errors.Join(conn.reqReader.Close(), conn.respWriter.Close()) +} + +func (conn *dohQueryStreamConn) Read(b []byte) (int, error) { + log.Printf("[debug] Got DoT response (%v bytes)\n", len(b)) + return conn.respReader.Read(b) +} + +func (conn *dohQueryStreamConn) Write(b []byte) (int, error) { + log.Printf("[debug] Handling DoT request (%v bytes)\n", len(b)) + return conn.reqWriter.Write(b) +} + +// LocalAddr returns nil. +func (*dohQueryStreamConn) LocalAddr() net.Addr { + return nil +} + +// RemoteAddr returns nil. +func (*dohQueryStreamConn) RemoteAddr() net.Addr { + return nil +} + +// SetDeadline is not supported. +func (*dohQueryStreamConn) SetDeadline(t time.Time) error { + return errUnsupported +} + +// SetReadDeadline is not supported. +func (*dohQueryStreamConn) SetReadDeadline(t time.Time) error { + return errUnsupported +} + +// SetWriteDeadline is not supported. +func (*dohQueryStreamConn) SetWriteDeadline(t time.Time) error { + return errUnsupported +} diff --git a/intra/stream_dialer.go b/intra/stream_dialer.go new file mode 100644 index 0000000..ff5140e --- /dev/null +++ b/intra/stream_dialer.go @@ -0,0 +1,109 @@ +// Copyright 2023 Jigsaw Operations LLC +// +// 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 intra + +import ( + "context" + "errors" + "fmt" + "log" + "net" + "net/netip" + "sync/atomic" + "time" + + "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh" + "github.com/Jigsaw-Code/outline-go-tun2socks/intra/protect" + "github.com/Jigsaw-Code/outline-go-tun2socks/intra/split" + "github.com/Jigsaw-Code/outline-sdk/transport" +) + +type intraStreamDialer struct { + fakeDNSAddr netip.AddrPort + dns atomic.Pointer[doh.Transport] + dialer *net.Dialer + alwaysSplitHTTPS atomic.Bool + listener TCPListener + sniReporter *tcpSNIReporter +} + +var _ transport.StreamDialer = (*intraStreamDialer)(nil) + +func makeIntraStreamDialer(fakeDNS netip.AddrPort, dns doh.Transport, protector protect.Protector, listener TCPListener, sniReporter *tcpSNIReporter) (*intraStreamDialer, error) { + if dns == nil { + return nil, errors.New("dns is required") + } + + dohsd := &intraStreamDialer{ + fakeDNSAddr: fakeDNS, + dialer: protect.MakeDialer(protector), + listener: listener, + sniReporter: sniReporter, + } + dohsd.dns.Store(&dns) + return dohsd, nil +} + +// Dial implements StreamDialer.Dial. +func (sd *intraStreamDialer) Dial(ctx context.Context, raddr string) (transport.StreamConn, error) { + log.Printf("[debug] Dialing TCP traffic to %v\n", raddr) + dest, err := netip.ParseAddrPort(raddr) + if err != nil { + return nil, fmt.Errorf("invalid raddr (%v): %w", raddr, err) + } + + if dest == sd.fakeDNSAddr { + log.Println("[debug] Doing DoT request over DoH server...") + return makeDoHQueryStreamConn(*sd.dns.Load()), nil + } + + stats := makeTCPSocketSummary(dest) + beforeConn := time.Now() + conn, err := sd.dial(ctx, dest, stats) + if err != nil { + return nil, fmt.Errorf("failed to dial to target: %w", err) + } + stats.Synack = int32(time.Since(beforeConn).Milliseconds()) + + return makeTCPWrapConn(conn, stats, sd.listener, sd.sniReporter), nil +} + +func (sd *intraStreamDialer) SetDNS(dns doh.Transport) error { + if dns == nil { + return errors.New("dns is required") + } + sd.dns.Store(&dns) + return nil +} + +func (sd *intraStreamDialer) dial(ctx context.Context, dest netip.AddrPort, stats *TCPSocketSummary) (transport.StreamConn, error) { + if dest.Port() == 443 { + log.Println("[debug] Dialing HTTPS traffic") + if sd.alwaysSplitHTTPS.Load() { + log.Println("[debug] Dialing TCP traffic over split dialer") + return split.DialWithSplit(sd.dialer, net.TCPAddrFromAddrPort(dest)) + } else { + log.Println("[debug] Dialing TCP traffic over retryable split dialer") + stats.Retry = &split.RetryStats{} + return split.DialWithSplitRetry(sd.dialer, net.TCPAddrFromAddrPort(dest), stats.Retry) + } + } else { + log.Println("[debug] Dialing TCP traffic directly over internet") + tcpsd := &transport.TCPStreamDialer{ + Dialer: *sd.dialer, + } + return tcpsd.Dial(ctx, dest.String()) + } +} diff --git a/intra/tcp.go b/intra/tcp.go index 13b8fd6..4258f2a 100644 --- a/intra/tcp.go +++ b/intra/tcp.go @@ -18,34 +18,15 @@ package intra import ( "io" - "net" + "net/netip" + "sync" + "sync/atomic" "time" - "github.com/eycorsican/go-tun2socks/common/log" - "github.com/eycorsican/go-tun2socks/core" - - "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh" "github.com/Jigsaw-Code/outline-go-tun2socks/intra/split" + "github.com/Jigsaw-Code/outline-sdk/transport" ) -// TCPHandler is a core TCP handler that also supports DOH and splitting control. -type TCPHandler interface { - core.TCPConnHandler - SetDNS(doh.Transport) - SetAlwaysSplitHTTPS(bool) - EnableSNIReporter(file io.ReadWriter, suffix, country string) error -} - -type tcpHandler struct { - TCPHandler - fakedns net.TCPAddr - dns doh.Atomic - alwaysSplitHTTPS bool - dialer *net.Dialer - listener TCPListener - sniReporter tcpSNIReporter -} - // TCPSocketSummary provides information about each TCP socket, reported when it is closed. type TCPSocketSummary struct { DownloadBytes int64 // Total bytes downloaded. @@ -57,116 +38,107 @@ type TCPSocketSummary struct { Retry *split.RetryStats } +func makeTCPSocketSummary(dest netip.AddrPort) *TCPSocketSummary { + stats := &TCPSocketSummary{ + ServerPort: int16(dest.Port()), + } + if stats.ServerPort != 0 && stats.ServerPort != 80 && stats.ServerPort != 443 { + stats.ServerPort = -1 + } + return stats +} + // TCPListener is notified when a socket closes. type TCPListener interface { OnTCPSocketClosed(*TCPSocketSummary) } -// NewTCPHandler returns a TCP forwarder with Intra-style behavior. -// Connections to `fakedns` are redirected to DOH. -// All other traffic is forwarded using `dialer`. -// `listener` is provided with a summary of each socket when it is closed. -func NewTCPHandler(fakedns net.TCPAddr, dialer *net.Dialer, listener TCPListener) TCPHandler { - return &tcpHandler{ - fakedns: fakedns, - dialer: dialer, - listener: listener, +type tcpWrapConn struct { + transport.StreamConn + + wg *sync.WaitGroup + rDone, wDone atomic.Bool + + beginTime time.Time + stats *TCPSocketSummary + + listener TCPListener + sniReporter *tcpSNIReporter +} + +func makeTCPWrapConn(c transport.StreamConn, stats *TCPSocketSummary, listener TCPListener, sniReporter *tcpSNIReporter) (conn *tcpWrapConn) { + conn = &tcpWrapConn{ + StreamConn: c, + wg: &sync.WaitGroup{}, + beginTime: time.Now(), + stats: stats, + listener: listener, + sniReporter: sniReporter, } + + // Wait until both read and write are done + conn.wg.Add(2) + go func() { + conn.wg.Wait() + conn.stats.Duration = int32(time.Since(conn.beginTime)) + if conn.listener != nil { + conn.listener.OnTCPSocketClosed(conn.stats) + } + if conn.stats.Retry != nil && conn.sniReporter != nil { + conn.sniReporter.Report(*conn.stats) + } + }() + + return } -// TODO: Propagate TCP RST using local.Abort(), on appropriate errors. -func (h *tcpHandler) handleUpload(local core.TCPConn, remote split.DuplexConn, upload chan int64) { - bytes, _ := remote.ReadFrom(local) - local.CloseRead() - remote.CloseWrite() - upload <- bytes +func (conn *tcpWrapConn) Close() error { + defer conn.close(&conn.wDone) + defer conn.close(&conn.rDone) + return conn.StreamConn.Close() } -func (h *tcpHandler) handleDownload(local core.TCPConn, remote split.DuplexConn) (bytes int64, err error) { - bytes, err = io.Copy(local, remote) - local.CloseWrite() - remote.CloseRead() - return +func (conn *tcpWrapConn) CloseRead() error { + defer conn.close(&conn.rDone) + return conn.StreamConn.CloseRead() } -func (h *tcpHandler) forward(local net.Conn, remote split.DuplexConn, summary *TCPSocketSummary) { - localtcp := local.(core.TCPConn) - upload := make(chan int64) - start := time.Now() - go h.handleUpload(localtcp, remote, upload) - download, _ := h.handleDownload(localtcp, remote) - summary.DownloadBytes = download - summary.UploadBytes = <-upload - summary.Duration = int32(time.Since(start).Seconds()) - h.listener.OnTCPSocketClosed(summary) - if summary.Retry != nil { - h.sniReporter.Report(*summary) - } +func (conn *tcpWrapConn) CloseWrite() error { + defer conn.close(&conn.wDone) + return conn.StreamConn.CloseWrite() } -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 +func (conn *tcpWrapConn) Read(b []byte) (n int, err error) { + defer func() { + conn.stats.DownloadBytes += int64(n) + }() + return conn.StreamConn.Read(b) } -// TODO: Request upstream to make `conn` a `core.TCPConn` so we can avoid a type assertion. -func (h *tcpHandler) Handle(conn net.Conn, target *net.TCPAddr) error { - // DNS override - if target.IP.Equal(h.fakedns.IP) && target.Port == h.fakedns.Port { - dns := h.dns.Load() - go doh.Accept(dns, conn) - return nil - } - var summary TCPSocketSummary - summary.ServerPort = filteredPort(target) - start := time.Now() - var c split.DuplexConn - var err error - // TODO: Cancel dialing if c is closed. - if summary.ServerPort == 443 { - if h.alwaysSplitHTTPS { - c, err = split.DialWithSplit(h.dialer, target) - } else { - summary.Retry = &split.RetryStats{} - c, err = split.DialWithSplitRetry(h.dialer, target, summary.Retry) - } - } else { - var generic net.Conn - generic, err = h.dialer.Dial(target.Network(), target.String()) - if generic != nil { - c = generic.(*net.TCPConn) - } - } - if err != nil { - return err - } - 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 +func (conn *tcpWrapConn) WriteTo(w io.Writer) (n int64, err error) { + defer func() { + conn.stats.DownloadBytes += n + }() + return io.Copy(w, conn.StreamConn) } -func (h *tcpHandler) SetDNS(dns doh.Transport) { - h.dns.Store(dns) - h.sniReporter.SetDNS(dns) +func (conn *tcpWrapConn) Write(b []byte) (n int, err error) { + defer func() { + conn.stats.UploadBytes += int64(n) + }() + return conn.StreamConn.Write(b) } -func (h *tcpHandler) SetAlwaysSplitHTTPS(s bool) { - h.alwaysSplitHTTPS = s +func (conn *tcpWrapConn) ReadFrom(r io.Reader) (n int64, err error) { + defer func() { + conn.stats.UploadBytes += n + }() + return io.Copy(conn.StreamConn, r) } -func (h *tcpHandler) EnableSNIReporter(file io.ReadWriter, suffix, country string) error { - return h.sniReporter.Configure(file, suffix, country) +func (conn *tcpWrapConn) close(done *atomic.Bool) { + // make sure conn.wg is being called at most once for a specific `done` flag + if done.CompareAndSwap(false, true) { + conn.wg.Done() + } } diff --git a/intra/tunnel.go b/intra/tunnel.go index ef36b58..416ebe6 100644 --- a/intra/tunnel.go +++ b/intra/tunnel.go @@ -16,16 +16,16 @@ package intra import ( "errors" + "fmt" "io" "net" "os" "strings" - "time" - - "github.com/eycorsican/go-tun2socks/core" "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh" - "github.com/Jigsaw-Code/outline-go-tun2socks/tunnel" + "github.com/Jigsaw-Code/outline-go-tun2socks/intra/protect" + "github.com/Jigsaw-Code/outline-sdk/network" + "github.com/Jigsaw-Code/outline-sdk/network/lwip2transport" ) // Listener receives usage statistics when a UDP or TCP socket is closed, @@ -37,95 +37,85 @@ type Listener interface { } // Tunnel represents an Intra session. -type Tunnel interface { - tunnel.Tunnel - // Get the DNSTransport (default: nil). - GetDNS() doh.Transport - // Set the DNSTransport. This method must be called before connecting the transport - // to the TUN device. The transport can be changed at any time during operation, but - // must not be nil. - SetDNS(doh.Transport) - // When set to true, Intra will pre-emptively split all HTTPS connections. - SetAlwaysSplitHTTPS(bool) - // Enable reporting of SNIs that resulted in connection failures, using the - // Choir library for privacy-preserving error reports. `file` is the path - // that Choir should use to store its persistent state, `suffix` is the - // authoritative domain to which reports will be sent, and `country` is a - // two-letter ISO country code for the user's current location. - EnableSNIReporter(file, suffix, country string) error -} +type Tunnel struct { + network.IPDevice -type intratunnel struct { - tunnel.Tunnel - tcp TCPHandler - udp UDPHandler - dns doh.Transport + sd *intraStreamDialer + pp *intraPacketProxy + sni *tcpSNIReporter + tun io.Closer } // NewTunnel creates a connected Intra session. // // `fakedns` is the DNS server (IP and port) that will be used by apps on the TUN device. -// This will normally be a reserved or remote IP address, port 53. +// +// 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. +// +// These will normally be localhost with a high-numbered port. +// // `dohdns` is the initial DOH transport. -// `tunWriter` is the downstream VPN tunnel. IntraTunnel.Disconnect() will close `tunWriter`. -// `dialer` and `config` will be used for all network activity. // `listener` will be notified at the completion of every tunneled socket. -func NewTunnel(fakedns string, dohdns doh.Transport, tunWriter io.WriteCloser, dialer *net.Dialer, config *net.ListenConfig, listener Listener) (Tunnel, error) { - if tunWriter == nil { - return nil, errors.New("Must provide a valid TUN writer") - } - core.RegisterOutputFn(tunWriter.Write) - t := &intratunnel{ - Tunnel: tunnel.NewTunnel(tunWriter, core.NewLWIPStack()), +func NewTunnel(fakedns string, dohdns doh.Transport, tun io.Closer, protector protect.Protector, listener Listener) (t *Tunnel, err error) { + if listener == nil { + return nil, errors.New("listener is required") } - if err := t.registerConnectionHandlers(fakedns, dialer, config, listener); err != nil { - return nil, err + + fakeDNSAddr, err := net.ResolveUDPAddr("udp", fakedns) + if err != nil { + return nil, fmt.Errorf("failed to resolve fakedns: %w", err) } - t.SetDNS(dohdns) - return t, nil -} -// Registers Intra's custom UDP and TCP connection handlers to the tun2socks core. -func (t *intratunnel) registerConnectionHandlers(fakedns string, dialer *net.Dialer, config *net.ListenConfig, listener Listener) error { - // RFC 4787 REQ-5 requires a timeout no shorter than 5 minutes. - timeout, _ := time.ParseDuration("5m") + t = &Tunnel{ + sni: &tcpSNIReporter{ + dns: dohdns, + }, + tun: tun, + } - udpfakedns, err := net.ResolveUDPAddr("udp", fakedns) + t.sd, err = makeIntraStreamDialer(fakeDNSAddr.AddrPort(), dohdns, protector, listener, t.sni) if err != nil { - return err + return nil, fmt.Errorf("failed to create stream dialer: %w", err) } - t.udp = NewUDPHandler(*udpfakedns, timeout, config, listener) - core.RegisterUDPConnHandler(t.udp) - tcpfakedns, err := net.ResolveTCPAddr("tcp", fakedns) + t.pp, err = makeIntraPacketProxy(fakeDNSAddr.AddrPort(), dohdns, protector, listener) if err != nil { - return err + return nil, fmt.Errorf("failed to create packet proxy: %w", err) } - t.tcp = NewTCPHandler(*tcpfakedns, dialer, listener) - core.RegisterTCPConnHandler(t.tcp) - return nil -} -func (t *intratunnel) SetDNS(dns doh.Transport) { - t.dns = dns - t.udp.SetDNS(dns) - t.tcp.SetDNS(dns) -} + if t.IPDevice, err = lwip2transport.ConfigureDevice(t.sd, t.pp); err != nil { + return nil, fmt.Errorf("failed to configure lwIP stack: %w", err) + } -func (t *intratunnel) GetDNS() doh.Transport { - return t.dns + t.SetDNS(dohdns) + return } -func (t *intratunnel) SetAlwaysSplitHTTPS(s bool) { - t.tcp.SetAlwaysSplitHTTPS(s) +// Set the DNSTransport. This method must be called before connecting the transport +// to the TUN device. The transport can be changed at any time during operation, but +// must not be nil. +func (t *Tunnel) SetDNS(dns doh.Transport) { + t.sd.SetDNS(dns) + t.pp.SetDNS(dns) + t.sni.SetDNS(dns) } -func (t *intratunnel) EnableSNIReporter(filename, suffix, country string) error { +// Enable reporting of SNIs that resulted in connection failures, using the +// Choir library for privacy-preserving error reports. `file` is the path +// that Choir should use to store its persistent state, `suffix` is the +// authoritative domain to which reports will be sent, and `country` is a +// two-letter ISO country code for the user's current location. +func (t *Tunnel) EnableSNIReporter(filename, suffix, country string) error { f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } - return t.tcp.EnableSNIReporter(f, suffix, strings.ToLower(country)) + return t.sni.Configure(f, suffix, strings.ToLower(country)) +} + +func (t *Tunnel) Disconnect() { + t.Close() + t.tun.Close() } diff --git a/intra/udp.go b/intra/udp.go index 165b9e3..de8fa01 100644 --- a/intra/udp.go +++ b/intra/udp.go @@ -17,17 +17,8 @@ package intra import ( - "context" - "errors" - "fmt" - "net" - "sync" + "sync/atomic" "time" - - "github.com/eycorsican/go-tun2socks/common/log" - "github.com/eycorsican/go-tun2socks/core" - - "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh" ) // UDPSocketSummary describes a non-DNS UDP association, reported when it is discarded. @@ -43,153 +34,13 @@ type UDPListener interface { } type tracker struct { - conn *net.UDPConn start time.Time - upload int64 // Non-DNS upload bytes - download int64 // Non-DNS download bytes -} - -func makeTracker(conn *net.UDPConn) *tracker { - return &tracker{conn, time.Now(), 0, 0} -} - -// UDPHandler adds DOH support to the base UDPConnHandler interface. -type UDPHandler interface { - core.UDPConnHandler - SetDNS(dns doh.Transport) -} - -type udpHandler struct { - UDPHandler - sync.RWMutex - - timeout time.Duration - udpConns map[core.UDPConn]*tracker - fakedns net.UDPAddr - dns doh.Transport - config *net.ListenConfig - 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 DOH. -// `timeout` controls the effective NAT mapping lifetime. -// `config` is used to bind new external UDP ports. -// `listener` receives a summary about each UDP binding when it expires. -func NewUDPHandler(fakedns net.UDPAddr, timeout time.Duration, config *net.ListenConfig, listener UDPListener) UDPHandler { - return &udpHandler{ - timeout: timeout, - udpConns: make(map[core.UDPConn]*tracker, 8), - fakedns: fakedns, - config: config, - listener: listener, - } -} - -func (h *udpHandler) fetchUDPInput(conn core.UDPConn, t *tracker) { - buf := core.NewBytes(core.BufSize) - - defer func() { - h.Close(conn) - core.FreeBytes(buf) - }() - - for { - t.conn.SetDeadline(time.Now().Add(h.timeout)) - n, addr, err := t.conn.ReadFrom(buf) - if err != nil { - return - } - - udpaddr := addr.(*net.UDPAddr) - t.download += int64(n) - _, err = conn.WriteFrom(buf[:n], udpaddr) - if err != nil { - log.Warnf("failed to write UDP data to TUN") - return - } - } -} - -func (h *udpHandler) Connect(conn core.UDPConn, target *net.UDPAddr) error { - bindAddr := &net.UDPAddr{IP: nil, Port: 0} - pc, err := h.config.ListenPacket(context.TODO(), bindAddr.Network(), bindAddr.String()) - if err != nil { - log.Errorf("failed to bind udp address") - return err - } - t := makeTracker(pc.(*net.UDPConn)) - h.Lock() - h.udpConns[conn] = t - h.Unlock() - go h.fetchUDPInput(conn, t) - log.Infof("new proxy connection for target: %s:%s", target.Network(), target.String()) - return nil -} - -func (h *udpHandler) doDoh(dns doh.Transport, t *tracker, conn core.UDPConn, data []byte) { - resp, err := dns.Query(data) - if resp != nil { - _, err = conn.WriteFrom(resp, &h.fakedns) - } - if err != nil { - log.Warnf("DoH query failed: %v", err) - } - // Note: Reading t.upload and t.download on this thread, while they are written on - // other threads, is theoretically a race condition. In practice, this race is - // impossible on 64-bit platforms, likely impossible on 32-bit platforms, and - // low-impact if it occurs (a mixed-use socket might be closed early). - if t.upload == 0 && t.download == 0 { - // conn was only used for this DNS query, so it's unlikely to be used again. - h.Close(conn) - } + upload atomic.Int64 // Non-DNS upload bytes + download atomic.Int64 // Non-DNS download bytes } -func (h *udpHandler) ReceiveTo(conn core.UDPConn, data []byte, addr *net.UDPAddr) error { - h.RLock() - dns := h.dns - t, ok1 := h.udpConns[conn] - h.RUnlock() - - if !ok1 { - return fmt.Errorf("connection %v->%v does not exists", conn.LocalAddr(), addr) +func makeTracker() *tracker { + return &tracker{ + start: time.Now(), } - - // Update deadline. - t.conn.SetDeadline(time.Now().Add(h.timeout)) - - if addr.IP.Equal(h.fakedns.IP) && addr.Port == h.fakedns.Port { - dataCopy := append([]byte{}, data...) - go h.doDoh(dns, t, conn, dataCopy) - return nil - } - 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") - } - return nil -} - -func (h *udpHandler) Close(conn core.UDPConn) { - conn.Close() - - h.Lock() - defer h.Unlock() - - if t, ok := h.udpConns[conn]; ok { - t.conn.Close() - // TODO: Cancel any outstanding DoH queries. - duration := int32(time.Since(t.start).Seconds()) - h.listener.OnUDPSocketClosed(&UDPSocketSummary{t.upload, t.download, duration}) - delete(h.udpConns, conn) - } -} - -func (h *udpHandler) SetDNS(dns doh.Transport) { - h.Lock() - h.dns = dns - h.Unlock() } From e30b14545db822540abd0de11c069a20a463f7d9 Mon Sep 17 00:00:00 2001 From: Junyi Yi Date: Thu, 7 Sep 2023 22:01:02 -0400 Subject: [PATCH 2/5] update gomobile to the latest version --- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 4b11b16..8e0d5a5 100644 --- a/go.mod +++ b/go.mod @@ -8,16 +8,16 @@ require ( github.com/Jigsaw-Code/outline-sdk v0.0.2 github.com/crazy-max/xgo v0.26.0 github.com/eycorsican/go-tun2socks v1.16.11 - golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c - golang.org/x/net v0.8.0 - golang.org/x/sys v0.6.0 + golang.org/x/mobile v0.0.0-20230906132913-2077a3224571 + golang.org/x/net v0.15.0 + golang.org/x/sys v0.12.0 ) require ( github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/mod v0.9.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/tools v0.7.0 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/tools v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index 385f335..f859f6f 100644 --- a/go.sum +++ b/go.sum @@ -21,25 +21,25 @@ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4= -golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/mobile v0.0.0-20230906132913-2077a3224571 h1:QDvQ2KLFHHQWRID6IkZOBf6uLIh9tZ0G+mw61pFQxuo= +golang.org/x/mobile v0.0.0-20230906132913-2077a3224571/go.mod h1:wEyOn6VvNW7tcf+bW/wBz1sehi2s2BZ4TimyR7qZen4= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 26985785aa25280bfecda9e395b9f91ae827e959 Mon Sep 17 00:00:00 2001 From: "J. Yi" <93548144+jyyi1@users.noreply.github.com> Date: Fri, 8 Sep 2023 19:45:14 -0400 Subject: [PATCH 3/5] Update intra/packet_proxy.go Co-authored-by: Vinicius Fortuna --- intra/packet_proxy.go | 1 - 1 file changed, 1 deletion(-) diff --git a/intra/packet_proxy.go b/intra/packet_proxy.go index 80a3c6a..2e2cfe8 100644 --- a/intra/packet_proxy.go +++ b/intra/packet_proxy.go @@ -45,7 +45,6 @@ func makeIntraPacketProxy(fakeDNS netip.AddrPort, dns doh.Transport, protector p pl := &transport.UDPPacketListener{ ListenConfig: *protect.MakeListenConfig(protector), - Address: ":0", } // TODO: add timeout option in SDK so we can configure the timeout to 5 minutes From cddafa6395b88610d8e9d6f169b3422c5745cdf7 Mon Sep 17 00:00:00 2001 From: Junyi Yi Date: Mon, 18 Sep 2023 16:28:19 -0400 Subject: [PATCH 4/5] replace queryconn with net.Pipe --- go.mod | 2 +- go.sum | 4 +- intra/android/tun2socks.go | 17 +++--- intra/packet_proxy.go | 7 ++- intra/queryconn.go | 122 ------------------------------------- intra/stream_dialer.go | 44 ++++++++++++- intra/tunnel.go | 14 +++-- 7 files changed, 67 insertions(+), 143 deletions(-) delete mode 100644 intra/queryconn.go diff --git a/go.mod b/go.mod index 8e0d5a5..b811b40 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/Jigsaw-Code/choir v1.0.1 github.com/Jigsaw-Code/getsni v1.0.0 - github.com/Jigsaw-Code/outline-sdk v0.0.2 + github.com/Jigsaw-Code/outline-sdk v0.0.7 github.com/crazy-max/xgo v0.26.0 github.com/eycorsican/go-tun2socks v1.16.11 golang.org/x/mobile v0.0.0-20230906132913-2077a3224571 diff --git a/go.sum b/go.sum index f859f6f..cfe48bb 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/Jigsaw-Code/choir v1.0.1 h1:WeRt6aTn5L+MtRNqRJ+J1RKgoO8CyXXt1dtZghy2K github.com/Jigsaw-Code/choir v1.0.1/go.mod h1:c4Wd1y1PeCajZbKZV+ZmcFGMDoduyqMCEMHW5iqzWXI= github.com/Jigsaw-Code/getsni v1.0.0 h1:OUTIu7wTBi/7DMX+RkZrN7XhU3UDevTEsAWK4gsqSwE= github.com/Jigsaw-Code/getsni v1.0.0/go.mod h1:Ps0Ec3fVMKLyAItVbMKoQFq1lDjtFQXZ+G5nRNNh/QE= -github.com/Jigsaw-Code/outline-sdk v0.0.2 h1:uCuyJMaWj57IYEG/Hdml8YMdk9chU60ZkSxJXBhyGHU= -github.com/Jigsaw-Code/outline-sdk v0.0.2/go.mod h1:hhlKz0+r9wSDFT8usvN8Zv/BFToCIFAUn1P2Qk8G2CM= +github.com/Jigsaw-Code/outline-sdk v0.0.7 h1:WlFaV1tFpIQ/pflrKwrQuNIP3kJpgh7yJuqiTb54sGA= +github.com/Jigsaw-Code/outline-sdk v0.0.7/go.mod h1:hhlKz0+r9wSDFT8usvN8Zv/BFToCIFAUn1P2Qk8G2CM= github.com/crazy-max/xgo v0.26.0 h1:vK4OfeXJoDGvnjlzdTCgPbeWLKENbzj84DTpU/VRonM= github.com/crazy-max/xgo v0.26.0/go.mod h1:m/aqfKaN/cYzfw+Pzk7Mk0tkmShg3/rCS4Zdhdugi4o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/intra/android/tun2socks.go b/intra/android/tun2socks.go index b8bebca..19c09c5 100644 --- a/intra/android/tun2socks.go +++ b/intra/android/tun2socks.go @@ -47,16 +47,18 @@ import ( // // `dohdns` is the initial DoH transport. It must not be `nil`. // `protector` is a wrapper for Android's VpnService.protect() method. -// `listener` will be provided with a summary of each TCP and UDP socket when it is closed. +// `eventListener` will be provided with a summary of each TCP and UDP socket when it is closed. // // Throws an exception if the TUN file descriptor cannot be opened, or if the tunnel fails to // connect. -func ConnectIntraTunnel(fd int, fakedns string, dohdns doh.Transport, protector protect.Protector, listener intra.Listener) (*intra.Tunnel, error) { +func ConnectIntraTunnel( + fd int, fakedns string, dohdns doh.Transport, protector protect.Protector, eventListener intra.Listener, +) (*intra.Tunnel, error) { tun, err := makeTunFile(fd) if err != nil { return nil, err } - t, err := intra.NewTunnel(fakedns, dohdns, tun, protector, listener) + t, err := intra.NewTunnel(fakedns, dohdns, tun, protector, eventListener) if err != nil { return nil, err } @@ -76,14 +78,16 @@ func ConnectIntraTunnel(fd int, fakedns string, dohdns doh.Transport, protector // // `protector` is the socket protector to use for all external network activity. // `auth` will provide a client certificate if required by the TLS server. -// `listener` will be notified after each DNS query succeeds or fails. -func NewDoHTransport(url string, ips string, protector protect.Protector, auth doh.ClientAuth, listener intra.Listener) (doh.Transport, error) { +// `eventListener` will be notified after each DNS query succeeds or fails. +func NewDoHTransport( + url string, ips string, protector protect.Protector, auth doh.ClientAuth, eventListener intra.Listener, +) (doh.Transport, error) { split := []string{} if len(ips) > 0 { split = strings.Split(ips, ",") } dialer := protect.MakeDialer(protector) - return doh.NewTransport(url, split, dialer, auth, listener) + return doh.NewTransport(url, split, dialer, auth, eventListener) } func copyUntilEOF(dst, src io.ReadWriteCloser) { @@ -96,7 +100,6 @@ func copyUntilEOF(dst, src io.ReadWriteCloser) { for { _, err := io.CopyBuffer(dst, src, buf) if err == nil || isErrClosed(err) { - return } } diff --git a/intra/packet_proxy.go b/intra/packet_proxy.go index 2e2cfe8..a83e548 100644 --- a/intra/packet_proxy.go +++ b/intra/packet_proxy.go @@ -38,7 +38,9 @@ type intraPacketProxy struct { var _ network.PacketProxy = (*intraPacketProxy)(nil) -func makeIntraPacketProxy(fakeDNS netip.AddrPort, dns doh.Transport, protector protect.Protector, listener UDPListener) (*intraPacketProxy, error) { +func newIntraPacketProxy( + fakeDNS netip.AddrPort, dns doh.Transport, protector protect.Protector, listener UDPListener, +) (*intraPacketProxy, error) { if dns == nil { return nil, errors.New("dns is required") } @@ -47,9 +49,8 @@ func makeIntraPacketProxy(fakeDNS netip.AddrPort, dns doh.Transport, protector p ListenConfig: *protect.MakeListenConfig(protector), } - // TODO: add timeout option in SDK so we can configure the timeout to 5 minutes // RFC 4787 REQ-5 requires a timeout no shorter than 5 minutes. - pp, err := network.NewPacketProxyFromPacketListener(pl) + pp, err := network.NewPacketProxyFromPacketListener(pl, network.WithPacketListenerWriteIdleTimeout(5*time.Minute)) if err != nil { return nil, fmt.Errorf("failed to create packet proxy from listener: %w", err) } diff --git a/intra/queryconn.go b/intra/queryconn.go deleted file mode 100644 index bedf777..0000000 --- a/intra/queryconn.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2023 Jigsaw Operations LLC -// -// 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 intra - -import ( - "errors" - "io" - "log" - "net" - "time" - - "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh" - "github.com/Jigsaw-Code/outline-sdk/transport" -) - -var errUnsupported = errors.New("feature is not supported") - -// twoWayPipe connects two I/O endpoints (source and dest) bidirectionally. -// The type itself also acts as the dest's [io.ReadWriteCloser]. -type dohQueryServerConn struct { - conn *dohQueryStreamConn -} - -// dohQueryStreamConn is an "outbound" [transport.StreamConn] that handles DNS-over-TCP (DoT) traffic. -// "Outbound" means Reading responses from the remote DoT server, and Writing requests to DoT server. -// -// dohQueryStreamConn also contains a corresponding serverConn, which is an "inbound" [io.ReadWriteCloser]. -// "Inbound" means Reading requests from the client, and Writing responses to the client. -type dohQueryStreamConn struct { - reqReader, respReader *io.PipeReader - reqWriter, respWriter *io.PipeWriter - serverConn io.ReadWriteCloser -} - -var _ io.ReadWriteCloser = (*dohQueryServerConn)(nil) -var _ transport.StreamConn = (*dohQueryStreamConn)(nil) - -func makeDoHQueryStreamConn(dns doh.Transport) (conn *dohQueryStreamConn) { - defer log.Println("[info] DoT over DoH session initialized") - conn = &dohQueryStreamConn{} - conn.reqReader, conn.reqWriter = io.Pipe() - conn.respReader, conn.respWriter = io.Pipe() - conn.serverConn = &dohQueryServerConn{conn} - - go doh.Accept(dns, conn) - return -} - -func (p *dohQueryServerConn) Close() error { - return p.conn.Close() -} - -func (p *dohQueryServerConn) Read(data []byte) (int, error) { - log.Printf("[debug] Sending DoH request (%v bytes)\n", len(data)) - return p.conn.reqReader.Read(data) -} - -func (p *dohQueryServerConn) Write(data []byte) (int, error) { - log.Printf("[debug] Got DoH response (%v bytes)\n", len(data)) - return p.conn.respWriter.Write(data) -} - -func (conn *dohQueryStreamConn) Close() error { - return errors.Join(conn.CloseRead(), conn.CloseWrite()) -} - -func (conn *dohQueryStreamConn) CloseRead() error { - defer log.Println("[info] DoT over DoH read session terminated") - return errors.Join(conn.respReader.Close(), conn.respWriter.Close()) -} - -func (conn *dohQueryStreamConn) CloseWrite() error { - defer log.Println("[info] DoT over DoH write session terminated") - return errors.Join(conn.reqReader.Close(), conn.respWriter.Close()) -} - -func (conn *dohQueryStreamConn) Read(b []byte) (int, error) { - log.Printf("[debug] Got DoT response (%v bytes)\n", len(b)) - return conn.respReader.Read(b) -} - -func (conn *dohQueryStreamConn) Write(b []byte) (int, error) { - log.Printf("[debug] Handling DoT request (%v bytes)\n", len(b)) - return conn.reqWriter.Write(b) -} - -// LocalAddr returns nil. -func (*dohQueryStreamConn) LocalAddr() net.Addr { - return nil -} - -// RemoteAddr returns nil. -func (*dohQueryStreamConn) RemoteAddr() net.Addr { - return nil -} - -// SetDeadline is not supported. -func (*dohQueryStreamConn) SetDeadline(t time.Time) error { - return errUnsupported -} - -// SetReadDeadline is not supported. -func (*dohQueryStreamConn) SetReadDeadline(t time.Time) error { - return errUnsupported -} - -// SetWriteDeadline is not supported. -func (*dohQueryStreamConn) SetWriteDeadline(t time.Time) error { - return errUnsupported -} diff --git a/intra/stream_dialer.go b/intra/stream_dialer.go index ff5140e..d608493 100644 --- a/intra/stream_dialer.go +++ b/intra/stream_dialer.go @@ -41,7 +41,13 @@ type intraStreamDialer struct { var _ transport.StreamDialer = (*intraStreamDialer)(nil) -func makeIntraStreamDialer(fakeDNS netip.AddrPort, dns doh.Transport, protector protect.Protector, listener TCPListener, sniReporter *tcpSNIReporter) (*intraStreamDialer, error) { +func newIntraStreamDialer( + fakeDNS netip.AddrPort, + dns doh.Transport, + protector protect.Protector, + listener TCPListener, + sniReporter *tcpSNIReporter, +) (*intraStreamDialer, error) { if dns == nil { return nil, errors.New("dns is required") } @@ -66,7 +72,9 @@ func (sd *intraStreamDialer) Dial(ctx context.Context, raddr string) (transport. if dest == sd.fakeDNSAddr { log.Println("[debug] Doing DoT request over DoH server...") - return makeDoHQueryStreamConn(*sd.dns.Load()), nil + src, dst := net.Pipe() + go doh.Accept(*sd.dns.Load(), dst) + return newStreamConnFromPipeConns(src, dst) } stats := makeTCPSocketSummary(dest) @@ -107,3 +115,35 @@ func (sd *intraStreamDialer) dial(ctx context.Context, dest netip.AddrPort, stat return tcpsd.Dial(ctx, dest.String()) } } + +// transport.StreamConn wrapper around net.Pipe call + +type pipeconn struct { + net.Conn + remote net.Conn +} + +var _ transport.StreamConn = (*pipeconn)(nil) + +// newStreamConnFromPipeConns creates a new [transport.StreamConn] that wraps around the local [net.Conn]. +// The remote [net.Conn] will be closed when you call CloseRead() on the returned [transport.StreamConn] +func newStreamConnFromPipeConns(local, remote net.Conn) (transport.StreamConn, error) { + if local == nil || remote == nil { + return nil, errors.New("local conn and remote conn are required") + } + return &pipeconn{local, remote}, nil +} + +func (c *pipeconn) Close() error { + return errors.Join(c.CloseRead(), c.CloseWrite()) +} + +// CloseRead makes sure all read on the local conn returns io.EOF, and write on the remote conn returns ErrClosedPipe. +func (c *pipeconn) CloseRead() error { + return c.remote.Close() +} + +// CloseWrite makes sure all read on the remote conn returns io.EOF, and write on the local conn returns ErrClosedPipe. +func (c *pipeconn) CloseWrite() error { + return c.Conn.Close() +} diff --git a/intra/tunnel.go b/intra/tunnel.go index 416ebe6..e059cee 100644 --- a/intra/tunnel.go +++ b/intra/tunnel.go @@ -57,10 +57,12 @@ type Tunnel struct { // These will normally be localhost with a high-numbered port. // // `dohdns` is the initial DOH transport. -// `listener` will be notified at the completion of every tunneled socket. -func NewTunnel(fakedns string, dohdns doh.Transport, tun io.Closer, protector protect.Protector, listener Listener) (t *Tunnel, err error) { - if listener == nil { - return nil, errors.New("listener is required") +// `eventListener` will be notified at the completion of every tunneled socket. +func NewTunnel( + fakedns string, dohdns doh.Transport, tun io.Closer, protector protect.Protector, eventListener Listener, +) (t *Tunnel, err error) { + if eventListener == nil { + return nil, errors.New("eventListener is required") } fakeDNSAddr, err := net.ResolveUDPAddr("udp", fakedns) @@ -75,12 +77,12 @@ func NewTunnel(fakedns string, dohdns doh.Transport, tun io.Closer, protector pr tun: tun, } - t.sd, err = makeIntraStreamDialer(fakeDNSAddr.AddrPort(), dohdns, protector, listener, t.sni) + t.sd, err = newIntraStreamDialer(fakeDNSAddr.AddrPort(), dohdns, protector, eventListener, t.sni) if err != nil { return nil, fmt.Errorf("failed to create stream dialer: %w", err) } - t.pp, err = makeIntraPacketProxy(fakeDNSAddr.AddrPort(), dohdns, protector, listener) + t.pp, err = newIntraPacketProxy(fakeDNSAddr.AddrPort(), dohdns, protector, eventListener) if err != nil { return nil, fmt.Errorf("failed to create packet proxy: %w", err) } From 5356b05e8f4753e74a9d6510f0a51b35f420e215 Mon Sep 17 00:00:00 2001 From: Junyi Yi Date: Tue, 19 Sep 2023 18:00:26 -0400 Subject: [PATCH 5/5] remove logs which might hurt performance --- intra/ip.go | 23 +++++++++++++ intra/ip_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++ intra/packet_proxy.go | 19 +---------- intra/stream_dialer.go | 9 +---- 4 files changed, 101 insertions(+), 26 deletions(-) create mode 100644 intra/ip.go create mode 100644 intra/ip_test.go diff --git a/intra/ip.go b/intra/ip.go new file mode 100644 index 0000000..11bc265 --- /dev/null +++ b/intra/ip.go @@ -0,0 +1,23 @@ +// Copyright 2023 Jigsaw Operations LLC +// +// 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 intra + +import "net/netip" + +// isEquivalentAddrPort checks if addr1 and addr2 are equivalent. More specifically, it will treat +// "ffff::127.0.0.1" (IPv4-in-6) and "127.0.0.1" (IPv4) as equivalent, even though they are "!=" in Go. +func isEquivalentAddrPort(addr1, addr2 netip.AddrPort) bool { + return addr1.Addr().Unmap() == addr2.Addr().Unmap() && addr1.Port() == addr2.Port() +} diff --git a/intra/ip_test.go b/intra/ip_test.go new file mode 100644 index 0000000..ed28660 --- /dev/null +++ b/intra/ip_test.go @@ -0,0 +1,76 @@ +// Copyright 2023 Jigsaw Operations LLC +// +// 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 intra + +import ( + "net/netip" + "testing" +) + +func TestIsEquivalentAddrPort(t *testing.T) { + cases := []struct { + in1, in2 netip.AddrPort + want bool + msg string + }{ + { + in1: netip.MustParseAddrPort("12.34.56.78:80"), + in2: netip.AddrPortFrom(netip.AddrFrom4([4]byte{12, 34, 56, 78}), 80), + want: true, + }, + { + in1: netip.MustParseAddrPort("[fe80::1234:5678]:443"), + in2: netip.AddrPortFrom(netip.AddrFrom16([16]byte{0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78}), 443), + want: true, + }, + { + in1: netip.MustParseAddrPort("0.0.0.0:80"), + in2: netip.MustParseAddrPort("127.0.0.1:80"), + want: false, + }, + { + in1: netip.AddrPortFrom(netip.IPv6Unspecified(), 80), + in2: netip.AddrPortFrom(netip.IPv6Loopback(), 80), + want: false, + }, + { + in1: netip.MustParseAddrPort("127.0.0.1:38880"), + in2: netip.MustParseAddrPort("127.0.0.1:38888"), + want: false, + }, + { + in1: netip.MustParseAddrPort("[2001:db8:85a3:8d3:1319:8a2e:370:7348]:33443"), + in2: netip.MustParseAddrPort("[2001:db8:85a3:8d3:1319:8a2e:370:7348]:33444"), + want: false, + }, + { + in1: netip.MustParseAddrPort("127.0.0.1:8080"), + in2: netip.MustParseAddrPort("[::ffff:127.0.0.1]:8080"), + want: true, + }, + { + in1: netip.AddrPortFrom(netip.IPv6Loopback(), 80), + in2: netip.MustParseAddrPort("127.0.0.1:80"), + want: false, + }, + } + + for _, tc := range cases { + actual := isEquivalentAddrPort(tc.in1, tc.in2) + if actual != tc.want { + t.Fatalf(`"%v" == "%v"? want %v, actual %v`, tc.in1, tc.in2, tc.want, actual) + } + } +} diff --git a/intra/packet_proxy.go b/intra/packet_proxy.go index a83e548..f835042 100644 --- a/intra/packet_proxy.go +++ b/intra/packet_proxy.go @@ -17,7 +17,6 @@ package intra import ( "errors" "fmt" - "log" "net" "net/netip" "sync/atomic" @@ -67,9 +66,6 @@ func newIntraPacketProxy( // NewSession implements PacketProxy.NewSession. func (p *intraPacketProxy) NewSession(resp network.PacketResponseReceiver) (network.PacketRequestSender, error) { - log.Println("[debug] initializing a new UDP session...") - defer log.Println("[info] New UDP session initialized") - dohResp := &dohPacketRespReceiver{ PacketResponseReceiver: resp, stats: makeTracker(), @@ -77,7 +73,6 @@ func (p *intraPacketProxy) NewSession(resp network.PacketResponseReceiver) (netw } req, err := p.proxy.NewSession(dohResp) if err != nil { - log.Printf("[error] failed to create UDP session: %v\n", err) return nil, fmt.Errorf("failed to create new session: %w", err) } @@ -119,41 +114,31 @@ var _ network.PacketResponseReceiver = (*dohPacketRespReceiver)(nil) // WriteTo implements PacketRequestSender.WriteTo. It will query the DoH server if the packet a DNS packet. func (req *dohPacketReqSender) WriteTo(p []byte, destination netip.AddrPort) (int, error) { - log.Printf("[debug] Sending raw UDP packet (%v bytes) to %v\n", len(p), destination) - - if destination == req.proxy.fakeDNSAddr { + if isEquivalentAddrPort(destination, req.proxy.fakeDNSAddr) { defer func() { // conn was only used for this DNS query, so it's unlikely to be used again if req.stats.download.Load() == 0 && req.stats.upload.Load() == 0 { - log.Println("[debug] DoH dedicated session finished, Closing...") req.Close() } }() - log.Println("[debug] Doing DNS request over DoH server...") resp, err := (*req.proxy.dns.Load()).Query(p) if err != nil { - log.Printf("[error] DoH request failed: %v\n", err) return 0, fmt.Errorf("DoH request error: %w", err) } if len(resp) == 0 { - log.Println("[error] DoH response is empty") return 0, errors.New("empty DoH response") } - log.Printf("[info] Write DoH response (%v bytes) from %v\n", len(resp), req.proxy.fakeDNSAddr) return req.response.writeFrom(resp, net.UDPAddrFromAddrPort(req.proxy.fakeDNSAddr), false) } - log.Printf("[debug] UDP Session: upload %v bytes to %v\n", len(p), destination) req.stats.upload.Add(int64(len(p))) return req.PacketRequestSender.WriteTo(p, destination) } // Close terminates the UDP session, and reports session stats to the listener. func (resp *dohPacketRespReceiver) Close() error { - log.Println("[debug] UDP session terminating...") - defer log.Printf("[info] UDP session terminated: down = %v, up = %v\n", resp.stats.download.Load(), resp.stats.upload.Load()) if resp.listener != nil { resp.listener.OnUDPSocketClosed(&UDPSocketSummary{ Duration: int32(time.Since(resp.stats.start)), @@ -166,7 +151,6 @@ func (resp *dohPacketRespReceiver) Close() error { // WriteFrom implements PacketResponseReceiver.WriteFrom. func (resp *dohPacketRespReceiver) WriteFrom(p []byte, source net.Addr) (int, error) { - log.Printf("[debug] Receiving raw UDP packet (%v bytes) from %v\n", len(p), source) return resp.writeFrom(p, source, true) } @@ -174,7 +158,6 @@ func (resp *dohPacketRespReceiver) WriteFrom(p []byte, source net.Addr) (int, er // It will also add len(p) to downloadBytes if doStat is true. func (resp *dohPacketRespReceiver) writeFrom(p []byte, source net.Addr, doStat bool) (int, error) { if doStat { - log.Printf("[debug] UDP Session: download %v bytes from %v\n", len(p), source) resp.stats.download.Add(int64(len(p))) } return resp.PacketResponseReceiver.WriteFrom(p, source) diff --git a/intra/stream_dialer.go b/intra/stream_dialer.go index d608493..1dcd5d8 100644 --- a/intra/stream_dialer.go +++ b/intra/stream_dialer.go @@ -18,7 +18,6 @@ import ( "context" "errors" "fmt" - "log" "net" "net/netip" "sync/atomic" @@ -64,14 +63,12 @@ func newIntraStreamDialer( // Dial implements StreamDialer.Dial. func (sd *intraStreamDialer) Dial(ctx context.Context, raddr string) (transport.StreamConn, error) { - log.Printf("[debug] Dialing TCP traffic to %v\n", raddr) dest, err := netip.ParseAddrPort(raddr) if err != nil { return nil, fmt.Errorf("invalid raddr (%v): %w", raddr, err) } - if dest == sd.fakeDNSAddr { - log.Println("[debug] Doing DoT request over DoH server...") + if isEquivalentAddrPort(dest, sd.fakeDNSAddr) { src, dst := net.Pipe() go doh.Accept(*sd.dns.Load(), dst) return newStreamConnFromPipeConns(src, dst) @@ -98,17 +95,13 @@ func (sd *intraStreamDialer) SetDNS(dns doh.Transport) error { func (sd *intraStreamDialer) dial(ctx context.Context, dest netip.AddrPort, stats *TCPSocketSummary) (transport.StreamConn, error) { if dest.Port() == 443 { - log.Println("[debug] Dialing HTTPS traffic") if sd.alwaysSplitHTTPS.Load() { - log.Println("[debug] Dialing TCP traffic over split dialer") return split.DialWithSplit(sd.dialer, net.TCPAddrFromAddrPort(dest)) } else { - log.Println("[debug] Dialing TCP traffic over retryable split dialer") stats.Retry = &split.RetryStats{} return split.DialWithSplitRetry(sd.dialer, net.TCPAddrFromAddrPort(dest), stats.Retry) } } else { - log.Println("[debug] Dialing TCP traffic directly over internet") tcpsd := &transport.TCPStreamDialer{ Dialer: *sd.dialer, }