Terminal relay: answer Hammerhead's terminal sessions through the agent
When someone opens a terminal for this site, Hammerhead lists the session in the site's config. The agent connects out to Hammerhead and joins that connection to its own SSH server, so no port has to be open on an edge. The agent now reports where its SSH server listens and that it can relay. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1fba65fd3e
commit
a2403ecf9d
7 files changed
+225
-6
No files matched your search
@@ -0,0 +1,114 @@
|
||||
package bamboo
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
func TestSSHTarget(t *testing.T) {
|
||||
for in, want := range map[string]string{
|
||||
"": "",
|
||||
"127.0.0.1:2222": "127.0.0.1:2222",
|
||||
":2222": "127.0.0.1:2222",
|
||||
"0.0.0.0:2200": "127.0.0.1:2200",
|
||||
"[::]:2222": "127.0.0.1:2222",
|
||||
"10.0.0.5:2222": "10.0.0.5:2222",
|
||||
} {
|
||||
if got := sshTarget(in); got != want {
|
||||
t.Errorf("sshTarget(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hammerhead lists a terminal session; the agent calls back with its token and joins the connection
|
||||
// to its SSH server. Here the "SSH server" is a local echo, and Hammerhead is a test server.
|
||||
func TestTerminalRelayJoinsHammerheadToTheLocalSSHServer(t *testing.T) {
|
||||
echo, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer echo.Close()
|
||||
go func() {
|
||||
for {
|
||||
c, err := echo.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() { defer c.Close(); io.Copy(c, c) }()
|
||||
}
|
||||
}()
|
||||
|
||||
got := make(chan *websocket.Conn, 4)
|
||||
auth := make(chan string, 4)
|
||||
paths := make(chan string, 4)
|
||||
hh := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth <- r.Header.Get("Authorization")
|
||||
paths <- r.URL.Path
|
||||
release := make(chan struct{})
|
||||
websocket.Server{Handshake: func(*websocket.Config, *http.Request) error { return nil }, Handler: func(ws *websocket.Conn) {
|
||||
ws.PayloadType = websocket.BinaryFrame
|
||||
got <- ws
|
||||
<-release
|
||||
}}.ServeHTTP(w, r)
|
||||
close(release)
|
||||
}))
|
||||
defer hh.Close()
|
||||
|
||||
b, err := New(Config{Hammerhead: hh.URL, Token: "site-token", DataDir: t.TempDir(), SSH: echo.Addr().String()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.openTerminals([]string{"abc"})
|
||||
b.openTerminals([]string{"abc"}) // fetching the config again must not answer the same session twice
|
||||
|
||||
var ws *websocket.Conn
|
||||
select {
|
||||
case ws = <-got:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the agent never called back")
|
||||
}
|
||||
if a := <-auth; a != "Bearer site-token" {
|
||||
t.Errorf("call-back authorization: %q", a)
|
||||
}
|
||||
if p := <-paths; p != "/site/terminal/abc" {
|
||||
t.Errorf("call-back path: %q", p)
|
||||
}
|
||||
if _, err := ws.Write([]byte("hello through the relay")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := make([]byte, 64)
|
||||
ws.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
n, err := ws.Read(buf)
|
||||
if err != nil || string(buf[:n]) != "hello through the relay" {
|
||||
t.Fatalf("echo through the relay: %q, %v", buf[:n], err)
|
||||
}
|
||||
select {
|
||||
case <-got:
|
||||
t.Error("the same session was answered twice")
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
}
|
||||
ws.Close()
|
||||
}
|
||||
|
||||
// With no SSH server the agent does not call back at all, rather than connect and hang.
|
||||
func TestTerminalRelayDoesNothingWithoutAnSSHServer(t *testing.T) {
|
||||
called := make(chan struct{}, 1)
|
||||
hh := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called <- struct{}{} }))
|
||||
defer hh.Close()
|
||||
b, err := New(Config{Hammerhead: hh.URL, Token: "t", DataDir: t.TempDir()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.openTerminals([]string{"x"})
|
||||
select {
|
||||
case <-called:
|
||||
t.Error("the agent called Hammerhead although it has no SSH server")
|
||||
case <-time.After(400 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user