From a837312cc7a824fc333f50ef501bde5e7ba8238f Mon Sep 17 00:00:00 2001 From: Rolf Neugebauer Date: Mon, 7 Aug 2017 11:18:20 +0100 Subject: [PATCH 1/7] cmd: Switch packet.net API to github.com/bzub/packngo This fork has initial support for always-pxe and was suggested to use be used as an interim solution here: https://github.com/packethost/packngo/issues/22#issuecomment-319973502 Signed-off-by: Rolf Neugebauer --- src/cmd/linuxkit/run_packet.go | 2 +- src/cmd/linuxkit/vendor.conf | 2 +- .../{packethost => bzub}/packngo/LICENSE.txt | 0 .../{packethost => bzub}/packngo/README.md | 0 .../{packethost => bzub}/packngo/devices.go | 50 ++++++++++--------- .../{packethost => bzub}/packngo/email.go | 0 .../packngo/facilities.go | 0 .../{packethost => bzub}/packngo/ip.go | 0 .../packngo/operatingsystems.go | 0 .../{packethost => bzub}/packngo/packngo.go | 0 .../{packethost => bzub}/packngo/plans.go | 0 .../{packethost => bzub}/packngo/projects.go | 0 .../{packethost => bzub}/packngo/rate.go | 0 .../{packethost => bzub}/packngo/sshkeys.go | 0 .../{packethost => bzub}/packngo/timestamp.go | 0 .../{packethost => bzub}/packngo/user.go | 0 .../{packethost => bzub}/packngo/utils.go | 0 .../{packethost => bzub}/packngo/volumes.go | 0 18 files changed, 29 insertions(+), 25 deletions(-) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/LICENSE.txt (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/README.md (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/devices.go (76%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/email.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/facilities.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/ip.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/operatingsystems.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/packngo.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/plans.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/projects.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/rate.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/sshkeys.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/timestamp.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/user.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/utils.go (100%) rename src/cmd/linuxkit/vendor/github.com/{packethost => bzub}/packngo/volumes.go (100%) diff --git a/src/cmd/linuxkit/run_packet.go b/src/cmd/linuxkit/run_packet.go index 740b8f988..00cec1991 100644 --- a/src/cmd/linuxkit/run_packet.go +++ b/src/cmd/linuxkit/run_packet.go @@ -8,7 +8,7 @@ import ( "os" "path/filepath" - "github.com/packethost/packngo" + "github.com/bzub/packngo" // TODO(rn): Update to official once iPXE is merged log "github.com/sirupsen/logrus" ) diff --git a/src/cmd/linuxkit/vendor.conf b/src/cmd/linuxkit/vendor.conf index a8bcbd571..86941c2d7 100644 --- a/src/cmd/linuxkit/vendor.conf +++ b/src/cmd/linuxkit/vendor.conf @@ -1,6 +1,7 @@ github.com/Azure/azure-sdk-for-go 26132835cbefa2669a306b777f34b929b56aa0a2 github.com/Azure/go-ansiterm 19f72df4d05d31cbe1c56bfc8045c96babff6c7e github.com/Azure/go-autorest 58f6f26e200fa5dfb40c9cd1c83f3e2c860d779d +github.com/bzub/packngo 087cff23a860cd69a8c4818c8f6411bd47f6ab96 github.com/Microsoft/go-winio f533f7a102197536779ea3a8cb881d639e21ec5a github.com/aws/aws-sdk-go fa107560b5f3528a859a1a1511086646731bb1a8 github.com/dgrijalva/jwt-go 6c8dedd55f8a2e41f605de6d5d66e51ed1f299fc @@ -12,7 +13,6 @@ github.com/gophercloud/gophercloud 2804b72cf099b41d2e25c8afcca786f9f962ddee github.com/jmespath/go-jmespath bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d github.com/mitchellh/go-ps 4fdf99ab29366514c69ccccddab5dc58b8d84062 github.com/moby/hyperkit a82b409a87f12fa3306813410c37f4eed270efac -github.com/packethost/packngo 91d54000aa56874149d348a884ba083c41d38091 github.com/radu-matei/azure-sdk-for-go 3b12823551999669c9a325a32472508e0af7978e github.com/radu-matei/azure-vhd-utils e52754d5569d2a643a7775f72ff2a6cf524f4c25 github.com/rn/iso9660wrap 4606f848a055435cdef85305960b0e1bb788d506 diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/LICENSE.txt b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/LICENSE.txt similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/LICENSE.txt rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/LICENSE.txt diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/README.md b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/README.md similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/README.md rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/README.md diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/devices.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/devices.go similarity index 76% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/devices.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/devices.go index f411bf65c..a6552d5d5 100644 --- a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/devices.go +++ b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/devices.go @@ -23,21 +23,23 @@ type devicesRoot struct { // Device represents a Packet device type Device struct { - ID string `json:"id"` - Href string `json:"href,omitempty"` - Hostname string `json:"hostname,omitempty"` - State string `json:"state,omitempty"` - Created string `json:"created_at,omitempty"` - Updated string `json:"updated_at,omitempty"` - Locked bool `json:"locked,omitempty"` - BillingCycle string `json:"billing_cycle,omitempty"` - Tags []string `json:"tags,omitempty"` - Network []*IPAddress `json:"ip_addresses"` - OS *OS `json:"operating_system,omitempty"` - Plan *Plan `json:"plan,omitempty"` - Facility *Facility `json:"facility,omitempty"` - Project *Project `json:"project,omitempty"` - ProvisionPer float32 `json:"provisioning_percentage,omitempty"` + ID string `json:"id"` + Href string `json:"href,omitempty"` + Hostname string `json:"hostname,omitempty"` + State string `json:"state,omitempty"` + Created string `json:"created_at,omitempty"` + Updated string `json:"updated_at,omitempty"` + Locked bool `json:"locked,omitempty"` + BillingCycle string `json:"billing_cycle,omitempty"` + Tags []string `json:"tags,omitempty"` + Network []*IPAddress `json:"ip_addresses"` + OS *OS `json:"operating_system,omitempty"` + Plan *Plan `json:"plan,omitempty"` + Facility *Facility `json:"facility,omitempty"` + Project *Project `json:"project,omitempty"` + ProvisionPer float32 `json:"provisioning_percentage,omitempty"` + IPXEScriptUrl string `json:"ipxe_script_url,omitempty"` + AlwaysPXE bool `json:"always_pxe,omitempty"` } func (d Device) String() string { @@ -46,14 +48,16 @@ func (d Device) String() string { // DeviceCreateRequest type used to create a Packet device type DeviceCreateRequest struct { - HostName string `json:"hostname"` - Plan string `json:"plan"` - Facility string `json:"facility"` - OS string `json:"operating_system"` - BillingCycle string `json:"billing_cycle"` - ProjectID string `json:"project_id"` - UserData string `json:"userdata"` - Tags []string `json:"tags"` + HostName string `json:"hostname"` + Plan string `json:"plan"` + Facility string `json:"facility"` + OS string `json:"operating_system"` + BillingCycle string `json:"billing_cycle"` + ProjectID string `json:"project_id"` + UserData string `json:"userdata"` + Tags []string `json:"tags"` + IPXEScriptUrl string `json:"ipxe_script_url,omitempty"` + AlwaysPXE bool `json:"always_pxe,omitempty"` } func (d DeviceCreateRequest) String() string { diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/email.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/email.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/email.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/email.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/facilities.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/facilities.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/facilities.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/facilities.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/ip.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/ip.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/ip.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/ip.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/operatingsystems.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/operatingsystems.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/operatingsystems.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/operatingsystems.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/packngo.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/packngo.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/packngo.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/packngo.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/plans.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/plans.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/plans.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/plans.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/projects.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/projects.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/projects.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/projects.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/rate.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/rate.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/rate.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/rate.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/sshkeys.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/sshkeys.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/sshkeys.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/sshkeys.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/timestamp.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/timestamp.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/timestamp.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/timestamp.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/user.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/user.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/user.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/user.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/utils.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/utils.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/utils.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/utils.go diff --git a/src/cmd/linuxkit/vendor/github.com/packethost/packngo/volumes.go b/src/cmd/linuxkit/vendor/github.com/bzub/packngo/volumes.go similarity index 100% rename from src/cmd/linuxkit/vendor/github.com/packethost/packngo/volumes.go rename to src/cmd/linuxkit/vendor/github.com/bzub/packngo/volumes.go From 71a4cd2f3c8e201481ca080729a47ec45217f2df Mon Sep 17 00:00:00 2001 From: Rolf Neugebauer Date: Mon, 7 Aug 2017 11:28:27 +0100 Subject: [PATCH 2/7] cmd: Prefix packet.net host name with username If a packet account is shared between users, this makes it clearer who created the machine. Also, switch "moby" to "linuxkit" Signed-off-by: Rolf Neugebauer --- src/cmd/linuxkit/run_packet.go | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/cmd/linuxkit/run_packet.go b/src/cmd/linuxkit/run_packet.go index 00cec1991..ecd112a8b 100644 --- a/src/cmd/linuxkit/run_packet.go +++ b/src/cmd/linuxkit/run_packet.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "os" + "os/user" "path/filepath" "github.com/bzub/packngo" // TODO(rn): Update to official once iPXE is merged @@ -13,18 +14,28 @@ import ( ) const ( - packetDefaultZone = "ams1" - packetDefaultMachine = "baremetal_0" - packetDefaultHostname = "moby" - packetBaseURL = "PACKET_BASE_URL" - packetZoneVar = "PACKET_ZONE" - packetMachineVar = "PACKET_MACHINE" - packetAPIKeyVar = "PACKET_API_KEY" - packetProjectIDVar = "PACKET_PROJECT_ID" - packetHostnameVar = "PACKET_HOSTNAME" - packetNameVar = "PACKET_NAME" + packetDefaultZone = "ams1" + packetDefaultMachine = "baremetal_0" + packetBaseURL = "PACKET_BASE_URL" + packetZoneVar = "PACKET_ZONE" + packetMachineVar = "PACKET_MACHINE" + packetAPIKeyVar = "PACKET_API_KEY" + packetProjectIDVar = "PACKET_PROJECT_ID" + packetHostnameVar = "PACKET_HOSTNAME" + packetNameVar = "PACKET_NAME" ) +var ( + packetDefaultHostname = "linuxkit" +) + +func init() { + // Prefix host name with username + if u, err := user.Current(); err == nil { + packetDefaultHostname = u.Username + "-" + packetDefaultHostname + } +} + // ValidateHTTPURL does a sanity check that a URL returns a 200 or 300 response func ValidateHTTPURL(url string) { log.Printf("Validating URL: %s", url) From bdc06ee30af8ab56c7e4c7febf5f857e46c02f15 Mon Sep 17 00:00:00 2001 From: Rolf Neugebauer Date: Mon, 7 Aug 2017 12:02:34 +0100 Subject: [PATCH 3/7] cmd: Add packet.net env variable names to help message Signed-off-by: Rolf Neugebauer --- src/cmd/linuxkit/run_packet.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cmd/linuxkit/run_packet.go b/src/cmd/linuxkit/run_packet.go index ecd112a8b..b63afd45d 100644 --- a/src/cmd/linuxkit/run_packet.go +++ b/src/cmd/linuxkit/run_packet.go @@ -58,13 +58,13 @@ func runPacket(args []string) { fmt.Printf("Options:\n\n") flags.PrintDefaults() } - baseURLFlag := flags.String("base-url", "", "Base URL that the kernel and initrd are served from.") - zoneFlag := flags.String("zone", packetDefaultZone, "Packet Zone") - machineFlag := flags.String("machine", packetDefaultMachine, "Packet Machine Type") - apiKeyFlag := flags.String("api-key", "", "Packet API key") - projectFlag := flags.String("project-id", "", "Packet Project ID") - hostNameFlag := flags.String("hostname", packetDefaultHostname, "Hostname of new instance") - nameFlag := flags.String("img-name", "", "Overrides the prefix used to identify the files. Defaults to [name]") + baseURLFlag := flags.String("base-url", "", "Base URL that the kernel and initrd are served from (or "+packetBaseURL+")") + zoneFlag := flags.String("zone", packetDefaultZone, "Packet Zone (or "+packetZoneVar+")") + machineFlag := flags.String("machine", packetDefaultMachine, "Packet Machine Type (or "+packetMachineVar+")") + apiKeyFlag := flags.String("api-key", "", "Packet API key (or "+packetAPIKeyVar+")") + projectFlag := flags.String("project-id", "", "Packet Project ID (or "+packetProjectIDVar+")") + hostNameFlag := flags.String("hostname", packetDefaultHostname, "Hostname of new instance (or "+packetHostnameVar+")") + nameFlag := flags.String("img-name", "", "Overrides the prefix used to identify the files. Defaults to [name] (or "+packetNameVar+")") if err := flags.Parse(args); err != nil { log.Fatal("Unable to parse args") } From c804ff0dc12d9a2220c7e8c53ec508c5e12b7834 Mon Sep 17 00:00:00 2001 From: Rolf Neugebauer Date: Mon, 7 Aug 2017 13:46:29 +0100 Subject: [PATCH 4/7] cmd: Enable always-pxe for packet.net This option configures the machine to always PXE boot. By default it would only PXE boot for the first time. Signed-off-by: Rolf Neugebauer --- src/cmd/linuxkit/run_packet.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cmd/linuxkit/run_packet.go b/src/cmd/linuxkit/run_packet.go index b63afd45d..fd980b535 100644 --- a/src/cmd/linuxkit/run_packet.go +++ b/src/cmd/linuxkit/run_packet.go @@ -65,6 +65,7 @@ func runPacket(args []string) { projectFlag := flags.String("project-id", "", "Packet Project ID (or "+packetProjectIDVar+")") hostNameFlag := flags.String("hostname", packetDefaultHostname, "Hostname of new instance (or "+packetHostnameVar+")") nameFlag := flags.String("img-name", "", "Overrides the prefix used to identify the files. Defaults to [name] (or "+packetNameVar+")") + alwaysPXE := flags.Bool("always-pxe", true, "Reboot from PXE every time.") if err := flags.Parse(args); err != nil { log.Fatal("Unable to parse args") } @@ -93,6 +94,7 @@ func runPacket(args []string) { name := getStringValue(packetNameVar, *nameFlag, prefix) osType := "custom_ipxe" billing := "hourly" + // TODO(rn): Extract the kernel commandline from the file generated by moby build userData := fmt.Sprintf("#!ipxe\n\ndhcp\nset base-url %s\nset kernel-params ip=dhcp nomodeset ro serial console=ttyS1,115200\nkernel ${base-url}/%s-kernel ${kernel-params}\ninitrd ${base-url}/%s-initrd.img\nboot", url, name, name) log.Debugf("Using userData of:\n%s\n", userData) initrdURL := fmt.Sprintf("%s/%s-initrd.img", url, name) @@ -110,6 +112,7 @@ func runPacket(args []string) { ProjectID: projectID, UserData: userData, Tags: tags, + AlwaysPXE: *alwaysPXE, } d, _, err := client.Devices.Create(&req) if err != nil { From 148b51347fa3a6c7130480fb9500fa5712ec664c Mon Sep 17 00:00:00 2001 From: Rolf Neugebauer Date: Mon, 7 Aug 2017 18:12:02 +0100 Subject: [PATCH 5/7] vendor: add ssh agent package This is needed for ssh into the serial console on packet.net Signed-off-by: Rolf Neugebauer --- .../golang.org/x/crypto/ssh/agent/client.go | 659 ++++++++++++++++++ .../golang.org/x/crypto/ssh/agent/forward.go | 103 +++ .../golang.org/x/crypto/ssh/agent/keyring.go | 215 ++++++ .../golang.org/x/crypto/ssh/agent/server.go | 451 ++++++++++++ 4 files changed, 1428 insertions(+) create mode 100644 src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/client.go create mode 100644 src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/forward.go create mode 100644 src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/keyring.go create mode 100644 src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/server.go diff --git a/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/client.go b/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/client.go new file mode 100644 index 000000000..ecfd7c58d --- /dev/null +++ b/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/client.go @@ -0,0 +1,659 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package agent implements the ssh-agent protocol, and provides both +// a client and a server. The client can talk to a standard ssh-agent +// that uses UNIX sockets, and one could implement an alternative +// ssh-agent process using the sample server. +// +// References: +// [PROTOCOL.agent]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent?rev=HEAD +package agent // import "golang.org/x/crypto/ssh/agent" + +import ( + "bytes" + "crypto/dsa" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "io" + "math/big" + "sync" + + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/ssh" +) + +// Agent represents the capabilities of an ssh-agent. +type Agent interface { + // List returns the identities known to the agent. + List() ([]*Key, error) + + // Sign has the agent sign the data using a protocol 2 key as defined + // in [PROTOCOL.agent] section 2.6.2. + Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) + + // Add adds a private key to the agent. + Add(key AddedKey) error + + // Remove removes all identities with the given public key. + Remove(key ssh.PublicKey) error + + // RemoveAll removes all identities. + RemoveAll() error + + // Lock locks the agent. Sign and Remove will fail, and List will empty an empty list. + Lock(passphrase []byte) error + + // Unlock undoes the effect of Lock + Unlock(passphrase []byte) error + + // Signers returns signers for all the known keys. + Signers() ([]ssh.Signer, error) +} + +// AddedKey describes an SSH key to be added to an Agent. +type AddedKey struct { + // PrivateKey must be a *rsa.PrivateKey, *dsa.PrivateKey or + // *ecdsa.PrivateKey, which will be inserted into the agent. + PrivateKey interface{} + // Certificate, if not nil, is communicated to the agent and will be + // stored with the key. + Certificate *ssh.Certificate + // Comment is an optional, free-form string. + Comment string + // LifetimeSecs, if not zero, is the number of seconds that the + // agent will store the key for. + LifetimeSecs uint32 + // ConfirmBeforeUse, if true, requests that the agent confirm with the + // user before each use of this key. + ConfirmBeforeUse bool +} + +// See [PROTOCOL.agent], section 3. +const ( + agentRequestV1Identities = 1 + agentRemoveAllV1Identities = 9 + + // 3.2 Requests from client to agent for protocol 2 key operations + agentAddIdentity = 17 + agentRemoveIdentity = 18 + agentRemoveAllIdentities = 19 + agentAddIdConstrained = 25 + + // 3.3 Key-type independent requests from client to agent + agentAddSmartcardKey = 20 + agentRemoveSmartcardKey = 21 + agentLock = 22 + agentUnlock = 23 + agentAddSmartcardKeyConstrained = 26 + + // 3.7 Key constraint identifiers + agentConstrainLifetime = 1 + agentConstrainConfirm = 2 +) + +// maxAgentResponseBytes is the maximum agent reply size that is accepted. This +// is a sanity check, not a limit in the spec. +const maxAgentResponseBytes = 16 << 20 + +// Agent messages: +// These structures mirror the wire format of the corresponding ssh agent +// messages found in [PROTOCOL.agent]. + +// 3.4 Generic replies from agent to client +const agentFailure = 5 + +type failureAgentMsg struct{} + +const agentSuccess = 6 + +type successAgentMsg struct{} + +// See [PROTOCOL.agent], section 2.5.2. +const agentRequestIdentities = 11 + +type requestIdentitiesAgentMsg struct{} + +// See [PROTOCOL.agent], section 2.5.2. +const agentIdentitiesAnswer = 12 + +type identitiesAnswerAgentMsg struct { + NumKeys uint32 `sshtype:"12"` + Keys []byte `ssh:"rest"` +} + +// See [PROTOCOL.agent], section 2.6.2. +const agentSignRequest = 13 + +type signRequestAgentMsg struct { + KeyBlob []byte `sshtype:"13"` + Data []byte + Flags uint32 +} + +// See [PROTOCOL.agent], section 2.6.2. + +// 3.6 Replies from agent to client for protocol 2 key operations +const agentSignResponse = 14 + +type signResponseAgentMsg struct { + SigBlob []byte `sshtype:"14"` +} + +type publicKey struct { + Format string + Rest []byte `ssh:"rest"` +} + +// Key represents a protocol 2 public key as defined in +// [PROTOCOL.agent], section 2.5.2. +type Key struct { + Format string + Blob []byte + Comment string +} + +func clientErr(err error) error { + return fmt.Errorf("agent: client error: %v", err) +} + +// String returns the storage form of an agent key with the format, base64 +// encoded serialized key, and the comment if it is not empty. +func (k *Key) String() string { + s := string(k.Format) + " " + base64.StdEncoding.EncodeToString(k.Blob) + + if k.Comment != "" { + s += " " + k.Comment + } + + return s +} + +// Type returns the public key type. +func (k *Key) Type() string { + return k.Format +} + +// Marshal returns key blob to satisfy the ssh.PublicKey interface. +func (k *Key) Marshal() []byte { + return k.Blob +} + +// Verify satisfies the ssh.PublicKey interface. +func (k *Key) Verify(data []byte, sig *ssh.Signature) error { + pubKey, err := ssh.ParsePublicKey(k.Blob) + if err != nil { + return fmt.Errorf("agent: bad public key: %v", err) + } + return pubKey.Verify(data, sig) +} + +type wireKey struct { + Format string + Rest []byte `ssh:"rest"` +} + +func parseKey(in []byte) (out *Key, rest []byte, err error) { + var record struct { + Blob []byte + Comment string + Rest []byte `ssh:"rest"` + } + + if err := ssh.Unmarshal(in, &record); err != nil { + return nil, nil, err + } + + var wk wireKey + if err := ssh.Unmarshal(record.Blob, &wk); err != nil { + return nil, nil, err + } + + return &Key{ + Format: wk.Format, + Blob: record.Blob, + Comment: record.Comment, + }, record.Rest, nil +} + +// client is a client for an ssh-agent process. +type client struct { + // conn is typically a *net.UnixConn + conn io.ReadWriter + // mu is used to prevent concurrent access to the agent + mu sync.Mutex +} + +// NewClient returns an Agent that talks to an ssh-agent process over +// the given connection. +func NewClient(rw io.ReadWriter) Agent { + return &client{conn: rw} +} + +// call sends an RPC to the agent. On success, the reply is +// unmarshaled into reply and replyType is set to the first byte of +// the reply, which contains the type of the message. +func (c *client) call(req []byte) (reply interface{}, err error) { + c.mu.Lock() + defer c.mu.Unlock() + + msg := make([]byte, 4+len(req)) + binary.BigEndian.PutUint32(msg, uint32(len(req))) + copy(msg[4:], req) + if _, err = c.conn.Write(msg); err != nil { + return nil, clientErr(err) + } + + var respSizeBuf [4]byte + if _, err = io.ReadFull(c.conn, respSizeBuf[:]); err != nil { + return nil, clientErr(err) + } + respSize := binary.BigEndian.Uint32(respSizeBuf[:]) + if respSize > maxAgentResponseBytes { + return nil, clientErr(err) + } + + buf := make([]byte, respSize) + if _, err = io.ReadFull(c.conn, buf); err != nil { + return nil, clientErr(err) + } + reply, err = unmarshal(buf) + if err != nil { + return nil, clientErr(err) + } + return reply, err +} + +func (c *client) simpleCall(req []byte) error { + resp, err := c.call(req) + if err != nil { + return err + } + if _, ok := resp.(*successAgentMsg); ok { + return nil + } + return errors.New("agent: failure") +} + +func (c *client) RemoveAll() error { + return c.simpleCall([]byte{agentRemoveAllIdentities}) +} + +func (c *client) Remove(key ssh.PublicKey) error { + req := ssh.Marshal(&agentRemoveIdentityMsg{ + KeyBlob: key.Marshal(), + }) + return c.simpleCall(req) +} + +func (c *client) Lock(passphrase []byte) error { + req := ssh.Marshal(&agentLockMsg{ + Passphrase: passphrase, + }) + return c.simpleCall(req) +} + +func (c *client) Unlock(passphrase []byte) error { + req := ssh.Marshal(&agentUnlockMsg{ + Passphrase: passphrase, + }) + return c.simpleCall(req) +} + +// List returns the identities known to the agent. +func (c *client) List() ([]*Key, error) { + // see [PROTOCOL.agent] section 2.5.2. + req := []byte{agentRequestIdentities} + + msg, err := c.call(req) + if err != nil { + return nil, err + } + + switch msg := msg.(type) { + case *identitiesAnswerAgentMsg: + if msg.NumKeys > maxAgentResponseBytes/8 { + return nil, errors.New("agent: too many keys in agent reply") + } + keys := make([]*Key, msg.NumKeys) + data := msg.Keys + for i := uint32(0); i < msg.NumKeys; i++ { + var key *Key + var err error + if key, data, err = parseKey(data); err != nil { + return nil, err + } + keys[i] = key + } + return keys, nil + case *failureAgentMsg: + return nil, errors.New("agent: failed to list keys") + } + panic("unreachable") +} + +// Sign has the agent sign the data using a protocol 2 key as defined +// in [PROTOCOL.agent] section 2.6.2. +func (c *client) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + req := ssh.Marshal(signRequestAgentMsg{ + KeyBlob: key.Marshal(), + Data: data, + }) + + msg, err := c.call(req) + if err != nil { + return nil, err + } + + switch msg := msg.(type) { + case *signResponseAgentMsg: + var sig ssh.Signature + if err := ssh.Unmarshal(msg.SigBlob, &sig); err != nil { + return nil, err + } + + return &sig, nil + case *failureAgentMsg: + return nil, errors.New("agent: failed to sign challenge") + } + panic("unreachable") +} + +// unmarshal parses an agent message in packet, returning the parsed +// form and the message type of packet. +func unmarshal(packet []byte) (interface{}, error) { + if len(packet) < 1 { + return nil, errors.New("agent: empty packet") + } + var msg interface{} + switch packet[0] { + case agentFailure: + return new(failureAgentMsg), nil + case agentSuccess: + return new(successAgentMsg), nil + case agentIdentitiesAnswer: + msg = new(identitiesAnswerAgentMsg) + case agentSignResponse: + msg = new(signResponseAgentMsg) + case agentV1IdentitiesAnswer: + msg = new(agentV1IdentityMsg) + default: + return nil, fmt.Errorf("agent: unknown type tag %d", packet[0]) + } + if err := ssh.Unmarshal(packet, msg); err != nil { + return nil, err + } + return msg, nil +} + +type rsaKeyMsg struct { + Type string `sshtype:"17|25"` + N *big.Int + E *big.Int + D *big.Int + Iqmp *big.Int // IQMP = Inverse Q Mod P + P *big.Int + Q *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type dsaKeyMsg struct { + Type string `sshtype:"17|25"` + P *big.Int + Q *big.Int + G *big.Int + Y *big.Int + X *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type ecdsaKeyMsg struct { + Type string `sshtype:"17|25"` + Curve string + KeyBytes []byte + D *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type ed25519KeyMsg struct { + Type string `sshtype:"17|25"` + Pub []byte + Priv []byte + Comments string + Constraints []byte `ssh:"rest"` +} + +// Insert adds a private key to the agent. +func (c *client) insertKey(s interface{}, comment string, constraints []byte) error { + var req []byte + switch k := s.(type) { + case *rsa.PrivateKey: + if len(k.Primes) != 2 { + return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes)) + } + k.Precompute() + req = ssh.Marshal(rsaKeyMsg{ + Type: ssh.KeyAlgoRSA, + N: k.N, + E: big.NewInt(int64(k.E)), + D: k.D, + Iqmp: k.Precomputed.Qinv, + P: k.Primes[0], + Q: k.Primes[1], + Comments: comment, + Constraints: constraints, + }) + case *dsa.PrivateKey: + req = ssh.Marshal(dsaKeyMsg{ + Type: ssh.KeyAlgoDSA, + P: k.P, + Q: k.Q, + G: k.G, + Y: k.Y, + X: k.X, + Comments: comment, + Constraints: constraints, + }) + case *ecdsa.PrivateKey: + nistID := fmt.Sprintf("nistp%d", k.Params().BitSize) + req = ssh.Marshal(ecdsaKeyMsg{ + Type: "ecdsa-sha2-" + nistID, + Curve: nistID, + KeyBytes: elliptic.Marshal(k.Curve, k.X, k.Y), + D: k.D, + Comments: comment, + Constraints: constraints, + }) + case *ed25519.PrivateKey: + req = ssh.Marshal(ed25519KeyMsg{ + Type: ssh.KeyAlgoED25519, + Pub: []byte(*k)[32:], + Priv: []byte(*k), + Comments: comment, + Constraints: constraints, + }) + default: + return fmt.Errorf("agent: unsupported key type %T", s) + } + + // if constraints are present then the message type needs to be changed. + if len(constraints) != 0 { + req[0] = agentAddIdConstrained + } + + resp, err := c.call(req) + if err != nil { + return err + } + if _, ok := resp.(*successAgentMsg); ok { + return nil + } + return errors.New("agent: failure") +} + +type rsaCertMsg struct { + Type string `sshtype:"17|25"` + CertBytes []byte + D *big.Int + Iqmp *big.Int // IQMP = Inverse Q Mod P + P *big.Int + Q *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type dsaCertMsg struct { + Type string `sshtype:"17|25"` + CertBytes []byte + X *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type ecdsaCertMsg struct { + Type string `sshtype:"17|25"` + CertBytes []byte + D *big.Int + Comments string + Constraints []byte `ssh:"rest"` +} + +type ed25519CertMsg struct { + Type string `sshtype:"17|25"` + CertBytes []byte + Pub []byte + Priv []byte + Comments string + Constraints []byte `ssh:"rest"` +} + +// Add adds a private key to the agent. If a certificate is given, +// that certificate is added instead as public key. +func (c *client) Add(key AddedKey) error { + var constraints []byte + + if secs := key.LifetimeSecs; secs != 0 { + constraints = append(constraints, agentConstrainLifetime) + + var secsBytes [4]byte + binary.BigEndian.PutUint32(secsBytes[:], secs) + constraints = append(constraints, secsBytes[:]...) + } + + if key.ConfirmBeforeUse { + constraints = append(constraints, agentConstrainConfirm) + } + + if cert := key.Certificate; cert == nil { + return c.insertKey(key.PrivateKey, key.Comment, constraints) + } else { + return c.insertCert(key.PrivateKey, cert, key.Comment, constraints) + } +} + +func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string, constraints []byte) error { + var req []byte + switch k := s.(type) { + case *rsa.PrivateKey: + if len(k.Primes) != 2 { + return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes)) + } + k.Precompute() + req = ssh.Marshal(rsaCertMsg{ + Type: cert.Type(), + CertBytes: cert.Marshal(), + D: k.D, + Iqmp: k.Precomputed.Qinv, + P: k.Primes[0], + Q: k.Primes[1], + Comments: comment, + Constraints: constraints, + }) + case *dsa.PrivateKey: + req = ssh.Marshal(dsaCertMsg{ + Type: cert.Type(), + CertBytes: cert.Marshal(), + X: k.X, + Comments: comment, + Constraints: constraints, + }) + case *ecdsa.PrivateKey: + req = ssh.Marshal(ecdsaCertMsg{ + Type: cert.Type(), + CertBytes: cert.Marshal(), + D: k.D, + Comments: comment, + Constraints: constraints, + }) + case *ed25519.PrivateKey: + req = ssh.Marshal(ed25519CertMsg{ + Type: cert.Type(), + CertBytes: cert.Marshal(), + Pub: []byte(*k)[32:], + Priv: []byte(*k), + Comments: comment, + Constraints: constraints, + }) + default: + return fmt.Errorf("agent: unsupported key type %T", s) + } + + // if constraints are present then the message type needs to be changed. + if len(constraints) != 0 { + req[0] = agentAddIdConstrained + } + + signer, err := ssh.NewSignerFromKey(s) + if err != nil { + return err + } + if bytes.Compare(cert.Key.Marshal(), signer.PublicKey().Marshal()) != 0 { + return errors.New("agent: signer and cert have different public key") + } + + resp, err := c.call(req) + if err != nil { + return err + } + if _, ok := resp.(*successAgentMsg); ok { + return nil + } + return errors.New("agent: failure") +} + +// Signers provides a callback for client authentication. +func (c *client) Signers() ([]ssh.Signer, error) { + keys, err := c.List() + if err != nil { + return nil, err + } + + var result []ssh.Signer + for _, k := range keys { + result = append(result, &agentKeyringSigner{c, k}) + } + return result, nil +} + +type agentKeyringSigner struct { + agent *client + pub ssh.PublicKey +} + +func (s *agentKeyringSigner) PublicKey() ssh.PublicKey { + return s.pub +} + +func (s *agentKeyringSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) { + // The agent has its own entropy source, so the rand argument is ignored. + return s.agent.Sign(s.pub, data) +} diff --git a/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/forward.go b/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/forward.go new file mode 100644 index 000000000..fd24ba900 --- /dev/null +++ b/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/forward.go @@ -0,0 +1,103 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package agent + +import ( + "errors" + "io" + "net" + "sync" + + "golang.org/x/crypto/ssh" +) + +// RequestAgentForwarding sets up agent forwarding for the session. +// ForwardToAgent or ForwardToRemote should be called to route +// the authentication requests. +func RequestAgentForwarding(session *ssh.Session) error { + ok, err := session.SendRequest("auth-agent-req@openssh.com", true, nil) + if err != nil { + return err + } + if !ok { + return errors.New("forwarding request denied") + } + return nil +} + +// ForwardToAgent routes authentication requests to the given keyring. +func ForwardToAgent(client *ssh.Client, keyring Agent) error { + channels := client.HandleChannelOpen(channelType) + if channels == nil { + return errors.New("agent: already have handler for " + channelType) + } + + go func() { + for ch := range channels { + channel, reqs, err := ch.Accept() + if err != nil { + continue + } + go ssh.DiscardRequests(reqs) + go func() { + ServeAgent(keyring, channel) + channel.Close() + }() + } + }() + return nil +} + +const channelType = "auth-agent@openssh.com" + +// ForwardToRemote routes authentication requests to the ssh-agent +// process serving on the given unix socket. +func ForwardToRemote(client *ssh.Client, addr string) error { + channels := client.HandleChannelOpen(channelType) + if channels == nil { + return errors.New("agent: already have handler for " + channelType) + } + conn, err := net.Dial("unix", addr) + if err != nil { + return err + } + conn.Close() + + go func() { + for ch := range channels { + channel, reqs, err := ch.Accept() + if err != nil { + continue + } + go ssh.DiscardRequests(reqs) + go forwardUnixSocket(channel, addr) + } + }() + return nil +} + +func forwardUnixSocket(channel ssh.Channel, addr string) { + conn, err := net.Dial("unix", addr) + if err != nil { + return + } + + var wg sync.WaitGroup + wg.Add(2) + go func() { + io.Copy(conn, channel) + conn.(*net.UnixConn).CloseWrite() + wg.Done() + }() + go func() { + io.Copy(channel, conn) + channel.CloseWrite() + wg.Done() + }() + + wg.Wait() + conn.Close() + channel.Close() +} diff --git a/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/keyring.go b/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/keyring.go new file mode 100644 index 000000000..a6ba06ab3 --- /dev/null +++ b/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/keyring.go @@ -0,0 +1,215 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package agent + +import ( + "bytes" + "crypto/rand" + "crypto/subtle" + "errors" + "fmt" + "sync" + "time" + + "golang.org/x/crypto/ssh" +) + +type privKey struct { + signer ssh.Signer + comment string + expire *time.Time +} + +type keyring struct { + mu sync.Mutex + keys []privKey + + locked bool + passphrase []byte +} + +var errLocked = errors.New("agent: locked") + +// NewKeyring returns an Agent that holds keys in memory. It is safe +// for concurrent use by multiple goroutines. +func NewKeyring() Agent { + return &keyring{} +} + +// RemoveAll removes all identities. +func (r *keyring) RemoveAll() error { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return errLocked + } + + r.keys = nil + return nil +} + +// removeLocked does the actual key removal. The caller must already be holding the +// keyring mutex. +func (r *keyring) removeLocked(want []byte) error { + found := false + for i := 0; i < len(r.keys); { + if bytes.Equal(r.keys[i].signer.PublicKey().Marshal(), want) { + found = true + r.keys[i] = r.keys[len(r.keys)-1] + r.keys = r.keys[:len(r.keys)-1] + continue + } else { + i++ + } + } + + if !found { + return errors.New("agent: key not found") + } + return nil +} + +// Remove removes all identities with the given public key. +func (r *keyring) Remove(key ssh.PublicKey) error { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return errLocked + } + + return r.removeLocked(key.Marshal()) +} + +// Lock locks the agent. Sign and Remove will fail, and List will return an empty list. +func (r *keyring) Lock(passphrase []byte) error { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return errLocked + } + + r.locked = true + r.passphrase = passphrase + return nil +} + +// Unlock undoes the effect of Lock +func (r *keyring) Unlock(passphrase []byte) error { + r.mu.Lock() + defer r.mu.Unlock() + if !r.locked { + return errors.New("agent: not locked") + } + if len(passphrase) != len(r.passphrase) || 1 != subtle.ConstantTimeCompare(passphrase, r.passphrase) { + return fmt.Errorf("agent: incorrect passphrase") + } + + r.locked = false + r.passphrase = nil + return nil +} + +// expireKeysLocked removes expired keys from the keyring. If a key was added +// with a lifetimesecs contraint and seconds >= lifetimesecs seconds have +// ellapsed, it is removed. The caller *must* be holding the keyring mutex. +func (r *keyring) expireKeysLocked() { + for _, k := range r.keys { + if k.expire != nil && time.Now().After(*k.expire) { + r.removeLocked(k.signer.PublicKey().Marshal()) + } + } +} + +// List returns the identities known to the agent. +func (r *keyring) List() ([]*Key, error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + // section 2.7: locked agents return empty. + return nil, nil + } + + r.expireKeysLocked() + var ids []*Key + for _, k := range r.keys { + pub := k.signer.PublicKey() + ids = append(ids, &Key{ + Format: pub.Type(), + Blob: pub.Marshal(), + Comment: k.comment}) + } + return ids, nil +} + +// Insert adds a private key to the keyring. If a certificate +// is given, that certificate is added as public key. Note that +// any constraints given are ignored. +func (r *keyring) Add(key AddedKey) error { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return errLocked + } + signer, err := ssh.NewSignerFromKey(key.PrivateKey) + + if err != nil { + return err + } + + if cert := key.Certificate; cert != nil { + signer, err = ssh.NewCertSigner(cert, signer) + if err != nil { + return err + } + } + + p := privKey{ + signer: signer, + comment: key.Comment, + } + + if key.LifetimeSecs > 0 { + t := time.Now().Add(time.Duration(key.LifetimeSecs) * time.Second) + p.expire = &t + } + + r.keys = append(r.keys, p) + + return nil +} + +// Sign returns a signature for the data. +func (r *keyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return nil, errLocked + } + + r.expireKeysLocked() + wanted := key.Marshal() + for _, k := range r.keys { + if bytes.Equal(k.signer.PublicKey().Marshal(), wanted) { + return k.signer.Sign(rand.Reader, data) + } + } + return nil, errors.New("not found") +} + +// Signers returns signers for all the known keys. +func (r *keyring) Signers() ([]ssh.Signer, error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.locked { + return nil, errLocked + } + + r.expireKeysLocked() + s := make([]ssh.Signer, 0, len(r.keys)) + for _, k := range r.keys { + s = append(s, k.signer) + } + return s, nil +} diff --git a/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/server.go b/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/server.go new file mode 100644 index 000000000..68a333fa5 --- /dev/null +++ b/src/cmd/linuxkit/vendor/golang.org/x/crypto/ssh/agent/server.go @@ -0,0 +1,451 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package agent + +import ( + "crypto/dsa" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "encoding/binary" + "errors" + "fmt" + "io" + "log" + "math/big" + + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/ssh" +) + +// Server wraps an Agent and uses it to implement the agent side of +// the SSH-agent, wire protocol. +type server struct { + agent Agent +} + +func (s *server) processRequestBytes(reqData []byte) []byte { + rep, err := s.processRequest(reqData) + if err != nil { + if err != errLocked { + // TODO(hanwen): provide better logging interface? + log.Printf("agent %d: %v", reqData[0], err) + } + return []byte{agentFailure} + } + + if err == nil && rep == nil { + return []byte{agentSuccess} + } + + return ssh.Marshal(rep) +} + +func marshalKey(k *Key) []byte { + var record struct { + Blob []byte + Comment string + } + record.Blob = k.Marshal() + record.Comment = k.Comment + + return ssh.Marshal(&record) +} + +// See [PROTOCOL.agent], section 2.5.1. +const agentV1IdentitiesAnswer = 2 + +type agentV1IdentityMsg struct { + Numkeys uint32 `sshtype:"2"` +} + +type agentRemoveIdentityMsg struct { + KeyBlob []byte `sshtype:"18"` +} + +type agentLockMsg struct { + Passphrase []byte `sshtype:"22"` +} + +type agentUnlockMsg struct { + Passphrase []byte `sshtype:"23"` +} + +func (s *server) processRequest(data []byte) (interface{}, error) { + switch data[0] { + case agentRequestV1Identities: + return &agentV1IdentityMsg{0}, nil + + case agentRemoveAllV1Identities: + return nil, nil + + case agentRemoveIdentity: + var req agentRemoveIdentityMsg + if err := ssh.Unmarshal(data, &req); err != nil { + return nil, err + } + + var wk wireKey + if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil { + return nil, err + } + + return nil, s.agent.Remove(&Key{Format: wk.Format, Blob: req.KeyBlob}) + + case agentRemoveAllIdentities: + return nil, s.agent.RemoveAll() + + case agentLock: + var req agentLockMsg + if err := ssh.Unmarshal(data, &req); err != nil { + return nil, err + } + + return nil, s.agent.Lock(req.Passphrase) + + case agentUnlock: + var req agentLockMsg + if err := ssh.Unmarshal(data, &req); err != nil { + return nil, err + } + return nil, s.agent.Unlock(req.Passphrase) + + case agentSignRequest: + var req signRequestAgentMsg + if err := ssh.Unmarshal(data, &req); err != nil { + return nil, err + } + + var wk wireKey + if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil { + return nil, err + } + + k := &Key{ + Format: wk.Format, + Blob: req.KeyBlob, + } + + sig, err := s.agent.Sign(k, req.Data) // TODO(hanwen): flags. + if err != nil { + return nil, err + } + return &signResponseAgentMsg{SigBlob: ssh.Marshal(sig)}, nil + + case agentRequestIdentities: + keys, err := s.agent.List() + if err != nil { + return nil, err + } + + rep := identitiesAnswerAgentMsg{ + NumKeys: uint32(len(keys)), + } + for _, k := range keys { + rep.Keys = append(rep.Keys, marshalKey(k)...) + } + return rep, nil + + case agentAddIdConstrained, agentAddIdentity: + return nil, s.insertIdentity(data) + } + + return nil, fmt.Errorf("unknown opcode %d", data[0]) +} + +func parseRSAKey(req []byte) (*AddedKey, error) { + var k rsaKeyMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + if k.E.BitLen() > 30 { + return nil, errors.New("agent: RSA public exponent too large") + } + priv := &rsa.PrivateKey{ + PublicKey: rsa.PublicKey{ + E: int(k.E.Int64()), + N: k.N, + }, + D: k.D, + Primes: []*big.Int{k.P, k.Q}, + } + priv.Precompute() + + return &AddedKey{PrivateKey: priv, Comment: k.Comments}, nil +} + +func parseEd25519Key(req []byte) (*AddedKey, error) { + var k ed25519KeyMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + priv := ed25519.PrivateKey(k.Priv) + return &AddedKey{PrivateKey: &priv, Comment: k.Comments}, nil +} + +func parseDSAKey(req []byte) (*AddedKey, error) { + var k dsaKeyMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + priv := &dsa.PrivateKey{ + PublicKey: dsa.PublicKey{ + Parameters: dsa.Parameters{ + P: k.P, + Q: k.Q, + G: k.G, + }, + Y: k.Y, + }, + X: k.X, + } + + return &AddedKey{PrivateKey: priv, Comment: k.Comments}, nil +} + +func unmarshalECDSA(curveName string, keyBytes []byte, privScalar *big.Int) (priv *ecdsa.PrivateKey, err error) { + priv = &ecdsa.PrivateKey{ + D: privScalar, + } + + switch curveName { + case "nistp256": + priv.Curve = elliptic.P256() + case "nistp384": + priv.Curve = elliptic.P384() + case "nistp521": + priv.Curve = elliptic.P521() + default: + return nil, fmt.Errorf("agent: unknown curve %q", curveName) + } + + priv.X, priv.Y = elliptic.Unmarshal(priv.Curve, keyBytes) + if priv.X == nil || priv.Y == nil { + return nil, errors.New("agent: point not on curve") + } + + return priv, nil +} + +func parseEd25519Cert(req []byte) (*AddedKey, error) { + var k ed25519CertMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + pubKey, err := ssh.ParsePublicKey(k.CertBytes) + if err != nil { + return nil, err + } + priv := ed25519.PrivateKey(k.Priv) + cert, ok := pubKey.(*ssh.Certificate) + if !ok { + return nil, errors.New("agent: bad ED25519 certificate") + } + return &AddedKey{PrivateKey: &priv, Certificate: cert, Comment: k.Comments}, nil +} + +func parseECDSAKey(req []byte) (*AddedKey, error) { + var k ecdsaKeyMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + + priv, err := unmarshalECDSA(k.Curve, k.KeyBytes, k.D) + if err != nil { + return nil, err + } + + return &AddedKey{PrivateKey: priv, Comment: k.Comments}, nil +} + +func parseRSACert(req []byte) (*AddedKey, error) { + var k rsaCertMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + + pubKey, err := ssh.ParsePublicKey(k.CertBytes) + if err != nil { + return nil, err + } + + cert, ok := pubKey.(*ssh.Certificate) + if !ok { + return nil, errors.New("agent: bad RSA certificate") + } + + // An RSA publickey as marshaled by rsaPublicKey.Marshal() in keys.go + var rsaPub struct { + Name string + E *big.Int + N *big.Int + } + if err := ssh.Unmarshal(cert.Key.Marshal(), &rsaPub); err != nil { + return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err) + } + + if rsaPub.E.BitLen() > 30 { + return nil, errors.New("agent: RSA public exponent too large") + } + + priv := rsa.PrivateKey{ + PublicKey: rsa.PublicKey{ + E: int(rsaPub.E.Int64()), + N: rsaPub.N, + }, + D: k.D, + Primes: []*big.Int{k.Q, k.P}, + } + priv.Precompute() + + return &AddedKey{PrivateKey: &priv, Certificate: cert, Comment: k.Comments}, nil +} + +func parseDSACert(req []byte) (*AddedKey, error) { + var k dsaCertMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + pubKey, err := ssh.ParsePublicKey(k.CertBytes) + if err != nil { + return nil, err + } + cert, ok := pubKey.(*ssh.Certificate) + if !ok { + return nil, errors.New("agent: bad DSA certificate") + } + + // A DSA publickey as marshaled by dsaPublicKey.Marshal() in keys.go + var w struct { + Name string + P, Q, G, Y *big.Int + } + if err := ssh.Unmarshal(cert.Key.Marshal(), &w); err != nil { + return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err) + } + + priv := &dsa.PrivateKey{ + PublicKey: dsa.PublicKey{ + Parameters: dsa.Parameters{ + P: w.P, + Q: w.Q, + G: w.G, + }, + Y: w.Y, + }, + X: k.X, + } + + return &AddedKey{PrivateKey: priv, Certificate: cert, Comment: k.Comments}, nil +} + +func parseECDSACert(req []byte) (*AddedKey, error) { + var k ecdsaCertMsg + if err := ssh.Unmarshal(req, &k); err != nil { + return nil, err + } + + pubKey, err := ssh.ParsePublicKey(k.CertBytes) + if err != nil { + return nil, err + } + cert, ok := pubKey.(*ssh.Certificate) + if !ok { + return nil, errors.New("agent: bad ECDSA certificate") + } + + // An ECDSA publickey as marshaled by ecdsaPublicKey.Marshal() in keys.go + var ecdsaPub struct { + Name string + ID string + Key []byte + } + if err := ssh.Unmarshal(cert.Key.Marshal(), &ecdsaPub); err != nil { + return nil, err + } + + priv, err := unmarshalECDSA(ecdsaPub.ID, ecdsaPub.Key, k.D) + if err != nil { + return nil, err + } + + return &AddedKey{PrivateKey: priv, Certificate: cert, Comment: k.Comments}, nil +} + +func (s *server) insertIdentity(req []byte) error { + var record struct { + Type string `sshtype:"17|25"` + Rest []byte `ssh:"rest"` + } + + if err := ssh.Unmarshal(req, &record); err != nil { + return err + } + + var addedKey *AddedKey + var err error + + switch record.Type { + case ssh.KeyAlgoRSA: + addedKey, err = parseRSAKey(req) + case ssh.KeyAlgoDSA: + addedKey, err = parseDSAKey(req) + case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521: + addedKey, err = parseECDSAKey(req) + case ssh.KeyAlgoED25519: + addedKey, err = parseEd25519Key(req) + case ssh.CertAlgoRSAv01: + addedKey, err = parseRSACert(req) + case ssh.CertAlgoDSAv01: + addedKey, err = parseDSACert(req) + case ssh.CertAlgoECDSA256v01, ssh.CertAlgoECDSA384v01, ssh.CertAlgoECDSA521v01: + addedKey, err = parseECDSACert(req) + case ssh.CertAlgoED25519v01: + addedKey, err = parseEd25519Cert(req) + default: + return fmt.Errorf("agent: not implemented: %q", record.Type) + } + + if err != nil { + return err + } + return s.agent.Add(*addedKey) +} + +// ServeAgent serves the agent protocol on the given connection. It +// returns when an I/O error occurs. +func ServeAgent(agent Agent, c io.ReadWriter) error { + s := &server{agent} + + var length [4]byte + for { + if _, err := io.ReadFull(c, length[:]); err != nil { + return err + } + l := binary.BigEndian.Uint32(length[:]) + if l > maxAgentResponseBytes { + // We also cap requests. + return fmt.Errorf("agent: request too large: %d", l) + } + + req := make([]byte, l) + if _, err := io.ReadFull(c, req); err != nil { + return err + } + + repData := s.processRequestBytes(req) + if len(repData) > maxAgentResponseBytes { + return fmt.Errorf("agent: reply too large: %d bytes", len(repData)) + } + + binary.BigEndian.PutUint32(length[:], uint32(len(repData))) + if _, err := c.Write(length[:]); err != nil { + return err + } + if _, err := c.Write(repData); err != nil { + return err + } + } +} From 04a689fcc4c51261075a5c2fa826eefff6f1a9d8 Mon Sep 17 00:00:00 2001 From: Rolf Neugebauer Date: Mon, 7 Aug 2017 18:24:31 +0100 Subject: [PATCH 6/7] cmd: Enable console access for packet.net On 'linuxkit run packet' ssh into the SOS for the newly created machine. This requires that you have set up your ssh keys and have at least once logged into the ssh so that the host keys are present in the 'known_hosts' file. Signed-off-by: Rolf Neugebauer --- src/cmd/linuxkit/run_packet.go | 153 +++++++++++++++++++++++++++++---- 1 file changed, 135 insertions(+), 18 deletions(-) diff --git a/src/cmd/linuxkit/run_packet.go b/src/cmd/linuxkit/run_packet.go index fd980b535..1b41c928f 100644 --- a/src/cmd/linuxkit/run_packet.go +++ b/src/cmd/linuxkit/run_packet.go @@ -1,16 +1,22 @@ package main import ( + "bufio" "encoding/json" "flag" "fmt" + "net" "net/http" "os" "os/user" "path/filepath" + "strings" "github.com/bzub/packngo" // TODO(rn): Update to official once iPXE is merged log "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" + "golang.org/x/crypto/ssh/terminal" ) const ( @@ -36,19 +42,6 @@ func init() { } } -// ValidateHTTPURL does a sanity check that a URL returns a 200 or 300 response -func ValidateHTTPURL(url string) { - log.Printf("Validating URL: %s", url) - resp, err := http.Head(url) - if err != nil { - log.Fatal(err) - } - if resp.StatusCode >= 400 { - log.Fatal("Got a non 200- or 300- HTTP response code: %s", resp) - } - log.Printf("OK: %d response code", resp.StatusCode) -} - // Process the run arguments and execute run func runPacket(args []string) { flags := flag.NewFlagSet("packet", flag.ExitOnError) @@ -66,6 +59,7 @@ func runPacket(args []string) { hostNameFlag := flags.String("hostname", packetDefaultHostname, "Hostname of new instance (or "+packetHostnameVar+")") nameFlag := flags.String("img-name", "", "Overrides the prefix used to identify the files. Defaults to [name] (or "+packetNameVar+")") alwaysPXE := flags.Bool("always-pxe", true, "Reboot from PXE every time.") + consoleFlag := flags.Bool("console", true, "Provide interactive access on the console.") if err := flags.Parse(args); err != nil { log.Fatal("Unable to parse args") } @@ -99,8 +93,8 @@ func runPacket(args []string) { log.Debugf("Using userData of:\n%s\n", userData) initrdURL := fmt.Sprintf("%s/%s-initrd.img", url, name) kernelURL := fmt.Sprintf("%s/%s-kernel", url, name) - ValidateHTTPURL(kernelURL) - ValidateHTTPURL(initrdURL) + validateHTTPURL(kernelURL) + validateHTTPURL(initrdURL) client := packngo.NewClient("", apiKey, nil) tags := []string{} req := packngo.DeviceCreateRequest{ @@ -124,7 +118,130 @@ func runPacket(args []string) { } // log response json if in verbose mode log.Debugf("%s\n", string(b)) - // TODO: poll events api for bringup (requires extpacknogo) - // TODO: connect to serial console (requires API extension to get SSH URI) - // TODO: add ssh keys via API registered keys + + sshHost := "sos." + d.Facility.Code + ".packet.net" + if *consoleFlag { + // Connect to the serial console + if err := sshSOS(d.ID, sshHost); err != nil { + log.Fatal(err) + } + } else { + log.Printf("Machine booting") + log.Printf("Access the console with: ssh %s@%s", d.ID, sshHost) + } +} + +// validateHTTPURL does a sanity check that a URL returns a 200 or 300 response +func validateHTTPURL(url string) { + log.Printf("Validating URL: %s", url) + resp, err := http.Head(url) + if err != nil { + log.Fatal(err) + } + if resp.StatusCode >= 400 { + log.Fatal("Got a non 200- or 300- HTTP response code: %s", resp) + } + log.Printf("OK: %d response code", resp.StatusCode) +} + +func sshSOS(user, host string) error { + log.Printf("console: ssh %s@%s", user, host) + + hostKey, err := sshHostKey(host) + if err != nil { + return fmt.Errorf("Host key not found. Maybe need to add it? %v", err) + } + + sshConfig := &ssh.ClientConfig{ + User: user, + HostKeyCallback: ssh.FixedHostKey(hostKey), + Auth: []ssh.AuthMethod{ + sshAgent(), + }, + } + + c, err := ssh.Dial("tcp", host+":22", sshConfig) + if err != nil { + return fmt.Errorf("Failed to dial: %s", err) + } + + s, err := c.NewSession() + if err != nil { + return fmt.Errorf("Failed to create session: %v", err) + } + defer s.Close() + + s.Stdout = os.Stdout + s.Stderr = os.Stderr + s.Stdin = os.Stdin + + modes := ssh.TerminalModes{ + ssh.ECHO: 0, + ssh.IGNCR: 1, + } + + width, height, err := terminal.GetSize(0) + if err != nil { + log.Warningf("Error getting terminal size. Ignored. %v", err) + width = 80 + height = 40 + } + if err := s.RequestPty("vt100", width, height, modes); err != nil { + return fmt.Errorf("Request for PTY failed: %v", err) + } + oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return err + } + defer terminal.Restore(0, oldState) + + // Start remote shell + if err := s.Shell(); err != nil { + return fmt.Errorf("Failed to start shell: %v", err) + } + + s.Wait() + return nil +} + +// Get a ssh-agent AuthMethod +func sshAgent() ssh.AuthMethod { + sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) + if err != nil { + log.Fatalf("Failed to dial ssh-agent: %v", err) + } + return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers) +} + +// This function returns the host key for a given host (the SOS server). +// If it can't be found, it errors +func sshHostKey(host string) (ssh.PublicKey, error) { + f, err := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")) + if err != nil { + return nil, fmt.Errorf("Can't open know_hosts file: %v", err) + } + defer f.Close() + + s := bufio.NewScanner(f) + + var hostKey ssh.PublicKey + for s.Scan() { + fields := strings.Split(s.Text(), " ") + if len(fields) != 3 { + continue + } + if strings.Contains(fields[0], host) { + var err error + hostKey, _, _, _, err = ssh.ParseAuthorizedKey(s.Bytes()) + if err != nil { + return nil, fmt.Errorf("Error parsing %q: %v", fields[2], err) + } + break + } + } + + if hostKey == nil { + return nil, fmt.Errorf("No hostkey for %s", host) + } + return hostKey, nil } From df7c79fa8f5e8769a2582e502ae429075fe37ef4 Mon Sep 17 00:00:00 2001 From: Rolf Neugebauer Date: Mon, 7 Aug 2017 20:11:17 +0100 Subject: [PATCH 7/7] docs: Update packet documentation Signed-off-by: Rolf Neugebauer --- docs/platform-packet.md | 101 +++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 42 deletions(-) diff --git a/docs/platform-packet.md b/docs/platform-packet.md index 32943ff71..a00d3f8bb 100644 --- a/docs/platform-packet.md +++ b/docs/platform-packet.md @@ -18,64 +18,81 @@ is a work in progress. [Type 1]:https://www.packet.net/bare-metal/servers/type-1/ [Type 2A]:https://www.packet.net/bare-metal/servers/type-2a/ +The `linuxkit run packet` command can mostly either be configured via +command line options or with environment variables. see `linuxkit run +packet --help` for the options and environment variables. + ## Boot -Build an image with `moby build`. The [packet.yml](https://github.com/vielmetti/linuxkit/blob/master/examples/packet.yml) -example file provides a suitable template to start from. -Linuxkit on Packet [boots via iPXE]. This requires that you have -an HTTP server on which you can store your images. At the moment -there is no equivalent to "linuxkit push" to upload these images, -so you will have to host them yourself. The images can be served -from any HTTP server, though in the interest of performance you may -want to locate those images near the data center that you're booting in. -[boots via iPXE]:https://help.packet.net/technical/infrastructure/custom-ipxe +LinuxKit on Packet boots the `kernel+initrd` output from moby +via +[iPXE](https://help.packet.net/technical/infrastructure/custom-ipxe). iPXE +booting requires a HTTP server on which you can store your images. At +the moment there is no builtin support for this, although we are +working on this too. -Servers take several minutes to provision. During this time their -state can be seen from the Packet console. +A simple way to host files is via a simple local http server written in Go, e.g.: +```Go +package main + +import ( + "log" + "net/http" +) + +func main() { + // Simple static webserver: + log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))) +} ``` -$ linuxkit run packet --help -USAGE: linuxkit run packet [options] [name] -Options: +and then `go run` this in the directory with the `kernel+initrd`. + +The web server must be accessible from Packet. You can either run the +server on another Packet machine, or use tools +like [ngrok](https://ngrok.com/) to make it accessible via a reverse +proxy. + +You then specify the location of your http server using the +`-base-url` command line option. For example, to boot the +toplevel [linuxkit.yml](../linuxkit.yml) example: + +```sh +moby build linuxkit.yml +# run the web server +# run 'ngrok http 8080' in another window +PACKET_API_KEY= linuxkit run packet -base-url http://9b828514.ngrok.io -project-id linuxkit +``` + +**Note**: It may take several minutes to deploy a new server. If you +are attached to the console, you should see the BIOS and the boot +messages. + - -api-key string - Packet API key - -base-url string - Base URL that the kernel and initrd are served from. - -hostname string - Hostname of new instance (default "moby") - -img-name string - Overrides the prefix used to identify the files. Defaults to [name] - -machine string - Packet Machine Type (default "baremetal_0") - -project-id string - Packet Project ID - -zone string - Packet Zone (default "ams1") - ``` ## Console -If your LinuxKit system does not include an ssh or remote console -application, you can still connect to it via the Packet SOS ("Serial over SSH") -console. See https://help.packet.net/technical/networking/sos-rescue-mode -for details on that mode. +By default, `linuxkit run packet ...` will connect to the +Packet +[SOS ("Serial over SSH") console](https://help.packet.net/technical/networking/sos-rescue-mode). This +requires `ssh` access, i.e., you must have uploaded your SSH keys to +Packet beforehand. + +**Note**: We also require that the Packet SOS host is in your +`known_hosts` file, otherwise the connection to the console will +fail. There is a Packet SOS host per zone. + +You can disable the serial console access with the `-console=false` +command line option. + ## Disks At this moment the Linuxkit server boots from RAM, with no persistent -storage and there is no code that mounts disks. As a result, -when the Linuxkit image reboots, all is lost. +storage. We are working on adding persistent storage support on Packet. -Packet supports a [persistent iPXE] mode through its API -which would allow a server to come back up after a reboot -and re-start the PXE process. This is great for testing your -provisioning scripts. This is not yet available directly -through Linuxkit. - -[persistent iPXE]:https://help.packet.net/technical/infrastructure/custom-ipxe ## Networking