SSH access from the Sites page; containers and discovery dialog; dialogs fit small screens
Each site has an SSH button: it opens a TCP port on an edge whose target is Bamboo's built-in SSH server, and shows the ssh command to use. An end-to-end test signs in through the edge, the tunnel and Bamboo's server. The Found by dialog now lists Docker containers and discovered services, wraps long names and groups TCP and UDP ports. The dialog overlay no longer lets a wide table push a dialog off a phone screen. Builds against Bamboo v0.1.8. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
92262441db
commit
1c1f2b1d44
4 files changed
+193
-9
No files matched your search
+108
@@ -0,0 +1,108 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.veillax.com/Veillax/bamboo/sshd"
|
||||
"git.veillax.com/Veillax/blacktip"
|
||||
"git.veillax.com/Veillax/reef/model"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func newSSHSigner(t *testing.T) ssh.Signer {
|
||||
t.Helper()
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, err := ssh.NewSignerFromKey(priv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// A TCP port on the edge whose target is Bamboo's SSH server at the site reaches that server
|
||||
// with an ordinary ssh client: the path is client, edge, WireGuard tunnel, Bamboo, sshd.
|
||||
func TestSSHThroughTheEdgeToBamboosServer(t *testing.T) {
|
||||
w := setup(t, blacktip.TraefikConfig{})
|
||||
dir := t.TempDir()
|
||||
signer := newSSHSigner(t)
|
||||
keys := filepath.Join(dir, "authorized_keys")
|
||||
if err := os.WriteFile(keys, ssh.MarshalAuthorizedKey(signer.PublicKey()), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv, err := sshd.New(sshd.Config{AuthorizedKeys: keys, HostKey: filepath.Join(dir, "ssh_host_ed25519_key")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
go srv.Serve(ctx, ln)
|
||||
|
||||
edgePort := freePort(t)
|
||||
w.open(model.Mapping{Proto: model.ProtoTCP, Port: edgePort, Name: "ssh", Targets: tgt(w.site, ln.Addr().(*net.TCPAddr).Port)})
|
||||
addr := "127.0.0.1:" + strconv.Itoa(edgePort)
|
||||
|
||||
connect := func(auth ssh.AuthMethod) (*ssh.Client, error) {
|
||||
return ssh.Dial("tcp", addr, &ssh.ClientConfig{User: "alex", Auth: []ssh.AuthMethod{auth}, Timeout: 5 * time.Second,
|
||||
// the key the client sees through the tunnel must be Bamboo's, not something in between
|
||||
HostKeyCallback: func(_ string, _ net.Addr, k ssh.PublicKey) error {
|
||||
if got := ssh.FingerprintSHA256(k); got != srv.Fingerprint() {
|
||||
return fmt.Errorf("host key %s is not the Bamboo server's %s", got, srv.Fingerprint())
|
||||
}
|
||||
return nil
|
||||
}})
|
||||
}
|
||||
var client *ssh.Client
|
||||
var last error
|
||||
for i := 0; i < 60 && client == nil; i++ {
|
||||
if client, last = connect(ssh.PublicKeys(signer)); client == nil {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
if client == nil {
|
||||
t.Fatalf("could not sign in through the edge: %v", last)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if sshd.Supported {
|
||||
sess, err := client.NewSession()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := sess.Output("echo through-the-tunnel")
|
||||
sess.Close()
|
||||
if err != nil || strings.TrimSpace(string(out)) != "through-the-tunnel" {
|
||||
t.Fatalf("running a command: %q, %v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
// the same narrow rules hold through the tunnel
|
||||
if c, err := connect(ssh.PublicKeys(newSSHSigner(t))); err == nil {
|
||||
c.Close()
|
||||
t.Error("a key that is not in authorized_keys signed in")
|
||||
}
|
||||
if c, err := connect(ssh.Password("hunter2")); err == nil {
|
||||
c.Close()
|
||||
t.Error("a password signed in")
|
||||
}
|
||||
if c, err := client.Dial("tcp", "127.0.0.1:1"); err == nil {
|
||||
c.Close()
|
||||
t.Error("port forwarding through the SSH server is allowed")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user