From 045a0256e74dbe67f81f85a68f70e30628da74a2 Mon Sep 17 00:00:00 2001 From: "J.Yi" Date: Thu, 17 Aug 2023 17:11:55 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20OutlineDevice=20A?= =?UTF-8?q?PI=20that=20uses=20Outline=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 23 ++++++ go.sum | 50 +++++++++++ outline/config/config.go | 98 ++++++++++++++++++++++ outline/config/config_test.go | 141 ++++++++++++++++++++++++++++++++ outline/device/device.go | 68 +++++++++++++++ outline/device/device_test.go | 40 +++++++++ outline/device/packet_proxy.go | 82 +++++++++++++++++++ outline/device/stream_dialer.go | 37 +++++++++ 8 files changed, 539 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 outline/config/config.go create mode 100644 outline/config/config_test.go create mode 100644 outline/device/device.go create mode 100644 outline/device/device_test.go create mode 100644 outline/device/packet_proxy.go create mode 100644 outline/device/stream_dialer.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000..b15ba829ba --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module github.com/Jigsaw-Code/outline-apps + +go 1.21.0 + +require ( + github.com/Jigsaw-Code/outline-sdk v0.0.2 + github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20230807220427-893de7fdc6b8 + github.com/stretchr/testify v1.8.4 + golang.org/x/sys v0.11.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/eycorsican/go-tun2socks v1.16.11 // 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 + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/net v0.10.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 new file mode 100644 index 0000000000..1a7afdafd2 --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +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/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/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +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/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.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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.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.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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/config/config.go b/outline/config/config.go new file mode 100644 index 0000000000..c157a7d4e1 --- /dev/null +++ b/outline/config/config.go @@ -0,0 +1,98 @@ +// 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 config + +import ( + "encoding/json" + "fmt" + + "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" +) + +// An internal configuration data structure to be used by Outline transports. +type TransportConfig struct { + Hostname string + Port int + CryptoKey *shadowsocks.EncryptionKey + Prefix []byte +} + +// The configuration interface between the Outline backend and Outline apps. +// 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 transport configuration string in JSON format, and returns a corresponding +// TransportConfig. The JSON string `in` must match the ShadowsocksSessionConfig interface defined in Outline Client. +func ParseConfigFromJSON(in string) (config *TransportConfig, err error) { + var confJson configJSON + if err = json.Unmarshal([]byte(in), &confJson); err != nil { + return nil, err + } + if err = validateConfig(&confJson); err != nil { + return nil, fmt.Errorf("invalid configuration: %w", err) + } + + config = &TransportConfig{ + Hostname: confJson.Host, + Port: int(confJson.Port), + } + if config.CryptoKey, err = shadowsocks.NewEncryptionKey(confJson.Method, confJson.Password); err != nil { + return nil, fmt.Errorf("invalid cipher: %w", err) + } + if len(confJson.Prefix) > 0 { + if config.Prefix, err = parseStringPrefix(confJson.Prefix); err != nil { + return nil, fmt.Errorf("invalid configuration prefix: %w", err) + } + } + + return config, nil +} + +// validateConfig validates whether an Outline transport configuration is valid (it won't do any connectivity tests). +// +// Returns nil if it is valid; or an error if not. +func validateConfig(config *configJSON) error { + if len(config.Host) == 0 { + return fmt.Errorf("must provide a hostname or IP address") + } + if config.Port <= 0 || config.Port > 65535 { + return fmt.Errorf("port must be within range [1..65535]") + } + if len(config.Method) == 0 { + return fmt.Errorf("must provide an encryption cipher method") + } + if len(config.Password) == 0 { + return fmt.Errorf("must provide an encryption cipher password") + } + 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/config/config_test.go b/outline/config/config_test.go new file mode 100644 index 0000000000..12ca84c10d --- /dev/null +++ b/outline/config/config_test.go @@ -0,0 +1,141 @@ +// 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 config + +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"}`, + expectErr: true, + }, + { + name: "missing port", + input: `{"host":"192.0.2.1","method":"aes-192-gcm","password":"abcd1234"}`, + expectErr: true, + }, + { + name: "missing method", + input: `{"host":"192.0.2.1","port":12345,"password":"abcd1234"}`, + expectHost: "192.0.2.1", + expectErr: true, + }, + { + name: "missing password", + input: `{"host":"192.0.2.1","port":12345,"method":"chacha20-ietf-poly1305"}`, + expectErr: true, + }, + { + name: "empty host", + input: `{"host":"","port":12345,"method":"chacha20-ietf-poly1305","password":"abcd1234"}`, + expectErr: true, + }, + { + name: "zero port", + input: `{"host":"192.0.2.1","port":0,"method":"chacha20-ietf-poly1305","password":"abcd1234"}`, + expectErr: true, + }, + { + name: "empty method", + input: `{"host":"192.0.2.1","port":12345,"method":"","password":"abcd1234"}`, + expectErr: true, + }, + { + name: "empty password", + input: `{"host":"192.0.2.1","port":12345,"method":"chacha20-ietf-poly1305","password":""}`, + expectErr: true, + }, + { + name: "empty prefix", + input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234","prefix":""}`, + expectErr: true, + }, + { + 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/device/device.go b/outline/device/device.go new file mode 100644 index 0000000000..009957d66e --- /dev/null +++ b/outline/device/device.go @@ -0,0 +1,68 @@ +// 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 device + +import ( + "fmt" + + "github.com/Jigsaw-Code/outline-apps/outline/config" + "github.com/Jigsaw-Code/outline-sdk/network" + "github.com/Jigsaw-Code/outline-sdk/network/lwip2transport" + "github.com/Jigsaw-Code/outline-sdk/transport" +) + +const ( + connectivityTestDNSResolver = "1.1.1.1:53" + connectivityTestTargetDomain = "www.google.com" +) + +// OutlineDevice delegates the TCP and UDP traffic from local machine to the remote Outline server. +type OutlineDevice struct { + t2s network.IPDevice + pp *outlinePacketProxy + sd transport.StreamDialer +} + +// NewOutlineDevice creates a new [OutlineDevice] that can relay traffic to a remote Outline server. +func NewOutlineDevice(configJSON string) (d *OutlineDevice, err error) { + config, err := config.ParseConfigFromJSON(configJSON) + if err != nil { + return nil, err + } + + d = &OutlineDevice{} + + if d.sd, err = newOutlineStreamDialer(config); err != nil { + return nil, fmt.Errorf("failed to create TCP dialer: %w", err) + } + + if d.pp, err = newOutlinePacketProxy(config); err != nil { + return nil, fmt.Errorf("failed to create UDP proxy: %w", err) + } + + if d.t2s, err = lwip2transport.ConfigureDevice(d.sd, d.pp); err != nil { + return nil, fmt.Errorf("failed to configure lwIP: %w", err) + } + + return +} + +func (d *OutlineDevice) Close() error { + return d.t2s.Close() +} + +func (d *OutlineDevice) Refresh() error { + return d.pp.testConnectivityAndRefresh(connectivityTestDNSResolver, connectivityTestTargetDomain) +} diff --git a/outline/device/device_test.go b/outline/device/device_test.go new file mode 100644 index 0000000000..33c3fdec09 --- /dev/null +++ b/outline/device/device_test.go @@ -0,0 +1,40 @@ +// 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 device + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_NewOutlineDevice(t *testing.T) { + tests := []struct { + name string + input string + }{ + { + name: "normal configuration", + input: `{"host":"192.0.2.1","port":12345,"method":"chacha20-ietf-poly1305","password":"abcd1234"}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d, err := NewOutlineDevice(tt.input) + require.NoError(t, err) + require.NotNil(t, d) + }) + } +} diff --git a/outline/device/packet_proxy.go b/outline/device/packet_proxy.go new file mode 100644 index 0000000000..12a40ab561 --- /dev/null +++ b/outline/device/packet_proxy.go @@ -0,0 +1,82 @@ +// 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 device + +import ( + "context" + "fmt" + "net" + "strconv" + + "github.com/Jigsaw-Code/outline-apps/outline/config" + "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/transport/shadowsocks" + "github.com/Jigsaw-Code/outline-sdk/x/connectivity" +) + +type outlinePacketProxy struct { + network.DelegatePacketProxy + remote, fallback network.PacketProxy + remotePktListener transport.PacketListener // this will be used in connectivity test +} + +func newOutlinePacketProxy(config *config.TransportConfig) (proxy *outlinePacketProxy, err error) { + proxy = &outlinePacketProxy{} + + if proxy.remotePktListener, proxy.remote, err = makeRemotePacketProxy(config); err != nil { + return nil, fmt.Errorf("failed to create packet proxy: %w", err) + } + + if proxy.fallback, err = dnstruncate.NewPacketProxy(); err != nil { + return nil, fmt.Errorf("failed to create DNS fallback packet proxy: %w", err) + } + + if proxy.DelegatePacketProxy, err = network.NewDelegatePacketProxy(proxy.fallback); err != nil { + return nil, fmt.Errorf("failed to create mutable packet proxy: %w", err) + } + + return +} + +// makeRemotePacketProxy creates a pair of [transport.PacketListener] and [network.PacketProxy] that connects to the +// remote proxy using `config`. +func makeRemotePacketProxy( + config *config.TransportConfig, +) (pl transport.PacketListener, pp network.PacketProxy, err error) { + server := net.JoinHostPort(config.Hostname, strconv.Itoa(config.Port)) + if pl, err = shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: server}, config.CryptoKey); err != nil { + return nil, nil, err + } + if pp, err = network.NewPacketProxyFromPacketListener(pl); err != nil { + return nil, nil, err + } + return +} + +// testConnectivityAndRefresh tests whether the remote server can handle packet traffic and sets the underlying proxy +// to be either remote or fallback according to the result. +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/device/stream_dialer.go b/outline/device/stream_dialer.go new file mode 100644 index 0000000000..b395b0f2dd --- /dev/null +++ b/outline/device/stream_dialer.go @@ -0,0 +1,37 @@ +// 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 device + +import ( + "net" + "strconv" + + "github.com/Jigsaw-Code/outline-apps/outline/config" + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" +) + +// newOutlineStreamDialer creates a [transport.StreamDialer] that connects to the remote proxy using `config`. +func newOutlineStreamDialer(config *config.TransportConfig) (transport.StreamDialer, error) { + server := net.JoinHostPort(config.Hostname, strconv.Itoa(config.Port)) + dialer, err := shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: server}, config.CryptoKey) + if err != nil { + return nil, err + } + if len(config.Prefix) > 0 { + dialer.SaltGenerator = shadowsocks.NewPrefixSaltGenerator(config.Prefix) + } + return dialer, nil +} From e011453e8558875904b0614ffef52421d36a9fdf Mon Sep 17 00:00:00 2001 From: "J.Yi" Date: Thu, 17 Aug 2023 17:24:05 -0400 Subject: [PATCH 2/3] downgrade go version in go.mod for CodeQL task --- go.mod | 2 +- go.sum | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/go.mod b/go.mod index b15ba829ba..4e74235ce5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Jigsaw-Code/outline-apps -go 1.21.0 +go 1.20 require ( github.com/Jigsaw-Code/outline-sdk v0.0.2 diff --git a/go.sum b/go.sum index 1a7afdafd2..56672c45c6 100644 --- a/go.sum +++ b/go.sum @@ -7,11 +7,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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= @@ -34,7 +31,6 @@ golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLL 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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= @@ -45,6 +41,5 @@ 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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From bff3827bf26827b70d2342d70941e1bc060dd7c3 Mon Sep 17 00:00:00 2001 From: "J.Yi" Date: Thu, 17 Aug 2023 18:15:58 -0400 Subject: [PATCH 3/3] move config to a private data structure --- outline/{config => device}/config.go | 22 ++++---- outline/{config => device}/config_test.go | 66 ++++++++++------------- outline/device/device.go | 3 +- outline/device/packet_listener.go | 25 +++++++++ outline/device/packet_proxy.go | 27 +++------- outline/device/stream_dialer.go | 9 +--- 6 files changed, 74 insertions(+), 78 deletions(-) rename outline/{config => device}/config.go (86%) rename outline/{config => device}/config_test.go (61%) create mode 100644 outline/device/packet_listener.go diff --git a/outline/config/config.go b/outline/device/config.go similarity index 86% rename from outline/config/config.go rename to outline/device/config.go index c157a7d4e1..356dd778bc 100644 --- a/outline/config/config.go +++ b/outline/device/config.go @@ -12,21 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package device import ( "encoding/json" "fmt" + "net" + "strconv" "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" ) // An internal configuration data structure to be used by Outline transports. -type TransportConfig struct { - Hostname string - Port int - CryptoKey *shadowsocks.EncryptionKey - Prefix []byte +type transportConfig struct { + RemoteAddress string // the remote server address of "host:port" + CryptoKey *shadowsocks.EncryptionKey + Prefix []byte } // The configuration interface between the Outline backend and Outline apps. @@ -39,9 +40,9 @@ type configJSON struct { Prefix string `json:"prefix"` } -// ParseConfigFromJSON parses a transport configuration string in JSON format, and returns a corresponding +// parseConfigFromJSON parses a transport configuration string in JSON format, and returns a corresponding // TransportConfig. The JSON string `in` must match the ShadowsocksSessionConfig interface defined in Outline Client. -func ParseConfigFromJSON(in string) (config *TransportConfig, err error) { +func parseConfigFromJSON(in string) (config *transportConfig, err error) { var confJson configJSON if err = json.Unmarshal([]byte(in), &confJson); err != nil { return nil, err @@ -50,9 +51,8 @@ func ParseConfigFromJSON(in string) (config *TransportConfig, err error) { return nil, fmt.Errorf("invalid configuration: %w", err) } - config = &TransportConfig{ - Hostname: confJson.Host, - Port: int(confJson.Port), + config = &transportConfig{ + RemoteAddress: net.JoinHostPort(confJson.Host, strconv.Itoa(int(confJson.Port))), } if config.CryptoKey, err = shadowsocks.NewEncryptionKey(confJson.Method, confJson.Password); err != nil { return nil, fmt.Errorf("invalid cipher: %w", err) diff --git a/outline/config/config_test.go b/outline/device/config_test.go similarity index 61% rename from outline/config/config_test.go rename to outline/device/config_test.go index 12ca84c10d..0ef0996d15 100644 --- a/outline/config/config_test.go +++ b/outline/device/config_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package device import ( "testing" @@ -22,45 +22,39 @@ import ( func Test_ParseConfigFromJSON(t *testing.T) { tests := []struct { - name string - input string - expectErr bool - expectHost string - expectPort int - expectPrefix []byte + name string + input string + expectErr bool + expectAddress string + 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", + input: `{"host":"192.0.2.1","port":12345,"method":"chacha20-ietf-poly1305","password":"abcd1234"}`, + expectAddress: "192.0.2.1: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 prefix", + input: `{"host":"192.0.2.1","port":12345,"method":"aes-128-gcm","password":"abcd1234","prefix":"abc 123"}`, + expectAddress: "192.0.2.1: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: "normal config with extra fields", + input: `{"extra_field":"ignored","host":"192.0.2.1","port":12345,"method":"aes-192-gcm","password":"abcd1234"}`, + expectAddress: "192.0.2.1: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: "unprintable prefix", + input: `{"host":"192.0.2.1","port":12345,"method":"AES-256-gcm","password":"abcd1234","prefix":"abc 123","prefix":"\u0000\u0080\u00ff"}`, + expectAddress: "192.0.2.1: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: "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" + `"}`, + expectAddress: "192.0.2.1:12345", + expectPrefix: []byte{0x80, 0x81, 0xfd, 0xff}, }, { name: "missing host", @@ -73,10 +67,9 @@ func Test_ParseConfigFromJSON(t *testing.T) { expectErr: true, }, { - name: "missing method", - input: `{"host":"192.0.2.1","port":12345,"password":"abcd1234"}`, - expectHost: "192.0.2.1", - expectErr: true, + name: "missing method", + input: `{"host":"192.0.2.1","port":12345,"password":"abcd1234"}`, + expectErr: true, }, { name: "missing password", @@ -126,13 +119,12 @@ func Test_ParseConfigFromJSON(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ParseConfigFromJSON(tt.input) + 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.Equal(t, tt.expectAddress, got.RemoteAddress) require.NotNil(t, got.CryptoKey) require.Equal(t, tt.expectPrefix, got.Prefix) } diff --git a/outline/device/device.go b/outline/device/device.go index 009957d66e..1a9ce70456 100644 --- a/outline/device/device.go +++ b/outline/device/device.go @@ -17,7 +17,6 @@ package device import ( "fmt" - "github.com/Jigsaw-Code/outline-apps/outline/config" "github.com/Jigsaw-Code/outline-sdk/network" "github.com/Jigsaw-Code/outline-sdk/network/lwip2transport" "github.com/Jigsaw-Code/outline-sdk/transport" @@ -37,7 +36,7 @@ type OutlineDevice struct { // NewOutlineDevice creates a new [OutlineDevice] that can relay traffic to a remote Outline server. func NewOutlineDevice(configJSON string) (d *OutlineDevice, err error) { - config, err := config.ParseConfigFromJSON(configJSON) + config, err := parseConfigFromJSON(configJSON) if err != nil { return nil, err } diff --git a/outline/device/packet_listener.go b/outline/device/packet_listener.go new file mode 100644 index 0000000000..277cc12b4b --- /dev/null +++ b/outline/device/packet_listener.go @@ -0,0 +1,25 @@ +// 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 device + +import ( + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" +) + +// newOutlinePacketListener creates a [transport.PacketListener] that connects to the remote proxy using `config`. +func newOutlinePacketListener(config *transportConfig) (transport.PacketListener, error) { + return shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: config.RemoteAddress}, config.CryptoKey) +} diff --git a/outline/device/packet_proxy.go b/outline/device/packet_proxy.go index 12a40ab561..53a2f32a14 100644 --- a/outline/device/packet_proxy.go +++ b/outline/device/packet_proxy.go @@ -17,14 +17,10 @@ package device import ( "context" "fmt" - "net" - "strconv" - "github.com/Jigsaw-Code/outline-apps/outline/config" "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/transport/shadowsocks" "github.com/Jigsaw-Code/outline-sdk/x/connectivity" ) @@ -34,10 +30,14 @@ type outlinePacketProxy struct { remotePktListener transport.PacketListener // this will be used in connectivity test } -func newOutlinePacketProxy(config *config.TransportConfig) (proxy *outlinePacketProxy, err error) { +func newOutlinePacketProxy(config *transportConfig) (proxy *outlinePacketProxy, err error) { proxy = &outlinePacketProxy{} - if proxy.remotePktListener, proxy.remote, err = makeRemotePacketProxy(config); err != nil { + if proxy.remotePktListener, err = newOutlinePacketListener(config); err != nil { + return nil, fmt.Errorf("failed to create packet listener: %w", err) + } + + if proxy.remote, err = network.NewPacketProxyFromPacketListener(proxy.remotePktListener); err != nil { return nil, fmt.Errorf("failed to create packet proxy: %w", err) } @@ -52,21 +52,6 @@ func newOutlinePacketProxy(config *config.TransportConfig) (proxy *outlinePacket return } -// makeRemotePacketProxy creates a pair of [transport.PacketListener] and [network.PacketProxy] that connects to the -// remote proxy using `config`. -func makeRemotePacketProxy( - config *config.TransportConfig, -) (pl transport.PacketListener, pp network.PacketProxy, err error) { - server := net.JoinHostPort(config.Hostname, strconv.Itoa(config.Port)) - if pl, err = shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: server}, config.CryptoKey); err != nil { - return nil, nil, err - } - if pp, err = network.NewPacketProxyFromPacketListener(pl); err != nil { - return nil, nil, err - } - return -} - // testConnectivityAndRefresh tests whether the remote server can handle packet traffic and sets the underlying proxy // to be either remote or fallback according to the result. func (proxy *outlinePacketProxy) testConnectivityAndRefresh(resolver, domain string) error { diff --git a/outline/device/stream_dialer.go b/outline/device/stream_dialer.go index b395b0f2dd..b837bbc443 100644 --- a/outline/device/stream_dialer.go +++ b/outline/device/stream_dialer.go @@ -15,18 +15,13 @@ package device import ( - "net" - "strconv" - - "github.com/Jigsaw-Code/outline-apps/outline/config" "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks" ) // newOutlineStreamDialer creates a [transport.StreamDialer] that connects to the remote proxy using `config`. -func newOutlineStreamDialer(config *config.TransportConfig) (transport.StreamDialer, error) { - server := net.JoinHostPort(config.Hostname, strconv.Itoa(config.Port)) - dialer, err := shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: server}, config.CryptoKey) +func newOutlineStreamDialer(config *transportConfig) (transport.StreamDialer, error) { + dialer, err := shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: config.RemoteAddress}, config.CryptoKey) if err != nil { return nil, err }