Subnet network address CIDR suffix: validation direction after #10926

TL;DR

I’ve been looking at #10926 (the fix for #39159, now merged into develop). After testing it on a develop environment, I found two behaviors around input validation that I’d like to discuss:

  1. An invalid mask slips through validation and gets saved — but only when the network address field contains a suffix (e.g. /24). I believe this is a bug.
  2. Whether the suffix is applied depends on the state of the mask (valid / invalid / blank), so the behavior is inconsistent. Conflicting input is also saved without warning.

On top of that, I’d like to settle how the suffix in the network address field should be handled in general (reject / strip / a middle ground). My preference is to reject it. Once we agree on a direction, I’ll open a follow-up PR with regression tests. Details below. @lstejska


Background

I commented on the validation direction earlier on the PR itself, but the above surfaced when I checked the actual behavior after the merge, so I’m sharing it here. For context, I tested on develop and confirmed that app/models/subnet.rb contains strip_network_cidr (called from normalize_addresses as a before_validation hook). The relevant line is:

self.cidr = parts[1].to_i if mask.blank? || cidr.nil?

This means the suffix from the address is only adopted when the mask is blank or when the mask can’t be parsed (i.e. invalid). That condition seems to lead to the two behaviors below. Here are reproductions with hammer (organization/location options omitted for brevity).

1. An invalid mask slips through validation

Passing the same invalid mask (not-a-mask) gives different results depending on whether the address carries a suffix:

# No suffix -> rejected as expected
hammer subnet create --name example-no-suffix --network 192.168.90.0 --mask not-a-mask
Could not create the subnet:
  Cidr can't be blank
  Network mask is invalid

# With suffix (same invalid mask) -> created
hammer subnet create --name example-with-suffix --network 192.168.91.0/24 --mask not-a-mask
Subnet created.
# info: network 192.168.91.0 / cidr 24 / mask 255.255.255.0

In the suffixed case, the invalid mask is silently replaced with 255.255.255.0 and saved. In other words, mask validation stops taking effect the moment a suffix is added to the address. A typo in the mask can be quietly swallowed via the suffix, which I don’t think is the intended behavior.

2. Whether the suffix is adopted depends on the state of the mask

Even with the same “address plus suffix”, whether the suffix wins flips depending on the mask:

Mask state Example input Result Saved cidr
Valid (255.255.0.0 = /16) network 192.168.60.0/24 + mask 255.255.0.0 suffix ignored, /16 wins 16
Blank (unspecified) network 192.168.61.0/24 (no mask/prefix) suffix adopted (normal completion) 24
Invalid (not-a-mask) network 192.168.91.0/24 + mask not-a-mask suffix adopted, invalid mask swallowed 24

When a valid mask is passed, the suffix is ignored (even with /24, mask 255.255.0.0 takes priority and it’s saved as /16); only when the mask is invalid does the suffix become the source of truth. In addition, input where the suffix and the prefix/mask disagree (e.g. address /24 with prefix 16) is saved as-is without any warning. I confirmed the same in the WebUI — conflicting input is saved without an error.


Options and my take

I believe #1 above is a clear bug that should be fixed — does that seem right? Beyond that, I’d like to decide how the suffix in the network address field should be handled in the first place. I see three options:

  • (a) If a suffix (e.g. /24) is entered in the network address field, treat it as invalid input and reject it with a validation error (prefix length continues to be specified in the Network Prefix field as before).
  • (b) Keep the current behavior: accept the suffix in the network address field, strip it, and don’t store it as a value (the strip approach, #10926).
  • (c) Strip only the redundant case where the suffix is well-formed and matches the other fields (prefix/mask); reject an invalid suffix or one that conflicts with the other fields (middle ground).

I lean toward (a), for reasons on both the development and the operator side.

From a development standpoint:

  • Prefix length is managed in the Network Prefix field, so a suffix in the address field is duplicate information.
  • The Network Prefix field rejects invalid values, whereas the address field silently normalizes — that’s inconsistent behavior across fields.
  • The implementation is a simple check: reject if a suffix is present.

From an operator standpoint (the people actually creating subnets):

  • With the current approach, a typo in the mask isn’t rejected; you can end up in a “saved, but with a value different from what I intended” state. Tracking down that drift later makes it harder to work with, not easier.
  • With the reject approach, bad input errors out on the spot, which prevents a wrong configuration from being saved unnoticed.
  • Currently, when the suffix and prefix/mask disagree, which one wins depends on the mask state (the result flips between a valid and an invalid mask). A consistent rule — “don’t put a suffix in the address; put the prefix in its dedicated field” — makes the behavior more predictable for operators too.

For reference, here’s a rough implementation sketch for (a). The idea is to drop the merged strip_network_cidr and instead reject a network that contains a suffix:

   validate :validate_ranges
   validate :check_if_type_changed, :on => :update
+  validate :network_must_not_contain_prefix

+  def network_must_not_contain_prefix
+    return unless network.present? && network.to_s.include?('/')
+
+    errors.add(:network, _("is invalid"))
+  end

(This assumes strip_network_cidr and its call are removed as well. I’ll provide the actual implementation in the follow-up PR.)

As for (c):

  • It would have to handle a combination of conditions — whether a suffix is present, whether the suffix itself is well-formed, whether it matches the other fields, and whether mask or prefix is specified — so the number of cases grows and the implementation gets more complex.
  • I understand the convenience angle from #10926 (being able to paste CIDR notation directly), so (c) is a possible compromise, but the added complexity is a tradeoff worth weighing.

Once we agree on a direction, I’ll open a follow-up PR with regression tests. Feedback would be much appreciated.

Pure “Operator”/user perspective here:

I am very much afraid option a) might end up breaking existing installation/setups, which in my opinion is something that should be avoided if possible.
What exactly would happen if someone had a setup that relied on the current behavior and only ever put CIDR notation in when option a) is implemented? I would assume they would have to update every network to use a correct mask before being able to edit any existing subnets? Plus requiring potential updates to any existing automation solutions for managing subnets.

