Skip to content

Commit

Permalink
feature(agent,api,pkg): add VPN capability
Browse files Browse the repository at this point in the history
It adds to the ShellHub's Agent the capability to connect to a
ShellHub's Enterprise service, which provides a virtual private network
between devices registered into the same namespace.

To enable it, the ShellHub's instance must support it, and the
`SHELLHUB_VPN` environmental variable must be set to `TRUE` on the
ShellHub Agent startup.
  • Loading branch information
henrybarreto committed Sep 6, 2024
1 parent 54fb229 commit fd2bc56
Show file tree
Hide file tree
Showing 23 changed files with 1,022 additions and 14 deletions.
3 changes: 3 additions & 0 deletions agent/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/sftp v1.13.5 // indirect
github.com/sethvargo/go-envconfig v0.9.0 // indirect
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/vishvananda/netlink v1.2.1-beta.2 // indirect
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
go.opentelemetry.io/otel v1.26.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect
Expand Down
9 changes: 9 additions & 0 deletions agent/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ github.com/shellhub-io/ssh v0.0.0-20230224143412-edd48dfd6eea h1:7tEI9nukSYZViCj
github.com/shellhub-io/ssh v0.0.0-20230224143412-edd48dfd6eea/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
Expand All @@ -118,6 +120,11 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
Expand Down Expand Up @@ -159,6 +166,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
46 changes: 35 additions & 11 deletions agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func main() {
"tenant_id": cfg.TenantID,
"server_address": cfg.ServerAddress,
"preferred_hostname": cfg.PreferredHostname,
}).Info("Listening for connections")
}).Info("Listening for SSH connections")

// Disable check update in development mode
if AgentVersion != "latest" {
Expand Down Expand Up @@ -163,23 +163,47 @@ func main() {
}()
}

if err := ag.ListenSSH(ctx); err != nil {
log.WithError(err).WithFields(log.Fields{
"version": AgentVersion,
"mode": mode,
"tenant_id": cfg.TenantID,
"server_address": cfg.ServerAddress,
"preferred_hostname": cfg.PreferredHostname,
}).Fatal("Failed to listen for SSH connections")
}
go func() {
if err := ag.ListenSSH(ctx); err != nil {
log.WithError(err).WithFields(log.Fields{
"version": AgentVersion,
"mode": mode,
"tenant_id": cfg.TenantID,
"server_address": cfg.ServerAddress,
"preferred_hostname": cfg.PreferredHostname,
}).Fatal("Failed to listen for SSH connections")
}
}()

go func() {
if !cfg.VPN {
log.Info("VPN is disable")

return
}

log.Debug("VPN enabled")

for {
log.Info("VPN connected")

if err := ag.ConnectVPN(ctx); err != nil {
log.WithError(err).Error("Connect to VPN lost. Retrying in 10 seconds.")
}

time.Sleep(10 * time.Second)
}
}()

<-ctx.Done()

log.WithFields(log.Fields{
"version": AgentVersion,
"mode": mode,
"tenant_id": cfg.TenantID,
"server_address": cfg.ServerAddress,
"preferred_hostname": cfg.PreferredHostname,
}).Info("Stopped listening for connections")
}).Info("Agent Stopped")
},
}

Expand Down
10 changes: 10 additions & 0 deletions api/services/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ var (
ErrAPIKeyDuplicated = errors.New("APIKey duplicated", ErrLayer, ErrCodeDuplicated)
ErrAuthForbidden = errors.New("user is authenticated but cannot access this resource", ErrLayer, ErrCodeForbidden)
ErrRoleInvalid = errors.New("role is invalid", ErrLayer, ErrCodeForbidden)
ErrNamespaceIPInvalid = errors.New("ip is invalid", ErrLayer, ErrCodeForbidden)
ErrNamespaceIPNotPrivate = errors.New("ip is not a private address", ErrLayer, ErrCodeForbidden)
)

