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
1 change: 0 additions & 1 deletion 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 @@ -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
Expand Down
15 changes: 13 additions & 2 deletions outline/connectivity/connectivity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
121 changes: 71 additions & 50 deletions outline/electron/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you define a legacyConfigFromFlags(), and pass the config as a parameter?

Making the config explicit in the function call will make the call more understandable.

Let's use "start tunnel" to be analogous to the new code.
I'm imagining startLegacyShadowsocksTunnel(legacyConfigFromClient)

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 {
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 := 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)
Expand Down Expand Up @@ -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)
}
}
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 NewTransport(host string, port int, cipherName, password string, prefix []byte) (transport.StreamDialer, transport.PacketListener, error) {
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
}
123 changes: 0 additions & 123 deletions outline/shadowsocks/client.go

This file was deleted.

Loading