From fc7705a13f44f4659ef686b4e601e188790a555e Mon Sep 17 00:00:00 2001 From: Zeyad Yasser Date: Thu, 29 Apr 2021 21:18:58 +0200 Subject: [PATCH] zdtm: add network namespace locking test When criu dumps a process in a network namespace it locks the network so that no packets from peer enters the stack, otherwise RST will be sent by a kernel causing the connection to fail. In netns_lock.c we try to enter the netns created by post-start hook so that criu locks the network namespace between dump and restore. A TCP server is started in post-start hook inside the test netns and runs in the background detached from its parent so that it stays alive for the duration of the test. Other hooks (pre-dump, pre-restore, post-restore) try to connect to the server. Pre-dump and post-restore hooks should be able to connect successfully. Pre-restore hook client with SOCCR_MARK should also connect successfully. Pre-restore hook client without SOCCR_MARK should not be able to connect but also should not get connection refused as all packets are dropped in the namespace so the kernel shouldn't send an RST packet as a result. Instead we check that the connect operation causes a timeout. This test would be useful when testing that the network is locked using different ways (using iptables currently and other methods later). v2: - check that packets with SOCCR_MARK are allowed to pass when the netns is locked. v3: - fix pre-restore hook skipping non SOCCR_MARK connection test due to early exit in SOCCR_MARK variant. Signed-off-by: Zeyad Yasser --- test/zdtm/static/Makefile | 1 + test/zdtm/static/netns_lock.c | 71 +++++++++++++++ test/zdtm/static/netns_lock.desc | 6 ++ test/zdtm/static/netns_lock.hook | 144 +++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 test/zdtm/static/netns_lock.c create mode 100644 test/zdtm/static/netns_lock.desc create mode 100755 test/zdtm/static/netns_lock.hook diff --git a/test/zdtm/static/Makefile b/test/zdtm/static/Makefile index 1c11b36c3..d998dcc83 100644 --- a/test/zdtm/static/Makefile +++ b/test/zdtm/static/Makefile @@ -214,6 +214,7 @@ TST_NOFILE := \ uffd-events \ thread_different_uid_gid \ pipe03 \ + netns_lock \ netns_sub \ netns_sub_veth \ netns_sub_sysctl \ diff --git a/test/zdtm/static/netns_lock.c b/test/zdtm/static/netns_lock.c new file mode 100644 index 000000000..300349c03 --- /dev/null +++ b/test/zdtm/static/netns_lock.c @@ -0,0 +1,71 @@ +#include "zdtmtst.h" + +const char *test_doc = "Check network namespace is locked between dump and restore\n"; +const char *test_author = "Zeyad Yasser "; + +#include +#include +#include + +#define NS_PATH "/var/run/netns/criu-net-lock-test" +#define MAX_RETRY 3 + +int main(int argc, char **argv) +{ + int i, ns_fd; + + test_init(argc, argv); + + /* + * We try to enter the netns created by post-start hook so that + * criu locks the network namespace between dump and restore. + * + * A TCP server is started in post-start hook inside the netns + * and runs in the background detached from its parent so that + * it stays alive for the duration of the test. + * + * Other hooks (pre-dump, pre-restore, post-restore) try to + * connect to the server. + * + * Pre-dump and post-restore hooks should be able to connect + * successfully. + * + * Pre-restore hook client with SOCCR_MARK should also connect + * successfully. + * + * Pre-restore hook client without SOCCR_MARK should not be able + * to connect but also should not get connection refused as all + * packets are dropped in the namespace so the kernel shouldn't + * send an RST packet as a result. Instead we check that the + * connect operation causes a timeout. + */ + + for (i = 0; i < MAX_RETRY; i++) { + ns_fd = open(NS_PATH, O_RDONLY); + if (ns_fd < 0) { + /* Netns not created yet by post-start hook */ + sleep(1); + continue; + } + break; + } + + if (ns_fd < 0) { + pr_perror("can't open network ns"); + return 1; + } + + if (setns(ns_fd, CLONE_NEWNET)) { + pr_perror("setns %d", ns_fd); + return 1; + } + + close(ns_fd); + + test_daemon(); + test_waitsig(); + + pass(); + + return 0; +} diff --git a/test/zdtm/static/netns_lock.desc b/test/zdtm/static/netns_lock.desc new file mode 100644 index 000000000..79362790e --- /dev/null +++ b/test/zdtm/static/netns_lock.desc @@ -0,0 +1,6 @@ +{ + 'flavor': 'h', + 'flags': 'suid', + 'opts': '--tcp-established', + 'ropts': '--join-ns net:/var/run/netns/criu-net-lock-test' +} diff --git a/test/zdtm/static/netns_lock.hook b/test/zdtm/static/netns_lock.hook new file mode 100755 index 000000000..bd5967c94 --- /dev/null +++ b/test/zdtm/static/netns_lock.hook @@ -0,0 +1,144 @@ +#!/usr/bin/env python + +import subprocess +import socket +import sys +import os + +from ctypes import CDLL + +libc = CDLL('libc.so.6') + +SO_MARK = 36 # Define socket option for python2 compatibility +SOCCR_MARK = 0xC114 +CLONE_NEWNET = 0x40000000 +PORT = 8880 +NETNS = "criu-net-lock-test" +TIMEOUT = 0.1 + +def nsenter(): + with open("/var/run/netns/{}".format(NETNS)) as f: + libc.setns(f.fileno(), CLONE_NEWNET) + +if sys.argv[1] == "--post-start": + # Add test netns + subprocess.Popen(["ip", "netns", "add", NETNS]).wait() + nsenter() # Enter test netns + subprocess.Popen(["ip", "link", "set", "up", "dev", "lo"]).wait() + + # TCP server daemon + pid = os.fork() + if(pid == 0): + os.setsid() # Detach from parent + # Run server + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.bind(("127.0.0.1", PORT)) + srv.listen(1) + + # We should accept connections normally from + # pre-dump client, post-restore client and + # the pre-restore client using SOCCR_MARK + # The pre-restore client without SOCCR_MARK + # should fail with a timeout + + # Accept pre-dump client + cln, addr = srv.accept() + cln.sendall(str.encode("--pre-dump")) + cln.close() + + # Accept pre-restore client with SOCCR_MARK + srv.setsockopt(socket.SOL_SOCKET, SO_MARK, SOCCR_MARK) + cln, addr = srv.accept() + cln.sendall(str.encode("--pre-restore")) + cln.close() + srv.setsockopt(socket.SOL_SOCKET, SO_MARK, 0) + + # Accept post-restore client + cln, addr = srv.accept() + cln.sendall(str.encode("--post-restore")) + cln.close() + + srv.close() + +if sys.argv[1] == "--pre-dump": + # Network is not locked yet + # Client should be able to connect normally + + nsenter() # Enter test netns + + socket.setdefaulttimeout(TIMEOUT) + cln = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + cln.connect(("127.0.0.1", PORT)) + resp = cln.recv(100).decode() + if resp != sys.argv[1]: + print("Expected '{}', got '{}'".format(sys.argv[1], resp)) + sys.exit(1) + except socket.timeout: + print("Timeout when trying to connect to server") + sys.exit(1) + else: + sys.exit(0) + finally: + cln.close() + +if sys.argv[1] == "--pre-restore": + # Network should be locked now + # Only packets with SOCCR_MARK should be allowed + # Client should timeout when connecting without SOCCR_MARK + + nsenter() + + socket.setdefaulttimeout(TIMEOUT) + # SOCCR_MARK + try: + cln = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + cln.setsockopt(socket.SOL_SOCKET, SO_MARK, SOCCR_MARK) + cln.connect(("localhost", PORT)) + resp = cln.recv(100).decode() + if resp != sys.argv[1]: + print("Expected '{}', got '{}'".format(sys.argv[1], resp)) + sys.exit(1) + except socket.timeout: + print("Timeout when trying to connect to server") + sys.exit(1) + finally: + cln.close() + + # No SOCCR_MARK + try: + cln = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + cln.connect(("localhost", PORT)) + except socket.timeout: + sys.exit(0) + else: + print("Network should be locked, but client connected normally") + sys.exit(1) + finally: + cln.close() + +if sys.argv[1] == "--post-restore": + # Network should be unlocked now + # Client should be able to connect normally + + nsenter() + + socket.setdefaulttimeout(TIMEOUT) + cln = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + cln.connect(("localhost", PORT)) + resp = cln.recv(100).decode() + if resp != sys.argv[1]: + print("Expected '{}', got '{}'".format(sys.argv[1], resp)) + sys.exit(1) + except socket.timeout: + print("Timeout when trying to connect to server") + sys.exit(1) + else: + sys.exit(0) + finally: + cln.close() + +if sys.argv[1] == "--clean": + # Delete test netns + subprocess.Popen(["ip", "netns", "delete", NETNS]).wait()