Skip to content

Respect address tags in port_settings_* - #370

Open
cfzimmerman wants to merge 2 commits into
mainfrom
cory/ipwars2
Open

cfzimmerman wants to merge 2 commits into
mainfrom
cory/ipwars2

Conversation

@cfzimmerman

@cfzimmerman cfzimmerman commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Reproduces and fixes #342
See that issue if you have questions about this design.

Tests

These tests failed before the second commit.

failures:
    chaos_tests::port_settings::address_cmds_respect_tags
    chaos_tests::port_settings::apply_fails_on_tag_conflict
    chaos_tests::port_settings::settings_apply_respects_tags
    chaos_tests::port_settings::settings_clear_ignores_tags
    chaos_tests::port_settings::settings_rollback_respects_tags

test result: FAILED. 10 passed; 5 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.80s
  • settings_apply_respects_tags is the most important. It exercises the basic enforcement of tag separation. One tag creates an address. Another tag applies an address. The first tag's address still needs to exist. This is the motivating issue between tfportd and sled-agent. This test fails when tag1 lists addresses and sees that its address is missing.
  • address_cmds_respect_tags verifies tags are respected by address commands of the type used in swadm, tfportd, and maghemite. CRUD itself works, but the test fails in verification when port_settings_get returns more addresses than link_ipv*_list.
  • apply_fails_on_tag_conflict is a weird but meaningful edge case. The passing test create_addr_rejects_overwrites demonstrates that creating an address fails if the address already exists (under another tag). port_settings_apply should allow re-declaring addresses of the same tag, but it should fail when creating an address that belongs to a different tag rather than stealing the address from that tag. Stealing the address complicates deletion, which is implicated if a transaction ever needs to be rolled back. This particular test fails on "Addresses belong to another tag" because stealing is allowed in the current implementation.
  • settings_clear_ignores_tags exercises the same issue as settings_apply_respects_tags. If I remove the tag1 check, then this passes. So it's more collateral damage than a legit failure. After all, the test is meant to describe the current behavior rather than declare what it should be.
  • settings_rollback_respects_tags fails for various reasons due to rng. But it's usually because tag2 sees tag1's addresses before it has registered anything. A slightly rarer case is tag2 not finding its registered addresses because tag1 overwrote them.

Summary

The main change is in port_settings.rs, where we merge addresses belonging to other tags into the diffable representation.

Context

This PR makes a decision about address tagging in DPD, and it tries to avoid other structural changes. This is stacked below another PR with more bug reproductions and fixes: #374

There's some churn on similar code between the two, but I think keeping them separate is useful. This PR decides what to do about tags, and that PR decides what to do about soft state / asic state divergences amid failure.

Decisions

  • Tagged addresses are stored in a BTreeMap<Ipv*Addr, String>. So an address may necessarily only be added to a link by a single tag. I believe this was the previous behavior too, but it's more self-enforcing than BTreeSet<Ipv*Entry>. I would like a more descriptive Tag type, but that implies a lot of API changes and felt out of scope for this PR. Right now we're just trying to make the current API work as expected.
  • Currently addresses are the only tagged resource on a link.
  • If a resource is tagged, it is unconditionally tagged. This was already the case in the Link struct, but that invariant was a bit hidden. Where the dropshot api uses Option<String> as a tag, we unwrap that to "" in api_server.rs. We were already doing this in most/all places, but now it's more visible at the top layer.
  • LinkSpec holds addresses for all tags, which is necessary to avoid dropping other tags during port_settings_apply. This contrasts with LinkSettings, which only reflects addresses from a single tag.

Validation

This PR is in a stack. I'll attach Voxel test results to the top PR in the stack.

@cfzimmerman cfzimmerman changed the title Reproduce #342: address isolation Respect address tags in port_settings_apply Sep 4, 2026
cfzimmerman

This comment was marked as outdated.

@cfzimmerman
cfzimmerman force-pushed the cory/ipwars2 branch 5 times, most recently from 94ace38 to e6c362d Compare September 9, 2026 00:18
Comment thread dpd/src/link.rs Resolved
@cfzimmerman cfzimmerman changed the title Respect address tags in port_settings_apply Respect address tags in port_settings_* Sep 9, 2026
cfzimmerman

This comment was marked as resolved.

@cfzimmerman
cfzimmerman force-pushed the cory/ipwars2 branch 4 times, most recently from 02672af to 0e0a3d9 Compare September 11, 2026 23:57
Comment thread dpd/src/link.rs Resolved
cfzimmerman

