Skip to content
This repository has been archived by the owner on May 6, 2024. It is now read-only.

feat: ✨ add new OutlineDevice API that uses Outline SDK #118

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ android: $(BUILDDIR)/android/tun2socks.aar

$(BUILDDIR)/android/tun2socks.aar: $(GOMOBILE)
mkdir -p "$(BUILDDIR)/android"
$(ANDROID_BUILD_CMD) -o "$@" $(IMPORT_PATH)/outline/tun2socks $(IMPORT_PATH)/outline/shadowsocks
$(ANDROID_BUILD_CMD) -o "$@" $(IMPORT_PATH)/outline $(IMPORT_PATH)/outline/tun2socks $(IMPORT_PATH)/outline/shadowsocks

apple: $(BUILDDIR)/apple/Tun2socks.xcframework

$(BUILDDIR)/apple/Tun2socks.xcframework: $(GOMOBILE)
# MACOSX_DEPLOYMENT_TARGET and -iosversion should match what outline-client supports.
# TODO(fortuna): -s strips symbols and is obsolete. Why are we using it?
export MACOSX_DEPLOYMENT_TARGET=10.14; $(GOBIND) -iosversion=11.0 -target=ios,iossimulator,macos -o $@ -ldflags '-s -w' -bundleid org.outline.tun2socks $(IMPORT_PATH)/outline/tun2socks $(IMPORT_PATH)/outline/shadowsocks
export MACOSX_DEPLOYMENT_TARGET=10.14; $(GOBIND) -iosversion=11.0 -target=ios,iossimulator,macos -o $@ -ldflags '-s -w' -bundleid org.outline.tun2socks $(IMPORT_PATH)/outline $(IMPORT_PATH)/outline/tun2socks $(IMPORT_PATH)/outline/shadowsocks

apple_future: $(BUILDDIR)/apple_future/Tun2socks.xcframework

$(BUILDDIR)/apple_future/Tun2socks.xcframework: $(GOMOBILE)
$(GOBIND) -iosversion=13.1 -target=ios,iossimulator,maccatalyst -o $@ -ldflags '-s -w' -bundleid org.outline.tun2socks $(IMPORT_PATH)/outline/tun2socks $(IMPORT_PATH)/outline/shadowsocks
$(GOBIND) -iosversion=13.1 -target=ios,iossimulator,maccatalyst -o $@ -ldflags '-s -w' -bundleid org.outline.tun2socks $(IMPORT_PATH)/outline $(IMPORT_PATH)/outline/tun2socks $(IMPORT_PATH)/outline/shadowsocks


XGO=$(GOBIN)/xgo
Expand Down
29 changes: 29 additions & 0 deletions outline/client.go
fortuna marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
package outline

import (
"fmt"

internal "github.com/Jigsaw-Code/outline-go-tun2socks/outline/internal/shadowsocks"
"github.com/Jigsaw-Code/outline-go-tun2socks/outline/internal/utf8"
"github.com/Jigsaw-Code/outline-internal-sdk/transport"
)

Expand All @@ -25,3 +29,28 @@ type Client struct {
transport.StreamDialer
transport.PacketListener
}

// NewShadowsocksClientFromJSON creates a new Shadowsocks client from a JSON
// formatted configuration.
func NewShadowsocksClientFromJSON(configJSON string) (*Client, error) {
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
config, err := parseConfigFromJSON(configJSON)
if err != nil {
return nil, fmt.Errorf("failed to parse Shadowsocks configuration JSON: %w", err)
}

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
}
}

sd, pl, err := internal.NewShadowsocksConn(config.Host, int(config.Port), config.Method, config.Password, prefixBytes)
if err != nil {
return nil, err
}

return &Client{*sd, *pl}, nil
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
}
6 changes: 3 additions & 3 deletions outline/shadowsocks/client_test.go → outline/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package shadowsocks
package outline

import "testing"

