diff --git a/outline/android/tun2socks.go b/outline/android/tun2socks.go index c5aff47..d1366d0 100644 --- a/outline/android/tun2socks.go +++ b/outline/android/tun2socks.go @@ -15,13 +15,11 @@ package tun2socks import ( - "fmt" - "math" "runtime/debug" "github.com/Jigsaw-Code/outline-go-tun2socks/outline" + "github.com/Jigsaw-Code/outline-go-tun2socks/outline/shadowsocks" "github.com/Jigsaw-Code/outline-go-tun2socks/tunnel" - "github.com/Jigsaw-Code/outline-ss-server/client" "github.com/eycorsican/go-tun2socks/common/log" ) @@ -40,32 +38,20 @@ type OutlineTunnel interface { // Returns an OutlineTunnel instance and does *not* take ownership of the TUN file descriptor; the // caller is responsible for closing after OutlineTunnel disconnects. // -// `fd` is the TUN device. The OutlineTunnel acquires an additional reference to it, which +// - `fd` is the TUN device. The OutlineTunnel 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. -// `host` is IP address of the Shadowsocks proxy server. -// `port` is the port of the Shadowsocks proxy server. -// `password` is the password of the Shadowsocks proxy. -// `cipher` is the encryption cipher the Shadowsocks proxy. -// `prefix` is the salt prefix to use for TCP connections (optional, use with care). -// `isUDPEnabled` indicates whether the tunnel and/or network enable UDP proxying. +// - `client` is the Shadowsocks client (created by [shadowsocks.NewClient]). +// - `isUDPEnabled` indicates whether the tunnel and/or network enable UDP proxying. // // Throws an exception if the TUN file descriptor cannot be opened, or if the tunnel fails to // connect. -func ConnectShadowsocksTunnel(fd int, host string, port int, password, cipher string, prefix []byte, isUDPEnabled bool) (OutlineTunnel, error) { - if port <= 0 || port > math.MaxUint16 { - return nil, fmt.Errorf("Invalid port number: %v", port) - } +func ConnectShadowsocksTunnel(fd int, client *shadowsocks.Client, isUDPEnabled bool) (OutlineTunnel, error) { tun, err := tunnel.MakeTunFile(fd) if err != nil { return nil, err } - ssclient, err := client.NewClient(host, port, password, cipher) - if err != nil { - return nil, fmt.Errorf("failed to construct Shadowsocks client: %v", err) - } - ssclient.SetTCPSaltGenerator(client.NewPrefixSaltGenerator(prefix)) - t, err := outline.NewTunnel(ssclient, isUDPEnabled, tun) + t, err := outline.NewTunnel(client, isUDPEnabled, tun) if err != nil { return nil, err } diff --git a/outline/apple/tun2socks.go b/outline/apple/tun2socks.go index dcf6afa..d762564 100644 --- a/outline/apple/tun2socks.go +++ b/outline/apple/tun2socks.go @@ -16,14 +16,12 @@ package tun2socks import ( "errors" - "fmt" "io" - "math" "runtime/debug" "time" "github.com/Jigsaw-Code/outline-go-tun2socks/outline" - "github.com/Jigsaw-Code/outline-ss-server/client" + "github.com/Jigsaw-Code/outline-go-tun2socks/outline/shadowsocks" ) // OutlineTunnel embeds the tun2socks.Tunnel interface so it gets exported by gobind. @@ -54,24 +52,15 @@ func init() { // Returns an OutlineTunnel instance that should be used to input packets to the tunnel. // // `tunWriter` is used to output packets to the TUN (VPN). -// `host` is IP address of the Shadowsocks proxy server. -// `port` is the port of the Shadowsocks proxy server. -// `password` is the password of the Shadowsocks proxy. -// `cipher` is the encryption cipher the Shadowsocks proxy. -// `prefix` is the salt prefix to use for TCP connections (optional, use with care). +// `client` is the Shadowsocks client (created by [shadowsocks.NewClient]). // `isUDPEnabled` indicates whether the tunnel and/or network enable UDP proxying. // // Sets an error if the tunnel fails to connect. -func ConnectShadowsocksTunnel(tunWriter TunWriter, host string, port int, password, cipher string, prefix []byte, isUDPEnabled bool) (OutlineTunnel, error) { +func ConnectShadowsocksTunnel(tunWriter TunWriter, client *shadowsocks.Client, isUDPEnabled bool) (OutlineTunnel, error) { if tunWriter == nil { - return nil, errors.New("Must provide a TunWriter") - } else if port <= 0 || port > math.MaxUint16 { - return nil, fmt.Errorf("Invalid port number: %v", port) + return nil, errors.New("must provide a TunWriter") + } else if client == nil { + return nil, errors.New("must provide a client") } - ssclient, err := client.NewClient(host, port, password, cipher) - if err != nil { - return nil, fmt.Errorf("failed to construct Shadowsocks client: %v", err) - } - ssclient.SetTCPSaltGenerator(client.NewPrefixSaltGenerator(prefix)) - return outline.NewTunnel(ssclient, isUDPEnabled, tunWriter) + return outline.NewTunnel(client, isUDPEnabled, tunWriter) } diff --git a/outline/electron/main.go b/outline/electron/main.go index 75e07bd..12a1f63 100644 --- a/outline/electron/main.go +++ b/outline/electron/main.go @@ -18,7 +18,6 @@ import ( "flag" "fmt" "io" - "net/url" "os" "os/signal" "strings" @@ -27,7 +26,6 @@ import ( oss "github.com/Jigsaw-Code/outline-go-tun2socks/outline/shadowsocks" "github.com/Jigsaw-Code/outline-go-tun2socks/shadowsocks" - "github.com/Jigsaw-Code/outline-ss-server/client" "github.com/eycorsican/go-tun2socks/common/log" _ "github.com/eycorsican/go-tun2socks/common/log/simple" // Register a simple logger. "github.com/eycorsican/go-tun2socks/core" @@ -70,7 +68,7 @@ func main() { args.proxyPort = flag.Int("proxyPort", 0, "Shadowsocks proxy port number") args.proxyPassword = flag.String("proxyPassword", "", "Shadowsocks proxy password") args.proxyCipher = flag.String("proxyCipher", "chacha20-ietf-poly1305", "Shadowsocks proxy encryption cipher") - args.proxyPrefix = flag.String("proxyPrefix", "", "Shadowsocks connection prefix, URI-encoded (unsafe)") + args.proxyPrefix = flag.String("proxyPrefix", "", "Shadowsocks connection prefix, UTF8-encoded (unsafe)") args.logLevel = flag.String("logLevel", "info", "Logging level: debug|info|warn|error|none") args.dnsFallback = flag.Bool("dnsFallback", false, "Enable DNS fallback over TCP (overrides the UDP handler).") args.checkConnectivity = flag.Bool("checkConnectivity", false, "Check the proxy TCP and UDP connectivity and exit.") @@ -100,8 +98,33 @@ func main() { os.Exit(oss.IllegalConfiguration) } + config := oss.Config{ + Host: *args.proxyHost, + Port: *args.proxyPort, + Password: *args.proxyPassword, + CipherName: *args.proxyCipher, + } + + // The prefix is an 8-bit-clean byte sequence, stored in the codepoint + // values of a unicode string, which arrives here encoded in UTF-8. + prefixRunes := []rune(*args.proxyPrefix) + config.Prefix = make([]byte, len(prefixRunes)) + for i, r := range prefixRunes { + if (r & 0xFF) != r { + log.Errorf("Character out of range: %r", r) + os.Exit(oss.IllegalConfiguration) + } + config.Prefix[i] = byte(r) + } + + client, err := oss.NewClient(&config) + if err != nil { + log.Errorf("Failed to construct Shadowsocks client: %v", err) + os.Exit(oss.IllegalConfiguration) + } + if *args.checkConnectivity { - connErrCode, err := oss.CheckConnectivity(*args.proxyHost, *args.proxyPort, *args.proxyPassword, *args.proxyCipher) + connErrCode, err := oss.CheckConnectivity(client) log.Debugf("Connectivity checks error code: %v", connErrCode) if err != nil { log.Errorf("Failed to perform connectivity checks: %v", err) @@ -119,25 +142,14 @@ func main() { // Output packets to TUN device core.RegisterOutputFn(tunDevice.Write) - ssclient, err := client.NewClient(*args.proxyHost, *args.proxyPort, *args.proxyPassword, *args.proxyCipher) - if err != nil { - log.Errorf("Failed to construct Shadowsocks client: %v", err) - os.Exit(oss.IllegalConfiguration) - } - prefixBytes, err := url.PathUnescape(*args.proxyPrefix) - if err != nil { - log.Errorf("\"%s\" could not be URI-decoded", *args.proxyPrefix) - os.Exit(oss.IllegalConfiguration) - } - ssclient.SetTCPSaltGenerator(client.NewPrefixSaltGenerator([]byte(prefixBytes))) // Register TCP and UDP connection handlers - core.RegisterTCPConnHandler(shadowsocks.NewTCPHandler(ssclient)) + core.RegisterTCPConnHandler(shadowsocks.NewTCPHandler(client)) if *args.dnsFallback { // UDP connectivity not supported, fall back to DNS over TCP. log.Debugf("Registering DNS fallback UDP handler") core.RegisterUDPConnHandler(dnsfallback.NewUDPHandler()) } else { - core.RegisterUDPConnHandler(shadowsocks.NewUDPHandler(ssclient, udpTimeout)) + core.RegisterUDPConnHandler(shadowsocks.NewUDPHandler(client, udpTimeout)) } // Configure LWIP stack to receive input data from the TUN device diff --git a/outline/shadowsocks/config.go b/outline/shadowsocks/config.go new file mode 100644 index 0000000..64debc9 --- /dev/null +++ b/outline/shadowsocks/config.go @@ -0,0 +1,50 @@ +// 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 ( + "github.com/Jigsaw-Code/outline-ss-server/client" + "github.com/eycorsican/go-tun2socks/common/log" +) + +// Config represents a shadowsocks server configuration. +// Exported via gobind. +type Config struct { + Host string + Port int + Password string + CipherName string + Prefix []byte +} + +// Client provides a transparent container for [client.Client] that +// is exportable (as an opaque object) via gobind. +type Client struct { + client.Client +} + +// NewClient provides a gobind-compatible wrapper for [client.NewClient]. +func NewClient(config *Config) (*Client, error) { + c, err := client.NewClient(config.Host, config.Port, config.Password, config.CipherName) + if err != nil { + return nil, err + } + if len(config.Prefix) > 0 { + log.Debugf("Using salt prefix: %s", string(config.Prefix)) + c.SetTCPSaltGenerator(client.NewPrefixSaltGenerator(config.Prefix)) + } + + return &Client{c}, nil +} diff --git a/outline/shadowsocks/connectivity.go b/outline/shadowsocks/connectivity.go index c843219..d9c83c2 100644 --- a/outline/shadowsocks/connectivity.go +++ b/outline/shadowsocks/connectivity.go @@ -32,12 +32,7 @@ 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. -func CheckConnectivity(host string, port int, password, cipher string) (int, error) { - client, err := shadowsocks.NewClient(host, port, password, cipher) - if err != nil { - // TODO: Inspect error for invalid cipher error or proxy host resolution failure. - return Unexpected, err - } +func CheckConnectivity(client *Client) (int, error) { tcpChan := make(chan error) // Check whether the proxy is reachable and that the client is able to authenticate to the proxy go func() {