diff --git a/plist b/plist
index 1db7df480..2a7da06ed 100644
--- a/plist
+++ b/plist
@@ -531,6 +531,7 @@
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/DescriptionField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/EmailField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/HostnameField.php
+/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/IPPortField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/IntegerField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/InterfaceField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/JsonKeyValueStoreField.php
@@ -845,6 +846,7 @@
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/CertificateFieldTest/config.xml
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/CountryFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/Field_Framework_TestCase.php
+/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/IPPortFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/IntegerFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/InterfaceFieldTest.php
/usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/InterfaceFieldTest/config.xml
diff --git a/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/IPPortField.php b/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/IPPortField.php
new file mode 100644
index 000000000..650db0b76
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/IPPortField.php
@@ -0,0 +1,149 @@
+internalAsList) {
+ // return result as list
+ $result = array();
+ foreach (explode($this->internalFieldSeparator, $this->internalValue) as $address) {
+ $result[$address] = array("value" => $address, "selected" => 1);
+ }
+ return $result;
+ } else {
+ // normal, single field response
+ return $this->internalValue;
+ }
+ }
+
+ /**
+ * setter for address family
+ * @param $value address family [ipv4, ipv6, empty for all]
+ */
+ public function setAddressFamily($value)
+ {
+ $this->internalAddressFamily = trim(strtolower($value));
+ }
+
+ /**
+ * select if multiple IP-Port combinations may be selected at once
+ * @param $value string value Y/N
+ */
+ public function setAsList($value)
+ {
+ $this->internalAsList = trim(strtoupper($value)) == "Y";
+ }
+
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function defaultValidationMessage()
+ {
+ return gettext('Invalid IP-port combination.');
+ }
+
+ /**
+ * retrieve field validators for this field type
+ * @return array returns validators
+ */
+ public function getValidators()
+ {
+ $validators = parent::getValidators();
+ if ($this->internalValue != null) {
+ $validators[] = new CallbackValidator(["callback" => function ($data) {
+ foreach ($this->internalAsList ? explode($this->internalFieldSeparator, $data) : [$data] as $value) {
+ if ($this->internalAddressFamily == 'ipv4' || $this->internalAddressFamily == null) {
+ $parts = explode(':', $value);
+ if (count($parts) == 2 && Util::isIpv4Address($parts[0]) && Util::isPort($parts[1])) {
+ continue;
+ }
+ }
+
+ if ($this->internalAddressFamily == 'ipv6' || $this->internalAddressFamily == null) {
+ $parts = preg_split('/\[([^\]]+)\]/', $value, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+ if (count($parts) == 2 &&
+ Util::isIpv6Address($parts[0]) &&
+ str_contains($parts[1], ':') &&
+ Util::isPort(trim($parts[1], ': '))) {
+ continue;
+ }
+ }
+
+ return ["\"" . $value . "\" is invalid. " . $this->getValidationMessage()];
+ }
+ }]);
+ }
+
+ return $validators;
+ }
+}
\ No newline at end of file
diff --git a/src/opnsense/mvc/app/models/OPNsense/Diagnostics/Netflow.xml b/src/opnsense/mvc/app/models/OPNsense/Diagnostics/Netflow.xml
index a4b7f3373..7517fced7 100644
--- a/src/opnsense/mvc/app/models/OPNsense/Diagnostics/Netflow.xml
+++ b/src/opnsense/mvc/app/models/OPNsense/Diagnostics/Netflow.xml
@@ -24,10 +24,9 @@
v9
-
- /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):(6553[0-5]|655[0-2][0-9]|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{0,3})$/u
- Please enter valid targets (e.g. 192.168.0.1:2055).
- Y
+
+ Y
+ ipv4
diff --git a/src/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/IPPortFieldTest.php b/src/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/IPPortFieldTest.php
new file mode 100644
index 000000000..729d852b9
--- /dev/null
+++ b/src/opnsense/mvc/tests/app/models/OPNsense/Base/FieldTypes/IPPortFieldTest.php
@@ -0,0 +1,152 @@
+assertInstanceOf('\OPNsense\Base\FieldTypes\IPPortField', new IPPortField());
+ }
+
+ public function testRequiredEmpty()
+ {
+ $this->expectException(\Phalcon\Filter\Validation\Exception::class);
+ $this->expectExceptionMessage("PresenceOf");
+ $field = new IPPortField();
+ $field->setRequired("Y");
+ $field->setValue("");
+ $this->validateThrow($field);
+ }
+
+ public function testNotRequiredEmpty()
+ {
+ $field = new IPPortField();
+ $field->setValue("");
+ $this->assertEmpty($this->validate($field));
+ }
+
+ public function testRequiredNotEmpty()
+ {
+ $field = new IPPortField();
+ $field->setRequired("Y");
+ $field->setValue("127.0.0.1:2056");
+ $this->assertEmpty($this->validate($field));
+ }
+
+ public function testValidValueIpv4()
+ {
+ $field = new IPPortField();
+ $field->setValue("127.0.0.1:2056");
+ $this->assertEmpty($this->validate($field));
+ }
+
+ public function testValidValueAsListIpv4()
+ {
+ $field = new IPPortField();
+ $field->setAsList("Y");
+ $field->setValue("127.0.0.1:2056,192.168.1.1:1111");
+ $this->assertEmpty($this->validate($field));
+ }
+
+ public function testValidValueIpv6()
+ {
+ $field = new IPPortField();
+ $field->setValue("[::1]:2056");
+ $this->assertEmpty($this->validate($field));
+ }
+
+ public function testValidValueAsListIpv6()
+ {
+ $field = new IPPortField();
+ $field->setAsList("Y");
+ $field->setValue("[::1]:2056,[fe80::]:1111");
+ $this->assertEmpty($this->validate($field));
+ }
+
+ public function testInvalidValueIpv4()
+ {
+ $this->expectException(\Phalcon\Filter\Validation\Exception::class);
+ $field = new IPPortField();
+ $field->setValue("abcdefg");
+ $this->validateThrow($field);
+ }
+
+ public function testInvalidValueAsListIpv4()
+ {
+ $this->expectException(\Phalcon\Filter\Validation\Exception::class);
+ $field = new IPPortField();
+ $field->setAsList("Y");
+ $field->setValue("127.0.0.1:2056,abcdefg");
+ $this->validateThrow($field);
+ }
+
+ public function testInvalidValueIpv6()
+ {
+ $this->expectException(\Phalcon\Filter\Validation\Exception::class);
+ $field = new IPPortField();
+ $field->setValue("[::1]");
+ $this->validateThrow($field);
+ }
+
+ public function testInvalidValueAsListIpv6()
+ {
+ $this->expectException(\Phalcon\Filter\Validation\Exception::class);
+ $field = new IPPortField();
+ $field->setAsList("Y");
+ $field->setValue("[::1]:2056,[fe80::]");
+ $this->validateThrow($field);
+ }
+
+ public function testAddressFamilyIpv4()
+ {
+ $this->expectException(\Phalcon\Filter\Validation\Exception::class);
+ $field = new IPPortField();
+ $field->setAddressFamily("ipv4");
+ $field->setValue("[::1]:2056");
+ $this->validateThrow($field);
+ }
+
+ public function testAddressFamilyIpv6()
+ {
+ $this->expectException(\Phalcon\Filter\Validation\Exception::class);
+ $field = new IPPortField();
+ $field->setAddressFamily("ipv6");
+ $field->setValue("192.168.1.1:1111");
+ $this->validateThrow($field);
+ }
+}