func Test_NewClientFromJSON_Errors(t *testing.T) {
func Test_NewShadowsocksClientFromJSON_Errors(t *testing.T) {
tests := []struct {
name string
input string
Expand Down Expand Up @@ -68,7 +68,7 @@ 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 := NewShadowsocksClientFromJSON(tt.input)
if err == nil || got != nil {
t.Errorf("NewClientFromJSON() expects an error, got = %v", got)
return
Expand Down
36 changes: 1 addition & 35 deletions outline/shadowsocks/config.go → outline/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package shadowsocks
package outline

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 {
Expand All @@ -52,23 +38,3 @@ func parseConfigFromJSON(in string) (*configJSON, error) {
}
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
}
4 changes: 2 additions & 2 deletions outline/shadowsocks/config_test.go → outline/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package shadowsocks
package outline

import (
"testing"
)

func Test_parseConfigFromJSON(t *testing.T) {
func Test_ParseConfigFromJSON(t *testing.T) {
tests := []struct {
name string
input string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package connectivity
package outline
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved

import (
"context"
Expand All @@ -21,7 +21,6 @@ import (
"net/http"
"time"

"github.com/Jigsaw-Code/outline-go-tun2socks/outline"
"github.com/Jigsaw-Code/outline-go-tun2socks/outline/neterrors"
"github.com/Jigsaw-Code/outline-internal-sdk/transport"
)
Expand All @@ -48,7 +47,7 @@ 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.
func CheckConnectivity(client *outline.Client) (neterrors.Error, error) {
func CheckConnectivity(client *Client) (neterrors.Error, error) {
// Start asynchronous UDP support check.
udpChan := make(chan error)
go func() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package connectivity
package outline

import (
"context"
Expand Down
13 changes: 9 additions & 4 deletions outline/electron/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"syscall"
"time"

"github.com/Jigsaw-Code/outline-go-tun2socks/outline"
"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"
Expand Down Expand Up @@ -98,7 +99,7 @@ func main() {
}

if *args.checkConnectivity {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this back out. The connectivity test and the tunnel flows are separate. We need to clearly run one or the other like we had before.

Also, don't we need a check connectivity with json config, for the udp update?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need any connectivity test for the new json config tunnel. I'd rather expose a public OutlineTunnel.IsUDPSupported() method so the caller can retrieve that info.

connErrCode, err := shadowsocks.CheckConnectivity(client)
connErrCode, err := outline.CheckConnectivity(client)
log.Debugf("Connectivity checks error code: %v", connErrCode)
if err != nil {
log.Errorf("Failed to perform connectivity checks: %v", err)
Expand Down Expand Up @@ -163,9 +164,9 @@ func setLogLevel(level string) {

// newShadowsocksClientFromArgs creates a new shadowsocks.Client instance
// from the global CLI argument object args.
func newShadowsocksClientFromArgs() (*shadowsocks.Client, error) {
func newShadowsocksClientFromArgs() (*outline.Client, error) {
if jsonConfig := *args.proxyConfig; len(jsonConfig) > 0 {
return shadowsocks.NewClientFromJSON(jsonConfig)
return outline.NewShadowsocksClientFromJSON(jsonConfig)
} else {
// legacy raw flags
config := shadowsocks.Config{
Expand All @@ -181,6 +182,10 @@ func newShadowsocksClientFromArgs() (*shadowsocks.Client, error) {
config.Prefix = p
}
}
return shadowsocks.NewClient(&config)
ssClient, err := shadowsocks.NewClient(&config)
if err != nil {
return nil, err
}
return shadowsocks.ToOutlineClient(ssClient), nil
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
}
}
81 changes: 81 additions & 0 deletions outline/internal/shadowsocks/shadowsocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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 (
"fmt"
"net"

"github.com/Jigsaw-Code/outline-internal-sdk/transport"
"github.com/Jigsaw-Code/outline-internal-sdk/transport/shadowsocks"
"github.com/Jigsaw-Code/outline-internal-sdk/transport/shadowsocks/client"
"github.com/eycorsican/go-tun2socks/common/log"
)

func NewShadowsocksConn(host string, port int, cipherName, password string, prefix []byte) (*client.StreamDialer, *transport.PacketListener, error) {
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
if err := validateConfig(host, port, cipherName, password); err != nil {
return nil, nil, fmt.Errorf("invalid shadowsocks configuration: %w", err)
}

// TODO: consider using net.LookupIP to get a list of IPs, and add logic for optimal selection.
proxyIP, err := net.ResolveIPAddr("ip", host)
if err != nil {
return nil, nil, fmt.Errorf("failed to resolve proxy address: %w", err)
}

proxyTCPEndpoint := transport.TCPEndpoint{RemoteAddr: net.TCPAddr{IP: proxyIP.IP, Port: port}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: #120

Let me know of you want to merge that or not. It shouldn't conflict much.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, please merge that. BTW, I will work on the SDK change (to introduce IPDevice) before updating this PR as well.

proxyUDPEndpoint := transport.UDPEndpoint{RemoteAddr: net.UDPAddr{IP: proxyIP.IP, Port: port}}

cipher, err := shadowsocks.NewCipher(cipherName, password)
if err != nil {
return nil, nil, fmt.Errorf("failed to create Shadowsocks cipher: %w", err)
}

streamDialer, err := client.NewShadowsocksStreamDialer(proxyTCPEndpoint, cipher)
if err != nil {
return nil, nil, fmt.Errorf("failed to create StreamDialer: %w", err)
}
if len(prefix) > 0 {
log.Debugf("Using salt prefix: %s", string(prefix))
streamDialer.SetTCPSaltGenerator(client.NewPrefixSaltGenerator(prefix))
}

packetListener, err := client.NewShadowsocksPacketListener(proxyUDPEndpoint, cipher)
if err != nil {
return nil, nil, fmt.Errorf("failed to create PacketListener: %w", err)
}

return &streamDialer, &packetListener, 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
}
Loading