feat(control-loop/ensure-networks) Add notion of control loop, ensure network endpoints every 30 minutes#329
feat(control-loop/ensure-networks) Add notion of control loop, ensure network endpoints every 30 minutes#329leo-scalingo wants to merge 3 commits into
Conversation
b64bc4d to
346f5ee
Compare
| if err != nil { | ||
| // If the netns path no longer exists, deactivate the endpoint. Other | ||
| // activation errors should not prevent the remaining endpoints from being ensured. | ||
| if os.IsNotExist(errors.Cause(err)) { |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
A wrapped file-not-found error from erepo.Activate(...) may not be recognized here, so missing endpoint.TargetNetnsPath can skip erepo.Deactivate(...). An attacker who can cause stale netns paths during restore could leave orphaned endpoint network state behind.
More details about this
erepo.Activate(...) returns an error, and this code decides whether to clean up the endpoint by checking os.IsNotExist(errors.Cause(err)). That check only looks at the unwrapped cause, so a wrapped not exist error can be missed and the code will take the failed to ensure endpoint path instead of calling erepo.Deactivate(...).
A plausible abuse case is: 1) an attacker causes endpoint.TargetNetnsPath to point to a namespace path that has already been removed, 2) erepo.Activate(endpointCtx, network, endpoint, ...) returns a wrapped file-not-found error for that path, 3) this os.IsNotExist(errors.Cause(err)) check fails to recognize it, and 4) the stale endpoint is left active because erepo.Deactivate(endpointCtx, network, endpoint) is never called. In a system restoring many endpoints, repeatedly triggering this on selected endpoint.NetworkID / endpoint.TargetNetnsPath values can leave orphaned network state behind, which can keep unexpected connectivity or policy state attached to containers that should have been cleaned up.
To resolve this comment:
✨ Commit fix suggestion
| if os.IsNotExist(errors.Cause(err)) { | |
| if errors.Is(errors.Cause(err), fs.ErrNotExist) { |
View step-by-step instructions
-
Replace the
os.IsNotExist(...)check with the standard libraryerrors.Is(...)check against the not-exist sentinel error.
Changeos.IsNotExist(errors.Cause(err))tostderrors.Is(err, fs.ErrNotExist). -
Stop unwrapping the error with
errors.Cause(...)in this condition.
errors.Isalready walks wrapped errors, so it can match the original not-exist error througherrors.Wrap,fmt.Errorf(... %w ...), and similar wrappers. -
Add the standard library imports needed for the new check.
If this file already importsgithub.com/pkg/errorsaserrors, import the standard library package with an alias such asstderrors "errors", and addio/fsforfs.ErrNotExist. -
Keep the existing deactivate path the same, and only update the condition.
The resulting condition should look likeif stderrors.Is(err, fs.ErrNotExist) { ... }. -
Alternatively, if this file does not import
github.com/pkg/errorswith the nameerrors, use the standard library package directly aserrors.Is(err, fs.ErrNotExist).
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by os-error-is-not-exist.
You can view more details about this finding in the Semgrep AppSec Platform.
| return nil | ||
| func setupEndpointEnsureCron(ctx context.Context, c *config.Config, repo network.Repository, erepo endpoint.Repository) (func(), error) { | ||
| return cronsetup.Setup(ctx, cronsetup.SetupOpts{ | ||
| WithoutTelemetry: true, |
There was a problem hiding this comment.
question: is there a reason why you remove the telemtry?
There was a problem hiding this comment.
Because it wasn't installed yet in sand, this will change with my next PR
| WithoutTelemetry: true, | ||
| Jobs: []cronsetup.Job{ | ||
| { | ||
| Name: "ensure network endpoints", |
There was a problem hiding this comment.
question: is it safe to have a name with spaces in it?
There was a problem hiding this comment.
You're right, poor review from my side
| Name: "ensure network endpoints", | ||
| Rhythm: "@every " + c.EndpointEnsureInterval.String(), | ||
| Func: func(ctx context.Context) error { | ||
| return node.EnsureNetworkEndpoints(ctx, c, repo, erepo) |
There was a problem hiding this comment.
Ok I could wrap it, but it doesn't bring a lot of value at this level I'd say, but ok
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| err = client.NodeEnsureNetworkEndpoints(context.Background()) | ||
| if err != nil { | ||
| return err | ||
| } |
|
|
||
| func EnsureNetworkEndpoints(ctx context.Context, c *config.Config, repo network.Repository, erepo endpoint.Repository) error { | ||
| log := logger.Get(ctx) | ||
| ctx = logger.ToCtx(ctx, log) |
There was a problem hiding this comment.
| ctx = logger.ToCtx(ctx, log) |
| endpointLog := log.WithFields(logrus.Fields{ | ||
| "endpoint_id": endpoint.ID, | ||
| }) | ||
| endpointCtx := logger.ToCtx(ctx, endpointLog) |
There was a problem hiding this comment.
| endpointLog := log.WithFields(logrus.Fields{ | |
| "endpoint_id": endpoint.ID, | |
| }) | |
| endpointCtx := logger.ToCtx(ctx, endpointLog) | |
| ctx, log := logger.WithFieldsToCtx(ctx, logrus.Fields{ | |
| "endpoint_id": endpoint.ID, | |
| }) |
| endpointLog = endpointLog.WithFields(logrus.Fields{ | ||
| "network_id": endpoint.NetworkID, | ||
| "endpoint_netns_path": endpoint.TargetNetnsPath, | ||
| }) | ||
| endpointCtx = logger.ToCtx(ctx, endpointLog) |
There was a problem hiding this comment.
| endpointLog = endpointLog.WithFields(logrus.Fields{ | |
| "network_id": endpoint.NetworkID, | |
| "endpoint_netns_path": endpoint.TargetNetnsPath, | |
| }) | |
| endpointCtx = logger.ToCtx(ctx, endpointLog) | |
| ctx, log = logger.WithFieldsToCtx(ctx, logrus.Fields{ | |
| "network_id": endpoint.NetworkID, | |
| "endpoint_netns_path": endpoint.TargetNetnsPath, | |
| }) |
| log.Info("Ensure networks on node") | ||
|
|
||
| endpoints, err := erepo.List(ctx, map[string]string{"hostname": c.GetPeerHostname()}) | ||
| if errors.Cause(err) == store.ErrNotFound { |
There was a problem hiding this comment.
suggestion: should we already switch to go-utils/errors/v3.Is, even though this service does not yet use our own error library?
| "github.com/Scalingo/sand/store" | ||
| ) | ||
|
|
||
| func EnsureNetworkEndpoints(ctx context.Context, c *config.Config, repo network.Repository, erepo endpoint.Repository) error { |
There was a problem hiding this comment.
From what I understand EnsureNetworkEndpoints() is run by the cron every 30 minutes but also by the endpoint POST /node/ensure-network-endpoints what will happen if they collide and run at the same time ?
| if err != nil { | ||
| // If the netns path no longer exists, deactivate the endpoint. Other | ||
| // activation errors should not prevent the remaining endpoints from being ensured. | ||
| if os.IsNotExist(errors.Cause(err)) { |
Also add ability to trigger the logics by CLI/API
Fixes #328