Skip to content

Commit

Permalink
DEV-1491: Remote access v2 (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrbulinski authored Feb 1, 2024
1 parent d170f9e commit 7994f50
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 31 deletions.
2 changes: 1 addition & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (cli *Client) GetHTTPClient() *http.Client {

// WithBaseURL sets the base URL of the API endpoint.
func (cli *Client) WithBaseURL(baseURL string) *Client {
cli.baseURL = baseURL
cli.baseURL = strings.TrimSuffix(baseURL, "/")
return cli
}

Expand Down
128 changes: 126 additions & 2 deletions connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@ package client

import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"

"go.qbee.io/transport"

chisel "github.com/jpillora/chisel/client"
"golang.org/x/net/http/httpproxy"
)
Expand Down Expand Up @@ -220,10 +227,106 @@ func (cli *Client) ParseConnect(ctx context.Context, deviceID string, targets []
return cli.Connect(ctx, deviceID, parsedTargets)
}

// Connect establishes a connection to a remote device.
func (cli *Client) Connect(ctx context.Context, deviceID string, targets []RemoteAccessTarget) error {
// connectStdio connects to the given target using stdin/stdout.
func (cli *Client) connectStdio(ctx context.Context, client *transport.Client, target RemoteAccessTarget) error {
remoteHostPort := fmt.Sprintf("%s:%s", target.RemoteHost, target.RemotePort)

stream, err := client.OpenStream(ctx, transport.MessageTypeTCPTunnel, []byte(remoteHostPort))
if err != nil {
return fmt.Errorf("error opening stream: %w", err)
}
defer stream.Close()

// copy from stdin to stream
go func() {
_, _ = io.Copy(stream, os.Stdin)
}()

// copy from stream to stdout
_, err = io.Copy(os.Stdout, stream)

return err
}

// connect establishes a connection to a remote device.
func (cli *Client) connect(ctx context.Context, deviceUUID, edgeHost string, targets []RemoteAccessTarget) error {
edgeURL := fmt.Sprintf("https://%s/device/%s", edgeHost, deviceUUID)

var tlsConfig *tls.Config

// for testing purposes, allow connections to localhost without verifying the certificate
if strings.HasPrefix(edgeHost, "edge:") || strings.HasPrefix(edgeHost, "localhost:") {
tlsConfig = &tls.Config{
InsecureSkipVerify: true,
}
}

if len(targets) == 0 {
return fmt.Errorf("no targets defined")
}

client, err := transport.NewClient(ctx, edgeURL, cli.authToken, tlsConfig)
if err != nil {
return fmt.Errorf("error initializing remote access client: %w", err)
}

// close the client and all local listeners when the context is cancelled
closers := []io.Closer{client}
defer func() {
for _, closer := range closers {
_ = closer.Close()
}
}()

if len(targets) == 1 && targets[0].LocalPort == "stdio" {
return cli.connectStdio(ctx, client, targets[0])
}

for _, target := range targets {
if target.LocalPort == "stdio" {
return fmt.Errorf("stdio is only supported for single target connections")
}

localHostPort := fmt.Sprintf("localhost:%s", target.LocalPort)
remoteHostPort := fmt.Sprintf("%s:%s", target.RemoteHost, target.RemotePort)

switch target.Protocol {
case "tcp":
var tcpListener *net.TCPListener
if tcpListener, err = client.OpenTCPTunnel(ctx, localHostPort, remoteHostPort); err != nil {
return fmt.Errorf("error opening TCP tunnel: %w", err)
}

closers = append(closers, tcpListener)
case "udp":
var udpConn *transport.UDPTunnel
if udpConn, err = client.OpenUDPTunnel(ctx, localHostPort, remoteHostPort); err != nil {
return fmt.Errorf("error opening UDP tunnel: %w", err)
}

closers = append(closers, udpConn)
default:
return fmt.Errorf("invalid protocol %s", target.Protocol)
}

fmt.Printf("Tunneling %s %s to %s\n", target.Protocol, localHostPort, remoteHostPort)
}

// Wait for context to be cancelled
<-ctx.Done()

return nil
}

// legacyConnect establishes a connection to a remote device using the legacy remote access solution.
func (cli *Client) legacyConnect(ctx context.Context, deviceID string, targets []RemoteAccessTarget) error {
ports := make([]string, len(targets))
for _, target := range targets {
// only localhost is supported as remote host for legacy remote access
if target.RemoteHost != "localhost" {
return fmt.Errorf("invalid remote host: only localhost is supported")
}

ports = append(ports, fmt.Sprintf("%s:%s", target.Protocol, target.RemotePort))
}

Expand Down Expand Up @@ -281,3 +384,24 @@ func (cli *Client) Connect(ctx context.Context, deviceID string, targets []Remot
}
return nil
}

// Connect establishes a connection to a remote device.
func (cli *Client) Connect(ctx context.Context, deviceID string, targets []RemoteAccessTarget) error {
deviceStatus, err := cli.GetDeviceStatus(ctx, deviceID)
if err != nil {
return err
}

if !deviceStatus.RemoteAccess {
return fmt.Errorf("remote access is not available for device %s", deviceID)
}

switch deviceStatus.EdgeVersion {
case EdgeVersionOpenVPN:
return cli.legacyConnect(ctx, deviceID, targets)
case EdgeVersionNative:
return cli.connect(ctx, deviceStatus.UUID, deviceStatus.Edge, targets)
default:
return fmt.Errorf("unsupported edge version %d", deviceStatus.EdgeVersion)
}
}
51 changes: 51 additions & 0 deletions device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package client

import (
"context"
"fmt"
"net/http"
)

// EdgeVersion indicates the version of the edge server that the device is connected to.
type EdgeVersion uint8

const (
// EdgeVersionOpenVPN indicates that the device is connected to an OpenVPN edge.
EdgeVersionOpenVPN = 0

// EdgeVersionNative indicates that the device is connected to a native qbee remote access edge.
EdgeVersionNative = 1
)

// DeviceStatus is the status of a device.
type DeviceStatus struct {
// UUID is the UUID of the device.
UUID string `json:"uuid"`

// RemoteAccess is true if the device is connected to the edge.
RemoteAccess bool `json:"remote_access"`

// Edge is the edge host that the device is connected to.
// This field is only set if RemoteAccess is true.
// Format is <edge-host>:<edge-port>/edge/<edge-id>
Edge string `json:"edge,omitempty"`

// EdgeVersion is the version of the edge that the device is connected to.
// This field is only set if RemoteAccess is true.
// 0 - for OpenVPN edge
// 1 - for native qbee remote access
EdgeVersion EdgeVersion `json:"edge_version,omitempty"`
}

// GetDeviceStatus returns device status.
func (cli *Client) GetDeviceStatus(ctx context.Context, deviceID string) (*DeviceStatus, error) {
deviceStatus := new(DeviceStatus)

path := fmt.Sprintf("/api/v2/device/%s/status", deviceID)

if err := cli.Call(ctx, http.MethodGet, path, nil, deviceStatus); err != nil {
return nil, err
}

return deviceStatus, nil
}
12 changes: 7 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ go 1.21

require (
github.com/jpillora/chisel v1.9.1
golang.org/x/net v0.17.0
go.qbee.io/transport v1.24.7
golang.org/x/net v0.20.0
)

require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/jpillora/sizestr v1.0.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.15.0 // indirect
github.com/xtaci/smux v1.5.24 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
)
30 changes: 18 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,29 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jpillora/chisel v1.9.1 h1:nGOF58+45WHlvDcq6AZu7En8nWOBCZHqj9boo5rB4qU=
github.com/jpillora/chisel v1.9.1/go.mod h1:qvgGfFR9ZhiDoYJM4IM1omX1HLbQSkZag8miP9u4SsQ=
github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw=
github.com/jpillora/sizestr v1.0.0/go.mod h1:bUhLv4ctkknatr6gR42qPxirmd5+ds1u7mzD+MZ33f0=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY=
github.com/xtaci/smux v1.5.24/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
go.qbee.io/transport v1.24.6 h1:rMg6Zg2HPuFgmOLzZ9678Fx7OV+46/e+bR0WBENR0tE=
go.qbee.io/transport v1.24.6/go.mod h1:RSuaDe4RBsxEpXd9PiVKuo85bJd/G0bhK2I3j2fheC8=
go.qbee.io/transport v1.24.7 h1:pUp2nrMrvZy3CkYX6wRD0J5+qK4CluUzgyR+6vrvvlE=
go.qbee.io/transport v1.24.7/go.mod h1:RSuaDe4RBsxEpXd9PiVKuo85bJd/G0bhK2I3j2fheC8=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
6 changes: 0 additions & 6 deletions remote_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,6 @@ func ParseRemoteAccessTarget(targetString string) (RemoteAccessTarget, error) {

target.RemoteHost = parts[1]

// Only localhost is supported as remote host at the moment.
// Once we roll out the new remote access solution, will allow any host in the same network.
if target.RemoteHost != "localhost" {
return target, fmt.Errorf("invalid remote host: only localhost is supported")
}

remotePort := parts[2]
if strings.HasSuffix(remotePort, "/udp") {
target.Protocol = UDP
Expand Down
5 changes: 0 additions & 5 deletions remote_access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,6 @@ func TestParseRemoteAccessTarget(t *testing.T) {
targetString: "localhost",
wantErr: "invalid format",
},
{
name: "unsupported host",
targetString: "123:example.com:123",
wantErr: "invalid remote host: only localhost is supported",
},
{
name: "local port out of range",
targetString: "123456:localhost:2",
Expand Down

0 comments on commit 7994f50

Please sign in to comment.