diff --git a/src/usr/local/www/diag_packet_capture.php b/src/usr/local/www/diag_packet_capture.php index 6e974a62de..18a92df24f 100755 --- a/src/usr/local/www/diag_packet_capture.php +++ b/src/usr/local/www/diag_packet_capture.php @@ -28,584 +28,1355 @@ ##|*MATCH=diag_packet_capture.php* ##|-PRIV -$allowautocomplete = true; +require_once('util.inc'); +require_once('interfaces_fast.inc'); +require_once('guiconfig.inc'); +require_once('pfsense-utils.inc'); + +/* Packet capture filter section */ +define('PCAP_FSECTION_UNTAGGED',0); +define('PCAP_FSECTION_SINGLETAGGED',1); +define('PCAP_FSECTION_DOUBLETAGGED',2); + +/* Packet capture attribute match options for filter sections and attributes; also used in js code */ +define('PCAP_AMATCH_NONE',10); +define('PCAP_AMATCH_NONEOF',11); +define('PCAP_AMATCH_AND_ANYOF',12); +define('PCAP_AMATCH_OR_ANYOF',13); +// simple filter match options +define('PCAP_FMATCH_ANY',18); +define('PCAP_FMATCH_UNTAGGED',19); +define('PCAP_FMATCH_TAGGED',20); +define('PCAP_FMATCH_CUSTOM',21); + +/* Packet capture filter attribute types */ +define('PCAP_ATYPE_MATCH',50); +define('PCAP_ATYPE_VLAN',51); +define('PCAP_ATYPE_ETHERTYPE',52); +define('PCAP_ATYPE_PROTOCOL',53); +define('PCAP_ATYPE_IPADDRESS',54); +define('PCAP_ATYPE_MACADDRESS',55); +define('PCAP_ATYPE_PORT',56); +define('PCAP_ATYPE_SIMPLE',57); + +/** + * Represents a tcpdump filter attribute type; used in the generation of a pcap expression string. + */ +class FilterAttribute { + public int $type = 0; + private int $section = 0; + private int $operator = 0; + private string $values = ''; + private bool $exclude = false; + private bool $required = false; + + /** + * @param int $section Which filter section this attribute belongs to; untagged, single-tagged, or double-tagged + * @param int $operator The match operator to use when combining multiple values from the given attribute input + * @param int $type The input attribute type + * + * @throws Exception If it's an invalid type and operator combination + * @return FilterAttribute Filter attribute object + */ + public function __construct(int $filter_section, int $attribute_operator, int $attribute_type) { + /* Validate and set attribute type */ + if (in_array($attribute_type, array(PCAP_ATYPE_MATCH, PCAP_ATYPE_VLAN, PCAP_ATYPE_ETHERTYPE, PCAP_ATYPE_PROTOCOL, PCAP_ATYPE_IPADDRESS, PCAP_ATYPE_MACADDRESS, PCAP_ATYPE_PORT, PCAP_ATYPE_SIMPLE))) { + $this->type = $attribute_type; + } else { + throw new Exception("Invalid attribute type {$filter_section}:{$attribute_operator}:{$attribute_type}"); + } -function fixup_logic($value) { - return str_replace(array(" ", ",", "+", "|", "!"), array("", "and ", "and ", "or ", "not "), $value); -} + /* Validate and set attribute's filter section */ + if (in_array($filter_section, array(PCAP_FSECTION_UNTAGGED, PCAP_FSECTION_SINGLETAGGED, PCAP_FSECTION_DOUBLETAGGED))) { + $this->section = $filter_section; + } else { + throw new Exception("Invalid filter section {$filter_section}:{$attribute_operator}:{$attribute_type}"); + } -function strip_logic($value) { - return str_replace(array(" ", ",", "+", "|", "!"), array("", "", "", "", ""), $value); -} + /* Validate and set the match operator */ + $attribute_operator = strtolower($attribute_operator); + if ($attribute_type == PCAP_ATYPE_SIMPLE) { + if ($filter_section == PCAP_FSECTION_UNTAGGED && !in_array($attribute_operator, array(PCAP_FMATCH_UNTAGGED, PCAP_FMATCH_TAGGED, PCAP_FMATCH_ANY, PCAP_FMATCH_CUSTOM))) { + throw new Exception("Invalid match operator for simple expression {$filter_section}:{$attribute_operator}:{$attribute_type}"); + } + } elseif ($attribute_type == PCAP_ATYPE_MATCH) { + if (in_array($attribute_operator, array(PCAP_AMATCH_OR_ANYOF, PCAP_AMATCH_NONE, PCAP_AMATCH_AND_ANYOF, PCAP_AMATCH_NONEOF))) { + if ($attribute_operator == PCAP_AMATCH_NONEOF) { + $this->exclude = true; + } elseif ($filter_section == PCAP_FSECTION_UNTAGGED && !in_array($attribute_operator, (array(PCAP_AMATCH_NONE, PCAP_AMATCH_OR_ANYOF)))) { + throw new Exception("Invalid untagged match operator and offset combination {$filter_section}:{$attribute_operator}:{$attribute_type}"); + } + } else { + throw new Exception("Invalid match operator for filter {$filter_section}:{$attribute_operator}:{$attribute_type}"); + } + } else { + switch ($attribute_operator) { + case PCAP_AMATCH_NONEOF: + // Exclude this attribute's input from capture + $this->required = true; + $this->exclude = true; + break; + case PCAP_AMATCH_AND_ANYOF: + // Explicitly include this attribute's input in capture + $this->required = true; + break; + case PCAP_AMATCH_OR_ANYOF: + // Optionally include this attribute's input in capture + $this->required = false; + break; + case PCAP_AMATCH_NONE: + // Exclude everything for this section + $this->exclude = false; + break; + default: + throw new Exception("Invalid match operator {$filter_section}:{$attribute_operator}:{$attribute_type}"); + break; + } + } + $this->operator = $attribute_operator; + } -function get_boolean($value, $entry) { - $value = str_replace(array("!", $entry), array("", ""), $value); - $andor = ""; - switch (trim($value)) { - case "|": - $andor = "or "; - break; - case ",": - case "+": - $andor = "and "; - break; + public function setInput(string $values) { + $this->values = $values; } - return $andor; -} + public function getMatchOffset() { + return $this->section; + } -function has_not($value) { - return strpos($value, '!') !== false; -} + public function getMatch() { + return $this->operator; + } -function fixup_not($value) { - return str_replace("!", "not ", $value); -} + public function getInput() { + return $this->values; + } -function strip_not($value) { - return ltrim(trim($value), '!'); -} + public function getIsExcluded() { + return $this->exclude; + } -function fixup_hostport($value, $position, $type) { - $item = strip_logic($value); - $not = has_not($value) ? "not " : ""; - $andor = ($position > 0) ? get_boolean($value, $item) : ""; - if ($type == 'port') { - return "{$andor}port {$not}" . $item; - } elseif (is_ipaddr($item)) { - return "{$andor}host {$not}" . $item; - } elseif (is_subnet($item)) { - return "{$andor}net {$not}" . $item; - } elseif (is_macaddr($item, false)) { - return "{$andor}ether host {$not}" . $item; - } elseif (is_macaddr($item, true)) { - /* Try to match a partial MAC address. tcpdump only allows - * matching 1, 2, or 4 byte chunks so enforce that limit - */ - $searchmac = "0x"; - $partcount = 0; - /* is_macaddr will fail a partial match that has empty sections - * but sections may only have one digit (leading 0) so add a - * left 0 pad. - */ - foreach (explode(':', $item) as $mp) { - $searchmac .= str_pad($mp, 2, "0", STR_PAD_LEFT); - $partcount++; - } - if (!in_array($partcount, array(1, 2, 4))) { - return ""; - } - $eq = has_not($value) ? "!=" : "=="; - // ether[0:2] == 0x0090 or ether[6:2] == 0x0090 - return "{$andor} ( ether[0:{$partcount}] {$eq} {$searchmac} or ether[6:{$partcount}] {$eq} {$searchmac} )"; - } else { - return ""; + public function getIsRequired() { + return $this->required; } } -function precheck_hostport($item_array) { - $item_string = str_replace(array(" ", "|", ","), array("", "#|", "#+"), $item_array); +/** + * Constructs tcpdump filter string based on a specific filter attribute type. + * + * @param FilterAttribute $fa Filter attribute object + * + * @throws Exception If there's invalid input + * @return string Attribute filter string + */ +function get_filter_attribute_string(FilterAttribute $fa) { + if ($fa->getMatch() == PCAP_AMATCH_NONE) { + throw new Exception("Cannot generate a filter attribute string when the match operator is set to none."); + } + + /* Return an empty string if the input is empty; '0' is a valid attribute input string */ + if (empty($fa->getInput()) && $fa->getInput() != '0') { + return ''; + } - if (strpos($item_string, '#') === false) { - $items = array($item_array); + /* Set the attribute value's match operator */ + $fa_string = ''; + if ($fa->getIsExcluded()) { + $prefix = 'not '; + $infix = ' and not '; } else { - $items = explode('#', $item_string); + $prefix = ''; + $infix = ' or '; } - return $items; -} -function hostport_array_fixer($items,$type) { - $itemmatch = ""; - $itemcount = 0; + /* Loop through this attribute's space-separated input string */ + $items = array(); + $value = ''; + $input_error = 'Invalid input.'; + switch ($fa->type) { + case PCAP_ATYPE_VLAN: + // Construct string parts for the VLAN tag attribute + if ($fa->getMatchOffset() == PCAP_FSECTION_SINGLETAGGED) { + $vlan_offset = 'ether[14:2]'; + } else { + $vlan_offset = 'ether[18:2]'; + } + foreach (preg_split('/\s+/', $fa->getInput(), -1, PREG_SPLIT_NO_EMPTY) as $input) { + // Validate input values + $vlan_tag = intval($input, 10); + if ($vlan_tag >= 0 && $vlan_tag <= 4095) { + $value = sprintf('%1$s==%2$s', $vlan_offset, $vlan_tag); + } else { + $input_error = sprintf('Invalid VLAN tag %s', $input); + unset($items); + break; + } + if (!empty($value)) { + $items[] = $value; + } + } + break; + case PCAP_ATYPE_ETHERTYPE: + // Construct string parts for the ethertype attribute + foreach (preg_split('/\s+/', $fa->getInput(), -1, PREG_SPLIT_NO_EMPTY) as $input) { + // Validate input values + switch ($input) { + case 'ipv4': + $value = 'ip'; + break; + case 'ipv6'; + $value = 'ip6'; + break; + case 'arp'; + $value = 'arp'; + break; + default: + if (preg_match('/^[0-9a-f]{4}$/i', $input)) { + $value = sprintf('0x%s', strtolower($input)); + } elseif (preg_match('/^0[x][0-9a-f]{4}$/i', $input)) { + $value = strtolower($input); + } else { + $input_error = sprintf('Invalid ethertype %s', $input); + unset($items); + break 2; + } + + if ($value != '0x8100' || $value != '0x88a8') { + $value = sprintf('ether proto %s', $value); + } else { + $input_error = sprintf('Matching for VLAN must be done with filter match selection %s', $input); + unset($items); + break 2; + } + break; + } + if (!empty($value)) { + $items[] = $value; + } + } + break; + case PCAP_ATYPE_PROTOCOL: + // Construct string parts for the protocol attribute + foreach (preg_split('/\s+/', $fa->getInput(), -1, PREG_SPLIT_NO_EMPTY) as $input) { + // Validate input values + switch ($input) { + case 'icmp': + $value = 'icmp'; + break; + case 'icmp6'; + $value = 'icmp6'; + break; + case 'tcp'; + $value = 'tcp'; + break; + case 'udp'; + $value = 'udp'; + break; + case 'ipsec'; + $value = '(esp or (udp port 4500 and udp[8:4]!=0))'; + break; + case 'carp'; + $value = 'proto 112'; + break; + case 'pfsync'; + $value = 'proto pfsync'; + break; + case 'ospf'; + $value = 'proto ospf'; + break; + default: + if (preg_match('/\d{1,3}/', $input) && ($input >= 0 && $input <= 255)) { + $value = sprintf('proto %s', $input); + } else { + $input_error = sprintf('Invalid protocol %s', $input); + unset($items); + break 2; + } + break; + } + if (!empty($value)) { + $items[] = $value; + } + } + break; + case PCAP_ATYPE_IPADDRESS: + // Construct string parts for the host IP address/subnet attribute + foreach (preg_split('/\s+/', $fa->getInput(), -1, PREG_SPLIT_NO_EMPTY) as $input) { + if (is_ipaddr($input)) { + // Validate IPv4/IPv6 address input + $value = sprintf('host %s', $input); + } elseif (is_subnet($input)) { + // Validate IPv4/IPv6 subnet input + $subnet_parts = explode('/', $input); + $subnet_cidr = intval($subnet_parts[array_key_last($subnet_parts)], 10); + if ($subnet_cidr >= 0 && $subnet_cidr <= 128) { + $subnet_address = gen_subnet($subnet_parts[array_key_first($subnet_parts)], $subnet_cidr); + $value = sprintf('net %1$s/%2$s', $subnet_address, $subnet_cidr); + } else { + $input_error = sprintf('Invalid subnet %s', $input); + unset($items); + break; + } + } else { + $input_error = sprintf('Invalid IP address or subnet %s', $input); + unset($items); + break; + } + if (!empty($value)) { + $items[] = $value; + } + } + break; + case PCAP_ATYPE_MACADDRESS: + // Construct string parts for the MAC address attribute + foreach (preg_split('/\s+/', $fa->getInput(), -1, PREG_SPLIT_NO_EMPTY) as $input) { + $mac_parts = array(); + $mac_parts_count = 0; + // Pad MAC parts with 0 if needed to construct a valid partial match string + foreach (explode(':', $input) as $macpart) { + $mac_parts[] = str_pad($macpart, 2, '0', STR_PAD_LEFT); + } - foreach ($items as $i) { - if ($type == 'port') { - $i = fixup_hostport($i, $itemcount++,'port'); - } else { - $i = fixup_hostport($i, $itemcount++,'host'); - } + $mac_parts_count = count($mac_parts); + if (in_array($mac_parts_count, array(1, 2, 4))) { + // Validate partial MAC address. tcpdump will only accept 1, 2, or 4 byte segments + $mac_string = implode($mac_parts); + if (is_macaddr($input, true)) { + $value = sprintf('ether[0:%1$s]==0x%2$s%3$sether[6:%1$s]==0x%2$s', $mac_parts_count, $mac_string, $infix); + } else { + $input_error = sprintf('Invalid partial MAC address %s', $input); + unset($items); + break; + } + } elseif ($mac_parts_count == 6) { + // Validate full MAC address + $mac_string = implode(':', $mac_parts); + if (is_macaddr($input, false)) { + $value = sprintf('ether host %s', $mac_string); + } else { + $input_error = sprintf('Invalid MAC address %s', $input); + unset($items); + break; + } + } else { + $input_error = sprintf('Invalid MAC address length; can only match 1, 2, 4, or 6 segments %s', $input); + unset($items); + break; + } + if (!empty($value)) { + $items[] = $value; + } + } + break; + case PCAP_ATYPE_PORT: + // Construct string parts for the port attribute + foreach (preg_split('/\s+/', $fa->getInput(), -1, PREG_SPLIT_NO_EMPTY) as $input) { + if (is_port($input)) { + // Validate input values + $value = sprintf('port %s', $input); + } else { + $input_error = sprintf('Invalid port %s', $input); + unset($items); + break; + } + // Save the attribute filter string + if (!empty($value)) { + $items[] = $value; + } + } + break; + default: + break; + } - if (!empty($i)) { - $itemmatch .= " " . $i; + if (empty($items)) { + throw new Exception("{$input_error}"); + } else { + /* Construct the filter string for this attribute */ + foreach ($items as $key => $value) { + if ($key == array_key_first($items)) { + $fa_string = sprintf('%1$s%2$s', $prefix, $value); + } else { + $fa_string .= sprintf('%1$s%2$s', $infix, $value); + } } - } - if (!empty($itemmatch)) { - return "({$itemmatch})"; + /* Return the attribute filter string */ + return $fa_string; } } -if ($_POST['downloadbtn'] == gettext("Download Capture")) { - $nocsrf = true; -} - -$pgtitle = array(gettext("Diagnostics"), gettext("Packet Capture")); -require_once("guiconfig.inc"); -require_once("pfsense-utils.inc"); -require_once("ipsec.inc"); - -$fp = "/root/"; -$fn = "packetcapture.cap"; -$fns = "packetcapture.start"; -$snaplen = 0;//default packet length -$count = 100;//default number of packets to capture -$max_display_size = 50*1024*1024; // 50MB limit on GUI capture display. See https://redmine.pfsense.org/issues/9239 - -$fams = array('ip', 'ip6'); -$protos = array('icmp', 'icmp6', 'tcp', 'udp', 'arp', 'carp', 'esp', 'pfsync', 'ospf', - '!icmp', '!icmp6', '!tcp', '!udp', '!arp', '!carp', '!esp', '!pfsync', '!ospf'); +/** + * Constructs a tcpdump expression string based on untagged, outer-tag, and inner-tag (aka section) filters. + * + * @param array $filterattributes array of FilterAttribute objects + * + * @throws Exception Thrown by get_filter_attribute_string() + * @return string Expression string; can be empty + */ +function get_filter_expression_string(array $filterattributes) { + $expression_string = ''; + $fs_simplefilter = ''; + $fs_attribute = ''; + // The index of the following arrays relate to the filter section + $fs_section_include = array(true, true, true); + $fs_section_start = array(true, true, true); + $fs_section_strings = array('', '', ''); + + /* Generate each section's filter string */ + foreach ($filterattributes as $fa) { + if ($fa->type == PCAP_ATYPE_SIMPLE && $fa->getMatch() != PCAP_FMATCH_CUSTOM) { + // An expression string are not needed when using a simple filter, hence skip everything + $fs_simplefilter = $fa->getMatch(); + break; + } + if ($fa->type == PCAP_ATYPE_MATCH && $fa->getMatch() == PCAP_AMATCH_NONE) { + // Attribute filter strings are not needed when the respective session is exlcuded + $fs_section_include[$fa->getMatchOffset()] = false; + continue; + } elseif (empty($fa->getInput())) { + // Attribute filter strings are not needed when the attribute's input is empty + continue; + } -$input_errors = array(); + try { + // Generate this attribute's filter string + $fs_attribute = get_filter_attribute_string($fa); + } catch (Exception $e) { + throw new Exception($e->getMessage()); + } -$interfaces = get_configured_interface_with_descr(); -if (ipsec_enabled()) { - $interfaces['enc0'] = "IPsec"; -} -$interfaces['lo0'] = "Localhost"; + if ($fa->getIsRequired() || $fa->type == PCAP_ATYPE_VLAN) { + $fs_attributes_operator = 'and'; + } else { + $fs_attributes_operator = 'or'; + } -foreach (array('server' => gettext('OpenVPN Server'), 'client' => gettext('OpenVPN Client')) as $mode => $mode_descr) { - if (is_array($config['openvpn']["openvpn-{$mode}"])) { - foreach ($config['openvpn']["openvpn-{$mode}"] as $id => $setting) { - if (!isset($setting['disable'])) { - $interfaces['ovpn' . substr($mode, 0, 1) . $setting['vpnid']] = $mode_descr . ": ".htmlspecialchars($setting['description']); - } + if ($fs_section_start[$fa->getMatchOffset()]) { + // Avoid adding an attribute operator at the beginning of the attribute's filter string + $fs_section_start[$fa->getMatchOffset()] = false; + $fs_section_strings[$fa->getMatchOffset()] = sprintf('(%s)', $fs_attribute); + } else { + $fs_section_strings[$fa->getMatchOffset()] .= sprintf(' %1$s (%2$s)', $fs_attributes_operator, $fs_attribute); } } -} -$interfaces = array_merge($interfaces, interface_ipsec_vti_list_all()); + /* Use a simple expression string if possible */ + if ($fs_simplefilter == PCAP_FMATCH_ANY) { + $expression_string = ''; + } elseif ($fs_simplefilter == PCAP_FMATCH_UNTAGGED) { + $expression_string = 'not vlan'; + } elseif ($fs_simplefilter == PCAP_FMATCH_TAGGED) { + $expression_string = 'vlan'; + } else { + // Combine all section filter strings + if (!$fs_section_include[PCAP_FSECTION_UNTAGGED]) { + $s0 = ''; + } elseif (!empty($fs_section_strings[PCAP_FSECTION_UNTAGGED])) { + $s0 = sprintf('(%s)', $fs_section_strings[PCAP_FSECTION_UNTAGGED]); + } else { + $s0 = '(not ether proto 0x8100 and not ether proto 0x88a8)'; + } -if ($_POST) { - $host = $_POST['host']; - $selectedif = $_POST['interface']; - $promiscuous = isset($_POST['promiscuous']); - $count = $_POST['count']; - $snaplen = $_POST['snaplen']; - $port = $_POST['port']; - $detail = $_POST['detail']; - $fam = $_POST['fam']; - $proto = $_POST['proto']; - - if (!array_key_exists($selectedif, $interfaces)) { - $input_errors[] = gettext("Invalid interface."); - } + if (!$fs_section_include[PCAP_FSECTION_SINGLETAGGED]) { + $s1 = 'not vlan'; + } elseif (!empty($fs_section_strings[PCAP_FSECTION_SINGLETAGGED])) { + $s1 = sprintf('(vlan and %s)', $fs_section_strings[PCAP_FSECTION_SINGLETAGGED]); + } else { + $s1 = '(vlan)'; + } - if ($fam !== "" && $fam !== "ip" && $fam !== "ip6") { - $input_errors[] = gettext("Invalid address family."); - } + if (!$fs_section_include[PCAP_FSECTION_DOUBLETAGGED]) { + $s2 = 'not vlan'; + } elseif (!empty($fs_section_strings[PCAP_FSECTION_DOUBLETAGGED])) { + $s2 = sprintf('(vlan and %s)', $fs_section_strings[PCAP_FSECTION_DOUBLETAGGED]); + } else { + $s2 = '(vlan)'; + } - if ($fam !== "" && $proto !== "") { - if ($fam == "ip" && $proto == "icmp6") { - $input_errors[] = gettext("IPv4 with ICMPv6 is not valid."); + if (empty($s0)) { + $o1 = ''; + } elseif ($s1 == 'not vlan') { + $o1 = ' and '; + } else { + $o1 = ' or '; } - if ($fam == "ip6" && $proto == "icmp") { - $input_errors[] = gettext("IPv6 with ICMP is not valid."); + + if ($s1 == 'not vlan') { + $o2 = ''; + } elseif ($s2 == 'not vlan') { + $o2 = ' and '; + } else { + $o2 = ' or '; } - if ($proto =="arp") { - $input_errors[] = gettext("Selecting an Address Family for ARP is not valid."); + + if (empty($o1) && $s1 == 'not vlan' || ($s0 == '(not ether proto 0x8100 and not ether proto 0x88a8)' && $s1 == '(vlan)' && $o2 == ' or ' && $s2 == '(vlan)')) { + // Invalid selection, or custom filter is the same as "Everything" filter + $expression_string = ''; + } elseif (empty($o2) || ($s1 == $s2 && $o2 == ' or ')) { + $expression_string = sprintf('%1$s%2$s%3$s', $s0, $o1, $s1); + } else { + $expression_string = sprintf('%1$s%2$s%3$s%4$s%5$s', $s0, $o1, $s1, $o2, $s2); } } - if ($proto !== "" && !in_array(strip_not($proto), $protos)) { - $input_errors[] = gettext("Invalid protocol."); - } + return $expression_string; +} - if ($host != "") { - $hosts = precheck_hostport($host); - foreach ($hosts as $h) { - $h = strip_logic($h); - if (!is_subnet($h) && !is_ipaddr($h) && !is_macaddr($h, true)) { - $input_errors[] = sprintf(gettext("A valid IP address, CIDR block, or MAC address must be specified. [%s]"), $h); - } - /* Check length of partial MAC */ - if (!is_macaddr($h, false) && is_macaddr($h, true)) { - $mac_parts = explode(':', $h); - if (!in_array(count($mac_parts), array(1, 2, 4))) { - $input_errors[] = gettext("Partial MAC addresses can only be matched using 1, 2, or 4 MAC segments (bytes)."); - } +/** + * Constructs an ordered list of all (including unassigned) interfaces and their descriptions. + * + * @return array Interface List ordered by config name and keyed by port name with description as the value + */ +function get_interfaces_sorted() { + // Get all interfaces and their descriptions + $i_ports = get_interface_arr(); + $i_names = convert_real_interface_to_friendly_interface_name_fast(); + $i_descriptions = get_configured_interface_with_descr(); + + /* Group interfaces and their descriptions in a consistent order */ + $i_list_assigned = $i_names; + $i_list_assigned_append = $i_names; + $i_list_unassigned = array(); + $i_list_unassigned_append = array(); + foreach ($i_ports as $i) { + $append = false; + $assigned = false; + $description = sprintf('unassigned (%s)', $i); + if (in_array($i, array('pfsync0', 'pflog0'))) { + // Ignore these interfaces + continue; + } elseif (preg_match('/(enc0|lo0|ovpn[sc]\d+|tun_wg\d+|ipsec\d+)/', $i)) { + $append = true; + if ($i == 'enc0') { + $assigned = true; + $description = 'IPsec (enc0)'; + } elseif ($i == 'lo0') { + $assigned = true; + $description = 'Loopback (lo0)'; } } - } - if ($port != "") { - $ports = precheck_hostport($port); - foreach ($ports as $p) { - $p = strip_logic($p); - if (!is_port(strip_not($p))) { - $input_errors[] = gettext("Invalid value specified for port."); + // Set the interface description + if (!$assigned && array_key_exists($i, $i_names)) { + if (array_key_exists($i_names[$i], $i_descriptions)) { + $assigned = true; + $description = sprintf('%1$s (%2$s)', $i_descriptions[$i_names[$i]], $i); } } - } - if ($snaplen == "") { - $snaplen = 0; - } else { - if (!is_numeric($snaplen) || $snaplen < 0) { - $input_errors[] = gettext("Invalid value specified for packet length."); + // Save the interface to the respective group + if ($append) { + if ($assigned) { + $i_list_assigned_append[$i] = $description; + unset($i_list_assigned[$i]); + } else { + $i_list_unassigned_append[$i] = $description; + } + } else { + if ($assigned) { + $i_list_assigned[$i] = $description; + unset($i_list_assigned_append[$i]); + } else { + $i_list_unassigned[$i] = $description; + } } } - if ($count == "") { - $count = 0; - } else { - if (!is_numeric($count) || $count < 0) { - $input_errors[] = gettext("Invalid value specified for packet count."); - } - } + /* return ordered interface list */ + return array_merge($i_list_assigned, $i_list_assigned_append, $i_list_unassigned, $i_list_unassigned_append); +} - if (!count($input_errors)) { - $do_tcpdump = true; +/* Page properties */ +$allowautocomplete = true; +if ($_POST['downloadbtn'] != '') { + $nocsrf = true; +} +$pgtitle = array(gettext('Diagnostics'), gettext('Packet Capture')); + +/* Page variables */ +$available_interfaces = get_interfaces_sorted(); +$max_view_size = 50*1024*1024; // Limit viewing capture files to 50MB. See https://redmine.pfsense.org/issues/9239 +$pcap_files_root = $g['tmp_path']; +$pcap_files_list = array(); +foreach (array_filter(glob("{$pcap_files_root}/packetcapture-*.pcap"),'is_file') as $file) { + if (preg_match('/.*-\d{14}\.pcap/', $file)) { + // Include the file in the array with the start time as the key + $pcap_files_list[strtotime(substr($file, -19, 14))] = $file; + } +} +ksort($pcap_files_list, SORT_NUMERIC); +$pcap_file_last = empty($pcap_files_list) ? null : array_reverse($pcap_files_list)[0]; // example: packetcapture-WAN_1-20220701000101.pcap +$run_capture = false; +$expression_string = ''; +$input_errors = ''; +/* Actions taken on form button click */ +if ($_POST) { + if ($_POST['startbtn'] != '') { + $action = 'start'; + $run_capture = true; + } elseif ($_POST['stopbtn'] != '') { + $action = 'stop'; + } elseif ($_POST['viewbtn'] != '') { + $action = 'view'; + } elseif ($_POST['downloadbtn'] != '') { + $action = 'download'; + } elseif ($_POST['clearbtn'] != '') { + $action = 'clear'; + } - if ($_POST['promiscuous']) { - //if promiscuous mode is checked - $disablepromiscuous = ""; - } else { - //if promiscuous mode is unchecked - $disablepromiscuous = "-p"; + try { + /* Save previous input to use on page load after submission */ + // capture options + $input_interface = $_POST['interface']; + if (!array_key_exists($input_interface, $available_interfaces)) { + throw new Exception('No valid interface selected.'); } - - if ($_POST['dnsquery']) { - //if dns lookup is checked - $disabledns = ""; + $input_filter = $_POST['filter']; + if ($_POST['count'] == '0') { + $input_count = 0; } else { - //if dns lookup is unchecked - $disabledns = "-n"; + $input_count = empty($_POST['count']) ? 1000 : $_POST['count']; } + $input_length = empty($_POST['length']) ? 0 : $_POST['length']; + $input_promiscuous = empty($_POST['promiscuous']) ? false : $_POST['promiscuous']; + // view options + $input_detail = empty($_POST['detail']) ? 'normal' : $_POST['detail']; + $input_lookup = empty($_POST['lookup']) ? false : $_POST['lookup']; + + // filter options + $filterattributes = array(); + $filterattributes[] = new FilterAttribute(0, $input_filter, PCAP_ATYPE_SIMPLE); + if ($input_filter == PCAP_FMATCH_CUSTOM) { + foreach ($_POST as $key => $value) { + unset($fa_offset); + if (preg_match('/^untagged_.*_match$/', $key)) { + $fa_offset = PCAP_FSECTION_UNTAGGED; + $fa_name_type = substr_replace(substr($key, 9), '', -6); + } elseif (preg_match('/^(singletagged)_.*_match$/', $key)) { + $fa_offset = PCAP_FSECTION_SINGLETAGGED; + $fa_name_type = substr_replace(substr($key, 13), '', -6); + } elseif (preg_match('/^(doubletagged)_.*_match$/', $key)) { + $fa_offset = PCAP_FSECTION_DOUBLETAGGED; + $fa_name_type = substr_replace(substr($key, 13), '', -6); + } else { + continue; + } - if ($_POST['startbtn'] != "") { - $action = gettext("Start"); - - //delete previous packet capture if it exists - if (file_exists($fp.$fn)) { - unlink ($fp.$fn); + if (isset($fa_offset)) { + $fa_name = substr_replace($key, '', -6); // untagged_ethertype, singletagged_ethertype, doubletagged_ethertype + + switch ($fa_name_type) { + case 'ethertype': + $fa_type = PCAP_ATYPE_ETHERTYPE; + break; + case 'protocol': + $fa_type = PCAP_ATYPE_PROTOCOL; + break; + case 'ipaddress': + $fa_type = PCAP_ATYPE_IPADDRESS; + break; + case 'macaddress': + $fa_type = PCAP_ATYPE_MACADDRESS; + break; + case 'port': + $fa_type = PCAP_ATYPE_PORT; + break; + case 'tag': + if ($value == PCAP_AMATCH_NONE) { + $fa_type = PCAP_ATYPE_MATCH; + } else { + $fa_type = PCAP_ATYPE_VLAN; + } + break; + case 'notag': + $fa_type = PCAP_ATYPE_MATCH; + break; + default: + unset($fa_type); + break; + } + + if (isset($fa_type)) { + // Generate variable variables to pre-fill form input based on POST data + ${'input_' . $key} = $value; + ${'input_' . $fa_name} = $_POST[$fa_name]; + + if ($fa_type == PCAP_ATYPE_MATCH) { + // Create untagged section match operator + $filterattributes[] = new FilterAttribute($fa_offset, $value, PCAP_ATYPE_MATCH); + } elseif ($value == PCAP_AMATCH_NONE) { + // If any filter match selection is none, treat it as a section match operator + $filterattributes[] = new FilterAttribute($fa_offset, $value, PCAP_ATYPE_MATCH); + } else { + // Create filter attributes + if (in_array($value, array(PCAP_AMATCH_NONEOF, PCAP_AMATCH_AND_ANYOF, PCAP_AMATCH_OR_ANYOF))) { + // Match selection is the attribute operator + $fa = new FilterAttribute($fa_offset, $value, $fa_type); + $fa->setInput(empty($_POST[$fa_name]) ? '' : $_POST[$fa_name]); + } else { + // Match selection is the attribute input + $fa = new FilterAttribute($fa_offset, PCAP_AMATCH_AND_ANYOF, $fa_type); + $fa->setInput(empty($value) ? '' : $value); + } + $filterattributes[] = $fa; + } + } + } } + } - } elseif ($_POST['stopbtn'] != "") { - $action = gettext("Stop"); - $processes_running = trim(shell_exec("/bin/ps axw -O pid= | /usr/bin/grep tcpdump | /usr/bin/grep {$fn} | /usr/bin/egrep -v '(pflog|grep)'")); - - //explode processes into an array, (delimiter is new line) - $processes_running_array = explode("\n", $processes_running); - - //kill each of the packetcapture processes - foreach ($processes_running_array as $process) { - $process_id_pos = strpos($process, ' '); - $process_id = substr($process, 0, $process_id_pos); - exec("kill $process_id"); - } - } elseif ($_POST['downloadbtn'] != "") { - //download file - send_user_download('file', $fp.$fn); + $expression_string = get_filter_expression_string($filterattributes); + if (empty($expression_string) && $input_filter == PCAP_FMATCH_CUSTOM) { + throw new Exception('Custom filter cannot be empty.'); } + } catch (Exception $e) { + $run_capture = false; + $input_errors = $e->getMessage(); } -} else { - $do_tcpdump = false; } -$excl = gettext("Exclude"); +/* Header page HTML */ +include('head.inc'); -$protocollist = array( - '' => 'Any', - 'icmp' => 'ICMP', - '!icmp' => $excl . ' ICMP', +/* Show input validation errors; shown betwen navigation breadcrumbs and page contents */ +if (!empty($input_errors) && $action == 'start') { + print_input_errors(array(gettext($input_errors))); +} + +/* Form drop-down lists */ +$form_filters = array( + PCAP_FMATCH_ANY => 'Everything', + PCAP_FMATCH_UNTAGGED => 'Only Untagged', + PCAP_FMATCH_TAGGED => 'Only Tagged', + PCAP_FMATCH_CUSTOM => 'Custom Filter' +); +$form_detail = array( + 'normal' => gettext('Normal'), + 'medium' => gettext('Medium'), + 'high' => gettext('High'), + 'full' => gettext('Full'), +); +$form_untagged = array( + PCAP_AMATCH_NONE => 'NONE', + PCAP_AMATCH_OR_ANYOF => 'ANY OF' +); +$form_singletagged = array( + PCAP_AMATCH_NONE => 'NONE', + PCAP_AMATCH_AND_ANYOF => 'ANY OF', + PCAP_AMATCH_NONEOF => 'NONE OF' +); +$form_doubletagged = array( + PCAP_AMATCH_NONE => 'NONE', + PCAP_AMATCH_AND_ANYOF => 'ANY OF', + PCAP_AMATCH_NONEOF => 'NONE OF' +); +$form_match_ethertype = array( + PCAP_AMATCH_AND_ANYOF => 'ANY OF', + PCAP_AMATCH_OR_ANYOF => 'ANY OF [OR]', + PCAP_AMATCH_NONEOF => 'NONE OF', + 'ipv4' => 'IPv4', + 'ipv6' => 'IPv6', + 'arp' => 'ARP' +); +$form_match_protocol = array( + PCAP_AMATCH_AND_ANYOF => 'ANY OF', + PCAP_AMATCH_OR_ANYOF => 'ANY OF [OR]', + PCAP_AMATCH_NONEOF => 'NONE OF', + 'icmp' => 'ICMPv4', 'icmp6' => 'ICMPv6', - '!icmp6' => $excl . ' ICMPv6', 'tcp' => 'TCP', - '!tcp' => $excl . ' TCP', 'udp' => 'UDP', - '!udp' => $excl . ' UDP', - 'arp' => 'ARP', - '!arp' => $excl . ' ARP', + 'ipsec' => 'IPsec', 'carp' => 'CARP', - '!carp' => $excl . ' CARP', 'pfsync' => 'pfsync', - '!pfsync' => $excl . ' pfsync', - 'esp' => 'ESP', - '!esp' => $excl . ' ESP', - 'ospf' => 'OSPF', - '!ospf' => $excl . ' OSPF' + 'ospf' => 'OSPF' ); - -include("head.inc"); - -if ($input_errors) { - print_input_errors($input_errors); -} - -$form = new Form(false); // No button yet. We add those later depending on the required action - +$form_match_ipaddress = array( + PCAP_AMATCH_AND_ANYOF => 'ANY OF', + PCAP_AMATCH_OR_ANYOF => 'ANY OF [OR]', + PCAP_AMATCH_NONEOF => 'NONE OF' +); +$form_match_macaddress = array( + PCAP_AMATCH_AND_ANYOF => 'ANY OF', + PCAP_AMATCH_OR_ANYOF => 'ANY OF [OR]', + PCAP_AMATCH_NONEOF => 'NONE OF' +); +$form_match_port = array( + PCAP_AMATCH_AND_ANYOF => 'ANY OF', + PCAP_AMATCH_OR_ANYOF => 'ANY OF [OR]', + PCAP_AMATCH_NONEOF => 'NONE OF' +); +/* pre-filled form input */ +$input_filter = isset($input_filter) ? $input_filter : PCAP_FMATCH_ANY; +$input_promiscuous = isset($input_promiscuous) ? $input_promiscuous : true; +$input_detail = isset($input_detail) ? $input_detail : 'normal'; +$input_lookup = isset($input_lookup) ? $input_lookup : false; +$input_untagged_notag_match = isset($input_untagged_notag_match) ? $input_untagged_notag_match : PCAP_AMATCH_OR_ANYOF; +$input_doubletagged_tag_match = isset($input_doubletagged_tag_match) ? $input_doubletagged_tag_match : PCAP_AMATCH_NONE; + +/* Prepare the form */ +$form = new Form(false); $section = new Form_Section('Packet Capture Options'); -$section->addInput(new Form_Select( +// CAPTURE OPTIONS +$group = new Form_Group('Capture Options'); +$group->add(new Form_Select( 'interface', - '*Interface', - $selectedif, - $interfaces -))->setHelp('Select the interface on which to capture traffic. '); - -$section->addInput(new Form_Checkbox( + null, + $input_interface, + $available_interfaces +))->setHelp('Interface to caputer packets on.')->setWidth(4); +$group->add(new Form_Select( + 'filter', + null, + $input_filter, + $form_filters +))->setHelp('Packet capture filter.')->addClass('match-selection')->setWidth(2); +$section->add($group); +$group = new Form_Group(''); +$group->add(new Form_Input( + 'count', + 'Packet Count', + null, + $input_count, + array('type' => 'number', 'min' => 0, 'step' => 1) +))->setHelp('Max number of packets to capture (default 1000). Enter 0 (zero) for no limit.')->setWidth(2); +$group->add(new Form_Input( + 'length', + 'Packet Length', + null, + $input_length, + array('type' => 'number', 'min' => 0, 'step' => 1) +))->setHelp('Max bytes per packet (default 0). Enter 0 (zero) for no limit.')->setWidth(2); +$group->add(new Form_Checkbox( 'promiscuous', - 'Promiscuous', - 'Enable promiscuous mode', - $promiscuous -))->setHelp('%1$sNon-promiscuous mode captures only traffic that is directly relevant to the host (sent by it, sent or broadcast to it, or routed through it) and ' . - 'does not show packets that are ignored at network adapter level.%2$s%3$sPromiscuous mode%4$s ("sniffing") captures all data seen by the adapter, whether ' . - 'or not it is valid or related to the host, but in some cases may have undesirable side effects and not all adapters support this option. Click Info for details %5$s' . - 'Promiscuous mode requires more kernel processing of packets. This puts a slightly higher demand on system resources, especially ' . - 'on very busy networks or low power processors. The change in packet processing may allow a hostile host to detect that an adapter is in promiscuous mode ' . - 'or to \'fingerprint\' the kernel (see %6$s). Some network adapters may not support or work well in promiscuous mode (see %7$s).%8$s', - - '

