diff --git a/README.md b/README.md index bd2986aed6..b90509307e 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,21 @@ # Outline Apps -[![Mattermost](https://badgen.net/badge/Mattermost/Outline%20Community/blue)](https://community.internetfreedomfestival.org/community/channels/outline-community) [![Reddit](https://badgen.net/badge/Reddit/r%2Foutlinevpn/orange)](https://www.reddit.com/r/outlinevpn/) +[![Mattermost](https://badgen.net/badge/Mattermost/Outline%20Community/blue)](https://community.internetfreedomfestival.org/community/channels/outline-community) [![Reddit](https://badgen.net/badge/Reddit/r%2Foutlinevpn/orange)](https://www.reddit.com/r/outlinevpn/) +[![codecov](https://codecov.io/gh/Jigsaw-Code/outline-app/branch/master/graph/badge.svg?token=gasD8v5tjn)](https://codecov.io/gh/Jigsaw-Code/outline-app) ## Access to the free and open internet! Outline makes it easy to create a VPN server, giving anyone access to the free and open internet. -We have two core apps: [Ouline Manager](./server_manager) and [Ouline Client](./src). Go to https://getoutline.org for ready-to-use versions of both. +We have two core apps -### Outline Manager +- Outline Manager Logo **Outline Manager** ([`/server_manager`](server_manager)): A graphical user interface for managing Outline servers. It is available for Windows, macOS, and Linux. -The Outline Manager is a graphical user interface for managing Outline servers. It is available for Windows, macOS, and Linux. +- Outline Client Logo **Outline Client** ([`/client`](client)): A cross-platform proxy client for Windows, macOS, iOS, Android, and Linux. The Outline Client is designed for use with the server deployed with the Outline Manager, but it is also fully compatible with any [Shadowsocks](https://shadowsocks.org/) server. -### Outline Client +Go to https://getoutline.org for ready-to-use versions of both. -The Outline Client is a cross-platform proxy client for Windows, macOS, iOS, Android, and Linux. The Outline Client is designed for use with the server deployed with the Outline Manager, but it is also fully compatible with any [Shadowsocks](https://shadowsocks.org/) server. - - - -![Build and Test](https://github.com/Jigsaw-Code/outline-client/actions/workflows/build_and_test_debug_client.yml/badge.svg?branch=master) [![codecov](https://codecov.io/gh/Jigsaw-Code/outline-client/branch/master/graph/badge.svg?token=gasD8v5tjn)](https://codecov.io/gh/Jigsaw-Code/outline-client) - -> [!NOTE] -> Test coverage currently only tracks the Apple Libraries and core web view code. - -We have two core apps: [Ouline Manager](./server_manager) and [Ouline Client](./client). - -To join our Outline Community, [sign up for the IFF Mattermost](https://wiki.digitalrights.community/index.php?title=IFF_Mattermost). - -#### Requirements for all builds - -### Outline Client - -> 💡 NOTE: if you have `nvm` installed, run `nvm use` to switch to the correct node version! - -After cloning this repo, install all node dependencies: - -```sh -npm install -``` - -#### Building the shared web app - -Outline clients share the same web app across all platforms. This code is located in the src/www directory. If you are making changes to the shared web app and do not need to test platform-specific functionality, you can test in a desktop browser by running: - -```sh -npm run action client/src/www/start -``` - -The latter command will open a browser instance running the app. Browser platform development will use fake servers to test successful and unsuccessful connections. - -The app logic is located in [src/www/app](src/www/app). UI components are located in [src/www/ui_components](src/www/ui_components). If you want to work specifically on an individual UI element, try the storybook!: - -```sh -npm run action client/src/www/storybook -``` - -> [!NOTE] -> The `src` part of the path is optional. `npm run action www/start` resolves to the same script. - -> [!NOTE] -> Every script in this repository can be run with `npm run action` - -> for a CLI-like experience, add something like -> -> ```sh -> alias outline="npm run action" -> ``` -> -> _(you can call it whatever you like)_ -> -> to your shell, then try `outline www/start`! - -#### Passing configuration flags to actions - -Certain actions take configuration flags - but since we're running them through `npm`, you'll have to use the `--` seperator to funnel them through to the underlying process. For example, to set up a MacOS project in release mode, you'd run: - -```sh -SENTRY_DSN= npm run action client/src/cordova/setup macos -- --buildMode=release --versionName= -``` - -#### Life of a Packet - -[How does the Outline Client work?](docs/life_of_a_packet.md) - -#### Accepting a server invite - -[Looking for instructions on how to accept a server invite?](docs/invitation_instructions.md) - -#### Platform-specific development - -Each platform is handled differently: - -1. [Developing for Apple **(MacOS and iOS)**](src/cordova/apple) -2. [Developing for **Android**](src/cordova/android) -3. [Developing for Electron **(Windows and Linux)**](src/electron) - -#### Error reporting - -To enable error reporting through [Sentry](https://sentry.io/) for local builds, run: - -```bash -export SENTRY_DSN=[Sentry development API key] -[platform-specific build command] -``` - -Release builds on CI are configured with a production Sentry API key. +**Join the Outline Community** by signing up for the [IFF Mattermost](https://wiki.digitalrights.community/index.php?title=IFF_Mattermost)! ## Support diff --git a/client/README.md b/client/README.md index 3787ff9d6b..d5c5b148c7 100644 --- a/client/README.md +++ b/client/README.md @@ -1,16 +1,14 @@ # Outline Client -![Build and Test](https://github.com/Jigsaw-Code/outline-apps/actions/workflows/build_and_test_debug_client.yml/badge.svg?branch=master) +![Build and Test](https://github.com/Jigsaw-Code/outline-apps/actions/workflows/build_and_test_debug_client.yml/badge.svg?branch=master) [![codecov](https://codecov.io/gh/Jigsaw-Code/outline-apps/graph/badge.svg?token=gasD8v5tjn)](https://codecov.io/gh/Jigsaw-Code/outline-apps) -> **Test coverage currently only tracks the Apple Libraries and core web view code:** -> -> [![codecov](https://codecov.io/gh/Jigsaw-Code/outline-apps/branch/master/graph/badge.svg?token=gasD8v5tjn)](https://codecov.io/gh/Jigsaw-Code/outline-apps) +> [!NOTE] +> Test coverage currently only tracks the Apple Libraries and core web view code. The Outline Client is a cross-platform VPN or proxy client for Windows, macOS, iOS, Android, and ChromeOS. The Outline Client is designed for use with the [Outline Server](https://github.com/Jigsaw-Code/outline-server) software, but it is fully compatible with any [Shadowsocks](https://shadowsocks.org/) server. The client's user interface is implemented in [Polymer](https://www.polymer-project.org/) 2.0. Platform support is provided by [Cordova](https://cordova.apache.org/) and [Electron](https://electronjs.org/), with additional native components in this repository. -To join our Outline Community, [sign up for the IFF Mattermost](https://internetfreedomfestival.org/wiki/index.php/IFF_Mattermost). ## Requirements for all builds @@ -40,7 +38,8 @@ The app logic is located in [src/www/app](src/www/app). UI components are locate npm run action client/src/www/storybook ``` -> 💡 NOTE: every script in this repository can be run with `npm run action` - +> [!NOTE] +> Every script in this repository can be run with `npm run action` - > for a CLI-like experience, add something like > > ```sh diff --git a/docs/resources/logo_client.png b/docs/resources/logo_client.png new file mode 100644 index 0000000000..d6e6944e11 Binary files /dev/null and b/docs/resources/logo_client.png differ diff --git a/docs/resources/logo_manager.png b/docs/resources/logo_manager.png new file mode 100644 index 0000000000..c7a527695b Binary files /dev/null and b/docs/resources/logo_manager.png differ diff --git a/outline/device/config.go b/outline/device/config.go deleted file mode 100644 index 356dd778bc..0000000000 --- a/outline/device/config.go +++ /dev/null @@ -1,98 +0,0 @@ -// 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 deleted file mode 100644 index 0ef0996d15..0000000000 --- a/outline/device/config_test.go +++ /dev/null @@ -1,133 +0,0 @@ -// 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 deleted file mode 100644 index 1a9ce70456..0000000000 --- a/outline/device/device.go +++ /dev/null @@ -1,67 +0,0 @@ -// 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 deleted file mode 100644 index 33c3fdec09..0000000000 --- a/outline/device/device_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// 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 deleted file mode 100644 index 277cc12b4b..0000000000 --- a/outline/device/packet_listener.go +++ /dev/null @@ -1,25 +0,0 @@ -// 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 deleted file mode 100644 index 53a2f32a14..0000000000 --- a/outline/device/packet_proxy.go +++ /dev/null @@ -1,67 +0,0 @@ -// 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 deleted file mode 100644 index b837bbc443..0000000000 --- a/outline/device/stream_dialer.go +++ /dev/null @@ -1,32 +0,0 @@ -// 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 -}