func NewErrRoleInvalid() error {
Expand Down Expand Up @@ -471,3 +473,11 @@ func NewErrDeviceMaxDevicesReached(count int) error {
func NewErrAuthForbidden() error {
return NewErrForbidden(ErrAuthForbidden, nil)
}

func NewErrNamespaceIPInvalid() error {
return NewErrInvalid(ErrNamespaceIPInvalid, nil, nil)
}

func NewErrNamespaceIPNotPrivate() error {
return NewErrInvalid(ErrNamespaceIPNotPrivate, nil, nil)
}
31 changes: 31 additions & 0 deletions api/services/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package services
import (
"context"
"errors"
"net"
"strings"

"github.com/shellhub-io/shellhub/api/store"
"github.com/shellhub-io/shellhub/api/store/mongo"
"github.com/shellhub-io/shellhub/pkg/api/authorizer"
"github.com/shellhub-io/shellhub/pkg/api/internalclient"
"github.com/shellhub-io/shellhub/pkg/api/requests"
"github.com/shellhub-io/shellhub/pkg/clock"
"github.com/shellhub-io/shellhub/pkg/envs"
Expand Down Expand Up @@ -204,6 +206,27 @@ func (s *service) EditNamespace(ctx context.Context, req *requests.NamespaceEdit
ConnectionAnnouncement: req.Settings.ConnectionAnnouncement,
}

if envs.IsEnterprise() {
changes.VPNEnable = req.VPN.Enable

if req.VPN.Address != nil {
address := *req.VPN.Address
ip := net.IPv4(address[0], address[1], address[2], address[3])

if ip.IsLoopback() || ip.IsUnspecified() {
return nil, NewErrNamespaceIPInvalid()
}

if !ip.IsPrivate() {
return nil, NewErrNamespaceIPNotPrivate()
}

changes.VPNAddress = &address
}

changes.VPNMask = req.VPN.Mask
}

if err := s.store.NamespaceEdit(ctx, req.Tenant, changes); err != nil {
switch {
case errors.Is(err, store.ErrNoDocuments):
Expand All @@ -213,6 +236,14 @@ func (s *service) EditNamespace(ctx context.Context, req *requests.NamespaceEdit
}
}

if envs.IsEnterprise() {
cli := s.client.(internalclient.Client)

if err := cli.VPNStopRouter(req.Tenant); err != nil {
return nil, err
}
}

return s.store.NamespaceGet(ctx, req.Tenant, true)
}

Expand Down
5 changes: 5 additions & 0 deletions api/services/namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,7 @@ func TestEditNamespace(t *testing.T) {
tenantID: "xxxxx",
namespaceName: "newname",
requiredMocks: func() {
envMock.On("Get", "SHELLHUB_ENTERPRISE").Return("false").Once()
mock.On("NamespaceEdit", ctx, "xxxxx", &models.NamespaceChanges{Name: "newname"}).
Return(store.ErrNoDocuments).
Once()
Expand All @@ -951,6 +952,7 @@ func TestEditNamespace(t *testing.T) {
tenantID: "xxxxx",
namespaceName: "newname",
requiredMocks: func() {
envMock.On("Get", "SHELLHUB_ENTERPRISE").Return("false").Once()
mock.On("NamespaceEdit", ctx, "xxxxx", &models.NamespaceChanges{Name: "newname"}).
Return(errors.New("error")).
Once()
Expand All @@ -965,6 +967,7 @@ func TestEditNamespace(t *testing.T) {
namespaceName: "newName",
tenantID: "xxxxx",
requiredMocks: func() {
envMock.On("Get", "SHELLHUB_ENTERPRISE").Return("false").Once()
mock.On("NamespaceEdit", ctx, "xxxxx", &models.NamespaceChanges{Name: "newname"}).
Return(nil).
Once()
Expand All @@ -991,6 +994,7 @@ func TestEditNamespace(t *testing.T) {
namespaceName: "newname",
tenantID: "xxxxx",
requiredMocks: func() {
envMock.On("Get", "SHELLHUB_ENTERPRISE").Return("false").Once()
mock.On("NamespaceEdit", ctx, "xxxxx", &models.NamespaceChanges{Name: "newname"}).
Return(nil).
Once()
Expand All @@ -1000,6 +1004,7 @@ func TestEditNamespace(t *testing.T) {
Name: "newname",
}

envMock.On("Get", "SHELLHUB_ENTERPRISE").Return("false").Once()
mock.On("NamespaceGet", ctx, "xxxxx", true).
Return(namespace, nil).
Once()
Expand Down
1 change: 1 addition & 0 deletions api/store/mongo/migrations/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func GenerateMigrations() []migrate.Migration {
migration74,
migration75,
migration76,
migration77,
}
}

Expand Down
64 changes: 64 additions & 0 deletions api/store/mongo/migrations/migration_77.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package migrations

import (
"context"

"github.com/shellhub-io/shellhub/pkg/envs"
"github.com/sirupsen/logrus"
migrate "github.com/xakep666/mongo-migrate"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)

var migration77 = migrate.Migration{
Version: 77,
Description: "Adding VPN settings to namespace",
Up: migrate.MigrationFunc(func(ctx context.Context, db *mongo.Database) error {
logrus.WithFields(logrus.Fields{
"component": "migration",
"version": 77,
"action": "Up",
}).Info("Applying migration")

if envs.IsEnterprise() {
update := bson.M{
"$set": bson.M{
"vpn": bson.M{
"enable": false,
"address": bson.A{10, 0, 0, 0},
"mask": 16,
},
},
}

_, err := db.
Collection("namespaces").
UpdateMany(ctx, bson.M{}, update)

return err
}

return nil
}),
Down: migrate.MigrationFunc(func(ctx context.Context, db *mongo.Database) error {
logrus.WithFields(logrus.Fields{
"component": "migration",
"version": 77,
"action": "Down",
}).Info("Reverting migration")

if envs.IsEnterprise() {
update := bson.M{
"$unset": bson.M{"vpn": ""},
}

_, err := db.
Collection("namespaces").
UpdateMany(ctx, bson.M{}, update)

return err
}

return nil
}),
}
Loading

0 comments on commit fd2bc56

Please sign in to comment.