base code for docker events processing & container exposing on startup

This commit is contained in:
Pavel 2023-11-16 22:50:21 +03:00
parent 214646b227
commit 1da76f397d
6 changed files with 225 additions and 13 deletions

View File

@ -21,8 +21,23 @@ var rootCmd = &cobra.Command{
Short: "Expose your Docker services to the Internet via SSH.", Short: "Expose your Docker services to the Internet via SSH.",
Long: `sshpoke is a CLI application that listens to the docker socket and automatically exposes relevant services to the Internet.`, Long: `sshpoke is a CLI application that listens to the docker socket and automatically exposes relevant services to the Internet.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var err error
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
events, err := docker.ListenToEvents(ctx) docker.Default, err = docker.New(ctx)
if err != nil {
logger.Sugar.Fatalf("cannot connect to docker daemon: %s", err)
}
for id, item := range docker.Default.Containers() {
logger.Sugar.Debugw("registering container",
"id", id,
"ip", item.IP.String(),
"port", item.Port,
"server", item.Server,
"domain", item.Domain)
}
events, err := docker.Default.Listen()
if err != nil { if err != nil {
logger.Sugar.Fatalf("cannot listen to docker events: %s", err) logger.Sugar.Fatalf("cannot listen to docker events: %s", err)
} }

2
go.mod
View File

@ -10,12 +10,10 @@ require (
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.17.0 github.com/spf13/viper v1.17.0
go.uber.org/zap v1.26.0 go.uber.org/zap v1.26.0
golang.design/x/lockfree v0.0.1
) )
require ( require (
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/changkun/lockfree v0.0.1 // indirect
github.com/distribution/reference v0.5.0 // indirect github.com/distribution/reference v0.5.0 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect

4
go.sum
View File

@ -43,8 +43,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/changkun/lockfree v0.0.1 h1:5WefVJLglY4IHRqOQmh6Ao6wkJYaJkarshKU8VUtId4=
github.com/changkun/lockfree v0.0.1/go.mod h1:3bKiaXn/iNzIPlSvSOMSVbRQUQtAp8qUAyBUtzU11s4=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -227,8 +225,6 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.design/x/lockfree v0.0.1 h1:IHFNwZgM5bnZYWkEbzn5lWHMYr8WsRBdCJ/RBVY0xMM=
golang.design/x/lockfree v0.0.1/go.mod h1:iaZUx6UgZaOdePjzI6wFd+seYMl1i0rsG8+xKvA8c4I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -7,25 +7,101 @@ import (
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/internal/model"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client" "github.com/docker/docker/client"
) )
func ListenToEvents(ctx context.Context) (chan events.Message, error) { var Default *Docker
type Docker struct {
cli *client.Client
ctx context.Context
}
func New(ctx context.Context) (*Docker, error) {
cli, err := client.NewClientWithOpts(config.DefaultConfig.Docker.Opts)
if err != nil {
return nil, err
}
return &Docker{
cli: cli,
ctx: ctx,
}, nil
}
func (d *Docker) Containers() map[string]model.Container {
items, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{
Filters: filters.NewArgs(filters.Arg("status", "running")),
})
if err != nil {
logger.Sugar.Errorf("cannot get containers list: %s", err)
return nil
}
containers := map[string]model.Container{}
for _, item := range items {
container, ok := dockerContainerToInternal(item)
if !ok {
continue
}
containers[item.ID] = container
}
return containers
}
func (d *Docker) Listen() (chan model.ContainerEvent, error) {
cli, err := client.NewClientWithOpts(config.DefaultConfig.Docker.Opts) cli, err := client.NewClientWithOpts(config.DefaultConfig.Docker.Opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
output := make(chan events.Message) output := make(chan model.ContainerEvent)
go func() { go func() {
for { for {
eventSource, errSource := cli.Events(ctx, types.EventsOptions{}) eventSource, errSource := cli.Events(d.ctx, types.EventsOptions{
Filters: filters.NewArgs(filters.Arg("type", "container")),
})
select { select {
case event := <-eventSource: case event := <-eventSource:
logger.Sugar.Debugf("docker event: %#v", event) eventType := model.TypeFromAction(event.Action)
output <- event if (eventType != model.EventStart && eventType != model.EventStop) || !actorEnabled(event.Actor) {
continue
}
if eventType == model.EventStop {
logger.Sugar.Debugw("stopping session",
"type", event.Action, "container.id", event.Actor.ID)
output <- model.ContainerEvent{
Type: eventType,
ID: event.Actor.ID,
}
continue
}
container, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{
Filters: filters.NewArgs(filters.Arg("id", event.Actor.ID)),
})
if err != nil || len(container) != 1 {
logger.Sugar.Errorw("cannot get container info",
"id", event.Actor.ID, "err", err)
continue
}
converted, ok := dockerContainerToInternal(container[0])
if !ok {
continue
}
newEvent := model.ContainerEvent{
Type: eventType,
ID: event.Actor.ID,
Container: converted,
}
logger.Sugar.Debugw("exposing container",
"type", event.Action,
"container.id", event.Actor.ID,
"container.ip", converted.IP.String(),
"container.port", converted.Port,
"container.server", converted.Server,
"container.domain", converted.Domain)
output <- newEvent
case err := <-errSource: case err := <-errSource:
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
logger.Sugar.Debug("stopping docker event listener...") logger.Sugar.Debug("stopping docker event listener...")

View File

@ -0,0 +1,92 @@
package docker
import (
"net"
"strconv"
"github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/internal/model"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/network"
"github.com/mitchellh/mapstructure"
)
type labelsConfig struct {
Enable boolStr `mapstructure:"sshpoke.enable"`
Network string `mapstructure:"sshpoke.network"`
Server string `mapstructure:"sshpoke.server"`
Port string `mapstructure:"sshpoke.port"`
Domain string `mapstructure:"sshpoke.domain"`
}
type boolStr string
func (b boolStr) Bool() bool {
return string(b) == "true"
}
func actorEnabled(actor events.Actor) bool {
label, ok := actor.Attributes["sshpoke.enable"]
if !ok {
return false
}
return boolStr(label).Bool()
}
func dockerContainerToInternal(container types.Container) (result model.Container, ok bool) {
var labels labelsConfig
if err := mapstructure.Decode(container.Labels, &labels); err != nil {
logger.Sugar.Debugf("skipping container %s because configuration is invalid: %s", container.ID, err)
return result, false
}
if !labels.Enable.Bool() {
logger.Sugar.Debugf("skipping container %s because sshpoke is not enabled for it", container.ID)
return result, false
}
if labels.Server == "" {
logger.Sugar.Debugf("skipping container %s because 'sshpoke.server' is not defined", container.ID)
return result, false
}
if container.NetworkSettings == nil ||
container.NetworkSettings.Networks == nil ||
len(container.NetworkSettings.Networks) == 0 {
logger.Sugar.Debugf("skipping container %s because network settings are empty", container.ID)
return result, false
}
var ip net.IP
if len(container.NetworkSettings.Networks) == 1 {
ip = net.ParseIP(getKeyVal(container.NetworkSettings.Networks).IPAddress)
} else {
if labels.Network == "" {
logger.Sugar.Debugf("container %s has %d networks and 'sshpoke.network' label is not specified",
container.ID, len(container.NetworkSettings.Networks))
return result, false
}
ntwrk, exists := container.NetworkSettings.Networks[labels.Network]
if !exists {
logger.Sugar.Debugf("container %s does not have network %s", container.ID, labels.Network)
return result, false
}
ip = net.ParseIP(ntwrk.IPAddress)
}
port, err := strconv.Atoi(labels.Port)
if err != nil {
logger.Sugar.Debugf("skipping container %s because 'sshpoke.port' is not numeric", container.ID)
return result, false
}
return model.Container{
IP: ip,
Port: uint16(port),
Server: labels.Server,
Domain: labels.Domain,
}, true
}
func getKeyVal(m map[string]*network.EndpointSettings) *network.EndpointSettings {
for _, v := range m {
return v
}
return nil
}

View File

@ -0,0 +1,35 @@
package model
import "net"
type EventType uint8
const (
EventStart EventType = iota
EventStop
EventUnknown
)
func TypeFromAction(action string) EventType {
switch action {
case "start":
return EventStart
case "stop", "die":
return EventStop
default:
return EventUnknown
}
}
type ContainerEvent struct {
Type EventType
ID string
Container Container
}
type Container struct {
IP net.IP
Port uint16
Server string
Domain string
}