From e60341a6d502c6651d44b8f26cc002dfec44c110 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Sat, 25 Jan 2025 00:52:44 -0500 Subject: [PATCH] Updates --- client/go/outline/client.go | 6 +- client/go/outline/electron/main.go | 2 +- client/go/outline/method_channel.go | 24 +--- client/go/outline/parse.go | 114 ++++++++++++++++++ client/go/outline/parse_test.go | 38 ++++++ client/go/outline/platerrors/error_code.go | 7 +- client/go/outline/vpn/errors.go | 4 +- client/go/outline/vpn/vpn_linux.go | 8 +- client/go/outline/vpn_linux.go | 2 +- .../org/outline/vpn/VpnTunnelService.java | 2 +- .../Sources/OutlineError/OutlineError.swift | 8 +- .../Sources/PacketTunnelProvider.m | 4 +- .../Sources/PacketTunnelProvider.swift | 6 +- client/src/www/model/platform_error.ts | 6 +- 14 files changed, 186 insertions(+), 45 deletions(-) create mode 100644 client/go/outline/parse.go create mode 100644 client/go/outline/parse_test.go diff --git a/client/go/outline/client.go b/client/go/outline/client.go index b1e811a691d..4b67c657f08 100644 --- a/client/go/outline/client.go +++ b/client/go/outline/client.go @@ -64,7 +64,7 @@ func newClientWithBaseDialers(transportConfig string, tcpDialer transport.Stream transportYAML, err := config.ParseConfigYAML(transportConfig) if err != nil { return nil, &platerrors.PlatformError{ - Code: platerrors.IllegalConfig, + Code: platerrors.InvalidConfig, Message: "config is not valid YAML", Cause: platerrors.ToPlatformError(err), } @@ -74,13 +74,13 @@ func newClientWithBaseDialers(transportConfig string, tcpDialer transport.Stream if err != nil { if errors.Is(err, errors.ErrUnsupported) { return nil, &platerrors.PlatformError{ - Code: platerrors.IllegalConfig, + Code: platerrors.InvalidConfig, Message: "unsupported config", Cause: platerrors.ToPlatformError(err), } } else { return nil, &platerrors.PlatformError{ - Code: platerrors.IllegalConfig, + Code: platerrors.InvalidConfig, Message: "failed to create transport", Cause: platerrors.ToPlatformError(err), } diff --git a/client/go/outline/electron/main.go b/client/go/outline/electron/main.go index 79230f4d1d9..3610e86c90c 100644 --- a/client/go/outline/electron/main.go +++ b/client/go/outline/electron/main.go @@ -113,7 +113,7 @@ func main() { setLogLevel(*args.logLevel) if len(*args.transportConfig) == 0 { - printErrorAndExit(platerrors.PlatformError{Code: platerrors.IllegalConfig, Message: "transport config missing"}, exitCodeFailure) + printErrorAndExit(platerrors.PlatformError{Code: platerrors.InvalidConfig, Message: "transport config missing"}, exitCodeFailure) } clientResult := outline.NewClient(*args.transportConfig) if clientResult.Error != nil { diff --git a/client/go/outline/method_channel.go b/client/go/outline/method_channel.go index 92773563ce1..09de961cfa8 100644 --- a/client/go/outline/method_channel.go +++ b/client/go/outline/method_channel.go @@ -40,10 +40,10 @@ const ( // - Output: the content in raw string of the fetched resource MethodFetchResource = "FetchResource" - // GetFirstHop validates a transport config and returns the first hop. + // Parses the TunnelConfig and extracts the first hop or provider error as needed. // - Input: the transport config text - // - Output: the host:port address of the first hop, if applicable. - MethodGetFirstHop = "GetFirstHop" + // - Output: the TunnelConfigJson that Typescript needs + MethodParseTunnelConfig = "ParseTunnelConfig" ) // InvokeMethodResult represents the result of an InvokeMethod call. @@ -77,22 +77,8 @@ func InvokeMethod(method string, input string) *InvokeMethodResult { Error: platerrors.ToPlatformError(err), } - case MethodGetFirstHop: - result := NewClient(input) - if result.Error != nil { - return &InvokeMethodResult{ - Error: result.Error, - } - } - streamFirstHop := result.Client.sd.ConnectionProviderInfo.FirstHop - packetFirstHop := result.Client.pl.ConnectionProviderInfo.FirstHop - firstHop := "" - if streamFirstHop == packetFirstHop { - firstHop = streamFirstHop - } - return &InvokeMethodResult{ - Value: firstHop, - } + case MethodParseTunnelConfig: + return doParseTunnelConfig(input) default: return &InvokeMethodResult{Error: &platerrors.PlatformError{ diff --git a/client/go/outline/parse.go b/client/go/outline/parse.go new file mode 100644 index 00000000000..8c9d1ab1271 --- /dev/null +++ b/client/go/outline/parse.go @@ -0,0 +1,114 @@ +// Copyright 2024 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 outline + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" + "gopkg.in/yaml.v3" +) + +type parseTunnelConfigRequest struct { + Transport yaml.Node + Error *struct { + Message string + Details string + } +} + +// tunnelConfigJson must match the definition in config.ts. +type tunnelConfigJson struct { + FirstHop string `json:"firstHop"` + Transport string `json:"transport"` +} + +func doParseTunnelConfig(input string) *InvokeMethodResult { + var transportConfigText string + + input = strings.TrimSpace(input) + // Input may be one of: + // - ss:// link + // - Legacy Shadowsocks JSON (parsed as YAML) + // - New advanced YAML format + if strings.HasPrefix(input, "ss://") { + transportConfigText = input + } else { + // Parse as YAML. + tunnelConfig := parseTunnelConfigRequest{} + if err := yaml.Unmarshal([]byte(input), &tunnelConfig); err != nil { + return &InvokeMethodResult{ + Error: &platerrors.PlatformError{ + Code: platerrors.InvalidConfig, + Message: fmt.Sprintf("failed to parse: %s", err), + }, + } + } + + // Process provider error, if present. + if tunnelConfig.Error != nil { + platErr := &platerrors.PlatformError{ + Code: platerrors.ProviderError, + Message: tunnelConfig.Error.Message, + } + if tunnelConfig.Error.Details != "" { + platErr.Details = map[string]any{ + "details": tunnelConfig.Error.Details, + } + } + return &InvokeMethodResult{Error: platErr} + } + + // Extract transport config as an opaque string. + transportConfigBytes, err := yaml.Marshal(tunnelConfig.Transport) + if err != nil { + return &InvokeMethodResult{ + Error: &platerrors.PlatformError{ + Code: platerrors.InvalidConfig, + Message: fmt.Sprintf("failed normalize config: %s", err), + }, + } + } + transportConfigText = string(transportConfigBytes) + } + + result := NewClient(transportConfigText) + if result.Error != nil { + return &InvokeMethodResult{ + Error: result.Error, + } + } + streamFirstHop := result.Client.sd.ConnectionProviderInfo.FirstHop + packetFirstHop := result.Client.pl.ConnectionProviderInfo.FirstHop + response := tunnelConfigJson{Transport: transportConfigText} + if streamFirstHop == packetFirstHop { + response.FirstHop = streamFirstHop + } + responseBytes, err := json.Marshal(response) + if err != nil { + return &InvokeMethodResult{ + Error: &platerrors.PlatformError{ + Code: platerrors.InternalError, + Message: fmt.Sprintf("failed to serialize JSON response: %v", err), + }, + } + } + + return &InvokeMethodResult{ + Value: string(responseBytes), + } +} diff --git a/client/go/outline/parse_test.go b/client/go/outline/parse_test.go new file mode 100644 index 00000000000..d262532aeac --- /dev/null +++ b/client/go/outline/parse_test.go @@ -0,0 +1,38 @@ +// Copyright 2024 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 outline + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_doParseTunnelConfig(t *testing.T) { + result := doParseTunnelConfig(` +transport: + $type: tcpudp + tcp: &shared + $type: shadowsocks + endpoint: example.com:80 + cipher: chacha20-ietf-poly1305 + secret: SECRET + udp: *shared`) + + require.Nil(t, result.Error) + require.Equal(t, + "{\"firstHop\":\"example.com:80\",\"transport\":\"$type: tcpudp\\ntcp: \\u0026shared\\n $type: shadowsocks\\n endpoint: example.com:80\\n cipher: chacha20-ietf-poly1305\\n secret: SECRET\\nudp: *shared\\n\"}", + result.Value) +} diff --git a/client/go/outline/platerrors/error_code.go b/client/go/outline/platerrors/error_code.go index 72ba6f80c5c..dfbb99a2f3c 100644 --- a/client/go/outline/platerrors/error_code.go +++ b/client/go/outline/platerrors/error_code.go @@ -97,6 +97,9 @@ const ( // FetchConfigFailed means we failed to fetch a config from a remote location. FetchConfigFailed ErrorCode = "ERR_FETCH_CONFIG_FAILURE" - // IllegalConfig indicates an invalid config to connect to a remote server. - IllegalConfig ErrorCode = "ERR_ILLEGAL_CONFIG" + // ProviderError indicates an error returned by the provider in the Dynamic Config. + ProviderError ErrorCode = "ERR_PROVIDER" + + // InvalidConfig indicates an invalid config to connect to a remote server. + InvalidConfig ErrorCode = "ERR_INVALID_CONFIG" ) diff --git a/client/go/outline/vpn/errors.go b/client/go/outline/vpn/errors.go index 0b2399832b4..f7a2122ebe0 100644 --- a/client/go/outline/vpn/errors.go +++ b/client/go/outline/vpn/errors.go @@ -28,8 +28,8 @@ func errCancelled(cause error) error { } } -func errIllegalConfig(msg string, params ...any) error { - return errPlatError(perrs.IllegalConfig, msg, nil, params...) +func errInvalidConfig(msg string, params ...any) error { + return errPlatError(perrs.InvalidConfig, msg, nil, params...) } func errSetupVPN(msg string, cause error, params ...any) error { diff --git a/client/go/outline/vpn/vpn_linux.go b/client/go/outline/vpn/vpn_linux.go index 1310ee4dc06..02054ff5eda 100644 --- a/client/go/outline/vpn/vpn_linux.go +++ b/client/go/outline/vpn/vpn_linux.go @@ -50,18 +50,18 @@ func newPlatformVPNConn(conf *Config) (_ platformVPNConn, err error) { } if c.nmOpts.Name == "" { - return nil, errIllegalConfig("must provide a valid connection name") + return nil, errInvalidConfig("must provide a valid connection name") } if c.nmOpts.TUNName == "" { - return nil, errIllegalConfig("must provide a valid TUN interface name") + return nil, errInvalidConfig("must provide a valid TUN interface name") } if c.nmOpts.TUNAddr4 == nil { - return nil, errIllegalConfig("must provide a valid TUN interface IP(v4)") + return nil, errInvalidConfig("must provide a valid TUN interface IP(v4)") } for _, dns := range conf.DNSServers { dnsIP := net.ParseIP(dns).To4() if dnsIP == nil { - return nil, errIllegalConfig("DNS server must be a valid IP(v4)", "dns", dns) + return nil, errInvalidConfig("DNS server must be a valid IP(v4)", "dns", dns) } c.nmOpts.DNSServers4 = append(c.nmOpts.DNSServers4, dnsIP) } diff --git a/client/go/outline/vpn_linux.go b/client/go/outline/vpn_linux.go index fb2f29b1f34..d9b839b51be 100644 --- a/client/go/outline/vpn_linux.go +++ b/client/go/outline/vpn_linux.go @@ -36,7 +36,7 @@ func establishVPN(configStr string) error { var conf vpnConfigJSON if err := json.Unmarshal([]byte(configStr), &conf); err != nil { return perrs.PlatformError{ - Code: perrs.IllegalConfig, + Code: perrs.InvalidConfig, Message: "invalid VPN config format", Cause: perrs.ToPlatformError(err), } diff --git a/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java b/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java index 732db1efdfa..dce141c7cdc 100644 --- a/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java +++ b/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java @@ -182,7 +182,7 @@ private synchronized PlatformError startTunnel( final TunnelConfig config, boolean isAutoStart) { LOG.info(String.format(Locale.ROOT, "Starting tunnel %s for server %s", config.id, config.name)); if (config.id == null || config.transportConfig == null) { - return new PlatformError(Platerrors.IllegalConfig, "id and transportConfig are required"); + return new PlatformError(Platerrors.InvalidConfig, "id and transportConfig are required"); } final boolean isRestart = tunnelConfig != null; if (isRestart) { diff --git a/client/src/cordova/apple/OutlineAppleLib/Sources/OutlineError/OutlineError.swift b/client/src/cordova/apple/OutlineAppleLib/Sources/OutlineError/OutlineError.swift index 21f60126ee6..1521f82117a 100644 --- a/client/src/cordova/apple/OutlineAppleLib/Sources/OutlineError/OutlineError.swift +++ b/client/src/cordova/apple/OutlineAppleLib/Sources/OutlineError/OutlineError.swift @@ -36,7 +36,7 @@ public enum OutlineError: Error, CustomNSError { case internalError(message: String) /// Indicates the VPN config is not valid. - case illegalConfig(message: String) + case invalidConfig(message: String) /// Indicates the user did not grant VPN permissions. case vpnPermissionNotGranted(cause: Error) @@ -55,8 +55,8 @@ public enum OutlineError: Error, CustomNSError { return error.code case .internalError(_): return PlaterrorsInternalError - case .illegalConfig(_): - return PlaterrorsIllegalConfig + case .invalidConfig(_): + return PlaterrorsInvalidConfig case .vpnPermissionNotGranted(_): return PlaterrorsVPNPermissionNotGranted case .setupSystemVPNFailed(_): @@ -143,7 +143,7 @@ private func marshalErrorJson(outlineError: OutlineError) -> String { } return errorJson - case .internalError(let message), .illegalConfig(let message): + case .internalError(let message), .invalidConfig(let message): return marshalErrorJson(code: outlineError.code, message: message) case .vpnPermissionNotGranted(let cause), .setupSystemVPNFailed(let cause): diff --git a/client/src/cordova/apple/OutlineLib/VpnExtension/Sources/PacketTunnelProvider.m b/client/src/cordova/apple/OutlineLib/VpnExtension/Sources/PacketTunnelProvider.m index fc9523bc8d9..da3e6d68f74 100644 --- a/client/src/cordova/apple/OutlineLib/VpnExtension/Sources/PacketTunnelProvider.m +++ b/client/src/cordova/apple/OutlineLib/VpnExtension/Sources/PacketTunnelProvider.m @@ -72,7 +72,7 @@ - (void)startTunnelWithOptions:(NSDictionary *)options // MARK: Process Config. if (self.protocolConfiguration == nil) { DDLogError(@"Failed to retrieve NETunnelProviderProtocol."); - return startDone([SwiftBridge newIllegalConfigOutlineErrorWithMessage:@"no config specified"]); + return startDone([SwiftBridge newInvalidConfigOutlineErrorWithMessage:@"no config specified"]); } NETunnelProviderProtocol *protocol = (NETunnelProviderProtocol *)self.protocolConfiguration; NSString *tunnelId = protocol.providerConfiguration[@"id"]; @@ -84,7 +84,7 @@ - (void)startTunnelWithOptions:(NSDictionary *)options NSString *transportConfig = protocol.providerConfiguration[@"transport"]; if (![transportConfig isKindOfClass:[NSString class]]) { DDLogError(@"Failed to retrieve the transport configuration."); - return startDone([SwiftBridge newIllegalConfigOutlineErrorWithMessage:@"config is not a String"]); + return startDone([SwiftBridge newInvalidConfigOutlineErrorWithMessage:@"config is not a String"]); } self.transportConfig = transportConfig; diff --git a/client/src/cordova/apple/OutlineLib/VpnExtension/Sources/PacketTunnelProvider.swift b/client/src/cordova/apple/OutlineLib/VpnExtension/Sources/PacketTunnelProvider.swift index 8acd1ed8857..d3277bcba08 100644 --- a/client/src/cordova/apple/OutlineLib/VpnExtension/Sources/PacketTunnelProvider.swift +++ b/client/src/cordova/apple/OutlineLib/VpnExtension/Sources/PacketTunnelProvider.swift @@ -61,10 +61,10 @@ public class SwiftBridge: NSObject { } /** - Creates a NSError (of `OutlineError.errorDomain`) from the `OutlineError.illegalConfig` error. + Creates a NSError (of `OutlineError.errorDomain`) from the `OutlineError.invalidConfig` error. */ - public static func newIllegalConfigOutlineError(message: String) -> NSError { - return OutlineError.illegalConfig(message: message) as NSError + public static func newInvalidConfigOutlineError(message: String) -> NSError { + return OutlineError.invalidConfig(message: message) as NSError } /** diff --git a/client/src/www/model/platform_error.ts b/client/src/www/model/platform_error.ts index 72126ebfcc1..4edd036bb42 100644 --- a/client/src/www/model/platform_error.ts +++ b/client/src/www/model/platform_error.ts @@ -91,7 +91,7 @@ function convertRawErrorObjectToError(rawObj: object): Error { switch (code) { case GoErrorCode.FETCH_CONFIG_FAILED: return new errors.SessionConfigFetchFailed(detailsMessage, {cause}); - case GoErrorCode.ILLEGAL_CONFIG: + case GoErrorCode.INVALID_CONFIG: return new errors.InvalidServiceConfiguration(detailsMessage, {cause}); case GoErrorCode.PROXY_SERVER_UNREACHABLE: return new errors.ServerUnreachable(detailsMessage, {cause}); @@ -100,7 +100,7 @@ function convertRawErrorObjectToError(rawObj: object): Error { case GoErrorCode.PROVIDER_ERROR: return new errors.SessionProviderError( detailsMessage, - (detailsMap as {providerDetails?: string})?.providerDetails + (detailsMap as {details?: string})?.details ); default: { const error = new Error(detailsMessage, {cause}); @@ -288,7 +288,7 @@ export function deserializeError(errObj: string | Error | unknown): Error { export enum GoErrorCode { INTERNAL_ERROR = 'ERR_INTERNAL_ERROR', FETCH_CONFIG_FAILED = 'ERR_FETCH_CONFIG_FAILURE', - ILLEGAL_CONFIG = 'ERR_ILLEGAL_CONFIG', + INVALID_CONFIG = 'ERR_INVALID_CONFIG', PROVIDER_ERROR = 'ERR_PROVIDER', VPN_PERMISSION_NOT_GRANTED = 'ERR_VPN_PERMISSION_NOT_GRANTED', PROXY_SERVER_UNREACHABLE = 'ERR_PROXY_SERVER_UNREACHABLE',