1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
19 "git.arvados.org/arvados.git/lib/controller/router"
20 "git.arvados.org/arvados.git/lib/controller/rpc"
21 "git.arvados.org/arvados.git/lib/crunchrun"
22 "git.arvados.org/arvados.git/lib/ctrlctx"
23 "git.arvados.org/arvados.git/sdk/go/arvados"
24 "git.arvados.org/arvados.git/sdk/go/arvadostest"
25 "git.arvados.org/arvados.git/sdk/go/ctxlog"
26 "golang.org/x/crypto/ssh"
27 check "gopkg.in/check.v1"
30 var _ = check.Suite(&ContainerGatewaySuite{})
32 type ContainerGatewaySuite struct {
38 func (s *ContainerGatewaySuite) SetUpTest(c *check.C) {
39 s.localdbSuite.SetUpTest(c)
41 s.ctrUUID = arvadostest.QueuedContainerUUID
43 h := hmac.New(sha256.New, []byte(s.cluster.SystemRootToken))
44 fmt.Fprint(h, s.ctrUUID)
45 authKey := fmt.Sprintf("%x", h.Sum(nil))
47 rtr := router.New(s.localdb, router.Config{})
48 srv := httptest.NewUnstartedServer(rtr)
50 // the test setup doesn't use lib/service so
51 // service.URLFromContext() returns nothing -- instead, this
52 // is how we advertise our internal URL and enable
53 // proxy-to-other-controller mode,
54 forceInternalURLForTest = &arvados.URL{Scheme: "https", Host: srv.Listener.Addr().String()}
55 ac := &arvados.Client{
56 APIHost: srv.Listener.Addr().String(),
57 AuthToken: arvadostest.Dispatch1Token,
60 s.gw = &crunchrun.Gateway{
61 ContainerUUID: s.ctrUUID,
63 Address: "localhost:0",
64 Log: ctxlog.TestLogger(c),
65 Target: crunchrun.GatewayTargetStub{},
68 c.Assert(s.gw.Start(), check.IsNil)
69 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
70 // OK if this line fails (because state is already Running
71 // from a previous test case) as long as the following line
73 s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
75 Attrs: map[string]interface{}{
76 "state": arvados.ContainerStateLocked}})
77 _, err := s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
79 Attrs: map[string]interface{}{
80 "state": arvados.ContainerStateRunning,
81 "gateway_address": s.gw.Address}})
82 c.Assert(err, check.IsNil)
84 s.cluster.Containers.ShellAccess.Admin = true
85 s.cluster.Containers.ShellAccess.User = true
86 _, err = s.db.Exec(`update containers set interactive_session_started=$1 where uuid=$2`, false, s.ctrUUID)
87 c.Check(err, check.IsNil)
90 func (s *ContainerGatewaySuite) TestConfig(c *check.C) {
91 for _, trial := range []struct {
97 {true, true, arvadostest.ActiveTokenV2, 0},
98 {true, false, arvadostest.ActiveTokenV2, 503},
99 {false, true, arvadostest.ActiveTokenV2, 0},
100 {false, false, arvadostest.ActiveTokenV2, 503},
101 {true, true, arvadostest.AdminToken, 0},
102 {true, false, arvadostest.AdminToken, 0},
103 {false, true, arvadostest.AdminToken, 403},
104 {false, false, arvadostest.AdminToken, 503},
106 c.Logf("trial %#v", trial)
107 s.cluster.Containers.ShellAccess.Admin = trial.configAdmin
108 s.cluster.Containers.ShellAccess.User = trial.configUser
109 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, trial.sendToken)
110 sshconn, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
111 if trial.errorCode == 0 {
112 if !c.Check(err, check.IsNil) {
115 if !c.Check(sshconn.Conn, check.NotNil) {
120 c.Check(err, check.NotNil)
121 err, ok := err.(interface{ HTTPStatus() int })
122 if c.Check(ok, check.Equals, true) {
123 c.Check(err.HTTPStatus(), check.Equals, trial.errorCode)
129 func (s *ContainerGatewaySuite) TestDirectTCP(c *check.C) {
130 // Set up servers on a few TCP ports
132 for i := 0; i < 3; i++ {
133 ln, err := net.Listen("tcp", ":0")
134 c.Assert(err, check.IsNil)
136 addrs = append(addrs, ln.Addr().String())
139 conn, err := ln.Accept()
144 fmt.Fscanf(conn, "%s\n", &gotAddr)
145 c.Logf("stub server listening at %s received string %q from remote %s", ln.Addr().String(), gotAddr, conn.RemoteAddr())
146 if gotAddr == ln.Addr().String() {
147 fmt.Fprintf(conn, "%s\n", ln.Addr().String())
154 c.Logf("connecting to %s", s.gw.Address)
155 sshconn, err := s.localdb.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
156 c.Assert(err, check.IsNil)
157 c.Assert(sshconn.Conn, check.NotNil)
158 defer sshconn.Conn.Close()
159 conn, chans, reqs, err := ssh.NewClientConn(sshconn.Conn, "zzzz-dz642-abcdeabcdeabcde", &ssh.ClientConfig{
160 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil },
162 c.Assert(err, check.IsNil)
163 client := ssh.NewClient(conn, chans, reqs)
164 for _, expectAddr := range addrs {
165 _, port, err := net.SplitHostPort(expectAddr)
166 c.Assert(err, check.IsNil)
168 c.Logf("trying foo:%s", port)
170 conn, err := client.Dial("tcp", "foo:"+port)
171 c.Assert(err, check.IsNil)
172 conn.SetDeadline(time.Now().Add(time.Second))
173 buf, err := ioutil.ReadAll(conn)
174 c.Check(err, check.IsNil)
175 c.Check(string(buf), check.Equals, "")
178 c.Logf("trying localhost:%s", port)
180 conn, err := client.Dial("tcp", "localhost:"+port)
181 c.Assert(err, check.IsNil)
182 conn.SetDeadline(time.Now().Add(time.Second))
183 conn.Write([]byte(expectAddr + "\n"))
185 fmt.Fscanf(conn, "%s\n", &gotAddr)
186 c.Check(gotAddr, check.Equals, expectAddr)
191 func (s *ContainerGatewaySuite) TestConnect(c *check.C) {
192 c.Logf("connecting to %s", s.gw.Address)
193 sshconn, err := s.localdb.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
194 c.Assert(err, check.IsNil)
195 c.Assert(sshconn.Conn, check.NotNil)
196 defer sshconn.Conn.Close()
198 done := make(chan struct{})
202 // Receive text banner
203 buf := make([]byte, 12)
204 _, err := io.ReadFull(sshconn.Conn, buf)
205 c.Check(err, check.IsNil)
206 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
209 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
210 c.Check(err, check.IsNil)
213 _, err = io.ReadFull(sshconn.Conn, buf[:4])
214 c.Check(err, check.IsNil)
216 // If we can get this far into an SSH handshake...
217 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
221 case <-time.After(time.Second):
224 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
225 c.Check(err, check.IsNil)
226 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)
229 func (s *ContainerGatewaySuite) TestConnectFail(c *check.C) {
230 c.Log("trying with no token")
231 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, "")
232 _, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
233 c.Check(err, check.ErrorMatches, `.* 401 .*`)
235 c.Log("trying with anonymous token")
236 ctx = ctrlctx.NewWithToken(s.ctx, s.cluster, arvadostest.AnonymousToken)
237 _, err = s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
238 c.Check(err, check.ErrorMatches, `.* 404 .*`)
241 func (s *ContainerGatewaySuite) TestCreateTunnel(c *check.C) {
243 conn, err := s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
246 c.Check(err, check.ErrorMatches, `authentication error`)
247 c.Check(conn.Conn, check.IsNil)
250 conn, err = s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
252 AuthSecret: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
254 c.Check(err, check.ErrorMatches, `authentication error`)
255 c.Check(conn.Conn, check.IsNil)
258 conn, err = s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
260 AuthSecret: s.gw.AuthSecret,
262 c.Check(err, check.IsNil)
263 c.Check(conn.Conn, check.NotNil)
266 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyOK(c *check.C) {
267 forceProxyForTest = true
268 defer func() { forceProxyForTest = false }()
269 s.cluster.Services.Controller.InternalURLs[*forceInternalURLForTest] = arvados.ServiceInstance{}
270 defer delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
271 s.testConnectThroughTunnel(c, "")
274 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyError(c *check.C) {
275 forceProxyForTest = true
276 defer func() { forceProxyForTest = false }()
277 // forceInternalURLForTest shouldn't be used because it isn't
278 // listed in s.cluster.Services.Controller.InternalURLs
279 s.testConnectThroughTunnel(c, `.*tunnel endpoint is invalid.*`)
282 func (s *ContainerGatewaySuite) TestConnectThroughTunnelNoProxyOK(c *check.C) {
283 s.testConnectThroughTunnel(c, "")
286 func (s *ContainerGatewaySuite) testConnectThroughTunnel(c *check.C, expectErrorMatch string) {
287 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
288 // Until the tunnel starts up, set gateway_address to a value
289 // that can't work. We want to ensure the only way we can
290 // reach the gateway is through the tunnel.
291 tungw := &crunchrun.Gateway{
292 ContainerUUID: s.ctrUUID,
293 AuthSecret: s.gw.AuthSecret,
294 Log: ctxlog.TestLogger(c),
295 Target: crunchrun.GatewayTargetStub{},
296 ArvadosClient: s.gw.ArvadosClient,
297 UpdateTunnelURL: func(url string) {
298 c.Logf("UpdateTunnelURL(%q)", url)
299 gwaddr := "tunnel " + url
300 s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
302 Attrs: map[string]interface{}{
303 "gateway_address": gwaddr}})
306 c.Assert(tungw.Start(), check.IsNil)
308 // We didn't supply an external hostname in the Address field,
309 // so Start() should assign a local address.
310 host, _, err := net.SplitHostPort(tungw.Address)
311 c.Assert(err, check.IsNil)
312 c.Check(host, check.Equals, "127.0.0.1")
314 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
316 Attrs: map[string]interface{}{
317 "state": arvados.ContainerStateRunning,
319 c.Assert(err, check.IsNil)
321 for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Second / 2) {
322 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
323 c.Assert(err, check.IsNil)
324 c.Check(ctr.InteractiveSessionStarted, check.Equals, false)
325 c.Logf("ctr.GatewayAddress == %s", ctr.GatewayAddress)
326 if strings.HasPrefix(ctr.GatewayAddress, "tunnel ") {
331 c.Log("connecting to gateway through tunnel")
332 arpc := rpc.NewConn("", &url.URL{Scheme: "https", Host: s.gw.ArvadosClient.APIHost}, true, rpc.PassthroughTokenProvider)
333 sshconn, err := arpc.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
334 if expectErrorMatch != "" {
335 c.Check(err, check.ErrorMatches, expectErrorMatch)
338 c.Assert(err, check.IsNil)
339 c.Assert(sshconn.Conn, check.NotNil)
340 defer sshconn.Conn.Close()
342 done := make(chan struct{})
346 // Receive text banner
347 buf := make([]byte, 12)
348 _, err := io.ReadFull(sshconn.Conn, buf)
349 c.Check(err, check.IsNil)
350 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
353 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
354 c.Check(err, check.IsNil)
357 _, err = io.ReadFull(sshconn.Conn, buf[:4])
358 c.Check(err, check.IsNil)
360 // If we can get this far into an SSH handshake...
361 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
365 case <-time.After(time.Second):
368 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
369 c.Check(err, check.IsNil)
370 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)