mirror of
https://frontier.innolan.net/rainlance/c-ares.git
synced 2025-11-24 02:49:59 +00:00
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.
This commit is contained in:
54
m4/ax_check_user_namespace.m4
Normal file
54
m4/ax_check_user_namespace.m4
Normal file
@ -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
|
||||
76
m4/ax_check_uts_namespace.m4
Normal file
76
m4/ax_check_uts_namespace.m4
Normal file
@ -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
|
||||
|
||||
141
test/ares-test-ns.cc
Normal file
141
test/ares-test-ns.cc
Normal file
@ -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])
|
||||
|
||||
Reference in New Issue
Block a user