Skip to content

Commit

Permalink
refactor vpn package architecture
Browse files Browse the repository at this point in the history
  • Loading branch information
jyyi1 committed Dec 16, 2024
1 parent 70f8303 commit a6606f8
Show file tree
Hide file tree
Showing 12 changed files with 337 additions and 247 deletions.
52 changes: 29 additions & 23 deletions client/electron/vpn_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '../src/www/app/outline_server_repository/vpn';

// TODO: Separate this config into LinuxVpnConfig and WindowsVpnConfig. Some fields may share.
interface EstablishVpnRequest {
interface VpnConfig {
id: string;
interfaceName: string;
connectionName: string;
Expand All @@ -28,6 +28,10 @@ interface EstablishVpnRequest {
routingTableId: number;
routingPriority: number;
protectionMark: number;
}

interface EstablishVpnRequest {
vpn: VpnConfig;
transport: string;
}

Expand All @@ -38,28 +42,30 @@ export async function establishVpn(request: StartRequestJson) {
statusCb?.(currentRequestId, TunnelStatus.RECONNECTING);

const config: EstablishVpnRequest = {
id: currentRequestId,

// TUN device name, being compatible with old code:
// https://github.com/Jigsaw-Code/outline-apps/blob/client/linux/v1.14.0/client/electron/linux_proxy_controller/outline_proxy_controller.h#L203
interfaceName: 'outline-tun0',

// Network Manager connection name, Use "TUN Connection" instead of "VPN Connection"
// because Network Manager has a dedicated "VPN Connection" concept that we did not implement
connectionName: 'Outline TUN Connection',

// TUN IP, being compatible with old code:
// https://github.com/Jigsaw-Code/outline-apps/blob/client/linux/v1.14.0/client/electron/linux_proxy_controller/outline_proxy_controller.h#L204
ipAddress: '10.0.85.1',

// DNS server list, being compatible with old code:
// https://github.com/Jigsaw-Code/outline-apps/blob/client/linux/v1.14.0/client/electron/linux_proxy_controller/outline_proxy_controller.h#L207
dnsServers: ['9.9.9.9'],

// Outline magic numbers, 7113 and 0x711E visually resembles "T L I E" in "ouTLInE"
routingTableId: 7113,
routingPriority: 0x711e,
protectionMark: 0x711e,
vpn: {
id: currentRequestId,

// TUN device name, being compatible with old code:
// https://github.com/Jigsaw-Code/outline-apps/blob/client/linux/v1.14.0/client/electron/linux_proxy_controller/outline_proxy_controller.h#L203
interfaceName: 'outline-tun0',

// Network Manager connection name, Use "TUN Connection" instead of "VPN Connection"
// because Network Manager has a dedicated "VPN Connection" concept that we did not implement
connectionName: 'Outline TUN Connection',

// TUN IP, being compatible with old code:
// https://github.com/Jigsaw-Code/outline-apps/blob/client/linux/v1.14.0/client/electron/linux_proxy_controller/outline_proxy_controller.h#L204
ipAddress: '10.0.85.1',

// DNS server list, being compatible with old code:
// https://github.com/Jigsaw-Code/outline-apps/blob/client/linux/v1.14.0/client/electron/linux_proxy_controller/outline_proxy_controller.h#L207
dnsServers: ['9.9.9.9'],

// Outline magic numbers, 7113 and 0x711E visually resembles "T L I E" in "ouTLInE"
routingTableId: 7113,
routingPriority: 0x711e,
protectionMark: 0x711e,
},

// The actual transport config
transport: JSON.stringify(request.config.transport),
Expand Down
47 changes: 25 additions & 22 deletions client/go/outline/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
package outline

import (
"context"
"errors"
"log/slog"

perrs "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors"
"github.com/Jigsaw-Code/outline-apps/client/go/outline/vpn"
"github.com/Jigsaw-Code/outline-sdk/network"
"github.com/Jigsaw-Code/outline-sdk/network/dnstruncate"
"github.com/Jigsaw-Code/outline-sdk/network/lwip2transport"
Expand All @@ -28,26 +31,29 @@ type Device struct {

c *Client
pkt network.DelegatePacketProxy
supportsUDP *bool
supportsUDP bool

remote, fallback network.PacketProxy
}

type LinuxOptions struct {
FWMark uint32
var _ vpn.ProxyDevice = (*Device)(nil)

func NewDevice(c *Client) (*Device, error) {
if c == nil {
return nil, errors.New("Client must be provided")
}
return &Device{c: c}, nil
}

type DeviceOptions struct {
LinuxOpts *LinuxOptions
func (d *Device) SupportsUDP() bool {
return d.supportsUDP
}

func NewDevice(c *Client) *Device {
return &Device{
c: c,
func (d *Device) Connect(ctx context.Context) (err error) {
if ctx.Err() != nil {
return perrs.PlatformError{Code: perrs.OperationCanceled}
}
}

func (d *Device) Connect() (err error) {
d.remote, err = network.NewPacketProxyFromPacketListener(d.c.PacketListener)
if err != nil {
return errSetupHandler("failed to create datagram handler", err)
Expand All @@ -59,7 +65,7 @@ func (d *Device) Connect() (err error) {
}
slog.Debug("[Outline] local DNS-fallback UDP handler created")

if err = d.RefreshConnectivity(); err != nil {
if err = d.RefreshConnectivity(ctx); err != nil {
return
}

Expand All @@ -69,19 +75,21 @@ func (d *Device) Connect() (err error) {
}
slog.Debug("[Outline] lwIP network stack configured")

slog.Info("[Outline] successfully connected to Outline server")
return nil
}

func (d *Device) Close() (err error) {
if d.IPDevice != nil {
err = d.IPDevice.Close()
}
slog.Info("[Outline] successfully disconnected from Outline server")
return
}

func (d *Device) RefreshConnectivity() (err error) {
func (d *Device) RefreshConnectivity(ctx context.Context) (err error) {
if ctx.Err() != nil {
return perrs.PlatformError{Code: perrs.OperationCanceled}
}

slog.Debug("[Outine] Testing connectivity of Outline server ...")
result := CheckTCPAndUDPConnectivity(d.c)
if result.TCPError != nil {
Expand All @@ -90,14 +98,14 @@ func (d *Device) RefreshConnectivity() (err error) {
}

var proxy network.PacketProxy
canHandleUDP := false
d.supportsUDP = false
if result.UDPError != nil {
slog.Warn("[Outline] server cannot handle UDP traffic", "err", result.UDPError)
proxy = d.fallback
} else {
slog.Debug("[Outline] server can handle UDP traffic")
proxy = d.remote
canHandleUDP = true
d.supportsUDP = true
}

if d.pkt == nil {
Expand All @@ -109,15 +117,10 @@ func (d *Device) RefreshConnectivity() (err error) {
return errSetupHandler("failed to update combined datagram handler", err)
}
}
d.supportsUDP = &canHandleUDP
slog.Info("[Outline] Outline server connectivity test done", "supportsUDP", canHandleUDP)
slog.Info("[Outline] Outline server connectivity test done", "supportsUDP", d.supportsUDP)
return nil
}

func (d *Device) SupportsUDP() *bool {
return d.supportsUDP
}

func errSetupHandler(msg string, cause error) error {
slog.Error("[Outline] "+msg, "err", cause)
return perrs.PlatformError{
Expand Down
32 changes: 14 additions & 18 deletions client/go/outline/electron/go_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,8 @@ import (

"github.com/Jigsaw-Code/outline-apps/client/go/outline"
"github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors"
"github.com/Jigsaw-Code/outline-apps/client/go/outline/vpn"
)

// init initializes the backend module.
// It sets up a default logger based on the OUTLINE_DEBUG environment variable.
func init() {
opts := slog.HandlerOptions{Level: slog.LevelInfo}

dbg := os.Getenv("OUTLINE_DEBUG")
if dbg != "" && dbg != "false" && dbg != "0" {
opts.Level = slog.LevelDebug
}

logger := slog.New(slog.NewTextHandler(os.Stderr, &opts))
slog.SetDefault(logger)

// Register VPN handlers for desktop environments
vpn.RegisterMethodHandlers()
}

// InvokeMethod is the unified entry point for TypeScript to invoke various Go functions.
//
// The input and output are all defined as string, but they may represent either a raw string,
Expand Down Expand Up @@ -109,3 +91,17 @@ func marshalCGoErrorJson(e *platerrors.PlatformError) *C.char {
}
return newCGoString(json)
}

// init initializes the backend module.
// It sets up a default logger based on the OUTLINE_DEBUG environment variable.
func init() {
opts := slog.HandlerOptions{Level: slog.LevelInfo}

dbg := os.Getenv("OUTLINE_DEBUG")
if dbg != "" && dbg != "false" && dbg != "0" {
opts.Level = slog.LevelDebug
}

logger := slog.New(slog.NewTextHandler(os.Stderr, &opts))
slog.SetDefault(logger)
}
33 changes: 12 additions & 21 deletions client/go/outline/method_channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,6 @@ const (
MethodCloseVPN = "CloseVPN"
)

// Handler is an interface that defines a method for handling requests from TypeScript.
type Handler func(string) (string, error)

// handlers is a map of registered handlers.
var handlers = make(map[string]Handler)

// RegisterMethodHandler registers a native function handler for the given method.
//
// Instead of having [InvokeMethod] directly depend on other packages, we use dependency inversion
// pattern here. This breaks Go's dependency cycle and makes the code more flexible.
func RegisterMethodHandler(method string, handler Handler) {
handlers[method] = handler
}

// InvokeMethodResult represents the result of an InvokeMethod call.
//
// We use a struct instead of a tuple to preserve a strongly typed error that gobind recognizes.
Expand All @@ -74,15 +60,20 @@ func InvokeMethod(method string, input string) *InvokeMethodResult {
Error: platerrors.ToPlatformError(err),
}

default:
if h, ok := handlers[method]; ok {
val, err := h(input)
return &InvokeMethodResult{
Value: val,
Error: platerrors.ToPlatformError(err),
}
case MethodEstablishVPN:
conn, err := establishVPN(input)
return &InvokeMethodResult{
Value: conn,
Error: platerrors.ToPlatformError(err),
}

case MethodCloseVPN:
err := closeVPN()
return &InvokeMethodResult{
Error: platerrors.ToPlatformError(err),
}

default:
return &InvokeMethodResult{Error: &platerrors.PlatformError{
Code: platerrors.InternalError,
Message: fmt.Sprintf("unsupported Go method: %s", method),
Expand Down
82 changes: 82 additions & 0 deletions client/go/outline/vpn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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"
"net"

perrs "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors"
"github.com/Jigsaw-Code/outline-apps/client/go/outline/vpn"
)

type vpnConfigJSON struct {
VPNConfig vpn.Config `json:"vpn"`
TransportConfig string `json:"transport"`
}

func establishVPN(configStr string) (string, error) {
var conf vpnConfigJSON
if err := json.Unmarshal([]byte(configStr), &conf); err != nil {
return "", perrs.PlatformError{
Code: perrs.IllegalConfig,
Message: "invalid VPN config format",
Cause: perrs.ToPlatformError(err),
}
}

// Create Outline Client and Device
tcpControl, err := vpn.TCPDialerControl(&conf.VPNConfig)
if err != nil {
return "", err
}
tcp := net.Dialer{
Control: tcpControl,
KeepAlive: -1,
}
udpControl, err := vpn.UDPDialerControl(&conf.VPNConfig)
if err != nil {
return "", err
}
udp := net.Dialer{Control: udpControl}
c, err := newClientWithBaseDialers(conf.TransportConfig, tcp, udp)
if err != nil {
return "", err
}
proxy, err := NewDevice(c)
if err != nil {
return "", err
}

// Establish system VPN to the proxy
conn, err := vpn.EstablishVPN(&conf.VPNConfig, proxy)
if err != nil {
return "", err
}

connJson, err := json.Marshal(conn)
if err != nil {
return "", perrs.PlatformError{
Code: perrs.InternalError,
Message: "failed to return VPN connection as JSON",
Cause: perrs.ToPlatformError(err),
}
}
return string(connJson), nil
}

func closeVPN() error {
return vpn.CloseVPN()
}
19 changes: 19 additions & 0 deletions client/go/outline/vpn/dialer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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 vpn

import "syscall"

type ControlFn = func(network, address string, c syscall.RawConn) error
Loading

0 comments on commit a6606f8

Please sign in to comment.