allow random (ephemeral) host ports for ssh server

also fixes an issue with authorized_keys files that
contain multiple pks: it now checks all of them not
just the first one it parses.
diff --git a/cmd/sketch/main.go b/cmd/sketch/main.go
index 94f7559..e6fdbc0 100644
--- a/cmd/sketch/main.go
+++ b/cmd/sketch/main.go
@@ -50,7 +50,7 @@
 	workingDir := flag.String("C", "", "when set, change to this directory before running")
 	sshServerIdentity := flag.String("ssh_server_identity", "", "location of the file containing the private key that the container's ssh server will use to identify itself")
 	sshAuthorizedKeys := flag.String("ssh_authorized_keys", "", "location of the file containing the public keys that the container's ssh server will authorize")
-	sshPort := flag.Int("ssh_port", 2022, "the host port number that the container's ssh server will listen on")
+	sshPort := flag.Int("ssh_port", 0, "the host port number that the container's ssh server will listen on, or a randomly chosen port if this value is 0")
 
 	// Flags geared towards sketch developers or sketch internals:
 	gitUsername := flag.String("git-username", "", "(internal) username for git commits")
diff --git a/dockerimg/dockerimg.go b/dockerimg/dockerimg.go
index b47f7d9..ae18bba 100644
--- a/dockerimg/dockerimg.go
+++ b/dockerimg/dockerimg.go
@@ -250,11 +250,21 @@
 	}
 
 	// Get the sketch server port from the container
-	localAddr, err := getContainerPort(ctx, cntrName)
+	localAddr, err := getContainerPort(ctx, cntrName, "80")
 	if err != nil {
 		return appendInternalErr(err)
 	}
 
+	localSSHAddr, err := getContainerPort(ctx, cntrName, "22")
+	if err != nil {
+		return appendInternalErr(err)
+	}
+	sshHost, sshPort, err := net.SplitHostPort(localSSHAddr)
+	if err != nil {
+		fmt.Println("Error splitting ssh host and port:", err)
+	}
+	fmt.Printf("ssh into this container with: ssh root@%s -p %s\n", sshHost, sshPort)
+
 	// Tell the sketch container which git server port and commit to initialize with.
 	go func() {
 		// TODO: Why is this called in a goroutine? I have found that when I pull this out
@@ -379,8 +389,10 @@
 	if config.SketchPubKey != "" {
 		cmdArgs = append(cmdArgs, "-e", "SKETCH_PUB_KEY="+config.SketchPubKey)
 	}
-	if config.SSHPort != 0 {
-		cmdArgs = append(cmdArgs, "-p", fmt.Sprintf("%d:2022", config.SSHPort)) // forward container ssh port to host ssh port
+	if config.SSHPort > 0 {
+		cmdArgs = append(cmdArgs, "-p", fmt.Sprintf("%d:22", config.SSHPort)) // forward container ssh port to host ssh port
+	} else {
+		cmdArgs = append(cmdArgs, "-p", "22") // use an ephemeral host port for ssh.
 	}
 	if relPath != "." {
 		cmdArgs = append(cmdArgs, "-w", "/app/"+relPath)
@@ -467,9 +479,9 @@
 	return dst, nil
 }
 
-func getContainerPort(ctx context.Context, cntrName string) (string, error) {
+func getContainerPort(ctx context.Context, cntrName, cntrPort string) (string, error) {
 	localAddr := ""
-	if out, err := combinedOutput(ctx, "docker", "port", cntrName, "80"); err != nil {
+	if out, err := combinedOutput(ctx, "docker", "port", cntrName, cntrPort); err != nil {
 		return "", fmt.Errorf("failed to find container port: %s: %v", out, err)
 	} else {
 		v4, _, found := strings.Cut(string(out), "\n")
@@ -500,8 +512,6 @@
 		return fmt.Errorf("init msg: %w", err)
 	}
 
-	slog.DebugContext(ctx, "/init POST", slog.String("initMsg", string(initMsg)))
-
 	// Note: this /init POST is handled in loop/server/loophttp.go:
 	initMsgByteReader := bytes.NewReader(initMsg)
 	req, err := http.NewRequest("POST", localURL+"/init", initMsgByteReader)
diff --git a/loop/server/sshserver.go b/loop/server/sshserver.go
index 2b1f9e8..56fd679 100644
--- a/loop/server/sshserver.go
+++ b/loop/server/sshserver.go
@@ -1,6 +1,7 @@
 package server
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"io"
@@ -19,17 +20,44 @@
 }
 
 func (s *Server) ServeSSH(ctx context.Context, hostKey, authorizedKeys []byte) error {
-	allowed, _, _, _, err := ssh.ParseAuthorizedKey(authorizedKeys)
-	if err != nil {
-		return fmt.Errorf("ServeSSH: couldn't parse authorized keys: %w", err)
+	// Parse all authorized keys
+	allowedKeys := make([]ssh.PublicKey, 0)
+	rest := authorizedKeys
+	var err error
+
+	// Continue parsing as long as there are bytes left
+	for len(rest) > 0 {
+		var key ssh.PublicKey
+		key, _, _, rest, err = ssh.ParseAuthorizedKey(rest)
+		if err != nil {
+			// If we hit an error, check if we have more lines to try
+			if i := bytes.IndexByte(rest, '\n'); i >= 0 {
+				// Skip to the next line and continue
+				rest = rest[i+1:]
+				continue
+			}
+			// No more lines and we hit an error, so stop parsing
+			break
+		}
+		allowedKeys = append(allowedKeys, key)
 	}
-	return ssh.ListenAndServe(":2022",
+	if len(allowedKeys) == 0 {
+		return fmt.Errorf("ServeSSH: no valid authorized keys found")
+	}
+
+	return ssh.ListenAndServe(":22",
 		func(s ssh.Session) {
 			handleSessionfunc(ctx, s)
 		},
 		ssh.HostKeyPEM(hostKey),
 		ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
-			return ssh.KeysEqual(key, allowed)
+			// Check if the provided key matches any of our allowed keys
+			for _, allowedKey := range allowedKeys {
+				if ssh.KeysEqual(key, allowedKey) {
+					return true
+				}
+			}
+			return false
 		}),
 	)
 }