', - '

', - '', - '', - '
', - ' [1]' . - ' [2]', - ' [3]', - '

' -); + null, + 'Promiscuous Mode', + $input_promiscuous +))->setHelp('Capture all traffic seen by the interface. Disable this option to only capture traffic to and from the interface, including broadcast and multicast traffic.')->setWidth(5); +$section->add($group); + +// VIEW OPTIONS +$group = new Form_Group('View Options'); +$group->add(new Form_Select( + 'detail', + 'View Detail', + $input_detail, + $form_detail +))->setHelp('The level of detail shown when viewing the packet capture. Does not affect the packet capture itself.')->setWidth(4); +$group->add(new Form_Checkbox( + 'lookup', + null, + 'Name Lookup', + $input_lookup +))->setHelp('Perform a name lookup for the port and host address, including MAC OUI. This can cause delays when viewing large packet captures.')->setWidth(5); +$section->add($group); +$form->add($section); -$section->addInput(new Form_Select( - 'fam', - '*Address Family', - $fam, - array('' => 'Any', - 'ip' => gettext('IPv4 Only'), - 'ip6' => gettext('IPv6 Only') - ) -))->setHelp('Select the type of traffic to be captured.'); - -$section->addInput(new Form_Select( - 'proto', - '*Protocol', - $proto, - $protocollist -))->setHelp('Select the protocol to capture, or "Any". '); - -$section->addInput(new Form_Input( - 'host', - 'Host Address', +// UNTAGGED OPTIONS +$section = new Form_Section('Custom Filter Options'); +$section->addClass('custom-options'); +$section->addInput(new Form_StaticText( + 'Hint', + sprintf('All input is %1$sspace-separated%2$s. When selecting %1$sANY OF [OR]%2$s, at least two attributes should be ' . + 'specified (e.g. Ethertype and Port). This will capture packets that match either attribute input instead of explicitly both.', '', '') +)); +$section->addInput(new Form_StaticText( + 'Untagged Filter', + sprintf('Filter options for packets without any VLAN tags. Select %1$sNONE%2$s to exclude all untagged packets.', '', '') +)); +$group = new Form_Group(''); +$group->add(new Form_Select( + 'untagged_notag_match', + null, + $input_untagged_notag_match, + $form_untagged +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_StaticText( + null, + null +))->setWidth(3); +$group->add(new Form_Select( + 'untagged_ethertype_match', + null, + $input_untagged_ethertype_match, + $form_match_ethertype +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'untagged_ethertype', + 'Ethertype', 'text', - $host -))->setHelp('This value is either the Source or Destination IP address, subnet in CIDR notation, or MAC address.%1$s' . - 'Matching can be negated by preceding the value with "!". Multiple IP addresses or CIDR subnets may be specified. Comma (",") separated values perform a boolean "AND". ' . - 'Separating with a pipe ("|") performs a boolean "OR".%1$s' . - 'MAC addresses must be entered in colon-separated format, such as xx:xx:xx:xx:xx:xx or a partial address consisting of one (xx), two (xx:xx), or four (xx:xx:xx:xx) segments.%1$s' . - 'If this field is left blank, all packets on the specified interface will be captured.', - '
'); - -$section->addInput(new Form_Input( - 'port', - 'Port', + $input_untagged_ethertype, + array('title' => 'EXAMPLE: arp 8100 0x8200') +))->setHelp('')->setWidth(3); +$section->add($group); +$group = new Form_Group(''); +$group->add(new Form_Select( + 'untagged_port_match', + null, + $input_untagged_port_match, + $form_match_port +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'untagged_port', + 'Port number', 'text', - $port -))->setHelp('The port can be either the source or destination port. The packet capture will look for this port in either field. ' . - 'Matching can be negated by preceding the value with "!". Multiple ports may be specified. Comma (",") separated values perform a boolean "AND". Separating with a pipe ("|") performs a boolean "OR".' . - 'Leave blank if not filtering by port.'); - -$section->addInput(new Form_Input( - 'snaplen', - 'Packet Length', + $input_untagged_port, + array('title' => 'EXAMPLE: 80 443') +))->setHelp('')->setWidth(3); +$group->add(new Form_Select( + 'untagged_protocol_match', + null, + $input_untagged_protocol_match, + $form_match_protocol +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'untagged_protocol', + 'Protocol', 'text', - $snaplen -))->setHelp('The Packet length is the number of bytes of each packet that will be captured. Default value is 0, ' . - 'which will capture the entire frame regardless of its size.'); - -$section->addInput(new Form_Input( - 'count', - 'Count', + $input_untagged_protocol, + array('title' => 'EXAMPLE: tcp 17') +))->setHelp('')->setWidth(3); +$section->add($group); +$group = new Form_Group(''); +$group->add(new Form_Select( + 'untagged_ipaddress_match', + null, + $input_untagged_ipaddress_match, + $form_match_ipaddress +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'untagged_ipaddress', + 'Host IP Address or Subnet', 'text', - $count -))->setHelp('This is the number of packets the packet capture will grab. Default value is 100.%s' . - 'Enter 0 (zero) for no count limit.', - '
'); - -$section->addInput(new Form_Select( - 'detail', - 'Level of detail', - $detail, - array('normal' => gettext('Normal'), - 'medium' => gettext('Medium'), - 'high' => gettext('High'), - 'full' => gettext('Full'), - 'none' => gettext('None'), - ) -))->setHelp('This is the level of detail that will be displayed after hitting "Stop" when the packets have been captured.%s' . - 'This option does not affect the level of detail when downloading the packet capture. ', - '
'); - -$section->addInput(new Form_Checkbox( - 'dnsquery', - 'Reverse DNS Lookup', - 'Do reverse DNS lookup', - $_POST['dnsquery'] -))->setHelp('The packet capture will perform a reverse DNS lookup associated with all IP addresses.%s' . - 'This option can cause delays for large packet captures.', - '
'); - + $input_untagged_ipaddress, + array('title' => 'EXAMPLE: 10.1.1.1 10.2.2.0/24') +))->setHelp('')->setWidth(3); +$group->add(new Form_Select( + 'untagged_macaddress_match', + null, + $input_untagged_macaddress_match, + $form_match_macaddress +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'untagged_macaddress', + 'Host MAC Address', + 'text', + $input_untagged_macaddress, + array('title' => 'EXAMPLE: 00:02 00:00:00:04 00:00:00:00:00:06') +))->setHelp('')->setWidth(3); +$section->add($group); + +// SINGLE-TAGGED options +$section->addInput(new Form_StaticText( + 'Single-Tagged Filter', + sprintf('Filter options for packets with only an outer VLAN tag. Select %1$sNONE%2$s to exclude all single-tagged packets.', '', '') +)); +$group = new Form_Group(''); +$group->add(new Form_Select( + 'singletagged_tag_match', + null, + $input_singletagged_tag_match, + $form_singletagged +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'singletagged_tag', + 'VLAN Tag', + 'text', + $input_singletagged_tag, + array('title' => 'EXAMPLE: 100 200') +))->setHelp('')->setWidth(3); +$group->add(new Form_Select( + 'singletagged_ethertype_match', + null, + $input_singletagged_ethertype_match, + $form_match_ethertype +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'singletagged_ethertype', + 'Ethertype', + 'text', + $input_singletagged_ethertype, + array('title' => 'EXAMPLE: arp 8100 0x8200') +))->setHelp('')->setWidth(3); +$section->add($group); +$group = new Form_Group(''); +$group->add(new Form_Select( + 'singletagged_port_match', + null, + $input_singletagged_port_match, + $form_match_port +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'singletagged_port', + 'Port number', + 'text', + $input_singletagged_port, + array('title' => 'EXAMPLE: 80 443') +))->setHelp('')->setWidth(3); +$group->add(new Form_Select( + 'singletagged_protocol_match', + null, + $input_singletagged_protocol_match, + $form_match_protocol +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'singletagged_protocol', + 'Protocol', + 'text', + $input_singletagged_protocol, + array('title' => 'EXAMPLE: tcp 17') +))->setHelp('')->setWidth(3); +$section->add($group); +$group = new Form_Group(''); +$group->add(new Form_Select( + 'singletagged_ipaddress_match', + null, + $input_singletagged_ipaddress_match, + $form_match_ipaddress +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'singletagged_ipaddress', + 'Host IP Address or Subnet', + 'text', + $input_singletagged_ipaddress, + array('title' => 'EXAMPLE: 10.1.1.1 10.2.2.0/24') +))->setHelp('')->setWidth(3); +$group->add(new Form_Select( + 'singletagged_macaddress_match', + null, + $input_singletagged_macaddress_match, + $form_match_macaddress +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'singletagged_macaddress', + 'Host MAC Address', + 'text', + $input_singletagged_macaddress, + array('title' => 'EXAMPLE: 00:02 00:00:00:04 00:00:00:00:00:06') +))->setHelp('')->setWidth(3); +$section->add($group); + +// DOUBLE-TAGGED options +$section->addInput(new Form_StaticText( + 'Double-Tagged Filter', + sprintf('Filter options for packets with both an outer and inner VLAN tag. Select %1$sNONE%2$s to exclude all double-tagged packets.', '', '') +)); +$group = new Form_Group(''); +$group->add(new Form_Select( + 'doubletagged_tag_match', + null, + $input_doubletagged_tag_match, + $form_doubletagged +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'doubletagged_tag', + 'VLAN Tag', + 'text', + $input_doubletagged_tag, + array('title' => 'EXAMPLE: 100 200') +))->setHelp('')->setWidth(3); +$group->add(new Form_Select( + 'doubletagged_ethertype_match', + null, + $input_doubletagged_ethertype_match, + $form_match_ethertype +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'doubletagged_ethertype', + 'Ethertype', + 'text', + $input_doubletagged_ethertype, + array('title' => 'EXAMPLE: arp 8100 0x8200') +))->setHelp('')->setWidth(3); +$section->add($group); +$group = new Form_Group(''); +$group->add(new Form_Select( + 'doubletagged_port_match', + null, + $input_doubletagged_port_match, + $form_match_port +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'doubletagged_port', + 'Port number', + 'text', + $input_doubletagged_port, + array('title' => 'EXAMPLE: 80 443') +))->setHelp('')->setWidth(3); +$group->add(new Form_Select( + 'doubletagged_protocol_match', + null, + $input_doubletagged_protocol_match, + $form_match_protocol +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'doubletagged_protocol', + 'Protocol', + 'text', + $input_doubletagged_protocol, + array('title' => 'EXAMPLE: tcp 17') +))->setHelp('')->setWidth(3); +$section->add($group); +$group = new Form_Group(''); +$group->add(new Form_Select( + 'doubletagged_ipaddress_match', + null, + $input_doubletagged_ipaddress_match, + $form_match_ipaddress +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'doubletagged_ipaddress', + 'Host IP Address or Subnet', + 'text', + $input_doubletagged_ipaddress, + array('title' => 'EXAMPLE: 10.1.1.1 10.2.2.0/24') +))->setHelp('')->setWidth(3); +$group->add(new Form_Select( + 'doubletagged_macaddress_match', + null, + $input_doubletagged_macaddress_match, + $form_match_macaddress +))->setHelp('')->addClass('match-selection')->setWidth(2); +$group->add(new Form_Input( + 'doubletagged_macaddress', + 'Host MAC Address', + 'text', + $input_doubletagged_macaddress, + array('title' => 'EXAMPLE: 00:02 00:00:00:04 00:00:00:00:00:06') +))->setHelp('')->setWidth(3); +$section->add($group); $form->add($section); -/* check to see if packet capture tcpdump is already running */ -$processcheck = (trim(shell_exec("/bin/ps axw -O pid= | /usr/bin/grep tcpdump | /usr/bin/grep {$fn} | /usr/bin/egrep -v '(pflog|grep)'"))); - -$processisrunning = ($processcheck != ""); - -if (($action == gettext("Stop") or $action == "") and $processisrunning != true) { - $form->addGlobal(new Form_Button( - 'startbtn', - 'Start', - null, - 'fa-play-circle' - ))->addClass('btn-success'); -} else { +/* Display form buttons depending on process state and form action */ +// Check if any matching packet captures are running +$processcheck = shell_exec("/bin/pgrep -f '^\/usr\/sbin\/tcpdump.*packetcapture-.*-[0-9]+\.pcap'"); +$processisrunning = (!empty($processcheck)); +if ($run_capture && ($action == 'start' || (empty($action) && $processisrunning))) { $form->addGlobal(new Form_Button( 'stopbtn', 'Stop', null, 'fa-stop-circle' ))->addClass('btn-warning'); - if ($action == gettext("Start")) { - touch("/root/packetcapture.start"); - } - if (file_exists($fp.$fns)) { - $section->addInput(new Form_StaticText( - 'Last capture start', - date("F jS, Y g:i:s a.", filemtime($fp.$fns)) - )); +} else { + if ($action == 'stop' && $processisrunning) { + // Kill relevant running tcpdump processes. Do not defer to background to avoid displaying an empty packet capture. + mwexec("/bin/pkill -f '^\/usr\/sbin\/tcpdump.*packetcapture-.*-[0-9]+\.pcap'"); } -} - -if (file_exists($fp.$fn) and $processisrunning != true) { - $form->addGlobal(new Form_Button( - 'viewbtn', - 'View Capture', - null, - 'fa-file-text-o' - ))->addClass('btn-primary'); - $form->addGlobal(new Form_Button( - 'downloadbtn', - 'Download Capture', + 'startbtn', + 'Start', null, - 'fa-download' - ))->addClass('btn-primary'); - - if (file_exists($fp.$fns)) { - $section->addInput(new Form_StaticText( - 'Last capture start', - date("F jS, Y g:i:s a.", filemtime($fp.$fns)) - )); + 'fa-play-circle' + ))->addClass('btn-success'); + if (file_exists($pcap_file_last)) { + if ($action == 'clear') { + foreach ($pcap_files_list as $pcapfile) { + unlink_if_exists($pcapfile); + } + } else { + if ($action == 'download') { + send_user_download('file', $pcap_file_last); + } + $form->addGlobal(new Form_Button( + 'viewbtn', + 'View', + null, + 'fa-file-text-o' + ))->addClass('btn-primary'); + $form->addGlobal(new Form_Button( + 'downloadbtn', + 'Download', + null, + 'fa-download' + ))->addClass('btn-primary'); + $form->addGlobal(new Form_Button( + 'clearbtn', + 'Clear Captures', + null, + 'fa-trash' + ))->addClass('btn-danger'); + $section->addInput(new Form_StaticText( + 'Last capture start', + date('F jS, Y g:i:s a.', strtotime(substr($pcap_file_last, -19, 14))) + )); + + $section->addInput(new Form_StaticText( + 'Last capture stop', + date('F jS, Y g:i:s a.', filemtime($pcap_file_last)) + )); + } } - $section->addInput(new Form_StaticText( - 'Last capture stop', - date("F jS, Y g:i:s a.", filemtime($fp.$fn)) - )); } +/* Show the form */ print($form); -if ($do_tcpdump) : - $matches = array(); - - if (in_array($fam, $fams)) { - $matches[] = $fam; - } - - if (in_array($proto, $protos)) { - switch (ltrim($proto, '!')) { - case 'ospf': - $proto = str_replace('ospf', 'proto ospf', $proto); - break; - case 'carp': - $proto = str_replace('carp', 'proto 112', $proto); - break; - case 'pfsync': - $proto = str_replace('pfsync', 'proto pfsync', $proto); - break; - default: - break; - } - $matches[] = fixup_not($proto); - } - - if ($port != "") { - $matches[] = hostport_array_fixer($ports,'port'); - } - - if ($host != "") { - $matches[] = hostport_array_fixer($hosts,'host'); - } - - if ($count != "0") { - $searchcount = "-c " . $count; - } else { - $searchcount = ""; - } - - $selectedif = convert_friendly_interface_to_real_interface_name($selectedif); +/* Run tcpdump */ +if ($run_capture) { + // Generate the file name to write to + $pcap_file_current = sprintf('%1$s/packetcapture-%2$s-%3$s.pcap', $pcap_files_root, $input_interface, date('YmdHis')); + unlink_if_exists($pcap_file_current); + + // Handle capture options + $cmd_promiscuous = $input_promiscuous ? '' : ' -p'; + $cmd_count = sprintf(' -c %d', $input_count); + $cmd_length = sprintf(' -s %d', $input_length); + $cmd_expression_string = $expression_string ? escapeshellarg($expression_string) : ''; + + // Run the capture + $cmd_run = sprintf('/usr/sbin/tcpdump -ni %1$s%2$s%3$s%4$s -w %5$s %6$s', $input_interface, $cmd_promiscuous, $cmd_count, $$cmd_length, $pcap_file_current, $cmd_expression_string); + print_info_box(gettext('Packet capture is running:') . '
' . htmlspecialchars($cmd_run), 'info'); + mwexec_bg ($cmd_run); +} - if ($action == gettext("Start")) { - $matchstr = implode(" and ", $matches); - $cmd = "/usr/sbin/tcpdump -i {$selectedif} {$disablepromiscuous} {$searchcount} -s {$snaplen} -w {$fp}{$fn} " . escapeshellarg($matchstr); - print_info_box(gettext('Packet capture is running'), 'info'); - ?> -
- -
- +
-

+

'); - if (file_exists($fp.$fn) && (filesize($fp.$fn) > $max_display_size)) { - print(gettext("Packet capture file is too large to display in the GUI.") . - "\n" . - gettext("Download the file, or view it in the console or ssh shell.")); - } elseif (!file_exists($fp.$fn)) { - print(gettext("No capture file to display.")); - } elseif ($detail == 'none') { - print(gettext("Select a detail level to view the contents of the packet capture.")); - } else { - system("/usr/sbin/tcpdump {$disabledns} {$detail_args} {$iscarp} -r {$fp}{$fn}"); - } - print(''); + $cmd_view = sprintf('/usr/sbin/tcpdump%1$s%2$s -r %3$s', $cmd_lookup, $cmd_detail, $pcap_file_last); + // View the capture file + print(''); ?>
+ + + +