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/device.go b/client/go/outline/device.go deleted file mode 100644 index 8c60d8a7a1..0000000000 --- a/client/go/outline/device.go +++ /dev/null @@ -1,139 +0,0 @@ -// 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" - "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" -) - -// Device is an IPDevice that connects to a remote Outline server. -// It also implements the vpn.ProxyDevice interface. -type Device struct { - network.IPDevice - - c *Client - pkt network.DelegatePacketProxy - supportsUDP bool - - remote, fallback network.PacketProxy -} - -var _ vpn.ProxyDevice = (*Device)(nil) - -// NewDevice creates a new [Device] using the given [Client]. -func NewDevice(c *Client) (*Device, error) { - if c == nil { - return nil, errors.New("Client must be provided") - } - return &Device{c: c}, nil -} - -// SupportsUDP returns true if the the Outline server forwards UDP traffic. -// This value will be refreshed after Connect or RefreshConnectivity. -func (d *Device) SupportsUDP() bool { - return d.supportsUDP -} - -// Connect tries to connect to the Outline server. -func (d *Device) Connect(ctx context.Context) (err error) { - if ctx.Err() != nil { - return perrs.PlatformError{Code: perrs.OperationCanceled} - } - - d.remote, err = network.NewPacketProxyFromPacketListener(d.c.PacketListener) - if err != nil { - return errSetupHandler("failed to create datagram handler", err) - } - slog.Debug("[Outline] remote UDP handler created") - - if d.fallback, err = dnstruncate.NewPacketProxy(); err != nil { - return errSetupHandler("failed to create datagram handler for DNS fallback", err) - } - slog.Debug("[Outline] local DNS-fallback UDP handler created") - - if err = d.RefreshConnectivity(ctx); err != nil { - return - } - - d.IPDevice, err = lwip2transport.ConfigureDevice(d.c.StreamDialer, d.pkt) - if err != nil { - return errSetupHandler("failed to configure Outline network stack", err) - } - slog.Debug("[Outline] lwIP network stack configured") - - return nil -} - -// Close closes the connection to the Outline server. -func (d *Device) Close() (err error) { - if d.IPDevice != nil { - err = d.IPDevice.Close() - } - return -} - -// RefreshConnectivity refreshes the connectivity to the Outline server. -func (d *Device) RefreshConnectivity(ctx context.Context) (err error) { - if ctx.Err() != nil { - return perrs.PlatformError{Code: perrs.OperationCanceled} - } - - slog.Debug("[Outline] Testing connectivity of Outline server ...") - result := CheckTCPAndUDPConnectivity(d.c) - if result.TCPError != nil { - slog.Warn("[Outline] Outline server connectivity test failed", "err", result.TCPError) - return result.TCPError - } - - var proxy network.PacketProxy - 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 - d.supportsUDP = true - } - - 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("[Outline] Outline server connectivity test done", "supportsUDP", d.supportsUDP) - return nil -} - -func errSetupHandler(msg string, cause error) error { - slog.Error("[Outline] "+msg, "err", cause) - return perrs.PlatformError{ - Code: perrs.SetupTrafficHandlerFailed, - Message: msg, - Cause: perrs.ToPlatformError(cause), - } -} diff --git a/client/go/outline/dialer_linux.go b/client/go/outline/dialer_linux.go index 98560d2fff..dd2ce58edf 100644 --- a/client/go/outline/dialer_linux.go +++ b/client/go/outline/dialer_linux.go @@ -21,7 +21,7 @@ import ( // newFWMarkProtectedTCPDialer creates a base TCP dialer for [Client] // protected by the specified firewall mark. -func newFWMarkProtectedTCPDialer(fwmark uint32) (net.Dialer, error) { +func newFWMarkProtectedTCPDialer(fwmark uint32) net.Dialer { return net.Dialer{ KeepAlive: -1, Control: func(network, address string, c syscall.RawConn) error { @@ -29,17 +29,17 @@ func newFWMarkProtectedTCPDialer(fwmark uint32) (net.Dialer, error) { syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(fwmark)) }) }, - }, nil + } } // newFWMarkProtectedUDPDialer creates a new UDP dialer for [Client] // protected by the specified firewall mark. -func newFWMarkProtectedUDPDialer(fwmark uint32) (net.Dialer, error) { +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)) }) }, - }, nil + } } diff --git a/client/go/outline/dialer_others.go b/client/go/outline/dialer_others.go index a4a76cc0f8..2d03fd7fb8 100644 --- a/client/go/outline/dialer_others.go +++ b/client/go/outline/dialer_others.go @@ -16,17 +16,12 @@ package outline -import ( - "errors" - "net" -) +import "net" -// newFWMarkProtectedTCPDialer is not supported on non-Linux platforms. -func newFWMarkProtectedTCPDialer(fwmark uint32) (net.Dialer, error) { - return net.Dialer{}, errors.ErrUnsupported +func newFWMarkProtectedTCPDialer(fwmark uint32) net.Dialer { + panic("SO_MARK socket option is only supported on Linux") } -// newFWMarkProtectedUDPDialer is not supported on non-Linux platforms. -func newFWMarkProtectedUDPDialer(fwmark uint32) (net.Dialer, error) { - return net.Dialer{}, errors.ErrUnsupported +func newFWMarkProtectedUDPDialer(fwmark uint32) net.Dialer { + panic("SO_MARK socket option is only supported on Linux") } diff --git a/client/go/outline/method_channel.go b/client/go/outline/method_channel.go index dfb92b9a98..65500ba498 100644 --- a/client/go/outline/method_channel.go +++ b/client/go/outline/method_channel.go @@ -61,9 +61,8 @@ func InvokeMethod(method string, input string) *InvokeMethodResult { } case MethodEstablishVPN: - conn, err := establishVPN(input) + err := establishVPN(input) return &InvokeMethodResult{ - Value: conn, Error: platerrors.ToPlatformError(err), } diff --git a/client/go/outline/vpn.go b/client/go/outline/vpn.go index 7714d61173..fb2f29b1f3 100644 --- a/client/go/outline/vpn.go +++ b/client/go/outline/vpn.go @@ -15,6 +15,7 @@ package outline import ( + "context" "encoding/json" perrs "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" @@ -30,51 +31,26 @@ type vpnConfigJSON struct { // The configuration string should be a JSON object containing the VPN configuration // and the transport configuration. // -// The function returns a JSON string representing the established VPN connection, -// or an error if the connection fails. -func establishVPN(configStr string) (string, error) { +// 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{ + return perrs.PlatformError{ Code: perrs.IllegalConfig, Message: "invalid VPN config format", Cause: perrs.ToPlatformError(err), } } - // Create Outline Client and Device - tcp, err := newFWMarkProtectedTCPDialer(conf.VPNConfig.ProtectionMark) - if err != nil { - return "", err - } - udp, err := newFWMarkProtectedUDPDialer(conf.VPNConfig.ProtectionMark) - if err != nil { - return "", err - } + tcp := newFWMarkProtectedTCPDialer(conf.VPNConfig.ProtectionMark) + udp := newFWMarkProtectedUDPDialer(conf.VPNConfig.ProtectionMark) c, err := newClientWithBaseDialers(conf.TransportConfig, tcp, udp) if err != nil { - return "", err - } - proxy, err := NewDevice(c) - if err != nil { - return "", err + 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 + _, err = vpn.EstablishVPN(context.Background(), &conf.VPNConfig, c, c) + return err } // closeVPN closes the currently active VPN connection. diff --git a/client/go/outline/vpn/device.go b/client/go/outline/vpn/device.go new file mode 100644 index 0000000000..daef6b9afb --- /dev/null +++ b/client/go/outline/vpn/device.go @@ -0,0 +1,131 @@ +// 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" + "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 IPDevice that connects to a remote Outline server. +type RemoteDevice struct { + network.IPDevice + + 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.IPDevice, 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.IPDevice != nil { + err = dev.IPDevice.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 index 0e4b77ccde..0b2399832b 100644 --- a/client/go/outline/vpn/errors.go +++ b/client/go/outline/vpn/errors.go @@ -20,29 +20,29 @@ import ( perrs "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" ) -const ( - ioLogPfx = "[IO] " - nmLogPfx = "[NMDBus] " - vpnLogPfx = "[VPN] " - proxyLogPfx = "[Proxy] " -) +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(pfx, msg string, cause error, params ...any) error { - return errPlatError(perrs.SetupSystemVPNFailed, pfx+msg, cause, params...) +func errSetupVPN(msg string, cause error, params ...any) error { + return errPlatError(perrs.SetupSystemVPNFailed, msg, cause, params...) } -func errCloseVPN(pfx, msg string, cause error, params ...any) error { - return errPlatError(perrs.DisconnectSystemVPNFailed, pfx+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...) - // time.Sleep(60 * time.Second) details := perrs.ErrorDetails{} for i := 1; i < len(params); i += 2 { diff --git a/client/go/outline/vpn/nmconn_linux.go b/client/go/outline/vpn/nmconn_linux.go index 8c3e153781..8a97e55bc9 100644 --- a/client/go/outline/vpn/nmconn_linux.go +++ b/client/go/outline/vpn/nmconn_linux.go @@ -16,7 +16,6 @@ package vpn import ( "encoding/binary" - "errors" "log/slog" "net" "time" @@ -37,7 +36,7 @@ type nmConnectionOptions struct { func establishNMConnection(nm gonm.NetworkManager, opts *nmConnectionOptions) (ac gonm.ActiveConnection, err error) { if nm == nil { - return nil, errors.New("must provide a NetworkManager") + panic("a NetworkManager must be provided") } defer func() { if err != nil { @@ -48,58 +47,58 @@ func establishNMConnection(nm gonm.NetworkManager, opts *nmConnectionOptions) (a dev, err := waitForTUNDeviceToBeAvailable(nm, opts.TUNName) if err != nil { - return nil, errSetupVPN(nmLogPfx, "failed to find TUN device", err, "tun", opts.TUNName) + return nil, errSetupVPN("failed to find tun device", err, "tun", opts.TUNName, "api", "NetworkManager") } - slog.Debug(nmLogPfx+"located TUN device", "tun", opts.TUNName, "dev", dev.GetPath()) + slog.Debug("located tun device in NetworkManager", "tun", opts.TUNName, "dev", dev.GetPath()) if err = dev.SetPropertyManaged(true); err != nil { - return nil, errSetupVPN(nmLogPfx, "failed to set TUN device to be managed", err, "dev", dev.GetPath()) + return nil, errSetupVPN("failed to manage tun device", err, "dev", dev.GetPath(), "api", "NetworkManager") } - slog.Debug(nmLogPfx+"set TUN device to be managed", "dev", dev.GetPath()) + 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(nmLogPfx+"populated NetworkManager connection settings", "settings", props) + 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(nmLogPfx+"trying to create connection for TUN device ...", "dev", dev.GetPath()) + 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(nmLogPfx+"waiting for TUN device being managed", "err", err) + slog.Debug("failed to create NetworkManager connection, will retry later", "err", err) time.Sleep(50 * time.Millisecond) } if err != nil { - return ac, errSetupVPN(nmLogPfx, "failed to create new connection for device", err, "dev", dev.GetPath()) + 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 { - return errors.New("must provide a NetworkManager") + panic("a NetworkManager must be provided") } if ac == nil { return nil } if err := nm.DeactivateConnection(ac); err != nil { - slog.Warn(nmLogPfx+"not able to deactivate connection", "err", err, "conn", ac.GetPath()) + slog.Warn("failed to deactivate NetworkManager connection", "err", err, "conn", ac.GetPath()) } - slog.Debug(nmLogPfx+"deactivated connection", "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(nmLogPfx, "failed to delete connection", err, "conn", ac.GetPath()) + return errCloseVPN("failed to delete NetworkManager connection", err, "conn", ac.GetPath()) } - slog.Info(nmLogPfx+"connection deleted", "conn", ac.GetPath()) + slog.Info("NetworkManager connection deleted", "conn", ac.GetPath()) return nil } diff --git a/client/go/outline/vpn/tun_linux.go b/client/go/outline/vpn/tun_linux.go index f19f9917f2..5cde54d86a 100644 --- a/client/go/outline/vpn/tun_linux.go +++ b/client/go/outline/vpn/tun_linux.go @@ -37,7 +37,7 @@ func newTUNDevice(name string) (io.ReadWriteCloser, error) { return nil, err } if tun.Name() != name { - return nil, fmt.Errorf("TUN device name mismatch: requested `%s`, created `%s`", name, tun.Name()) + return nil, fmt.Errorf("tun device name mismatch: requested `%s`, created `%s`", name, tun.Name()) } return tun, nil } @@ -46,13 +46,13 @@ func newTUNDevice(name string) (io.ReadWriteCloser, error) { // in the specific NetworkManager. func waitForTUNDeviceToBeAvailable(nm gonm.NetworkManager, name string) (dev gonm.Device, err error) { for retries := 20; retries > 0; retries-- { - slog.Debug(nmLogPfx+"trying to find TUN device ...", "tun", name) + slog.Debug("trying to find tun device in NetworkManager...", "tun", name) dev, err = nm.GetDeviceByIpIface(name) if dev != nil && err == nil { return } - slog.Debug(nmLogPfx+"waiting for TUN device to be available", "err", err) + slog.Debug("waiting for tun device to be available in NetworkManager", "err", err) time.Sleep(50 * time.Millisecond) } - return nil, errSetupVPN(nmLogPfx, "failed to find TUN device", err, "tun", name) + 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 index ed7d9d2707..39a800d474 100644 --- a/client/go/outline/vpn/vpn.go +++ b/client/go/outline/vpn/vpn.go @@ -16,12 +16,11 @@ package vpn import ( "context" - "errors" "io" "log/slog" "sync" - "github.com/Jigsaw-Code/outline-sdk/network" + "github.com/Jigsaw-Code/outline-sdk/transport" ) // Config holds the configuration to establish a system-wide [VPNConnection]. @@ -36,32 +35,6 @@ type Config struct { ProtectionMark uint32 `json:"protectionMark"` } -// Status defines the possible states of a [VPNConnection]. -type Status string - -// Constants representing the different VPN connection statuses. -const ( - StatusUnknown Status = "Unknown" - StatusConnected Status = "Connected" - StatusDisconnected Status = "Disconnected" - StatusConnecting Status = "Connecting" - StatusDisconnecting Status = "Disconnecting" -) - -// ProxyDevice is an interface representing a remote proxy server device. -type ProxyDevice interface { - network.IPDevice - - // Connect establishes a connection to the proxy device. - Connect(ctx context.Context) error - - // SupportsUDP returns true if the proxy device is able to handle UDP traffic. - SupportsUDP() bool - - // RefreshConnectivity refreshes the UDP support of the proxy device. - RefreshConnectivity(ctx context.Context) error -} - // 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. @@ -76,28 +49,15 @@ type platformVPNConn interface { // VPNConnection represents a system-wide VPN connection. type VPNConnection struct { - ID string `json:"id"` - Status Status `json:"status"` - SupportsUDP *bool `json:"supportsUDP"` + ID string - ctx context.Context - cancel context.CancelFunc + cancelEst context.CancelFunc wgEst, wgCopy sync.WaitGroup - proxy ProxyDevice + proxy *RemoteDevice platform platformVPNConn } -// SetStatus sets the status of the VPN connection. -func (c *VPNConnection) SetStatus(s Status) { - c.Status = s -} - -// SetSupportsUDP sets whether the VPN connection supports UDP. -func (c *VPNConnection) SetSupportsUDP(v bool) { - c.SupportsUDP = &v -} - // The global singleton VPN connection. // This package allows at most one active VPN connection at the same time. var mu sync.Mutex @@ -108,20 +68,22 @@ var conn *VPNConnection // 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(conf *Config, proxy ProxyDevice) (_ *VPNConnection, err error) { +func EstablishVPN( + ctx context.Context, conf *Config, sd transport.StreamDialer, pl transport.PacketListener, +) (_ *VPNConnection, err error) { if conf == nil { - return nil, errors.New("a VPN Config must be provided") + panic("a VPN config must be provided") } - if proxy == nil { - return nil, errors.New("a proxy device must be provided") + if sd == nil { + panic("a StreamDialer must be provided") } - - c := &VPNConnection{ - ID: conf.ID, - Status: StatusDisconnected, - proxy: proxy, + if pl == nil { + panic("a PacketListener must be provided") } - c.ctx, c.cancel = context.WithCancel(context.Background()) + + c := &VPNConnection{ID: conf.ID} + ctx, c.cancelEst = context.WithCancel(ctx) + if c.platform, err = newPlatformVPNConn(conf); err != nil { return } @@ -134,25 +96,15 @@ func EstablishVPN(conf *Config, proxy ProxyDevice) (_ *VPNConnection, err error) return } - slog.Debug(vpnLogPfx+"establishing VPN connection ...", "id", c.ID) - - c.SetStatus(StatusConnecting) - defer func() { - if err == nil { - c.SetStatus(StatusConnected) - } else { - c.SetStatus(StatusUnknown) - } - }() + slog.Debug("establishing vpn connection ...", "id", c.ID) - if err = c.proxy.Connect(c.ctx); err != nil { - slog.Error(proxyLogPfx+"failed to connect to the proxy", "err", err) + if c.proxy, err = ConnectRemoteDevice(ctx, sd, pl); err != nil { + slog.Error("failed to connect to the remote device", "err", err) return } - slog.Info(proxyLogPfx + "connected to the proxy") - c.SetSupportsUDP(c.proxy.SupportsUDP()) + slog.Info("connected to the remote device") - if err = c.platform.Establish(c.ctx); err != nil { + 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 } @@ -160,18 +112,18 @@ func EstablishVPN(conf *Config, proxy ProxyDevice) (_ *VPNConnection, err error) c.wgCopy.Add(2) go func() { defer c.wgCopy.Done() - slog.Debug(ioLogPfx + "copying traffic from TUN Device -> OutlineDevice...") + slog.Debug("copying traffic from tun device -> remote device...") n, err := io.Copy(c.proxy, c.platform.TUN()) - slog.Debug(ioLogPfx+"TUN Device -> OutlineDevice done", "n", n, "err", err) + slog.Debug("tun device -> remote device traffic done", "n", n, "err", err) }() go func() { defer c.wgCopy.Done() - slog.Debug(ioLogPfx + "copying traffic from OutlineDevice -> TUN Device...") + slog.Debug("copying traffic from remote device -> tun device...") n, err := io.Copy(c.platform.TUN(), c.proxy) - slog.Debug(ioLogPfx+"OutlineDevice -> TUN Device done", "n", n, "err", err) + slog.Debug("remote device -> tun device traffic done", "n", n, "err", err) }() - slog.Info(vpnLogPfx+"VPN connection established", "id", c.ID) + slog.Info("vpn connection established", "id", c.ID) return c, nil } @@ -186,12 +138,12 @@ func CloseVPN() error { func atomicReplaceVPNConn(newConn *VPNConnection) error { mu.Lock() defer mu.Unlock() - slog.Debug(vpnLogPfx+"creating VPN Connection ...", "id", newConn.ID) + slog.Debug("replacing the global vpn connection...", "id", newConn.ID) if err := closeVPNNoLock(); err != nil { return err } conn = newConn - slog.Info(vpnLogPfx+"VPN Connection created", "id", newConn.ID) + slog.Info("global vpn connection replaced", "id", newConn.ID) return nil } @@ -202,21 +154,17 @@ func closeVPNNoLock() (err error) { return nil } - conn.SetStatus(StatusDisconnecting) defer func() { if err == nil { - slog.Info(vpnLogPfx+"VPN Connection closed", "id", conn.ID) - conn.SetStatus(StatusDisconnected) + slog.Info("vpn connection terminated", "id", conn.ID) conn = nil - } else { - conn.SetStatus(StatusUnknown) } }() - slog.Debug(vpnLogPfx+"closing existing VPN Connection ...", "id", conn.ID) + slog.Debug("terminating the global vpn connection...", "id", conn.ID) // Cancel the Establish process and wait - conn.cancel() + conn.cancelEst() conn.wgEst.Wait() // This is the only error that matters @@ -224,9 +172,9 @@ func closeVPNNoLock() (err error) { // We can ignore the following error if err2 := conn.proxy.Close(); err2 != nil { - slog.Warn(proxyLogPfx + "failed to disconnect from the proxy") + slog.Warn("failed to disconnect from the remote device") } else { - slog.Info(proxyLogPfx + "disconnected from the proxy") + slog.Info("disconnected from the remote device") } // Wait for traffic copy go routines to finish diff --git a/client/go/outline/vpn/vpn_linux.go b/client/go/outline/vpn/vpn_linux.go index 9701d2378c..1310ee4dc0 100644 --- a/client/go/outline/vpn/vpn_linux.go +++ b/client/go/outline/vpn/vpn_linux.go @@ -67,9 +67,9 @@ func newPlatformVPNConn(conf *Config) (_ platformVPNConn, err error) { } if c.nm, err = gonm.NewNetworkManager(); err != nil { - return nil, errSetupVPN(nmLogPfx, "failed to connect", err) + return nil, errSetupVPN("failed to connect NetworkManager DBus", err) } - slog.Debug(nmLogPfx + "connected") + slog.Debug("NetworkManager DBus connected") return c, nil } @@ -84,14 +84,14 @@ func (c *linuxVPNConn) Establish(ctx context.Context) (err error) { } if c.tun, err = newTUNDevice(c.nmOpts.TUNName); err != nil { - return errSetupVPN(ioLogPfx, "failed to create TUN device", err, "name", c.nmOpts.Name) + return errSetupVPN("failed to create tun device", err, "name", c.nmOpts.Name) } - slog.Info(vpnLogPfx+"TUN device created", "name", c.nmOpts.TUNName) + slog.Info("tun device created", "name", c.nmOpts.TUNName) if c.ac, err = establishNMConnection(c.nm, c.nmOpts); err != nil { return } - slog.Info(nmLogPfx+"successfully configured NetworkManager connection", "conn", c.ac.GetPath()) + slog.Info("successfully configured NetworkManager connection", "conn", c.ac.GetPath()) return nil } @@ -105,9 +105,9 @@ func (c *linuxVPNConn) Close() (err error) { if c.tun != nil { // this is the only error that matters if err = c.tun.Close(); err != nil { - err = errCloseVPN(vpnLogPfx, "failed to close TUN device", err, "name", c.nmOpts.TUNName) + err = errCloseVPN("failed to delete tun device", err, "name", c.nmOpts.TUNName) } else { - slog.Info(vpnLogPfx+"closed TUN device", "name", c.nmOpts.TUNName) + slog.Info("tun device deleted", "name", c.nmOpts.TUNName) } } diff --git a/client/go/outline/vpn/vpn_others.go b/client/go/outline/vpn/vpn_others.go index 30086fd2a9..64e9d48c03 100644 --- a/client/go/outline/vpn/vpn_others.go +++ b/client/go/outline/vpn/vpn_others.go @@ -16,8 +16,6 @@ package vpn -import "errors" - func newPlatformVPNConn(conf *Config) (_ platformVPNConn, err error) { - return nil, errors.ErrUnsupported + panic("VPN connection not supported on non-Linux OS") }