Skip to content

Commit

Permalink
gTmhUzJpVqGitpd9LjV1ldLZiaTxImZNdcW15gfxjKA=
Browse files Browse the repository at this point in the history
  • Loading branch information
henrybarreto committed Sep 20, 2023
1 parent 92a53e6 commit 601d7ae
Show file tree
Hide file tree
Showing 10 changed files with 916 additions and 19 deletions.
77 changes: 77 additions & 0 deletions agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/kelseyhightower/envconfig"
"github.com/shellhub-io/shellhub/pkg/agent"
"github.com/shellhub-io/shellhub/pkg/agent/pkg/selfupdater"
"github.com/shellhub-io/shellhub/pkg/connector"
"github.com/shellhub-io/shellhub/pkg/envs"
"github.com/shellhub-io/shellhub/pkg/loglevel"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -265,6 +266,82 @@ It is initialized by the agent when a new SFTP session is created.`,
},
})

rootCmd.AddCommand(&cobra.Command{ // nolint: exhaustruct
Use: "connector",
Short: "Starts the Connector",
Long: "Starts the Connector, a special kind of Agent that turns your docker containers into ShellHub devices.",
Run: func(cmd *cobra.Command, args []string) {
cfg, err := envs.ParseWithPrefix[agent.ConfigConnector]("shellhub")
if err != nil {
envconfig.Usage("shellhub", &cfg) // nolint:errcheck
log.Fatal(err)
}

log.WithFields(log.Fields{
"version": AgentVersion,
"address": cfg.ServerAddress,
"tenant_id": cfg.TenantID,
}).Info("Starting ShellHub Connector")

connector, err := connector.NewDockerConnector(cfg.ServerAddress, cfg.TenantID)
if err != nil {
log.WithError(err).WithFields(log.Fields{
"version": AgentVersion,
"address": cfg.ServerAddress,
"tenant_id": cfg.TenantID,
}).Fatal("Failed to create connector")
}

containers, err := connector.List(cmd.Context())
if err != nil {
log.WithError(err).WithFields(log.Fields{
"version": AgentVersion,
"address": cfg.ServerAddress,
"tenant_id": cfg.TenantID,
}).Fatal("Failed to list containers")
}

for _, container := range containers {
connector.Start(cmd.Context(), container)
}

go func() {
msgs, err := connector.Events(cmd.Context())

for {
select {
case err := <-err:
log.WithError(err).Error("Failed to get docker events")
case msg := <-msgs:
// NOTICE(r): The actions "kill" and "start" are the only ones that we are interested in.
// "kill" is called every time a container is killed or stopped. We use this action to stop
// the agent that is running for that container. The action "start", otherwise, is called
// every time a container is started. We use this action to start a new agent for that
// container.
switch msg.Action {
case "kill":
connector.Stop(cmd.Context(), msg.ID)

log.WithFields(log.Fields{
"id": msg.ID[:12],
"image": msg.From,
}).Info("Container stopped")
case "start":
connector.Start(cmd.Context(), msg.ID)

log.WithFields(log.Fields{
"id": msg.ID[:12],
"image": msg.From,
}).Info("Container started")
}
}
}
}()

select {}
},
})

rootCmd.Version = AgentVersion

rootCmd.SetVersionTemplate(fmt.Sprintf("{{ .Name }} version: {{ .Version }}\ngo: %s\n",
Expand Down
111 changes: 95 additions & 16 deletions pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@ import (
"time"

"github.com/Masterminds/semver"
dockerclient "github.com/docker/docker/client"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"github.com/shellhub-io/shellhub/pkg/agent/pkg/keygen"
"github.com/shellhub-io/shellhub/pkg/agent/pkg/sysinfo"
"github.com/shellhub-io/shellhub/pkg/agent/pkg/tunnel"
"github.com/shellhub-io/shellhub/pkg/agent/server"
"github.com/shellhub-io/shellhub/pkg/agent/server/modes"
"github.com/shellhub-io/shellhub/pkg/api/client"
"github.com/shellhub-io/shellhub/pkg/models"
"github.com/shellhub-io/shellhub/pkg/revdial"
Expand All @@ -89,6 +91,30 @@ var AgentVersion string
// [ShellHub Agent]: https://github.com/shellhub-io/shellhub/tree/master/agent
var AgentPlatform string

// Mode is the Agente execution mode.
//
// The agent can be executed in two different modes: host and connector.
// The host mode is the default mode, where the agent will listen for incoming connections and will be responsible for
// the SSH server. The connector mode is used to turn all containers inside a host into a single device and be
// responsible for the SSH server of all containers.
//
// Check [ModeHost] and [ModeConnector] for more information.
type Mode string

const (
// ModeHost is the Agent execution mode for `host`.
//
// The host mode is the default mode one, and turns the host machine into a ShellHub's Agent. The host is
// responsible for the SSH server, authentication and authorization, `/etc/passwd`, `/etc/shadow`, and etc.
ModeHost Mode = "host"
// ModeConnector is the Agent execution mode for `connector`.
//
// The connector mode is used to turn a container inside a host into a single device ShellHub's Agent. The host is
// responsible for the SSH server, but the authentication and authorization is made by either the conainer
// internals, `passwd` or `shadow`, or by the ShellHub API.
ModeConnector Mode = "connector"
)

// Config provides the configuration for the agent service.
type Config struct {
// Set the ShellHub Cloud server address the agent will use to connect.
Expand Down Expand Up @@ -122,6 +148,32 @@ type Config struct {
// multi-user mode (with root privileges) is enabled by default.
// NOTE: The password hash could be generated by ```openssl passwd```.
SingleUserPassword string `envconfig:"simple_user_password"`
// Mode is the Agent execution mode that it will operate.
//
// Check [Mode] for more information.
Mode Mode `envconfig:"mode" default:"host"`
}

// ConfigConnector provides the configuration for the agent connector service.
type ConfigConnector struct {
// Set the ShellHub Cloud server address the agent will use to connect.
// This is required.
ServerAddress string `envconfig:"server_address" required:"true"`

// Specify the path to store the devices/containers private keys.
// If not provided, the agent will generate a new one.
// This is required.
PrivateKeys string `envconfig:"private_keys" required:"true"`

// Sets the account tenant id used during communication to associate the
// devices to a specific tenant.
// This is required.
TenantID string `envconfig:"tenant_id" required:"true"`

// Determine the interval to send the keep alive message to the server. This
// has a direct impact of the bandwidth used by the device when in idle
// state. Default is 30 seconds.
KeepAliveInterval int `envconfig:"keepalive_interval" default:"30"`
}

type Agent struct {
Expand Down Expand Up @@ -267,20 +319,47 @@ func (a *Agent) generateDeviceIdentity() error {

// loadDeviceInfo load some device informations like OS name, version, arch and platform.
func (a *Agent) loadDeviceInfo() error {
osrelease, err := sysinfo.GetOSRelease()
if err != nil {
return err
}
switch a.config.Mode {
case ModeHost:
osrelease, err := sysinfo.GetOSRelease()
if err != nil {
return err
}

a.Info = &models.DeviceInfo{
ID: osrelease.ID,
PrettyName: osrelease.Name,
Version: AgentVersion,
Arch: runtime.GOARCH,
Platform: AgentPlatform,
}
a.Info = &models.DeviceInfo{
ID: osrelease.ID,
PrettyName: osrelease.Name,
Version: AgentVersion,
Arch: runtime.GOARCH,
Platform: AgentPlatform,
}

return nil
return nil
case ModeConnector:
cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
if err != nil {
return err
}

defer cli.Close()

info, err := cli.ContainerInspect(context.Background(), a.config.PreferredIdentity)
if err != nil {
return err
}

a.Info = &models.DeviceInfo{
ID: "docker",
PrettyName: info.Config.Image,
Version: AgentVersion,
Arch: runtime.GOARCH,
Platform: AgentPlatform,
}

return nil
default:
return errors.New("invalid Agent execution mode")
}
}

// probeServerInfo probe server information.
Expand Down Expand Up @@ -322,8 +401,8 @@ func (a *Agent) Close() error {
//
// listening parameter is a channel that is notified when the agent is listing for connections. It can be used to
// start to ping the server, synchronizing device information or other tasks.
func (a *Agent) Listen(ctx context.Context, listining chan bool) error {
a.server = server.NewServer(a.cli, a.authData, a.config.PrivateKey, a.config.KeepAliveInterval, a.config.SingleUserPassword)
func (a *Agent) Listen(ctx context.Context, listening chan bool) error {
a.server = server.NewServer(a.cli, a.authData, a.config.PrivateKey, a.config.KeepAliveInterval, a.config.SingleUserPassword, modes.Mode(a.config.Mode))

serv := a.server

Expand Down Expand Up @@ -444,11 +523,11 @@ func (a *Agent) Listen(ctx context.Context, listining chan bool) error {
"sshid": sshid,
}).Info("Server connection established")

throw(listining, true)
throw(listening, true)
if err := a.tunnel.Listen(listener); err != nil {
continue
}
throw(listining, false)
throw(listening, false)
}
}

Expand Down
117 changes: 117 additions & 0 deletions pkg/agent/server/modes/connector/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package connector

import (
"archive/tar"
"crypto"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"

dockerclient "github.com/docker/docker/client"
gliderssh "github.com/gliderlabs/ssh"
"github.com/shellhub-io/shellhub/pkg/agent/server/modes"
"github.com/shellhub-io/shellhub/pkg/api/client"
"github.com/shellhub-io/shellhub/pkg/models"
gossh "golang.org/x/crypto/ssh"
)

// NOTICE: Ensures the Authenticator interface is implemented.
var _ modes.Authenticator = (*Authenticator)(nil)

// Authenticator implements the Authenticator interface when the server is running in connector mode.
type Authenticator struct {
// api is a client to communicate with the ShellHub's API.
api client.Client
// authData is the authentication data received from the API to authenticate the device.
authData *models.DeviceAuthResponse
// container is the device name.
//
// NOTICE: Uses a pointer for later assignment.
container *string
docker dockerclient.APIClient
}

// NewAuthenticator creates a new instance of Authenticator for the connector mode.
func NewAuthenticator(api client.Client, docker dockerclient.APIClient, authData *models.DeviceAuthResponse, container *string) *Authenticator {
return &Authenticator{
api: api,
authData: authData,
container: container,
docker: docker,
}
}

// Password handles the server's SSH password authentication when server is running in connector mode.
func (a *Authenticator) Password(ctx gliderssh.Context, username string, password string) bool {
passwdTar, _, _ := a.docker.CopyFromContainer(ctx, *a.container, "/etc/passwd")
passwd := tar.NewReader(passwdTar)
passwd.Next() //nolint:errcheck

user, err := LookupUserDocker(username, passwd)
if err != nil {
return false
}

if user.Password == "" {
// NOTICE(r): when the user doesn't have password, we block the login.
return false
}

shadowTar, _, _ := a.docker.CopyFromContainer(ctx, *a.container, "/etc/shadow")
shadow := tar.NewReader(shadowTar)
shadow.Next() //nolint:errcheck

return authUser(username, password, shadow)
}

// PublicKey handles the server's SSH public key authentication when server is running in connector mode.
func (a *Authenticator) PublicKey(ctx gliderssh.Context, username string, key gliderssh.PublicKey) bool {
type Signature struct {
Username string
Namespace string
}

sig := &Signature{
Username: username,
Namespace: *a.container,
}

sigBytes, err := json.Marshal(sig)
if err != nil {
return false
}

sigHash := sha256.Sum256(sigBytes)

res, err := a.api.AuthPublicKey(&models.PublicKeyAuthRequest{
Fingerprint: gossh.FingerprintLegacyMD5(key),
Data: string(sigBytes),
}, a.authData.Token)
if err != nil {
return false
}

digest, err := base64.StdEncoding.DecodeString(res.Signature)
if err != nil {
return false
}

cryptoKey, ok := key.(gossh.CryptoPublicKey)
if !ok {
return false
}

pubCrypto := cryptoKey.CryptoPublicKey()

pubKey, ok := pubCrypto.(*rsa.PublicKey)
if !ok {
return false
}

if err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, sigHash[:], digest); err != nil {
return false
}

return true
}
Loading

0 comments on commit 601d7ae

Please sign in to comment.