Skip to content

Commit ee2891e

Browse files
committed
Merge remote-tracking branch 'origin/main' into 16634-networking-stack
2 parents b79093b + 1cb864b commit ee2891e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+906
-182
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,8 @@ jobs:
12191219
kubectl --namespace coder rollout status deployment/coder
12201220
kubectl --namespace coder rollout restart deployment/coder-provisioner
12211221
kubectl --namespace coder rollout status deployment/coder-provisioner
1222+
kubectl --namespace coder rollout restart deployment/coder-provisioner-tagged
1223+
kubectl --namespace coder rollout status deployment/coder-provisioner-tagged
12221224
12231225
deploy-wsproxies:
12241226
runs-on: ubuntu-latest

agent/agent.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ type Options struct {
8888
BlockFileTransfer bool
8989
Execer agentexec.Execer
9090
ContainerLister agentcontainers.Lister
91+
92+
ExperimentalContainersEnabled bool
9193
}
9294

9395
type Client interface {
@@ -188,6 +190,8 @@ func New(options Options) Agent {
188190
metrics: newAgentMetrics(prometheusRegistry),
189191
execer: options.Execer,
190192
lister: options.ContainerLister,
193+
194+
experimentalDevcontainersEnabled: options.ExperimentalContainersEnabled,
191195
}
192196
// Initially, we have a closed channel, reflecting the fact that we are not initially connected.
193197
// Each time we connect we replace the channel (while holding the closeMutex) with a new one
@@ -258,6 +262,8 @@ type agent struct {
258262
metrics *agentMetrics
259263
execer agentexec.Execer
260264
lister agentcontainers.Lister
265+
266+
experimentalDevcontainersEnabled bool
261267
}
262268

263269
func (a *agent) TailnetConn() *tailnet.Conn {
@@ -297,6 +303,9 @@ func (a *agent) init() {
297303
a.sshServer,
298304
a.metrics.connectionsTotal, a.metrics.reconnectingPTYErrors,
299305
a.reconnectingPTYTimeout,
306+
func(s *reconnectingpty.Server) {
307+
s.ExperimentalContainersEnabled = a.experimentalDevcontainersEnabled
308+
},
300309
)
301310
go a.runLoop()
302311
}

agent/agent_test.go

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,28 @@ import (
2525
"testing"
2626
"time"
2727

28+
"go.uber.org/goleak"
29+
"tailscale.com/net/speedtest"
30+
"tailscale.com/tailcfg"
31+
2832
"github.com/bramvdbogaerde/go-scp"
2933
"github.com/google/uuid"
34+
"github.com/ory/dockertest/v3"
35+
"github.com/ory/dockertest/v3/docker"
3036
"github.com/pion/udp"
3137
"github.com/pkg/sftp"
3238
"github.com/prometheus/client_golang/prometheus"
3339
promgo "github.com/prometheus/client_model/go"
3440
"github.com/spf13/afero"
3541
"github.com/stretchr/testify/assert"
3642
"github.com/stretchr/testify/require"
37-
"go.uber.org/goleak"
3843
"golang.org/x/crypto/ssh"
3944
"golang.org/x/exp/slices"
4045
"golang.org/x/xerrors"
41-
"tailscale.com/net/speedtest"
42-
"tailscale.com/tailcfg"
4346

4447
"cdr.dev/slog"
4548
"cdr.dev/slog/sloggers/slogtest"
49+
4650
"github.com/coder/coder/v2/agent"
4751
"github.com/coder/coder/v2/agent/agentssh"
4852
"github.com/coder/coder/v2/agent/agenttest"
@@ -1761,6 +1765,74 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
17611765
}
17621766
}
17631767