This comment was marked as resolved.

@cfzimmerman
cfzimmerman marked this pull request as ready for review September 12, 2026 01:43
@cfzimmerman

Copy link
Copy Markdown
Contributor Author

Btw once this merges, sled-agent should not need this anymore: https://github.com/oxidecomputer/omicron/blob/6652b9a3a321cb306ea0f2e2328ac789f829f098/sled-agent/scrimlet-reconcilers/src/dpd_reconciler/port_reconciler.rs#L466-L474

It's only required now because sled agent is seeing addresses tagged to tfportd.

@andrewjstone fyi, as a friend and ally of port_settings_* functions.

Comment on lines +634 to +640
/// Verifies the current known behavior of port_settings_clear, which
/// is to delete the link and all its config from the port.
///
/// This test isn't necessarily endorsing the implementation, but
/// it does seek to track API's current behavior.
#[tokio::test]
async fn settings_clear_ignores_tags() -> anyhow::Result<()> {

@cfzimmerman cfzimmerman Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a strange behavior. But I also don't see a clearly better design under the current API.

  • Neither the link nor most of its properties are tagged. So port_settings_clear could delete only the addresses belonging to the tag. But what should it do about non-tagged settings? The current solution of just dropping the link conveniently avoids deciding or caching what a link's default settings should be.
  • We could reject "clear" if a link has multiple tags. But that makes this route useless because we expect links to often have multiple tags.
  • We could delete this and have caller use link_delete instead. I like this option, but I didn't want this PR to have breaking API changes. The goal right now is just fixing internal issues.

@cfzimmerman
cfzimmerman added this pull request to stack #379 September 25, 2026 23:49
@cfzimmerman
cfzimmerman removed this pull request from stack #379 September 29, 2026 01:26
@cfzimmerman
cfzimmerman added this pull request to stack #382 September 29, 2026 01:44
Comment thread dpd/src/link.rs
Comment on lines +240 to +244
/// IPv4 addresses assigned to this link.
/// Each is associated with its creator's ID tag.
pub ipv4: BTreeMap<Ipv4Addr, String>,
/// IPv6 addresses assigned to this link.
pub ipv6: BTreeMap<Ipv6Addr, String>,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These move to a global table in #374

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why the choice to make each address only ever have a single tag? versus something like a BTreeMap<Ipv4Addr, BTreeSet<String>> that could support multiple tags.

Do we have a good read on the history behind the tags, or is this more of a decision on how to move forward because we don't really have that history available to consult?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@cfzimmerman cfzimmerman Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do we have a good read on the history behind the tags, or is this more of a decision on how to move forward because we don't really have that history available to consult?

I really considered ditching tags. Some of that decision process is in #342

Here's what convinced me:

  1. One motivation behind tagging was to prevent sled-agent from reconciling away swadm addresses. Tags accomplish that. And without tags, sled-agent must know to ignore IPv6 link local addresses that are implicitly owned by tfportd. John G reasonably called that a code smell.
  2. Ownership (through tags) is currently kind of a second-class concept in dpd. However, someday we may need an actual authentication scheme for commands. And then it's useful to have an existing ownership model. So if dpd is headed in a direction, it seems more towards tagging/ownership than away from it.

Why the choice to make each address only ever have a single tag? versus something like a BTreeMap<Ipv4Addr, BTreeSet> that could support multiple tags.

Complexity, and I wasn't sure we needed it. Multiple tags per address implicates something like reference counting, which is complex. Was hoping to avoid that if possible. Do you know of any case where multiple tags need to register the same address?

@taspelund taspelund Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the coexistence of swadm/sled-agent you're already looking at may warrant multiple tags to make some things less awkward.
e.g.
Let's say sled-agent registers IP X, then swadm adds also wants IP X. If there's only one owner, then how can swadm ensure IP X survives even when sled-agent doesn't want it anymore? There would need to be a separate mechanism to query the IPs from dpd, then to re-add IP X after sled-agent deletes it... Which would leave a timing gap between IP X's removal and re-installation.

If multiple parties can register interest in an IP, that defers deletion until both parties have withdrawn theit interest and there's no timing window where the IP is needed but gone.

@cfzimmerman cfzimmerman Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That seems reasonable. I'll look into multi-tag ownership on IP addresses.

TODO: implement and link commit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Don't take my thoughts as a mandate by any means. I think it could help when clients have overlapping interest, but I'm not sure how often we would expect that to happen... even in cases where swadm and sled-agent coexist. So if you feel the complexity isn't justified, that's okay too

@cfzimmerman cfzimmerman Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was not excited about the complexity, but it does seem like a useful feature.

I think the reverse of your example is even more pernicious. Say, for example, someone is debugging a system and uses swadm to create a link and put a configured address on it. Then, every time sled-agent tries to reconcile that same address onto the link, its cmd will be rejected completely because that address already exists under a different owner (swadm). sled-agent will not succeed until someone manually deletes that address with swadm and allows sled-agent to register it.

I've been trying to measure complexity against customer/engineering value, and I think that warrants it. Although this affects many of the tests and implies meaningful changes.

Comment thread dpd/src/link.rs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

#374 addresses some of the code duplication in this module.

cfzimmerman

This comment was marked as resolved.

@rcgoodfellow

rcgoodfellow commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

My high-level read of this is the following.

  1. Internal functions for IP address creation/deletion now take tags.
  2. When the port settings apply endpoint calls these functions it uses said tags, so addresses not created by the port settings apply endpoint cannot be deleted by the port settings apply endpoint. This is what fixes port_settings_apply clobbers link-local addresses added via link_ipv6_create #342
  3. The dpd API endpoint for IP address creation takes an optional tag, and uses it for creating addresses when supplied. Thus tfportd will also have tagged address objects.
  4. The dpd API endpoint for IP address deletion does not take a tag, so no tag can be supplied to the internal IP address deletion function without an API change. This creates the potential for consumers of that API endpoint such as tfportd to delete addresses it did not create pretty easily.

Is that right? I think addressing (4) is worth the API change to make it harder for clients to delete addresses they did not create - that is assuming the tagging mechanism moves forward with the current design. That does not need to be done in this PR, but is worth a follow on issue.

- Tagged port_settings_apply can delete addresses from another tag.
- Tagged port_settings_get returns addresses belonging to other tags.
- Tagged port_settings_clear can delete addresses from another tag.
  Not changing this, just observing it.
- Tagged port_settings_apply steals tagged addresses in a way
  create_ipv*_addr does not.
Scope link address CRUD to user-provided tags as advertised
in the dropshot API.

This avoids some tempting breaking changes in favor of just
fixing what is broken for now. If we want breaking changes,
the two highest priority are (1) removing tags from
port_settings_clear, which does not use them and (2) adding
tags to link_ipv*_delete.
@cfzimmerman
cfzimmerman force-pushed the cory/ipwars2 branch 2 times, most recently from 9e2681d to 6275b6a Compare September 29, 2026 23:45
Comment thread dpd/src/api_server.rs Resolved
Comment thread dpd/src/link.rs
@@ -487,10 +489,7 @@ impl Link {

/// Return the link-local address for this link, if one has been added.
pub fn link_local(&self) -> Option<Ipv6Addr> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It might be worth tweaking the method name or the doc string to call out that this is specific to IPv6 just to remove any possibility for confusion.

Perhaps also in a later commit/PR we could consider using oxnet::UnicastLinkLocalIpv6Addr to enforce this at the type level without needing .map()/.find()

@cfzimmerman cfzimmerman Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree this is problematic. Might be useful to discuss from a design level. Afaict here's the issue:

This function is used in one place to construct a TfportData instance used by tfportd. It returns the first IPv6 unicast link local address on a link. This breaks down immediately if there are two such addresses on a link. That raises the question, should we allow more than one IPv6 unicast link local address on a link? RFC 4291 allows it, and fwiw illumos supports this.

All interfaces are required to have at least one Link-Local unicast
address ... A single interface may also have multiple IPv6 addresses of any type
(unicast, anycast, and multicast) or scope.
RFC 4291

Screenshot From 2026-10-06 12-41-12

If we allow multiple, then a function returning an Option<Ipv6Addr> is incoherent, and either dpd or tfportd absorbs the complexity of a link possibly having more than one IPv6 unicast link local address. I'm inclined to make tfportd handle this. Its job can be "ensure the link has the IPv6 link local address(es) I want", but it can leave other addresses alone.

TODO: implement and link the commit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My understanding from (past, fuzzy) conversations with Ry is that we don't intend for operators to manually configure link-local addresses, so there should really only ever be one (which is derived from the link's MAC via SLAAC/EUI-64). It seems preferable to me to enforce that invariant (or at least abstract away the lack of enforcement) in a single place. tfportd seems like a natural place to do that, but I still don't feel as comfortable with the dendrite codebase as I'd like, so I'm not 100% confident that's the best spot.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yes. There should only ever be one link local on an interface and that link local should be EUI-64 derived from the physical MAC address.

Comment thread dpd/src/link.rs
Comment on lines +1081 to +1083
/// If tag is None, the address is deleted regardless of tag.
/// If tag is Some, the address is only deleted if its registration
/// tag matches the given tag.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For something like this where None effectively operates as force=true, I think we may be better served by adding a helper enum that effectively spells this out more clearly without relying on comments for clarity around meaning.
e.g.

enum TagSelector {
    Single(String),
    All,
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point. This Option<String> pattern is used all over these three PRs. I'll transition that to an enum.

TODO: implement and replace stale docs

Comment on lines +51 to +61
/// A random IP address generator.
pub struct IpRng {
rng: StdRng,
claimed: HashSet<IpAddr>,
}

impl IpRng {
/// Creates a new ip address generator from the given seed.
pub fn new(seed: u64) -> Self {
Self { rng: StdRng::seed_from_u64(seed), claimed: HashSet::default() }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This feels like it might be a good use case for proptest.

Not necessarily something that needs to be done in this PR, but something to think about

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed that dpd could use some prop testing, although yes I'm inclined to make that its own project. This was just to avoid hard coded address strings in individual tests, which would presumably be kind of distracting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Update: I experimented with proptests on this stack but scrapped that. Multiple places in DPD will benefit from proptesting. I think the port settings API and basically everything involving table ops. Preferred plan is to wait for Zeeshan's multicast proptests and then run a standalone project proptesting the general DPD API.

Comment on lines +42 to +43
const LINK_CREATE: LinkCreate = LinkCreate {
lane: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could maybe tuck this behind #[cfg(test)] or rename to TEST_LINK_CREATE so it's obvious we wouldn't want this in prod code (e.g. as an impl Default for LinkCreate`)

@cfzimmerman cfzimmerman Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sgtm. Renamed to TEST_LINK_CONF.

Edit: ... in the branch above. The two have diverged so far at this point, I'm inclined to sunset this PR and include these commits in the parent PR.

Comment thread dpd/src/port_settings.rs
.ipv4
.iter()
.filter(|(_, entry_tag)| entry_tag.as_str() != tag)
.map(|(addr, entry_tag)| (*addr, entry_tag.to_string())),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it be simpler to use .cloned() here instead of deref/copying addr and converting &str to String each time?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same feedback for the other instances below

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd prefer to only clone after filter. We could put this work inside filter_map, but rustfmt makes that one more LOC than this.

It's mapping (&IpAddr, &String) to (IpAddr, String), so I don't think cloned is a drop in replacement: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=e4cada359f5cb2fbd5af2031bf7e8be4

Do you know of a better way to do this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess not. I was thinking .cloned() would clone the values inside the tuple rather than the tuple itself, but that doesn't seem to be the case

@taspelund taspelund left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall I'm happy with this PR. I've got some nits here and there, but nothing I'd consider to be a blocker

@nicolaskagami nicolaskagami left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One thing I'm concerned about is the change in semantics from the "stealing" to outright rejection in the case of collision with a different existing tag. An alternative would be a "best effort" application of all entries that don't conflict, which would more closely match the current behavior (with the exception of the collided entry).

I've been thinking a lot about the reconciler pattern and this is one of many things it could opine about or at least present good practices for. Tags are a great tool for solving some of these conflict problems and I think we should have a discussion about their use.

@cfzimmerman

Copy link
Copy Markdown
Contributor Author

One thing I'm concerned about is the change in semantics from the "stealing" to outright rejection in the case of collision with a different existing tag. An alternative would be a "best effort" application of all entries that don't conflict, which would more closely match the current behavior (with the exception of the collided entry).

I've been thinking a lot about the reconciler pattern and this is one of many things it could opine about or at least present good practices for. Tags are a great tool for solving some of these conflict problems and I think we should have a discussion about their use.

Per Trey's comment, the new design is to reference count tags. So two tags can both declare that they want the same address on an interface, and the address is only deleted from the asic when the final tag deletes it. Thoughts on that?

@cfzimmerman
cfzimmerman removed this pull request from stack #382 November 6, 2026 15:33
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.

port_settings_apply clobbers link-local addresses added via link_ipv6_create

4 participants