diff --git a/transport/packet.go b/transport/packet.go index d612f970..bddb609d 100644 --- a/transport/packet.go +++ b/transport/packet.go @@ -57,8 +57,8 @@ func (e *PacketDialerEndpoint) Connect(ctx context.Context) (net.Conn, error) { // PacketDialer provides a way to dial a destination and establish datagram connections. type PacketDialer interface { - // Dial connects to `raddr`. - // `raddr` has the form `host:port`, where `host` can be a domain name or IP address. + // Dial connects to `addr`. + // `addr` has the form `host:port`, where `host` can be a domain name or IP address. Dial(ctx context.Context, addr string) (net.Conn, error) } diff --git a/x/outline-cli/main.go b/x/outline-cli/main.go index 72f1feee..27efd91b 100644 --- a/x/outline-cli/main.go +++ b/x/outline-cli/main.go @@ -16,18 +16,12 @@ package main import ( "fmt" - "io" "net" "os" "os/signal" "strconv" - "time" + "sync" - "github.com/Jigsaw-Code/outline-internal-sdk/network" - "github.com/Jigsaw-Code/outline-internal-sdk/network/dnstruncate" - "github.com/Jigsaw-Code/outline-internal-sdk/network/lwip2transport" - "github.com/Jigsaw-Code/outline-internal-sdk/transport" - "github.com/Jigsaw-Code/outline-internal-sdk/transport/shadowsocks" "github.com/songgao/water" "github.com/vishvananda/netlink" "golang.org/x/sys/unix" @@ -48,7 +42,7 @@ const OUTLINE_ROUTING_TABLE = 233 // : the outline server port (e.g. 21532) // : the outline server password func main() { - fmt.Println("OutlineVPN CLI (experimental-07211507)") + fmt.Println("OutlineVPN CLI (experimental-08031526)") svrIp := os.Args[1] svrIpCidr := svrIp + "/32" @@ -63,8 +57,8 @@ func main() { return } - // wait for go routine to terminate - defer time.Sleep(1 * time.Second) + bgWait := &sync.WaitGroup{} + defer bgWait.Wait() tun, err := setupTunDevice() if err != nil { @@ -82,6 +76,28 @@ func main() { return } + ss, err := NewOutlineDevice(&OutlineConfig{ + Hostname: svrIp, + Port: uint16(svrPort), + Password: svrPass, + Cipher: "chacha20-ietf-poly1305", + }) + if err != nil { + fmt.Printf("fatal error: %v", err) + return + } + defer ss.Close() + + ss.Refresh() + + bgWait.Add(1) + go func() { + defer bgWait.Done() + if err := ss.RelayTraffic(tun); err != nil { + fmt.Printf("Traffic bridge destroyed: %v\n", err) + } + }() + err = setupRouting() if err != nil { return @@ -102,30 +118,6 @@ func main() { return } - t2s, err := startTun2Socks(tun, svrIp, svrPass, svrPort) - if err != nil { - return - } - defer stopTun2Socks(t2s) - - go func() { - fmt.Printf("debug: start receiving data from tun %v\n", tun.Name()) - if _, err := io.Copy(t2s, tun); err != nil { - fmt.Printf("warning: failed to write data to network stack: %v\n", err) - } else { - fmt.Printf("debug: %v -> t2s eof\n", tun.Name()) - } - }() - - go func() { - fmt.Printf("debug: start forwarding t2s data to tun %v\n", tun.Name()) - if _, err := io.Copy(tun, t2s); err != nil { - fmt.Printf("warning: failed to forward t2s data to tun: %v\n", err) - } else { - fmt.Printf("debug: t2s -> %v eof\n", tun.Name()) - } - }() - sigc := make(chan os.Signal, 1) signal.Notify(sigc, os.Interrupt, unix.SIGTERM, unix.SIGHUP) s := <-sigc @@ -332,59 +324,3 @@ func cleanUpRule(rule *netlink.Rule) error { fmt.Println("ip rule of routing table deleted") return nil } - -func startTun2Socks(tun *water.Interface, ip, pass string, port int) (network.IPDevice, error) { - fmt.Println("starting outline-go-tun2socks...") - - cipher, err := shadowsocks.NewEncryptionKey("chacha20-ietf-poly1305", pass) - if err != nil { - fmt.Printf("fatal error: failed to create Shadowsocks cipher, %v\n", err) - return nil, err - } - - proxyIP, err := net.ResolveIPAddr("ip", ip) - if err != nil { - fmt.Printf("fatal error: failed to resolve proxy address, %v\n", err) - return nil, err - } - proxyAddress := net.JoinHostPort(proxyIP.String(), fmt.Sprint(port)) - - sd, err := shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: proxyAddress}, cipher) - if err != nil { - fmt.Printf("fatal error: failed to create StreamDialer, %v\n", err) - return nil, err - } - - // pl, err := shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: proxyAddress}, cipher) - // pl, err := dnsovertcp.NewPacketListener(sd) - // pl, err := dnstruncate.NewPacketListener() - if err != nil { - fmt.Printf("fatal error: failed to create PacketListener, %v\n", err) - return nil, err - } - // ph, err := network.NewPacketProxyFromPacketListener(pl) - ph, err := dnstruncate.NewPacketProxy() - if err != nil { - fmt.Printf("fatal error: failed to create PacketProxy, %v\n", err) - return nil, err - } - - t2s, err := lwip2transport.ConfigureDevice(sd, ph) - if err != nil { - fmt.Printf("fatal error: failed to create Tun2Socks device, %v\n", err) - return nil, err - } - - fmt.Println("lwIP tun2socks created") - return t2s, nil -} - -func stopTun2Socks(t2s network.IPDevice) error { - fmt.Println("stopping outline-go-tun2socks...") - err := t2s.Close() - if err != nil { - fmt.Printf("fatal error: %v\n", err) - } - fmt.Println("outline-go-tun2socks stopped") - return err -} diff --git a/x/outline-cli/outline_device.go b/x/outline-cli/outline_device.go new file mode 100644 index 00000000..454b0282 --- /dev/null +++ b/x/outline-cli/outline_device.go @@ -0,0 +1,157 @@ +// 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 +// +// https://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 main + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "strconv" + "sync" + + "github.com/Jigsaw-Code/outline-internal-sdk/network" + "github.com/Jigsaw-Code/outline-internal-sdk/network/dnstruncate" + "github.com/Jigsaw-Code/outline-internal-sdk/network/lwip2transport" + "github.com/Jigsaw-Code/outline-internal-sdk/transport" + "github.com/Jigsaw-Code/outline-internal-sdk/transport/shadowsocks" + "github.com/Jigsaw-Code/outline-internal-sdk/x/connectivity" +) + +const ( + connectivityTestDomain = "www.google.com" + connectivityTestResolver = "1.1.1.1:53" +) + +type OutlineConfig struct { + Hostname string + Port uint16 + Password string + Cipher string +} + +type OutlineDevice struct { + t2s network.IPDevice + pktProxy network.DelegatePacketProxy + fallbackPktProxy network.PacketProxy + ssStreamDialer transport.StreamDialer + ssPktListener transport.PacketListener + ssPktProxy network.PacketProxy +} + +func NewOutlineDevice(config *OutlineConfig) (od *OutlineDevice, err error) { + od = &OutlineDevice{} + + cipher, err := shadowsocks.NewEncryptionKey(config.Cipher, config.Password) + if err != nil { + return nil, fmt.Errorf("failed to create cipher `%v`: %w", config.Cipher, err) + } + + ssAddress := net.JoinHostPort(config.Hostname, strconv.Itoa(int(config.Port))) + + // Create Shadowsocks TCP StreamDialer + od.ssStreamDialer, err = shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: ssAddress}, cipher) + if err != nil { + return nil, fmt.Errorf("failed to create TCP dialer: %w", err) + } + + // Create DNS Truncated PacketProxy + od.fallbackPktProxy, err = dnstruncate.NewPacketProxy() + if err != nil { + return nil, fmt.Errorf("failed to create DNS truncate proxy: %w", err) + } + + // Create Shadowsocks UDP PacketProxy + od.ssPktListener, err = shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: ssAddress}, cipher) + if err != nil { + return nil, fmt.Errorf("failed to create UDP listener: %w", err) + } + + od.ssPktProxy, err = network.NewPacketProxyFromPacketListener(od.ssPktListener) + if err != nil { + return nil, fmt.Errorf("failed to create UDP proxy: %w", err) + } + + // Create DelegatePacketProxy + od.pktProxy, err = network.NewDelegatePacketProxy(od.fallbackPktProxy) + if err != nil { + return nil, fmt.Errorf("failed to create delegate UDP proxy: %w", err) + } + + // Configure lwIP Device + od.t2s, err = lwip2transport.ConfigureDevice(od.ssStreamDialer, od.pktProxy) + if err != nil { + return nil, fmt.Errorf("failed to configure lwIP: %w", err) + } + + return +} + +func (d *OutlineDevice) Close() error { + return d.t2s.Close() +} + +func (d *OutlineDevice) Refresh() error { + fmt.Println("debug: testing TCP connectivity...") + streamResolver := &transport.StreamDialerEndpoint{Dialer: d.ssStreamDialer, Address: connectivityTestResolver} + _, err := connectivity.TestResolverStreamConnectivity(context.Background(), streamResolver, connectivityTestDomain) + if err != nil { + return fmt.Errorf("failed to connect to the remote Shadowsocks server: %w", err) + } + + fmt.Println("debug: testing UDP connectivity...") + dialer := transport.PacketListenerDialer{Listener: d.ssPktListener} + packetResolver := &transport.PacketDialerEndpoint{Dialer: dialer, Address: connectivityTestResolver} + _, err = connectivity.TestResolverPacketConnectivity(context.Background(), packetResolver, connectivityTestDomain) + fmt.Printf("debug: UDP connectivity test result: %v\n", err) + + if err != nil { + fmt.Println("info: remote Shadowsocks server doesn't support UDP, switching to local DNS truncation handler") + return d.pktProxy.SetProxy(d.fallbackPktProxy) + } else { + fmt.Println("info: remote Shadowsocks server supports UDP traffic") + return d.pktProxy.SetProxy(d.ssPktProxy) + } +} + +func (d *OutlineDevice) RelayTraffic(netDev io.ReadWriter) error { + var err1, err2 error + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + + fmt.Println("debug: OutlineDevice start receiving data from tun") + if _, err2 = io.Copy(d.t2s, netDev); err2 != nil { + fmt.Printf("warning: failed to write data to OutlineDevice: %v\n", err2) + } else { + fmt.Println("debug: tun -> OutlineDevice eof") + } + }() + + fmt.Println("debug: start forwarding OutlineDevice data to tun") + if _, err1 = io.Copy(netDev, d.t2s); err1 != nil { + fmt.Printf("warning: failed to forward OutlineDevice data to tun: %v\n", err1) + } else { + fmt.Println("debug: OutlineDevice -> tun eof") + } + + wg.Wait() + + return errors.Join(err1, err2) +}