From 67fc8db0e8cb925c7dc4db4163c75a6260add7a7 Mon Sep 17 00:00:00 2001 From: "Fabian Franz, BSc" Date: Fri, 1 Dec 2017 21:40:40 +0100 Subject: [PATCH] Iperf3 (#396) --- net/iperf/Makefile | 8 + net/iperf/pkg-descr | 11 + net/iperf/src/etc/inc/plugins.inc.d/iperf.inc | 50 +++++ net/iperf/src/etc/rc.d/iperf | 13 ++ .../OPNsense/iperf/Api/InstanceController.php | 101 +++++++++ .../OPNsense/iperf/Api/ServiceController.php | 68 ++++++ .../OPNsense/iperf/IndexController.php | 45 ++++ .../iperf/forms/instance_settings.xml | 9 + .../mvc/app/models/OPNsense/iperf/ACL/ACL.xml | 9 + .../models/OPNsense/iperf/FakeInstance.php | 37 +++ .../models/OPNsense/iperf/FakeInstance.xml | 11 + .../app/models/OPNsense/iperf/Menu/Menu.xml | 7 + .../mvc/app/views/OPNsense/iperf/index.volt | 138 ++++++++++++ .../src/opnsense/scripts/iperf/ruby_iperf.rb | 211 ++++++++++++++++++ .../service/conf/actions.d/actions_iperf.conf | 23 ++ 15 files changed, 741 insertions(+) create mode 100644 net/iperf/Makefile create mode 100644 net/iperf/pkg-descr create mode 100644 net/iperf/src/etc/inc/plugins.inc.d/iperf.inc create mode 100755 net/iperf/src/etc/rc.d/iperf create mode 100644 net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/Api/InstanceController.php create mode 100644 net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/Api/ServiceController.php create mode 100644 net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/IndexController.php create mode 100644 net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/forms/instance_settings.xml create mode 100644 net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/ACL/ACL.xml create mode 100644 net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.php create mode 100644 net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.xml create mode 100644 net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/Menu/Menu.xml create mode 100644 net/iperf/src/opnsense/mvc/app/views/OPNsense/iperf/index.volt create mode 100755 net/iperf/src/opnsense/scripts/iperf/ruby_iperf.rb create mode 100644 net/iperf/src/opnsense/service/conf/actions.d/actions_iperf.conf diff --git a/net/iperf/Makefile b/net/iperf/Makefile new file mode 100644 index 000000000..be7ad5710 --- /dev/null +++ b/net/iperf/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= iperf3 +PLUGIN_VERSION= 0.0.1 +PLUGIN_COMMENT= iperf3 connection speed tester +PLUGIN_DEPENDS= iperf3 ruby +PLUGIN_MAINTAINER= franz.fabian.94@gmail.com +PLUGIN_DEVEL= yes + +.include "../../Mk/plugins.mk" diff --git a/net/iperf/pkg-descr b/net/iperf/pkg-descr new file mode 100644 index 000000000..31fb79d6d --- /dev/null +++ b/net/iperf/pkg-descr @@ -0,0 +1,11 @@ +iperf3 is a tool for measuring the achievable TCP, UDP, and SCTP +throughput along a path between two hosts. It allows the tuning of +various parameters such as socket buffer sizes and maximum attempted +throughput. It reports (among other things) bandwidth, delay jitter, +and datagram loss. iperf was originally developed by NLANR/DAST. + +iperf3 is a new implementation developed from scratch at the Energy +Sciences Network (ESnet). Among its goals were a smaller, simpler +code base (compared to its predecessor, iperf2) and a library version +of the functionality that can be used in other programs. Note that +iperf3 does not interoperate with with iperf 2.x. diff --git a/net/iperf/src/etc/inc/plugins.inc.d/iperf.inc b/net/iperf/src/etc/inc/plugins.inc.d/iperf.inc new file mode 100644 index 000000000..ae8c4dc1f --- /dev/null +++ b/net/iperf/src/etc/inc/plugins.inc.d/iperf.inc @@ -0,0 +1,50 @@ +registerAnchor('iperf', 'fw'); +} + +function iperf_services() +{ + $services = array(); + + $services[] = array( + 'description' => gettext('iperf Performance Test'), + 'configd' => array( + 'restart' => array('iperf restart'), + 'start' => array('iperf start'), + 'stop' => array('iperf stop'), + ), + 'name' => 'iperf', + 'pidfile' => '/var/run/iperf.pid' + ); + return $services; +} diff --git a/net/iperf/src/etc/rc.d/iperf b/net/iperf/src/etc/rc.d/iperf new file mode 100755 index 000000000..8a7578031 --- /dev/null +++ b/net/iperf/src/etc/rc.d/iperf @@ -0,0 +1,13 @@ +#!/bin/sh +# REQUIRE: LOGIN DAEMON +. /etc/rc.subr +name=iperf +rcvar=iperf_enable +command_interpreter=/usr/local/bin/ruby +command=/usr/local/opnsense/scripts/iperf/ruby_iperf.rb +iperf_user=root +iperf_pidfile=/var/run/iperf.pid +start_cmd="/usr/sbin/daemon -u $iperf_user -p $iperf_pidfile -f $command" +load_rc_config $name +: ${iperf_enable="YES"} +run_rc_command "$1" diff --git a/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/Api/InstanceController.php b/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/Api/InstanceController.php new file mode 100644 index 000000000..d2fdcfab3 --- /dev/null +++ b/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/Api/InstanceController.php @@ -0,0 +1,101 @@ +configdRun('iperf start'); + } + if (!isset($_POST['instance']['interface'])) { + return array('status' => 'error', + 'error' => 'interface parameter is missing'); + } + $interface_name = $_POST['instance']['interface']; + if ($interface = $this->get_real_interface_name($interface_name)) { + // start iperf + return $this->send_command("start $interface", $backend); + } else { + return array('status' => 'error', + 'error' => 'interface is unknown'); + } + } + + public function queryAction() { + $backend = new Backend(); + return $this->send_command('query', $backend); + } + + private function send_command($command, $backend) { + try { + $socket = @stream_socket_client(InstanceController::$SOCKET_PATH, $error_code, $error_msg);} + catch (\Exception $e) { + $socket = null; + } + if (!$socket) { + // in case of an error: try to restart the service and if that fails too + // don't retry anymore + $backend->configdRun('iperf restart'); + $socket = @stream_socket_client(InstanceController::$SOCKET_PATH, $error_code, $error_msg); + if (!$socket) { + return array('state' => 'error', 'code' => $error_code, 'msg' => $error_msg); + } + } + fwrite($socket, "$command\n"); + $data = fgets($socket); + fwrite($socket, "bye\n"); + fgets($socket); + fclose($socket); + return json_decode($data,true); + + } + private function get_real_interface_name($name) { + $config = Config::getInstance()->toArray(); + if (isset($config['interfaces'][$name])) { + return $config['interfaces'][$name]['if']; + } + return null; + } +} diff --git a/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/Api/ServiceController.php b/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/Api/ServiceController.php new file mode 100644 index 000000000..435a1b2c9 --- /dev/null +++ b/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/Api/ServiceController.php @@ -0,0 +1,68 @@ + 'failed'); + $res = $backend->configdRun('iperf status'); + if (stripos($res, 'is running')) { + $result['result'] = 'running'; + } else { + $result['result'] = 'stopped'; + } + return $result; + } + + public function startAction() + { + $backend = new Backend(); + $result = array('result' => $backend->configdRun('iperf start')); + return $result; + } + + public function stopAction() + { + $backend = new Backend(); + $result = array("result" => $backend->configdRun('iperf stop')); + return $result; + } + + public function restartAction() + { + $this->stopAction(); + return $this->startAction(); + } +} diff --git a/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/IndexController.php b/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/IndexController.php new file mode 100644 index 000000000..8992b913e --- /dev/null +++ b/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/IndexController.php @@ -0,0 +1,45 @@ +view->instance_settings = $this->getForm("instance_settings"); + $this->view->pick('OPNsense/iperf/index'); + } +} diff --git a/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/forms/instance_settings.xml b/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/forms/instance_settings.xml new file mode 100644 index 000000000..3d03b1aa6 --- /dev/null +++ b/net/iperf/src/opnsense/mvc/app/controllers/OPNsense/iperf/forms/instance_settings.xml @@ -0,0 +1,9 @@ +
+ + instance.interface + + dropdown + N + Choose the interface on which the port should be opened. + +
diff --git a/net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/ACL/ACL.xml b/net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/ACL/ACL.xml new file mode 100644 index 000000000..156f61f7d --- /dev/null +++ b/net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + iperf + + ui/iperf/* + api/iperf/* + + + diff --git a/net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.php b/net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.php new file mode 100644 index 000000000..d6c0c2198 --- /dev/null +++ b/net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.php @@ -0,0 +1,37 @@ + + //OPNsense/Iperf3 + Fake model for the API - will be never stored to config (only used for defaults, validation etc.). + + + lan + Y + N + + + diff --git a/net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/Menu/Menu.xml b/net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/Menu/Menu.xml new file mode 100644 index 000000000..6803c24ea --- /dev/null +++ b/net/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/Menu/Menu.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/net/iperf/src/opnsense/mvc/app/views/OPNsense/iperf/index.volt b/net/iperf/src/opnsense/mvc/app/views/OPNsense/iperf/index.volt new file mode 100644 index 000000000..333d97135 --- /dev/null +++ b/net/iperf/src/opnsense/mvc/app/views/OPNsense/iperf/index.volt @@ -0,0 +1,138 @@ +{# + +Copyright © 2017 Fabian Franz +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +#} + + + +
+ {{ partial("layout_partials/base_form",['fields': instance_settings,'id':'instance'])}} +
+
+ +
+
+ +
diff --git a/net/iperf/src/opnsense/scripts/iperf/ruby_iperf.rb b/net/iperf/src/opnsense/scripts/iperf/ruby_iperf.rb new file mode 100755 index 000000000..767a00200 --- /dev/null +++ b/net/iperf/src/opnsense/scripts/iperf/ruby_iperf.rb @@ -0,0 +1,211 @@ +#!/usr/local/bin/ruby + +=begin +Copyright (C) 2017 Fabian Franz +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +=end + +require 'open3' +require 'pp' +require 'json' +require 'logger' +require 'rexml/document' +require 'timeout' +require 'socket' + +$instances = {} +SOCKET_FILE = '/var/run/iperf-manager.sock' +ONE_HOUR = 3600 +KEY_START_TIME = 'start_time' +KEY_PORT = 'port' +LF = "\n" + +def execute_firewall_port(rule) + Open3.popen3('pfctl -a iperf -f -') do |stdin, stdout, stderr, wait_thr| + stdin.puts rule + stdin.close + stdout.close + stderr.close + end +end + +def flush_firewall_rules + `pfctl -a iperf -F rules` +end + +def create_open_firewall_port_rule(interface, port, log = 'log') + "pass in #{log} quick on #{interface} inet proto tcp from {any} to {(self)} port {#{port}} keep state" +end + +def gen_firewall_rules + rules = '' + $instances.each do |thread, entry| + pp (["thread alive?", thread.alive?, entry]) + if thread.alive? + if entry.has_key?('interface') && entry.has_key?('port') && !entry.has_key?('result') + rules << create_open_firewall_port_rule(entry['interface'], entry['port']) << LF + end + end + end + execute_firewall_port(rules) if rules.length > 0 +end + +def open_ports + `sockstat -l`.lines.map do |x| + # scan for integers after : + x.scan(/.*:(\d+).*/)&.first&.first&.to_i + end.uniq.reject(&:nil?).sort +end + +def forwarded_ports + config = REXML::Document.new(File.new("/conf/config.xml")) + xml_firewall = config.elements['opnsense/nat'].children.select do |x| + x.node_type == :element && x.name == 'rule' + end + xml_firewall.map do |x| + x&.elements['local-port']&.text&.to_i + end.sort.uniq + +end + +def find_open_ports + ports = (1024..65000).to_a + # remove the ports open by the firewall itself + begin + ports -= open_ports + rescue + print $! + end +# remove the nat ports + begin + ports -= forwarded_ports + rescue + print $! + end + ports +end + +def find_open_port + find_open_ports.sample +end + +def run_iperf3(port) + output = pid = exit_status = '' + Open3.popen3(['iperf3', '-J', '-f', 'M', '-V', '-s', '-1', '-p', port].join ' ') do |stdin, stdout, stderr, wait_thr| + pid = wait_thr.pid # pid of the started process. + stdin.close + exit_status = wait_thr.value + output = JSON.parse(stdout.read) + stdout.close + stderr.close + end + output +end + +def run_test(interface = 'any', data) + ret = nil + data[KEY_PORT] = port = find_open_port + # regenerate ruleset + flush_firewall_rules + gen_firewall_rules + # do perform test + begin + # timeout 10 min + begin + Timeout.timeout(600) do + data['result'] = ret = run_iperf3 port + end + rescue Timeout::Error + data['result'] = ret = [-1,{'error' => 'timeout'}] + end + rescue + puts $! + end + # end perform test + # regenerate ruleset + flush_firewall_rules + gen_firewall_rules + ret +end + +def run_test_thread(interface = 'any') + data = {} + t = Thread.new do + data[KEY_START_TIME] = Time.now + data['interface'] = interface + run_test(interface, data) + end + $instances[t] = data +end + +Thread.new do + loop do + $instances.each do |key, value| + current_time = Time.now + if (current_time - value[KEY_START_TIME]) > ONE_HOUR + $instances.delete(key) + key.kill unless key.stop? + end + end + sleep 10 + end +end + +# delete stale socket file +File.unlink(SOCKET_FILE) if File.exist? SOCKET_FILE + +server = UNIXServer.new(SOCKET_FILE) +begin + loop do + Thread.start(server.accept) do |connection| + until connection.closed? + begin + command = connection.gets.strip.split(' ') + case command.shift + when 'start' + interface = 'any' + if command.length > 0 + intf = command.shift + # check if a valid interface was given + interface = intf if intf =~ /^[a-z0-9_-]+$/ + end + data = run_test_thread interface + connection.puts '{"status": "queued job"}' + when 'query' + connection.puts $instances.values.to_json + when 'bye' + connection.puts '{"status": "disconnecting"}' + connection.close + else + connection.puts '{"status": "unknown command"}' + end + rescue + end + end + end + end +rescue + server.close +end + diff --git a/net/iperf/src/opnsense/service/conf/actions.d/actions_iperf.conf b/net/iperf/src/opnsense/service/conf/actions.d/actions_iperf.conf new file mode 100644 index 000000000..ba921496d --- /dev/null +++ b/net/iperf/src/opnsense/service/conf/actions.d/actions_iperf.conf @@ -0,0 +1,23 @@ +[start] +command:/usr/local/etc/rc.d/iperf start +parameters: +type:script +message:starting iperf daemon + +[stop] +command:/usr/local/etc/rc.d/iperf stop +parameters: +type:script +message:stopping iperf daemon + +[restart] +command:/usr/local/etc/rc.d/iperf restart +parameters: +type:script +message:restarting iperf daemon + +[status] +command:/usr/local/etc/rc.d/iperf status +parameters: +type:script_output +message:request iperf daemon status