python-iptables icon indicating copy to clipboard operation
python-iptables copied to clipboard

When using iptc.Target() inside namespace it keep it live even when delete those namespace - probably more important bug in table _cache mechanism..

Open hetii opened this issue 7 years ago • 4 comments

Hi, I try use iptc module inside namespace and as soon as I use iptc.Target() somehow it do something with my namespace that when I delete it from system my veth v0p0 is still there.

To reproduce this issue please uncomment target line and run below code, finally call: ip netns del ns1 ifconfig v0p0 and you will see v0p0 that is not removed.

Without "target" all works as expected so when I destroy network namespace then my veths gone as well.

    from pyroute2 import IPDB
    from pyroute2 import NetNS
    from nsenter import Namespace
    import iptc

    import logging
    import signal

    FORMAT = '%(asctime)-15s %(message)s'
    logging.basicConfig(format=FORMAT, level=logging.DEBUG)
    log = logging.getLogger(__name__)

    def main():
        log.debug("Runner started")
        netns_name = 'ns1'

        while True:
            log.debug("Before creating namespace.")
            NetNS(netns_name).close()

            with IPDB() as ip:
                with ip.create(ifname='v0p0', peer='v0p1', kind='veth') as veth_link:
                    veth_link.up()

                with ip.interfaces.v0p1 as v:
                    v.net_ns_fd = netns_name

            with Namespace('/var/run/netns/%s' % netns_name, 'net') as ns:
                table = iptc.Table(iptc.Table.NAT, autocommit = False)
                rule = iptc.Rule()
                # When uncomment and delete ns1 veth v0p0 is still on the host side.
                # target = iptc.Target(rule, "DNAT")
                table.close()

            signal.pause()

    if __name__ == '__main__':
        main()

hetii avatar May 01 '18 12:05 hetii

Here is even simpler example that show us when iptc.Target() if invoked then in the background 5 sockets are opened and till process is up they are also there so that`s the reason why my namespace is not removed in proper way.

#!/usr/bin/env python3
from pyroute2 import IPDB
from pyroute2 import NetNS
from nsenter import Namespace
import iptc

import logging
import signal

FORMAT = '%(asctime)-15s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.DEBUG)
log = logging.getLogger(__name__)

def main():
    log.debug("Runner started")
    netns_name = 'ns1'
    rule = iptc.Rule()
    target = iptc.Target(rule, "DNAT")
    signal.pause()

if __name__ == '__main__':
    main()

root@nec:/# python b.py &
[1] 359

root@nec:# ll /proc/359/fd
dr-x------ 2 root root  0 maj  1 15:34 ./
dr-xr-xr-x 9 root root  0 maj  1 15:34 ../
lrwx------ 1 root root 64 maj  1 15:34 0 -> /dev/pts/0
lrwx------ 1 root root 64 maj  1 15:34 1 -> /dev/pts/0
lrwx------ 1 root root 64 maj  1 15:34 2 -> /dev/pts/0
lrwx------ 1 root root 64 maj  1 15:34 3 -> 'socket:[1739512]'
lrwx------ 1 root root 64 maj  1 15:34 4 -> 'socket:[1739513]'
lrwx------ 1 root root 64 maj  1 15:34 5 -> 'socket:[1739514]'
lrwx------ 1 root root 64 maj  1 15:34 6 -> 'socket:[1739515]'
lrwx------ 1 root root 64 maj  1 15:34 7 -> 'socket:[1739516]'

hetii avatar May 01 '18 13:05 hetii

ok next small update.

It seams that when first time rule.tables is invoked then those socket object are created.

in [1]: import iptc
In [2]: rule = iptc.Rule()
In [3]: print(rule.tables)
[<iptc.ip4tc.Table at 0x7f4341cf6b38>,
 <iptc.ip4tc.Table at 0x7f4341d0b710>,
 <iptc.ip4tc.Table at 0x7f4341d0b160>,
 <iptc.ip4tc.Table at 0x7f4341d0b278>,
 <iptc.ip4tc.Table at 0x7f4341d0b6d8>]

lrwx------ 1 root root 64 maj  1 18:50 13 -> 'socket:[1996564]'
lrwx------ 1 root root 64 maj  1 18:50 14 -> 'socket:[1996565]'
lrwx------ 1 root root 64 maj  1 18:50 15 -> 'socket:[1996566]'
lrwx------ 1 root root 64 maj  1 18:50 16 -> 'socket:[1996567]'

It`s possible to close them by:

for tab in rule.tables:
     tab.close()

and my sockets gone, so far so good.

But now when I again create my rule and check his tables the same instances are still there.

In [6]: rule = iptc.Rule()
In [7]: rule.tables
Out[7]: 
[<iptc.ip4tc.Table at 0x7f4341cf6b38>,
 <iptc.ip4tc.Table at 0x7f4341d0b710>,
 <iptc.ip4tc.Table at 0x7f4341d0b160>,
 <iptc.ip4tc.Table at 0x7f4341d0b278>,
 <iptc.ip4tc.Table at 0x7f4341d0b6d8>]

So when we try again do something with iptc like creating target: target = iptc.Target(rule, "DNAT")

It will crash python and raise: memory protection violation, because we try to talk via closed sockets.

Because of that I decide at the end to clear internal cache that hold this table objects by: rule.tables[0]._cache.clear()

So now, it seams that I can create again my rules, but not sure how this _cache mechanism will impact other iptc components.

hetii avatar May 01 '18 17:05 hetii

I was about to ask if there might be some part of iptc maybe conflicting with switching network namespaces when I luckily found this bug report. I'm not exactly clear from this issue report how I can at least temporarily disable the table caching until the library author can fix this issue, or provide an official way to clear the cache?

After some source code reading I'm under the impression that I first need to close the table and then clear the cache inside the Table class, this assumes that t references a Table object:

t.close()
t._cache.clear()

thediveo avatar May 28 '18 17:05 thediveo

Hello there! After doing some digging I think it is safe to say that this is related to the underlying libiptc library itself and the open handles for each Table.

When creating an object Table(), the code instantiates the iptc library, which creates a handle that is populated on refresh(): https://github.com/ldx/python-iptables/blob/master/iptc/ip4tc.py#L1588. The handle is active until the table is close().

By looking at your code I see this flow of events:

  1. Create a Rule() object: rule = iptc.Rule()
  2. Create a Target() object: target = iptc.Target(rule, "DNAT")
  3. The initialization code of the Target object ends up calling rule.tables, which in turn creates the 5 Table instances (https://github.com/ldx/python-iptables/blob/542efdb739b4e3ef6f28274d23b506bf0027eec2/iptc/ip4tc.py#L980)
  4. As long as the objects are not GC and/or deleted, the sockets you see will remain open. The close() method is called upon deletion of the object (https://github.com/ldx/python-iptables/blob/master/iptc/ip4tc.py#L1600)

As far as this library is concerned, I don't see an easy way to provide netns awareness but do it on demand as @TheDiveO mentioned up there.

jllorente avatar Jan 30 '21 21:01 jllorente