Skip to content

Commit

Permalink
Updates
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna committed Jan 25, 2025
1 parent 0fa5ab0 commit e60341a
Show file tree
Hide file tree
Showing 14 changed files with 186 additions and 45 deletions.
6 changes: 3 additions & 3 deletions client/go/outline/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand All @@ -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),
}
Expand Down
2 changes: 1 addition & 1 deletion client/go/outline/electron/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
24 changes: 5 additions & 19 deletions client/go/outline/method_channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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{
Expand Down
114 changes: 114 additions & 0 deletions client/go/outline/parse.go
Original file line number Diff line number Diff line change
@@ -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),
}
}
38 changes: 38 additions & 0 deletions client/go/outline/parse_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
7 changes: 5 additions & 2 deletions client/go/outline/platerrors/error_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
4 changes: 2 additions & 2 deletions client/go/outline/vpn/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions client/go/outline/vpn/vpn_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion client/go/outline/vpn_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(_):
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
6 changes: 3 additions & 3 deletions client/src/www/model/platform_error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand All @@ -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});
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit e60341a

Please sign in to comment.