Skip to content

feat(control-loop/ensure-networks) Add notion of control loop, ensure network endpoints every 30 minutes#329

Open
leo-scalingo wants to merge 3 commits into
masterfrom
feat/328/control-loop-ensure
Open

feat(control-loop/ensure-networks) Add notion of control loop, ensure network endpoints every 30 minutes#329
leo-scalingo wants to merge 3 commits into
masterfrom
feat/328/control-loop-ensure

Conversation

@leo-scalingo

@leo-scalingo leo-scalingo commented Jul 17, 2026

Copy link
Copy Markdown
Member

Also add ability to trigger the logics by CLI/API

Fixes #328

@leo-scalingo
leo-scalingo requested a review from a team as a code owner July 17, 2026 13:34
@leo-scalingo
leo-scalingo requested review from EtienneM and removed request for a team July 17, 2026 13:34
@leo-scalingo leo-scalingo self-assigned this Jul 17, 2026
@leo-scalingo
leo-scalingo requested a review from briceamen July 17, 2026 13:34
@leo-scalingo leo-scalingo changed the title feat(control-loop/ensure-networks) Add notion of control loop, ensure every 30 minutes feat(control-loop/ensure-networks) Add notion of control loop, ensure network endpoints every 30 minutes Jul 17, 2026
@leo-scalingo
leo-scalingo force-pushed the feat/328/control-loop-ensure branch from b64bc4d to 346f5ee Compare July 17, 2026 13:38
Comment thread node/ensure.go
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)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
if os.IsNotExist(errors.Cause(err)) {
if errors.Is(errors.Cause(err), fs.ErrNotExist) {
View step-by-step instructions
  1. Replace the os.IsNotExist(...) check with the standard library errors.Is(...) check against the not-exist sentinel error.
    Change os.IsNotExist(errors.Cause(err)) to stderrors.Is(err, fs.ErrNotExist).

  2. Stop unwrapping the error with errors.Cause(...) in this condition.
    errors.Is already walks wrapped errors, so it can match the original not-exist error through errors.Wrap, fmt.Errorf(... %w ...), and similar wrappers.

  3. Add the standard library imports needed for the new check.
    If this file already imports github.com/pkg/errors as errors, import the standard library package with an alias such as stderrors "errors", and add io/fs for fs.ErrNotExist.

  4. Keep the existing deactivate path the same, and only update the condition.
    The resulting condition should look like if stderrors.Is(err, fs.ErrNotExist) { ... }.

  5. Alternatively, if this file does not import github.com/pkg/errors with the name errors, use the standard library package directly as errors.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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I second this

@EtienneM EtienneM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to

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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is there a reason why you remove the telemtry?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it wasn't installed yet in sand, this will change with my next PR

WithoutTelemetry: true,
Jobs: []cronsetup.Job{
{
Name: "ensure network endpoints",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is it safe to have a name with spaces in it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: wrap the error

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I could wrap it, but it doesn't bring a lot of value at this level I'd say, but ok

Comment on lines +12 to +19
if err != nil {
return err
}

err = client.NodeEnsureNetworkEndpoints(context.Background())
if err != nil {
return err
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: wrap the errors

Comment thread node/ensure.go

func EnsureNetworkEndpoints(ctx context.Context, c *config.Config, repo network.Repository, erepo endpoint.Repository) error {
log := logger.Get(ctx)
ctx = logger.ToCtx(ctx, log)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ctx = logger.ToCtx(ctx, log)

Comment thread node/ensure.go
Comment on lines +33 to +36
endpointLog := log.WithFields(logrus.Fields{
"endpoint_id": endpoint.ID,
})
endpointCtx := logger.ToCtx(ctx, endpointLog)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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,
})

Comment thread node/ensure.go
Comment on lines +43 to +47
endpointLog = endpointLog.WithFields(logrus.Fields{
"network_id": endpoint.NetworkID,
"endpoint_netns_path": endpoint.TargetNetnsPath,
})
endpointCtx = logger.ToCtx(ctx, endpointLog)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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,
})

Comment thread node/ensure.go
log.Info("Ensure networks on node")

endpoints, err := erepo.List(ctx, map[string]string{"hostname": c.GetPeerHostname()})
if errors.Cause(err) == store.ErrNotFound {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: should we already switch to go-utils/errors/v3.Is, even though this service does not yet use our own error library?

Comment thread node/ensure.go
"github.com/Scalingo/sand/store"
)

func EnsureNetworkEndpoints(ctx context.Context, c *config.Config, repo network.Repository, erepo endpoint.Repository) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

Comment thread node/ensure.go
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)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I second this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Periodically reconcile active overlay network state

3 participants