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
20 changes: 13 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
48 changes: 34 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,64 @@ 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=
github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
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=
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)
}
}
96 changes: 96 additions & 0 deletions outline/internal/sdk/config.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading