diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 75a20ea6c3..ed168d43e1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,11 +6,12 @@ labels: 'bug' assignees: '' --- +This issue tracker is for bug reports only. **If you want to request troubleshooting assistance with Outline Manager or Outline Client, please do not submit a bug here. Instead, please contact us using our [support website](https://support.getoutline.org/s/contactsupport).** Developers, please post your questions on the [SDK Discussion board](https://github.com/Jigsaw-Code/outline-sdk/discussions). **Describe the bug** A clear and concise description of what the bug is. -**To Reproduce** +**To reproduce** How can the broken behavior be reproduced? **Expected behavior** @@ -19,11 +20,11 @@ What behavior did you expect? **Screenshots** If applicable, add screenshots to help explain your problem. -**Client System (please complete the following information):** +**Client system (please complete the following information):** - Outline Client Version [e.g. iOS 1.3.1] - Your Operating System: [e.g. iOS 14.2, Windows 10] -**Submit Feedback** +**Submit feedback** Please submit feedback through the app and label it with this issue number. **Additional context** diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 1a538ee543..c844dc569b 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,10 +1,12 @@ --- name: Question -about: The issue tracker is not for questions. Please ask questions at support@getoutline.org. +about: Questions about Outline title: '' -labels: '' +labels: 'question' assignees: '' --- -The issue tracker is not for questions. Please ask questions at support@getoutline.org. +The issue tracker is not for questions. + +If you would like support with Outline Manager or Outline Client, please review the help articles on [support.getoutline.org](https://support.getoutline.org/), and [contact us](https://support.getoutline.org/s/contactsupport) if you need further assistance. Developers, please post your questions on the SDK [Discussion board](https://github.com/Jigsaw-Code/outline-sdk/discussions). diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md new file mode 100644 index 0000000000..1d849a50fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support_request.md @@ -0,0 +1,11 @@ +--- +name: Support request +about: Get help troubleshooting an issue +title: '' +labels: 'customer support' +assignees: '' + +--- +The issue tracker is not for support requests. + +If you would like support with Outline Manager or Outline Client, please review the help articles on [support.getoutline.org](https://support.getoutline.org/), and [contact us](https://support.getoutline.org/s/contactsupport) if you need further assistance. Developers, please post your questions on the SDK [Discussion board](https://github.com/Jigsaw-Code/outline-sdk/discussions). diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000..4e74235ce5 --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module github.com/Jigsaw-Code/outline-apps + +go 1.20 + +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..56672c45c6 --- /dev/null +++ b/go.sum @@ -0,0 +1,45 @@ +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/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/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/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/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/device/config.go b/outline/device/config.go new file mode 100644 index 0000000000..356dd778bc --- /dev/null +++ b/outline/device/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 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 { + RemoteAddress string // the remote server address of "host:port" + 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{ + 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) + } + 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/device/config_test.go b/outline/device/config_test.go new file mode 100644 index 0000000000..0ef0996d15 --- /dev/null +++ b/outline/device/config_test.go @@ -0,0 +1,133 @@ +// 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_ParseConfigFromJSON(t *testing.T) { + tests := []struct { + 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"}`, + 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"}`, + 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"}`, + 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"}`, + 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" + `"}`, + expectAddress: "192.0.2.1: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"}`, + 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.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 new file mode 100644 index 0000000000..1a9ce70456 --- /dev/null +++ b/outline/device/device.go @@ -0,0 +1,67 @@ +// 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-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 := 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_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 new file mode 100644 index 0000000000..53a2f32a14 --- /dev/null +++ b/outline/device/packet_proxy.go @@ -0,0 +1,67 @@ +// 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" + + "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/x/connectivity" +) + +type outlinePacketProxy struct { + network.DelegatePacketProxy + remote, fallback network.PacketProxy + remotePktListener transport.PacketListener // this will be used in connectivity test +} + +func newOutlinePacketProxy(config *transportConfig) (proxy *outlinePacketProxy, err error) { + proxy = &outlinePacketProxy{} + + 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) + } + + 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 +} + +// 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..b837bbc443 --- /dev/null +++ b/outline/device/stream_dialer.go @@ -0,0 +1,32 @@ +// 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" +) + +// newOutlineStreamDialer creates a [transport.StreamDialer] that connects to the remote proxy using `config`. +func newOutlineStreamDialer(config *transportConfig) (transport.StreamDialer, error) { + dialer, err := shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: config.RemoteAddress}, config.CryptoKey) + if err != nil { + return nil, err + } + if len(config.Prefix) > 0 { + dialer.SaltGenerator = shadowsocks.NewPrefixSaltGenerator(config.Prefix) + } + return dialer, nil +}