1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
20 "git.arvados.org/arvados.git/lib/config"
21 "git.arvados.org/arvados.git/lib/controller/router"
22 "git.arvados.org/arvados.git/lib/controller/rpc"
23 "git.arvados.org/arvados.git/lib/crunchrun"
24 "git.arvados.org/arvados.git/sdk/go/arvados"
25 "git.arvados.org/arvados.git/sdk/go/arvadostest"
26 "git.arvados.org/arvados.git/sdk/go/auth"
27 "git.arvados.org/arvados.git/sdk/go/ctxlog"
28 "golang.org/x/crypto/ssh"
29 check "gopkg.in/check.v1"
32 var _ = check.Suite(&ContainerGatewaySuite{})
34 type ContainerGatewaySuite struct {
35 cluster *arvados.Cluster
42 func (s *ContainerGatewaySuite) TearDownSuite(c *check.C) {
43 // Undo any changes/additions to the user database so they
44 // don't affect subsequent tests.
45 arvadostest.ResetEnv()
46 c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
49 func (s *ContainerGatewaySuite) SetUpSuite(c *check.C) {
50 cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
51 c.Assert(err, check.IsNil)
52 s.cluster, err = cfg.GetCluster("")
53 c.Assert(err, check.IsNil)
54 s.localdb = NewConn(s.cluster)
55 s.ctx = auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
57 s.ctrUUID = arvadostest.QueuedContainerUUID
59 h := hmac.New(sha256.New, []byte(s.cluster.SystemRootToken))
60 fmt.Fprint(h, s.ctrUUID)
61 authKey := fmt.Sprintf("%x", h.Sum(nil))
63 rtr := router.New(s.localdb, router.Config{})
64 srv := httptest.NewUnstartedServer(rtr)
66 // the test setup doesn't use lib/service so
67 // service.URLFromContext() returns nothing -- instead, this
68 // is how we advertise our internal URL and enable
69 // proxy-to-other-controller mode,
70 forceInternalURLForTest = &arvados.URL{Scheme: "https", Host: srv.Listener.Addr().String()}
71 ac := &arvados.Client{
72 APIHost: srv.Listener.Addr().String(),
73 AuthToken: arvadostest.Dispatch1Token,
76 s.gw = &crunchrun.Gateway{
77 ContainerUUID: s.ctrUUID,
79 Address: "localhost:0",
80 Log: ctxlog.TestLogger(c),
81 Target: crunchrun.GatewayTargetStub{},
84 c.Assert(s.gw.Start(), check.IsNil)
85 rootctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{s.cluster.SystemRootToken}})
86 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
88 Attrs: map[string]interface{}{
89 "state": arvados.ContainerStateLocked}})
90 c.Assert(err, check.IsNil)
93 func (s *ContainerGatewaySuite) SetUpTest(c *check.C) {
94 // clear any tunnel sessions started by previous test cases
95 s.localdb.gwTunnelsLock.Lock()
96 s.localdb.gwTunnels = nil
97 s.localdb.gwTunnelsLock.Unlock()
99 rootctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{s.cluster.SystemRootToken}})
100 _, err := s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
102 Attrs: map[string]interface{}{
103 "state": arvados.ContainerStateRunning,
104 "gateway_address": s.gw.Address}})
105 c.Assert(err, check.IsNil)
107 s.cluster.Containers.ShellAccess.Admin = true
108 s.cluster.Containers.ShellAccess.User = true
109 _, err = arvadostest.DB(c, s.cluster).Exec(`update containers set interactive_session_started=$1 where uuid=$2`, false, s.ctrUUID)
110 c.Check(err, check.IsNil)
113 func (s *ContainerGatewaySuite) TestConfig(c *check.C) {
114 for _, trial := range []struct {
120 {true, true, arvadostest.ActiveTokenV2, 0},
121 {true, false, arvadostest.ActiveTokenV2, 503},
122 {false, true, arvadostest.ActiveTokenV2, 0},
123 {false, false, arvadostest.ActiveTokenV2, 503},
124 {true, true, arvadostest.AdminToken, 0},
125 {true, false, arvadostest.AdminToken, 0},
126 {false, true, arvadostest.AdminToken, 403},
127 {false, false, arvadostest.AdminToken, 503},
129 c.Logf("trial %#v", trial)
130 s.cluster.Containers.ShellAccess.Admin = trial.configAdmin
131 s.cluster.Containers.ShellAccess.User = trial.configUser
132 ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: []string{trial.sendToken}})
133 sshconn, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
134 if trial.errorCode == 0 {
135 if !c.Check(err, check.IsNil) {
138 if !c.Check(sshconn.Conn, check.NotNil) {
143 c.Check(err, check.NotNil)
144 err, ok := err.(interface{ HTTPStatus() int })
145 if c.Check(ok, check.Equals, true) {
146 c.Check(err.HTTPStatus(), check.Equals, trial.errorCode)
152 func (s *ContainerGatewaySuite) TestDirectTCP(c *check.C) {
153 // Set up servers on a few TCP ports
155 for i := 0; i < 3; i++ {
156 ln, err := net.Listen("tcp", ":0")
157 c.Assert(err, check.IsNil)
159 addrs = append(addrs, ln.Addr().String())
162 conn, err := ln.Accept()
167 fmt.Fscanf(conn, "%s\n", &gotAddr)
168 c.Logf("stub server listening at %s received string %q from remote %s", ln.Addr().String(), gotAddr, conn.RemoteAddr())
169 if gotAddr == ln.Addr().String() {
170 fmt.Fprintf(conn, "%s\n", ln.Addr().String())
177 c.Logf("connecting to %s", s.gw.Address)
178 sshconn, err := s.localdb.ContainerSSH(s.ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
179 c.Assert(err, check.IsNil)
180 c.Assert(sshconn.Conn, check.NotNil)
181 defer sshconn.Conn.Close()
182 conn, chans, reqs, err := ssh.NewClientConn(sshconn.Conn, "zzzz-dz642-abcdeabcdeabcde", &ssh.ClientConfig{
183 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil },
185 c.Assert(err, check.IsNil)
186 client := ssh.NewClient(conn, chans, reqs)
187 for _, expectAddr := range addrs {
188 _, port, err := net.SplitHostPort(expectAddr)
189 c.Assert(err, check.IsNil)
191 c.Logf("trying foo:%s", port)
193 conn, err := client.Dial("tcp", "foo:"+port)
194 c.Assert(err, check.IsNil)
195 conn.SetDeadline(time.Now().Add(time.Second))
196 buf, err := ioutil.ReadAll(conn)
197 c.Check(err, check.IsNil)
198 c.Check(string(buf), check.Equals, "")
201 c.Logf("trying localhost:%s", port)
203 conn, err := client.Dial("tcp", "localhost:"+port)
204 c.Assert(err, check.IsNil)
205 conn.SetDeadline(time.Now().Add(time.Second))
206 conn.Write([]byte(expectAddr + "\n"))
208 fmt.Fscanf(conn, "%s\n", &gotAddr)
209 c.Check(gotAddr, check.Equals, expectAddr)
214 func (s *ContainerGatewaySuite) TestConnect(c *check.C) {
215 c.Logf("connecting to %s", s.gw.Address)
216 sshconn, err := s.localdb.ContainerSSH(s.ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
217 c.Assert(err, check.IsNil)
218 c.Assert(sshconn.Conn, check.NotNil)
219 defer sshconn.Conn.Close()
221 done := make(chan struct{})
225 // Receive text banner
226 buf := make([]byte, 12)
227 _, err := io.ReadFull(sshconn.Conn, buf)
228 c.Check(err, check.IsNil)
229 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
232 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
233 c.Check(err, check.IsNil)
236 _, err = io.ReadFull(sshconn.Conn, buf[:4])
237 c.Check(err, check.IsNil)
239 // If we can get this far into an SSH handshake...
240 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
244 case <-time.After(time.Second):
247 ctr, err := s.localdb.ContainerGet(s.ctx, arvados.GetOptions{UUID: s.ctrUUID})
248 c.Check(err, check.IsNil)
249 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)
252 func (s *ContainerGatewaySuite) TestConnectFail(c *check.C) {
253 c.Log("trying with no token")
254 ctx := auth.NewContext(context.Background(), &auth.Credentials{})
255 _, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
256 c.Check(err, check.ErrorMatches, `.* 401 .*`)
258 c.Log("trying with anonymous token")
259 ctx = auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.AnonymousToken}})
260 _, err = s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
261 c.Check(err, check.ErrorMatches, `.* 404 .*`)
264 func (s *ContainerGatewaySuite) TestCreateTunnel(c *check.C) {
266 conn, err := s.localdb.ContainerGatewayTunnel(s.ctx, arvados.ContainerGatewayTunnelOptions{
269 c.Check(err, check.ErrorMatches, `authentication error`)
270 c.Check(conn.Conn, check.IsNil)
273 conn, err = s.localdb.ContainerGatewayTunnel(s.ctx, arvados.ContainerGatewayTunnelOptions{
275 AuthSecret: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
277 c.Check(err, check.ErrorMatches, `authentication error`)
278 c.Check(conn.Conn, check.IsNil)
281 conn, err = s.localdb.ContainerGatewayTunnel(s.ctx, arvados.ContainerGatewayTunnelOptions{
283 AuthSecret: s.gw.AuthSecret,
285 c.Check(err, check.IsNil)
286 c.Check(conn.Conn, check.NotNil)
289 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyOK(c *check.C) {
290 forceProxyForTest = true
291 defer func() { forceProxyForTest = false }()
292 s.cluster.Services.Controller.InternalURLs[*forceInternalURLForTest] = arvados.ServiceInstance{}
293 defer delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
294 s.testConnectThroughTunnel(c, "")
297 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyError(c *check.C) {
298 forceProxyForTest = true
299 defer func() { forceProxyForTest = false }()
300 // forceInternalURLForTest shouldn't be used because it isn't
301 // listed in s.cluster.Services.Controller.InternalURLs
302 s.testConnectThroughTunnel(c, `.*tunnel endpoint is invalid.*`)
305 func (s *ContainerGatewaySuite) TestConnectThroughTunnelNoProxyOK(c *check.C) {
306 s.testConnectThroughTunnel(c, "")
309 func (s *ContainerGatewaySuite) testConnectThroughTunnel(c *check.C, expectErrorMatch string) {
310 rootctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{s.cluster.SystemRootToken}})
311 // Until the tunnel starts up, set gateway_address to a value
312 // that can't work. We want to ensure the only way we can
313 // reach the gateway is through the tunnel.
314 gwaddr := "127.0.0.1:0"
315 tungw := &crunchrun.Gateway{
316 ContainerUUID: s.ctrUUID,
317 AuthSecret: s.gw.AuthSecret,
318 Log: ctxlog.TestLogger(c),
319 Target: crunchrun.GatewayTargetStub{},
320 ArvadosClient: s.gw.ArvadosClient,
321 UpdateTunnelURL: func(url string) {
322 c.Logf("UpdateTunnelURL(%q)", url)
323 gwaddr = "tunnel " + url
324 s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
326 Attrs: map[string]interface{}{
327 "gateway_address": gwaddr}})
330 c.Assert(tungw.Start(), check.IsNil)
332 // We didn't supply an external hostname in the Address field,
333 // so Start() should assign a local address.
334 host, _, err := net.SplitHostPort(tungw.Address)
335 c.Assert(err, check.IsNil)
336 c.Check(host, check.Equals, "127.0.0.1")
338 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
340 Attrs: map[string]interface{}{
341 "state": arvados.ContainerStateRunning,
342 "gateway_address": gwaddr}})
343 c.Assert(err, check.IsNil)
345 for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Second / 2) {
346 ctr, err := s.localdb.ContainerGet(s.ctx, arvados.GetOptions{UUID: s.ctrUUID})
347 c.Assert(err, check.IsNil)
348 c.Check(ctr.InteractiveSessionStarted, check.Equals, false)
349 c.Logf("ctr.GatewayAddress == %s", ctr.GatewayAddress)
350 if strings.HasPrefix(ctr.GatewayAddress, "tunnel ") {
355 c.Log("connecting to gateway through tunnel")
356 arpc := rpc.NewConn("", &url.URL{Scheme: "https", Host: s.gw.ArvadosClient.APIHost}, true, rpc.PassthroughTokenProvider)
357 sshconn, err := arpc.ContainerSSH(s.ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
358 if expectErrorMatch != "" {
359 c.Check(err, check.ErrorMatches, expectErrorMatch)
362 c.Assert(err, check.IsNil)
363 c.Assert(sshconn.Conn, check.NotNil)
364 defer sshconn.Conn.Close()
366 done := make(chan struct{})
370 // Receive text banner
371 buf := make([]byte, 12)
372 _, err := io.ReadFull(sshconn.Conn, buf)
373 c.Check(err, check.IsNil)
374 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
377 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
378 c.Check(err, check.IsNil)
381 _, err = io.ReadFull(sshconn.Conn, buf[:4])
382 c.Check(err, check.IsNil)
384 // If we can get this far into an SSH handshake...
385 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
389 case <-time.After(time.Second):
392 ctr, err := s.localdb.ContainerGet(s.ctx, arvados.GetOptions{UUID: s.ctrUUID})
393 c.Check(err, check.IsNil)
394 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)