diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml index 1f5d44d26f..342e2b36cf 100644 --- a/.github/workflows/license.yml +++ b/.github/workflows/license.yml @@ -44,4 +44,4 @@ jobs: - name: Check Go dependency tree licenses # We allow only "notice" type of licenses. - run: go run github.com/google/go-licenses@latest check --ignore=golang.org/x --allowed_licenses=Apache-2.0,Apache-3,BSD-3-Clause,BSD-4-Clause,CC0-1.0,ISC,MIT ./... + run: go run github.com/google/go-licenses@latest check --ignore=golang.org/x --allowed_licenses=Apache-2.0,Apache-3,BSD-2-Clause,BSD-3-Clause,BSD-4-Clause,CC0-1.0,ISC,MIT ./... diff --git a/client/electron/debian/after_install.sh b/client/electron/debian/after_install.sh index f2a785aae1..eca685a976 100644 --- a/client/electron/debian/after_install.sh +++ b/client/electron/debian/after_install.sh @@ -16,10 +16,24 @@ # Dependencies: # - libcap2-bin: setcap +# - patchelf: patchelf set -eux +# Capabilitites will disable LD_LIBRARY_PATH, and $ORIGIN evaluation in binary's +# rpath. So we need to set the rpath to an absolute path. (for libffmpeg.so) +# This command will also reset capabilitites, so we need to run this before setcap. +/usr/bin/patchelf --add-rpath /opt/Outline /opt/Outline/Outline + # Grant specific capabilities so Outline can run without root permisssion # - cap_net_admin: configure network interfaces, set up routing tables, etc. # - cap_dac_override: modify network configuration files owned by root /usr/sbin/setcap cap_net_admin,cap_dac_override+eip /opt/Outline/Outline + +# From electron's hint: +# > The SUID sandbox helper binary was found, but is not configured correctly. +# > Rather than run without sandboxing I'm aborting now. You need to make sure +# > that /opt/Outline/chrome-sandbox is owned by root and has mode 4755. +# +# https://github.com/electron/electron/issues/42510 +/usr/bin/chmod 4755 /opt/Outline/chrome-sandbox diff --git a/client/electron/electron-builder.json b/client/electron/electron-builder.json index b2227c1e00..b4ba54c123 100644 --- a/client/electron/electron-builder.json +++ b/client/electron/electron-builder.json @@ -19,8 +19,8 @@ "deb": { "depends": [ - "gconf2", "gconf-service", "libnotify4", "libappindicator1", "libxtst6", "libnss3", - "libcap2-bin" + "libnotify4", "libxtst6", "libnss3", + "libcap2-bin", "patchelf" ], "afterInstall": "client/electron/debian/after_install.sh" }, diff --git a/client/electron/go_plugin.ts b/client/electron/go_plugin.ts index ac65c06314..76fe00c9af 100644 --- a/client/electron/go_plugin.ts +++ b/client/electron/go_plugin.ts @@ -56,9 +56,9 @@ export async function invokeMethod( ); } - console.debug('[Backend] - calling InvokeMethod ...'); + console.debug(`[Backend] - calling InvokeMethod "${method}" ...`); const result = await invokeMethodFunc(method, input); - console.debug('[Backend] - InvokeMethod returned', result); + console.debug(`[Backend] - InvokeMethod "${method}" returned`, result); if (result.ErrorJson) { throw Error(result.ErrorJson); } diff --git a/client/electron/index.ts b/client/electron/index.ts index 3b90a583f9..c75bb18326 100644 --- a/client/electron/index.ts +++ b/client/electron/index.ts @@ -38,6 +38,7 @@ import {invokeMethod} from './go_plugin'; import {GoVpnTunnel} from './go_vpn_tunnel'; import {installRoutingServices, RoutingDaemon} from './routing_service'; import {TunnelStore} from './tunnel_store'; +import {closeVpn, establishVpn, onVpnStatusChanged} from './vpn_service'; import {VpnTunnel} from './vpn_tunnel'; import * as config from '../src/www/app/outline_server_repository/config'; import { @@ -56,7 +57,7 @@ declare const APP_VERSION: string; // Run-time environment variables: const debugMode = process.env.OUTLINE_DEBUG === 'true'; -const isLinux = os.platform() === 'linux'; +const IS_LINUX = os.platform() === 'linux'; // Used for the auto-connect feature. There will be a tunnel in store // if the user was connected at shutdown. @@ -158,7 +159,7 @@ function setupWindow(): void { // // The ideal solution would be: either electron-builder supports the app icon; or we add // dpi-aware features to this app. - if (isLinux) { + if (IS_LINUX) { mainWindow.setIcon( path.join( app.getAppPath(), @@ -251,7 +252,7 @@ function updateTray(status: TunnelStatus) { {type: 'separator'} as MenuItemConstructorOptions, {label: localizedStrings['quit'], click: quitApp}, ]; - if (isLinux) { + if (IS_LINUX) { // Because the click event is never fired on Linux, we need an explicit open option. menuTemplate = [ { @@ -309,7 +310,7 @@ function interceptShadowsocksLink(argv: string[]) { async function setupAutoLaunch(request: StartRequestJson): Promise { try { await tunnelStore.save(request); - if (isLinux) { + if (IS_LINUX) { if (process.env.APPIMAGE) { const outlineAutoLauncher = new autoLaunch({ name: 'OutlineClient', @@ -327,7 +328,7 @@ async function setupAutoLaunch(request: StartRequestJson): Promise { async function tearDownAutoLaunch() { try { - if (isLinux) { + if (IS_LINUX) { const outlineAutoLauncher = new autoLaunch({ name: 'OutlineClient', }); @@ -368,6 +369,15 @@ async function createVpnTunnel( // Invoked by both the start-proxying event handler and auto-connect. async function startVpn(request: StartRequestJson, isAutoConnect: boolean) { + if (IS_LINUX && !process.env.APPIMAGE) { + onVpnStatusChanged((id, status) => { + setUiTunnelStatus(status, id); + console.info('VPN Status Changed: ', id, status); + }); + await establishVpn(request); + return; + } + if (currentTunnel) { throw new Error('already connected'); } @@ -401,6 +411,11 @@ async function startVpn(request: StartRequestJson, isAutoConnect: boolean) { // Invoked by both the stop-proxying event and quit handler. async function stopVpn() { + if (IS_LINUX && !process.env.APPIMAGE) { + await Promise.all([closeVpn(), tearDownAutoLaunch()]); + return; + } + if (!currentTunnel) { return; } diff --git a/client/electron/vpn_service.ts b/client/electron/vpn_service.ts new file mode 100644 index 0000000000..a43068a5d7 --- /dev/null +++ b/client/electron/vpn_service.ts @@ -0,0 +1,90 @@ +// 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. + +import {invokeMethod} from './go_plugin'; +import { + StartRequestJson, + TunnelStatus, +} from '../src/www/app/outline_server_repository/vpn'; + +// TODO: Separate this config into LinuxVpnConfig and WindowsVpnConfig. Some fields may share. +interface VpnConfig { + id: string; + interfaceName: string; + connectionName: string; + ipAddress: string; + dnsServers: string[]; + routingTableId: number; + routingPriority: number; + protectionMark: number; +} + +interface EstablishVpnRequest { + vpn: VpnConfig; + transport: string; +} + +let currentRequestId: string | undefined = undefined; + +export async function establishVpn(request: StartRequestJson) { + currentRequestId = request.id; + statusCb?.(currentRequestId, TunnelStatus.RECONNECTING); + + const config: EstablishVpnRequest = { + 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), + }; + + await invokeMethod('EstablishVPN', JSON.stringify(config)); + statusCb?.(currentRequestId, TunnelStatus.CONNECTED); +} + +export async function closeVpn(): Promise { + statusCb?.(currentRequestId!, TunnelStatus.DISCONNECTING); + await invokeMethod('CloseVPN', ''); + statusCb?.(currentRequestId!, TunnelStatus.DISCONNECTED); +} + +export type VpnStatusCallback = (id: string, status: TunnelStatus) => void; + +let statusCb: VpnStatusCallback | undefined = undefined; + +export function onVpnStatusChanged(cb: VpnStatusCallback): void { + statusCb = cb; +} diff --git a/client/go/outline/client.go b/client/go/outline/client.go index eef75dbe32..cc39396bb0 100644 --- a/client/go/outline/client.go +++ b/client/go/outline/client.go @@ -42,23 +42,29 @@ type NewClientResult struct { // NewClient creates a new Outline client from a configuration string. func NewClient(transportConfig string) *NewClientResult { - config, err := parseConfigFromJSON(transportConfig) + client, err := newClientWithBaseDialers(transportConfig, net.Dialer{KeepAlive: -1}, net.Dialer{}) + return &NewClientResult{ + Client: client, + Error: platerrors.ToPlatformError(err), + } +} + +func newClientWithBaseDialers(transportConfig string, tcpDialer, udpDialer net.Dialer) (*Client, error) { + conf, err := parseConfigFromJSON(transportConfig) if err != nil { - return &NewClientResult{Error: platerrors.ToPlatformError(err)} + return nil, err } - prefixBytes, err := ParseConfigPrefixFromString(config.Prefix) + prefixBytes, err := ParseConfigPrefixFromString(conf.Prefix) if err != nil { - return &NewClientResult{Error: platerrors.ToPlatformError(err)} + return nil, err } - client, err := newShadowsocksClient(config.Host, int(config.Port), config.Method, config.Password, prefixBytes) - return &NewClientResult{ - Client: client, - Error: platerrors.ToPlatformError(err), - } + return newShadowsocksClient(conf.Host, int(conf.Port), conf.Method, conf.Password, prefixBytes, tcpDialer, udpDialer) } -func newShadowsocksClient(host string, port int, cipherName, password string, prefix []byte) (*Client, error) { +func newShadowsocksClient( + host string, port int, cipherName, password string, prefix []byte, tcpDialer, udpDialer net.Dialer, +) (*Client, error) { if err := validateConfig(host, port, cipherName, password); err != nil { return nil, err } @@ -74,7 +80,7 @@ func newShadowsocksClient(host string, port int, cipherName, password string, pr // We disable Keep-Alive as per https://datatracker.ietf.org/doc/html/rfc1122#page-101, which states that it should only be // enabled in server applications. This prevents the device from unnecessarily waking up to send keep alives. - streamDialer, err := shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: proxyAddress, Dialer: net.Dialer{KeepAlive: -1}}, cryptoKey) + streamDialer, err := shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: proxyAddress, Dialer: tcpDialer}, cryptoKey) if err != nil { return nil, platerrors.PlatformError{ Code: platerrors.SetupTrafficHandlerFailed, @@ -88,7 +94,7 @@ func newShadowsocksClient(host string, port int, cipherName, password string, pr streamDialer.SaltGenerator = shadowsocks.NewPrefixSaltGenerator(prefix) } - packetListener, err := shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: proxyAddress}, cryptoKey) + packetListener, err := shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: proxyAddress, Dialer: udpDialer}, cryptoKey) if err != nil { return nil, platerrors.PlatformError{ Code: platerrors.SetupTrafficHandlerFailed, diff --git a/client/go/outline/connectivity.go b/client/go/outline/connectivity.go index 088ff4bf14..7241acb6c9 100644 --- a/client/go/outline/connectivity.go +++ b/client/go/outline/connectivity.go @@ -15,18 +15,10 @@ package outline import ( - "net" - "github.com/Jigsaw-Code/outline-apps/client/go/outline/connectivity" "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" ) -const ( - tcpTestWebsite = "http://example.com" - dnsServerIP = "1.1.1.1" - dnsServerPort = 53 -) - // TCPAndUDPConnectivityResult represents the result of TCP and UDP connectivity checks. // // We use a struct instead of a tuple to preserve a strongly typed error that gobind recognizes. @@ -40,16 +32,7 @@ type TCPAndUDPConnectivityResult struct { // containing a TCP error and a UDP error. // If the connectivity check was successful, the corresponding error field will be nil. func CheckTCPAndUDPConnectivity(client *Client) *TCPAndUDPConnectivityResult { - // Start asynchronous UDP support check. - udpErrChan := make(chan error) - go func() { - resolverAddr := &net.UDPAddr{IP: net.ParseIP(dnsServerIP), Port: dnsServerPort} - udpErrChan <- connectivity.CheckUDPConnectivityWithDNS(client, resolverAddr) - }() - - tcpErr := connectivity.CheckTCPConnectivityWithHTTP(client, tcpTestWebsite) - udpErr := <-udpErrChan - + tcpErr, udpErr := connectivity.CheckTCPAndUDPConnectivity(client, client) return &TCPAndUDPConnectivityResult{ TCPError: platerrors.ToPlatformError(tcpErr), UDPError: platerrors.ToPlatformError(udpErr), diff --git a/client/go/outline/connectivity/connectivity.go b/client/go/outline/connectivity/connectivity.go index 2bb3bbb392..51078e04a7 100644 --- a/client/go/outline/connectivity/connectivity.go +++ b/client/go/outline/connectivity/connectivity.go @@ -32,6 +32,31 @@ const ( bufferLength = 512 ) +const ( + testTCPWebsite = "http://example.com" + testDNSServerIP = "1.1.1.1" + testDNSServerPort = 53 +) + +// CheckTCPAndUDPConnectivity checks whether the given `tcp` and `udp` clients can relay traffic. +// +// It parallelizes the execution of TCP and UDP checks, and returns a TCP error and a UDP error. +// A nil error indicates successful connectivity for the corresponding protocol. +func CheckTCPAndUDPConnectivity( + tcp transport.StreamDialer, udp transport.PacketListener, +) (tcpErr error, udpErr error) { + // Start asynchronous UDP support check. + udpErrChan := make(chan error) + go func() { + resolverAddr := &net.UDPAddr{IP: net.ParseIP(testDNSServerIP), Port: testDNSServerPort} + udpErrChan <- CheckUDPConnectivityWithDNS(udp, resolverAddr) + }() + + tcpErr = CheckTCPConnectivityWithHTTP(tcp, testTCPWebsite) + udpErr = <-udpErrChan + return +} + // CheckUDPConnectivityWithDNS determines whether the Outline proxy represented by `client` and // the network support UDP traffic by issuing a DNS query though a resolver at `resolverAddr`. // Returns nil on success or an error on failure. diff --git a/client/go/outline/dialer_linux.go b/client/go/outline/dialer_linux.go new file mode 100644 index 0000000000..dd2ce58edf --- /dev/null +++ b/client/go/outline/dialer_linux.go @@ -0,0 +1,45 @@ +// 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 ( + "net" + "syscall" +) + +// newFWMarkProtectedTCPDialer creates a base TCP dialer for [Client] +// protected by the specified firewall mark. +func newFWMarkProtectedTCPDialer(fwmark uint32) net.Dialer { + return net.Dialer{ + KeepAlive: -1, + Control: func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(fwmark)) + }) + }, + } +} + +// newFWMarkProtectedUDPDialer creates a new UDP dialer for [Client] +// protected by the specified firewall mark. +func newFWMarkProtectedUDPDialer(fwmark uint32) net.Dialer { + return net.Dialer{ + Control: func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(fwmark)) + }) + }, + } +} diff --git a/client/go/outline/method_channel.go b/client/go/outline/method_channel.go index 19cb95f958..65500ba498 100644 --- a/client/go/outline/method_channel.go +++ b/client/go/outline/method_channel.go @@ -26,6 +26,19 @@ const ( // - Input: the URL string of the resource to fetch // - Output: the content in raw string of the fetched resource MethodFetchResource = "FetchResource" + + // EstablishVPN initiates a VPN connection and directs all network traffic through Outline. + // + // - Input: a JSON string of vpn.configJSON. + // - Output: a JSON string of vpn.connectionJSON. + MethodEstablishVPN = "EstablishVPN" + + // CloseVPN closes an existing VPN connection and restores network traffic to the default + // network interface. + // + // - Input: null + // - Output: null + MethodCloseVPN = "CloseVPN" ) // InvokeMethodResult represents the result of an InvokeMethod call. @@ -47,6 +60,18 @@ func InvokeMethod(method string, input string) *InvokeMethodResult { Error: platerrors.ToPlatformError(err), } + case MethodEstablishVPN: + err := establishVPN(input) + return &InvokeMethodResult{ + Error: platerrors.ToPlatformError(err), + } + + case MethodCloseVPN: + err := closeVPN() + return &InvokeMethodResult{ + Error: platerrors.ToPlatformError(err), + } + default: return &InvokeMethodResult{Error: &platerrors.PlatformError{ Code: platerrors.InternalError, diff --git a/client/go/outline/platerrors/error_code.go b/client/go/outline/platerrors/error_code.go index a51f8bf722..72ba6f80c5 100644 --- a/client/go/outline/platerrors/error_code.go +++ b/client/go/outline/platerrors/error_code.go @@ -31,6 +31,9 @@ type ErrorCode = string const ( // InternalError represents a general internal service error. InternalError ErrorCode = "ERR_INTERNAL_ERROR" + + // OperationCanceled means that user canceled the long running operation. + OperationCanceled ErrorCode = "ERR_OPERATION_CANCELED_BY_USER" ) ////////// @@ -56,6 +59,10 @@ const ( // SetupSystemVPNFailed means we failed to configure the system VPN to route to us. SetupSystemVPNFailed ErrorCode = "ERR_SYSTEM_VPN_SETUP_FAILURE" + // DisconnectSystemVPNFailed means we failed to remove the system VPN. + // This typically indicates that a reboot is needed. + DisconnectSystemVPNFailed ErrorCode = "ERR_SYSTEM_VPN_DISCONNECT_FAILURE" + // DataTransmissionFailed means we failed to copy data from one device to another. DataTransmissionFailed ErrorCode = "ERR_DATA_TRANSMISSION_FAILURE" ) diff --git a/client/go/outline/vpn/device.go b/client/go/outline/vpn/device.go new file mode 100644 index 0000000000..6bb79c64de --- /dev/null +++ b/client/go/outline/vpn/device.go @@ -0,0 +1,132 @@ +// 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 ( + "context" + "errors" + "io" + "log/slog" + + "github.com/Jigsaw-Code/outline-apps/client/go/outline/connectivity" + perrs "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" + "github.com/Jigsaw-Code/outline-sdk/network" + "github.com/Jigsaw-Code/outline-sdk/network/dnstruncate" + "github.com/Jigsaw-Code/outline-sdk/network/lwip2transport" + "github.com/Jigsaw-Code/outline-sdk/transport" +) + +// RemoteDevice is an IO device that connects to a remote Outline server. +type RemoteDevice struct { + io.ReadWriteCloser + + sd transport.StreamDialer + pl transport.PacketListener + + pkt network.DelegatePacketProxy + remote, fallback network.PacketProxy +} + +func ConnectRemoteDevice( + ctx context.Context, sd transport.StreamDialer, pl transport.PacketListener, +) (_ *RemoteDevice, err error) { + if sd == nil { + return nil, errors.New("StreamDialer must be provided") + } + if pl == nil { + return nil, errors.New("PacketListener must be provided") + } + if ctx.Err() != nil { + return nil, errCancelled(ctx.Err()) + } + + dev := &RemoteDevice{sd: sd, pl: pl} + + dev.remote, err = network.NewPacketProxyFromPacketListener(pl) + if err != nil { + return nil, errSetupHandler("failed to create remote UDP handler", err) + } + slog.Debug("remote device remote UDP handler created") + + if dev.fallback, err = dnstruncate.NewPacketProxy(); err != nil { + return nil, errSetupHandler("failed to create UDP handler for DNS-fallback", err) + } + slog.Debug("remote device local DNS-fallback UDP handler created") + + if err = dev.RefreshConnectivity(ctx); err != nil { + return + } + + dev.ReadWriteCloser, err = lwip2transport.ConfigureDevice(sd, dev.pkt) + if err != nil { + return nil, errSetupHandler("remote device failed to configure network stack", err) + } + slog.Debug("remote device lwIP network stack configured") + + return dev, nil +} + +// Close closes the connection to the Outline server. +func (dev *RemoteDevice) Close() (err error) { + if dev.ReadWriteCloser != nil { + err = dev.ReadWriteCloser.Close() + } + return +} + +// RefreshConnectivity refreshes the connectivity to the Outline server. +func (d *RemoteDevice) RefreshConnectivity(ctx context.Context) (err error) { + if ctx.Err() != nil { + return errCancelled(ctx.Err()) + } + + slog.Debug("remote device is testing connectivity of server...") + tcpErr, udpErr := connectivity.CheckTCPAndUDPConnectivity(d.sd, d.pl) + if tcpErr != nil { + slog.Warn("remote device server connectivity test failed", "err", tcpErr) + return tcpErr + } + + var proxy network.PacketProxy + if udpErr != nil { + slog.Warn("remote device server cannot handle UDP traffic", "err", udpErr) + proxy = d.fallback + } else { + slog.Debug("remote device server can handle UDP traffic") + proxy = d.remote + } + + if d.pkt == nil { + if d.pkt, err = network.NewDelegatePacketProxy(proxy); err != nil { + return errSetupHandler("failed to create combined datagram handler", err) + } + } else { + if err = d.pkt.SetProxy(proxy); err != nil { + return errSetupHandler("failed to update combined datagram handler", err) + } + } + + slog.Info("remote device server connectivity test done", "supportsUDP", proxy == d.remote) + return nil +} + +func errSetupHandler(msg string, cause error) error { + slog.Error(msg, "err", cause) + return perrs.PlatformError{ + Code: perrs.SetupTrafficHandlerFailed, + Message: msg, + Cause: perrs.ToPlatformError(cause), + } +} diff --git a/client/go/outline/vpn/errors.go b/client/go/outline/vpn/errors.go new file mode 100644 index 0000000000..0b2399832b --- /dev/null +++ b/client/go/outline/vpn/errors.go @@ -0,0 +1,59 @@ +// 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 ( + "log/slog" + + perrs "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" +) + +func errCancelled(cause error) error { + slog.Warn("operation was cancelled", "cause", cause) + return perrs.PlatformError{ + Code: perrs.OperationCanceled, + Cause: perrs.ToPlatformError(cause), + } +} + +func errIllegalConfig(msg string, params ...any) error { + return errPlatError(perrs.IllegalConfig, msg, nil, params...) +} + +func errSetupVPN(msg string, cause error, params ...any) error { + return errPlatError(perrs.SetupSystemVPNFailed, msg, cause, params...) +} + +func errCloseVPN(msg string, cause error, params ...any) error { + return errPlatError(perrs.DisconnectSystemVPNFailed, msg, cause, params...) +} + +func errPlatError(code perrs.ErrorCode, msg string, cause error, params ...any) error { + logParams := append(params, "err", cause) + slog.Error(msg, logParams...) + + details := perrs.ErrorDetails{} + for i := 1; i < len(params); i += 2 { + if key, ok := params[i-1].(string); ok { + details[key] = params[i] + } + } + return perrs.PlatformError{ + Code: code, + Message: msg, + Details: details, + Cause: perrs.ToPlatformError(cause), + } +} diff --git a/client/go/outline/vpn/nmconn_linux.go b/client/go/outline/vpn/nmconn_linux.go new file mode 100644 index 0000000000..8a97e55bc9 --- /dev/null +++ b/client/go/outline/vpn/nmconn_linux.go @@ -0,0 +1,178 @@ +// 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 ( + "encoding/binary" + "log/slog" + "net" + "time" + + gonm "github.com/Wifx/gonetworkmanager/v2" + "golang.org/x/sys/unix" +) + +type nmConnectionOptions struct { + Name string + TUNName string + TUNAddr4 net.IP + DNSServers4 []net.IP + FWMark uint32 + RoutingTable uint32 + RoutingPriority uint32 +} + +func establishNMConnection(nm gonm.NetworkManager, opts *nmConnectionOptions) (ac gonm.ActiveConnection, err error) { + if nm == nil { + panic("a NetworkManager must be provided") + } + defer func() { + if err != nil { + closeNMConnection(nm, ac) + ac = nil + } + }() + + dev, err := waitForTUNDeviceToBeAvailable(nm, opts.TUNName) + if err != nil { + return nil, errSetupVPN("failed to find tun device", err, "tun", opts.TUNName, "api", "NetworkManager") + } + slog.Debug("located tun device in NetworkManager", "tun", opts.TUNName, "dev", dev.GetPath()) + + if err = dev.SetPropertyManaged(true); err != nil { + return nil, errSetupVPN("failed to manage tun device", err, "dev", dev.GetPath(), "api", "NetworkManager") + } + slog.Debug("NetworkManager now manages the tun device", "dev", dev.GetPath()) + + props := make(map[string]map[string]interface{}) + configureCommonProps(props, opts) + configureTUNProps(props) + configureIPv4Props(props, opts) + slog.Debug("populated NetworkManager connection settings", "settings", props) + + // The previous SetPropertyManaged call needs some time to take effect (typically within 50ms) + for retries := 20; retries > 0; retries-- { + slog.Debug("trying to create NetworkManager connection for tun device...", "dev", dev.GetPath()) + ac, err = nm.AddAndActivateConnection(props, dev) + if err == nil { + break + } + slog.Debug("failed to create NetworkManager connection, will retry later", "err", err) + time.Sleep(50 * time.Millisecond) + } + if err != nil { + return ac, errSetupVPN("failed to create connection", err, "dev", dev.GetPath(), "api", "NetworkManager") + } + return +} + +func closeNMConnection(nm gonm.NetworkManager, ac gonm.ActiveConnection) error { + if nm == nil { + panic("a NetworkManager must be provided") + } + if ac == nil { + return nil + } + + if err := nm.DeactivateConnection(ac); err != nil { + slog.Warn("failed to deactivate NetworkManager connection", "err", err, "conn", ac.GetPath()) + } + slog.Debug("deactivated NetworkManager connection", "conn", ac.GetPath()) + + conn, err := ac.GetPropertyConnection() + if err == nil { + err = conn.Delete() + } + if err != nil { + return errCloseVPN("failed to delete NetworkManager connection", err, "conn", ac.GetPath()) + } + slog.Info("NetworkManager connection deleted", "conn", ac.GetPath()) + + return nil +} + +// NetworkManager settings reference: +// https://networkmanager.pages.freedesktop.org/NetworkManager/NetworkManager/nm-settings-dbus.html + +func configureCommonProps(props map[string]map[string]interface{}, opts *nmConnectionOptions) { + props["connection"] = map[string]interface{}{ + "id": opts.Name, + "interface-name": opts.TUNName, + } +} + +func configureTUNProps(props map[string]map[string]interface{}) { + props["tun"] = map[string]interface{}{ + // The operating mode of the virtual device. + // Allowed values are 1 (tun) to create a layer 3 device and 2 (tap) to create an Ethernet-like layer 2 one. + "mode": uint32(1), + } +} + +func configureIPv4Props(props map[string]map[string]interface{}, opts *nmConnectionOptions) { + dnsList := make([]uint32, 0, len(opts.DNSServers4)) + for _, dns := range opts.DNSServers4 { + // net.IP is already BigEndian, if we use BigEndian.Uint32, it will be reversed back to LittleEndian. + dnsList = append(dnsList, binary.NativeEndian.Uint32(dns)) + } + + props["ipv4"] = map[string]interface{}{ + "method": "manual", + + // Array of IPv4 addresses. Each address dictionary contains at least 'address' and 'prefix' entries, + // containing the IP address as a string, and the prefix length as a uint32. + "address-data": []map[string]interface{}{{ + "address": opts.TUNAddr4.String(), + "prefix": uint32(32), + }}, + + // Array of IP addresses of DNS servers (as network-byte-order integers) + "dns": dnsList, + + // A lower value has a higher priority. + // Negative values will exclude other configurations with a greater value. + // The default value is 50 for VPN connections (and 100 for regular connections). + "dns-priority": -99, + + // routing domain to exclude all other DNS resolvers + // https://manpages.ubuntu.com/manpages/jammy/man5/resolved.conf.5.html + "dns-search": []string{"~."}, + + // NetworkManager will add these routing entries: + // - default via 10.0.85.5 dev outline-tun0 table 13579 proto static metric 450 + // - 10.0.85.5 dev outline-tun0 table 13579 proto static scope link metric 450 + "route-data": []map[string]interface{}{{ + "dest": "0.0.0.0", + "prefix": uint32(0), + "next-hop": opts.TUNAddr4.String(), + "table": opts.RoutingTable, + }}, + + // Array of dictionaries for routing rules. Each routing rule supports the following options: + // action (y), dport-end (q), dport-start (q), family (i), from (s), from-len (y), fwmark (u), fwmask (u), + // iifname (s), invert (b), ipproto (s), oifname (s), priority (u), sport-end (q), sport-start (q), + // supress-prefixlength (i), table (u), to (s), tos (y), to-len (y), range-end (u), range-start (u). + // + // - not fwmark "0x711E" table "113" priority "456" + "routing-rules": []map[string]interface{}{{ + "family": unix.AF_INET, + "priority": opts.RoutingPriority, + "fwmark": opts.FWMark, + "fwmask": uint32(0xFFFFFFFF), + "invert": true, + "table": opts.RoutingTable, + }}, + } +} diff --git a/client/go/outline/vpn/tun_linux.go b/client/go/outline/vpn/tun_linux.go new file mode 100644 index 0000000000..5cde54d86a --- /dev/null +++ b/client/go/outline/vpn/tun_linux.go @@ -0,0 +1,58 @@ +// 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 ( + "fmt" + "io" + "log/slog" + "time" + + gonm "github.com/Wifx/gonetworkmanager/v2" + "github.com/songgao/water" +) + +// newTUNDevice creates a non-persist layer 3 TUN device with the given name. +func newTUNDevice(name string) (io.ReadWriteCloser, error) { + tun, err := water.New(water.Config{ + DeviceType: water.TUN, + PlatformSpecificParams: water.PlatformSpecificParams{ + Name: name, + Persist: false, + }, + }) + if err != nil { + return nil, err + } + if tun.Name() != name { + return nil, fmt.Errorf("tun device name mismatch: requested `%s`, created `%s`", name, tun.Name()) + } + return tun, nil +} + +// waitForTUNDeviceToBeAvailable waits for the TUN device with the given name to be available +// in the specific NetworkManager. +func waitForTUNDeviceToBeAvailable(nm gonm.NetworkManager, name string) (dev gonm.Device, err error) { + for retries := 20; retries > 0; retries-- { + slog.Debug("trying to find tun device in NetworkManager...", "tun", name) + dev, err = nm.GetDeviceByIpIface(name) + if dev != nil && err == nil { + return + } + slog.Debug("waiting for tun device to be available in NetworkManager", "err", err) + time.Sleep(50 * time.Millisecond) + } + return nil, errSetupVPN("failed to find tun device in NetworkManager", err, "tun", name) +} diff --git a/client/go/outline/vpn/vpn.go b/client/go/outline/vpn/vpn.go new file mode 100644 index 0000000000..acf35e99ca --- /dev/null +++ b/client/go/outline/vpn/vpn.go @@ -0,0 +1,188 @@ +// 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 ( + "context" + "io" + "log/slog" + "sync" + + "github.com/Jigsaw-Code/outline-sdk/transport" +) + +// Config holds the configuration to establish a system-wide [VPNConnection]. +type Config struct { + ID string `json:"id"` + InterfaceName string `json:"interfaceName"` + IPAddress string `json:"ipAddress"` + DNSServers []string `json:"dnsServers"` + ConnectionName string `json:"connectionName"` + RoutingTableId uint32 `json:"routingTableId"` + RoutingPriority uint32 `json:"routingPriority"` + ProtectionMark uint32 `json:"protectionMark"` +} + +// platformVPNConn is an interface representing an OS-specific VPN connection. +type platformVPNConn interface { + // Establish creates a TUN device and routes all system traffic to it. + Establish(ctx context.Context) error + + // TUN returns a L3 IP tun device associated with the VPN connection. + TUN() io.ReadWriteCloser + + // Close terminates the VPN connection and closes the TUN device. + Close() error +} + +// VPNConnection represents a system-wide VPN connection. +type VPNConnection struct { + ID string + + cancelEst context.CancelFunc + wgEst, wgCopy sync.WaitGroup + + proxy *RemoteDevice + platform platformVPNConn +} + +// The global singleton VPN connection. +// This package allows at most one active VPN connection at the same time. +var mu sync.Mutex +var conn *VPNConnection + +// EstablishVPN establishes a new active [VPNConnection] connecting to a [ProxyDevice] +// with the given VPN [Config]. +// It first closes any active [VPNConnection] using [CloseVPN], and then marks the +// newly created [VPNConnection] as the currently active connection. +// It returns the new [VPNConnection], or an error if the connection fails. +func EstablishVPN( + ctx context.Context, conf *Config, sd transport.StreamDialer, pl transport.PacketListener, +) (_ *VPNConnection, err error) { + if conf == nil { + panic("a VPN config must be provided") + } + if sd == nil { + panic("a StreamDialer must be provided") + } + if pl == nil { + panic("a PacketListener must be provided") + } + + c := &VPNConnection{ID: conf.ID} + ctx, c.cancelEst = context.WithCancel(ctx) + + if c.platform, err = newPlatformVPNConn(conf); err != nil { + return + } + + c.wgEst.Add(1) + defer c.wgEst.Done() + + if err = atomicReplaceVPNConn(c); err != nil { + c.platform.Close() + return + } + + slog.Debug("establishing vpn connection ...", "id", c.ID) + + if c.proxy, err = ConnectRemoteDevice(ctx, sd, pl); err != nil { + slog.Error("failed to connect to the remote device", "err", err) + return + } + slog.Info("connected to the remote device") + + if err = c.platform.Establish(ctx); err != nil { + // No need to call c.platform.Close() cuz it's already tracked in the global conn + return + } + + c.wgCopy.Add(2) + go func() { + defer c.wgCopy.Done() + slog.Debug("copying traffic from tun device -> remote device...") + n, err := io.Copy(c.proxy, c.platform.TUN()) + slog.Debug("tun device -> remote device traffic done", "n", n, "err", err) + }() + go func() { + defer c.wgCopy.Done() + slog.Debug("copying traffic from remote device -> tun device...") + n, err := io.Copy(c.platform.TUN(), c.proxy) + slog.Debug("remote device -> tun device traffic done", "n", n, "err", err) + }() + + slog.Info("vpn connection established", "id", c.ID) + return c, nil +} + +// CloseVPN terminates the currently active [VPNConnection] and disconnects the proxy. +func CloseVPN() error { + mu.Lock() + defer mu.Unlock() + return closeVPNNoLock() +} + +// atomicReplaceVPNConn atomically replaces the global conn with newConn. +func atomicReplaceVPNConn(newConn *VPNConnection) error { + mu.Lock() + defer mu.Unlock() + slog.Debug("replacing the global vpn connection...", "id", newConn.ID) + if err := closeVPNNoLock(); err != nil { + return err + } + conn = newConn + slog.Info("global vpn connection replaced", "id", newConn.ID) + return nil +} + +// closeVPNNoLock closes the current VPN connection stored in conn without acquiring +// the mutex. It is assumed that the caller holds the mutex. +func closeVPNNoLock() (err error) { + if conn == nil { + return nil + } + + defer func() { + if err == nil { + slog.Info("vpn connection terminated", "id", conn.ID) + conn = nil + } + }() + + slog.Debug("terminating the global vpn connection...", "id", conn.ID) + + // Cancel the Establish process and wait + conn.cancelEst() + conn.wgEst.Wait() + + // This is the only error that matters + if conn.platform != nil { + err = conn.platform.Close() + } + + // We can ignore the following error + if conn.proxy != nil { + if err2 := conn.proxy.Close(); err2 != nil { + slog.Warn("failed to disconnect from the remote device") + } else { + slog.Info("disconnected from the remote device") + } + } + + // Wait for traffic copy go routines to finish + conn.wgCopy.Wait() + + return +} diff --git a/client/go/outline/vpn/vpn_linux.go b/client/go/outline/vpn/vpn_linux.go new file mode 100644 index 0000000000..1310ee4dc0 --- /dev/null +++ b/client/go/outline/vpn/vpn_linux.go @@ -0,0 +1,115 @@ +// 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 ( + "context" + "io" + "log/slog" + "net" + + perrs "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" + gonm "github.com/Wifx/gonetworkmanager/v2" +) + +// linuxVPNConn implements a platformVPNConn on the Linux platform. +type linuxVPNConn struct { + tun io.ReadWriteCloser + nmOpts *nmConnectionOptions + nm gonm.NetworkManager + ac gonm.ActiveConnection +} + +var _ platformVPNConn = (*linuxVPNConn)(nil) + +// newPlatformVPNConn creates a new Linux-specific platformVPNConn. +// You need to call Establish() in order to make it connected. +func newPlatformVPNConn(conf *Config) (_ platformVPNConn, err error) { + c := &linuxVPNConn{ + nmOpts: &nmConnectionOptions{ + Name: conf.ConnectionName, + TUNName: conf.InterfaceName, + TUNAddr4: net.ParseIP(conf.IPAddress).To4(), + DNSServers4: make([]net.IP, 0, 2), + FWMark: conf.ProtectionMark, + RoutingTable: conf.RoutingTableId, + RoutingPriority: conf.RoutingPriority, + }, + } + + if c.nmOpts.Name == "" { + return nil, errIllegalConfig("must provide a valid connection name") + } + if c.nmOpts.TUNName == "" { + return nil, errIllegalConfig("must provide a valid TUN interface name") + } + if c.nmOpts.TUNAddr4 == nil { + return nil, errIllegalConfig("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) + } + c.nmOpts.DNSServers4 = append(c.nmOpts.DNSServers4, dnsIP) + } + + if c.nm, err = gonm.NewNetworkManager(); err != nil { + return nil, errSetupVPN("failed to connect NetworkManager DBus", err) + } + slog.Debug("NetworkManager DBus connected") + + return c, nil +} + +// TUN returns the Linux L3 TUN device. +func (c *linuxVPNConn) TUN() io.ReadWriteCloser { return c.tun } + +// Establish tries to create the TUN device and route all traffic to it. +func (c *linuxVPNConn) Establish(ctx context.Context) (err error) { + if ctx.Err() != nil { + return perrs.PlatformError{Code: perrs.OperationCanceled} + } + + if c.tun, err = newTUNDevice(c.nmOpts.TUNName); err != nil { + return errSetupVPN("failed to create tun device", err, "name", c.nmOpts.Name) + } + slog.Info("tun device created", "name", c.nmOpts.TUNName) + + if c.ac, err = establishNMConnection(c.nm, c.nmOpts); err != nil { + return + } + slog.Info("successfully configured NetworkManager connection", "conn", c.ac.GetPath()) + return nil +} + +// Close tries to restore the routing and deletes the TUN device. +func (c *linuxVPNConn) Close() (err error) { + if c == nil { + return nil + } + + closeNMConnection(c.nm, c.ac) + if c.tun != nil { + // this is the only error that matters + if err = c.tun.Close(); err != nil { + err = errCloseVPN("failed to delete tun device", err, "name", c.nmOpts.TUNName) + } else { + slog.Info("tun device deleted", "name", c.nmOpts.TUNName) + } + } + + return +} diff --git a/client/go/outline/vpn/vpn_others.go b/client/go/outline/vpn/vpn_others.go new file mode 100644 index 0000000000..64e9d48c03 --- /dev/null +++ b/client/go/outline/vpn/vpn_others.go @@ -0,0 +1,21 @@ +// 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. + +//go:build !linux + +package vpn + +func newPlatformVPNConn(conf *Config) (_ platformVPNConn, err error) { + panic("VPN connection not supported on non-Linux OS") +} diff --git a/client/go/outline/vpn_linux.go b/client/go/outline/vpn_linux.go new file mode 100644 index 0000000000..fb2f29b1f3 --- /dev/null +++ b/client/go/outline/vpn_linux.go @@ -0,0 +1,59 @@ +// 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 ( + "context" + "encoding/json" + + 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"` +} + +// establishVPN establishes a VPN connection using the given configuration string. +// The configuration string should be a JSON object containing the VPN configuration +// and the transport configuration. +// +// The function returns a non-nil error if the connection fails. +func establishVPN(configStr 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), + } + } + + tcp := newFWMarkProtectedTCPDialer(conf.VPNConfig.ProtectionMark) + udp := newFWMarkProtectedUDPDialer(conf.VPNConfig.ProtectionMark) + c, err := newClientWithBaseDialers(conf.TransportConfig, tcp, udp) + if err != nil { + return err + } + + _, err = vpn.EstablishVPN(context.Background(), &conf.VPNConfig, c, c) + return err +} + +// closeVPN closes the currently active VPN connection. +func closeVPN() error { + return vpn.CloseVPN() +} diff --git a/client/go/outline/vpn_others.go b/client/go/outline/vpn_others.go new file mode 100644 index 0000000000..7e0c17f4a4 --- /dev/null +++ b/client/go/outline/vpn_others.go @@ -0,0 +1,22 @@ +// 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. + +//go:build !linux + +package outline + +import "errors" + +func establishVPN(configStr string) error { return errors.ErrUnsupported } +func closeVPN() error { return errors.ErrUnsupported } diff --git a/go.mod b/go.mod index cbe671c5ca..44ffb1843f 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,12 @@ go 1.21 require ( github.com/Jigsaw-Code/outline-sdk v0.0.14-0.20240216220040-f741c57bf854 + github.com/Wifx/gonetworkmanager/v2 v2.1.0 github.com/eycorsican/go-tun2socks v1.16.11 github.com/go-task/task/v3 v3.36.0 github.com/google/addlicense v1.1.1 github.com/google/go-licenses v1.6.0 + github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 github.com/stretchr/testify v1.9.0 golang.org/x/mobile v0.0.0-20240716161057-1ad2df20a8b6 golang.org/x/sys v0.22.0 @@ -21,6 +23,7 @@ require ( github.com/fatih/color v1.16.0 // indirect github.com/go-logr/logr v1.2.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect @@ -39,7 +42,6 @@ require ( github.com/sajari/fuzzy v1.0.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect - github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect github.com/spf13/cobra v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/src-d/gcfg v1.4.0 // indirect diff --git a/go.sum b/go.sum index 4d28c77ddb..14f92288de 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/Jigsaw-Code/outline-sdk v0.0.14-0.20240216220040-f741c57bf854/go.mod github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Wifx/gonetworkmanager/v2 v2.1.0 h1:2PNs7P6wgOyc57YK7AKMwNxGCLvWU6zFBXoEILV4at8= +github.com/Wifx/gonetworkmanager/v2 v2.1.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= @@ -128,6 +130,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/task/v3 v3.36.0 h1:XVJ5hQ5hdzTAulHpAGzbUMUuYr9MUOEQFOFazI3hUsY= github.com/go-task/task/v3 v3.36.0/go.mod h1:XBCIAzuyG/mgZVHMUm3cCznz4+IpsBQRlW1gw7OA5sA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -185,6 +189,8 @@ github.com/google/go-licenses v1.6.0 h1:MM+VCXf0slYkpWO0mECvdYDVCxZXIQNal5wqUIXE github.com/google/go-licenses v1.6.0/go.mod h1:Z8jgz2isEhdenOqd/00pq7I4y4k1xVVQJv415otjclo= github.com/google/go-replayers/httpreplay v1.1.1 h1:H91sIMlt1NZzN7R+/ASswyouLJfW0WLW7fhyUFvDEkY= github.com/google/go-replayers/httpreplay v1.1.1/go.mod h1:gN9GeLIs7l6NUoVaSSnv2RiqK1NiwAmD0MrKeC9IIks= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148 h1:TJsAqW6zLRMDTyGmc9TPosfn9OyVlHs8Hrn3pY6ONSY= github.com/google/licenseclassifier v0.0.0-20210722185704-3043a050f148/go.mod h1:rq9F0RSpNKlrefnf6ZYMHKUnEJBCNzf6AcCXMYBeYvE= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=