ср, 9 мар. 2022 г. в 19:40, Daniel P. Berrangé <berrange@xxxxxxxxxx>:
On Wed, Mar 02, 2022 at 11:34:56AM +0300, Nikolay Shirokovskiy wrote:
> In order to delete or rename tree of chains in netfilter we use generic
> code that inspect current state of netfilter and traverse the tree
> accordingly. I found the way traverse is done a bit hard to think about.
>
> As an example I'll take ebtable rules generated by
> ebiptablesTearOldRules for input chains only. Common arguments like '-t
> nat' and '--concurrent' are also filtered. To illustrate the point
> libvirt-I-vnet1 has one subchain I-vnet1-mac.
>
> Let's look at ebtablesRemoveSubChainsFW[1] work. It make a recursive
> deletion of chains. I marked below the ebtables calls that it generates
> in the whole list of commands that generates ebiptablesTearOldRules. One
> can see that the rules perplex with the ebtablesRenameTmpSubAndRootChainsFW[2]
> rules and this looks a bit hard to manage. I'd prefer that the order of
> ebtables commands will be aligned with code.
>
> ebtables -D PREROUTING -i vnet1 -j libvirt-I-vnet1
> ebtables -L libvirt-I-vnet1 # listing to detect subchains[1]
> ebtables -F libvirt-I-vnet1
> ebtables -X libvirt-I-vnet1
> ebtables -L libvirt-J-vnet1 # [2]
> ebtables -E libvirt-J-vnet1 libvirt-I-vnet1 # [2]
> ebtables -L I-vnet1-mac # listing to detect subchains[1]
> ebtables -F I-vnet1-mac # flushing to unlink subchains[1]
> ebtables -X I-vnet1-mac # deleting chain itself[1]
> ebtables -L J-vnet1-mac # [2]
> ebtables -F I-vnet1-mac # [2]
> ebtables -X I-vnet1-mac # [2]
> ebtables -E J-vnet1-mac I-vnet1-mac # [2]
>
> Notice also that we need to flush libvirt-I-vnet1 to unlink subchains in
> order to be able to delete them. And again code look like reversed:
>
> ebtablesRemoveSubChainsFW(fw, ifname);
> ebtablesRemoveRootChainFW(fw, true, ifname);
> ebtablesRemoveRootChainFW(fw, false, ifname);
>
> This is because ebtablesRemoveSubChainsFW only adds listing call and
> makes actual subchains cleanup in the the process of virFirewallApply.
>
> So to make it more manageble/straightforward I propose here to list and
> add resulted commands inplace. Note that in the process we need:
>
> 1. Move ebtablesRemoveTmpRootChainFW rules to the _ebtablesRemoveSubChainsFW
> to keep correct order - first flush parent and then delete subchains.
>
> 2. Avoid using virFirewallStartRollback as otherwise there will be
> unnesseary listing call done in ebtablesRemoveTmpSubChainsFW.
>
> The fact we can't use virFirewallStartRollback in this approach can be
> seen as a donwside. However it is not a real rollback - you need to code
> all the steps to rollback on you own so it can be easily replaced by
> same virFirewallApply. Not only in this place but in all the other places.
>
> Now compare the commands order in ebiptablesTearOldRules after patch:
>
> ebtables -L libvirt-I-vnet1 # listing to detect subchains [1]
> ebtables -L I-vnet1-mac # listing to detect subchains [1]
> ebtables -D PREROUTING -i vnet1 -j libvirt-I-vnet1
> ebtables -F libvirt-I-vnet1 # flushing and deleting of chain [1]
> ebtables -X libvirt-I-vnet1
> ebtables -F I-vnet1-mac # flushing and deleting of subchain [1]
> ebtables -X I-vnet1-mac
> ebtables -L libvirt-J-vnet1
> ebtables -E libvirt-J-vnet1 libvirt-I-vnet1
> ebtables -L J-vnet1-mac
> ebtables -F I-vnet1-mac
> ebtables -X I-vnet1-mac
> ebtables -E J-vnet1-mac I-vnet1-mac
>
> Notice that listing commands from ebtablesRemoveSubChainsFW pop up to
> the top of the list. This may seen again as perplexing but it is of
> a different kind. It only moves nonmodifying listing commands to the top
> which does not perplexing understanding command flow much. Also if the
> calls will be optimized we have only one listing command on the
> top.
> ---
> src/nwfilter/nwfilter_ebiptables_driver.c | 154 +++++++++++-----------
> 1 file changed, 75 insertions(+), 79 deletions(-)
>
> diff --git a/src/nwfilter/nwfilter_ebiptables_driver.c b/src/nwfilter/nwfilter_ebiptables_driver.c
> index 54065a0f75..6952ebc059 100644
> --- a/src/nwfilter/nwfilter_ebiptables_driver.c
> +++ b/src/nwfilter/nwfilter_ebiptables_driver.c
> @@ -148,6 +148,42 @@ static char chainprefixes_host_temp[3] = {
> 0
> };
>
> +
> +static int
> +ebiptablesQueryCallback(virFirewall *fw G_GNUC_UNUSED,
> + virFirewallLayer layer G_GNUC_UNUSED,
> + const char *const *lines,
> + void *opaque)
> +{
> + GStrv *capture = opaque;
> +
> + *capture = g_strdupv((gchar **)lines);
> + return 0;
> +}
> +
> +
> +static GStrv
> +ebiptablesQuery(virFirewallLayer layer,
> + ...)
> +{
> + g_autoptr(virFirewall) fw = virFirewallNew();
> + g_auto(GStrv) capture = NULL;
> + va_list args;
> +
> + virFirewallStartTransaction(fw, 0);
> +
> + va_start(args, layer);
> + virFirewallAddRuleFullV(fw, layer, true,
> + ebiptablesQueryCallback, &capture, args);
> + va_end(args);
> +
> + if (virFirewallApply(fw) < 0)
> + return NULL;
> +
> + return g_steal_pointer(&capture);
> +}
This method illustrates why we didn't take this approach in the
first place. The intention of the current design is to have
clean separation between deciding what iptables commands to
invoke, and when we actually invoke them.
This breaks that separation, because now in the middle of
populating the virFirewall rules we want to run, we have
to actually invoke the firewall tools multiple times to
query current state.
> @@ -2649,10 +2644,9 @@ static int
> ebtablesRemoveSubChainsQuery(virFirewall *fw,
> virFirewallLayer layer,
> const char *const *lines,
> - void *opaque)
> + const char *chainprefixes)
> {
> size_t i, j;
> - const char *chainprefixes = opaque;
>
> for (i = 0; lines[i] != NULL; i++) {
> char *tmp = strstr(lines[i], "-j ");
> @@ -2665,17 +2659,21 @@ ebtablesRemoveSubChainsQuery(virFirewall *fw,
> for (j = 0; chainprefixes[j]; j++) {
> if (tmp[0] == chainprefixes[j] &&
> tmp[1] == '-') {
> + g_auto(GStrv) capture = NULL;
> +
> VIR_DEBUG("Processing chain '%s'", tmp);
> - virFirewallAddRuleFull(fw, layer,
> - false, ebtablesRemoveSubChainsQuery,
> - (void *)chainprefixes,
> - "-t", "nat", "-L", tmp, NULL);
> + capture = ebiptablesQuery(layer,
> + "-t", "nat", "-L", tmp, NULL);
> virFirewallAddRuleFull(fw, layer,
> true, NULL, NULL,
> "-t", "nat", "-F", tmp, NULL);
> virFirewallAddRuleFull(fw, layer,
> true, NULL, NULL,
> "-t", "nat", "-X", tmp, NULL);
> + if (capture)
> + ebtablesRemoveSubChainsQuery(fw, layer,
> + (const char *const *)capture,
> + chainprefixes);
> }
....here we're still populating the 'fw' object, but actually
also running firewall commands at the same time inside the
ebtablesRemoveSubChainsQuery method.
The current implementation on the backend of virFirewallApply
is not technically transactional, but the intent behind the
design is that we would be able to serialize execution of the
virFirewallApply method. This wouldn't be possible with the
way this patch changes things, as now we're dealing with
multiple virFirewall object instances.
Well I'd say if we'd have a transaction like with a nfttables we anyway first
have to inspect tables then make a patch and then apply it. You have to inspect tables
anyway outside the transaction. Which is basically what this patch does.
Anyway I think it is for good to address the issue with perplexing rules from different
larger logical operations which is illustrated above (removing old chains and renaming tmp chains
to non-tmp ones.
As an alternative I considered that rules that are created inside the transaction could be added
to some marked place in the rules list instead of always at the end of the list. But IMHO this
is more complicated. I'd prefer a simple approach which is to inspect tables/create patch/apply patch.
Nikolay