diff --git a/go/nix/bnix/bnix.go b/go/nix/bnix/bnix.go index 9c26fa334a..7542daa5a5 100644 --- a/go/nix/bnix/bnix.go +++ b/go/nix/bnix/bnix.go @@ -4,18 +4,25 @@ import ( "context" "flag" "fmt" + "io" "log" + "net/url" "os" + "golang.org/x/crypto/ssh" "hg.lukegb.com/lukegb/depot/go/nix/nixdrv" "hg.lukegb.com/lukegb/depot/go/nix/nixstore" ) +var ( + remoteFlag = flag.String("remote", "unix://", "Remote.") +) + func ensure(ctx context.Context, d *nixstore.Daemon, at *nixstore.ActivityTracker, path string) error { return d.EnsurePath(at, path) } -var rs nixdrv.LocalFSResolver +var rs nixdrv.Resolver func loadDerivation(ctx context.Context, path string) (*nixdrv.Derivation, error) { return rs.LoadDerivation(path) @@ -40,6 +47,115 @@ func buildDerivation(ctx context.Context, d *nixstore.Daemon, at *nixstore.Activ return nil } +func connectTo(ctx context.Context, remote string) (*nixstore.Daemon, nixdrv.Resolver, error) { + u, err := url.Parse(remote) + if err != nil { + return nil, nil, fmt.Errorf("parsing remote %q as URL: %w", remote, err) + } + + switch u.Scheme { + case "unix": + if u.Path == "" { + u.Path = nixstore.DaemonSock + } + d, err := nixstore.OpenDaemon(u.Path) + if err != nil { + return nil, nil, err + } + return d, nixdrv.LocalFSResolver{}, nil + case "ssh-ng": + // Construct a ClientConfig from the URL. + cfg := &ssh.ClientConfig{} + if u.Query().Has("privkey") { + var keys []ssh.Signer + for _, privkeyPath := range u.Query()["privkey"] { + privkeyF, err := os.Open(privkeyPath) + if err != nil { + return nil, nil, fmt.Errorf("opening privkey %q: %w", privkeyPath, err) + } + defer privkeyF.Close() + privkeyB, err := io.ReadAll(privkeyF) + if err != nil { + return nil, nil, fmt.Errorf("reading privkey %q: %w", privkeyPath, err) + } + + privkey, err := ssh.ParsePrivateKey(privkeyB) + if err != nil { + return nil, nil, fmt.Errorf("parsing privkey %q: %w", privkeyPath, err) + } + keys = append(keys, privkey) + } + cfg.Auth = append(cfg.Auth, ssh.PublicKeys(keys...)) + } + if u.User != nil { + cfg.User = u.User.Username() + if pw, ok := u.User.Password(); ok { + cfg.Auth = append(cfg.Auth, ssh.Password(pw)) + } + } + switch { + case u.Query().Has("host-key"): + hkStr := u.Query().Get("host-key") + _, _, hk, _, _, err := ssh.ParseKnownHosts(append([]byte("x "), []byte(hkStr)...)) + if err != nil { + return nil, nil, fmt.Errorf("parsing host-key %q: %w", hkStr, err) + } + cfg.HostKeyCallback = ssh.FixedHostKey(hk) + case u.Query().Has("insecure-allow-any-ssh-host-key"): + cfg.HostKeyCallback = ssh.InsecureIgnoreHostKey() + default: + return nil, nil, fmt.Errorf("some SSH host key configuration is required (?host-key=; ?insecure-allow-any-ssh-host-key)") + } + + // Work out other misc parameters. + // ...remote command. + remoteCmd := "nix-daemon --stdio" + if u.Query().Has("remote-cmd") { + remoteCmd = u.Query().Get("remote-cmd") + } + + // Work out the host:port to connect to. + remote := u.Hostname() + if portStr := u.Port(); portStr != "" { + remote = remote + ":" + portStr + } else { + remote = remote + ":22" + } + + conn, err := ssh.Dial("tcp", remote, cfg) + if err != nil { + return nil, nil, fmt.Errorf("dialing %v via SSH: %w", remote, err) + } + sess, err := conn.NewSession() + if err != nil { + conn.Close() + return nil, nil, fmt.Errorf("opening SSH session to %v: %w", remote, err) + } + stdin, err := sess.StdinPipe() + if err != nil { + conn.Close() + return nil, nil, fmt.Errorf("opening stdin pipe: %w", err) + } + stdout, err := sess.StdoutPipe() + if err != nil { + conn.Close() + return nil, nil, fmt.Errorf("opening stdout pipe: %w", err) + } + if err := sess.Start(remoteCmd); err != nil { + conn.Close() + return nil, nil, fmt.Errorf("starting %q: %w", remoteCmd, err) + } + d, err := nixstore.OpenDaemonWithIOs(stdout, stdin, conn) + if err != nil { + conn.Close() + return nil, nil, fmt.Errorf("establishing connection to daemon: %w", err) + } + return d, nixdrv.LocalFSResolver{}, nil // TODO: change from LocalFSResolver? + default: + return nil, nil, fmt.Errorf("unknown remote %q", remote) + } +} + func main() { flag.Parse() @@ -87,13 +203,14 @@ func main() { at := nixstore.NewActivityTracker() - d, err := nixstore.OpenDaemon(nixstore.DaemonSock) + ctx := context.Background() + d, rss, err := connectTo(ctx, *remoteFlag) if err != nil { - log.Fatalf("OpenDaemon: %v", err) + log.Fatalf("connectTo(%q): %v", *remoteFlag, err) } defer d.Close() + rs = rss - ctx := context.Background() if err := cmd(ctx, d, at); err != nil { log.Fatalf("%s: %s", flag.Arg(0), err) } diff --git a/go/nix/default.nix b/go/nix/default.nix index 4ca56894fc..dc2a79a382 100644 --- a/go/nix/default.nix +++ b/go/nix/default.nix @@ -5,6 +5,7 @@ args: { nar = import ./nar args; + nixdrv = import ./nixdrv args; nixstore = import ./nixstore args; nixwire = import ./nixwire args; bcachegc = import ./bcachegc args; diff --git a/go/nix/nixdrv/default.nix b/go/nix/nixdrv/default.nix new file mode 100644 index 0000000000..eb4cfb4f99 --- /dev/null +++ b/go/nix/nixdrv/default.nix @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2023 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }: +depot.third_party.buildGo.package { + name = "nixdrv"; + path = "hg.lukegb.com/lukegb/depot/go/nix/nixdrv"; + srcs = [ + ./nixdrv.go + ./localfs.go + ]; + deps = with depot; [ + ]; +} diff --git a/go/nix/nixstore/default.nix b/go/nix/nixstore/default.nix index 8e4ec1c48f..29882edc28 100644 --- a/go/nix/nixstore/default.nix +++ b/go/nix/nixstore/default.nix @@ -7,6 +7,7 @@ depot.third_party.buildGo.package { name = "nixstore"; path = "hg.lukegb.com/lukegb/depot/go/nix/nixstore"; srcs = [ + ./activities.go ./nixstore.go ./remotestore.go ./sqlitestore.go diff --git a/go/nix/nixstore/remotestore.go b/go/nix/nixstore/remotestore.go index 1904c7aa60..46e42c406a 100644 --- a/go/nix/nixstore/remotestore.go +++ b/go/nix/nixstore/remotestore.go @@ -29,7 +29,7 @@ const ( ) type Daemon struct { - conn net.Conn + conn io.Closer w *nixwire.Serializer r *nixwire.Deserializer mu sync.Mutex @@ -453,6 +453,15 @@ func OpenDaemon(path string) (*Daemon, error) { return nil, fmt.Errorf("sending hello to %v: %w", path, err) } - // /nix/var/nix/daemon-socket/socket + return d, nil +} + +func OpenDaemonWithIOs(r io.Reader, w io.Writer, c io.Closer) (*Daemon, error) { + d := &Daemon{conn: c, w: &nixwire.Serializer{w}, r: &nixwire.Deserializer{r}} + if err := d.hello(); err != nil { + d.Close() + return nil, fmt.Errorf("sending hello: %w", err) + } + return d, nil } diff --git a/go/nix/nixwire/default.nix b/go/nix/nixwire/default.nix index c4a76ee39c..198868a015 100644 --- a/go/nix/nixwire/default.nix +++ b/go/nix/nixwire/default.nix @@ -9,4 +9,7 @@ depot.third_party.buildGo.package { srcs = [ ./nixwire.go ]; + deps = with depot; [ + go.nix.nixdrv + ]; }