/* $Id: HostDnsServiceLinux.cpp 98068 2023-01-13 04:14:20Z vboxsync $ */ /** @file * Linux specific DNS information fetching. */ /* * Copyright (C) 2013-2022 Oracle and/or its affiliates. * * This file is part of VirtualBox base platform packages, as * available from https://www.virtualbox.org. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, in version 3 of the * License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * SPDX-License-Identifier: GPL-3.0-only */ /********************************************************************************************************************************* * Header Files * *********************************************************************************************************************************/ #define LOG_GROUP LOG_GROUP_MAIN_HOST #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* Workaround for defining __flexarr to [] which beats us in * struct inotify_event (char name __flexarr). */ #include #undef __flexarr #define __flexarr [0] #include #include #include #include #include "../HostDnsService.h" /********************************************************************************************************************************* * Global Variables * *********************************************************************************************************************************/ static int g_DnsMonitorStop[2]; static const char g_szEtcFolder[] = "/etc"; static const char g_szResolvConfPath[] = "/etc/resolv.conf"; static const char g_szResolvConfFilename[] = "resolv.conf"; class FileDescriptor { public: FileDescriptor(int d = -1) : fd(d) {} virtual ~FileDescriptor() { if (fd != -1) close(fd); } int fileDescriptor() const {return fd;} protected: int fd; }; class AutoNotify : public FileDescriptor { public: AutoNotify() { FileDescriptor::fd = inotify_init(); AssertReturnVoid(FileDescriptor::fd != -1); } }; HostDnsServiceLinux::~HostDnsServiceLinux() { } HRESULT HostDnsServiceLinux::init(HostDnsMonitorProxy *pProxy) { return HostDnsServiceResolvConf::init(pProxy, "/etc/resolv.conf"); } int HostDnsServiceLinux::monitorThreadShutdown(RTMSINTERVAL uTimeoutMs) { RT_NOREF(uTimeoutMs); send(g_DnsMonitorStop[0], "", 1, 0); /** @todo r=andy Do we have to wait for something here? Can this fail? */ return VINF_SUCCESS; } #ifdef LOG_ENABLED /** * Format the notifcation event mask into a buffer for logging purposes. */ static const char *InotifyMaskToStr(char *psz, size_t cb, uint32_t fMask) { static struct { const char *pszName; uint32_t cchName, fFlag; } const s_aFlags[] = { # define ENTRY(fFlag) { #fFlag, sizeof(#fFlag) - 1, fFlag } ENTRY(IN_ACCESS), ENTRY(IN_MODIFY), ENTRY(IN_ATTRIB), ENTRY(IN_CLOSE_WRITE), ENTRY(IN_CLOSE_NOWRITE), ENTRY(IN_OPEN), ENTRY(IN_MOVED_FROM), ENTRY(IN_MOVED_TO), ENTRY(IN_CREATE), ENTRY(IN_DELETE), ENTRY(IN_DELETE_SELF), ENTRY(IN_MOVE_SELF), ENTRY(IN_Q_OVERFLOW), ENTRY(IN_IGNORED), ENTRY(IN_UNMOUNT), ENTRY(IN_ISDIR), }; size_t offDst = 0; for (size_t i = 0; i < RT_ELEMENTS(s_aFlags); i++) if (fMask & s_aFlags[i].fFlag) { if (offDst && offDst < cb) psz[offDst++] = ' '; if (offDst < cb) { size_t cbToCopy = RT_MIN(s_aFlags[i].cchName, cb - offDst); memcpy(&psz[offDst], s_aFlags[i].pszName, cbToCopy); offDst += cbToCopy; } fMask &= ~s_aFlags[i].fFlag; if (!fMask) break; } if (fMask && offDst < cb) RTStrPrintf(&psz[offDst], cb - offDst, offDst ? " %#x" : "%#x", fMask); else psz[RT_MIN(offDst, cb - 1)] = '\0'; return psz; } #endif /** * Helper for HostDnsServiceLinux::monitorThreadProc. */ static int monitorSymlinkedDir(int iInotifyFd, char szRealResolvConf[PATH_MAX], size_t *poffFilename) { RT_BZERO(szRealResolvConf, PATH_MAX); /* Check that it's a symlink first. */ struct stat st; if ( lstat(g_szResolvConfPath, &st) >= 0 && S_ISLNK(st.st_mode)) { /* If realpath fails, the file must've been deleted while we were busy: */ if ( realpath(g_szResolvConfPath, szRealResolvConf) && strchr(szRealResolvConf, '/')) { /* Cut of the filename part. We only need that for deletion checks and such. */ size_t const offFilename = strrchr(szRealResolvConf, '/') - &szRealResolvConf[0]; *poffFilename = offFilename + 1; szRealResolvConf[offFilename] = '\0'; /* Try set up directory monitoring. (File monitoring is done via the symlink.) */ return inotify_add_watch(iInotifyFd, szRealResolvConf, IN_MOVE | IN_CREATE | IN_DELETE); } } *poffFilename = 0; szRealResolvConf[0] = '\0'; return -1; } int HostDnsServiceLinux::monitorThreadProc(void) { /* * inotify initialization. * * The order here helps keep the descriptor values stable. * * Note! Ignoring failures here is safe, because poll will ignore entires * with negative fd values. */ AutoNotify Notify; /* Monitor the /etc directory so we can detect moves, unliking and creations involving /etc/resolv.conf: */ int const iWdDir = inotify_add_watch(Notify.fileDescriptor(), g_szEtcFolder, IN_MOVE | IN_CREATE | IN_DELETE); /* In case g_szResolvConfPath is a symbolic link, monitor the target directory too for changes to what it links to. */ char szRealResolvConf[PATH_MAX]; size_t offRealResolvConfName = 0; int iWdSymDir = ::monitorSymlinkedDir(Notify.fileDescriptor(), szRealResolvConf, &offRealResolvConfName); /* Monitor the resolv.conf itself if it exists, following all symlinks. */ int iWdFile = inotify_add_watch(Notify.fileDescriptor(), g_szResolvConfPath, IN_CLOSE_WRITE | IN_DELETE_SELF); Log5Func(("iWdDir=%d iWdSymDir=%d iWdFile=%d\n", iWdDir, iWdSymDir, iWdFile)); /* * Create a socket pair for signalling shutdown via (see monitorThreadShutdown). */ int rc = socketpair(AF_LOCAL, SOCK_DGRAM, 0, g_DnsMonitorStop); AssertMsgReturn(rc == 0, ("socketpair: failed (%d: %s)\n", errno, strerror(errno)), E_FAIL); /* automatic cleanup tricks */ FileDescriptor stopper0(g_DnsMonitorStop[0]); FileDescriptor stopper1(g_DnsMonitorStop[1]); /* * poll initialization: */ pollfd aFdPolls[2]; RT_ZERO(aFdPolls); aFdPolls[0].fd = Notify.fileDescriptor(); aFdPolls[0].events = POLLIN; aFdPolls[1].fd = g_DnsMonitorStop[1]; aFdPolls[1].events = POLLIN; onMonitorThreadInitDone(); /* * The monitoring loop. */ for (;;) { /* * Wait for something to happen. */ rc = poll(aFdPolls, RT_ELEMENTS(aFdPolls), -1 /*infinite timeout*/); if (rc == -1) { LogRelMax(32, ("HostDnsServiceLinux::monitorThreadProc: poll failed %d: errno=%d\n", rc, errno)); RTThreadSleep(1); continue; } Log5Func(("poll returns %d: [0]=%#x [1]=%#x\n", rc, aFdPolls[1].revents, aFdPolls[0].revents)); AssertMsgReturn( (aFdPolls[0].revents & (POLLERR | POLLNVAL)) == 0 && (aFdPolls[1].revents & (POLLERR | POLLNVAL)) == 0, ("Debug Me"), VERR_INTERNAL_ERROR); /* * Check for shutdown first. */ if (aFdPolls[1].revents & POLLIN) return VINF_SUCCESS; if (aFdPolls[0].revents & POLLIN) { /* * Read the notification event. */ #define INOTIFY_EVENT_SIZE (RT_UOFFSETOF(struct inotify_event, name)) union { uint8_t abBuf[(INOTIFY_EVENT_SIZE * 2 - 1 + NAME_MAX) / INOTIFY_EVENT_SIZE * INOTIFY_EVENT_SIZE * 4]; uint64_t uAlignTrick[2]; } uEvtBuf; ssize_t cbEvents = read(Notify.fileDescriptor(), &uEvtBuf, sizeof(uEvtBuf)); Log5Func(("read(inotify) -> %zd\n", cbEvents)); if (cbEvents > 0) Log5(("%.*Rhxd\n", cbEvents, &uEvtBuf)); /* * Process the events. * * We'll keep the old watch descriptor number till after we're done * parsing this block of events. Even so, the removal of watches * isn't race free, as they'll get automatically removed when what * is being watched is unliked. */ int iWdFileNew = iWdFile; int iWdSymDirNew = iWdSymDir; bool fTryReRead = false; struct inotify_event const *pCurEvt = (struct inotify_event const *)&uEvtBuf; while (cbEvents >= (ssize_t)INOTIFY_EVENT_SIZE) { #ifdef LOG_ENABLED char szTmp[64]; if (pCurEvt->len == 0) Log5Func(("event: wd=%#x mask=%#x (%s) cookie=%#x\n", pCurEvt->wd, pCurEvt->mask, InotifyMaskToStr(szTmp, sizeof(szTmp), pCurEvt->mask), pCurEvt->cookie)); else Log5Func(("event: wd=%#x mask=%#x (%s) cookie=%#x len=%#x '%s'\n", pCurEvt->wd, pCurEvt->mask, InotifyMaskToStr(szTmp, sizeof(szTmp), pCurEvt->mask), pCurEvt->cookie, pCurEvt->len, pCurEvt->name)); #endif /* * The file itself (symlinks followed, remember): */ if (pCurEvt->wd == iWdFile) { if (pCurEvt->mask & IN_CLOSE_WRITE) { Log5Func(("file: close-after-write => trigger re-read\n")); fTryReRead = true; } else if (pCurEvt->mask & IN_DELETE_SELF) { Log5Func(("file: deleted self\n")); if (iWdFileNew != -1) { rc = inotify_rm_watch(Notify.fileDescriptor(), iWdFileNew); AssertMsg(rc >= 0, ("%d/%d\n", rc, errno)); iWdFileNew = -1; } } else if (pCurEvt->mask & IN_IGNORED) iWdFileNew = -1; /* file deleted */ else AssertMsgFailed(("file: mask=%#x\n", pCurEvt->mask)); } /* * The /etc directory * * We only care about events relating to the creation, deletion and * renaming of 'resolv.conf'. We'll restablish both the direct file * watching and the watching of any symlinked directory on all of * these events, although for the former we'll delay the re-starting * of the watching till all events have been processed. */ else if (pCurEvt->wd == iWdDir) { if ( pCurEvt->len > 0 && strcmp(g_szResolvConfFilename, pCurEvt->name) == 0) { if (pCurEvt->mask & (IN_MOVE | IN_CREATE | IN_DELETE)) { if (iWdFileNew >= 0) { rc = inotify_rm_watch(Notify.fileDescriptor(), iWdFileNew); Log5Func(("dir: moved / created / deleted: dropped file watch (%d - rc=%d/err=%d)\n", iWdFileNew, rc, errno)); iWdFileNew = -1; } if (iWdSymDirNew >= 0) { rc = inotify_rm_watch(Notify.fileDescriptor(), iWdSymDirNew); Log5Func(("dir: moved / created / deleted: dropped symlinked dir watch (%d - %s/%s - rc=%d/err=%d)\n", iWdSymDirNew, szRealResolvConf, &szRealResolvConf[offRealResolvConfName], rc, errno)); iWdSymDirNew = -1; offRealResolvConfName = 0; } if (pCurEvt->mask & (IN_MOVED_TO | IN_CREATE)) { Log5Func(("dir: moved_to / created: trigger re-read\n")); fTryReRead = true; iWdSymDirNew = ::monitorSymlinkedDir(Notify.fileDescriptor(), szRealResolvConf, &offRealResolvConfName); if (iWdSymDirNew < 0) Log5Func(("dir: moved_to / created: re-stablished symlinked-directory monitoring: iWdSymDir=%d (%s/%s)\n", iWdSymDirNew, szRealResolvConf, &szRealResolvConf[offRealResolvConfName])); } } else AssertMsgFailed(("dir: %#x\n", pCurEvt->mask)); } } /* * The directory of a symlinked resolv.conf. * * Where we only care when the symlink target is created, moved_to, * deleted or moved_from - i.e. a minimal version of the /etc event * processing above. * * Note! Since we re-statablish monitoring above, szRealResolvConf * might not match the event we're processing. Fortunately, * this shouldn't be important except for debug logging. */ else if (pCurEvt->wd == iWdSymDir) { if ( pCurEvt->len > 0 && offRealResolvConfName > 0 && strcmp(&szRealResolvConf[offRealResolvConfName], pCurEvt->name) == 0) { if (iWdFileNew >= 0) { rc = inotify_rm_watch(Notify.fileDescriptor(), iWdFileNew); Log5Func(("symdir: moved / created / deleted: drop file watch (%d - rc=%d/err=%d)\n", iWdFileNew, rc, errno)); iWdFileNew = -1; } if (pCurEvt->mask & (IN_MOVED_TO | IN_CREATE)) { Log5Func(("symdir: moved_to / created: trigger re-read\n")); fTryReRead = true; } } } /* We can get here it seems if our inotify_rm_watch calls above takes place after new events relating to the two descriptors happens. */ else Log5Func(("Unknown (obsoleted) wd value: %d (mask=%#x cookie=%#x len=%#x)\n", pCurEvt->wd, pCurEvt->mask, pCurEvt->cookie, pCurEvt->len)); /* advance to the next event */ Assert(pCurEvt->len / INOTIFY_EVENT_SIZE * INOTIFY_EVENT_SIZE == pCurEvt->len); size_t const cbCurEvt = INOTIFY_EVENT_SIZE + pCurEvt->len; pCurEvt = (struct inotify_event const *)((uintptr_t)pCurEvt + cbCurEvt); cbEvents -= cbCurEvt; } /* * Commit the new watch descriptor numbers now that we're * done processing event using the old ones. */ iWdFile = iWdFileNew; iWdSymDir = iWdSymDirNew; /* * If the resolv.conf watch descriptor is -1, try restablish it here. */ if (iWdFile == -1) { iWdFile = inotify_add_watch(Notify.fileDescriptor(), g_szResolvConfPath, IN_CLOSE_WRITE | IN_DELETE_SELF); if (iWdFile >= 0) { Log5Func(("Re-established file watcher: iWdFile=%d\n", iWdFile)); fTryReRead = true; } } /* * If any of the events indicate that we should re-read the file, we * do so now. Should reduce number of unnecessary re-reads. */ if (fTryReRead) { Log5Func(("Calling readResolvConf()...\n")); readResolvConf(); } } } }