Compare commits

..

12 Commits

Author SHA1 Message Date
Timmy Welch
7f96d42ce5 Wait for network
Some checks failed
Push / CI (push) Has been cancelled
2025-10-30 19:02:57 -07:00
Timmy Welch
cae3dc71a7 Merge remote-tracking branch 'origin/main' 2025-10-30 19:00:52 -07:00
Timmy Welch
01d6116f7f Fix hostkey being too short fix TERM not being set
Some checks failed
Push / CI (push) Has been cancelled
2025-10-30 01:17:10 -07:00
Timmy Welch
e452de8806 Fix check shell before executing
Some checks failed
Push / CI (push) Has been cancelled
2025-07-12 18:03:48 -07:00
Timmy Welch
88cba45310 Check shell before executing
Some checks failed
Push / CI (push) Has been cancelled
2025-07-12 16:59:32 -07:00
Timmy Welch
67b8cd2449 Enable start on boot
Some checks failed
Push / CI (push) Has been cancelled
2025-07-12 14:35:50 -07:00
Timmy Welch
01a983bea8 Use a login shell if no command is given 2025-07-12 14:34:43 -07:00
dependabot[bot]
04ed8761da Bump golang.org/x/crypto from 0.33.0 to 0.35.0 (#23)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.33.0 to 0.35.0.
- [Commits](https://github.com/golang/crypto/compare/v0.33.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.35.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-12 14:33:27 -07:00
Timmy Welch
f736b48d5d testing
All checks were successful
Push / CI (push) Successful in 12m32s
2025-05-15 14:37:10 -07:00
Timmy Welch
b499ac2afe Merge remote-tracking branch 'upstream/main'
Some checks failed
Push / CI (push) Has been cancelled
2025-03-09 15:45:46 -07:00
Timmy Welch
f4af94e536 Install apps to /bin on startup 2025-03-09 15:37:52 -07:00
Timmy Welch
bba58e7a3a Stuff
Some checks failed
Push / CI (push) Has been cancelled
Implement certificate authentication, certificate requires :gokrazy: principal
Read first line of /etc/passwd for home and shell
Shell uses `-l` to make it a login shell which will run .profile
2025-02-16 17:53:26 -08:00
5 changed files with 199 additions and 93 deletions

View File

@@ -6,16 +6,18 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"crypto/ed25519"
"crypto/rand" "crypto/rand"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors"
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"os" "os"
"path"
"strings" "strings"
"syscall" "syscall"
@@ -31,6 +33,10 @@ var (
"/perm/breakglass.authorized_keys", "/perm/breakglass.authorized_keys",
"path to an OpenSSH authorized_keys file; if the value is 'ec2', fetch the SSH key(s) from the AWS IMDSv2 metadata") "path to an OpenSSH authorized_keys file; if the value is 'ec2', fetch the SSH key(s) from the AWS IMDSv2 metadata")
authorizedUserCAPath = flag.String("authorized_ca",
"/perm/breakglass.authorized_user_ca",
"path to an OpenSSH TrustedUserCAKeys file; note the certificate must list ':gokrazy:' as a valid principal")
hostKeyPath = flag.String("host_key", hostKeyPath = flag.String("host_key",
"/perm/breakglass.host_key", "/perm/breakglass.host_key",
"path to a PEM-encoded RSA, DSA or ECDSA private key (create using e.g. ssh-keygen -f /perm/breakglass.host_key -N '' -t rsa)") "path to a PEM-encoded RSA, DSA or ECDSA private key (create using e.g. ssh-keygen -f /perm/breakglass.host_key -N '' -t rsa)")
@@ -46,6 +52,9 @@ var (
forwarding = flag.String("forward", forwarding = flag.String("forward",
"", "",
"allow port forwarding. Use `loopback` for loopback interfaces and `private-network` for private networks") "allow port forwarding. Use `loopback` for loopback interfaces and `private-network` for private networks")
home = "/perm/home"
shell = ""
) )
func loadAuthorizedKeys(path string) (map[string]bool, error) { func loadAuthorizedKeys(path string) (map[string]bool, error) {
@@ -81,6 +90,19 @@ func loadAuthorizedKeys(path string) (map[string]bool, error) {
return result, nil return result, nil
} }
func loadPasswd(passwd string) {
b, err := os.ReadFile(passwd)
if err != nil {
return
}
fields := bytes.SplitN(bytes.SplitN(b, []byte("\n"), 2)[0], []byte(":"), 7)
if len(fields) != 7 {
return
}
home = path.Clean(string(fields[5]))
shell = path.Clean(string(fields[6]))
}
func loadHostKey(path string) (ssh.Signer, error) { func loadHostKey(path string) (ssh.Signer, error) {
b, err := ioutil.ReadFile(path) b, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
@@ -91,7 +113,7 @@ func loadHostKey(path string) (ssh.Signer, error) {
} }
func createHostKey(path string) (ssh.Signer, error) { func createHostKey(path string) (ssh.Signer, error) {
key, err := rsa.GenerateKey(rand.Reader, 1024) _, key, err := ed25519.GenerateKey(rand.Reader)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -172,7 +194,9 @@ func main() {
flag.Parse() flag.Parse()
log.SetFlags(log.LstdFlags | log.Lshortfile) log.SetFlags(log.LstdFlags | log.Lshortfile)
gokrazy.DontStartOnBoot() installBusybox()
loadPasswd("/etc/passwd")
authorizedKeys, err := loadAuthorizedKeys(*authorizedKeysPath) authorizedKeys, err := loadAuthorizedKeys(*authorizedKeysPath)
if err != nil { if err != nil {
@@ -182,19 +206,59 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
authorizedUserCertificateCA, err := loadAuthorizedKeys(strings.TrimPrefix(*authorizedUserCAPath, "ec2"))
if err != nil {
if os.IsNotExist(err) {
log.Printf("TrustedUserCAKeys not loaded")
}
}
if err := initMOTD(); err != nil { if err := initMOTD(); err != nil {
log.Print(err) log.Print(err)
} }
certChecker := ssh.CertChecker{
config := &ssh.ServerConfig{ IsUserAuthority: func(auth ssh.PublicKey) bool {
PublicKeyCallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { return authorizedUserCertificateCA[string(auth.Marshal())]
},
UserKeyFallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
if authorizedKeys[string(pubKey.Marshal())] { if authorizedKeys[string(pubKey.Marshal())] {
log.Printf("user %q successfully authorized from remote addr %s", conn.User(), conn.RemoteAddr()) log.Printf("user %q successfully authorized from remote addr %s", conn.User(), conn.RemoteAddr())
return nil, nil return &ssh.Permissions{map[string]string{}, map[string]string{}}, nil
} }
return nil, fmt.Errorf("public key not found in %s", *authorizedKeysPath) return nil, fmt.Errorf("public key not found in %s", *authorizedKeysPath)
}, },
} }
config := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
cert, ok := key.(*ssh.Certificate)
if !ok {
if certChecker.UserKeyFallback != nil {
return certChecker.UserKeyFallback(conn, key)
}
return nil, errors.New("ssh: normal key pairs not accepted")
}
if cert.CertType != ssh.UserCert {
return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
}
if !certChecker.IsUserAuthority(cert.SignatureKey) {
return nil, fmt.Errorf("ssh: certificate signed by unrecognized authority")
}
if err := certChecker.CheckCert(":gokrazy:", cert); err != nil {
return nil, err
}
if cert.Permissions.CriticalOptions == nil {
cert.Permissions.CriticalOptions = map[string]string{}
}
if cert.Permissions.Extensions == nil {
cert.Permissions.Extensions = map[string]string{}
}
return &cert.Permissions, nil
},
}
signer, err := loadHostKey(*hostKeyPath) signer, err := loadHostKey(*hostKeyPath)
if err != nil { if err != nil {
@@ -244,7 +308,7 @@ func main() {
} }
go func(conn net.Conn) { go func(conn net.Conn) {
_, chans, reqs, err := ssh.NewServerConn(conn, config) c, chans, reqs, err := ssh.NewServerConn(conn, config)
if err != nil { if err != nil {
log.Printf("handshake: %v", err) log.Printf("handshake: %v", err)
return return
@@ -254,12 +318,13 @@ func main() {
go ssh.DiscardRequests(reqs) go ssh.DiscardRequests(reqs)
for newChannel := range chans { for newChannel := range chans {
handleChannel(newChannel) handleChannel(newChannel, c)
} }
}(conn) }(conn)
} }
} }
gokrazy.WaitFor("net-route")
addrs, err := gokrazy.PrivateInterfaceAddrs() addrs, err := gokrazy.PrivateInterfaceAddrs()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@@ -26,6 +26,7 @@ import (
"github.com/gokrazy/internal/config" "github.com/gokrazy/internal/config"
"github.com/gokrazy/internal/httpclient" "github.com/gokrazy/internal/httpclient"
"github.com/gokrazy/internal/instanceflag" "github.com/gokrazy/internal/instanceflag"
"github.com/gokrazy/internal/tlsflag"
"github.com/gokrazy/internal/updateflag" "github.com/gokrazy/internal/updateflag"
) )
@@ -34,11 +35,9 @@ type bg struct {
cfg *config.Struct cfg *config.Struct
forceRestart bool forceRestart bool
sshConfig string sshConfig string
insecure bool
// state // state
GOARCH string GOARCH string
update updateflag.Value
} }
func (bg *bg) startBreakglass() error { func (bg *bg) startBreakglass() error {
@@ -47,26 +46,35 @@ func (bg *bg) startBreakglass() error {
return err return err
} }
updateHttpClient, _, updateBaseURL, err := httpclient.For(bg.update, bg.cfg) updateHttpClient, foundMatchingCertificate, updateBaseURL, err := httpclient.For(bg.cfg)
if err != nil { if err != nil {
return err return err
} }
updateHttpClient.Jar = jar updateHttpClient.Jar = jar
remoteScheme, err := httpclient.GetRemoteScheme(updateBaseURL)
if remoteScheme == "https" && !tlsflag.Insecure() {
updateBaseURL.Scheme = "https"
updateflag.SetUpdate(updateBaseURL.String())
}
if updateBaseURL.Scheme != "https" && foundMatchingCertificate {
fmt.Printf("\n")
fmt.Printf("!!!WARNING!!! Possible SSL-Stripping detected!\n")
fmt.Printf("Found certificate for hostname in your client configuration but the host does not offer https!\n")
fmt.Printf("\n")
if !tlsflag.Insecure() {
log.Fatalf("update canceled: TLS certificate found, but negotiating a TLS connection with the target failed")
}
fmt.Printf("Proceeding anyway as requested (-insecure).\n")
}
if err != nil {
return err
}
form, err := updateHttpClient.Get(updateBaseURL.String() + "status?path=/user/breakglass") form, err := updateHttpClient.Get(updateBaseURL.String() + "status?path=/user/breakglass")
if err != nil { if err != nil {
if updateBaseURL.Scheme == "https" && bg.insecure {
// Try falling back to HTTP
bg.cfg.Update.UseTLS = "off"
updateHttpClient, _, updateBaseURL, err = httpclient.For(bg.update, bg.cfg)
if err != nil {
return err
}
form, err = updateHttpClient.Get(updateBaseURL.String() + "status?path=/user/breakglass")
if err != nil {
return err
}
}
return err return err
} }
if form.StatusCode == http.StatusNotFound { if form.StatusCode == http.StatusNotFound {
@@ -190,11 +198,6 @@ func breakglass() error {
false, false,
"prepare the SSH connection only, but do not execute SSH (useful for using breakglass within an SSH ProxyCommand)") "prepare the SSH connection only, but do not execute SSH (useful for using breakglass within an SSH ProxyCommand)")
insecure = flag.Bool(
"insecure",
false,
"Fall back to HTTP if HTTPS is configured, but does not work.")
proxy = flag.Bool( proxy = flag.Bool(
"proxy", "proxy",
false, false,
@@ -206,6 +209,13 @@ func breakglass() error {
"an alternative per-user configuration file for ssh and scp") "an alternative per-user configuration file for ssh and scp")
) )
// TODO: remove the -tls and -gokrazy_url flags after 2023-June (half a year
// after the introduction of instance centric config), so that we can then
// merge these flags into tools/internal/oldpacker and remove their global
// state.
tlsflag.RegisterFlags(flag.CommandLine)
updateflag.RegisterFlags(flag.CommandLine, "gokrazy_url")
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0])
@@ -221,6 +231,13 @@ func breakglass() error {
log.Fatalf("syntax: breakglass <hostname> [command]") log.Fatalf("syntax: breakglass <hostname> [command]")
} }
// If the user did not explicitly specify -update=yes, we default to it.
// This differs from the gokr-packer, but breakglass is only useful for
// gokrazy instances that already exist.
if updateflag.NewInstallation() {
updateflag.SetUpdate("yes")
}
instance := flag.Arg(0) instance := flag.Arg(0)
instanceflag.SetInstance(instance) instanceflag.SetInstance(instance)
@@ -238,8 +255,6 @@ func breakglass() error {
cfg: cfg, cfg: cfg,
forceRestart: *forceRestart, forceRestart: *forceRestart,
sshConfig: *sshConfig, sshConfig: *sshConfig,
insecure: *insecure,
update: updateflag.Value{Update: "yes"},
} }
if cfg.Update.Hostname == "" { if cfg.Update.Hostname == "" {
cfg.Update.Hostname = cfg.Hostname cfg.Update.Hostname = cfg.Hostname

21
go.mod
View File

@@ -3,22 +3,25 @@ module github.com/gokrazy/breakglass
go 1.24.0 go 1.24.0
require ( require (
github.com/gokrazy/gokapi v0.0.0-20250222071133-506fdb322775 github.com/gokrazy/gokapi v0.0.0-20250530162407-ee2d0d668321
github.com/gokrazy/gokrazy v0.0.0-20250222061409-bd0bb5f1d0b5 github.com/gokrazy/gokrazy v0.0.0-20250916200752-e39b02bec165
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82 github.com/gokrazy/internal v0.0.0-20250526201501-559979153369
github.com/google/renameio/v2 v2.0.0 github.com/google/renameio/v2 v2.0.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/kr/pty v1.1.8 github.com/kr/pty v1.1.8
github.com/pkg/sftp v1.13.5 github.com/pkg/sftp v1.13.10
golang.org/x/crypto v0.45.0 golang.org/x/crypto v0.43.0
) )
require ( require (
github.com/creack/pty v1.1.18 // indirect github.com/antihax/optional v1.0.0 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/kenshaw/evdev v0.1.0 // indirect github.com/kenshaw/evdev v0.1.0 // indirect
github.com/kr/fs v0.1.0 // indirect github.com/kr/fs v0.1.0 // indirect
github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5 // indirect github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.10 // indirect
golang.org/x/oauth2 v0.27.0 // indirect github.com/vishvananda/netlink v1.3.1 // indirect
golang.org/x/sys v0.38.0 // indirect github.com/vishvananda/netns v0.0.5 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sys v0.37.0 // indirect
) )

71
go.sum
View File

@@ -1,14 +1,16 @@
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gokrazy/gokapi v0.0.0-20250222071133-506fdb322775 h1:f5+2UMRRbr3+e/gdWCBNn48chS/KMMljfbmlSSHfRBA= github.com/gokrazy/gokapi v0.0.0-20250530162407-ee2d0d668321 h1:WIkgE9uiyZw1/YzywulZQP2o2YfrFWV/BBbshhcWY4Y=
github.com/gokrazy/gokapi v0.0.0-20250222071133-506fdb322775/go.mod h1:q9mIV8al0wqmqFXJhKiO3SOHkL9/7Q4kIMynqUQWhgU= github.com/gokrazy/gokapi v0.0.0-20250530162407-ee2d0d668321/go.mod h1:rVItujrJo0NpYZhFR5dYdzLDqMoMCtjEZkdxoCRDo+o=
github.com/gokrazy/gokrazy v0.0.0-20250222061409-bd0bb5f1d0b5 h1:VQhDGxRliP4ZTQ8+33v4VKtOpX4VzN8pA4zBMZQSSxs= github.com/gokrazy/gokrazy v0.0.0-20250916200752-e39b02bec165 h1:WfWsmxzNJZFSO8mE3pGbWzaUtcfNFjqp1BMrHAYuKg8=
github.com/gokrazy/gokrazy v0.0.0-20250222061409-bd0bb5f1d0b5/go.mod h1:6fAh0J7aH6o5HWSiwN6uxNlm6Rjx1BxeNMWyNBQZ6sI= github.com/gokrazy/gokrazy v0.0.0-20250916200752-e39b02bec165/go.mod h1:0UPC4KuPLfHR3WMaqGrhT6Y90s6QkzI2g8bzLtpCneE=
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82 h1:4ghNfD9NaZLpFrqQiBF6mPVFeMYXJSky38ubVA4ic2E= github.com/gokrazy/internal v0.0.0-20250526201501-559979153369 h1:aNni2iPwJbowfHW1SFapKLfY+ZPUIcBfFrJvYPAh3p4=
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82/go.mod h1:dQY4EMkD4L5ZjYJ0SPtpgYbV7MIUMCxNIXiOfnZ6jP4= github.com/gokrazy/internal v0.0.0-20250526201501-559979153369/go.mod h1:dQY4EMkD4L5ZjYJ0SPtpgYbV7MIUMCxNIXiOfnZ6jP4=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
@@ -23,32 +25,27 @@ github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5 h1:80FAK3TW5lVymfHu3kvB1QvTZvy9Kmx1lx6sT5Ep16s= github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5 h1:80FAK3TW5lVymfHu3kvB1QvTZvy9Kmx1lx6sT5Ep16s=
github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5/go.mod h1:z0QjVpjpK4jksEkffQwS3+abQ3XFTm1bnimyDzWyUk0= github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5/go.mod h1:z0QjVpjpK4jksEkffQwS3+abQ3XFTm1bnimyDzWyUk0=
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

54
ssh.go
View File

@@ -2,16 +2,19 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
"net" "net"
"os" "os"
"os/exec" "os/exec"
"path"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"time"
"unsafe" "unsafe"
"github.com/gokrazy/gokrazy" "github.com/gokrazy/gokrazy"
@@ -21,11 +24,15 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
func handleChannel(newChan ssh.NewChannel) { func handleChannel(newChan ssh.NewChannel, conn *ssh.ServerConn) {
switch t := newChan.ChannelType(); t { switch t := newChan.ChannelType(); t {
case "session": case "session":
handleSession(newChan) handleSession(newChan, conn)
case "direct-tcpip": case "direct-tcpip":
if _, portForwardDenied := conn.Permissions.Extensions["no-port-forwarding"]; portForwardDenied {
newChan.Reject(ssh.Prohibited, "port forwarding is disabled. For you in particular :-P")
return
}
handleTCPIP(newChan) handleTCPIP(newChan)
default: default:
newChan.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %q", t)) newChan.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %q", t))
@@ -112,7 +119,7 @@ func handleTCPIP(newChan ssh.NewChannel) {
}() }()
} }
func handleSession(newChannel ssh.NewChannel) { func handleSession(newChannel ssh.NewChannel, conn *ssh.ServerConn) {
channel, requests, err := newChannel.Accept() channel, requests, err := newChannel.Accept()
if err != nil { if err != nil {
log.Printf("Could not accept channel (%s)", err) log.Printf("Could not accept channel (%s)", err)
@@ -120,12 +127,12 @@ func handleSession(newChannel ssh.NewChannel) {
} }
// Sessions have out-of-band requests such as "shell", "pty-req" and "env" // Sessions have out-of-band requests such as "shell", "pty-req" and "env"
go func(channel ssh.Channel, requests <-chan *ssh.Request) { go func(channel ssh.Channel, requests <-chan *ssh.Request, conn *ssh.ServerConn) {
ctx, canc := context.WithCancel(context.Background()) ctx, canc := context.WithCancel(context.Background())
defer canc() defer canc()
s := session{channel: channel} s := session{channel: channel}
for req := range requests { for req := range requests {
if err := s.request(ctx, req); err != nil { if err := s.request(ctx, req, conn); err != nil {
log.Printf("request(%q): %v", req.Type, err) log.Printf("request(%q): %v", req.Type, err)
errmsg := []byte(err.Error()) errmsg := []byte(err.Error())
// Append a trailing newline; the error message is // Append a trailing newline; the error message is
@@ -139,7 +146,7 @@ func handleSession(newChannel ssh.NewChannel) {
} }
} }
log.Printf("requests exhausted") log.Printf("requests exhausted")
}(channel, requests) }(channel, requests, conn)
} }
func expandPath(env []string) []string { func expandPath(env []string) []string {
@@ -214,30 +221,45 @@ type exitStatus struct {
Status uint32 Status uint32
} }
func shellWorks(shell string) bool {
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
cmd := exec.CommandContext(ctx, shell, "-c", "exit 58")
cmd.Run()
return cmd.ProcessState != nil && cmd.ProcessState.ExitCode() == 58
}
func findShell() string { func findShell() string {
if _, err := os.Stat(wellKnownBusybox); err == nil { if _, err := os.Stat(wellKnownBusybox); err == nil {
// Install busybox to /bin to provide the typical userspace utilities // Install busybox to /bin to provide the typical userspace utilities
// in standard locations (makes Emacs TRAMP work, for example). // in standard locations (makes Emacs TRAMP work, for example).
if err := installBusybox(); err != nil { if err := installBusybox(); err != nil {
log.Printf("installing busybox failed: %v", err) log.Printf("installing busybox failed: %v", err)
// fallthrough // fallthrough, we don't return /bin/sh as we read /etc/passwd
} else {
return "/bin/sh" // available after installation
} }
} }
if path, err := exec.LookPath("sh"); err == nil { if _, err := exec.LookPath(shell); path.IsAbs(shell) && shellWorks(shell) && err == nil {
return shell
}
if path, err := exec.LookPath("bash"); shellWorks(path) && err == nil {
return path
}
if path, err := exec.LookPath("sh"); shellWorks(path) && err == nil {
return path return path
} }
const wellKnownSerialShell = "/tmp/serial-busybox/ash" const wellKnownSerialShell = "/tmp/serial-busybox/ash"
if _, err := os.Stat(wellKnownSerialShell); err == nil { if _, err := exec.LookPath(wellKnownSerialShell); err == nil {
return wellKnownSerialShell return wellKnownSerialShell
} }
return "" return ""
} }
func (s *session) request(ctx context.Context, req *ssh.Request) error { func (s *session) request(ctx context.Context, req *ssh.Request, conn *ssh.ServerConn) error {
switch req.Type { switch req.Type {
case "pty-req": case "pty-req":
if _, portForwardDenied := conn.Permissions.Extensions["no-pty"]; portForwardDenied {
return errors.New("Pseudo-Terminal is disabled. For you in particular :-P")
}
var r ptyreq var r ptyreq
if err := ssh.Unmarshal(req.Payload, &r); err != nil { if err := ssh.Unmarshal(req.Payload, &r); err != nil {
return err return err
@@ -358,21 +380,25 @@ func (s *session) request(ctx context.Context, req *ssh.Request) error {
// Ensure the $HOME directory exists so that shell history works without // Ensure the $HOME directory exists so that shell history works without
// any extra steps. // any extra steps.
if err := os.MkdirAll("/perm/home", 0755); err != nil { if err := os.MkdirAll(home, 0755); err != nil {
// TODO: Suppress -EROFS // TODO: Suppress -EROFS
log.Print(err) log.Print(err)
} }
var cmd *exec.Cmd var cmd *exec.Cmd
if shell := findShell(); shell != "" { if shell := findShell(); shell != "" {
if len(cmdline) == 0 || (len(cmdline) == 1 && cmdline[0] == "sh") {
cmd = exec.CommandContext(ctx, shell, "-l")
} else {
cmd = exec.CommandContext(ctx, shell, "-c", r.Command) cmd = exec.CommandContext(ctx, shell, "-c", r.Command)
}
} else { } else {
cmd = exec.CommandContext(ctx, cmdline[0], cmdline[1:]...) cmd = exec.CommandContext(ctx, cmdline[0], cmdline[1:]...)
} }
log.Printf("Starting cmd %q", cmd.Args) log.Printf("Starting cmd %q", cmd.Args)
env := expandPath(s.env) env := expandPath(s.env)
env = append(env, env = append(env,
"HOME=/perm/home", "HOME="+home,
"TMPDIR=/tmp") "TMPDIR=/tmp")
cmd.Env = env cmd.Env = env
cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr = &syscall.SysProcAttr{}