Skip to content

feat: add IsCoderConnectRunning to workspacesdk #17361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion codersdk/workspacesdk/workspacesdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,19 @@ func init() {
}
}

type resolver interface {
LookupIP(ctx context.Context, network, host string) ([]net.IP, error)
}

type Client struct {
client *codersdk.Client

// overridden in tests
resolver resolver
}

func New(c *codersdk.Client) *Client {
return &Client{client: c}
return &Client{client: c, resolver: net.DefaultResolver}
}

// AgentConnectionInfo returns required information for establishing
Expand Down Expand Up @@ -384,3 +391,46 @@ func (c *Client) AgentReconnectingPTY(ctx context.Context, opts WorkspaceAgentRe
}
return websocket.NetConn(context.Background(), conn, websocket.MessageBinary), nil
}

type CoderConnectQueryOptions struct {
HostnameSuffix string
}

// IsCoderConnectRunning checks if Coder Connect (OS level tunnel to workspaces) is running on the system. If you
// already know the hostname suffix your deployment uses, you can pass it in the CoderConnectQueryOptions to avoid an
// API call to AgentConnectionInfoGeneric.
func (c *Client) IsCoderConnectRunning(ctx context.Context, o CoderConnectQueryOptions) (bool, error) {
suffix := o.HostnameSuffix
if suffix == "" {
info, err := c.AgentConnectionInfoGeneric(ctx)
if err != nil {
return false, xerrors.Errorf("get agent connection info: %w", err)
}
suffix = info.HostnameSuffix
}
domainName := fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, suffix)
var dnsError *net.DNSError
ips, err := c.resolver.LookupIP(ctx, "ip6", domainName)
if xerrors.As(err, &dnsError) {
if dnsError.IsNotFound {
return false, nil
}
}
if err != nil {
return false, xerrors.Errorf("lookup DNS %s: %w", domainName, err)
}

// The returned IP addresses are probably from the Coder Connect DNS server, but there are sometimes weird captive
// internet setups where the DNS server is configured to return an address for any IP query. So, to avoid false
// positives, check if we can find an address from our service prefix.
for _, ip := range ips {
addr, ok := netip.AddrFromSlice(ip)
if !ok {
continue
}
if tailnet.CoderServicePrefix.AsNetip().Contains(addr) {
return true, nil
}
}
return false, nil
}
86 changes: 86 additions & 0 deletions codersdk/workspacesdk/workspacesdk_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package workspacesdk

import (
"context"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"

"tailscale.com/net/tsaddr"

"github.com/coder/coder/v2/tailnet"
)

func TestClient_IsCoderConnectRunning(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)

srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v2/workspaceagents/connection", r.URL.Path)
httpapi.Write(ctx, rw, http.StatusOK, AgentConnectionInfo{
HostnameSuffix: "test",
})
}))
defer srv.Close()

apiURL, err := url.Parse(srv.URL)
require.NoError(t, err)
sdkClient := codersdk.New(apiURL)
client := New(sdkClient)

// Right name, right IP
expectedName := fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "test")
client.resolver = &fakeResolver{t: t, hostMap: map[string][]net.IP{
expectedName: {net.ParseIP(tsaddr.CoderServiceIPv6().String())},
}}

result, err := client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{})
require.NoError(t, err)
require.True(t, result)

// Wrong name
result, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{HostnameSuffix: "coder"})
require.NoError(t, err)
require.False(t, result)

// Not found
client.resolver = &fakeResolver{t: t, err: &net.DNSError{IsNotFound: true}}
result, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{})
require.NoError(t, err)
require.False(t, result)

// Some other error
client.resolver = &fakeResolver{t: t, err: xerrors.New("a bad thing happened")}
_, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{})
require.Error(t, err)

// Right name, wrong IP
client.resolver = &fakeResolver{t: t, hostMap: map[string][]net.IP{
expectedName: {net.ParseIP("2001::34")},
}}
result, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{})
require.NoError(t, err)
require.False(t, result)
}

type fakeResolver struct {
t testing.TB
hostMap map[string][]net.IP
err error
}

func (f *fakeResolver) LookupIP(_ context.Context, network, host string) ([]net.IP, error) {
assert.Equal(f.t, "ip6", network)
return f.hostMap[host], f.err
}
Loading