From a user perspective, I would expect option c). Error out on conflicting/actually invalid input, but allow keeping the current behavior in cases where there is actually valid input.
In a fresh implementation scenario, I would argue that a) is probably the cleanest solution (I didn’t even know you could pit cidr in the network field), but if I understand the implications correctly, the user impact of breaking existing workflows should at least be considered.

Thanks for the feedback, @areyus.
Backward compatibility is exactly the kind of thing we should weigh here,
so I appreciate you raising it.

Let me start with what I think is the most important point: #10926 has only been merged into develop — it isn’t part of any GA release yet (Foreman 5.0 is currently scheduled for 2026-09-08). So the strip behavior, where you can put CIDR notation into the address field, is a new behavior that hasn’t shipped in any stable release. There shouldn’t be any production setups depending on it yet.

Because of that, if we settle the direction now, while it’s still on develop, we can decide this before anyone comes to rely on the new strip behavior — which avoids the very “breaking existing workflows” scenario you’re worried about. If anything, changing it after it’s gone GA is what would actually introduce that incompatibility.

On timing: the 5.0 schedule has branching on 2026-08-11, so if we want this decision to make it into 5.0, that’s effectively the deadline. That’s part of why I’d like to nail down the direction now.

As for (c), I do see its advantage for backward compatibility, and your framing — error out on conflicting/invalid input, but keep the current behavior when the input is actually valid — makes sense to me. That said, to be honest, since we’re still pre-GA and nothing depends on the strip behavior yet, this feels like a good opportunity to go with the simpler shape of (a) while we can.

1 Like

Thanks for the clarification, I was under the impression that the inconsistent behavior you described in the initial post was already a thing in current GA releases, und the merged PR was an initial effort to make the behavior more consistent.
If that is not the case in any GA release up until now (i.e. the subnet field does not accept cidr notation at all in current GA), I also support a) as the cleanest and most intuitive solution.

1 Like

Thanks, I’m glad you’re on board with (a).
Let me add one clarification just to be precise.

Strictly speaking, entering CIDR notation in the address field was already possible in current GA releases — but in that case the prefix ended up duplicated on save, e.g. 192.168.1.0/24/24. That’s the original bug (#39159). #10926 resolved that duplication by stripping the CIDR from the input. The inconsistencies I reported here (the invalid mask slipping through, and the behavior changing depending on the mask state) came in together with that stripping logic.

So the “accepts CIDR notation” behavior did exist before, but it was more of a broken behavior than a real feature. And the post-strip behavior I’m raising here hasn’t shipped in GA yet — so I think the point still holds that now is a good window to move toward the simpler shape of (a).

I’ll go ahead and open a new issue to track this and follow up with a fix along the lines of (a).

Thanks again for the input.

1 Like

Just to be clear, I’m not rushing to lock in the direction — I’d very much like to hear other opinions before opening the issue, especially from @lstejska since they worked on #10926. If anyone has thoughts on (a) vs (c) or sees a concern I’ve missed, please chime in.

Quick update: I’ve opened an issue and a PR implementing the reject approach (a).

The PR removes strip_network_cidr and instead rejects a network address that contains a CIDR suffix.
Thanks for the input, @areyus.

1 Like

Awesome,
Thanks @spesnova717 for opening the PRs.

I came back from PTO and have been busy with other priorities, but I’ll take a look as soon as possible.

Thanks!
I’d appreciate your review whenever you get a chance.

I am wondering if dropping one, or the other way of inputting masks would solve the confusion.

2 Likes

Thanks.
That’s a fair point.
Having both mask and prefix as inputs for the same information may well be the deeper root of the confusion.
Consolidating to one of them would certainly make things cleaner.
This PR was meant as a more contained first step ahead of that.

1 Like