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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy