Skip to content

Conversation

@huntergregory
Copy link
Contributor

@huntergregory huntergregory commented Nov 4, 2021

Changes for Applying IPSets

  • use ipset save file to add/delete members
  • update error handling logic to skip previously run lines in a restore file

Design Decision

Instead of calculating which members should be removed from ipsets, ipset manager will use the current kernel state via ipset save and apply the cache as the expected state.

Pros

  • for the dirty sets, the kernel will always become exactly as we desire (delete everything not desired from the kernel i.e. no leftover set members that aren't in the cache)
  • we are effectively reconciling and mitigating security concerns for dirty sets
  • no need to modify cache management at this time

Cons

  • increased compute and latency
  • added complexity and perhaps harder to maintain

Looking Forward

We will eventually have an infrequent reconcile routine which will reconcile the whole cache with the kernel state.

We will assess the extra latency of this design choice later.

Other changes

  • Allow proper exit codes and stdouts for piped commands in UTs.

TODO:

  • move some UT code into smaller-scoped functions for less complexity

…ipsets to skip previously run lines. Also update some logging for iptables chain management
@huntergregory huntergregory added the npm Related to NPM. label Nov 4, 2021
@huntergregory huntergregory changed the title [NPM] Update Linux IPSet Management feat: [NPM] Update Linux IPSet Management Nov 9, 2021
// error handling principal:
// - if contract with ipset save (or grep) is breaking, salvage what we can, take a snapshot without grep, and log the failure
// - have a background process for sending/removing snapshots intermittently
func (iMgr *IPSetManager) updateDirtyKernelSets(saveFile []byte, creator *ioutil.FileCreator) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this function walk through all ipset information stored in kernel?
If so, is it necessary?
If there are large number of ipsets are programmed in kernel, this cost will be high?

Do we leverage -exist option for add and del to make it simple?

# from IPSet man page
add SETNAME ADD-ENTRY [ ADD-OPTIONS ]
Add a given entry to the set. If the -exist option is specified, ipset ignores if the entry already added to the set.
del SETNAME DEL-ENTRY [ DEL-OPTIONS ]
Delete an entry from a set. If the -exist option is specified and the entry is not in the set (maybe already expired), then the command is ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we grep for all npm sets, so the work is linear in the number of sets in NPM. For non-dirty sets, we do skip reading all add lines in the save file, so this won't be too intensive. We wouldn't know what sets to delete without walking through the ipset save file, so del --exist couldn't work, and since we're already determining the members to delete for dirty sets, determining the exact members to add is equal work, and we write a smaller restore file compared to looping through all members and running add --exist

Copy link
Contributor

Choose a reason for hiding this comment

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

I may misunderstand the code, but in the code, it also has string match. So the work is not linear to the number of sets. It is linear to the number of sets and its string in each set.

My fundamental question in the code is why we need to walk through all ipsets (read from kernel and store in saveFile variable) in every applyIPSets call and derive some actions (add or deletion) even though we maintain a lot of data structures in ipsetmanager_linux.go. Can we leverage those information and apply what we need?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I guess the decision is between these options:

  1. read in save file for dirty sets
  2. maintain copy of ipsets object from before they became dirty

We're doing the first here. The second would require extra memory (worst case, doubling the memory for the ipset cache before applying) and would require a redesign of the whole ipset manager.

Thoughts @vakalapa ?

Copy link
Contributor

Choose a reason for hiding this comment

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

This operation is definitely very intensive wrt string matching, even if we skip over the section of some ipsets we will be doing first of line matching.

We chose not to save deleted members of IPSets to save memory because we are already very constrained on memory in linux and data structures we are maintaining in Ipset manager is just source of truth. This is modelled against k8s philosophy, we have a source of truth (our internal ipset cache), we read the state on the machine and try to reconcile to that state.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, this approach is robust in the sense that kernel state will hardly deviate from expected state since NPM takes a snapshot of sets to be updated and then applies changes based on that, current IPSM's draw back is that as errors occur and as NPM runs for longer duration, kernel state heavily deviates from what is expected and this results in a cascading affect of issues,

Hopefully in the future, when we figure out all the errors and have low-probability-for-error controller, then we can tone this approach down. But for initial purposes i feel, even with this compute intensive update, we are VERY fast compared to previous generation.

Wdygt?

Copy link
Contributor

Choose a reason for hiding this comment

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

If you guys think this is a way, I am ok with this approach.

I just wrote my two cents.
Based on my understanding, the goal of this enhancements in ipset in high-level is

  1. Compared to v1 NPM, in v2 we do kind of batch process (one shot for one controller event) unlike in v1 NPM (sequentially called exec.run call for each ipset operation) to improve performance.
  2. More fined-grained ipset management with reference counts to avoid known issues (e.g., used in kernel)
    Do I miss something?

I may be wrong and miss some details, but to me, it looks like that we took a kind of reverse engineering way, which is hard and complex way while we have enough information to derive all information since data plane is based on all information in control plane.

  1. We know when and which operations we need to program (e.g., add, deletion, update) based on controller event)
  2. We know what information are programmed from ipset manger data-structures
  3. All the references counts for ipsets
    So, I think we can achieve the same functionality with those information in much simple way.

About performance, with a lot of ipsets, I am not sure how much we gains since we do string match O(# of ipsets * # of characters per ipset) in every event which needs applyIPset.

Copy link
Contributor

Choose a reason for hiding this comment

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

As we discussed offline, this current design is more secure than alternatives because even if some malicious actor changes the ipsets, NPM will try to reconcile to the desired state and overwrite manual entries. Granted NPM will only do this for "dirty" sets. A backlog item is added to have a background thread "at leisure" should try to reconcile all ipsets to the desired state, this design will make NPM more secure in the long run.

Also, perf evaluation is planned on this current design to understand at scale what latency this compute intensive task is going to induce in rule programming.

deleteErrCode, deleteErr := pMgr.runIPTablesCommand(util.IptablesDeletionFlag, jumpFromForwardToAzureChainArgs...)
hadDeleteError := deleteErr != nil && deleteErrCode != couldntLoadTargetErrorCode
if hadDeleteError {
deleteErrCode, deleteErr := pMgr.runIPTablesCommandAndIgnoreErrorCode(couldntLoadTargetErrorCode, util.IptablesDeletionFlag, jumpFromForwardToAzureChainArgs...)
Copy link
Contributor

Choose a reason for hiding this comment

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

In deletion operation, is it ok not to get error code about doesNotExistErrorCode or couldntLoadTargetErrorCode, etc?
Anyway, the goal is to clean up the tables.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if the AZURE-NPM chain doesn't exist, we'll get couldntLoadTargetErrorCode and if the chain exists but the rule doesn't exist, we'll get doesNotExistErrorCode. So this should ignore both of these codes actually, since this is called before initializing DP.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if there's an error besides those two, something's wrong, but it could be several things, so ya maybe we just log and continue.

For instance, we get couldntLoadTargetErrorCode for "unknown option" and for a malformed IP address.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

these changes will be moved to the next PR which updates policy/chain management

allArgsString := strings.Join(allArgs, " ")
msgStr := strings.TrimSuffix(string(output), "\n")
if errCode > 0 && operationFlag != util.IptablesCheckFlag {
if errCode > 0 && errCode != errCodeToIgnore {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you check removeNPMChains case with errCodeToIgnore?
In case the chain does not exists, it is fine to proceed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry, could you clarify this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems you resolved it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

these changes will be moved to the next PR which updates policy/chain management

return pMgr.runIPTablesCommandAndIgnoreErrorCode(-1, operationFlag, args...)
}

func (pMgr *PolicyManager) runIPTablesCommandAndIgnoreErrorCode(errCodeToIgnore int, operationFlag string, args ...string) (int, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It is a little hard to follow which operations are ok to ignore which errors.
Do we delegate error handling in a caller side?

We just have one run function and a caller will do right actions bases on return values.
It will be easier to follow up codes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The main consideration for ignoring an error code in this function is that this function logs when there's an error, but I think it shouldn't log an error if we ignore the error later. We previously had some of that code baked into run() for check operations, but I think we should also do it for deleting a rule.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

these changes will be moved to the next PR which updates policy/chain management

@JungukCho
Copy link
Contributor

One comment: naming (file-creator.go and its contents) is misleading if I correctly understand the code. IPset and iptables use stdout to program them. Initially, I am looking for where is file.

@huntergregory
Copy link
Contributor Author

One comment: naming (file-creator.go and its contents) is misleading if I correctly understand the code. IPset and iptables use stdout to program them. Initially, I am looking for where is file.

Open to suggestions. How about restore-file-creator.go?

@JungukCho
Copy link
Contributor

One comment: naming (file-creator.go and its contents) is misleading if I correctly understand the code. IPset and iptables use stdout to program them. Initially, I am looking for where is file.

Open to suggestions. How about restore-file-creator.go?
May be restoreLinuxDP.go since both iptables and ipset uses this for mainly restore, but your suggestion is ok.
Just having file makes me confused, but I may be so sensitive.

@huntergregory
Copy link
Contributor Author

One comment: naming (file-creator.go and its contents) is misleading if I correctly understand the code. IPset and iptables use stdout to program them. Initially, I am looking for where is file.

Open to suggestions. How about restore-file-creator.go?
May be restoreLinuxDP.go since both iptables and ipset uses this for mainly restore, but your suggestion is ok.
Just having file makes me confused, but I may be so sensitive.

ok I'll call it restore_linux.go. Are you ok with the name FileCreator for the struct?

@JungukCho
Copy link
Contributor

JungukCho commented Nov 11, 2021

One comment: naming (file-creator.go and its contents) is misleading if I correctly understand the code. IPset and iptables use stdout to program them. Initially, I am looking for where is file.

Open to suggestions. How about restore-file-creator.go?
May be restoreLinuxDP.go since both iptables and ipset uses this for mainly restore, but your suggestion is ok.
Just having file makes me confused, but I may be so sensitive.

ok I'll call it restore_linux.go. Are you ok with the name FileCreator for the struct?

If it makes sense to you, I am fine with it.

@huntergregory huntergregory changed the title feat: [NPM] Update Linux IPSet Management feat: [NPM] ipset save before restoring and fixing grep UTs Nov 11, 2021
@vakalapa
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@vakalapa
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

if len(iMgr.toAddOrUpdateCache) > 0 {
saveFile, saveError = iMgr.ipsetSave()
if saveError != nil {
return fmt.Errorf("%w", saveError)
Copy link
Member

Choose a reason for hiding this comment

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

can we wrap with context so we know the save error came from this stack

Copy link
Contributor

@JungukCho JungukCho left a comment

Choose a reason for hiding this comment

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

LGTM! Thank you for applying comments.
We can walk about translation and linux dataplane before integration test.

@huntergregory huntergregory merged commit 17ed0b8 into master Nov 16, 2021
@vakalapa vakalapa deleted the ipset-save-restore branch November 16, 2021 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

npm Related to NPM.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants