test: Add framework for containerized testing

On Linux we can potentially use user and UTS namespaces to run  a test
in a pseudo-container with:
 - arbitrary filesystem (e.g. /etc/resolv.conf, /etc/nsswitch.conf, /etc/hosts)
 - arbitrary hostname/domainname.

Include a first pass at the framework code to allow this, along with a
first test case that uses the container.
pull/34/head
David Drysdale 9 years ago
parent 439eb45cc0
commit 5dc450a4b3
  1. 54
      m4/ax_check_user_namespace.m4
  2. 76
      m4/ax_check_uts_namespace.m4
  3. 1
      test/Makefile.inc
  4. 44
      test/ares-test-init.cc
  5. 141
      test/ares-test-ns.cc
  6. 58
      test/ares-test.cc
  7. 44
      test/ares-test.h
  8. 2
      test/configure.ac

@ -0,0 +1,54 @@
# -*- Autoconf -*-
# SYNOPSIS
#
# AX_CHECK_USER_NAMESPACE
#
# DESCRIPTION
#
# This macro checks whether the local system supports Linux user namespaces.
# If so, it calls AC_DEFINE(HAVE_USER_NAMESPACE).
AC_DEFUN([AX_CHECK_USER_NAMESPACE],[dnl
AC_CACHE_CHECK([whether user namespaces are supported],
ax_cv_user_namespace,[
AC_LANG_PUSH([C])
AC_RUN_IFELSE([AC_LANG_SOURCE([[
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int userfn(void *d) {
usleep(100000); /* synchronize by sleep */
return (getuid() != 0);
}
char userst[1024*1024];
int main() {
char buffer[1024];
int rc, status, fd;
pid_t child = clone(userfn, userst + 1024*1024, CLONE_NEWUSER|SIGCHLD, 0);
if (child < 0) return 1;
sprintf(buffer, "/proc/%d/uid_map", child);
fd = open(buffer, O_CREAT|O_WRONLY|O_TRUNC, 0755);
sprintf(buffer, "0 %d 1\n", getuid());
write(fd, buffer, strlen(buffer));
close(fd);
rc = waitpid(child, &status, 0);
if (rc <= 0) return 1;
if (!WIFEXITED(status)) return 1;
return WEXITSTATUS(status);
}
]])],[ax_cv_user_namespace=yes], [ax_cv_user_namespace=no])
AC_LANG_POP([C])
])
if test "$ax_cv_user_namespace" = yes; then
AC_DEFINE([HAVE_USER_NAMESPACE],[1],[Whether user namespaces are available])
fi
]) # AX_CHECK_USER_NAMESPACE

@ -0,0 +1,76 @@
# -*- Autoconf -*-
# SYNOPSIS
#
# AX_CHECK_UTS_NAMESPACE
#
# DESCRIPTION
#
# This macro checks whether the local system supports Linux UTS namespaces.
# Also requires user namespaces to be available, so that non-root users
# can enter the namespace.
# If so, it calls AC_DEFINE(HAVE_UTS_NAMESPACE).
AC_DEFUN([AX_CHECK_UTS_NAMESPACE],[dnl
AC_CACHE_CHECK([whether UTS namespaces are supported],
ax_cv_uts_namespace,[
AC_LANG_PUSH([C])
AC_RUN_IFELSE([AC_LANG_SOURCE([[
#define _GNU_SOURCE
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int utsfn(void *d) {
char buffer[1024];
const char *name = "autoconftest";
int rc = sethostname(name, strlen(name));
if (rc != 0) return 1;
gethostname(buffer, 1024);
return (strcmp(buffer, name) != 0);
}
char st2[1024*1024];
int fn(void *d) {
pid_t child;
int rc, status;
usleep(100000); /* synchronize by sleep */
if (getuid() != 0) return 1;
child = clone(utsfn, st2 + 1024*1024, CLONE_NEWUTS|SIGCHLD, 0);
if (child < 0) return 1;
rc = waitpid(child, &status, 0);
if (rc <= 0) return 1;
if (!WIFEXITED(status)) return 1;
return WEXITSTATUS(status);
}
char st[1024*1024];
int main() {
char buffer[1024];
int rc, status, fd;
pid_t child = clone(fn, st + 1024*1024, CLONE_NEWUSER|SIGCHLD, 0);
if (child < 0) return 1;
sprintf(buffer, "/proc/%d/uid_map", child);
fd = open(buffer, O_CREAT|O_WRONLY|O_TRUNC, 0755);
sprintf(buffer, "0 %d 1\n", getuid());
write(fd, buffer, strlen(buffer));
close(fd);
rc = waitpid(child, &status, 0);
if (rc <= 0) return 1;
if (!WIFEXITED(status)) return 1;
return WEXITSTATUS(status);
}
]])
],[ax_cv_uts_namespace=yes], [ax_cv_uts_namespace=no])
AC_LANG_POP([C])
])
if test "$ax_cv_uts_namespace" = yes; then
AC_DEFINE([HAVE_UTS_NAMESPACE],[1],[Whether UTS namespaces are available])
fi
]) # AX_CHECK_UTS_NAMESPACE

@ -1,6 +1,7 @@
TESTSOURCES = ares-test-main.cc \
ares-test-init.cc \
ares-test.cc \
ares-test-ns.cc \
ares-test-parse.cc \
ares-test-parse-a.cc \
ares-test-parse-aaaa.cc \

@ -1,4 +1,3 @@
#include "ares-test.h"
// library initialization is only needed for windows builds
@ -294,5 +293,48 @@ TEST(Init, NoLibraryInit) {
}
#endif
#ifdef HAVE_CONTAINER
// These tests rely on the ability of non-root users to create a chroot
// using Linux namespaces.
TEST(LibraryInit, ContainerChannelInit) {
TransientDir root("chroot");
TransientDir etc("chroot/etc");
TransientFile resolv("chroot/etc/resolv.conf",
"nameserver 1.2.3.4\n"
"search first.com second.com\n");
TransientFile hosts("chroot/etc/hosts",
"3.4.5.6 ahostname.com");
TransientFile nsswitch("chroot/etc/nsswitch.conf",
"hosts: files\n");
auto testfn = [] () {
ares_channel channel = nullptr;
EXPECT_EQ(ARES_SUCCESS, ares_init(&channel));
std::vector<std::string> actual = GetNameServers(channel);
std::vector<std::string> expected = {"1.2.3.4"};
EXPECT_EQ(expected, actual);
struct ares_options opts;
int optmask = 0;
ares_save_options(channel, &opts, &optmask);
EXPECT_EQ(2, opts.ndomains);
EXPECT_EQ(std::string("first.com"), std::string(opts.domains[0]));
EXPECT_EQ(std::string("second.com"), std::string(opts.domains[1]));
ares_destroy_options(&opts);
HostResult result;
ares_gethostbyname(channel, "ahostname.com", AF_INET, HostCallback, &result);
ProcessWork(channel, NoExtraFDs, nullptr);
EXPECT_TRUE(result.done_);
std::stringstream ss;
ss << result.host_;
EXPECT_EQ("{'ahostname.com' aliases=[] addrs=[3.4.5.6]}", ss.str());
return HasFailure();
};
CONTAINER_RUN("chroot", "myhostname", "mydomainname.org", testfn);
}
#endif
} // namespace test
} // namespace ares

@ -0,0 +1,141 @@
#include "ares-test.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <iostream>
#include <functional>
#include <string>
#include <sstream>
#include <vector>
#ifdef HAVE_CONTAINER
namespace ares {
namespace test {
namespace {
struct ContainerInfo {
std::string dirname_;
std::string hostname_;
std::string domainname_;
VoidToIntFn fn_;
};
int EnterContainer(void *data) {
ContainerInfo *container = (ContainerInfo*)data;
if (verbose) {
std::cerr << "Running function in container {chroot='"
<< container->dirname_ << "', hostname='" << container->hostname_
<< "', domainname='" << container->domainname_ << "'}"
<< std::endl;
}
// Ensure we are apparently root before continuing.
int count = 10;
while (getuid() != 0 && count > 0) {
usleep(100000);
count--;
}
if (getuid() != 0) {
std::cerr << "Child in user namespace has uid " << getuid() << std::endl;
return -1;
}
// Move into the specified directory.
if (chdir(container->dirname_.c_str()) != 0) {
std::cerr << "Failed to chdir('" << container->dirname_
<< "'), errno=" << errno << std::endl;
return -1;
}
// And make it the new root directory;
char buffer[PATH_MAX + 1];
if (getcwd(buffer, PATH_MAX) == NULL) {
std::cerr << "failed to retrieve cwd, errno=" << errno << std::endl;
return -1;
}
buffer[PATH_MAX] = '\0';
if (chroot(buffer) != 0) {
std::cerr << "chroot('" << buffer << "') failed, errno=" << errno << std::endl;
return -1;
}
// Set host/domainnames if specified
if (!container->hostname_.empty()) {
if (sethostname(container->hostname_.c_str(),
container->hostname_.size()) != 0) {
std::cerr << "Failed to sethostname('" << container->hostname_
<< "'), errno=" << errno << std::endl;
return -1;
}
}
if (!container->domainname_.empty()) {
if (setdomainname(container->domainname_.c_str(),
container->domainname_.size()) != 0) {
std::cerr << "Failed to setdomainname('" << container->domainname_
<< "'), errno=" << errno << std::endl;
return -1;
}
}
return container->fn_();
}
} // namespace
// Run a function while:
// - chroot()ed into a particular directory
// - having a specified hostname/domainname
int RunInContainer(const std::string& dirname, const std::string& hostname,
const std::string& domainname, VoidToIntFn fn) {
const int stack_size = 1024 * 1024;
std::vector<byte> stack(stack_size, 0);
ContainerInfo container = {dirname, hostname, domainname, fn};
// Start a child process in a new user and UTS namespace
pid_t child = clone(EnterContainer, stack.data() + stack_size,
CLONE_NEWUSER|CLONE_NEWUTS|SIGCHLD, (void *)&container);
if (child < 0) {
std::cerr << "Failed to clone()" << std::endl;
return -1;
}
// Build the UID map that makes us look like root inside the namespace.
std::stringstream mapfiless;
mapfiless << "/proc/" << child << "/uid_map";
std::string mapfile = mapfiless.str();
int fd = open(mapfile.c_str(), O_CREAT|O_WRONLY|O_TRUNC, 0644);
if (fd < 0) {
std::cerr << "Failed to create '" << mapfile << "'" << std::endl;
return -1;
}
std::stringstream contentss;
contentss << "0 " << getuid() << " 1" << std::endl;
std::string content = contentss.str();
int rc = write(fd, content.c_str(), content.size());
if (rc != (int)content.size()) {
std::cerr << "Failed to write uid map to '" << mapfile << "'" << std::endl;
}
close(fd);
// Wait for the child process and retrieve its status.
int status;
waitpid(child, &status, 0);
if (rc <= 0) {
std::cerr << "Failed to waitpid(" << child << ")" << std::endl;
return -1;
}
if (!WIFEXITED(status)) {
std::cerr << "Child " << child << " did not exit normally" << std::endl;
return -1;
}
return status;
}
} // namespace test
} // namespace ares
#endif

@ -5,10 +5,6 @@
#include "nameser.h"
#include "ares_dns.h"
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#ifdef HAVE_NETDB_H
#include <netdb.h>
#endif
@ -24,9 +20,11 @@
#ifdef WIN32
#define BYTE_CAST (char *)
#define sclose(x) closesocket(x)
#define mkdir_(d, p) mkdir(d)
#else
#define BYTE_CAST
#define sclose(x) close(x)
#define mkdir_(d, p) mkdir(d, p)
#endif
namespace ares {
@ -415,7 +413,7 @@ MockChannelOptsTest::MockChannelOptsTest(int count,
// Set up servers after construction so we can set individual ports
struct ares_addr_port_node* prev = nullptr;
struct ares_addr_port_node* first;
struct ares_addr_port_node* first = nullptr;
for (const auto& server : servers_) {
struct ares_addr_port_node* node = (struct ares_addr_port_node*)malloc(sizeof(*node));
if (prev) {
@ -624,29 +622,49 @@ std::vector<std::string> GetNameServers(ares_channel channel) {
return results;
}
TempFile::TempFile(const std::string& contents)
: filename_(tempnam(nullptr, "ares")) {
if (!filename_) {
std::cerr << "Error: failed to generate temporary filename" << std::endl;
return;
TransientDir::TransientDir(const std::string& dirname) : dirname_(dirname) {
if (mkdir_(dirname_.c_str(), 0755) != 0) {
std::cerr << "Failed to create subdirectory '" << dirname_ << "'" << std::endl;
}
FILE *f = fopen(filename_, "w");
if (!f) {
std::cerr << "Error: failed to create temporary file " << filename_ << std::endl;
}
TransientDir::~TransientDir() {
rmdir(dirname_.c_str());
}
TransientFile::TransientFile(const std::string& filename,
const std::string& contents)
: filename_(filename) {
FILE *f = fopen(filename.c_str(), "w");
if (f == nullptr) {
std::cerr << "Error: failed to create '" << filename << "'" << std::endl;
return;
}
int rc = fwrite(contents.data(), 1, contents.size(), f);
if (rc < (int)contents.size()) {
std::cerr << "Error: failed to store data in temporary file " << filename_ << std::endl;
if (rc != (int)contents.size()) {
std::cerr << "Error: failed to write contents of '" << filename << "'" << std::endl;
}
fclose(f);
}
TempFile::~TempFile() {
if (filename_) {
unlink(filename_);
free(filename_);
}
TransientFile::~TransientFile() {
unlink(filename_.c_str());
}
namespace {
std::string TempNam(const char *dir, const char *prefix) {
char *p = tempnam(dir, prefix);
std::string result(p);
free(p);
return result;
}
} // namespace
TempFile::TempFile(const std::string& contents)
: TransientFile(TempNam(nullptr, "ares"), contents) {
}
} // namespace test

@ -12,6 +12,10 @@
#include "gtest/gtest.h"
#include "gmock/gmock.h"
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <functional>
#include <map>
#include <memory>
@ -272,16 +276,32 @@ void NameInfoCallback(void *data, int status, int timeouts,
// Retrieve the name servers used by a channel.
std::vector<std::string> GetNameServers(ares_channel channel);
// RAII class to temporarily create a directory of a given name.
class TransientDir {
public:
TransientDir(const std::string& dirname);
~TransientDir();
private:
std::string dirname_;
};
// RAII class to temporarily create file of a given name and contents.
class TransientFile {
public:
TransientFile(const std::string &filename, const std::string &contents);
~TransientFile();
protected:
std::string filename_;
};
// RAII class for a temporary file with the given contents.
class TempFile {
class TempFile : public TransientFile {
public:
TempFile(const std::string& contents);
~TempFile();
const char *filename() const {
return filename_;
}
private:
char *filename_;
const char* filename() const { return filename_.c_str(); }
};
#ifndef WIN32
@ -310,6 +330,16 @@ class EnvValue {
};
#endif
// Linux-specific functionality for running code in a container.
#if defined(HAVE_USER_NAMESPACE) && defined(HAVE_UTS_NAMESPACE)
#define HAVE_CONTAINER
typedef std::function<int(void)> VoidToIntFn;
int RunInContainer(const std::string& dirname, const std::string& hostname,
const std::string& domainname, VoidToIntFn fn);
#define CONTAINER_RUN(dir, host, domain, fn) \
EXPECT_EQ(0, RunInContainer(dir, host, domain, static_cast<VoidToIntFn>(fn)));
#endif
} // namespace test
} // namespace ares

@ -12,6 +12,8 @@ LT_INIT
AC_SUBST(LIBTOOL_DEPS)
AX_PTHREAD
AX_CODE_COVERAGE
AX_CHECK_USER_NAMESPACE
AX_CHECK_UTS_NAMESPACE
AC_CHECK_HEADERS(netdb.h netinet/tcp.h)
AC_CONFIG_HEADERS([config.h])

Loading…
Cancel
Save