1 /* interface_frame.cpp
2  * Display of interfaces, including their respective data, and the
3  * capability to filter interfaces by type
4  *
5  * Wireshark - Network traffic analyzer
6  * By Gerald Combs <gerald@wireshark.org>
7  * Copyright 1998 Gerald Combs
8  *
9  * SPDX-License-Identifier: GPL-2.0-or-later
10  */
11 #include "config.h"
12 #include <ui_interface_frame.h>
13 
14 #include "capture/capture_ifinfo.h"
15 
16 #ifdef Q_OS_WIN
17 #include "capture/capture-wpcap.h"
18 #endif
19 
20 #include "ui/qt/interface_frame.h"
21 #include <ui/qt/simple_dialog.h>
22 #include <ui/qt/wireshark_application.h>
23 
24 #include <ui/qt/models/interface_tree_model.h>
25 #include <ui/qt/models/sparkline_delegate.h>
26 
27 #include <ui/qt/utils/color_utils.h>
28 
29 
30 #include "extcap.h"
31 
32 #include <ui/recent.h>
33 #include <wsutil/utf8_entities.h>
34 
35 #include <QDesktopServices>
36 #include <QFrame>
37 #include <QHBoxLayout>
38 #include <QItemSelection>
39 #include <QLabel>
40 #include <QPushButton>
41 #include <QUrl>
42 
43 #include <epan/prefs.h>
44 
45 #define BTN_IFTYPE_PROPERTY "ifType"
46 
47 #ifdef HAVE_LIBPCAP
48 const int stat_update_interval_ = 1000; // ms
49 #endif
50 const char *no_capture_link = "#no_capture";
51 
InterfaceFrame(QWidget * parent)52 InterfaceFrame::InterfaceFrame(QWidget * parent)
53 : QFrame(parent),
54   ui(new Ui::InterfaceFrame)
55   , proxy_model_(Q_NULLPTR)
56   , source_model_(Q_NULLPTR)
57   , info_model_(this)
58 #ifdef HAVE_LIBPCAP
59   ,stat_timer_(NULL)
60 #endif // HAVE_LIBPCAP
61 {
62     ui->setupUi(this);
63 
64     setStyleSheet(QString(
65                       "QFrame {"
66                       "  border: 0;"
67                       "}"
68                       "QTreeView {"
69                       "  border: 0;"
70                       "}"
71                       ));
72 
73     ui->warningLabel->hide();
74 
75 #ifdef Q_OS_MAC
76     ui->interfaceTree->setAttribute(Qt::WA_MacShowFocusRect, false);
77 #endif
78 
79     /* TODO: There must be a better way to do this */
80     ifTypeDescription.insert(IF_WIRED, tr("Wired"));
81     ifTypeDescription.insert(IF_AIRPCAP, tr("AirPCAP"));
82     ifTypeDescription.insert(IF_PIPE, tr("Pipe"));
83     ifTypeDescription.insert(IF_STDIN, tr("STDIN"));
84     ifTypeDescription.insert(IF_BLUETOOTH, tr("Bluetooth"));
85     ifTypeDescription.insert(IF_WIRELESS, tr("Wireless"));
86     ifTypeDescription.insert(IF_DIALUP, tr("Dial-Up"));
87     ifTypeDescription.insert(IF_USB, tr("USB"));
88     ifTypeDescription.insert(IF_EXTCAP, tr("External Capture"));
89     ifTypeDescription.insert(IF_VIRTUAL, tr ("Virtual"));
90 
91     QList<InterfaceTreeColumns> columns;
92     columns.append(IFTREE_COL_EXTCAP);
93     columns.append(IFTREE_COL_DISPLAY_NAME);
94     columns.append(IFTREE_COL_STATS);
95     proxy_model_.setColumns(columns);
96     proxy_model_.setStoreOnChange(true);
97     proxy_model_.setSourceModel(&source_model_);
98 
99     info_model_.setSourceModel(&proxy_model_);
100     info_model_.setColumn(columns.indexOf(IFTREE_COL_STATS));
101 
102     ui->interfaceTree->setModel(&info_model_);
103 
104     ui->interfaceTree->setItemDelegateForColumn(proxy_model_.mapSourceToColumn(IFTREE_COL_STATS), new SparkLineDelegate(this));
105 
106     ui->interfaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
107     connect(ui->interfaceTree, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showContextMenu(QPoint)));
108 
109     connect(wsApp, SIGNAL(appInitialized()), this, SLOT(interfaceListChanged()));
110     connect(wsApp, SIGNAL(localInterfaceListChanged()), this, SLOT(interfaceListChanged()));
111 
112     connect(ui->interfaceTree->selectionModel(), SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
113             this, SLOT(interfaceTreeSelectionChanged(const QItemSelection &, const QItemSelection &)));
114 }
115 
~InterfaceFrame()116 InterfaceFrame::~InterfaceFrame()
117 {
118     delete ui;
119 }
120 
getSelectionMenu()121 QMenu * InterfaceFrame::getSelectionMenu()
122 {
123     QMenu * contextMenu = new QMenu(this);
124     QList<int> typesDisplayed = proxy_model_.typesDisplayed();
125 
126     QMap<int, QString>::const_iterator it = ifTypeDescription.constBegin();
127     while (it != ifTypeDescription.constEnd())
128     {
129         int ifType = it.key();
130 
131         if (typesDisplayed.contains(ifType))
132         {
133             QAction *endp_action = new QAction(it.value(), this);
134             endp_action->setData(QVariant::fromValue(ifType));
135             endp_action->setCheckable(true);
136             endp_action->setChecked(proxy_model_.isInterfaceTypeShown(ifType));
137             connect(endp_action, SIGNAL(triggered()), this, SLOT(triggeredIfTypeButton()));
138             contextMenu->addAction(endp_action);
139         }
140         ++it;
141     }
142 
143 #ifdef HAVE_PCAP_REMOTE
144     if (proxy_model_.remoteInterfacesExist())
145     {
146         QAction * toggleRemoteAction = new QAction(tr("Remote interfaces"), this);
147         toggleRemoteAction->setCheckable(true);
148         toggleRemoteAction->setChecked(! proxy_model_.remoteDisplay());
149         connect(toggleRemoteAction, SIGNAL(triggered()), this, SLOT(toggleRemoteInterfaces()));
150         contextMenu->addAction(toggleRemoteAction);
151     }
152 #endif
153 
154 #if 0
155     // Disabled until bug 13354 is fixed
156     contextMenu->addSeparator();
157     QAction * toggleHideAction = new QAction(tr("Show hidden interfaces"), this);
158     toggleHideAction->setCheckable(true);
159     toggleHideAction->setChecked(! proxy_model_->filterHidden());
160     connect(toggleHideAction, SIGNAL(triggered()), this, SLOT(toggleHiddenInterfaces()));
161     contextMenu->addAction(toggleHideAction);
162 #endif
163 
164     return contextMenu;
165 }
166 
interfacesHidden()167 int InterfaceFrame::interfacesHidden()
168 {
169     return proxy_model_.interfacesHidden();
170 }
171 
interfacesPresent()172 int InterfaceFrame::interfacesPresent()
173 {
174     return source_model_.rowCount() - proxy_model_.interfacesHidden();
175 }
176 
ensureSelectedInterface()177 void InterfaceFrame::ensureSelectedInterface()
178 {
179 #ifdef HAVE_LIBPCAP
180     if (interfacesPresent() < 1) return;
181 
182     if (source_model_.selectedDevices().count() < 1) {
183         QModelIndex first_idx = info_model_.mapFromSource(proxy_model_.index(0, 0));
184         ui->interfaceTree->setCurrentIndex(first_idx);
185     }
186 
187     ui->interfaceTree->setFocus();
188 #endif
189 }
190 
hideEvent(QHideEvent *)191 void InterfaceFrame::hideEvent(QHideEvent *) {
192 #ifdef HAVE_LIBPCAP
193     if (stat_timer_)
194         stat_timer_->stop();
195     source_model_.stopStatistic();
196 #endif // HAVE_LIBPCAP
197 }
198 
showEvent(QShowEvent *)199 void InterfaceFrame::showEvent(QShowEvent *) {
200 
201 #ifdef HAVE_LIBPCAP
202     if (stat_timer_)
203         stat_timer_->start(stat_update_interval_);
204 #endif // HAVE_LIBPCAP
205 }
206 
actionButton_toggled(bool checked)207 void InterfaceFrame::actionButton_toggled(bool checked)
208 {
209     QVariant ifType = sender()->property(BTN_IFTYPE_PROPERTY);
210     if (ifType.isValid())
211     {
212         proxy_model_.setInterfaceTypeVisible(ifType.toInt(), checked);
213     }
214 
215     resetInterfaceTreeDisplay();
216 }
217 
triggeredIfTypeButton()218 void InterfaceFrame::triggeredIfTypeButton()
219 {
220     QAction *sender = qobject_cast<QAction *>(QObject::sender());
221     if (sender)
222     {
223         int ifType = sender->data().value<int>();
224         proxy_model_.toggleTypeVisibility(ifType);
225 
226         resetInterfaceTreeDisplay();
227         emit typeSelectionChanged();
228     }
229 }
230 
interfaceListChanged()231 void InterfaceFrame::interfaceListChanged()
232 {
233     info_model_.clearInfos();
234     if (prefs.capture_no_extcap)
235         info_model_.appendInfo(tr("External capture interfaces disabled."));
236 
237     resetInterfaceTreeDisplay();
238     // Ensure that device selection is consistent with the displayed selection.
239     updateSelectedInterfaces();
240 
241 #ifdef HAVE_LIBPCAP
242     if (!stat_timer_) {
243         updateStatistics();
244         stat_timer_ = new QTimer(this);
245         connect(stat_timer_, SIGNAL(timeout()), this, SLOT(updateStatistics()));
246         stat_timer_->start(stat_update_interval_);
247     }
248 #endif
249 }
250 
toggleHiddenInterfaces()251 void InterfaceFrame::toggleHiddenInterfaces()
252 {
253     proxy_model_.toggleFilterHidden();
254 
255     emit typeSelectionChanged();
256 }
257 
258 #ifdef HAVE_PCAP_REMOTE
toggleRemoteInterfaces()259 void InterfaceFrame::toggleRemoteInterfaces()
260 {
261     proxy_model_.toggleRemoteDisplay();
262     emit typeSelectionChanged();
263 }
264 #endif
265 
resetInterfaceTreeDisplay()266 void InterfaceFrame::resetInterfaceTreeDisplay()
267 {
268     ui->warningLabel->hide();
269     ui->warningLabel->clear();
270 
271     ui->warningLabel->setStyleSheet(QString(
272                 "QLabel {"
273                 "  border-radius: 0.5em;"
274                 "  padding: 0.33em;"
275                 "  margin-bottom: 0.25em;"
276                 // We might want to transition this to normal colors this after a timeout.
277                 "  background-color: %2;"
278                 "}"
279                 ).arg(ColorUtils::warningBackground().name()));
280 
281 #ifdef HAVE_LIBPCAP
282 #ifdef Q_OS_WIN
283     if (!has_wpcap) {
284         ui->warningLabel->setText(tr(
285             "<p>"
286             "Local interfaces are unavailable because no packet capture driver is installed."
287             "</p><p>"
288             "You can fix this by installing <a href=\"https://nmap.org/npcap/\">Npcap</a>"
289             " or <a href=\"https://www.winpcap.org/install/default.htm\">WinPcap</a>."
290             "</p>"));
291     } else if (!npf_sys_is_running()) {
292         ui->warningLabel->setText(tr(
293             "<p>"
294             "Local interfaces are unavailable because the packet capture driver isn't loaded."
295             "</p><p>"
296             "You can fix this by running <pre>net start npcap</pre> if you have Npcap installed"
297             " or <pre>net start npf</pre> if you have WinPcap installed."
298             " Both commands must be run as Administrator."
299             "</p>"));
300     }
301 #endif
302 
303     if (!haveLocalCapturePermissions())
304     {
305 #ifdef Q_OS_MAC
306         //
307         // NOTE: if you change this text, you must also change the
308         // definition of PLATFORM_PERMISSIONS_SUGGESTION that is
309         // used if __APPLE__ is defined, so that it reflects the
310         // new message text.
311         //
312         QString install_chmodbpf_path = wsApp->applicationDirPath() + "/../Resources/Extras/Install ChmodBPF.pkg";
313         ui->warningLabel->setText(tr(
314             "<p>"
315             "You don't have permission to capture on local interfaces."
316             "</p><p>"
317             "You can fix this by <a href=\"file://%1\">installing ChmodBPF</a>."
318             "</p>")
319             .arg(install_chmodbpf_path));
320 #else
321         //
322         // XXX - should this give similar platform-dependent recommendations,
323         // just as dumpcap gives platform-dependent recommendations in its
324         // PLATFORM_PERMISSIONS_SUGGESTION #define?
325         //
326         ui->warningLabel->setText(tr("You don't have permission to capture on local interfaces."));
327 #endif
328     }
329 
330     if (proxy_model_.rowCount() == 0)
331     {
332         ui->warningLabel->setText(tr("No interfaces found."));
333         ui->warningLabel->setText(proxy_model_.interfaceError());
334         if (prefs.capture_no_interface_load) {
335             ui->warningLabel->setText(tr("Interfaces not loaded (due to preference). Go to Capture " UTF8_RIGHTWARDS_ARROW " Refresh Interfaces to load."));
336         }
337     }
338 
339     // XXX Should we have a separate recent pref for each message?
340     if (!ui->warningLabel->text().isEmpty() && recent.sys_warn_if_no_capture)
341     {
342         QString warning_text = ui->warningLabel->text();
343         warning_text.append(QString("<p><a href=\"%1\">%2</a></p>")
344                             .arg(no_capture_link)
345                             .arg(SimpleDialog::dontShowThisAgain()));
346         ui->warningLabel->setText(warning_text);
347 
348         ui->warningLabel->show();
349     }
350 #endif // HAVE_LIBPCAP
351 
352     if (proxy_model_.rowCount() > 0)
353     {
354         ui->interfaceTree->show();
355         ui->interfaceTree->resizeColumnToContents(proxy_model_.mapSourceToColumn(IFTREE_COL_EXTCAP));
356         ui->interfaceTree->resizeColumnToContents(proxy_model_.mapSourceToColumn(IFTREE_COL_DISPLAY_NAME));
357         ui->interfaceTree->resizeColumnToContents(proxy_model_.mapSourceToColumn(IFTREE_COL_STATS));
358     }
359     else
360     {
361         ui->interfaceTree->hide();
362     }
363 }
364 
365 // XXX Should this be in capture/capture-pcap-util.[ch]?
haveLocalCapturePermissions() const366 bool InterfaceFrame::haveLocalCapturePermissions() const
367 {
368 #ifdef Q_OS_MAC
369     QFileInfo bpf0_fi = QFileInfo("/dev/bpf0");
370     return bpf0_fi.isReadable() && bpf0_fi.isWritable();
371 #else
372     // XXX Add checks for other platforms.
373     return true;
374 #endif
375 }
376 
updateSelectedInterfaces()377 void InterfaceFrame::updateSelectedInterfaces()
378 {
379     if (source_model_.rowCount() == 0)
380         return;
381 #ifdef HAVE_LIBPCAP
382     QItemSelection sourceSelection = source_model_.selectedDevices();
383     QItemSelection mySelection = info_model_.mapSelectionFromSource(proxy_model_.mapSelectionFromSource(sourceSelection));
384 
385     ui->interfaceTree->selectionModel()->clearSelection();
386     ui->interfaceTree->selectionModel()->select(mySelection, QItemSelectionModel::SelectCurrent);
387 #endif
388 }
389 
interfaceTreeSelectionChanged(const QItemSelection & selected,const QItemSelection & deselected)390 void InterfaceFrame::interfaceTreeSelectionChanged(const QItemSelection & selected, const QItemSelection & deselected)
391 {
392     if (selected.count() == 0 && deselected.count() == 0)
393         return;
394     if (source_model_.rowCount() == 0)
395         return;
396 
397 #ifdef HAVE_LIBPCAP
398     /* Take all selected interfaces, not just the newly ones */
399     QItemSelection allSelected = ui->interfaceTree->selectionModel()->selection();
400     QItemSelection sourceSelection = proxy_model_.mapSelectionToSource(info_model_.mapSelectionToSource(allSelected));
401 
402     if (source_model_.updateSelectedDevices(sourceSelection))
403         emit itemSelectionChanged();
404 #endif
405 }
406 
on_interfaceTree_doubleClicked(const QModelIndex & index)407 void InterfaceFrame::on_interfaceTree_doubleClicked(const QModelIndex &index)
408 {
409     QModelIndex realIndex = proxy_model_.mapToSource(info_model_.mapToSource(index));
410 
411     if (! realIndex.isValid())
412         return;
413 
414 #ifdef HAVE_LIBPCAP
415 
416     QString device_name = source_model_.getColumnContent(realIndex.row(), IFTREE_COL_NAME).toString();
417     QString extcap_string = source_model_.getColumnContent(realIndex.row(), IFTREE_COL_EXTCAP_PATH).toString();
418 
419     /* We trust the string here. If this interface is really extcap, the string is
420      * being checked immediatly before the dialog is being generated */
421     if (extcap_string.length() > 0)
422     {
423         /* this checks if configuration is required and not yet provided or saved via prefs */
424         if (extcap_has_configuration((const char *)(device_name.toStdString().c_str()), TRUE))
425         {
426             emit showExtcapOptions(device_name);
427             return;
428         }
429     }
430 #endif
431     emit startCapture();
432 }
433 
434 #ifdef HAVE_LIBPCAP
on_interfaceTree_clicked(const QModelIndex & index)435 void InterfaceFrame::on_interfaceTree_clicked(const QModelIndex &index)
436 {
437     if (index.column() == 0)
438     {
439         QModelIndex realIndex = proxy_model_.mapToSource(info_model_.mapToSource(index));
440 
441         if (! realIndex.isValid())
442             return;
443 
444         QString device_name = source_model_.getColumnContent(realIndex.row(), IFTREE_COL_NAME).toString();
445         QString extcap_string = source_model_.getColumnContent(realIndex.row(), IFTREE_COL_EXTCAP_PATH).toString();
446 
447         /* We trust the string here. If this interface is really extcap, the string is
448          * being checked immediatly before the dialog is being generated */
449         if (extcap_string.length() > 0)
450         {
451             /* this checks if configuration is required and not yet provided or saved via prefs */
452             if (extcap_has_configuration((const char *)(device_name.toStdString().c_str()), FALSE))
453             {
454                 emit showExtcapOptions(device_name);
455                 return;
456             }
457         }
458     }
459 }
460 #endif
461 
updateStatistics(void)462 void InterfaceFrame::updateStatistics(void)
463 {
464     if (source_model_.rowCount() == 0)
465         return;
466 
467 #ifdef HAVE_LIBPCAP
468 
469     for (int idx = 0; idx < proxy_model_.rowCount(); idx++)
470     {
471         QModelIndex selectIndex = info_model_.mapFromSource(proxy_model_.mapFromSource(source_model_.index(idx, 0)));
472 
473         /* Proxy model has not masked out the interface */
474         if (selectIndex.isValid())
475             source_model_.updateStatistic(idx);
476     }
477 
478 #endif
479 }
480 
481 /* Proxy Method so we do not need to expose the source model */
getPoints(int idx,PointList * pts)482 void InterfaceFrame::getPoints(int idx, PointList * pts)
483 {
484     source_model_.getPoints(idx, pts);
485 }
486 
showRunOnFile(void)487 void InterfaceFrame::showRunOnFile(void)
488 {
489     ui->warningLabel->setText("Interfaces not loaded on startup (run on capture file). Go to Capture -> Refresh Interfaces to load.");
490 }
491 
showContextMenu(QPoint pos)492 void InterfaceFrame::showContextMenu(QPoint pos)
493 {
494     QMenu ctx_menu;
495 
496     ctx_menu.addAction(tr("Start capture"), this, SIGNAL(startCapture()));
497     ctx_menu.exec(ui->interfaceTree->mapToGlobal(pos));
498 }
499 
on_warningLabel_linkActivated(const QString & link)500 void InterfaceFrame::on_warningLabel_linkActivated(const QString &link)
501 {
502     if (link.compare(no_capture_link) == 0) {
503         recent.sys_warn_if_no_capture = FALSE;
504         resetInterfaceTreeDisplay();
505     } else {
506         QDesktopServices::openUrl(QUrl(link));
507     }
508 }
509