1768+
// This tests end-to-end functionality of connecting to a running container
1769+
// and executing a command. It creates a real Docker container and runs a
1770+
// command. As such, it does not run by default in CI.
1771+
// You can run it manually as follows:
1772+
//
1773+
// CODER_TEST_USE_DOCKER=1 go test -count=1 ./agent -run TestAgent_ReconnectingPTYContainer
1774+
func TestAgent_ReconnectingPTYContainer(t *testing.T) {
1775+
t.Parallel()
1776+
if os.Getenv("CODER_TEST_USE_DOCKER") != "1" {
1777+
t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test")
1778+
}
1779+
1780+
ctx := testutil.Context(t, testutil.WaitLong)
1781+
1782+
pool, err := dockertest.NewPool("")
1783+
require.NoError(t, err, "Could not connect to docker")
1784+
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
1785+
Repository: "busybox",
1786+
Tag: "latest",
1787+
Cmd: []string{"sleep", "infnity"},
1788+
}, func(config *docker.HostConfig) {
1789+
config.AutoRemove = true
1790+
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
1791+
})
1792+
require.NoError(t, err, "Could not start container")
1793+
t.Cleanup(func() {
1794+
err := pool.Purge(ct)
1795+
require.NoError(t, err, "Could not stop container")
1796+
})
1797+
// Wait for container to start
1798+
require.Eventually(t, func() bool {
1799+
ct, ok := pool.ContainerByName(ct.Container.Name)
1800+
return ok && ct.Container.State.Running
1801+
}, testutil.WaitShort, testutil.IntervalSlow, "Container did not start in time")
1802+
1803+
// nolint: dogsled
1804+
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
1805+
o.ExperimentalContainersEnabled = true
1806+
})
1807+
ac, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "/bin/sh", func(arp *workspacesdk.AgentReconnectingPTYInit) {
1808+
arp.Container = ct.Container.ID
1809+
})
1810+
require.NoError(t, err, "failed to create ReconnectingPTY")
1811+
defer ac.Close()
1812+
tr := testutil.NewTerminalReader(t, ac)
1813+
1814+
require.NoError(t, tr.ReadUntil(ctx, func(line string) bool {
1815+
return strings.Contains(line, "#") || strings.Contains(line, "$")
1816+
}), "find prompt")
1817+
1818+
require.NoError(t, json.NewEncoder(ac).Encode(workspacesdk.ReconnectingPTYRequest{
1819+
Data: "hostname\r",
1820+
}), "write hostname")
1821+
require.NoError(t, tr.ReadUntil(ctx, func(line string) bool {
1822+
return strings.Contains(line, "hostname")
1823+
}), "find hostname command")
1824+
1825+
require.NoError(t, tr.ReadUntil(ctx, func(line string) bool {
1826+
return strings.Contains(line, ct.Container.Config.Hostname)
1827+
}), "find hostname output")
1828+
require.NoError(t, json.NewEncoder(ac).Encode(workspacesdk.ReconnectingPTYRequest{
1829+
Data: "exit\r",
1830+
}), "write exit command")
1831+
1832+
// Wait for the connection to close.
1833+
require.ErrorIs(t, tr.ReadUntil(ctx, nil), io.EOF)
1834+
}
1835+
17641836
func TestAgent_Dial(t *testing.T) {
17651837
t.Parallel()
17661838

agent/agentcontainers/containers_dockercli.go

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"context"
77
"encoding/json"
88
"fmt"
9-
"os"
109
"os/user"
1110
"slices"
1211
"sort"
@@ -15,6 +14,7 @@ import (
1514
"time"
1615

1716
"github.com/coder/coder/v2/agent/agentexec"
17+
"github.com/coder/coder/v2/agent/usershell"
1818
"github.com/coder/coder/v2/codersdk"
1919

2020
"golang.org/x/exp/maps"
@@ -37,6 +37,7 @@ func NewDocker(execer agentexec.Execer) Lister {
3737
// DockerEnvInfoer is an implementation of agentssh.EnvInfoer that returns
3838
// information about a container.
3939
type DockerEnvInfoer struct {
40+
usershell.SystemEnvInfo
4041
container string
4142
user *user.User
4243
userShell string
@@ -122,26 +123,13 @@ func EnvInfo(ctx context.Context, execer agentexec.Execer, container, containerU
122123
return &dei, nil
123124
}
124125

125-
func (dei *DockerEnvInfoer) CurrentUser() (*user.User, error) {
126+
func (dei *DockerEnvInfoer) User() (*user.User, error) {
126127
// Clone the user so that the caller can't modify it
127128
u := *dei.user
128129
return &u, nil
129130
}
130131

131-
func (*DockerEnvInfoer) Environ() []string {
132-
// Return a clone of the environment so that the caller can't modify it
133-
return os.Environ()
134-
}
135-
136-
func (*DockerEnvInfoer) UserHomeDir() (string, error) {
137-
// We default the working directory of the command to the user's home
138-
// directory. Since this came from inside the container, we cannot guarantee
139-
// that this exists on the host. Return the "real" home directory of the user
140-
// instead.
141-
return os.UserHomeDir()
142-
}
143-
144-
func (dei *DockerEnvInfoer) UserShell(string) (string, error) {
132+
func (dei *DockerEnvInfoer) Shell(string) (string, error) {
145133
return dei.userShell, nil
146134
}
147135

agent/agentcontainers/containers_internal_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -502,15 +502,15 @@ func TestDockerEnvInfoer(t *testing.T) {
502502
dei, err := EnvInfo(ctx, agentexec.DefaultExecer, ct.Container.ID, tt.containerUser)
503503
require.NoError(t, err, "Expected no error from DockerEnvInfo()")
504504

505-
u, err := dei.CurrentUser()
505+
u, err := dei.User()
506506
require.NoError(t, err, "Expected no error from CurrentUser()")
507507
require.Equal(t, tt.expectedUsername, u.Username, "Expected username to match")
508508

509-
hd, err := dei.UserHomeDir()
509+
hd, err := dei.HomeDir()
510510
require.NoError(t, err, "Expected no error from UserHomeDir()")
511511
require.NotEmpty(t, hd, "Expected user homedir to be non-empty")
512512

513-
sh, err := dei.UserShell(tt.containerUser)
513+
sh, err := dei.Shell(tt.containerUser)
514514
require.NoError(t, err, "Expected no error from UserShell()")
515515
require.Equal(t, tt.expectedUserShell, sh, "Expected user shell to match")
516516

agent/agentrsa/key.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package agentrsa
2+
3+
import (
4+
"crypto/rsa"
5+
"math/big"
6+
"math/rand"
7+
)
8+
9+
// GenerateDeterministicKey generates an RSA private key deterministically based on the provided seed.
10+
// This function uses a deterministic random source to generate the primes p and q, ensuring that the
11+
// same seed will always produce the same private key. The generated key is 2048 bits in size.
12+
//
13+
// Reference: https://pkg.go.dev/crypto/rsa#GenerateKey
14+
func GenerateDeterministicKey(seed int64) *rsa.PrivateKey {
15+
// Since the standard lib purposefully does not generate
16+
// deterministic rsa keys, we need to do it ourselves.
17+
18+
// Create deterministic random source
19+
// nolint: gosec
20+
deterministicRand := rand.New(rand.NewSource(seed))
21+
22+
// Use fixed values for p and q based on the seed
23+
p := big.NewInt(0)
24+
q := big.NewInt(0)
25+
e := big.NewInt(65537) // Standard RSA public exponent
26+
27+
for {
28+
// Generate deterministic primes using the seeded random
29+
// Each prime should be ~1024 bits to get a 2048-bit key
30+
for {
31+
p.SetBit(p, 1024, 1) // Ensure it's large enough
32+
for i := range 1024 {
33+
if deterministicRand.Int63()%2 == 1 {
34+
p.SetBit(p, i, 1)
35+
} else {
36+
p.SetBit(p, i, 0)
37+
}
38+
}
39+
p1 := new(big.Int).Sub(p, big.NewInt(1))
40+
if p.ProbablyPrime(20) && new(big.Int).GCD(nil, nil, e, p1).Cmp(big.NewInt(1)) == 0 {
41+
break
42+
}
43+
}
44+
45+
for {
46+
q.SetBit(q, 1024, 1) // Ensure it's large enough
47+
for i := range 1024 {
48+
if deterministicRand.Int63()%2 == 1 {
49+
q.SetBit(q, i, 1)
50+
} else {
51+
q.SetBit(q, i, 0)
52+
}
53+
}
54+
q1 := new(big.Int).Sub(q, big.NewInt(1))
55+
if q.ProbablyPrime(20) && p.Cmp(q) != 0 && new(big.Int).GCD(nil, nil, e, q1).Cmp(big.NewInt(1)) == 0 {
56+
break
57+
}
58+
}
59+
60+
// Calculate phi = (p-1) * (q-1)
61+
p1 := new(big.Int).Sub(p, big.NewInt(1))
62+
q1 := new(big.Int).Sub(q, big.NewInt(1))
63+
phi := new(big.Int).Mul(p1, q1)
64+
65+
// Calculate private exponent d
66+
d := new(big.Int).ModInverse(e, phi)
67+
if d != nil {
68+
// Calculate n = p * q
69+
n := new(big.Int).Mul(p, q)
70+
71+
// Create the private key
72+
privateKey := &rsa.PrivateKey{
73+
PublicKey: rsa.PublicKey{
74+
N: n,
75+
E: int(e.Int64()),
76+
},
77+
D: d,
78+
Primes: []*big.Int{p, q},
79+
}
80+
81+
// Compute precomputed values
82+
privateKey.Precompute()
83+
84+
return privateKey
85+
}
86+
}
87+
}

agent/agentrsa/key_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package agentrsa_test
2+
3+
import (
4+
"crypto/rsa"
5+
"math/rand/v2"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
10+
"github.com/coder/coder/v2/agent/agentrsa"
11+
)
12+
13+
func TestGenerateDeterministicKey(t *testing.T) {
14+
t.Parallel()
15+
16+
key1 := agentrsa.GenerateDeterministicKey(1234)
17+
key2 := agentrsa.GenerateDeterministicKey(1234)
18+
19+
assert.Equal(t, key1, key2)
20+
assert.EqualExportedValues(t, key1, key2)
21+
}
22+
23+
var result *rsa.PrivateKey
24+
25+
func BenchmarkGenerateDeterministicKey(b *testing.B) {
26+
var r *rsa.PrivateKey
27+
28+
for range b.N {
29+
// always record the result of DeterministicPrivateKey to prevent
30+
// the compiler eliminating the function call.
31+
r = agentrsa.GenerateDeterministicKey(rand.Int64())
32+
}
33+
34+
// always store the result to a package level variable
35+
// so the compiler cannot eliminate the Benchmark itself.
36+
result = r
37+
}
38+
39+
func FuzzGenerateDeterministicKey(f *testing.F) {
40+
testcases := []int64{0, 1234, 1010101010}
41+
for _, tc := range testcases {
42+
f.Add(tc) // Use f.Add to provide a seed corpus
43+
}
44+
f.Fuzz(func(t *testing.T, seed int64) {
45+
key1 := agentrsa.GenerateDeterministicKey(seed)
46+
key2 := agentrsa.GenerateDeterministicKey(seed)
47+
assert.Equal(t, key1, key2)
48+
assert.EqualExportedValues(t, key1, key2)
49+
})
50+
}

0 commit comments

Comments
 (0)