diff --git a/go.mod b/go.mod index a70bdb7..98fcb5e 100644 --- a/go.mod +++ b/go.mod @@ -5,19 +5,25 @@ 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-internal-sdk v0.0.0-20230503153405-c99d818ff195 + github.com/Jigsaw-Code/outline-sdk v0.0.2 + github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20230807220427-893de7fdc6b8 github.com/crazy-max/xgo v0.26.0 github.com/eycorsican/go-tun2socks v1.16.11 + github.com/stretchr/testify v1.8.2 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/net v0.10.0 + golang.org/x/sys v0.8.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/miekg/dns v1.1.54 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect 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.9.0 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/tools v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0d7beb6..fa9e6c6 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,24 @@ 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-internal-sdk v0.0.0-20230503153405-c99d818ff195 h1:9qSHRhYNHgnxzJPFotkiAhMcJGXJFJ6j1TWZKWgHwzQ= -github.com/Jigsaw-Code/outline-internal-sdk v0.0.0-20230503153405-c99d818ff195/go.mod h1:vxtE3esaFy5UG6TnipLyWx0esUQBy9LBXHLQx+SoER8= +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/x v0.0.0-20230807220427-893de7fdc6b8 h1:BxOHmmuppPM8K0DGUsfvajKF4PKfGxv9boNDhmbszFU= +github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20230807220427-893de7fdc6b8/go.mod h1:tBqJXpVm+kym+EAUdwNodcFxy872FfjVErfj8Br+gs0= 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eycorsican/go-tun2socks v1.16.11 h1:+hJDNgisrYaGEqoSxhdikMgMJ4Ilfwm/IZDrWRrbaH8= github.com/eycorsican/go-tun2socks v1.16.11/go.mod h1:wgB2BFT8ZaPKyKOQ/5dljMG/YIow+AIXyq4KBwJ5sGQ= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI= +github.com/miekg/dns v1.1.54/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28= @@ -17,29 +27,39 @@ github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHk github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= 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/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.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.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.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.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/outline/client.go b/outline/client.go index 85b8811..92806ca 100644 --- a/outline/client.go +++ b/outline/client.go @@ -19,7 +19,6 @@ import ( ) // Client provides a transparent container for [transport.StreamDialer] and [transport.PacketListener] -// that is exportable (as an opaque object) via gobind. // It's used by the connectivity test and the tun2socks handlers. type Client struct { transport.StreamDialer diff --git a/outline/connectivity/connectivity.go b/outline/connectivity/connectivity.go index e6692c5..58f1bee 100644 --- a/outline/connectivity/connectivity.go +++ b/outline/connectivity/connectivity.go @@ -48,15 +48,26 @@ type reachabilityError struct { // the current network. Parallelizes the execution of TCP and UDP checks, selects the appropriate // error code to return accounting for transient network failures. // Returns an error if an unexpected error ocurrs. +// +// Deprecated: keep for backward compatibility only. func CheckConnectivity(client *outline.Client) (neterrors.Error, error) { + return CheckTCPAndUDPConnectivity(client, client) +} + +// CheckTCPAndUDPConnectivity determines whether the StreamDialer `sd` and +// PacketListener `pl` relay TCP and UDP traffic under the current network. +// Parallelizes the execution of TCP and UDP checks, selects the appropriate +// error code to return accounting for transient network failures. +// Returns an error if an unexpected error ocurrs. +func CheckTCPAndUDPConnectivity(sd transport.StreamDialer, pl transport.PacketListener) (neterrors.Error, error) { // Start asynchronous UDP support check. udpChan := make(chan error) go func() { resolverAddr := &net.UDPAddr{IP: net.ParseIP("1.1.1.1"), Port: 53} - udpChan <- CheckUDPConnectivityWithDNS(client, resolverAddr) + udpChan <- CheckUDPConnectivityWithDNS(pl, resolverAddr) }() // Check whether the proxy is reachable and that the client is able to authenticate to the proxy - tcpErr := CheckTCPConnectivityWithHTTP(client, "http://example.com") + tcpErr := CheckTCPConnectivityWithHTTP(sd, "http://example.com") if tcpErr == nil { udpErr := <-udpChan if udpErr == nil { diff --git a/outline/electron/main.go b/outline/electron/main.go index 921e6b9..13e22d9 100644 --- a/outline/electron/main.go +++ b/outline/electron/main.go @@ -24,6 +24,8 @@ import ( "syscall" "time" + "github.com/Jigsaw-Code/outline-go-tun2socks/outline" + "github.com/Jigsaw-Code/outline-go-tun2socks/outline/connectivity" "github.com/Jigsaw-Code/outline-go-tun2socks/outline/internal/utf8" "github.com/Jigsaw-Code/outline-go-tun2socks/outline/neterrors" "github.com/Jigsaw-Code/outline-go-tun2socks/outline/shadowsocks" @@ -91,14 +93,81 @@ func main() { setLogLevel(*args.logLevel) - client, err := newShadowsocksClientFromArgs() + if jsonConfig := *args.proxyConfig; len(jsonConfig) > 0 { + startTunnelWithJsonConfig(jsonConfig) + } else { + startLegacyShadowsocksClient() + } + + log.Infof("tun2socks running...") + + osSignals := make(chan os.Signal, 1) + signal.Notify(osSignals, os.Interrupt, os.Kill, syscall.SIGTERM, syscall.SIGHUP) + sig := <-osSignals + log.Debugf("Received signal: %v", sig) +} + +func setLogLevel(level string) { + switch strings.ToLower(level) { + case "debug": + log.SetLevel(log.DEBUG) + case "info": + log.SetLevel(log.INFO) + case "warn": + log.SetLevel(log.WARN) + case "error": + log.SetLevel(log.ERROR) + case "none": + log.SetLevel(log.NONE) + default: + log.SetLevel(log.INFO) + } +} + +func startTunnelWithJsonConfig(jsonConfig string) { + if *args.checkConnectivity { + log.Errorf("Connectivity test is not supported for json config") + os.Exit(neterrors.Unexpected.Number()) + } + + // Open TUN device + dnsResolvers := strings.Split(*args.tunDNS, ",") + tunDevice, err := tun.OpenTunDevice(*args.tunName, *args.tunAddr, *args.tunGw, *args.tunMask, dnsResolvers, persistTun) + if err != nil { + log.Errorf("Failed to open TUN device: %v", err) + os.Exit(neterrors.SystemMisconfigured.Number()) + } + + if _, err := tun2socks.ConnectTunnel(jsonConfig, tunDevice); err != nil { + log.Errorf("Failed to create Tunnel from config: %v", err) + os.Exit(neterrors.IllegalConfiguration.Number()) + } +} + +func startLegacyShadowsocksClient() { + // legacy raw flags + config := shadowsocks.Config{ + Host: *args.proxyHost, + Port: *args.proxyPort, + CipherName: *args.proxyCipher, + Password: *args.proxyPassword, + } + if prefixStr := *args.proxyPrefix; len(prefixStr) > 0 { + if p, err := utf8.DecodeUTF8CodepointsToRawBytes(prefixStr); err != nil { + log.Errorf("Failed to parse prefix string: %w", err) + os.Exit(neterrors.IllegalConfiguration.Number()) + } else { + config.Prefix = p + } + } + client, err := shadowsocks.NewClient(&config) if err != nil { log.Errorf("Failed to create Shadowsocks client: %v", err) os.Exit(neterrors.IllegalConfiguration.Number()) } if *args.checkConnectivity { - connErrCode, err := shadowsocks.CheckConnectivity(client) + connErrCode, err := connectivity.CheckConnectivity((*outline.Client)(client)) log.Debugf("Connectivity checks error code: %v", connErrCode) if err != nil { log.Errorf("Failed to perform connectivity checks: %v", err) @@ -135,52 +204,4 @@ func main() { os.Exit(neterrors.Unexpected.Number()) } }() - - log.Infof("tun2socks running...") - - osSignals := make(chan os.Signal, 1) - signal.Notify(osSignals, os.Interrupt, os.Kill, syscall.SIGTERM, syscall.SIGHUP) - sig := <-osSignals - log.Debugf("Received signal: %v", sig) -} - -func setLogLevel(level string) { - switch strings.ToLower(level) { - case "debug": - log.SetLevel(log.DEBUG) - case "info": - log.SetLevel(log.INFO) - case "warn": - log.SetLevel(log.WARN) - case "error": - log.SetLevel(log.ERROR) - case "none": - log.SetLevel(log.NONE) - default: - log.SetLevel(log.INFO) - } -} - -// newShadowsocksClientFromArgs creates a new shadowsocks.Client instance -// from the global CLI argument object args. -func newShadowsocksClientFromArgs() (*shadowsocks.Client, error) { - if jsonConfig := *args.proxyConfig; len(jsonConfig) > 0 { - return shadowsocks.NewClientFromJSON(jsonConfig) - } else { - // legacy raw flags - config := shadowsocks.Config{ - Host: *args.proxyHost, - Port: *args.proxyPort, - CipherName: *args.proxyCipher, - Password: *args.proxyPassword, - } - if prefixStr := *args.proxyPrefix; len(prefixStr) > 0 { - if p, err := utf8.DecodeUTF8CodepointsToRawBytes(prefixStr); err != nil { - return nil, fmt.Errorf("Failed to parse prefix string: %w", err) - } else { - config.Prefix = p - } - } - return shadowsocks.NewClient(&config) - } } diff --git a/outline/internal/sdk/config.go b/outline/internal/sdk/config.go new file mode 100644 index 0000000..d77f3a5 --- /dev/null +++ b/outline/internal/sdk/config.go @@ -0,0 +1,96 @@ +// Copyright 2022 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. + +package sdk + +import ( + "encoding/json" + "fmt" + + "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" +) + +// TODO: move to "outline-apps/internal" once we have migrated to monorepo + +// An internal data structure to be used by Outline Shadowsocks transports +type sessionConfig struct { + Hostname string + Port int + CryptoKey *shadowsocks.EncryptionKey + Prefix []byte +} + +// An internal data structure to be used by JSON deserialization. +// Must match the ShadowsocksSessionConfig interface defined in Outline Client. +type configJSON struct { + Host string `json:"host"` + Port uint16 `json:"port"` + Password string `json:"password"` + Method string `json:"method"` + Prefix string `json:"prefix"` +} + +// ParseConfigFromJSON parses a JSON string `in` as a configJSON object. +// The JSON string `in` must match the ShadowsocksSessionConfig interface +// defined in Outline Client. +func parseConfigFromJSON(in string) (config *sessionConfig, err error) { + var confJson configJSON + if err = json.Unmarshal([]byte(in), &confJson); err != nil { + return nil, err + } + + config = &sessionConfig{ + Hostname: confJson.Host, + Port: int(confJson.Port), + } + if config.CryptoKey, err = shadowsocks.NewEncryptionKey(confJson.Method, confJson.Password); err != nil { + return nil, fmt.Errorf("invalid Outline configuration: %w", err) + } + if len(confJson.Prefix) > 0 { + if config.Prefix, err = parseStringPrefix(confJson.Prefix); err != nil { + return nil, fmt.Errorf("invalid Outline configuration Prefix: %w", err) + } + } + + if err = validateConfig(config); err != nil { + return nil, fmt.Errorf("invalid Outline configuration: %w", err) + } + return config, nil +} + +// validateConfig validates whether a Shadowsocks server configuration is valid +// (it won't do any connectivity tests) +// +// Returns nil if it is valid; or an error message. +func validateConfig(config *sessionConfig) error { + if len(config.Hostname) == 0 { + return fmt.Errorf("must provide a host name or IP address") + } + if config.Port <= 0 || config.Port > 65535 { + return fmt.Errorf("port must be within range [1..65535]") + } + return nil +} + +func parseStringPrefix(utf8Str string) ([]byte, error) { + runes := []rune(utf8Str) + rawBytes := make([]byte, len(runes)) + for i, r := range runes { + if (r & 0xFF) != r { + return nil, fmt.Errorf("character out of range: %d", r) + } + rawBytes[i] = byte(r) + } + return rawBytes, nil +} diff --git a/outline/internal/sdk/config_test.go b/outline/internal/sdk/config_test.go new file mode 100644 index 0000000..c503c59 --- /dev/null +++ b/outline/internal/sdk/config_test.go @@ -0,0 +1,149 @@ +// Copyright 2023 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. + +package sdk + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_ParseConfigFromJSON(t *testing.T) { + tests := []struct { + name string + input string + expectErr bool + expectHost string + expectPort int + expectPrefix []byte + }{ + { + name: "normal config", + input: `{"host":"192.0.2.1","port":12345,"method":"chacha20-ietf-poly1305","password":"abcd1234"}`, + expectHost: "192.0.2.1", + expectPort: 12345, + }, + { + name: "normal config with prefix", + input: `{"host":"192.0.2.1","port":12345,"method":"aes-128-gcm","password":"abcd1234","prefix":"abc 123"}`, + expectHost: "192.0.2.1", + expectPort: 12345, + expectPrefix: []byte("abc 123"), + }, + { + name: "normal config with extra fields", + input: `{"extra_field":"ignored","host":"192.0.2.1","port":12345,"method":"aes-192-gcm","password":"abcd1234"}`, + expectHost: "192.0.2.1", + expectPort: 12345, + }, + { + name: "unprintable prefix", + input: `{"host":"192.0.2.1","port":12345,"method":"AES-256-gcm","password":"abcd1234","prefix":"abc 123","prefix":"\u0000\u0080\u00ff"}`, + expectHost: "192.0.2.1", + expectPort: 12345, + expectPrefix: []byte{0x00, 0x80, 0xff}, + }, + { + name: "multi-byte utf-8 prefix", + input: `{"host":"192.0.2.1","port":12345,"method":"chacha20-ietf-poly1305","password":"abcd1234","prefix":"abc 123","prefix":"` + "\xc2\x80\xc2\x81\xc3\xbd\xc3\xbf" + `"}`, + expectHost: "192.0.2.1", + expectPort: 12345, + expectPrefix: []byte{0x80, 0x81, 0xfd, 0xff}, + }, + { + name: "missing host", + input: `{"port":12345,"method":"AES-128-GCM","password":"abcd1234"}`, + expectHost: "", + expectPort: 12345, + }, + { + name: "missing port", + input: `{"host":"192.0.2.1","method":"aes-192-gcm","password":"abcd1234"}`, + expectHost: "192.0.2.1", + expectPort: 0, + }, + { + name: "missing method", + input: `{"host":"192.0.2.1","port":12345,"password":"abcd1234"}`, + expectHost: "192.0.2.1", + expectPort: 12345, + }, + { + name: "missing password", + input: `{"host":"192.0.2.1","port":12345,"method":"chacha20-ietf-poly1305"}`, + expectHost: "192.0.2.1", + expectPort: 12345, + }, + { + name: "empty host", + input: `{"host":"","port":12345,"method":"chacha20-ietf-poly1305","password":"abcd1234"}`, + expectHost: "", + expectPort: 12345, + }, + { + name: "zero port", + input: `{"host":"192.0.2.1","port":0,"method":"chacha20-ietf-poly1305","password":"abcd1234"}`, + expectHost: "192.0.2.1", + expectPort: 0, + }, + { + name: "empty method", + input: `{"host":"192.0.2.1","port":12345,"method":"","password":"abcd1234"}`, + expectHost: "192.0.2.1", + expectPort: 12345, + }, + { + name: "empty password", + input: `{"host":"192.0.2.1","port":12345,"method":"chacha20-ietf-poly1305","password":""}`, + expectHost: "192.0.2.1", + expectPort: 12345, + }, + { + name: "empty prefix", + input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234","prefix":""}`, + expectHost: "192.0.2.1", + expectPort: 12345, + }, + { + name: "port -1", + input: `{"host":"192.0.2.1","port":-1,"method":"aes-128-gcm","password":"abcd1234"}`, + expectErr: true, + }, + { + name: "port 65536", + input: `{"host":"192.0.2.1","port":65536,"method":"aes-128-gcm","password":"abcd1234"}`, + expectErr: true, + }, + { + name: "prefix out-of-range", + input: `{"host":"192.0.2.1","port":8080,"method":"aes-128-gcm","password":"abcd1234","prefix":"\x1234"}`, + expectErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseConfigFromJSON(tt.input) + if tt.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectHost, got.Hostname) + require.Equal(t, tt.expectPort, got.Port) + require.NotNil(t, got.CryptoKey) + require.Equal(t, tt.expectPrefix, got.Prefix) + } + }) + } +} diff --git a/outline/internal/sdk/outline_device.go b/outline/internal/sdk/outline_device.go new file mode 100644 index 0000000..bdd7e09 --- /dev/null +++ b/outline/internal/sdk/outline_device.go @@ -0,0 +1,65 @@ +// Copyright 2022 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. + +package sdk + +import ( + "fmt" + + "github.com/Jigsaw-Code/outline-sdk/network" + "github.com/Jigsaw-Code/outline-sdk/network/lwip2transport" + "github.com/Jigsaw-Code/outline-sdk/transport" +) + +// TODO: move to "outline-apps/internal" once we have migrated to monorepo + +const ( + connectivityTestDNSResolver = "1.1.1.1:53" + connectivityTestTargetDomain = "www.google.com" +) + +type OutlineClientDevice struct { + t2s network.IPDevice + pp *outlinePacketProxy + sd transport.StreamDialer +} + +func NewOutlineClientDevice(configJSON string) (d *OutlineClientDevice, err error) { + d = &OutlineClientDevice{} + + d.sd, err = NewOutlineStreamDialer(configJSON) + if err != nil { + return nil, fmt.Errorf("failed to create TCP dialer: %w", err) + } + + d.pp, err = newOutlinePacketProxy(configJSON) + if err != nil { + return nil, fmt.Errorf("failed to create UDP proxy: %w", err) + } + + d.t2s, err = lwip2transport.ConfigureDevice(d.sd, d.pp) + if err != nil { + return nil, fmt.Errorf("failed to configure lwIP: %w", err) + } + + return +} + +func (d *OutlineClientDevice) Close() error { + return d.t2s.Close() +} + +func (d *OutlineClientDevice) Refresh() error { + return d.pp.testConnectivityAndRefresh(connectivityTestDNSResolver, connectivityTestTargetDomain) +} diff --git a/outline/internal/sdk/packet_listener.go b/outline/internal/sdk/packet_listener.go new file mode 100644 index 0000000..17dc676 --- /dev/null +++ b/outline/internal/sdk/packet_listener.go @@ -0,0 +1,37 @@ +// Copyright 2022 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. + +package sdk + +import ( + "fmt" + "net" + "strconv" + + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" +) + +// TODO: move to "outline-apps/internal" once we have migrated to monorepo + +// NewOutlinePacketListener creates an outline Shadowsocks PacketListener from the JSON config. +func NewOutlinePacketListener(configJSON string) (transport.PacketListener, error) { + config, err := parseConfigFromJSON(configJSON) + if err != nil { + return nil, fmt.Errorf("invalid Outline configuration: %w", err) + } + + ssAddress := net.JoinHostPort(config.Hostname, strconv.Itoa(config.Port)) + return shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: ssAddress}, config.CryptoKey) +} diff --git a/outline/internal/sdk/packet_proxy.go b/outline/internal/sdk/packet_proxy.go new file mode 100644 index 0000000..aa2072a --- /dev/null +++ b/outline/internal/sdk/packet_proxy.go @@ -0,0 +1,75 @@ +// Copyright 2022 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. + +package sdk + +import ( + "context" + "fmt" + + "github.com/Jigsaw-Code/outline-sdk/network" + "github.com/Jigsaw-Code/outline-sdk/network/dnstruncate" + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/x/connectivity" +) + +// TODO: move to "outline-apps/internal" once we have migrated to monorepo + +type outlinePacketProxy struct { + network.DelegatePacketProxy + + remotePktListener transport.PacketListener // this will be used in connectivity test + remote, fallback network.PacketProxy +} + +// NewOutlinePacketProxy creates an Outline Shadowsocks PacketProxy from the JSON config. +func newOutlinePacketProxy(configJSON string) (proxy *outlinePacketProxy, err error) { + proxy = &outlinePacketProxy{} + + proxy.fallback, err = dnstruncate.NewPacketProxy() + if err != nil { + return nil, fmt.Errorf("failed to create DNS truncate proxy: %w", err) + } + + // Create Shadowsocks UDP PacketProxy + proxy.remotePktListener, err = NewOutlinePacketListener(configJSON) + if err != nil { + return nil, fmt.Errorf("failed to create UDP listener: %w", err) + } + + proxy.remote, err = network.NewPacketProxyFromPacketListener(proxy.remotePktListener) + if err != nil { + return nil, fmt.Errorf("failed to create UDP proxy: %w", err) + } + + // Create DelegatePacketProxy + proxy.DelegatePacketProxy, err = network.NewDelegatePacketProxy(proxy.fallback) + if err != nil { + return nil, fmt.Errorf("failed to create delegate UDP proxy: %w", err) + } + + return +} + +func (proxy *outlinePacketProxy) testConnectivityAndRefresh(resolver, domain string) error { + dialer := transport.PacketListenerDialer{Listener: proxy.remotePktListener} + dnsResolver := &transport.PacketDialerEndpoint{Dialer: dialer, Address: resolver} + _, err := connectivity.TestResolverPacketConnectivity(context.Background(), dnsResolver, domain) + + if err != nil { + return proxy.SetProxy(proxy.fallback) + } else { + return proxy.SetProxy(proxy.remote) + } +} diff --git a/outline/internal/sdk/stream_dialer.go b/outline/internal/sdk/stream_dialer.go new file mode 100644 index 0000000..007d4a5 --- /dev/null +++ b/outline/internal/sdk/stream_dialer.go @@ -0,0 +1,45 @@ +// Copyright 2022 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. + +package sdk + +import ( + "fmt" + "net" + "strconv" + + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" +) + +// TODO: move to "outline-apps/internal" once we have migrated to monorepo + +// NewOutlineStreamDialer creates an outline Shadowsocks StreamDialer from the JSON config. +func NewOutlineStreamDialer(configJSON string) (transport.StreamDialer, error) { + config, err := parseConfigFromJSON(configJSON) + if err != nil { + return nil, fmt.Errorf("invalid Outline configuration: %w", err) + } + + ssAddress := net.JoinHostPort(config.Hostname, strconv.Itoa(config.Port)) + dialer, err := shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: ssAddress}, config.CryptoKey) + if err != nil { + return nil, err + } + if len(config.Prefix) > 0 { + dialer.SaltGenerator = shadowsocks.NewPrefixSaltGenerator(config.Prefix) + } + + return dialer, nil +} diff --git a/outline/proxy_tunnel.go b/outline/proxy_tunnel.go new file mode 100644 index 0000000..ded29fd --- /dev/null +++ b/outline/proxy_tunnel.go @@ -0,0 +1,32 @@ +// Copyright 2023 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. + +package outline + +import "github.com/Jigsaw-Code/outline-go-tun2socks/outline/internal/sdk" + +type ProxyTunnel struct { + client *sdk.OutlineClientDevice +} + +func NewProxyTunnel(configJSON string) (*ProxyTunnel, error) { + proxy := &ProxyTunnel{} + + var err error + if proxy.client, err = sdk.NewOutlineClientDevice(configJSON); err != nil { + return nil, err + } + + return proxy, nil +} diff --git a/outline/shadowsocks/client.go b/outline/shadowsocks/client.go index d0eb118..e2bc0dd 100644 --- a/outline/shadowsocks/client.go +++ b/outline/shadowsocks/client.go @@ -20,6 +20,7 @@ package shadowsocks import ( + "errors" "fmt" "net" "strconv" @@ -27,67 +28,65 @@ import ( "github.com/Jigsaw-Code/outline-go-tun2socks/outline" "github.com/Jigsaw-Code/outline-go-tun2socks/outline/connectivity" - "github.com/Jigsaw-Code/outline-go-tun2socks/outline/internal/utf8" - "github.com/Jigsaw-Code/outline-internal-sdk/transport" - "github.com/Jigsaw-Code/outline-internal-sdk/transport/shadowsocks" + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" "github.com/eycorsican/go-tun2socks/common/log" ) // A client object that can be used to connect to a remote Shadowsocks proxy. +// +// Deprecated: Keep for backward compatibility only type Client outline.Client +// Config represents a (legacy) shadowsocks server configuration. You can use +// NewClientFromJSON(string) instead. +// +// Deprecated: this object will be removed once we migrated from the old +// Outline Client logic. +type Config struct { + Host string + Port int + Password string + CipherName string + Prefix []byte +} + // NewClient creates a new Shadowsocks client from a non-nil configuration. // -// Deprecated: Please use NewClientFromJSON. +// Deprecated: Keep for backward compatibility only. func NewClient(config *Config) (*Client, error) { if config == nil { - return nil, fmt.Errorf("shadowsocks configuration is required") + return nil, errors.New("shadowsocks configuration is required") } - return newShadowsocksClient(config.Host, config.Port, config.CipherName, config.Password, config.Prefix) -} - -// NewClientFromJSON creates a new Shadowsocks client from a JSON formatted -// configuration. -func NewClientFromJSON(configJSON string) (*Client, error) { - config, err := parseConfigFromJSON(configJSON) - if err != nil { - return nil, fmt.Errorf("failed to parse Shadowsocks configuration JSON: %w", err) + if len(config.Host) == 0 { + return nil, errors.New("invalid configuration: host is required") } - var prefixBytes []byte = nil - if len(config.Prefix) > 0 { - if p, err := utf8.DecodeUTF8CodepointsToRawBytes(config.Prefix); err != nil { - return nil, fmt.Errorf("failed to parse prefix string: %w", err) - } else { - prefixBytes = p - } + if config.Port <= 0 || config.Port >= 65535 { + return nil, fmt.Errorf("invalid configuration: port %v is not valid", config.Port) } - return newShadowsocksClient(config.Host, int(config.Port), config.Method, config.Password, prefixBytes) -} - -func newShadowsocksClient(host string, port int, cipherName, password string, prefix []byte) (*Client, error) { - if err := validateConfig(host, port, cipherName, password); err != nil { - return nil, fmt.Errorf("invalid Shadowsocks configuration: %w", err) + if len(config.Password) == 0 || len(config.CipherName) == 0 { + return nil, errors.New("invalid configuration: cipher name and password are required") } // TODO: consider using net.LookupIP to get a list of IPs, and add logic for optimal selection. - proxyIP, err := net.ResolveIPAddr("ip", host) + proxyIP, err := net.ResolveIPAddr("ip", config.Host) if err != nil { return nil, fmt.Errorf("failed to resolve proxy address: %w", err) } - proxyAddress := net.JoinHostPort(proxyIP.String(), fmt.Sprint(port)) + proxyAddress := net.JoinHostPort(proxyIP.String(), fmt.Sprint(config.Port)) - cryptoKey, err := shadowsocks.NewEncryptionKey(cipherName, password) + cryptoKey, err := shadowsocks.NewEncryptionKey(config.CipherName, config.Password) if err != nil { - return nil, fmt.Errorf("failed to create Shadowsocks cipher: %w", err) + return nil, fmt.Errorf("failed to create cipher: %w", err) } streamDialer, err := shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: proxyAddress}, cryptoKey) if err != nil { return nil, fmt.Errorf("failed to create StreamDialer: %w", err) } - if len(prefix) > 0 { - log.Debugf("Using salt prefix: %s", string(prefix)) - streamDialer.SaltGenerator = shadowsocks.NewPrefixSaltGenerator(prefix) + if len(config.Prefix) > 0 { + log.Debugf("Using salt prefix: %s", string(config.Prefix)) + streamDialer.SaltGenerator = shadowsocks.NewPrefixSaltGenerator(config.Prefix) } packetListener, err := shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: proxyAddress}, cryptoKey) @@ -121,6 +120,8 @@ const reachabilityTimeout = 10 * time.Second // the current network. Parallelizes the execution of TCP and UDP checks, selects the appropriate // error code to return accounting for transient network failures. // Returns an error if an unexpected error ocurrs. +// +// Deprecated: Keep for backward compatibility only. func CheckConnectivity(client *Client) (int, error) { errCode, err := connectivity.CheckConnectivity((*outline.Client)(client)) return errCode.Number(), err @@ -128,6 +129,8 @@ func CheckConnectivity(client *Client) (int, error) { // CheckServerReachable determines whether the server at `host:port` is reachable over TCP. // Returns an error if the server is unreachable. +// +// Deprecated: Keep for backward compatibility only. func CheckServerReachable(host string, port int) error { conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, strconv.Itoa(port)), reachabilityTimeout) if err != nil { diff --git a/outline/shadowsocks/config.go b/outline/shadowsocks/config.go deleted file mode 100644 index 4b71248..0000000 --- a/outline/shadowsocks/config.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2022 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. - -package shadowsocks - -import ( - "encoding/json" - "fmt" -) - -// Config represents a (legacy) shadowsocks server configuration. You can use -// NewClientFromJSON(string) instead. -// -// Deprecated: this object will be removed once we migrated from the old -// Outline Client logic. -type Config struct { - Host string - Port int - Password string - CipherName string - Prefix []byte -} - -// An internal data structure to be used by JSON deserialization. -// Must match the ShadowsocksSessionConfig interface defined in Outline Client. -type configJSON struct { - Host string `json:"host"` - Port uint16 `json:"port"` - Password string `json:"password"` - Method string `json:"method"` - Prefix string `json:"prefix"` -} - -// ParseConfigFromJSON parses a JSON string `in` as a configJSON object. -// The JSON string `in` must match the ShadowsocksSessionConfig interface -// defined in Outline Client. -func parseConfigFromJSON(in string) (*configJSON, error) { - var conf configJSON - if err := json.Unmarshal([]byte(in), &conf); err != nil { - return nil, err - } - return &conf, nil -} - -// validateConfig validates whether a Shadowsocks server configuration is valid -// (it won't do any connectivity tests) -// -// Returns nil if it is valid; or an error message. -func validateConfig(host string, port int, cipher, password string) error { - if len(host) == 0 { - return fmt.Errorf("must provide a host name or IP address") - } - if port <= 0 || port > 65535 { - return fmt.Errorf("port must be within range [1..65535]") - } - if len(cipher) == 0 { - return fmt.Errorf("must provide an encryption cipher method") - } - if len(password) == 0 { - return fmt.Errorf("must provide a password") - } - return nil -} diff --git a/outline/shadowsocks/config_test.go b/outline/shadowsocks/config_test.go deleted file mode 100644 index 55e86e1..0000000 --- a/outline/shadowsocks/config_test.go +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright 2023 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. - -package shadowsocks - -import ( - "testing" -) - -func Test_parseConfigFromJSON(t *testing.T) { - tests := []struct { - name string - input string - want *configJSON - wantErr bool - }{ - { - name: "normal config", - input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234"}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 12345, - Method: "some-cipher", - Password: "abcd1234", - Prefix: "", - }, - }, - { - name: "normal config with prefix", - input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234","prefix":"abc 123"}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 12345, - Method: "some-cipher", - Password: "abcd1234", - Prefix: "abc 123", - }, - }, - { - name: "normal config with extra fields", - input: `{"extra_field":"ignored","host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234"}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 12345, - Method: "some-cipher", - Password: "abcd1234", - Prefix: "", - }, - }, - { - name: "unprintable prefix", - input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234","prefix":"abc 123","prefix":"\u0000\u0080\u00ff"}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 12345, - Method: "some-cipher", - Password: "abcd1234", - Prefix: "\u0000\u0080\u00ff", - }, - }, - { - name: "multi-byte utf-8 prefix", - input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234","prefix":"abc 123","prefix":"` + "\xc2\x80\xc2\x81\xc3\xbd\xc3\xbf" + `"}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 12345, - Method: "some-cipher", - Password: "abcd1234", - Prefix: "\u0080\u0081\u00fd\u00ff", - }, - }, - { - name: "missing host", - input: `{"port":12345,"method":"some-cipher","password":"abcd1234"}`, - want: &configJSON{ - Host: "", - Port: 12345, - Method: "some-cipher", - Password: "abcd1234", - Prefix: "", - }, - }, - { - name: "missing port", - input: `{"host":"192.0.2.1","method":"some-cipher","password":"abcd1234"}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 0, - Method: "some-cipher", - Password: "abcd1234", - Prefix: "", - }, - }, - { - name: "missing method", - input: `{"host":"192.0.2.1","port":12345,"password":"abcd1234"}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 12345, - Method: "", - Password: "abcd1234", - Prefix: "", - }, - }, - { - name: "missing password", - input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher"}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 12345, - Method: "some-cipher", - Password: "", - Prefix: "", - }, - }, - { - name: "empty host", - input: `{"host":"","port":12345,"method":"some-cipher","password":"abcd1234"}`, - want: &configJSON{ - Host: "", - Port: 12345, - Method: "some-cipher", - Password: "abcd1234", - Prefix: "", - }, - }, - { - name: "zero port", - input: `{"host":"192.0.2.1","port":0,"method":"some-cipher","password":"abcd1234"}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 0, - Method: "some-cipher", - Password: "abcd1234", - Prefix: "", - }, - }, - { - name: "empty method", - input: `{"host":"192.0.2.1","port":12345,"method":"","password":"abcd1234"}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 12345, - Method: "", - Password: "abcd1234", - Prefix: "", - }, - }, - { - name: "empty password", - input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":""}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 12345, - Method: "some-cipher", - Password: "", - Prefix: "", - }, - }, - { - name: "empty prefix", - input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234","prefix":""}`, - want: &configJSON{ - Host: "192.0.2.1", - Port: 12345, - Method: "some-cipher", - Password: "abcd1234", - Prefix: "", - }, - }, - { - name: "port -1", - input: `{"host":"192.0.2.1","port":-1,"method":"some-cipher","password":"abcd1234"}`, - wantErr: true, - }, - { - name: "port 65536", - input: `{"host":"192.0.2.1","port":65536,"method":"some-cipher","password":"abcd1234"}`, - wantErr: true, - }, - { - name: "prefix out-of-range", - input: `{"host":"192.0.2.1","port":8080,"method":"some-cipher","password":"abcd1234","prefix":"\x1234"}`, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseConfigFromJSON(tt.input) - if (err != nil) != tt.wantErr { - t.Errorf("ParseConfigFromJSON() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantErr { - return - } - if got.Host != tt.want.Host || - got.Port != tt.want.Port || - got.Method != tt.want.Method || - got.Password != tt.want.Password || - got.Prefix != tt.want.Prefix { - t.Errorf("ParseConfigFromJSON() = %v (prefix %+q), want %v (prefix %+q)", got, got.Prefix, tt.want, tt.want.Prefix) - } - }) - } -} diff --git a/outline/tun2socks/tunnel.go b/outline/tun2socks/tunnel.go index f23cfa9..2749520 100644 --- a/outline/tun2socks/tunnel.go +++ b/outline/tun2socks/tunnel.go @@ -23,7 +23,7 @@ import ( "github.com/eycorsican/go-tun2socks/core" "github.com/eycorsican/go-tun2socks/proxy/dnsfallback" - "github.com/Jigsaw-Code/outline-internal-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-go-tun2socks/outline/connectivity" "github.com/Jigsaw-Code/outline-go-tun2socks/tunnel" diff --git a/outline/tun2socks/tunnel_android.go b/outline/tun2socks/tunnel_android.go index 4244689..c717fd3 100644 --- a/outline/tun2socks/tunnel_android.go +++ b/outline/tun2socks/tunnel_android.go @@ -15,6 +15,8 @@ package tun2socks import ( + "errors" + "fmt" "runtime/debug" "github.com/Jigsaw-Code/outline-go-tun2socks/outline/shadowsocks" @@ -29,10 +31,10 @@ func init() { } // ConnectShadowsocksTunnel reads packets from a TUN device and routes it to a Shadowsocks proxy server. -// Returns an OutlineTunnel instance and does *not* take ownership of the TUN file descriptor; the -// caller is responsible for closing after OutlineTunnel disconnects. +// Returns an Tunnel instance and does *not* take ownership of the TUN file descriptor; the caller is +// responsible for closing after Tunnel disconnects. // -// - `fd` is the TUN device. The OutlineTunnel acquires an additional reference to it, which +// - `fd` is the TUN device. The Tunnel acquires an additional reference to it, which // is released by OutlineTunnel.Disconnect(), so the caller must close `fd` _and_ call // Disconnect() in order to close the TUN device. // - `client` is the Shadowsocks client (created by [shadowsocks.NewClient]). @@ -52,3 +54,29 @@ func ConnectShadowsocksTunnel(fd int, client *shadowsocks.Client, isUDPEnabled b go tunnel.ProcessInputPackets(t, tun) return t, nil } + +// ConnectTunnel reads packets from a TUN device represented by `fd` and routes +// them to a remote proxy server represented by `configJSON`. This function +// will also do connectivity tests before starting, so the caller is not +// required to do any UDP tests. +// +// If the function succeeds, it will return a nil error and a Tunnel instance. +// +// This function does *not* take ownership of the TUN file descriptor `fd`; the +// caller is responsible for closing after Tunnel disconnects. +func ConnectTunnel(configJSON string, fd int) (Tunnel, error) { + if len(configJSON) == 0 { + return nil, errors.New("tunnel configuration is required") + } + tunDev, err := tunnel.MakeTunFile(fd) + if err != nil { + return nil, err + } + tn, err := newTunnelFromJSON(configJSON, tunDev) + if err != nil { + return nil, fmt.Errorf("unable to create tunnel from config: %w", err) + } + + go tunnel.ProcessInputPackets(tn, tunDev) + return tn, nil +} diff --git a/outline/tun2socks/tunnel_darwin.go b/outline/tun2socks/tunnel_darwin.go index 04ee0fa..fb56693 100644 --- a/outline/tun2socks/tunnel_darwin.go +++ b/outline/tun2socks/tunnel_darwin.go @@ -43,7 +43,7 @@ func init() { } // ConnectShadowsocksTunnel reads packets from a TUN device and routes it to a Shadowsocks proxy server. -// Returns an OutlineTunnel instance that should be used to input packets to the tunnel. +// Returns an Tunnel instance that should be used to input packets to the tunnel. // // `tunWriter` is used to output packets to the TUN (VPN). // `client` is the Shadowsocks client (created by [shadowsocks.NewClient]). @@ -58,3 +58,22 @@ func ConnectShadowsocksTunnel(tunWriter TunWriter, client *shadowsocks.Client, i } return newTunnel(client, client, isUDPEnabled, tunWriter) } + +// ConnectTunnel reads packets from a TUN device represented by `tunDev` and +// routes them to a remote proxy server represented by `configJSON`. This +// function will also do connectivity tests before starting, so the caller is +// not required to do any UDP tests. +// +// If the function succeeds, it will return a nil error and a Tunnel instance. +// +// This function will close `tunDev` after Tunnel disconnects. +func ConnectTunnel(configJSON string, tunDev TunWriter) (Tunnel, error) { + if len(configJSON) == 0 { + return nil, errors.New("tunnel configuration is required") + } + if tunDev == nil { + return nil, errors.New("must provide a TunWriter") + } + return newTunnelFromJSON(configJSON, tunDev) + // we don't need to copy packets from tunDev to tn, caller will do so +} diff --git a/outline/tun2socks/tunnel_electron.go b/outline/tun2socks/tunnel_electron.go new file mode 100644 index 0000000..7f3346c --- /dev/null +++ b/outline/tun2socks/tunnel_electron.go @@ -0,0 +1,50 @@ +// Copyright 2023 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. + +//go:build (linux || windows) && !android + +package tun2socks + +import ( + "errors" + "fmt" + "io" +) + +const mtu = 1500 + +// ConnectTunnel reads packets from a TUN device represented by `tunDev` and +// routes them to a remote proxy server represented by `configJSON`. This +// function will also do connectivity tests before starting, so the caller is +// not required to do any UDP tests. +// +// If the function succeeds, it will return a nil error and a Tunnel instance. +// +// This function will close `tunDev` after Tunnel disconnects. +func ConnectTunnel(configJSON string, tunDev io.ReadWriteCloser) (Tunnel, error) { + if len(configJSON) == 0 { + return nil, errors.New("tunnel configuration is required") + } + if tunDev == nil { + return nil, errors.New("tun device is required") + } + tn, err := newTunnelFromJSON(configJSON, tunDev) + if err != nil { + return nil, fmt.Errorf("unable to create tunnel from config: %w", err) + } + + go io.CopyBuffer(tn, tunDev, make([]byte, mtu)) + + return tn, nil +} diff --git a/outline/shadowsocks/client_test.go b/outline/tun2socks/tunnel_test.go similarity index 91% rename from outline/shadowsocks/client_test.go rename to outline/tun2socks/tunnel_test.go index b9800c3..7f700af 100644 --- a/outline/shadowsocks/client_test.go +++ b/outline/tun2socks/tunnel_test.go @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package shadowsocks +package tun2socks import "testing" -func Test_NewClientFromJSON_Errors(t *testing.T) { +func Test_newTunnelFromJSON_Errors(t *testing.T) { tests := []struct { name string input string @@ -68,9 +68,9 @@ func Test_NewClientFromJSON_Errors(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := NewClientFromJSON(tt.input) + got, err := newTunnelFromJSON(tt.input, nil) if err == nil || got != nil { - t.Errorf("NewClientFromJSON() expects an error, got = %v", got) + t.Errorf("NewClientFromJSON() expects an error, err = %v, got = %v", err, got) return } })