2015-05-27 23:32:43 +00:00
|
|
|
/*
|
|
|
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package util
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2015-05-28 18:45:08 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/pem"
|
2015-05-27 23:32:43 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
2015-05-28 18:45:08 +00:00
|
|
|
mathrand "math/rand"
|
2015-05-27 23:32:43 +00:00
|
|
|
"net"
|
|
|
|
"os"
|
2015-06-02 16:52:35 +00:00
|
|
|
"time"
|
2015-05-27 23:32:43 +00:00
|
|
|
|
|
|
|
"github.com/golang/glog"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
)
|
|
|
|
|
|
|
|
// TODO: Unit tests for this code, we can spin up a test SSH server with instructions here:
|
|
|
|
// https://godoc.org/golang.org/x/crypto/ssh#ServerConn
|
|
|
|
type SSHTunnel struct {
|
2015-05-28 04:38:21 +00:00
|
|
|
Config *ssh.ClientConfig
|
|
|
|
Host string
|
|
|
|
SSHPort string
|
|
|
|
running bool
|
|
|
|
sock net.Listener
|
|
|
|
client *ssh.Client
|
2015-05-27 23:32:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SSHTunnel) copyBytes(out io.Writer, in io.Reader) {
|
|
|
|
if _, err := io.Copy(out, in); err != nil {
|
|
|
|
glog.Errorf("Error in SSH tunnel: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-28 04:38:21 +00:00
|
|
|
func NewSSHTunnel(user, keyfile, host string) (*SSHTunnel, error) {
|
2015-05-27 23:32:43 +00:00
|
|
|
signer, err := MakePrivateKeySigner(keyfile)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
config := ssh.ClientConfig{
|
|
|
|
User: user,
|
|
|
|
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
|
|
|
}
|
|
|
|
return &SSHTunnel{
|
2015-05-28 04:38:21 +00:00
|
|
|
Config: &config,
|
|
|
|
Host: host,
|
|
|
|
SSHPort: "22",
|
2015-05-27 23:32:43 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SSHTunnel) Open() error {
|
|
|
|
var err error
|
2015-05-28 04:38:21 +00:00
|
|
|
s.client, err = ssh.Dial("tcp", net.JoinHostPort(s.Host, s.SSHPort), s.Config)
|
2015-05-27 23:32:43 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2015-05-28 04:38:21 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SSHTunnel) Dial(network, address string) (net.Conn, error) {
|
|
|
|
return s.client.Dial(network, address)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SSHTunnel) tunnel(conn net.Conn, remoteHost, remotePort string) error {
|
|
|
|
tunnel, err := s.client.Dial("tcp", net.JoinHostPort(remoteHost, remotePort))
|
2015-05-27 23:32:43 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
go s.copyBytes(tunnel, conn)
|
|
|
|
go s.copyBytes(conn, tunnel)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-05-28 04:38:21 +00:00
|
|
|
func (s *SSHTunnel) Close() error {
|
2015-05-27 23:32:43 +00:00
|
|
|
if err := s.client.Close(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func RunSSHCommand(cmd, host string, signer ssh.Signer) (string, string, int, error) {
|
|
|
|
// Setup the config, dial the server, and open a session.
|
|
|
|
config := &ssh.ClientConfig{
|
|
|
|
User: os.Getenv("USER"),
|
|
|
|
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
|
|
|
}
|
|
|
|
client, err := ssh.Dial("tcp", host, config)
|
|
|
|
if err != nil {
|
|
|
|
return "", "", 0, fmt.Errorf("error getting SSH client to host %s: '%v'", host, err)
|
|
|
|
}
|
|
|
|
session, err := client.NewSession()
|
|
|
|
if err != nil {
|
|
|
|
return "", "", 0, fmt.Errorf("error creating session to host %s: '%v'", host, err)
|
|
|
|
}
|
|
|
|
defer session.Close()
|
|
|
|
|
|
|
|
// Run the command.
|
|
|
|
code := 0
|
|
|
|
var bout, berr bytes.Buffer
|
|
|
|
session.Stdout, session.Stderr = &bout, &berr
|
|
|
|
if err = session.Run(cmd); err != nil {
|
|
|
|
// Check whether the command failed to run or didn't complete.
|
|
|
|
if exiterr, ok := err.(*ssh.ExitError); ok {
|
|
|
|
// If we got an ExitError and the exit code is nonzero, we'll
|
|
|
|
// consider the SSH itself successful (just that the command run
|
|
|
|
// errored on the host).
|
|
|
|
if code = exiterr.ExitStatus(); code != 0 {
|
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Some other kind of error happened (e.g. an IOError); consider the
|
|
|
|
// SSH unsuccessful.
|
|
|
|
err = fmt.Errorf("failed running `%s` on %s: '%v'", cmd, host, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return bout.String(), berr.String(), code, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func MakePrivateKeySigner(key string) (ssh.Signer, error) {
|
|
|
|
// Create an actual signer.
|
|
|
|
file, err := os.Open(key)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error opening SSH key %s: '%v'", key, err)
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
buffer, err := ioutil.ReadAll(file)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error reading SSH key %s: '%v'", key, err)
|
|
|
|
}
|
|
|
|
signer, err := ssh.ParsePrivateKey(buffer)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error parsing SSH key %s: '%v'", key, err)
|
|
|
|
}
|
|
|
|
return signer, nil
|
|
|
|
}
|
2015-05-28 04:38:21 +00:00
|
|
|
|
|
|
|
type SSHTunnelEntry struct {
|
|
|
|
Address string
|
|
|
|
Tunnel *SSHTunnel
|
|
|
|
}
|
|
|
|
|
|
|
|
type SSHTunnelList []SSHTunnelEntry
|
|
|
|
|
|
|
|
func MakeSSHTunnels(user, keyfile string, addresses []string) (SSHTunnelList, error) {
|
|
|
|
tunnels := []SSHTunnelEntry{}
|
|
|
|
for ix := range addresses {
|
|
|
|
addr := addresses[ix]
|
|
|
|
tunnel, err := NewSSHTunnel(user, keyfile, addr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
tunnels = append(tunnels, SSHTunnelEntry{addr, tunnel})
|
|
|
|
}
|
|
|
|
return tunnels, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l SSHTunnelList) Open() error {
|
|
|
|
for ix := range l {
|
|
|
|
if err := l[ix].Tunnel.Open(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-06-05 21:49:26 +00:00
|
|
|
// Close asynchronously closes all tunnels in the list after waiting for 1
|
|
|
|
// minute. Tunnels will still be open upon this function's return, but should
|
|
|
|
// no longer be used.
|
2015-06-02 16:52:35 +00:00
|
|
|
func (l SSHTunnelList) Close() {
|
2015-05-28 04:38:21 +00:00
|
|
|
for ix := range l {
|
2015-06-02 16:52:35 +00:00
|
|
|
entry := l[ix]
|
|
|
|
go func() {
|
|
|
|
time.Sleep(1 * time.Minute)
|
|
|
|
if err := entry.Tunnel.Close(); err != nil {
|
|
|
|
glog.Errorf("Failed to close tunnel %v: %v", entry, err)
|
|
|
|
}
|
|
|
|
}()
|
2015-05-28 04:38:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l SSHTunnelList) Dial(network, addr string) (net.Conn, error) {
|
|
|
|
if len(l) == 0 {
|
|
|
|
return nil, fmt.Errorf("Empty tunnel list.")
|
|
|
|
}
|
2015-05-28 18:45:08 +00:00
|
|
|
return l[mathrand.Int()%len(l)].Tunnel.Dial(network, addr)
|
2015-05-28 04:38:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (l SSHTunnelList) Has(addr string) bool {
|
|
|
|
for ix := range l {
|
|
|
|
if l[ix].Address == addr {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
2015-05-28 18:45:08 +00:00
|
|
|
|
|
|
|
func EncodePrivateKey(private *rsa.PrivateKey) []byte {
|
|
|
|
return pem.EncodeToMemory(&pem.Block{
|
|
|
|
Bytes: x509.MarshalPKCS1PrivateKey(private),
|
|
|
|
Type: "RSA PRIVATE KEY",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func EncodePublicKey(public *rsa.PublicKey) ([]byte, error) {
|
|
|
|
publicBytes, err := x509.MarshalPKIXPublicKey(public)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return pem.EncodeToMemory(&pem.Block{
|
|
|
|
Bytes: publicBytes,
|
|
|
|
Type: "PUBLIC KEY",
|
|
|
|
}), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func EncodeSSHKey(public *rsa.PublicKey) ([]byte, error) {
|
|
|
|
publicKey, err := ssh.NewPublicKey(public)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return ssh.MarshalAuthorizedKey(publicKey), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func GenerateKey(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) {
|
|
|
|
private, err := rsa.GenerateKey(rand.Reader, bits)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return private, &private.PublicKey, nil
|
|
|
|
}
|