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 9584e45 commit 56531c9
Show file tree
Hide file tree
Showing 10 changed files with 944 additions and 30 deletions.
79 changes: 78 additions & 1 deletion 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 @@ -127,7 +128,7 @@ func main() {
// default.

ping := make(chan agent.Ping)
go ag.Ping(nil, ping)
go ag.Ping(nil, ping, nil)

for range ping {
log.WithFields(log.Fields{
Expand Down Expand Up @@ -246,6 +247,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
140 changes: 114 additions & 26 deletions pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
package agent

import (
"context"
"crypto/rsa"
"io"
"net"
Expand All @@ -56,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 Down Expand Up @@ -94,6 +97,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 @@ -127,6 +154,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 @@ -272,20 +325,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 @@ -328,7 +408,7 @@ 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(listining chan bool) error {
a.server = server.NewServer(a.cli, a.authData, a.config.PrivateKey, a.config.KeepAliveInterval, a.config.SingleUserPassword)
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 @@ -409,6 +489,8 @@ func (a *Agent) Listen(listining chan bool) error {

for {
if a.done {
log.Debug("Agent listing stopped due to close call")

return nil
}

Expand Down Expand Up @@ -450,28 +532,34 @@ func (a *Agent) Listen(listining chan bool) error {
// If the ticker is nil, it will be set to 10 minutes.
//
// ping parameter is a channel that is notified when the agent pings the server.
func (a *Agent) Ping(ticker *time.Ticker, ping chan Ping) {
func (a *Agent) Ping(ticker *time.Ticker, ping chan Ping, done <-chan bool) {
if ticker == nil {
ticker = time.NewTicker(10 * time.Minute)
}

for range ticker.C {
if a.done {
for {
select {
case <-done:
log.WithFields(log.Fields{
"namespace": a.authData.Namespace,
"hostname": a.authData.Name,
}).Debug("Agent ping stopped due to close call")

return
}
case <-ticker.C:
sessions := make([]string, 0, len(a.server.Sessions))
for key := range a.server.Sessions {
sessions = append(sessions, key)
}

sessions := make([]string, 0, len(a.server.Sessions))
for key := range a.server.Sessions {
sessions = append(sessions, key)
}
a.sessions = sessions

a.sessions = sessions
if err := a.authorize(); err != nil {
a.server.SetDeviceName(a.authData.Name)
}

if err := a.authorize(); err != nil {
a.server.SetDeviceName(a.authData.Name)
throw(ping, Ping{Timestamp: time.Now()})
}

throw(ping, Ping{Timestamp: time.Now()})
}
}

Expand Down
Loading

0 comments on commit 56531c9

Please sign in to comment.