From 5e2eafd7c43545740efb0c3304ff68bba1f4d0a6 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Tue, 11 Apr 2017 14:45:07 +0200 Subject: Implement TFTP server support in agent --- bbot/agent | 10 ++++ bbot/agent.cxx | 108 ++++++++++++++++++++++++++++++++++++----- bbot/buildfile | 1 + bbot/machine | 4 +- bbot/machine.cxx | 77 ++++++++++++++++++++++-------- bbot/tftp | 44 +++++++++++++++++ bbot/tftp.cxx | 127 +++++++++++++++++++++++++++++++++++++++++++++++++ bbot/utility | 12 ++++- tests/agent/testscript | 2 +- 9 files changed, 352 insertions(+), 33 deletions(-) create mode 100644 bbot/tftp create mode 100644 bbot/tftp.cxx diff --git a/bbot/agent b/bbot/agent index c333fe5..619fb36 100644 --- a/bbot/agent +++ b/bbot/agent @@ -5,6 +5,8 @@ #ifndef BBOT_AGENT #define BBOT_AGENT +#include // uid_t + #include #include @@ -19,6 +21,14 @@ namespace bbot extern string tc_name; // Toolchain name. extern string tc_num; // Toolchain number. extern string tc_id; // Toolchain id. + + extern uid_t uid; // Our effective user id. + extern string uname; // Our effective user name. + + // Return the IPv4 address of an interface. + // + string + iface_addr (const string&); } #endif // BBOT_AGENT diff --git a/bbot/agent.cxx b/bbot/agent.cxx index e608af2..2489ce4 100644 --- a/bbot/agent.cxx +++ b/bbot/agent.cxx @@ -4,9 +4,16 @@ #include +#include // getpwuid() #include // PATH_MAX #include // signal() -#include // sleep(), realink() +#include // sleep(), realink(), getuid() + +#include // ifreq +#include // sockaddr_in +#include // inet_ntop() +#include +#include #include @@ -19,23 +26,57 @@ #include #include +#include #include #include +using namespace std; +using namespace butl; +using namespace bbot; + namespace bbot { agent_options ops; - const string bs_prot ("1"); // Bootstrap protocol version. + const string bs_prot ("1"); - string tc_name; // Toolchain name. - string tc_num; // Toolchain number. - string tc_id; // Toolchain id. -} + string tc_name; + string tc_num; + string tc_id; -using namespace std; -using namespace butl; -using namespace bbot; + uid_t uid; + string uname; + + // Note: Linux-specific implementation. + // + string + iface_addr (const string& i) + { + if (i.size () >= IFNAMSIZ) + throw invalid_argument ("interface nama too long"); + + auto_fd fd (socket (AF_INET, SOCK_DGRAM, 0)); + + if (fd.get () == -1) + throw_system_error (errno); + + ifreq ifr; + ifr.ifr_addr.sa_family = AF_INET; + strcpy (ifr.ifr_name, i.c_str ()); + + if (ioctl (fd.get (), SIOCGIFADDR, &ifr) == -1) + throw_system_error (errno); + + char buf[3 * 4 + 3 + 1]; // IPv4 address. + if (inet_ntop (AF_INET, + &reinterpret_cast (&ifr.ifr_addr)->sin_addr, + buf, + sizeof (buf)) == nullptr) + throw_system_error (errno); + + return buf; + } +} // The btrfs tool likes to print informational messages, like "Created // snapshot such and such". Luckily, it writes them to stdout while proper @@ -65,6 +106,8 @@ bootstrap_machine (const dir_path& md, const machine_manifest& mm, optional obmm) { + tracer trace ("bootstrap_machine"); + bootstrapped_machine_manifest r { mm, toolchain_manifest {tc_id}, @@ -83,15 +126,51 @@ bootstrap_machine (const dir_path& md, r.machine.mac = "de:ad:be:ef:de:ad"; } else + try { + string br ("br1"); // Use private bridge for now. + + // Start the TFTP server (server chroot is /build/tftp). Map: + // + // GET requests to /build/tftp/toolchain//* + // PUT requests to /build/tftp/bootstrap//* + // + auto_rmdir arm (dir_path ("/build/tftp/bootstrap/" + tc_name)); + try_mkdir_p (arm.path ()); + + tftp_server tftpd ("Gr ^/?(.+)$ /toolchain/" + tc_name + "/\\1\n" + + "Pr ^/?(.+)$ /bootstrap/" + tc_name + "/\\1\n"); + + l2 ([&]{trace << "tftp server on port " << tftpd.port ();}); + + // Start the machine. + // unique_ptr m ( start_machine (md, mm, - obmm ? obmm->machine.mac : nullopt)); + obmm ? obmm->machine.mac : nullopt, + br, + tftpd.port ())); r.machine.mac = m->mac; - sleep (10); + // The first request should be the toolchain download. Wait for up to 60 + // seconds for that to arrive. In a sense we use it as an indication that + // the machine has booted and the bootstrap process has started. + // + size_t timeout (60); + if (tftpd.serve (timeout)) + { + l2 ([&]{trace << "received first request in " << 60 - timeout << "s";}); + } + else + { + // @@ What should be do here? Non-fatal? Mark the machine as failed? + // + error << "bootstrap timeout during first request for machine " << md; + m->forcedown (); + throw failed (); + } if (!m->shutdown ()) { @@ -100,6 +179,10 @@ bootstrap_machine (const dir_path& md, throw failed (); } } + catch (const system_error& e) + { + fail << "tftp server error: " << e; + } serialize_manifest (r, md / "manifest", "bootstrapped machine"); return r; @@ -389,6 +472,9 @@ try verb = ops.verbose (); + uid = getuid (); + uname = getpwuid (uid)->pw_name; + if (ops.systemd_daemon ()) { // Map to systemd severity prefixes (see sd-daemon(3) for details). Note diff --git a/bbot/buildfile b/bbot/buildfile index b43bc63..7e35a29 100644 --- a/bbot/buildfile +++ b/bbot/buildfile @@ -30,6 +30,7 @@ if ($cxx.target.class == "linux") {hxx cxx}{ diagnostics } \ {hxx cxx}{ machine-manifest } \ {hxx cxx}{ machine } \ + {hxx cxx}{ tftp } \ {hxx }{ types } \ {hxx cxx}{ types-parsers } \ {hxx txx cxx}{ utility } \ diff --git a/bbot/machine b/bbot/machine index 5ab8c15..ecdce22 100644 --- a/bbot/machine +++ b/bbot/machine @@ -44,7 +44,9 @@ namespace bbot unique_ptr start_machine (const dir_path&, const machine_manifest&, - const optional& mac); + const optional& mac, + const string& br_iface, + uint16_t tftp_port); } #endif // BBOT_MACHINE diff --git a/bbot/machine.cxx b/bbot/machine.cxx index e8f40b3..6677de5 100644 --- a/bbot/machine.cxx +++ b/bbot/machine.cxx @@ -4,12 +4,11 @@ #include +#include // sleep() + #include // sockaddr_un #include -#include // getuid() -#include // getuid() - #include // snprintf() #include // strcpy() @@ -21,32 +20,58 @@ using namespace butl; namespace bbot { + // Forward TFTP requests (UDP/69) coming from the machine to the specified + // port. + // + // This allows the machine to connect to any "unknown" IP (e.g., link-local + // 196.254.111.222) port 69 and end up being redirected to out TFTP server. + // + static void + iptables (tracer& t, + const char* a, + const string& tap, + const string& br, + uint16_t port) + { + run (t, + "sudo", "iptables", + "-t", "nat", + a, "PREROUTING", + "-p", "udp", + "-m", "udp", + "-m", "physdev", + "-i", br, + "--physdev-in", tap, + "--dport", 69, + "-j", "DNAT", + "--to-destination", iface_addr (br) + ':' + to_string (port)); + } + static string - create_tap () + create_tap (const string& br, uint16_t port) { tracer trace ("create_tap"); - string b ("br1"); // Use private bridge for now. string t ("tap" + tc_num); - auto uid (getuid ()); - // First try to delete it in case there is one from a previous run. // run_exit (trace, "sudo", "ip", "tuntap", "delete", t, "mode", "tap"); run (trace, "sudo", "ip", "tuntap", "add", t, "mode", "tap", "user", uid); run (trace, "sudo", "ip", "link", "set", t, "up"); - //sleep (1); - run (trace, "sudo", "ip", "link", "set", t, "master", b); + run (trace, "sudo", "ip", "link", "set", t, "master", br); + + iptables (trace, "-A", t, br, port); // Add. return t; } static void - destroy_tap (const string& t) + destroy_tap (const string& t, const string& br, uint16_t port) { - tracer trace ("create_tap"); + tracer trace ("destroy_tap"); + iptables (trace, "-D", t, br, port); // Delete. run (trace, "sudo", "ip", "tuntap", "delete", t, "mode", "tap"); } @@ -74,7 +99,9 @@ namespace bbot public: kvm_machine (const dir_path&, const machine_manifest&, - const optional& mac); + const optional& mac, + const string& br_iface, + uint16_t tftp_port); virtual bool shutdown () override; @@ -91,7 +118,11 @@ namespace bbot private: path kvm; // Hypervisor binary. - string tap; // Tap network interface. + + string br; // Bridge network interface. + string tap; // Tap network interface. + uint16_t port; // TFTP port. + path monitor; // QEMU monitor UNIX socket. process proc; }; @@ -99,12 +130,16 @@ namespace bbot kvm_machine:: kvm_machine (const dir_path& md, const machine_manifest& mm, - const optional& omac) + const optional& omac, + const string& br, + uint16_t port) : machine (mm.mac ? *mm.mac : // Fixed mac from machine manifest. omac ? *omac : // Generated mac from previous bootstrap. generate_mac ()), kvm ("kvm"), - tap (create_tap ()), + br (br), + tap (create_tap (br, port)), + port (port), monitor ("/tmp/" + tc_name + "-monitor") { tracer trace ("kvm_machine"); @@ -203,7 +238,7 @@ namespace bbot { run_io_finish (trace, proc, kvm); - destroy_tap (tap); + destroy_tap (tap, br, port); try_rmfile (monitor, true); // QEMU doesn't seem to remove it. } @@ -281,12 +316,16 @@ namespace bbot unique_ptr start_machine (const dir_path& md, const machine_manifest& mm, - const optional& mac) + const optional& mac, + const string& br_iface, + uint16_t tftp_port) { switch (mm.type) { - case machine_type::kvm: return make_unique (md, mm, mac); - case machine_type::nspawn: assert (false); //@@ TODO + case machine_type::kvm: + return make_unique (md, mm, mac, br_iface, tftp_port); + case machine_type::nspawn: + assert (false); //@@ TODO } return nullptr; diff --git a/bbot/tftp b/bbot/tftp new file mode 100644 index 0000000..cc39419 --- /dev/null +++ b/bbot/tftp @@ -0,0 +1,44 @@ +// file : bbot/tftp -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BBOT_TFTP +#define BBOT_TFTP + +#include +#include + +namespace bbot +{ + // A TFTP server "wrapper" over tftpd-hpa. + // + // In a nutshell, we are pretending to be inetd and when a request arrives, + // spawn tftpd-hpa to handle it. + // + class tftp_server + { + public: + // The map argument specifies the path mapping rules, one per line (see + // the tftpd-hpa --map-file|-m option for details). + // + tftp_server (const string& map); + + // Return the assigned port. + // + uint16_t + port () const; + + // Wait for a TFTP request for up to the specified number of seconds. If + // a request was served, update the timeout value and return true. Retain + // the original timeout value and return false otherwise. + // + bool + serve (size_t& seconds); + + private: + auto_fd fd_; + auto_rmfile map_; + }; +} + +#endif // BBOT_TFTP diff --git a/bbot/tftp.cxx b/bbot/tftp.cxx new file mode 100644 index 0000000..a7398be --- /dev/null +++ b/bbot/tftp.cxx @@ -0,0 +1,127 @@ +// file : bbot/tftp.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include // htonl() +#include // sockaddr_in +#include +#include + +#include // memset() + +#include + +using namespace std; +using namespace butl; + +namespace bbot +{ + tftp_server:: + tftp_server (const string& map) + { + int fd (socket (AF_INET, SOCK_DGRAM, 0)); + + if (fd == -1) + throw_system_error (errno); + + fd_.reset (fd); + + // Bind to ephemeral port. + // + sockaddr_in addr; + memset (&addr, 0, sizeof (addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl (INADDR_ANY); + addr.sin_port = htons (0); + + if (bind (fd, + reinterpret_cast (&addr), + sizeof (sockaddr_in)) == -1) + throw_system_error (errno); + + // Create the map file. + // + map_ = auto_rmfile (path::temp_path ("bbot-agent-tftp-map")); + ofdstream ofs (map_.path ()); + ofs << map << endl; + ofs.close (); + } + + uint16_t tftp_server:: + port () const + { + sockaddr_in addr; + socklen_t size (sizeof (addr)); + + if (getsockname (fd_.get (), + reinterpret_cast (&addr), + &size) == -1) + throw_system_error (errno); + + assert (size == sizeof (addr)); + return ntohs (addr.sin_port); + } + + bool tftp_server:: + serve (size_t& sec) + { + tracer trace ("tftp_server::serve"); + + int fd (fd_.get ()); + + // Note: Linux updates the timeout value which we rely upon. + // + timeval timeout {static_cast (sec), 0}; + + fd_set rd; + FD_ZERO (&rd); + + for (;;) + { + FD_SET (fd, &rd); + + int r (select (fd + 1, &rd, nullptr, nullptr, &timeout)); + + if (r == -1) + { + if (errno == EINTR) + continue; + + throw_system_error (errno); + } + else if (r == 0) // Timeout. + return false; + + if (FD_ISSET (fd, &rd)) + { + text << "connection"; + + // The inetd "protocol" is to pass the socket as stdin/stdout file + // descriptors. + // + // Notes/issues: + // + // 1. Writes diagnostics to syslog. + // + run_io (trace, + fddup (fd), + fddup (fd), + 2, + "sudo", // Required for --secure (chroot). + "/usr/sbin/in.tftpd", // Standard installation location. + "--timeout", 1, // Wait for more requests. + "--permissive", // Use inherited umask. + "--create", // Allow creating new files (PUT). + "--map-file", map_.path (), // Path remapping rules. + "--user", uname, // Run as our effective user. + "--secure", // Chroot to data directory. + "/build/tftp/"); + + sec = static_cast (timeout.tv_sec); + return true; + } + } + } +} diff --git a/bbot/utility b/bbot/utility index 5bd4821..4abb5a2 100644 --- a/bbot/utility +++ b/bbot/utility @@ -14,7 +14,8 @@ #include #include -#include // casecmp(), reverse_iterate(), etc +#include // casecmp(), reverse_iterate(), etc +#include #include #include @@ -40,6 +41,15 @@ namespace bbot using butl::exception_guard; using butl::make_exception_guard; + // + // + using butl::auto_fd; + + // + // + using butl::auto_rmdir; + using butl::auto_rmfile; + // Random number generator (currently not MT-safe and limited to RAND_MAX). // size_t diff --git a/tests/agent/testscript b/tests/agent/testscript index 55f9d7f..4671950 100644 --- a/tests/agent/testscript +++ b/tests/agent/testscript @@ -133,6 +133,6 @@ rm = $src_base/btrfs-rmdir /build/machines EOO EOE - -$rm + #-$rm } #\ -- cgit v1.1