mirror of
https://github.com/gravitational/teleport
synced 2024-10-21 17:53:28 +00:00
6d1c16f745
Updated services.ReverseTunnel to support type (proxy or node). For proxy types, which represent trusted cluster connections, when a services.ReverseTunnel is created, it's created on the remote side with name /reverseTunnels/example.com. For node types, services.ReverseTunnel is created on the main side as /reverseTunnels/{nodeUUID}.clusterName. Updated services.TunnelConn to support type (proxy or node). For proxy types, which represent trusted cluster connections, tunnel connections are created on the main side under /tunnelConnections/remote.example.com/{proxyUUID}-remote.example.com. For nodes, tunnel connections are created on the main side under /tunnelConnections/example.com/{proxyUUID}-example.com. This allows searching for tunnel connections by cluster then allows easily creating a set of proxies that are missing matching services.TunnelConn. The reverse tunnel server has been updated to handle heartbeats from proxies as well as nodes. Proxy heartbeat behavior has not changed. Heartbeats from nodes now add remote connections to the matching local site. In addition, the reverse tunnel server now proxies connection to the Auth Server for requests that are already authenticated (a second authentication to the Auth Server is required). For registration, nodes try and connect to the Auth Server to fetch host credentials. Upon failure, nodes now try and fallback to fetching host credentials from the web proxy. To establish a connection to an Auth Server, nodes first try and connect directly, and if the connection fails, fallback to obtaining a connection to the Auth Server through the reverse tunnel. If a connection is established directly, node startup behavior has not changed. If a node establishes a connection through the reverse tunnel, it creates an AgentPool that attempts to dial back to the cluster and establish a reverse tunnel. When nodes heartbeat, they also heartbeat if they are connected directly to the cluster or through a reverse tunnel. For nodes that are connected through a reverse tunnel, the proxy subsystem now directs the reverse tunnel server to establish a connection through the reverse tunnel instead of directly. When sending discovery requests, the domain field has been replaced with tunnelID. The tunnelID field is either the cluster name (same as before) for proxies, or {nodeUUID}.example.com for nodes.
1401 lines
40 KiB
Go
1401 lines
40 KiB
Go
/*
|
|
Copyright 2018 Gravitational, Inc.
|
|
|
|
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 integration
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rsa"
|
|
"crypto/x509/pkix"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/agent"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/lib/auth"
|
|
"github.com/gravitational/teleport/lib/auth/native"
|
|
"github.com/gravitational/teleport/lib/auth/testauthority"
|
|
"github.com/gravitational/teleport/lib/backend"
|
|
"github.com/gravitational/teleport/lib/backend/lite"
|
|
"github.com/gravitational/teleport/lib/client"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/events"
|
|
"github.com/gravitational/teleport/lib/reversetunnel"
|
|
"github.com/gravitational/teleport/lib/service"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/sshutils"
|
|
"github.com/gravitational/teleport/lib/teleagent"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
|
|
"github.com/gravitational/roundtrip"
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// SetTestTimeouts affects global timeouts inside Teleport, making connections
|
|
// work faster but consuming more CPU (useful for integration testing)
|
|
func SetTestTimeouts(t time.Duration) {
|
|
defaults.ReverseTunnelAgentHeartbeatPeriod = t
|
|
defaults.ServerKeepAliveTTL = t
|
|
defaults.SessionRefreshPeriod = t
|
|
defaults.HeartbeatCheckPeriod = t
|
|
defaults.CachePollPeriod = t
|
|
}
|
|
|
|
// TeleInstance represents an in-memory instance of a teleport
|
|
// process for testing
|
|
type TeleInstance struct {
|
|
// Secrets holds the keys (pub, priv and derived cert) of i instance
|
|
Secrets InstanceSecrets
|
|
|
|
// Slice of TCP ports used by Teleport services
|
|
Ports []int
|
|
|
|
// Hostname is the name of the host where instance is running
|
|
Hostname string
|
|
|
|
// Internal stuff...
|
|
Process *service.TeleportProcess
|
|
Config *service.Config
|
|
Tunnel reversetunnel.Server
|
|
Pool *reversetunnel.AgentPool
|
|
|
|
// Nodes is a list of additional nodes
|
|
// started with this instance
|
|
Nodes []*service.TeleportProcess
|
|
|
|
// UploadEventsC is a channel for upload events
|
|
UploadEventsC chan *events.UploadEvent
|
|
}
|
|
|
|
type User struct {
|
|
Username string `json:"username"`
|
|
AllowedLogins []string `json:"logins"`
|
|
Key *client.Key `json:"key"`
|
|
Roles []services.Role `json:"-"`
|
|
}
|
|
|
|
type InstanceSecrets struct {
|
|
// instance name (aka "site name")
|
|
SiteName string `json:"site_name"`
|
|
// instance keys+cert (reused for hostCA and userCA)
|
|
// PubKey is instance public key
|
|
PubKey []byte `json:"pub"`
|
|
// PrivKey is instance private key
|
|
PrivKey []byte `json:"priv"`
|
|
// Cert is SSH host certificate
|
|
Cert []byte `json:"cert"`
|
|
// TLSCACert is the certificate of the trusted certificate authority
|
|
TLSCACert []byte `json:"tls_ca_cert"`
|
|
// TLSCert is client TLS X509 certificate
|
|
TLSCert []byte `json:"tls_cert"`
|
|
// ListenAddr is a reverse tunnel listening port, allowing
|
|
// other sites to connect to i instance. Set to empty
|
|
// string if i instance is not allowing incoming tunnels
|
|
ListenAddr string `json:"tunnel_addr"`
|
|
// WebProxyAddr is address for web proxy
|
|
WebProxyAddr string `json:"web_proxy_addr"`
|
|
// list of users i instance trusts (key in the map is username)
|
|
Users map[string]*User `json:"users"`
|
|
}
|
|
|
|
func (s *InstanceSecrets) String() string {
|
|
bytes, _ := json.MarshalIndent(s, "", "\t")
|
|
return string(bytes)
|
|
}
|
|
|
|
// InstanceConfig is an instance configuration
|
|
type InstanceConfig struct {
|
|
// ClusterName is a cluster name of the instance
|
|
ClusterName string
|
|
// HostID is a host id of the instance
|
|
HostID string
|
|
// NodeName is a node name of the instance
|
|
NodeName string
|
|
// Ports is a list of assigned ports to use
|
|
Ports []int
|
|
// Priv is SSH private key of the instance
|
|
Priv []byte
|
|
// Pub is SSH public key of the instance
|
|
Pub []byte
|
|
// MultiplexProxy uses the same port for web and SSH reverse tunnel proxy
|
|
MultiplexProxy bool
|
|
}
|
|
|
|
// NewInstance creates a new Teleport process instance
|
|
func NewInstance(cfg InstanceConfig) *TeleInstance {
|
|
var err error
|
|
if len(cfg.Ports) < 5 {
|
|
fatalIf(fmt.Errorf("not enough free ports given: %v", cfg.Ports))
|
|
}
|
|
if cfg.NodeName == "" {
|
|
cfg.NodeName, err = os.Hostname()
|
|
fatalIf(err)
|
|
}
|
|
// generate instance secrets (keys):
|
|
keygen, err := native.New(context.TODO(), native.PrecomputeKeys(0))
|
|
fatalIf(err)
|
|
if cfg.Priv == nil || cfg.Pub == nil {
|
|
cfg.Priv, cfg.Pub, _ = keygen.GenerateKeyPair("")
|
|
}
|
|
rsaKey, err := ssh.ParseRawPrivateKey(cfg.Priv)
|
|
fatalIf(err)
|
|
|
|
tlsCAKey, tlsCACert, err := tlsca.GenerateSelfSignedCAWithPrivateKey(rsaKey.(*rsa.PrivateKey), pkix.Name{
|
|
CommonName: cfg.ClusterName,
|
|
Organization: []string{cfg.ClusterName},
|
|
}, nil, defaults.CATTL)
|
|
fatalIf(err)
|
|
|
|
cert, err := keygen.GenerateHostCert(services.HostCertParams{
|
|
PrivateCASigningKey: cfg.Priv,
|
|
PublicHostKey: cfg.Pub,
|
|
HostID: cfg.HostID,
|
|
NodeName: cfg.NodeName,
|
|
ClusterName: cfg.ClusterName,
|
|
Roles: teleport.Roles{teleport.RoleAdmin},
|
|
TTL: time.Duration(time.Hour * 24),
|
|
})
|
|
fatalIf(err)
|
|
tlsCA, err := tlsca.New(tlsCACert, tlsCAKey)
|
|
fatalIf(err)
|
|
cryptoPubKey, err := sshutils.CryptoPublicKey(cfg.Pub)
|
|
identity := tlsca.Identity{
|
|
Username: fmt.Sprintf("%v.%v", cfg.HostID, cfg.ClusterName),
|
|
Groups: []string{string(teleport.RoleAdmin)},
|
|
}
|
|
clock := clockwork.NewRealClock()
|
|
tlsCert, err := tlsCA.GenerateCertificate(tlsca.CertificateRequest{
|
|
Clock: clock,
|
|
PublicKey: cryptoPubKey,
|
|
Subject: identity.Subject(),
|
|
NotAfter: clock.Now().UTC().Add(time.Hour * 24),
|
|
})
|
|
fatalIf(err)
|
|
|
|
i := &TeleInstance{
|
|
Ports: cfg.Ports,
|
|
Hostname: cfg.NodeName,
|
|
UploadEventsC: make(chan *events.UploadEvent, 100),
|
|
}
|
|
secrets := InstanceSecrets{
|
|
SiteName: cfg.ClusterName,
|
|
PrivKey: cfg.Priv,
|
|
PubKey: cfg.Pub,
|
|
Cert: cert,
|
|
TLSCACert: tlsCACert,
|
|
TLSCert: tlsCert,
|
|
ListenAddr: net.JoinHostPort(cfg.NodeName, i.GetPortReverseTunnel()),
|
|
WebProxyAddr: net.JoinHostPort(cfg.NodeName, i.GetPortWeb()),
|
|
Users: make(map[string]*User),
|
|
}
|
|
if cfg.MultiplexProxy {
|
|
secrets.ListenAddr = secrets.WebProxyAddr
|
|
}
|
|
i.Secrets = secrets
|
|
return i
|
|
}
|
|
|
|
// GetRoles returns a list of roles to initiate for this secret
|
|
func (s *InstanceSecrets) GetRoles() []services.Role {
|
|
var roles []services.Role
|
|
for _, ca := range s.GetCAs() {
|
|
if ca.GetType() != services.UserCA {
|
|
continue
|
|
}
|
|
role := services.RoleForCertAuthority(ca)
|
|
role.SetLogins(services.Allow, s.AllowedLogins())
|
|
roles = append(roles, role)
|
|
}
|
|
return roles
|
|
}
|
|
|
|
// GetCAs return an array of CAs stored by the secrets object. In i
|
|
// case we always return hard-coded userCA + hostCA (and they share keys
|
|
// for simplicity)
|
|
func (s *InstanceSecrets) GetCAs() []services.CertAuthority {
|
|
hostCA := services.NewCertAuthority(services.HostCA, s.SiteName, [][]byte{s.PrivKey}, [][]byte{s.PubKey}, []string{})
|
|
hostCA.SetTLSKeyPairs([]services.TLSKeyPair{{Cert: s.TLSCACert, Key: s.PrivKey}})
|
|
return []services.CertAuthority{
|
|
hostCA,
|
|
services.NewCertAuthority(services.UserCA, s.SiteName, [][]byte{s.PrivKey}, [][]byte{s.PubKey}, []string{services.RoleNameForCertAuthority(s.SiteName)}),
|
|
}
|
|
}
|
|
|
|
func (s *InstanceSecrets) AllowedLogins() []string {
|
|
var logins []string
|
|
for i := range s.Users {
|
|
logins = append(logins, s.Users[i].AllowedLogins...)
|
|
}
|
|
return logins
|
|
}
|
|
|
|
func (s *InstanceSecrets) AsTrustedCluster(token string, roleMap services.RoleMap) services.TrustedCluster {
|
|
return &services.TrustedClusterV2{
|
|
Kind: services.KindTrustedCluster,
|
|
Version: services.V2,
|
|
Metadata: services.Metadata{
|
|
Name: s.SiteName,
|
|
},
|
|
Spec: services.TrustedClusterSpecV2{
|
|
Token: token,
|
|
Enabled: true,
|
|
ProxyAddress: s.WebProxyAddr,
|
|
ReverseTunnelAddress: s.ListenAddr,
|
|
RoleMap: roleMap,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (s *InstanceSecrets) AsSlice() []*InstanceSecrets {
|
|
return []*InstanceSecrets{s}
|
|
}
|
|
|
|
func (s *InstanceSecrets) GetIdentity() *auth.Identity {
|
|
i, err := auth.ReadIdentityFromKeyPair(&auth.PackedKeys{
|
|
Key: s.PrivKey,
|
|
Cert: s.Cert,
|
|
TLSCert: s.TLSCert,
|
|
TLSCACerts: [][]byte{s.TLSCACert},
|
|
})
|
|
fatalIf(err)
|
|
return i
|
|
}
|
|
|
|
func (i *TeleInstance) GetPortSSHInt() int {
|
|
return i.Ports[0]
|
|
}
|
|
|
|
func (i *TeleInstance) GetPortSSH() string {
|
|
return strconv.Itoa(i.GetPortSSHInt())
|
|
}
|
|
|
|
func (i *TeleInstance) GetPortAuth() string {
|
|
return strconv.Itoa(i.Ports[1])
|
|
}
|
|
|
|
func (i *TeleInstance) GetPortProxy() string {
|
|
return strconv.Itoa(i.Ports[2])
|
|
}
|
|
|
|
func (i *TeleInstance) GetPortWeb() string {
|
|
return strconv.Itoa(i.Ports[3])
|
|
}
|
|
|
|
func (i *TeleInstance) GetPortReverseTunnel() string {
|
|
return strconv.Itoa(i.Ports[4])
|
|
}
|
|
|
|
// GetSiteAPI() is a helper which returns an API endpoint to a site with
|
|
// a given name. i endpoint implements HTTP-over-SSH access to the
|
|
// site's auth server.
|
|
func (i *TeleInstance) GetSiteAPI(siteName string) auth.ClientI {
|
|
siteTunnel, err := i.Tunnel.GetSite(siteName)
|
|
if err != nil {
|
|
log.Warn(err)
|
|
return nil
|
|
}
|
|
siteAPI, err := siteTunnel.GetClient()
|
|
if err != nil {
|
|
log.Warn(err)
|
|
return nil
|
|
}
|
|
return siteAPI
|
|
}
|
|
|
|
// Create creates a new instance of Teleport which trusts a lsit of other clusters (other
|
|
// instances)
|
|
func (i *TeleInstance) Create(trustedSecrets []*InstanceSecrets, enableSSH bool, console io.Writer) error {
|
|
tconf := service.MakeDefaultConfig()
|
|
tconf.SSH.Enabled = enableSSH
|
|
tconf.Console = console
|
|
tconf.Proxy.DisableWebService = true
|
|
tconf.Proxy.DisableWebInterface = true
|
|
return i.CreateEx(trustedSecrets, tconf)
|
|
}
|
|
|
|
// UserCreds holds user client credentials
|
|
type UserCreds struct {
|
|
// Key is user client key and certificate
|
|
Key client.Key
|
|
// HostCA is a trusted host certificate authority
|
|
HostCA services.CertAuthority
|
|
}
|
|
|
|
// SetupUserCreds sets up user credentials for client
|
|
func SetupUserCreds(tc *client.TeleportClient, proxyHost string, creds UserCreds) error {
|
|
_, err := tc.AddKey(proxyHost, &creds.Key)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
err = tc.AddTrustedCA(creds.HostCA)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetupUser sets up user in the cluster
|
|
func SetupUser(process *service.TeleportProcess, username string, roles []services.Role) error {
|
|
auth := process.GetAuthServer()
|
|
teleUser, err := services.NewUser(username)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if len(roles) == 0 {
|
|
role := services.RoleForUser(teleUser)
|
|
role.SetLogins(services.Allow, []string{username})
|
|
|
|
// allow tests to forward agent, still needs to be passed in client
|
|
roleOptions := role.GetOptions()
|
|
roleOptions.ForwardAgent = services.NewBool(true)
|
|
role.SetOptions(roleOptions)
|
|
|
|
err = auth.UpsertRole(role)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
teleUser.AddRole(role.GetMetadata().Name)
|
|
roles = append(roles, role)
|
|
} else {
|
|
for _, role := range roles {
|
|
err := auth.UpsertRole(role)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
teleUser.AddRole(role.GetName())
|
|
}
|
|
}
|
|
err = auth.UpsertUser(teleUser)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GenerateUserCreds generates key to be used by client
|
|
func GenerateUserCreds(process *service.TeleportProcess, username string) (*UserCreds, error) {
|
|
priv, pub, err := testauthority.New().GenerateKeyPair("")
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
a := process.GetAuthServer()
|
|
sshCert, x509Cert, err := a.GenerateUserCerts(pub, username, time.Hour, teleport.CertificateFormatStandard)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
clusterName, err := a.GetClusterName()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
ca, err := a.GetCertAuthority(services.CertAuthID{
|
|
Type: services.HostCA,
|
|
DomainName: clusterName.GetClusterName(),
|
|
}, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return &UserCreds{
|
|
HostCA: ca,
|
|
Key: client.Key{
|
|
Priv: priv,
|
|
Pub: pub,
|
|
Cert: sshCert,
|
|
TLSCert: x509Cert,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// GenerateConfig generates instance config
|
|
func (i *TeleInstance) GenerateConfig(trustedSecrets []*InstanceSecrets, tconf *service.Config) (*service.Config, error) {
|
|
var err error
|
|
dataDir, err := ioutil.TempDir("", "cluster-"+i.Secrets.SiteName)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if tconf == nil {
|
|
tconf = service.MakeDefaultConfig()
|
|
}
|
|
tconf.DataDir = dataDir
|
|
tconf.UploadEventsC = i.UploadEventsC
|
|
tconf.CachePolicy.Enabled = true
|
|
tconf.Auth.ClusterName, err = services.NewClusterName(services.ClusterNameSpecV2{
|
|
ClusterName: i.Secrets.SiteName,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
tconf.Auth.StaticTokens, err = services.NewStaticTokens(services.StaticTokensSpecV2{
|
|
StaticTokens: []services.ProvisionTokenV1{
|
|
{
|
|
Roles: []teleport.Role{teleport.RoleNode, teleport.RoleProxy, teleport.RoleTrustedCluster},
|
|
Token: "token",
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
tconf.Auth.Authorities = append(tconf.Auth.Authorities, i.Secrets.GetCAs()...)
|
|
tconf.Identities = append(tconf.Identities, i.Secrets.GetIdentity())
|
|
for _, trusted := range trustedSecrets {
|
|
tconf.Auth.Authorities = append(tconf.Auth.Authorities, trusted.GetCAs()...)
|
|
tconf.Auth.Roles = append(tconf.Auth.Roles, trusted.GetRoles()...)
|
|
tconf.Identities = append(tconf.Identities, trusted.GetIdentity())
|
|
if trusted.ListenAddr != "" {
|
|
tconf.ReverseTunnels = []services.ReverseTunnel{
|
|
services.NewReverseTunnel(trusted.SiteName, []string{trusted.ListenAddr}),
|
|
}
|
|
}
|
|
}
|
|
tconf.Proxy.ReverseTunnelListenAddr.Addr = i.Secrets.ListenAddr
|
|
tconf.HostUUID = i.Secrets.GetIdentity().ID.HostUUID
|
|
tconf.SSH.Addr.Addr = net.JoinHostPort(i.Hostname, i.GetPortSSH())
|
|
tconf.SSH.PublicAddrs = []utils.NetAddr{
|
|
utils.NetAddr{
|
|
AddrNetwork: "tcp",
|
|
Addr: Loopback,
|
|
},
|
|
utils.NetAddr{
|
|
AddrNetwork: "tcp",
|
|
Addr: Host,
|
|
},
|
|
}
|
|
tconf.Auth.SSHAddr.Addr = net.JoinHostPort(i.Hostname, i.GetPortAuth())
|
|
tconf.Proxy.SSHAddr.Addr = net.JoinHostPort(i.Hostname, i.GetPortProxy())
|
|
tconf.Proxy.WebAddr.Addr = net.JoinHostPort(i.Hostname, i.GetPortWeb())
|
|
tconf.Proxy.PublicAddrs = []utils.NetAddr{
|
|
utils.NetAddr{
|
|
AddrNetwork: "tcp",
|
|
Addr: i.Hostname,
|
|
},
|
|
utils.NetAddr{
|
|
AddrNetwork: "tcp",
|
|
Addr: Loopback,
|
|
},
|
|
utils.NetAddr{
|
|
AddrNetwork: "tcp",
|
|
Addr: Host,
|
|
},
|
|
}
|
|
tconf.AuthServers = append(tconf.AuthServers, tconf.Auth.SSHAddr)
|
|
tconf.Auth.StorageConfig = backend.Config{
|
|
Type: lite.GetName(),
|
|
Params: backend.Params{"path": dataDir + string(os.PathListSeparator) + defaults.BackendDir, "poll_stream_period": 50 * time.Millisecond},
|
|
}
|
|
|
|
tconf.Keygen = testauthority.New()
|
|
i.Config = tconf
|
|
return tconf, nil
|
|
}
|
|
|
|
// CreateEx creates a new instance of Teleport which trusts a list of other clusters (other
|
|
// instances)
|
|
//
|
|
// Unlike Create() it allows for greater customization because it accepts
|
|
// a full Teleport config structure
|
|
func (i *TeleInstance) CreateEx(trustedSecrets []*InstanceSecrets, tconf *service.Config) error {
|
|
tconf, err := i.GenerateConfig(trustedSecrets, tconf)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
i.Config = tconf
|
|
i.Process, err = service.NewTeleport(tconf)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// if the auth server is not enabled, nothing more to do be done
|
|
if !tconf.Auth.Enabled {
|
|
return nil
|
|
}
|
|
|
|
// if this instance contains an auth server, configure the auth server as well.
|
|
// create users and roles if they don't exist, or sign their keys if they're
|
|
// already present
|
|
auth := i.Process.GetAuthServer()
|
|
|
|
for _, user := range i.Secrets.Users {
|
|
teleUser, err := services.NewUser(user.Username)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
var roles []services.Role
|
|
if len(user.Roles) == 0 {
|
|
role := services.RoleForUser(teleUser)
|
|
role.SetLogins(services.Allow, user.AllowedLogins)
|
|
|
|
// allow tests to forward agent, still needs to be passed in client
|
|
roleOptions := role.GetOptions()
|
|
roleOptions.ForwardAgent = services.NewBool(true)
|
|
role.SetOptions(roleOptions)
|
|
|
|
err = auth.UpsertRole(role)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
teleUser.AddRole(role.GetMetadata().Name)
|
|
roles = append(roles, role)
|
|
} else {
|
|
roles = user.Roles
|
|
for _, role := range user.Roles {
|
|
err := auth.UpsertRole(role)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
teleUser.AddRole(role.GetName())
|
|
}
|
|
}
|
|
err = auth.UpsertUser(teleUser)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
// if user keys are not present, auto-geneate keys:
|
|
if user.Key == nil || len(user.Key.Pub) == 0 {
|
|
priv, pub, _ := tconf.Keygen.GenerateKeyPair("")
|
|
user.Key = &client.Key{
|
|
Priv: priv,
|
|
Pub: pub,
|
|
}
|
|
}
|
|
// sign user's keys:
|
|
ttl := 24 * time.Hour
|
|
user.Key.Cert, user.Key.TLSCert, err = auth.GenerateUserCerts(user.Key.Pub, teleUser.GetName(), ttl, teleport.CertificateFormatStandard)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StartNode starts a SSH node and connects it to the cluster.
|
|
func (i *TeleInstance) StartNode(tconf *service.Config) (*service.TeleportProcess, error) {
|
|
dataDir, err := ioutil.TempDir("", "cluster-"+i.Secrets.SiteName)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
tconf.DataDir = dataDir
|
|
|
|
authServer := utils.MustParseAddr(net.JoinHostPort(i.Hostname, i.GetPortAuth()))
|
|
tconf.AuthServers = append(tconf.AuthServers, *authServer)
|
|
tconf.Token = "token"
|
|
tconf.UploadEventsC = i.UploadEventsC
|
|
var ttl time.Duration
|
|
tconf.CachePolicy = service.CachePolicy{
|
|
Enabled: true,
|
|
RecentTTL: &ttl,
|
|
}
|
|
tconf.SSH.PublicAddrs = []utils.NetAddr{
|
|
utils.NetAddr{
|
|
AddrNetwork: "tcp",
|
|
Addr: Loopback,
|
|
},
|
|
utils.NetAddr{
|
|
AddrNetwork: "tcp",
|
|
Addr: Host,
|
|
},
|
|
}
|
|
tconf.Auth.Enabled = false
|
|
tconf.Proxy.Enabled = false
|
|
|
|
// Create a new Teleport process and add it to the list of nodes that
|
|
// compose this "cluster".
|
|
process, err := service.NewTeleport(tconf)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
i.Nodes = append(i.Nodes, process)
|
|
|
|
// Build a list of expected events to wait for before unblocking based off
|
|
// the configuration passed in.
|
|
expectedEvents := []string{
|
|
service.NodeSSHReady,
|
|
}
|
|
|
|
// Start the process and block until the expected events have arrived.
|
|
receivedEvents, err := startAndWait(process, expectedEvents)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
log.Debugf("Teleport node (in instance %v) started: %v/%v events received.",
|
|
i.Secrets.SiteName, len(expectedEvents), len(receivedEvents))
|
|
return process, nil
|
|
}
|
|
|
|
// StartNodeAndProxy starts a SSH node and a Proxy Server and connects it to
|
|
// the cluster.
|
|
func (i *TeleInstance) StartNodeAndProxy(name string, sshPort, proxyWebPort, proxySSHPort int) error {
|
|
dataDir, err := ioutil.TempDir("", "cluster-"+i.Secrets.SiteName)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
tconf := service.MakeDefaultConfig()
|
|
|
|
authServer := utils.MustParseAddr(net.JoinHostPort(i.Hostname, i.GetPortAuth()))
|
|
tconf.AuthServers = append(tconf.AuthServers, *authServer)
|
|
tconf.Token = "token"
|
|
tconf.HostUUID = name
|
|
tconf.Hostname = name
|
|
tconf.UploadEventsC = i.UploadEventsC
|
|
tconf.DataDir = dataDir
|
|
var ttl time.Duration
|
|
tconf.CachePolicy = service.CachePolicy{
|
|
Enabled: true,
|
|
RecentTTL: &ttl,
|
|
}
|
|
|
|
tconf.Auth.Enabled = false
|
|
|
|
tconf.Proxy.Enabled = true
|
|
tconf.Proxy.SSHAddr.Addr = net.JoinHostPort(i.Hostname, fmt.Sprintf("%v", proxySSHPort))
|
|
tconf.Proxy.WebAddr.Addr = net.JoinHostPort(i.Hostname, fmt.Sprintf("%v", proxyWebPort))
|
|
tconf.Proxy.DisableReverseTunnel = true
|
|
tconf.Proxy.DisableWebService = true
|
|
|
|
tconf.SSH.Enabled = true
|
|
tconf.SSH.Addr.Addr = net.JoinHostPort(i.Hostname, fmt.Sprintf("%v", sshPort))
|
|
tconf.SSH.PublicAddrs = []utils.NetAddr{
|
|
utils.NetAddr{
|
|
AddrNetwork: "tcp",
|
|
Addr: Loopback,
|
|
},
|
|
utils.NetAddr{
|
|
AddrNetwork: "tcp",
|
|
Addr: Host,
|
|
},
|
|
}
|
|
|
|
// Create a new Teleport process and add it to the list of nodes that
|
|
// compose this "cluster".
|
|
process, err := service.NewTeleport(tconf)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
i.Nodes = append(i.Nodes, process)
|
|
|
|
// Build a list of expected events to wait for before unblocking based off
|
|
// the configuration passed in.
|
|
expectedEvents := []string{
|
|
service.ProxySSHReady,
|
|
service.NodeSSHReady,
|
|
}
|
|
|
|
// Start the process and block until the expected events have arrived.
|
|
receivedEvents, err := startAndWait(process, expectedEvents)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
log.Debugf("Teleport node and proxy (in instance %v) started: %v/%v events received.",
|
|
i.Secrets.SiteName, len(expectedEvents), len(receivedEvents))
|
|
return nil
|
|
}
|
|
|
|
// ProxyConfig is a set of configuration parameters for Proxy
|
|
type ProxyConfig struct {
|
|
// Name is a proxy name
|
|
Name string
|
|
// SSHPort is SSH proxy port
|
|
SSHPort int
|
|
// WebPort is web proxy port
|
|
WebPort int
|
|
// ReverseTunnelPort is a port for reverse tunnel addresses
|
|
ReverseTunnelPort int
|
|
}
|
|
|
|
// StartProxy starts another Proxy Server and connects it to the cluster.
|
|
func (i *TeleInstance) StartProxy(cfg ProxyConfig) error {
|
|
dataDir, err := ioutil.TempDir("", "cluster-"+i.Secrets.SiteName+"-"+cfg.Name)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
tconf := service.MakeDefaultConfig()
|
|
|
|
authServer := utils.MustParseAddr(net.JoinHostPort(i.Hostname, i.GetPortAuth()))
|
|
tconf.AuthServers = append(tconf.AuthServers, *authServer)
|
|
tconf.CachePolicy = service.CachePolicy{Enabled: true}
|
|
tconf.DataDir = dataDir
|
|
tconf.UploadEventsC = i.UploadEventsC
|
|
tconf.HostUUID = cfg.Name
|
|
tconf.Hostname = cfg.Name
|
|
tconf.Token = "token"
|
|
|
|
tconf.Auth.Enabled = false
|
|
|
|
tconf.SSH.Enabled = false
|
|
|
|
tconf.Proxy.Enabled = true
|
|
tconf.Proxy.SSHAddr.Addr = net.JoinHostPort(i.Hostname, fmt.Sprintf("%v", cfg.SSHPort))
|
|
tconf.Proxy.PublicAddrs = []utils.NetAddr{
|
|
utils.NetAddr{
|
|
AddrNetwork: "tcp",
|
|
Addr: Loopback,
|
|
},
|
|
utils.NetAddr{
|
|
AddrNetwork: "tcp",
|
|
Addr: Host,
|
|
},
|
|
}
|
|
tconf.Proxy.ReverseTunnelListenAddr.Addr = net.JoinHostPort(i.Hostname, fmt.Sprintf("%v", cfg.ReverseTunnelPort))
|
|
tconf.Proxy.WebAddr.Addr = net.JoinHostPort(i.Hostname, fmt.Sprintf("%v", cfg.WebPort))
|
|
tconf.Proxy.DisableReverseTunnel = false
|
|
tconf.Proxy.DisableWebService = true
|
|
|
|
// Create a new Teleport process and add it to the list of nodes that
|
|
// compose this "cluster".
|
|
process, err := service.NewTeleport(tconf)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
i.Nodes = append(i.Nodes, process)
|
|
|
|
// Build a list of expected events to wait for before unblocking based off
|
|
// the configuration passed in.
|
|
expectedEvents := []string{
|
|
service.ProxyReverseTunnelReady,
|
|
service.ProxySSHReady,
|
|
}
|
|
|
|
// Start the process and block until the expected events have arrived.
|
|
receivedEvents, err := startAndWait(process, expectedEvents)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
log.Debugf("Teleport proxy (in instance %v) started: %v/%v events received.",
|
|
i.Secrets.SiteName, len(expectedEvents), len(receivedEvents))
|
|
return nil
|
|
}
|
|
|
|
// Reset re-creates the teleport instance based on the same configuration
|
|
// This is needed if you want to stop the instance, reset it and start again
|
|
func (i *TeleInstance) Reset() (err error) {
|
|
i.Process, err = service.NewTeleport(i.Config)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddUserUserWithRole adds user with assigned role
|
|
func (i *TeleInstance) AddUserWithRole(username string, role services.Role) *User {
|
|
user := &User{
|
|
Username: username,
|
|
Roles: []services.Role{role},
|
|
}
|
|
i.Secrets.Users[username] = user
|
|
return user
|
|
}
|
|
|
|
// Adds a new user into i Teleport instance. 'mappings' is a comma-separated
|
|
// list of OS users
|
|
func (i *TeleInstance) AddUser(username string, mappings []string) *User {
|
|
log.Infof("teleInstance.AddUser(%v) mapped to %v", username, mappings)
|
|
if mappings == nil {
|
|
mappings = make([]string, 0)
|
|
}
|
|
user := &User{
|
|
Username: username,
|
|
AllowedLogins: mappings,
|
|
}
|
|
i.Secrets.Users[username] = user
|
|
return user
|
|
}
|
|
|
|
// Start will start the TeleInstance and then block until it is ready to
|
|
// process requests based off the passed in configuration.
|
|
func (i *TeleInstance) Start() error {
|
|
// Build a list of expected events to wait for before unblocking based off
|
|
// the configuration passed in.
|
|
expectedEvents := []string{}
|
|
if i.Config.Auth.Enabled {
|
|
expectedEvents = append(expectedEvents, service.AuthTLSReady)
|
|
}
|
|
if i.Config.Proxy.Enabled {
|
|
expectedEvents = append(expectedEvents, service.ProxyReverseTunnelReady)
|
|
expectedEvents = append(expectedEvents, service.ProxySSHReady)
|
|
expectedEvents = append(expectedEvents, service.ProxyAgentPoolReady)
|
|
if !i.Config.Proxy.DisableWebService {
|
|
expectedEvents = append(expectedEvents, service.ProxyWebServerReady)
|
|
}
|
|
}
|
|
if i.Config.SSH.Enabled {
|
|
expectedEvents = append(expectedEvents, service.NodeSSHReady)
|
|
}
|
|
|
|
// Start the process and block until the expected events have arrived.
|
|
receivedEvents, err := startAndWait(i.Process, expectedEvents)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// Extract and set reversetunnel.Server and reversetunnel.AgentPool upon
|
|
// receipt of a ProxyReverseTunnelReady and ProxyAgentPoolReady respectively.
|
|
for _, re := range receivedEvents {
|
|
switch re.Name {
|
|
case service.ProxyReverseTunnelReady:
|
|
ts, ok := re.Payload.(reversetunnel.Server)
|
|
if ok {
|
|
i.Tunnel = ts
|
|
}
|
|
case service.ProxyAgentPoolReady:
|
|
ap, ok := re.Payload.(*reversetunnel.AgentPool)
|
|
if ok {
|
|
i.Pool = ap
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Debugf("Teleport instance %v started: %v/%v events received.",
|
|
i.Secrets.SiteName, len(receivedEvents), len(expectedEvents))
|
|
return nil
|
|
}
|
|
|
|
// ClientConfig is a client configuration
|
|
type ClientConfig struct {
|
|
// Login is SSH login name
|
|
Login string
|
|
// Cluster is a cluster name to connect to
|
|
Cluster string
|
|
// Host string is a target host to connect to
|
|
Host string
|
|
// Port is a target port to connect to
|
|
Port int
|
|
// Proxy is an optional alternative proxy to use
|
|
Proxy *ProxyConfig
|
|
// ForwardAgent controls if the client requests it's agent be forwarded to
|
|
// the server.
|
|
ForwardAgent bool
|
|
}
|
|
|
|
// NewClientWithCreds creates client with credentials
|
|
func (i *TeleInstance) NewClientWithCreds(cfg ClientConfig, creds UserCreds) (tc *client.TeleportClient, err error) {
|
|
clt, err := i.NewUnauthenticatedClient(cfg)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
err = SetupUserCreds(clt, i.Config.Proxy.SSHAddr.Addr, creds)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return clt, nil
|
|
}
|
|
|
|
// NewUnauthenticatedClient returns a fully configured and pre-authenticated client
|
|
// (pre-authenticated with server CAs and signed session key)
|
|
func (i *TeleInstance) NewUnauthenticatedClient(cfg ClientConfig) (tc *client.TeleportClient, err error) {
|
|
keyDir, err := ioutil.TempDir(i.Config.DataDir, "tsh")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
proxyConf := &i.Config.Proxy
|
|
proxyHost, _, err := net.SplitHostPort(proxyConf.SSHAddr.Addr)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
var webProxyAddr string
|
|
var sshProxyAddr string
|
|
|
|
if cfg.Proxy == nil {
|
|
webProxyAddr = proxyConf.WebAddr.Addr
|
|
sshProxyAddr = proxyConf.SSHAddr.Addr
|
|
} else {
|
|
webProxyAddr = net.JoinHostPort(proxyHost, strconv.Itoa(cfg.Proxy.WebPort))
|
|
sshProxyAddr = net.JoinHostPort(proxyHost, strconv.Itoa(cfg.Proxy.SSHPort))
|
|
}
|
|
|
|
cconf := &client.Config{
|
|
Username: cfg.Login,
|
|
Host: cfg.Host,
|
|
HostPort: cfg.Port,
|
|
HostLogin: cfg.Login,
|
|
InsecureSkipVerify: true,
|
|
KeysDir: keyDir,
|
|
SiteName: cfg.Cluster,
|
|
ForwardAgent: cfg.ForwardAgent,
|
|
WebProxyAddr: webProxyAddr,
|
|
SSHProxyAddr: sshProxyAddr,
|
|
}
|
|
|
|
return client.NewClient(cconf)
|
|
}
|
|
|
|
// NewClient returns a fully configured and pre-authenticated client
|
|
// (pre-authenticated with server CAs and signed session key)
|
|
func (i *TeleInstance) NewClient(cfg ClientConfig) (*client.TeleportClient, error) {
|
|
tc, err := i.NewUnauthenticatedClient(cfg)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// configures the client authenticate using the keys from 'secrets':
|
|
user, ok := i.Secrets.Users[cfg.Login]
|
|
if !ok {
|
|
return nil, trace.BadParameter("unknown login %q", cfg.Login)
|
|
}
|
|
if user.Key == nil {
|
|
return nil, trace.BadParameter("user %q has no key", cfg.Login)
|
|
}
|
|
_, err = tc.AddKey(cfg.Host, user.Key)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
// tell the client to trust given CAs (from secrets). this is the
|
|
// equivalent of 'known hosts' in openssh
|
|
cas := i.Secrets.GetCAs()
|
|
for i := range cas {
|
|
err = tc.AddTrustedCA(cas[i])
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
}
|
|
return tc, nil
|
|
}
|
|
|
|
// StopProxy loops over the extra nodes in a TeleInstance and stops all
|
|
// nodes where the proxy server is enabled.
|
|
func (i *TeleInstance) StopProxy() error {
|
|
var errors []error
|
|
|
|
for _, p := range i.Nodes {
|
|
if p.Config.Proxy.Enabled {
|
|
if err := p.Close(); err != nil {
|
|
errors = append(errors, err)
|
|
log.Errorf("Failed closing extra proxy: %v.", err)
|
|
}
|
|
if err := p.Wait(); err != nil {
|
|
errors = append(errors, err)
|
|
log.Errorf("Failed to stop extra proxy: %v.", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return trace.NewAggregate(errors...)
|
|
}
|
|
|
|
// StopNodes stops additional nodes
|
|
func (i *TeleInstance) StopNodes() error {
|
|
var errors []error
|
|
for _, node := range i.Nodes {
|
|
if err := node.Close(); err != nil {
|
|
errors = append(errors, err)
|
|
log.Errorf("failed closing extra node %v", err)
|
|
}
|
|
if err := node.Wait(); err != nil {
|
|
errors = append(errors, err)
|
|
log.Errorf("failed stopping extra node %v", err)
|
|
}
|
|
}
|
|
return trace.NewAggregate(errors...)
|
|
}
|
|
|
|
func (i *TeleInstance) Stop(removeData bool) error {
|
|
if i.Config != nil && removeData {
|
|
err := os.RemoveAll(i.Config.DataDir)
|
|
if err != nil {
|
|
log.Error("failed removing temporary local Teleport directory", err)
|
|
}
|
|
}
|
|
|
|
log.Infof("Asking Teleport to stop")
|
|
err := i.Process.Close()
|
|
if err != nil {
|
|
log.Error(err)
|
|
return trace.Wrap(err)
|
|
}
|
|
defer func() {
|
|
log.Infof("Teleport instance '%v' stopped!", i.Secrets.SiteName)
|
|
}()
|
|
return i.Process.Wait()
|
|
}
|
|
|
|
func startAndWait(process *service.TeleportProcess, expectedEvents []string) ([]service.Event, error) {
|
|
// register to listen for all ready events on the broadcast channel
|
|
broadcastCh := make(chan service.Event)
|
|
for _, eventName := range expectedEvents {
|
|
process.WaitForEvent(context.TODO(), eventName, broadcastCh)
|
|
}
|
|
|
|
// start the process
|
|
err := process.Start()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// wait for all events to arrive or a timeout. if all the expected events
|
|
// from above are not received, this instance will not start
|
|
receivedEvents := []service.Event{}
|
|
timeoutCh := time.After(10 * time.Second)
|
|
|
|
for idx := 0; idx < len(expectedEvents); idx++ {
|
|
select {
|
|
case e := <-broadcastCh:
|
|
receivedEvents = append(receivedEvents, e)
|
|
case <-timeoutCh:
|
|
return nil, trace.BadParameter("timed out, only %v/%v events received. received: %v, expected: %v",
|
|
len(receivedEvents), len(expectedEvents), receivedEvents, expectedEvents)
|
|
}
|
|
}
|
|
|
|
// Not all services follow a non-blocking Start/Wait pattern. This means a
|
|
// *Ready event may be emit slightly before the service actually starts for
|
|
// blocking services. Long term those services should be re-factored, until
|
|
// then sleep for 250ms to handle this situation.
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
return receivedEvents, nil
|
|
}
|
|
|
|
type proxyServer struct {
|
|
sync.Mutex
|
|
count int
|
|
}
|
|
|
|
// ServeHTTP only accepts the CONNECT verb and will tunnel your connection to
|
|
// the specified host. Also tracks the number of connections that it proxies for
|
|
// debugging purposes.
|
|
func (p *proxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Validate http connect parameters.
|
|
if r.Method != http.MethodConnect {
|
|
trace.WriteError(w, trace.BadParameter("%v not supported", r.Method))
|
|
return
|
|
}
|
|
if r.Host == "" {
|
|
trace.WriteError(w, trace.BadParameter("host not set"))
|
|
return
|
|
}
|
|
|
|
// Dial to the target host, this is done before hijacking the connection to
|
|
// ensure the target host is accessible.
|
|
dconn, err := net.Dial("tcp", r.Host)
|
|
if err != nil {
|
|
trace.WriteError(w, err)
|
|
return
|
|
}
|
|
defer dconn.Close()
|
|
|
|
// Once the client receives 200 OK, the rest of the data will no longer be
|
|
// http, but whatever protocol is being tunneled.
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
// Hijack request so we can get underlying connection.
|
|
hj, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
trace.WriteError(w, trace.AccessDenied("unable to hijack connection"))
|
|
return
|
|
}
|
|
sconn, _, err := hj.Hijack()
|
|
if err != nil {
|
|
trace.WriteError(w, err)
|
|
return
|
|
}
|
|
defer sconn.Close()
|
|
|
|
// Success, we're proxying data now.
|
|
p.Lock()
|
|
p.count = p.count + 1
|
|
p.Unlock()
|
|
|
|
// Copy from src to dst and dst to src.
|
|
errc := make(chan error, 2)
|
|
replicate := func(dst io.Writer, src io.Reader) {
|
|
_, err := io.Copy(dst, src)
|
|
errc <- err
|
|
}
|
|
go replicate(sconn, dconn)
|
|
go replicate(dconn, sconn)
|
|
|
|
// Wait until done, error, or 10 second.
|
|
select {
|
|
case <-time.After(10 * time.Second):
|
|
case <-errc:
|
|
}
|
|
}
|
|
|
|
// Count returns the number of connections that have been proxied.
|
|
func (p *proxyServer) Count() int {
|
|
p.Lock()
|
|
defer p.Unlock()
|
|
return p.count
|
|
}
|
|
|
|
// discardServer is a SSH server that discards SSH exec requests and starts
|
|
// with the passed in host signer.
|
|
type discardServer struct {
|
|
sshServer *sshutils.Server
|
|
}
|
|
|
|
func newDiscardServer(host string, port int, hostSigner ssh.Signer) (*discardServer, error) {
|
|
ds := &discardServer{}
|
|
|
|
// create underlying ssh server
|
|
sshServer, err := sshutils.NewServer(
|
|
"integration-discard-server",
|
|
utils.NetAddr{AddrNetwork: "tcp", Addr: fmt.Sprintf("%v:%v", host, port)},
|
|
ds,
|
|
[]ssh.Signer{hostSigner},
|
|
sshutils.AuthMethods{
|
|
PublicKey: ds.userKeyAuth,
|
|
},
|
|
sshutils.SetInsecureSkipHostValidation(),
|
|
)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
ds.sshServer = sshServer
|
|
|
|
return ds, nil
|
|
}
|
|
|
|
func (s *discardServer) userKeyAuth(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *discardServer) Start() error {
|
|
return s.sshServer.Start()
|
|
}
|
|
|
|
func (s *discardServer) Stop() {
|
|
s.sshServer.Close()
|
|
}
|
|
|
|
func (s *discardServer) HandleNewChan(conn net.Conn, sconn *ssh.ServerConn, newChannel ssh.NewChannel) {
|
|
channel, reqs, err := newChannel.Accept()
|
|
if err != nil {
|
|
sconn.Close()
|
|
conn.Close()
|
|
return
|
|
}
|
|
|
|
go s.handleChannel(channel, reqs)
|
|
}
|
|
|
|
func (s *discardServer) handleChannel(channel ssh.Channel, reqs <-chan *ssh.Request) {
|
|
defer channel.Close()
|
|
|
|
for {
|
|
select {
|
|
case req := <-reqs:
|
|
if req == nil {
|
|
return
|
|
}
|
|
if req.Type == "exec" {
|
|
successPayload := ssh.Marshal(struct{ C uint32 }{C: uint32(0)})
|
|
channel.SendRequest("exit-status", false, successPayload)
|
|
if req.WantReply {
|
|
req.Reply(true, nil)
|
|
}
|
|
return
|
|
}
|
|
if req.WantReply {
|
|
req.Reply(true, nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// commandOptions controls how the SSH command is built.
|
|
type commandOptions struct {
|
|
forwardAgent bool
|
|
forcePTY bool
|
|
controlPath string
|
|
socketPath string
|
|
proxyPort string
|
|
nodePort string
|
|
command string
|
|
}
|
|
|
|
// externalSSHCommand runs an external SSH command (if an external ssh binary
|
|
// exists) with the passed in parameters.
|
|
func externalSSHCommand(o commandOptions) (*exec.Cmd, error) {
|
|
var execArgs []string
|
|
|
|
// Don't check the host certificate as part of the testing an external SSH
|
|
// client, this is done elsewhere.
|
|
execArgs = append(execArgs, "-oStrictHostKeyChecking=no")
|
|
execArgs = append(execArgs, "-oUserKnownHostsFile=/dev/null")
|
|
|
|
// ControlMaster is often used by applications like Ansible.
|
|
if o.controlPath != "" {
|
|
execArgs = append(execArgs, "-oControlMaster=auto")
|
|
execArgs = append(execArgs, "-oControlPersist=1s")
|
|
execArgs = append(execArgs, "-oConnectTimeout=2")
|
|
execArgs = append(execArgs, fmt.Sprintf("-oControlPath=%v", o.controlPath))
|
|
}
|
|
|
|
// The -tt flag is used to force PTY allocation. It's often used by
|
|
// applications like Ansible.
|
|
if o.forcePTY {
|
|
execArgs = append(execArgs, "-tt")
|
|
}
|
|
|
|
// Connect to node on the passed in port.
|
|
execArgs = append(execArgs, fmt.Sprintf("-p %v", o.nodePort))
|
|
|
|
// Build proxy command.
|
|
proxyCommand := []string{"ssh"}
|
|
proxyCommand = append(proxyCommand, "-oStrictHostKeyChecking=no")
|
|
proxyCommand = append(proxyCommand, "-oUserKnownHostsFile=/dev/null")
|
|
if o.forwardAgent {
|
|
proxyCommand = append(proxyCommand, "-oForwardAgent=yes")
|
|
}
|
|
proxyCommand = append(proxyCommand, fmt.Sprintf("-p %v", o.proxyPort))
|
|
proxyCommand = append(proxyCommand, `%r@localhost -s proxy:%h:%p`)
|
|
|
|
// Add in ProxyCommand option, needed for all Teleport connections.
|
|
execArgs = append(execArgs, fmt.Sprintf("-oProxyCommand=%v", strings.Join(proxyCommand, " ")))
|
|
|
|
// Add in the host to connect to and the command to run when connected.
|
|
execArgs = append(execArgs, Host)
|
|
execArgs = append(execArgs, o.command)
|
|
|
|
// Find the OpenSSH binary.
|
|
sshpath, err := exec.LookPath("ssh")
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Create an exec.Command and tell it where to find the SSH agent.
|
|
cmd, err := exec.Command(sshpath, execArgs...), nil
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
cmd.Env = []string{fmt.Sprintf("SSH_AUTH_SOCK=%v", o.socketPath)}
|
|
|
|
return cmd, nil
|
|
}
|
|
|
|
// createAgent creates a SSH agent with the passed in private key and
|
|
// certificate that can be used in tests. This is useful so tests don't
|
|
// clobber your system agent.
|
|
func createAgent(me *user.User, privateKeyByte []byte, certificateBytes []byte) (*teleagent.AgentServer, string, string, error) {
|
|
// create a path to the unix socket
|
|
sockDir, err := ioutil.TempDir("", "int-test")
|
|
if err != nil {
|
|
return nil, "", "", trace.Wrap(err)
|
|
}
|
|
sockPath := filepath.Join(sockDir, "agent.sock")
|
|
|
|
uid, err := strconv.Atoi(me.Uid)
|
|
if err != nil {
|
|
return nil, "", "", trace.Wrap(err)
|
|
}
|
|
gid, err := strconv.Atoi(me.Gid)
|
|
if err != nil {
|
|
return nil, "", "", trace.Wrap(err)
|
|
}
|
|
|
|
// transform the key and certificate bytes into something the agent can understand
|
|
publicKey, _, _, _, err := ssh.ParseAuthorizedKey(certificateBytes)
|
|
if err != nil {
|
|
return nil, "", "", trace.Wrap(err)
|
|
}
|
|
privateKey, err := ssh.ParseRawPrivateKey(privateKeyByte)
|
|
if err != nil {
|
|
return nil, "", "", trace.Wrap(err)
|
|
}
|
|
agentKey := agent.AddedKey{
|
|
PrivateKey: privateKey,
|
|
Certificate: publicKey.(*ssh.Certificate),
|
|
Comment: "",
|
|
LifetimeSecs: 0,
|
|
ConfirmBeforeUse: false,
|
|
}
|
|
|
|
// create a (unstarted) agent and add the key to it
|
|
teleAgent := teleagent.NewServer()
|
|
teleAgent.Add(agentKey)
|
|
|
|
// start the SSH agent
|
|
err = teleAgent.ListenUnixSocket(sockPath, uid, gid, 0600)
|
|
if err != nil {
|
|
return nil, "", "", trace.Wrap(err)
|
|
}
|
|
go teleAgent.Serve()
|
|
|
|
return teleAgent, sockDir, sockPath, nil
|
|
}
|
|
|
|
func closeAgent(teleAgent *teleagent.AgentServer, socketDirPath string) error {
|
|
err := teleAgent.Close()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
err = os.RemoveAll(socketDirPath)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createWebClient builds a *client.WebClient that is used to simulate
|
|
// browser requests.
|
|
func createWebClient(cluster *TeleInstance, opts ...roundtrip.ClientParam) (*client.WebClient, error) {
|
|
// Craft URL to Web UI.
|
|
u := &url.URL{
|
|
Scheme: "https",
|
|
Host: cluster.Config.Proxy.WebAddr.Addr,
|
|
}
|
|
|
|
opts = append(opts, roundtrip.HTTPClient(client.NewInsecureWebClient()))
|
|
wc, err := client.NewWebClient(u.String(), opts...)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return wc, nil
|
|
}
|
|
|
|
func fatalIf(err error) {
|
|
if err != nil {
|
|
log.Fatalf("%v at %v", string(debug.Stack()), err)
|
|
}
|
|
}
|
|
|
|
func makeKey() (priv, pub []byte) {
|
|
k, err := native.New(context.TODO(), native.PrecomputeKeys(0))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
priv, pub, _ = k.GenerateKeyPair("")
|
|
return priv, pub
|
|
}
|