diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f54090a6b9..b77b481693 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,11 +10,9 @@ assignees: '' **Important notices** Before you add a new report, we ask you kindly to acknowledge the following: -[-] I have read the contributing guide lines at https://github.com/opnsense/plugins/blob/master/CONTRIBUTING.md - -[-] I have searched the existing issues and I'm convinced that mine is new. - -[-] The title contains the plugin to which this issue belongs +- [ ] I have read the contributing guide lines at https://github.com/opnsense/plugins/blob/master/CONTRIBUTING.md +- [ ] I have searched the existing issues, open and closed, and I'm convinced that mine is new. +- [ ] The title contains the plugin to which this issue belongs **Describe the bug** A clear and concise description of what the bug is, including last known working version (if any). @@ -44,6 +42,6 @@ Add any other context about the problem here. Software version used and hardware type if relevant. e.g.: -OPNsense 19.1.1 (amd64, OpenSSL). +OPNsense 23.7.8 (amd64). Intel® Xeon™ E3-1225V5 3.3Ghz Quad Core Network Intel® I210-AT diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index c96c1c7467..fe3c8fca51 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -10,11 +10,9 @@ assignees: '' **Important notices** Before you add a new report, we ask you kindly to acknowledge the following: -[-] I have read the contributing guide lines at https://github.com/opnsense/plugins/blob/master/CONTRIBUTING.md - -[-] I have searched the existing issues and I'm convinced that mine is new. - -[-] When the request is meant for an existing plugin, I've added its name to the title. +- [ ] I have read the contributing guide lines at https://github.com/opnsense/plugins/blob/master/CONTRIBUTING.md +- [ ] I have searched the existing issues, open and closed, and I'm convinced that mine is new. +- [ ] When the request is meant for an existing plugin, I've added its name to the title. **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 6152184ac3..b4d4daf5e5 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -13,6 +13,5 @@ Our forum is located at https://forum.opnsense.org , please consider joining dis Before you ask a new question, we ask you kindly to acknowledge the following: -[-] I have read the contributing guide lines at https://github.com/opnsense/plugins/blob/master/CONTRIBUTING.md - -[-] I have searched the existing issues and I'm convinced that mine is new. +- [ ] I have read the contributing guide lines at https://github.com/opnsense/plugins/blob/master/CONTRIBUTING.md +- [ ] I have searched the existing issues, open and closed, and I'm convinced that mine is new. diff --git a/.gitignore b/.gitignore index 855a6c3936..6213804b6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.pyc +.*DS_Store .idea +.sass-cache venv /*/*/work diff --git a/Keywords/sample.ucl b/Keywords/sample.ucl new file mode 100644 index 0000000000..ea5bf5db18 --- /dev/null +++ b/Keywords/sample.ucl @@ -0,0 +1,57 @@ +# MAINTAINER: portmgr@FreeBSD.org +# +# @sample etc/somefile.conf.sample +# or +# @sample file1 file2 +# +# Where file1 is considered as a sample file and file2 the target file +# +# This will install the somefile.conf.sample and automatically copy to +# somefile.conf if it doesn't exist. On deinstall it will remove the +# somefile.conf if it still matches the sample, otherwise it is +# kept. +# +# This replaces the old pattern: +# @unexec if cmp -s %D/etc/pkgtools.conf %D/etc/pkgtools.conf.sample; then rm -f %D/etc/pkgtools.conf; fi +# etc/pkgtools.conf.sample +# @exec [ -f %B/pkgtools.conf ] || cp %B/%f %B/pkgtools.conf + +actions: [file(1)] +arguments: true +post-install: < +Copyright (c) 2023-2025 A. Kulikov +Copyright (c) 2015-2025 Ad Schellevis +Copyright (c) 2022 agh1467 +Copyright (c) 2024 Alex Smith +Copyright (c) 2021 Alexander Noack Copyright (c) 2021 Andreas Stuerz -Copyright (c) 2019 Cloudfence - Julio Camargo (JCC) +Copyright (c) 2025 Andy Binder +Copyright (c) 2024 AnShen +Copyright (c) 2025 Anton Avramov +Copyright (c) 2025 Arcan Consulting +Copyright (c) 2021 Axelrtgs +Copyright (c) 2023 Bernhard Frenking +Copyright (c) 2023 Cannon Matthews +Copyright (c) 2023-2025 Cedrik Pischem +Copyright (c) 2025 Christopher Linn, BackendMedia IT-Services GmbH Copyright (c) 2005-2006 Colin Smith -Copyright (c) 2020 D. Domig -Copyright (c) 2011 Dan Myers +Copyright (c) 2021 Dan Lundqvist +Copyright (c) 2021 David Berry Copyright (c) 2017-2018 David Harrigan Copyright (c) 2021 David Hughes -Copyright (c) 2014-2021 Deciso B.V. -Copyright (c) 2008 Donovan Schonknecht -Copyright (c) 2016-2019 EURO-LOG AG +Copyright (c) 2014-2025 Deciso B.V. +Copyright (c) 2020 devNan0 +Copyright (c) 2023 Dmitry Shinkaruk +Copyright (c) 2024 DollarSign23 Copyright (c) 2006 Eric Friesen -Copyright (c) 2008-2010 Ermal Luçi -Copyright (c) 2017-2019 Fabian Franz +Copyright (c) 2008-2014 Ermal Luçi +Copyright (c) 2016-2019 EURO-LOG AG +Copyright (c) 2017-2020 Fabian Franz Copyright (c) 2019 Felix Matouschek -Copyright (c) 2014-2021 Franco Fichtner -Copyright (c) 2016-2021 Frank Wall +Copyright (c) 2025 Florian Latifi +Copyright (c) 2024 Francisco Dimattia +Copyright (c) 2014-2025 Franco Fichtner +Copyright (c) 2016-2025 Frank Wall Copyright (c) 2021 Github-jjw +Copyright (c) 2023 Greg Glockner +Copyright (c) 2024 Hasan Ucak +Copyright (c) 2023 Ingo Lafrenz Copyright (c) 2016 IT-assistans Sverige AB -Copyright (c) 2021 Jan Winkler +Copyright (c) 2021-2023 Jan Winkler +Copyright (c) 2023-2026 Jeremy Gutierrez Copyright (c) 2010 Jim Pingle -Copyright (c) 2015 Jos Schellevis +Copyright (c) 2015 Jos Schellevis +Copyright (c) 2025 Joseph Bauser Copyright (c) 2018 João Vilaça -Copyright (c) 2019 Juergen Kellerer -Copyright (c) 2020 Manuel Faux +Copyright (c) 2019-2022 Juergen Kellerer +Copyright (c) 2024 laraveluser +Copyright (c) 2026 Leandro Scardua +Copyright (c) 2023 Liam Steckler +Copyright (c) 2020-2021 Manuel Faux Copyright (c) 2021 Manuel Hofmann -Copyright (c) 2003-2004 Manuel Kasper -Copyright (c) 2020 Marc Leuser +Copyright (c) 2003-2005 Manuel Kasper +Copyright (c) 2023 Marc Bartelt Copyright (c) 2021 Marcel Koepfli Copyright (c) 2021 Markus Peter +Copyright (c) 2022 Markus Reiter Copyright (c) 2020 Martin Wasley -Copyright (c) 2017-2021 Michael Muenz +Copyright (c) 2022 Marvo2011 +Copyright (c) 2025 Matthias Valvekens +Copyright (c) 2025 Maxime Thiebaut +Copyright (c) 2017-2025 Michael Muenz +Copyright (c) 2024 Michał Brzeziński +Copyright (c) 2024 Mike Shuey +Copyright (c) 2023-2024 Mikhail Kharisov +Copyright (c) 2023 mleinart +Copyright (c) 2024 MVZ Labor Ludwigsburg GbR +Copyright (c) 2025 Neil Merchant +Copyright (c) 2025 NetBird GmbH +Copyright (c) 2025 Nick Card +Copyright (c) 2021-2024 Nicola Pellegrini +Copyright (c) 2022 Nikolaj Brinch Jørgensen Copyright (c) 2021 Nim G +Copyright (c) 2023 Oliver Hartl +Copyright (c) 2025 Oliver Traber +Copyright (c) 2024 Olly Baker +Copyright (c) 2019 Pascal Mathis +Copyright (c) 2025 Peter Vos +Copyright (c) 2025 Ralph Moser, PJ Monitoring GmbH +Copyright (c) 2024 realizelol +Copyright (c) 2025 Renat Gorbushin +Copyright (c) 2022 Robbert Rijkse +Copyright (c) 2023 sattamjh Copyright (c) 2004-2012 Scott Ullrich -Copyright (c) 2010 Seth Mos +Copyright (c) 2010-2012 Seth Mos +Copyright (c) 2024 Sheridan Computers Copyright (c) 2008 Shrew Soft Inc. -Copyright (c) 2017-2019 Smart-Soft -Copyright (c) 2013 Stanley P. Miller \ stan-qaz +Copyright (c) 2017-2018 Smart-Soft +Copyright (c) 2025 sourceforge807 +Copyright (c) 2025 squared GmbH Copyright (c) 2020 Starkstromkonsument +Copyright (c) 2023-2024 Thomas Cekal +Copyright (c) 2026 Thomas Moore Copyright (c) 2020 Tobias Boehnert -Copyright (c) 2010 Yehuda Katz +Copyright (c) 2024 txr13 +Copyright (c) 2024 W516 +Copyright (c) 2022 Wouter Deurholt +Copyright (c) 2025 Yann Bayart +Copyright (c) 2025 Yann Demoulin Copyright (c) 2015 YoungJoo.Kim -Copyright (c) 2020 devNan0 All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/Makefile b/Makefile index 76fc96735e..abd83c36f1 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Copyright (c) 2015-2020 Franco Fichtner +# Copyright (c) 2015-2025 Franco Fichtner # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -23,34 +23,43 @@ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. -all: - @cat ${.CURDIR}/README.md | ${PAGER} +PLUGINSDIR?= ${.CURDIR} -.include "Mk/defaults.mk" - -CATEGORIES!= ls -1d [a-z0-9]* -CATEGORIES:= ${CATEGORIES:Nruleset.xml} +_CATEGORIES!= ls -1d [a-z0-9]* +CATEGORIES?= ${_CATEGORIES} .for CATEGORY in ${CATEGORIES} _${CATEGORY}!= ls -1d ${CATEGORY}/* PLUGIN_DIRS+= ${_${CATEGORY}} .endfor +all: + @cat ${.CURDIR}/README.md | ${PAGER} + +.include "Mk/defaults.mk" +.include "Mk/common.mk" +.include "Mk/git.mk" + list: .for PLUGIN_DIR in ${PLUGIN_DIRS} @echo ${PLUGIN_DIR} -- $$(${MAKE} -C ${PLUGIN_DIR} -v PLUGIN_COMMENT) \ - $$(if [ -n "$$(${MAKE} -C ${PLUGIN_DIR} -v PLUGIN_DEVEL _PLUGIN_DEVEL=)" ]; then echo "(development only)"; fi) + $$(if [ "$$(${MAKE} -C ${PLUGIN_DIR} -v PLUGIN_MAINTAINER)" = "N/A" ]; then echo "(not maintained)"; fi) \ + $$(if [ -n "$$(${MAKE} -C ${PLUGIN_DIR} -v PLUGIN_DEVEL _PLUGIN_DEVEL=)" ]; then echo "(development only)"; fi) \ + $$(if [ -n "$$(${MAKE} -C ${PLUGIN_DIR} -v PLUGIN_OBSOLETE)" ]; then echo "(pending removal)"; fi) .endfor -# shared targets that are sane to run from the root directory -TARGETS= clean lint revision style style-fix style-python sweep test +# known good targets (expanded below) +TARGETS= clean glint lint revision style sweep test -.for TARGET in ${TARGETS} -${TARGET}: -. for PLUGIN_DIR in ${PLUGIN_DIRS} - @echo ">>> Entering ${PLUGIN_DIR}" - @${MAKE} -C ${PLUGIN_DIR} ${TARGET} -. endfor +.for _TARGET in ${.TARGETS} +__TARGET= ${TARGETS:M${_TARGET:C/-.*//}} +. if "${__TARGET}" != "" +${_TARGET}: +. for PLUGIN_DIR in ${PLUGIN_DIRS} + @echo ">>> Entering ${PLUGIN_DIR} with target '${_TARGET}'" + @${MAKE} -C ${PLUGIN_DIR} ${_TARGET} +. endfor +. endif .endfor license: diff --git a/Mk/common.mk b/Mk/common.mk new file mode 100644 index 0000000000..bfd8be9ffd --- /dev/null +++ b/Mk/common.mk @@ -0,0 +1,26 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +.-include "${PLUGINSDIR}/../core/Mk/common.mk" diff --git a/Mk/contrib.mk b/Mk/contrib.mk new file mode 100644 index 0000000000..22f9df38d2 --- /dev/null +++ b/Mk/contrib.mk @@ -0,0 +1,46 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +.for TARGET in ${PLUGIN_CONTRIB} +.if "${ROOT_${TARGET}}" == "" +.error "No ROOT directory set for target: ${TARGET}" +.endif + +# fixup root target dir +ROOT_${TARGET}:=${ROOT_${TARGET}:S/^\/$//} + +install-${TARGET}: + @mkdir -p ${DESTDIR}${ROOT_${TARGET}}/${TARGET} + @tar -C ${TARGET} -cf - . | tar -C ${DESTDIR}${ROOT_${TARGET}}/${TARGET} -xf - + +install: install-${TARGET} + +plist-${TARGET}: + @(cd ${TARGET}; find * -type f) | while read FILE; do \ + echo "${ROOT_${TARGET}}/${TARGET}/$${FILE}"; \ + done + +plist: plist-${TARGET} +.endfor diff --git a/Mk/defaults.mk b/Mk/defaults.mk index b794cebfba..e2eefeb481 100644 --- a/Mk/defaults.mk +++ b/Mk/defaults.mk @@ -1,4 +1,4 @@ -# Copyright (c) 2016-2021 Franco Fichtner +# Copyright (c) 2016-2025 Franco Fichtner # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -34,32 +34,29 @@ PKG= true .endif GIT!= which git || echo true +SCRIPTSDIR= ${PLUGINSDIR}/Scripts +TEMPLATESDIR= ${PLUGINSDIR}/Templates + GITVERSION= ${SCRIPTSDIR}/version.sh _PLUGIN_ARCH!= uname -p PLUGIN_ARCH?= ${_PLUGIN_ARCH} -OPENSSL?= ${LOCALBASE}/bin/openssl - -.if ! defined(PLUGIN_FLAVOUR) -.if exists(${OPENSSL}) -_PLUGIN_FLAVOUR!= ${OPENSSL} version -PLUGIN_FLAVOUR?= ${_PLUGIN_FLAVOUR:[1]} -.else -.warning "Detected 'Base' flavour is not currently supported" -PLUGIN_FLAVOUR?= Base -.endif -.endif - VERSIONBIN= ${LOCALBASE}/sbin/opnsense-version .if exists(${VERSIONBIN}) _PLUGIN_ABI!= ${VERSIONBIN} -a -PLUGIN_ABI?= ${_PLUGIN_ABI} +PLUGIN_ABIS?= ${_PLUGIN_ABI} .else -PLUGIN_ABI?= 21.7 +PLUGIN_ABIS?= 26.1 .endif +PLUGIN_ABI?= ${PLUGIN_ABIS:[1]} + +PLUGIN_MAINS= master main +PLUGIN_MAIN?= ${PLUGIN_MAINS:[1]} +PLUGIN_STABLE?= stable/${PLUGIN_ABI} + PHPBIN= ${LOCALBASE}/bin/php .if exists(${PHPBIN}) @@ -74,7 +71,6 @@ _PLUGIN_PYTHON!=${PYTHONLINK} -V PLUGIN_PYTHON?= ${_PLUGIN_PYTHON:[2]:S/./ /g:[1..2]:tW:S/ //} .endif - .for REPLACEMENT in ABI PHP PYTHON . if empty(PLUGIN_${REPLACEMENT}) . warning Cannot build without PLUGIN_${REPLACEMENT} set @@ -83,12 +79,13 @@ PLUGIN_PYTHON?= ${_PLUGIN_PYTHON:[2]:S/./ /g:[1..2]:tW:S/ //} REPLACEMENTS= PLUGIN_ABI \ PLUGIN_ARCH \ - PLUGIN_FLAVOUR \ + PLUGIN_CONFLICTS \ PLUGIN_HASH \ PLUGIN_MAINTAINER \ PLUGIN_NAME \ PLUGIN_PKGNAME \ PLUGIN_PKGVERSION \ + PLUGIN_TIER \ PLUGIN_VARIANT \ PLUGIN_WWW @@ -98,58 +95,8 @@ SED_REPLACE= # empty SED_REPLACE+= -e "s=%%${REPLACEMENT}%%=${${REPLACEMENT}}=g" .endfor -ARGS= diff mfc - -# handle argument expansion for required targets -.for TARGET in ${.TARGETS} -_TARGET= ${TARGET:C/\-.*//} -.if ${_TARGET} != ${TARGET} -.for ARGUMENT in ${ARGS} -.if ${_TARGET} == ${ARGUMENT} -${_TARGET}_ARGS+= ${TARGET:C/^[^\-]*(\-|\$)//:S/,/ /g} -${TARGET}: ${_TARGET} -.endif -.endfor -${_TARGET}_ARG= ${${_TARGET}_ARGS:[0]} -.endif -.endfor - -ensure-stable: - @if ! git show-ref --verify --quiet refs/heads/stable/${PLUGIN_ABI}; then \ - git update-ref refs/heads/stable/${PLUGIN_ABI} refs/remotes/origin/stable/${PLUGIN_ABI}; \ - git config branch.stable/${PLUGIN_ABI}.merge refs/heads/stable/${PLUGIN_ABI}; \ - git config branch.stable/${PLUGIN_ABI}.remote origin; \ - fi - -diff: ensure-stable - @git diff --stat -p stable/${PLUGIN_ABI} ${.CURDIR}/${diff_ARGS:[1]} - -mfc: ensure-stable -.for MFC in ${mfc_ARGS} -.if exists(${MFC}) - @git diff --stat -p stable/${PLUGIN_ABI} ${.CURDIR}/${MFC} > /tmp/mfc.diff - @git checkout stable/${PLUGIN_ABI} - @git apply /tmp/mfc.diff - @git add ${.CURDIR} - @if ! git diff --quiet HEAD; then \ - git commit -m "${MFC}: sync with master"; \ - fi -.else - @git checkout stable/${PLUGIN_ABI} - @if ! git cherry-pick -x ${MFC}; then \ - git cherry-pick --abort; \ - fi -.endif - @git checkout master -.endfor - -stable: - @git checkout stable/${PLUGIN_ABI} - -master: - @git checkout master - -rebase: - @git checkout stable/${PLUGIN_ABI} - @git rebase -i - @git checkout master +WRKDIR?= ${.CURDIR}/work +MFCDIR?= /tmp/mfc.dir +PKGDIR?= ${WRKDIR}/pkg +WRKSRC?= ${WRKDIR}/src +TESTDIR?= ${.CURDIR}/src/opnsense/mvc/tests diff --git a/Mk/devel.mk b/Mk/devel.mk new file mode 100644 index 0000000000..f1a8bfd79a --- /dev/null +++ b/Mk/devel.mk @@ -0,0 +1,28 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +# This file merely exists on the development +# branch to force plugins to development mode: +PLUGIN_DEVEL?= yes diff --git a/Mk/git.mk b/Mk/git.mk new file mode 100644 index 0000000000..4aded1e189 --- /dev/null +++ b/Mk/git.mk @@ -0,0 +1,32 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +CORE_ABI= ${PLUGIN_ABI} +CORE_ABIS= ${PLUGIN_ABIS} +CORE_MAIN= ${PLUGIN_MAIN} +CORE_MAINS= ${PLUGIN_MAINS} +CORE_STABLE= ${PLUGIN_STABLE} + +.-include "${PLUGINSDIR}/../core/Mk/git.mk" diff --git a/Mk/lint.mk b/Mk/lint.mk new file mode 100644 index 0000000000..ddc3c2ad73 --- /dev/null +++ b/Mk/lint.mk @@ -0,0 +1,26 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +.-include "${PLUGINSDIR}/../core/Mk/lint.mk" diff --git a/Mk/plugins.mk b/Mk/plugins.mk index ac715b3ecf..2c89efc64e 100644 --- a/Mk/plugins.mk +++ b/Mk/plugins.mk @@ -1,4 +1,4 @@ -# Copyright (c) 2015-2021 Franco Fichtner +# Copyright (c) 2015-2025 Franco Fichtner # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -23,14 +23,13 @@ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. +PLUGINSDIR?= ${.CURDIR}/../.. + all: check + @cat ${.CURDIR}/${PLUGIN_DESC} | ${PAGER} .include "defaults.mk" -PLUGINSDIR?= ${.CURDIR}/../.. -SCRIPTSDIR= ${PLUGINSDIR}/Scripts -TEMPLATESDIR= ${PLUGINSDIR}/Templates - .if exists(${GIT}) && exists(${GITVERSION}) PLUGIN_COMMIT!= ${GITVERSION} .else @@ -44,10 +43,18 @@ PLUGIN_SCRIPTS= +PRE_INSTALL +POST_INSTALL \ +PRE_DEINSTALL +POST_DEINSTALL PLUGIN_WWW?= https://opnsense.org/ +PLUGIN_MAINTAINER?= N/A +PLUGIN_LICENSE?= BSD2CLAUSE +PLUGIN_TIER?= 3 PLUGIN_REVISION?= 0 -PLUGIN_REQUIRES= PLUGIN_NAME PLUGIN_VERSION PLUGIN_COMMENT \ - PLUGIN_MAINTAINER +PLUGIN_REQUIRES= PLUGIN_NAME PLUGIN_VERSION PLUGIN_COMMENT + +.include "common.mk" +.include "git.mk" +.include "lint.mk" +.include "style.mk" +.include "sweep.mk" check: .for PLUGIN_REQUIRE in ${PLUGIN_REQUIRES} @@ -56,44 +63,58 @@ check: . endif .endfor +_PLUGIN_COMMENT:= ${PLUGIN_COMMENT} + .if defined(_PLUGIN_DEVEL) PLUGIN_DEVEL?:= ${_PLUGIN_DEVEL} .else -PLUGIN_DEVEL?= yes +.-include "devel.mk" .endif PLUGIN_PREFIX?= os- PLUGIN_SUFFIX?= -devel +.for CONFLICT in ${PLUGIN_CONFLICTS} +PLUGIN_CONFLICTS+= ${CONFLICT}${PLUGIN_SUFFIX} +.endfor + .if !empty(PLUGIN_VARIANTS) PLUGIN_VARIANT?= ${PLUGIN_VARIANTS:[1]} -.endif - -.if !empty(PLUGIN_VARIANT) +.if "${PLUGIN_VARIANT}" == "" +.error Plugin variant cannot be empty +.else PLUGIN_NAME:= ${${PLUGIN_VARIANT}_NAME} .if empty(PLUGIN_NAME) .error Plugin variant '${PLUGIN_VARIANT}' does not exist .endif -.for _PLUGIN_VARIANT in ${PLUGIN_VARIANTS} -PLUGIN_CONFLICTS+= ${${_PLUGIN_VARIANT}_NAME} +.for _PLUGIN_VARIANT in ${PLUGIN_VARIANTS:N${PLUGIN_VARIANT}} +PLUGIN_CONFLICTS+= ${${_PLUGIN_VARIANT}_NAME}${PLUGIN_SUFFIX} \ + ${${_PLUGIN_VARIANT}_NAME} .endfor PLUGIN_DEPENDS+= ${${PLUGIN_VARIANT}_DEPENDS} +.if !empty(${PLUGIN_VARIANT}_COMMENT) +_PLUGIN_COMMENT:= ${${PLUGIN_VARIANT}_COMMENT} +.endif +.endif .endif -PLUGIN_PKGNAMES= ${PLUGIN_PREFIX}${PLUGIN_NAME}${PLUGIN_SUFFIX} \ - ${PLUGIN_PREFIX}${PLUGIN_NAME} -.for CONFLICT in ${PLUGIN_CONFLICTS} -PLUGIN_PKGNAMES+= ${PLUGIN_PREFIX}${CONFLICT}${PLUGIN_SUFFIX} \ - ${PLUGIN_PREFIX}${CONFLICT} -.endfor +.if !empty(PLUGIN_NAME) +PLUGIN_DIR?= ${.CURDIR:S/\// /g:[-2]}/${.CURDIR:S/\// /g:[-1]} +.endif .if "${PLUGIN_DEVEL}" != "" +PLUGIN_CONFLICTS+= ${PLUGIN_NAME} PLUGIN_PKGSUFFIX= ${PLUGIN_SUFFIX} +PLUGIN_TIER= 4 .else +PLUGIN_CONFLICTS+= ${PLUGIN_NAME}${PLUGIN_SUFFIX} PLUGIN_PKGSUFFIX= # empty .endif +PLUGIN_CONFLICTS:= ${PLUGIN_CONFLICTS:S/^/${PLUGIN_PREFIX}/g:O} + PLUGIN_PKGNAME= ${PLUGIN_PREFIX}${PLUGIN_NAME}${PLUGIN_PKGSUFFIX} +PLUGIN_PKGNAMES= ${PLUGIN_CONFLICTS} ${PLUGIN_PKGNAME} .if "${PLUGIN_REVISION}" != "" && "${PLUGIN_REVISION}" != "0" PLUGIN_PKGVERSION= ${PLUGIN_VERSION}_${PLUGIN_REVISION} @@ -105,13 +126,13 @@ manifest: check @echo "name: ${PLUGIN_PKGNAME}" @echo "version: \"${PLUGIN_PKGVERSION}\"" @echo "origin: opnsense/${PLUGIN_PKGNAME}" - @echo "comment: \"${PLUGIN_COMMENT}\"" + @echo "comment: \"${_PLUGIN_COMMENT}\"" @echo "maintainer: \"${PLUGIN_MAINTAINER}\"" @echo "categories: [ \"${.CURDIR:S/\// /g:[-2]}\" ]" @echo "www: \"${PLUGIN_WWW}\"" @echo "prefix: \"${LOCALBASE}\"" @echo "licenselogic: \"single\"" - @echo "licenses: [ \"BSD2CLAUSE\" ]" + @echo "licenses: [ \"${PLUGIN_LICENSE}\" ]" .if defined(PLUGIN_NO_ABI) @echo "arch: \"${OSABIPREFIX:tl}:*:*\"" @echo "abi: \"${OSABIPREFIX}:*:*\"" @@ -127,14 +148,30 @@ manifest: check done @echo "}" .endif + @if [ -f ${WRKSRC}${LOCALBASE}/opnsense/version/${PLUGIN_NAME} ]; then \ + echo "annotations $$(cat ${WRKSRC}${LOCALBASE}/opnsense/version/${PLUGIN_NAME})"; \ + fi -scripts: check scripts-pre scripts-auto scripts-manual scripts-post +# Package scripts generation handling 101: +# +# "auto" generates automatic hooks that a plugin may need in order to +# reload on the fly. ".pre" and ".post" suffixed files can be used to +# extend the auto-generated content. +# +# "manual" overwrites the automatic script and also ignores ".pre" and +# ".post" files since they do not make sense in manual mode. +# +# Furthermore, variable replacement via %%PLUGIN_VAR%% takes place in +# "manual" as well as ".pre" and ".post" scripts provided. + +scripts: check scripts-pre scripts-auto scripts-post scripts-manual scripts-pre: @for SCRIPT in ${PLUGIN_SCRIPTS}; do \ rm -f ${DESTDIR}/$${SCRIPT}; \ if [ -f ${.CURDIR}/$${SCRIPT}.pre ]; then \ - cp ${.CURDIR}/$${SCRIPT}.pre ${DESTDIR}/$${SCRIPT}; \ + sed ${SED_REPLACE} -- ${.CURDIR}/$${SCRIPT}.pre > \ + ${DESTDIR}/$${SCRIPT}; \ fi; \ done @@ -178,40 +215,72 @@ scripts-auto: ${DESTDIR}/+POST_INSTALL; \ done; \ fi + @if [ -d ${.CURDIR}/src/opnsense/scripts/firmware/repos ]; then \ + for FILE in $$(cd ${.CURDIR}/src && find -s \ + opnsense/scripts/firmware/repos -type f); do \ + echo "${LOCALBASE}/$${FILE#.}" >> ${DESTDIR}/+POST_INSTALL; \ + done \ + fi -scripts-manual: +scripts-post: @for SCRIPT in ${PLUGIN_SCRIPTS}; do \ - if [ -f ${.CURDIR}/$${SCRIPT} ]; then \ - cp ${.CURDIR}/$${SCRIPT} ${DESTDIR}/$${SCRIPT}; \ + if [ -f ${.CURDIR}/$${SCRIPT}.post ]; then \ + sed ${SED_REPLACE} -- ${.CURDIR}/$${SCRIPT}.post >> \ + ${DESTDIR}/$${SCRIPT}; \ fi; \ done -scripts-post: +scripts-manual: @for SCRIPT in ${PLUGIN_SCRIPTS}; do \ - if [ -f ${.CURDIR}/$${SCRIPT}.post ]; then \ - cat ${.CURDIR}/$${SCRIPT}.post >> ${DESTDIR}/$${SCRIPT}; \ + if [ -f ${.CURDIR}/$${SCRIPT} ]; then \ + sed ${SED_REPLACE} -- ${.CURDIR}/$${SCRIPT} > \ + ${DESTDIR}/$${SCRIPT}; \ fi; \ done install: check @mkdir -p ${DESTDIR}${LOCALBASE}/opnsense/version - @(cd ${.CURDIR}/src; find * -type f) | while read FILE; do \ - tar -C ${.CURDIR}/src -cpf - $${FILE} | \ + @if [ -d ${.CURDIR}/contrib ]; then ${MAKE} DESTDIR=$$(readlink -f ${DESTDIR}) -C ${.CURDIR}/contrib install; fi + @(cd ${.CURDIR}/src 2> /dev/null && find * -type f) | while read FILE; do \ + tar -C ${.CURDIR}/src -cpf - "$${FILE}" | \ tar -C ${DESTDIR}${LOCALBASE} -xpf -; \ if [ "$${FILE%%.in}" != "$${FILE}" ]; then \ sed -i '' ${SED_REPLACE} "${DESTDIR}${LOCALBASE}/$${FILE}"; \ mv "${DESTDIR}${LOCALBASE}/$${FILE}" "${DESTDIR}${LOCALBASE}/$${FILE%%.in}"; \ + FILE="$${FILE%%.in}"; \ + fi; \ + if [ "$${FILE%%.shadow}" != "$${FILE}" ]; then \ + mv "${DESTDIR}${LOCALBASE}/$${FILE}" \ + "${DESTDIR}${LOCALBASE}/$${FILE%%.shadow}.sample"; \ + fi; \ + if [ "$${FILE%%/*}" == "man" ]; then \ + gzip -cn "${DESTDIR}${LOCALBASE}/$${FILE}" > \ + "${DESTDIR}${LOCALBASE}/$${FILE}.gz"; \ + rm "${DESTDIR}${LOCALBASE}/$${FILE}"; \ fi; \ done @cat ${TEMPLATESDIR}/version | sed ${SED_REPLACE} > "${DESTDIR}${LOCALBASE}/opnsense/version/${PLUGIN_NAME}" plist: check - @(cd ${.CURDIR}/src; find * -type f) | while read FILE; do \ + @(if [ -d ${.CURDIR}/contrib ]; then \ + ${MAKE} -C ${.CURDIR}/contrib plist; \ + fi; \ + (cd ${.CURDIR}/src 2> /dev/null && find * -type f) | while read FILE; do \ if [ -f "$${FILE}.in" ]; then continue; fi; \ - FILE="$${FILE%%.in}"; \ - echo ${LOCALBASE}/$${FILE}; \ - done - @echo "${LOCALBASE}/opnsense/version/${PLUGIN_NAME}" + FILE="$${FILE%%.in}"; PREFIX=""; \ + if [ "$${FILE%%.sample}" != "$${FILE}" ]; then \ + PREFIX="@sample "; \ + elif [ "$${FILE%%.shadow}" != "$${FILE}" ]; then \ + FILE="$${FILE%%.shadow}.sample"; \ + PREFIX="@shadow "; \ + fi; \ + if [ "$${FILE%%/*}" == "man" ]; then \ + FILE="$${FILE}.gz"; \ + fi; \ + echo "$${PREFIX}${LOCALBASE}/$${FILE}"; \ + done; \ + echo "${LOCALBASE}/opnsense/version/${PLUGIN_NAME}" \ + ) | sort description: check @if [ -f ${.CURDIR}/${PLUGIN_DESC} ]; then \ @@ -226,41 +295,37 @@ metadata: check @${MAKE} DESTDIR=${DESTDIR} plist > ${DESTDIR}/plist collect: check - @(cd ${.CURDIR}/src; find * -type f) | while read FILE; do \ - tar -C ${DESTDIR}${LOCALBASE} -cpf - $${FILE} | \ + @(cd ${.CURDIR}/src 2> /dev/null && find * -type f) | while read FILE; do \ + tar -C ${DESTDIR}${LOCALBASE} -cpf - "$${FILE}" | \ tar -C ${.CURDIR}/src -xpf -; \ done remove: check - @(cd ${.CURDIR}/src; find * -type f) | while read FILE; do \ - rm -f ${DESTDIR}${LOCALBASE}/$${FILE}; \ + @(cd ${.CURDIR}/src 2> /dev/null && find * -type f) | while read FILE; do \ + rm -f "${DESTDIR}${LOCALBASE}/$${FILE}"; \ done - @(cd ${.CURDIR}/src; find * -type d -depth) | while read DIR; do \ - if [ -d ${DESTDIR}${LOCALBASE}/$${DIR} ]; then \ - rmdir ${DESTDIR}${LOCALBASE}/$${DIR} 2> /dev/null || true; \ + @(cd ${.CURDIR}/src 2> /dev/null && find * -type d -depth) | while read DIR; do \ + if [ -d "${DESTDIR}${LOCALBASE}/$${DIR}" ]; then \ + rmdir "${DESTDIR}${LOCALBASE}/$${DIR}" 2> /dev/null || true; \ fi; \ done -WRKDIR?=${.CURDIR}/work -WRKSRC?=${WRKDIR}/src -PKGDIR?=${WRKDIR}/pkg - package: check @rm -rf ${WRKSRC} @mkdir -p ${WRKSRC} ${PKGDIR} .for DEP in ${PLUGIN_DEPENDS} @if ! ${PKG} info ${DEP} > /dev/null; then ${PKG} install -yA ${DEP}; fi .endfor - @echo -n ">>> Generating metadata for ${PLUGIN_PKGNAME}-${PLUGIN_PKGVERSION}..." - @${MAKE} DESTDIR=${WRKSRC} metadata - @echo " done" @echo -n ">>> Staging files for ${PLUGIN_PKGNAME}-${PLUGIN_PKGVERSION}..." @${MAKE} DESTDIR=${WRKSRC} install @echo " done" @echo ">>> Generated version info for ${PLUGIN_PKGNAME}-${PLUGIN_PKGVERSION}:" - @cat ${WRKSRC}/usr/local/opnsense/version/${PLUGIN_NAME} + @cat ${WRKSRC}${LOCALBASE}/opnsense/version/${PLUGIN_NAME} + @echo -n ">>> Generating metadata for ${PLUGIN_PKGNAME}-${PLUGIN_PKGVERSION}..." + @${MAKE} DESTDIR=${WRKSRC} metadata + @echo " done" @echo ">>> Packaging files for ${PLUGIN_PKGNAME}-${PLUGIN_PKGVERSION}:" - @${PKG} create -v -m ${WRKSRC} -r ${WRKSRC} \ + @PORTSDIR=${PLUGINSDIR} ${PKG} create -v -m ${WRKSRC} -r ${WRKSRC} \ -p ${WRKSRC}/plist -o ${PKGDIR} upgrade-check: check @@ -272,7 +337,7 @@ upgrade: upgrade-check package ${PKG} delete -fy ${NAME}; \ fi .endfor - @${PKG} add ${PKGDIR}/*.txz + @${PKG} add ${PKGDIR}/*.pkg mount: check mount_unionfs ${.CURDIR}/src ${DESTDIR}${LOCALBASE} @@ -288,101 +353,20 @@ clean: check fi @rm -rf ${.CURDIR}/work -lint-desc: check - @if [ ! -f ${.CURDIR}/${PLUGIN_DESC} ]; then \ - echo ">>> Missing ${PLUGIN_DESC}"; exit 1; \ - fi - -lint-shell: - @for FILE in $$(find ${.CURDIR}/src -name "*.sh" -type f); do \ - if [ "$$(head $${FILE} | grep -cx '#!\/bin\/sh')" == "0" ]; then \ - echo "Missing shebang in $${FILE}"; exit 1; \ - fi; \ - sh -n $${FILE} || exit 1; \ - done - -lint-xml: - @find ${.CURDIR}/src \ - -name "*.xml" -type f -print0 | xargs -0 -n1 xmllint --noout - -lint-exec: check -.for DIR in ${.CURDIR}/src/opnsense/scripts ${.CURDIR}/src/etc/rc.d ${.CURDIR}/src/etc/rc.syshook.d -.if exists(${DIR}) - @find ${DIR} -type f ! -name "*.xml" -print0 | \ - xargs -0 -t -n1 test -x || \ - (echo "Missing executable permission in ${DIR}"; exit 1) -.endif -.endfor - -LINTBIN?= ${.CURDIR}/../../../core/contrib/parallel-lint/parallel-lint - -lint-php: check -.if exists(${LINTBIN}) - @if [ -d ${.CURDIR}/src ]; then ${LINTBIN} src; fi -.else - @find ${.CURDIR}/src \ - ! -name "*.xml" ! -name "*.xml.sample" ! -name "*.eot" \ - ! -name "*.svg" ! -name "*.woff" ! -name "*.woff2" \ - ! -name "*.otf" ! -name "*.png" ! -name "*.js" ! -name "*.md" \ - ! -name "*.scss" ! -name "*.py" ! -name "*.ttf" ! -name "*.txz" \ - ! -name "*.tgz" ! -name "*.xml.dist" ! -name "*.sh" ! -name "bootstrap80.php" \ - -type f -print0 | xargs -0 -n1 php -l -.endif - -lint: lint-desc lint-shell lint-xml lint-exec lint-php - -sweep: check - find ${.CURDIR}/src -type f -name "*.map" -print0 | \ - xargs -0 -n1 rm - if grep -nr sourceMappingURL= ${.CURDIR}/src; then \ - echo "Mentions of sourceMappingURL must be removed"; \ - exit 1; \ - fi - find ${.CURDIR}/src ! -name "*.min.*" ! -name "*.svg" \ - ! -name "*.ser" -type f -print0 | \ - xargs -0 -n1 ${SCRIPTSDIR}/cleanfile - find ${.CURDIR} -type f -depth 1 -print0 | \ - xargs -0 -n1 ${SCRIPTSDIR}/cleanfile +glint: sweep lint revision: - @${SCRIPTSDIR}/revbump.sh ${.CURDIR} - -STYLEDIRS?= src/etc/inc src/opnsense - -style: check - @: > ${.CURDIR}/.style.out -.for STYLEDIR in ${STYLEDIRS} - @if [ -d ${.CURDIR}/${STYLEDIR} ]; then \ - (phpcs --standard=${PLUGINSDIR}/ruleset.xml \ - ${.CURDIR}/${STYLEDIR} || true) > \ - ${.CURDIR}/.style.out; \ - fi -.endfor - @echo -n "Total number of style warnings: " - @grep '| WARNING' ${.CURDIR}/.style.out | wc -l - @echo -n "Total number of style errors: " - @grep '| ERROR' ${.CURDIR}/.style.out | wc -l - @cat ${.CURDIR}/.style.out - @rm ${.CURDIR}/.style.out - -style-fix: check -.for STYLEDIR in ${STYLEDIRS} - @if [ -d ${.CURDIR}/${STYLEDIR} ]; then \ - phpcbf --standard=${PLUGINSDIR}/ruleset.xml \ - ${.CURDIR}/${STYLEDIR} || true; \ - fi -.endfor - -style-python: check - @if [ -d ${.CURDIR}/src ]; then \ - pycodestyle --ignore=E501 ${.CURDIR}/src || true; \ - fi + @MAKE=${MAKE} ${SCRIPTSDIR}/revbump.sh ${.CURDIR} test: check - @if [ -d ${.CURDIR}/src/opnsense/mvc/tests ]; then \ - cd /usr/local/opnsense/mvc/tests && \ - phpunit --configuration PHPunit.xml \ - ${.CURDIR}/src/opnsense/mvc/tests; \ - fi +.if exists(${TESTDIR}) + @cd ${TESTDIR} && phpunit || true; \ + rm -rf ${TESTDIR}/.phpunit.result.cache +.endif + +commit: + @mkdir -p ${MFCDIR} + @/bin/echo -n "${.CURDIR:C/\// /g:[-2]}/${.CURDIR:C/\// /g:[-1]}: " > \ + ${MFCDIR}/.commitmsg && git commit -eF ${MFCDIR}/.commitmsg . .PHONY: check diff --git a/Mk/style.mk b/Mk/style.mk new file mode 100644 index 0000000000..e9dcc839fd --- /dev/null +++ b/Mk/style.mk @@ -0,0 +1,28 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +CORE_PYTHON_DOT=${PLUGIN_PYTHON:C/./&./1} + +.-include "${PLUGINSDIR}/../core/Mk/style.mk" diff --git a/Mk/sweep.mk b/Mk/sweep.mk new file mode 100644 index 0000000000..ced1d0ea34 --- /dev/null +++ b/Mk/sweep.mk @@ -0,0 +1,26 @@ +# Copyright (c) 2025 Franco Fichtner +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +.-include "${PLUGINSDIR}/../core/Mk/sweep.mk" diff --git a/README.md b/README.md index 9bc15ce244..fa5c613514 100644 --- a/README.md +++ b/README.md @@ -35,40 +35,44 @@ devel/debug -- Debugging Tools devel/grid_example -- A sample framework application devel/helloworld -- A sample framework application dns/bind -- BIND domain name service +dns/ddclient -- Dynamic DNS client dns/dnscrypt-proxy -- Flexible DNS proxy supporting DNSCrypt and DoH -dns/dyndns -- Dynamic DNS Support dns/rfc2136 -- RFC-2136 Support emulators/qemu-guest-agent -- QEMU Guest Agent for OPNsense ftp/tftp -- TFTP server -mail/fetchmail -- Remote-mail retrieval utility mail/postfix -- SMTP mail relay mail/rspamd -- Protect your network from spam -misc/theme-cicada -- The cicada theme - dark grey +misc/theme-advanced -- Theme based on AdvancedTomato GUI +misc/theme-cicada -- The cicada theme - dark grey onyx +misc/theme-flexcolor -- Theme with 3 different color schemes: black as default, light and dark-light misc/theme-rebellion -- A suitably dark theme misc/theme-tukan -- The tukan theme - blue/white -misc/theme-vicuna -- The vicuna theme - dark anthrazit +misc/theme-vicuna -- The vicuna theme - blue sapphire net/chrony -- Chrony time synchronisation -net/firewall -- Firewall API supplemental package net/freeradius -- RADIUS Authentication, Authorization and Accounting Server net/frr -- The FRRouting Protocol Suite net/ftp-proxy -- Control ftp-proxy processes net/google-cloud-sdk -- Google Cloud SDK net/haproxy -- Reliable, high performance TCP/HTTP load balancer -net/igmp-proxy -- IGMP-Proxy Service +net/igmp-proxy -- IGMP-Proxy Service (not maintained) +net/isc-dhcp -- ISC DHCPv4/v6 server net/mdns-repeater -- Proxy multicast DNS between networks +net/ndp-proxy-go -- IPv6 Neighbor Discovery Protocol (NDP) Proxy +net/ndproxy -- Neighbor Discovery Proxy net/ntopng -- Traffic Analysis and Flow Collection net/radsecproxy -- RADIUS proxy provides both RADIUS UDP and TCP/TLS (RadSec) transport net/realtek-re -- Realtek re(4) vendor driver net/relayd -- Relayd Load Balancer net/shadowsocks -- Secure socks5 proxy net/siproxd -- Siproxd is a proxy daemon for the SIP protocol +net/sslh -- sslh configuration front-end net/tayga -- Tayga NAT64 -net/udpbroadcastrelay -- Control ubpbroadcastrelay processes -net/upnp -- Universal Plug and Play Service -net/vnstat -- vnStat is a console-based network traffic monitor -net/wireguard -- WireGuard VPN service -net/wol -- Wake on LAN Service -net/zerotier -- Virtual Networks That Just Work +net/turnserver -- The coturn STUN/TURN Server +net/udpbroadcastrelay -- Control udpbroadcastrelay processes +net/upnp -- UPnP IGD & PCP/NAT-PMP Service +net/vnstat -- Network traffic monitor +net/wol -- Wake on LAN Service (not maintained) +net/zerotier -- Virtual Networks That Just Work (not maintained) net-mgmt/collectd -- Collect system and application performance metrics periodically net-mgmt/lldpd -- LLDP allows you to know exactly on which port is a server net-mgmt/net-snmp -- Net-SNMP is a daemon for the SNMP protocol @@ -79,40 +83,51 @@ net-mgmt/zabbix-agent -- Zabbix monitoring agent net-mgmt/zabbix-proxy -- Zabbix monitoring proxy security/acme-client -- ACME Client security/clamav -- Antivirus engine for detecting malicious threats +security/crowdsec -- Lightweight and collaborative security engine security/etpro-telemetry -- ET Pro Telemetry Edition -security/intrusion-detection-content-et-open -- IDS Proofpoint ET open ruleset complementary subset for ET Pro Telemetry edition +security/intrusion-detection-content-et-open -- IDS Proofpoint full ET open ruleset complementary subset for ET Pro Telemetry edition security/intrusion-detection-content-et-pro -- IDS Proofpoint ET Pro ruleset (needs a valid subscription) -security/intrusion-detection-content-pt-open -- IDS PT Research ruleset (only for non-commercial use) +security/intrusion-detection-content-pt-open -- IDS Positive Technologies ESC ruleset security/intrusion-detection-content-snort-vrt -- IDS Snort VRT ruleset (needs registration or subscription) security/maltrail -- Malicious traffic detection system +security/netbird -- Peer-to-peer VPN that seamlessly connects your devices security/openconnect -- OpenConnect Client -security/softether -- Cross-platform Multi-protocol VPN Program (development only) +security/openvpn-legacy -- OpenVPN legacy support +security/q-feeds-connector -- Connector for Q-Feeds threat intel +security/strongswan-legacy -- IPsec legacy support security/stunnel -- Stunnel TLS proxy +security/tailscale -- VPN mesh securely connecting clients using WireGuard security/tinc -- Tinc VPN security/tor -- The Onion Router -sysutils/api-backup -- Provide the functionality to download the config.xml -sysutils/apuled -- PC Engine APU LED control (development only) -sysutils/boot-delay -- Apply a persistent 10 second boot delay -sysutils/dmidecode -- Display hardware information on the dashboard +security/wazuh-agent -- Agent for the open source security platform Wazuh +sysutils/apcupsd -- APCUPSD - APC UPS daemon +sysutils/beats -- Send logs, network, metrics and heartbeat to Elasticsearch +sysutils/cpu-microcode -- CPU microcode updates +sysutils/dec-hw -- Deciso hardware specific information +sysutils/dmidecode -- Display hardware information on the dashboard (not maintained) +sysutils/gdrive-backup -- Backup configurations using Google Drive sysutils/git-backup -- Track config changes using git sysutils/hw-probe -- Collect hardware diagnostics -sysutils/lcdproc-sdeclcd -- LCDProc for SDEC LCD devices +sysutils/lcdproc-sdeclcd -- LCDProc for SDEC LCD devices (not maintained) sysutils/mail-backup -- Send configuration file backup by e-mail sysutils/munin-node -- Munin monitoring agent -sysutils/nextcloud-backup -- Track config changes using NextCloud +sysutils/nextcloud-backup -- Track config changes using NextCloud (not maintained) sysutils/node_exporter -- Prometheus exporter for machine metrics sysutils/nut -- Network UPS Tools sysutils/puppet-agent -- Manage Puppet Agent -sysutils/smart -- SMART tools +sysutils/sftp-backup -- Backup configurations using SFTP +sysutils/smart -- SMART tools (not maintained) sysutils/virtualbox -- VirtualBox guest additions sysutils/vmware -- VMware tools sysutils/xen -- Xen guest utilities -vendor/sunnyvalley -- Vendor repository for Sensei (Next Generation Firewall Extensions) +vendor/sunnyvalley -- Vendor Repository for Zenarmor - Enterprise SASE & SSE platform (NGFW, SWG, CASB, ZTNA, SD-WAN) +www/OPNProxy -- OPNsense proxy additions (not maintained) www/c-icap -- c-icap connects the web proxy with a virus scanner www/cache -- Webserver cache +www/caddy -- Modern Reverse Proxy with Automatic HTTPS, Dynamic DNS and Layer4 Routing www/nginx -- Nginx HTTP server and reverse proxy -www/web-proxy-sso -- Kerberos authentication module -www/web-proxy-useracl -- Group and user ACL for the web proxy +www/squid -- Squid is a caching proxy for the web (not maintained) +www/web-proxy-sso -- Kerberos authentication module (not maintained) ``` A brief description of how to use the plugins repository @@ -137,9 +152,8 @@ The make targets for the root directory: * clean: remove all changes and unknown files * lint: run syntax checks * list: print a list of all plugin directories with comments -* style-fix: apply style fixes * style: run style checks -* sweep: apply whitespace fixes +* sweep: apply style fixes The make targets for any plugin directory: @@ -150,6 +164,5 @@ The make targets for any plugin directory: * package: creates a package * upgrade: upgrades existing package * remove: remove known files from target directory -* style-fix: apply style fixes * style: run style checks -* sweep: apply whitespace fixes +* sweep: apply style fixes diff --git a/Scripts/cleanfile b/Scripts/cleanfile deleted file mode 100755 index 8abf76025b..0000000000 --- a/Scripts/cleanfile +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env perl -# -# Clean a text file -- or directory of text files -- of stealth whitespace. -# WARNING: this can be a highly destructive operation. Use with caution. -# - -use bytes; -use File::Basename; - -# Default options -$max_width = 0; - -# Clean up space-tab sequences, either by removing spaces or -# replacing them with tabs. -sub clean_space_tabs($) -{ - no bytes; # Tab alignment depends on characters - - my($li) = @_; - my($lo) = ''; - my $pos = 0; - my $nsp = 0; - my($i, $c); - - for ($i = 0; $i < length($li); $i++) { - $c = substr($li, $i, 1); - if ($c eq "\t") { - my $npos = ($pos+$nsp+8) & ~7; - my $ntab = ($npos >> 3) - ($pos >> 3); - $lo .= "\t" x $ntab; - $pos = $npos; - $nsp = 0; - } elsif ($c eq "\n" || $c eq "\r") { - $lo .= " " x $nsp; - $pos += $nsp; - $nsp = 0; - $lo .= $c; - $pos = 0; - } elsif ($c eq " ") { - $nsp++; - } else { - $lo .= " " x $nsp; - $pos += $nsp; - $nsp = 0; - $lo .= $c; - $pos++; - } - } - $lo .= " " x $nsp; - return $lo; -} - -# Compute the visual width of a string -sub strwidth($) { - no bytes; # Tab alignment depends on characters - - my($li) = @_; - my($c, $i); - my $pos = 0; - my $mlen = 0; - - for ($i = 0; $i < length($li); $i++) { - $c = substr($li,$i,1); - if ($c eq "\t") { - $pos = ($pos+8) & ~7; - } elsif ($c eq "\n") { - $mlen = $pos if ($pos > $mlen); - $pos = 0; - } else { - $pos++; - } - } - - $mlen = $pos if ($pos > $mlen); - return $mlen; -} - -$name = basename($0); - -@files = (); - -while (defined($a = shift(@ARGV))) { - if ($a =~ /^-/) { - if ($a eq '-width' || $a eq '-w') { - $max_width = shift(@ARGV)+0; - } else { - print STDERR "Usage: $name [-width #] files...\n"; - exit 1; - } - } else { - push(@files, $a); - } -} - -foreach $f ( @files ) { - print STDERR "$name: $f\n"; - - if (! -f $f) { - print STDERR "$f: not a file\n"; - next; - } - - if (!open(FILE, '+<', $f)) { - print STDERR "$name: Cannot open file: $f: $!\n"; - next; - } - - binmode FILE; - - # First, verify that it is not a binary file; consider any file - # with a zero byte to be a binary file. Is there any better, or - # additional, heuristic that should be applied? - $is_binary = 0; - - while (read(FILE, $data, 65536) > 0) { - if ($data =~ /\0/) { - $is_binary = 1; - last; - } - } - - if ($is_binary) { - print STDERR "$name: $f: binary file\n"; - next; - } - - seek(FILE, 0, 0); - - $in_bytes = 0; - $out_bytes = 0; - $blank_bytes = 0; - - @blanks = (); - @lines = (); - $last = "\n"; - $lineno = 0; - - while ( defined($line = ) ) { - $lineno++; - $in_bytes += length($line); - $line =~ s/[ \t\r]*$//; # Remove trailing spaces - $line = clean_space_tabs($line); - $last = $line; - - if ( $line eq "\n" ) { - push(@blanks, $line); - $blank_bytes += length($line); - } else { - push(@lines, @blanks); - $out_bytes += $blank_bytes; - push(@lines, $line); - $out_bytes += length($line); - @blanks = (); - $blank_bytes = 0; - } - - $l_width = strwidth($line); - if ($max_width && $l_width > $max_width) { - print STDERR - "$f:$lineno: line exceeds $max_width characters ($l_width)\n"; - } - } - - if ( chop($last) ne "\n" ) { - # fix missing newline at EOF - push(@lines, "\n"); - # increment input bytes to signal character append - $in_bytes += 1; - } - - # Any blanks at the end of the file are discarded - - if ($in_bytes != $out_bytes) { - # Only write to the file if changed - seek(FILE, 0, 0); - print FILE @lines; - - if ( !defined($where = tell(FILE)) || - !truncate(FILE, $where) ) { - die "$name: Failed to truncate modified file: $f: $!\n"; - } - } - - close(FILE); -} diff --git a/Scripts/license b/Scripts/license index 01d2d97d58..38d61f2602 100755 --- a/Scripts/license +++ b/Scripts/license @@ -67,6 +67,7 @@ sub process_file my $filename = $File::Find::name; return if not -f "$cwd/$filename"; + return if $filename =~ /\/Private\//; my @lines = read_file( "$cwd/$filename" ); my $possibly_bsd; @@ -84,7 +85,7 @@ sub process_file for my $line ( @lines ) { my $copy = $line; next if $line !~ s/copyright\s+\(c\)\s+//i; - $line =~ s/^[."\*\\#\s]+//g; + $line =~ s/^[."\*\\#\s-]+//g; $line =~ s/\s+$//g; chomp $copy; $copy =~ s/^[\*\\#\s]+//g; @@ -93,7 +94,7 @@ sub process_file $start = $1; $start =~ s/[,\s\-]+//g; } - if ( $line =~ s/(\d\d\d\d\s*,?\s+)// ) { + if ( $line =~ s/^(\d\d\d\d\s*,?\s+)// ) { $end = $1; $end =~ s/[,\s]+//g; } @@ -113,7 +114,7 @@ sub process_file find( \&process_file, $src ); -for ( sort keys %copyrights ) { +for ( sort { lc($a) cmp lc($b) } keys %copyrights ) { my $date = $copyrights{$_}[0]; next if $date == 0; $date = join '-', @{ $copyrights{$_} } if $copyrights{$_}[1] != $date; diff --git a/Scripts/revbump.sh b/Scripts/revbump.sh index 31f1fa974f..674fd50c27 100755 --- a/Scripts/revbump.sh +++ b/Scripts/revbump.sh @@ -8,8 +8,8 @@ if [ -z "${DIR}" ]; then DIR=. fi -REV=$(make -C ${DIR} -v PLUGIN_REVISION) -REV=$(expr ${REV} \+ 1) +REV=$(${MAKE} -C ${DIR} -v PLUGIN_REVISION) +REV=$((REV + 1)) grep -v ^PLUGIN_REVISION ${DIR}/Makefile > ${DIR}/Makefile.tmp sed -e "s/^\(PLUGIN_VERSION.*\)/\1%PLUGIN_REVISION= ${REV}/g" \ diff --git a/Templates/version b/Templates/version index 95baa8fe04..82d753f288 100644 --- a/Templates/version +++ b/Templates/version @@ -1,11 +1,12 @@ { "product_abi": "%%PLUGIN_ABI%%", "product_arch": "%%PLUGIN_ARCH%%", + "product_conflicts": "%%PLUGIN_CONFLICTS%%", "product_email": "%%PLUGIN_MAINTAINER%%", - "product_flavour": "%%PLUGIN_FLAVOUR%%", "product_hash": "%%PLUGIN_HASH%%", "product_id": "%%PLUGIN_PKGNAME%%", "product_name": "%%PLUGIN_NAME%%", + "product_tier": "%%PLUGIN_TIER%%", "product_version": "%%PLUGIN_PKGVERSION%%", "product_website": "%%PLUGIN_WWW%%" } diff --git a/benchmarks/iperf/Makefile b/benchmarks/iperf/Makefile index 4c72430699..c3e8860aec 100644 --- a/benchmarks/iperf/Makefile +++ b/benchmarks/iperf/Makefile @@ -1,8 +1,8 @@ PLUGIN_NAME= iperf PLUGIN_VERSION= 1.0 -PLUGIN_REVISION= 1 +PLUGIN_REVISION= 2 PLUGIN_COMMENT= Connection speed tester -PLUGIN_DEPENDS= iperf3 ruby +PLUGIN_DEPENDS= iperf3 ruby rubygem-rexml PLUGIN_MAINTAINER= franz.fabian.94@gmail.com .include "../../Mk/plugins.mk" diff --git a/benchmarks/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.xml b/benchmarks/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.xml index 939f4e21f0..f3254be98e 100644 --- a/benchmarks/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.xml +++ b/benchmarks/iperf/src/opnsense/mvc/app/models/OPNsense/iperf/FakeInstance.xml @@ -1,11 +1,11 @@ - //OPNsense/Iperf3 - Fake model for the API - will be never stored to config (only used for defaults, validation etc.). - - - lan - Y - N - - + //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/benchmarks/iperf/src/opnsense/mvc/app/views/OPNsense/iperf/index.volt b/benchmarks/iperf/src/opnsense/mvc/app/views/OPNsense/iperf/index.volt index 85ca1d448c..8cef80501f 100644 --- a/benchmarks/iperf/src/opnsense/mvc/app/views/OPNsense/iperf/index.volt +++ b/benchmarks/iperf/src/opnsense/mvc/app/views/OPNsense/iperf/index.volt @@ -51,47 +51,55 @@ function result_to_html(elements) { // only if test did already run if ('result' in element) { - var result = element.result, - start = result.start, - connection = start.connected[0], - intervals = result.intervals, - test_end = result.end, - cpu = test_end.cpu_utilization_percent; - // General - output += "

{{ lang._('General') }}

"; - output += ''; - output += table_tr_kv("{{ lang._('Time') }}", start.timestamp.time); - output += table_tr_kv("{{ lang._('Duration') }}", start.test_start.duration); - output += table_tr_kv("{{ lang._('Block Size') }}", start.test_start.blksize); - output += "
"; - // connection - output += "

{{ lang._('Connection') }}

"; - output += ''; - output += table_tr_kv("{{ lang._('Local Host') }}", connection.local_host); - output += table_tr_kv("{{ lang._('Local Port') }}", connection.local_port); - output += table_tr_kv("{{ lang._('Remote Host') }}", connection.remote_host); - output += table_tr_kv("{{ lang._('Remote Port') }}", connection.remote_port); - output += "
"; - // CPU Usage - output += "

{{ lang._('CPU Usage') }}

"; - output += ''; - output += table_tr_kv("{{ lang._('Host Total') }}", cpu.host_total.toFixed(2)); - output += table_tr_kv("{{ lang._('Host User') }}", cpu.host_user.toFixed(2)); - output += table_tr_kv("{{ lang._('Host System') }}", cpu.host_system.toFixed(2)); - output += table_tr_kv("{{ lang._('Remote Total') }}", cpu.remote_total.toFixed(2)); - output += table_tr_kv("{{ lang._('Remote User') }}", cpu.remote_user.toFixed(2)); - output += table_tr_kv("{{ lang._('Remote System') }}", cpu.remote_system.toFixed(2)); - output += "
"; - // performance data - output += "

{{ lang._('Performance Data') }}

"; - output += ''; - var fields = ['sum_sent', 'sum_received']; - output += table_tr_transpose("{{ lang._('Start') }}","start",fields, test_end); - output += table_tr_transpose("{{ lang._('End') }}","end",fields, test_end); - output += table_tr_transpose("{{ lang._('Seconds') }}","seconds",fields, test_end); - output += table_tr_transpose("{{ lang._('Bytes') }}","bytes",fields, test_end); - output += table_tr_transpose("{{ lang._('Bits Per Second') }}","bits_per_second",fields, test_end); - output += "
"; + var result = element.result; + if ('error' in result) { + // We can't assume that any other fields exist when there's an error + output += "

{{ lang._('Error') }}

"; + output += ''; + output += table_tr_kv("{{ lang._('Error message') }}", result.error); + output += "
"; + } else { + var start = result.start, + connection = start.connected[0], + intervals = result.intervals, + test_end = result.end, + cpu = test_end.cpu_utilization_percent; + // General + output += "

{{ lang._('General') }}

"; + output += ''; + output += table_tr_kv("{{ lang._('Time') }}", start.timestamp.time); + output += table_tr_kv("{{ lang._('Duration') }}", start.test_start.duration); + output += table_tr_kv("{{ lang._('Block Size') }}", start.test_start.blksize); + output += "
"; + // connection + output += "

{{ lang._('Connection') }}

"; + output += ''; + output += table_tr_kv("{{ lang._('Local Host') }}", connection.local_host); + output += table_tr_kv("{{ lang._('Local Port') }}", connection.local_port); + output += table_tr_kv("{{ lang._('Remote Host') }}", connection.remote_host); + output += table_tr_kv("{{ lang._('Remote Port') }}", connection.remote_port); + output += "
"; + // CPU Usage + output += "

{{ lang._('CPU Usage') }}

"; + output += ''; + output += table_tr_kv("{{ lang._('Host Total') }}", cpu.host_total.toFixed(2)); + output += table_tr_kv("{{ lang._('Host User') }}", cpu.host_user.toFixed(2)); + output += table_tr_kv("{{ lang._('Host System') }}", cpu.host_system.toFixed(2)); + output += table_tr_kv("{{ lang._('Remote Total') }}", cpu.remote_total.toFixed(2)); + output += table_tr_kv("{{ lang._('Remote User') }}", cpu.remote_user.toFixed(2)); + output += table_tr_kv("{{ lang._('Remote System') }}", cpu.remote_system.toFixed(2)); + output += "
"; + // performance data + output += "

{{ lang._('Performance Data') }}

"; + output += ''; + var fields = ['sum_sent', 'sum_received']; + output += table_tr_transpose("{{ lang._('Start') }}","start",fields, test_end); + output += table_tr_transpose("{{ lang._('End') }}","end",fields, test_end); + output += table_tr_transpose("{{ lang._('Seconds') }}","seconds",fields, test_end); + output += table_tr_transpose("{{ lang._('Bytes') }}","bytes",fields, test_end); + output += table_tr_transpose("{{ lang._('Bits Per Second') }}","bits_per_second",fields, test_end); + output += "
"; + } } } $('#resultcontainer').html(output); diff --git a/databases/redis/Makefile b/databases/redis/Makefile index c633518201..c6639e95e5 100644 --- a/databases/redis/Makefile +++ b/databases/redis/Makefile @@ -1,8 +1,8 @@ PLUGIN_NAME= redis PLUGIN_VERSION= 1.1 -PLUGIN_REVISION= 1 +PLUGIN_REVISION= 3 PLUGIN_COMMENT= Redis DB -PLUGIN_DEPENDS= redis +PLUGIN_DEPENDS= redis72 PLUGIN_MAINTAINER= franz.fabian.94@gmail.com .include "../../Mk/plugins.mk" diff --git a/databases/redis/src/opnsense/mvc/app/models/OPNsense/Redis/Redis.xml b/databases/redis/src/opnsense/mvc/app/models/OPNsense/Redis/Redis.xml index 0750ed1d80..df895e9862 100644 --- a/databases/redis/src/opnsense/mvc/app/models/OPNsense/Redis/Redis.xml +++ b/databases/redis/src/opnsense/mvc/app/models/OPNsense/Redis/Redis.xml @@ -1,108 +1,108 @@ - //OPNsense/redis - Redis DB - - - - 0 - Y - - - N - Y - - - 1 - Y - - - 1 - 65536 - N - 6379 - This must be a valid port number. - - - Y - warning - - Debug - Verbose - Notice - Warning - - - - 0 - Y - - - Y - LOCAL0 - - USER - LOCAL0 - LOCAL1 - LOCAL2 - LOCAL3 - LOCAL4 - LOCAL5 - LOCAL6 - LOCAL7 - - - - 0 - Y - 16 - - - - - N - - + //OPNsense/redis + Redis DB + + + + 0 + Y + + + N + Y + + + 1 + Y + + + 1 + 65536 + N + 6379 + This must be a valid port number. + + + Y + warning + + Debug + Verbose + Notice + Warning + + + + 0 + Y + + + Y + LOCAL0 + + USER + LOCAL0 + LOCAL1 + LOCAL2 + LOCAL3 + LOCAL4 + LOCAL5 + LOCAL6 + LOCAL7 + + + + 0 + Y + 16 + + + + + N + + - - - - 0 - N - 10000 - - - 0 - N - - - Y - noeviction - - noeviction - volatile-ttl - allkeys-random - volatile-random - allkeys-lru - volatile-lru - - - - 0 - N - 5 - - - - - 0 - N - 10000 - - - 0 - N - 128 - - - + + + + 0 + N + 10000 + + + 0 + N + + + Y + noeviction + + noeviction + volatile-ttl + allkeys-random + volatile-random + allkeys-lru + volatile-lru + + + + 0 + N + 5 + + + + + 0 + N + 10000 + + + 0 + N + 128 + + + diff --git a/databases/redis/src/opnsense/service/conf/actions.d/actions_redis.conf b/databases/redis/src/opnsense/service/conf/actions.d/actions_redis.conf index f9183fec73..6c388a70f5 100644 --- a/databases/redis/src/opnsense/service/conf/actions.d/actions_redis.conf +++ b/databases/redis/src/opnsense/service/conf/actions.d/actions_redis.conf @@ -1,5 +1,5 @@ [start] -command:/usr/local/opnsense/scripts/redis/setup.sh;/usr/local/etc/rc.d/redis start +command:/usr/local/etc/rc.d/redis start parameters: type:script message:starting redis @@ -11,19 +11,19 @@ type:script message:stopping redis [restart] -command:/usr/local/opnsense/scripts/redis/setup.sh;/usr/local/etc/rc.d/redis restart +command:/usr/local/etc/rc.d/redis restart parameters: type:script message:restarting redis [status] -command:/usr/local/etc/rc.d/redis status;exit 0 +command:/usr/local/etc/rc.d/redis status; exit 0 parameters: type:script_output message:request redis status [resetdb] -command:/usr/local/etc/rc.d/redis stop;rm -rf /var/db/redis +command:/usr/local/etc/rc.d/redis stop; rm -rf /var/db/redis parameters: type:script message:remove all databases diff --git a/databases/redis/src/opnsense/service/templates/OPNsense/Redis/redis b/databases/redis/src/opnsense/service/templates/OPNsense/Redis/redis index 57fe4d2470..2ec3e47a26 100644 --- a/databases/redis/src/opnsense/service/templates/OPNsense/Redis/redis +++ b/databases/redis/src/opnsense/service/templates/OPNsense/Redis/redis @@ -1,5 +1,5 @@ {% if helpers.exists('OPNsense.redis.general.enabled') and OPNsense.redis.general.enabled == '1' %} -redis_var_script="/usr/local/opnsense/scripts/redis/setup.sh" +redis_setup="/usr/local/opnsense/scripts/redis/setup.sh" redis_enable="YES" {% else %} redis_enable="NO" diff --git a/databases/redis/src/opnsense/service/templates/OPNsense/Redis/redis.conf b/databases/redis/src/opnsense/service/templates/OPNsense/Redis/redis.conf index 6deeeda1ae..8882751ea7 100644 --- a/databases/redis/src/opnsense/service/templates/OPNsense/Redis/redis.conf +++ b/databases/redis/src/opnsense/service/templates/OPNsense/Redis/redis.conf @@ -54,12 +54,8 @@ {% endif %} {% if helpers.exists('virtualip') %} {% for intf_item in helpers.toList('virtualip.vip') %} -{% if intf_item.interface == listen_interface and intf_item.type == 'single' %} -{% if intf_item.subnet.find(':') > -1 %} -{% do listen_ip.append(interface_ip) %} -{% else %} -{% do listen_ip.append(interface_ip) %} -{% endif %} +{% if intf_item.interface == listen_interface and intf_item.mode in ['carp', 'ipalias'] %} +{% do listen_ip.append(intf_item.subnet) %} {% endif %} {% endfor %} {% endif %} diff --git a/devel/debug/Makefile b/devel/debug/Makefile index 52d7ef7886..e60a1f1f5d 100644 --- a/devel/debug/Makefile +++ b/devel/debug/Makefile @@ -1,11 +1,14 @@ PLUGIN_NAME= debug -PLUGIN_VERSION= 1.4 +PLUGIN_VERSION= 1.7 PLUGIN_COMMENT= Debugging Tools PLUGIN_DEPENDS= php${PLUGIN_PHP}-pear-PHP_CodeSniffer \ php${PLUGIN_PHP}-pecl-xdebug \ - phpunit7-php${PLUGIN_PHP} \ + phpunit9-php${PLUGIN_PHP} \ py${PLUGIN_PYTHON}-pycodestyle \ - p5-File-Slurp git + py${PLUGIN_PYTHON}-pytest \ + py${PLUGIN_PYTHON}-scapy \ + p5-File-Slurp git jq PLUGIN_MAINTAINER= franco@opnsense.org +PLUGIN_TIER= 2 .include "../../Mk/plugins.mk" diff --git a/devel/debug/src/bin/qyua b/devel/debug/src/bin/qyua new file mode 100755 index 0000000000..5b3426878a --- /dev/null +++ b/devel/debug/src/bin/qyua @@ -0,0 +1,183 @@ +#!/bin/sh + +# Copyright (c) 2024 Franco Fichtner +# +# 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. + +set -e + +COMPONENT=sys/netpfil/pf + +TESTDIR=/usr/src/tests/${COMPONENT} +DESTDIR=/usr/tests/${COMPONENT} + +if [ "$(id -u)" != "0" ]; then + echo "Must be root." >&2 + exit 1 +fi + +DO_ALL= +DO_BOOTSTRAP= +DO_DEBUG= +DO_LIST= +DO_INJECT=yes + +while getopts abdlrV OPT; do + case ${OPT} in + a) + DO_ALL="-a" + ;; + b) + DO_BOOTSTRAP="-b" + ;; + d) + DO_DEBUG="-d" + ;; + l) + DO_LIST="-l" + ;; + r) + # referenece testing via /usr/tests + unset DO_INJECT + ;; + V) + DO_VERBOSE="-V" + ;; + *) + echo "Usage: man ${0##*/}" >&2 + exit 1 + ;; + esac +done + +shift $((OPTIND - 1)) + +if [ -n "${DO_VERBOSE}" ]; then + set -x +fi + +if [ -n "${DO_BOOTSTRAP}" ]; then + opnsense-update -Q + + # XXX needed for inject mode (default) but not sure if the best way + #opnsense-code src +fi + +if [ -n "${DO_INJECT}" -a ! -d "${TESTDIR}" ]; then + echo "Source directory not found: ${TESTDIR}" >&2 + exit 1 +fi + +if [ ! -d "${DESTDIR}" ]; then + echo "Target directory not found: ${DESTDIR}" >&2 + exit 1 +fi + +# clear from previous run +rm -rf ${DESTDIR}/_* + +list() +{ + if [ -z "${DO_INJECT}" ]; then + cat ${DESTDIR}/Kyuafile | tr '"' ' ' | while read PRE NAME MORE; do + echo ${NAME} + done + else + for TEST in $(find -s ${TESTDIR} -name "*.sh"); do + TEST=$(basename ${TEST}) + echo ${TEST%.sh} + done + fi +} + +if [ -n "${DO_LIST}" ]; then + list + exit 0 +fi + +TESTS=${@} +if [ -n "${DO_ALL}" ]; then + TESTS=$(list) +fi + +if [ -z "${TESTS}" ]; then + echo "Nothing to do." + exit 0 +fi + +# XXX kldload required things now as kyua is powerless (but complains) + +# set up a shadow config +cat > ${DESTDIR}/_Kyuafile << EOF +-- Automatically generated by bsd.test.mk. + +syntax(2) + +test_suite("FreeBSD") + +EOF + +FINAL= + +for TEST in ${TESTS}; do + if [ -n "${DO_INJECT}" ]; then + CASE= + + if [ -n "${DO_DEBUG}" ]; then + if [ -n "${TEST%%*:*}" ]; then + echo "No test case given in debug mode, use '${TEST}:case'" >&2 + exit 1 + fi + + CASE=:${TEST##*:} + TEST=${TEST%%:*} + fi + + if [ ! -f ${TESTDIR}/${TEST}.sh ]; then + echo "Source file not found: ${TESTDIR}/${TEST}.sh" >&2 + exit 1 + fi + + cat >> ${DESTDIR}/_Kyuafile << EOF +atf_test_program{name="_${TEST}", is_exclusive=true} +EOF + + cat > ${DESTDIR}/_${TEST} << EOF +#! /usr/libexec/atf-sh + +$(cat ${TESTDIR}/${TEST}.sh) +EOF + + chmod 555 ${DESTDIR}/_${TEST} + fi + + FINAL="${FINAL} ${DO_INJECT+_}${TEST}${CASE}" +done + +if [ -z "${DO_DEBUG}" ]; then + exec kyua test -k ${DESTDIR}/${DO_INJECT+_}Kyuafile ${FINAL} +else + for TEST in ${FINAL}; do + # only support first one + exec kyua debug -k ${DESTDIR}/${DO_INJECT+_}Kyuafile ${TEST} + done +fi diff --git a/devel/debug/src/etc/php/ext-20-xdebug-settings.ini b/devel/debug/src/etc/php/ext-20-xdebug-settings.ini index 30ab5e7282..7821833984 100644 --- a/devel/debug/src/etc/php/ext-20-xdebug-settings.ini +++ b/devel/debug/src/etc/php/ext-20-xdebug-settings.ini @@ -1,4 +1,4 @@ xdebug.mode = profile; -xdebug.start_with_request = trigger; +xdebug.output_dir = /tmp xdebug.profiler_output_name = cachegrind.out.%t.%p -xdebug.profiler_output_dir = /tmp +xdebug.start_with_request = trigger; diff --git a/devel/debug/src/man/man1/qyua.1 b/devel/debug/src/man/man1/qyua.1 new file mode 100644 index 0000000000..ad9a6e3897 --- /dev/null +++ b/devel/debug/src/man/man1/qyua.1 @@ -0,0 +1,99 @@ +.\" +.\" Copyright (c) 2024 Franco Fichtner +.\" +.\" 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. +.\" +.Dd November 22, 2024 +.Dt QYUA 1 +.Os +.Sh NAME +.Nm qyua +.Nd Quick kyua wrapper +.Sh SYNOPSIS +.Nm +.Op Fl abdlrV +.Op Ar test ... +.Sh DESCRIPTION +The +.Nm +utility is a thin wrapper around +.Xr kyua 1 +in order to run test cases directly from the source tree at +.Pa /usr/src . +In order to do this, +the +.Pa /usr/tests +directory is required, +which is provided by the +.Li tests +set of +.Xr opnsense-update 8 . +The source tree can be installed using +.Xr opnsense-code 8 . +Internally, +.Xr kyua-test 1 +is used to run the given tests. +.Pp +Please note that +.Nm +is only concerned with a single test component, namely +.Pa sys/netpfil/pf . +The focus is the ability to assist with writing tests where +they are being committed/published in the first place without +the need to compile/install anything. +.Pp +The options are as follows: +.Bl -tag -width ".Fl a" -offset indent +.It Fl a +Select all available tests to run. +.It Fl b +Bootstrap mode installs the matching tests set. +.It Fl d +In debug mode, a single test case is selected from the specified +test file using +.Sq Ar file:name . +Internally, this invokes +.Xr kyua-debug 1 +instead. +.It Fl l +List all the tests that can be run. +.It Fl r +In reference mode, run the tests available in +.Pa /usr/tests . +This works without +.Pa /usr/src +being available, but is not the default mode. +.It Fl V +Set debug mode for shell script output. +.El +.Sh EXIT STATUS +.Ex -std +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyua-debug 1 , +.Xr kyua-test 1 , +.Xr opnsense-code 8 , +.Xr opnsense-update 8 +.Sh AUTHORS +.An Franco Fichtner Aq Mt franco@opnsense.org diff --git a/devel/grid_example/Makefile b/devel/grid_example/Makefile index 3b7bda5db9..f6c9214c6c 100644 --- a/devel/grid_example/Makefile +++ b/devel/grid_example/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= grid_example -PLUGIN_VERSION= 1.0 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.1 PLUGIN_COMMENT= A sample framework application PLUGIN_MAINTAINER= ad@opnsense.org diff --git a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/ServiceController.php b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/ServiceController.php new file mode 100644 index 0000000000..00ae072e08 --- /dev/null +++ b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/ServiceController.php @@ -0,0 +1,49 @@ + "ok"]; + } +} diff --git a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/SettingsController.php b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/SettingsController.php index 3cbe4725d4..a7f63c2ddf 100644 --- a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/SettingsController.php +++ b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/SettingsController.php @@ -1,7 +1,7 @@ searchBase("addresses.address", array('enabled', 'email'), "email"); + return $this->searchBase("addresses.address", null, "email"); } public function setItemAction($uuid) diff --git a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/IndexController.php b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/IndexController.php index e3e86e6474..79d674d0b3 100644 --- a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/IndexController.php +++ b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/IndexController.php @@ -1,7 +1,7 @@ view->pick('OPNsense/GridExample/index'); $this->view->formDialogAddress = $this->getForm("dialogAddress"); + // convert dialog for grid table + $this->view->formGridAddress = $this->getFormGrid("dialogAddress"); } } diff --git a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/forms/dialogAddress.xml b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/forms/dialogAddress.xml index 9a65a91a77..74f842bd75 100644 --- a/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/forms/dialogAddress.xml +++ b/devel/grid_example/src/opnsense/mvc/app/controllers/OPNsense/GridExample/forms/dialogAddress.xml @@ -1,13 +1,30 @@
address.enabled - + checkbox Enable this address + + + 6em + boolean + rowtoggle + address.email text + Enter the email address + + + address.description + + text + Enter an optional description + + + false +
diff --git a/devel/grid_example/src/opnsense/mvc/app/models/OPNsense/GridExample/GridExample.xml b/devel/grid_example/src/opnsense/mvc/app/models/OPNsense/GridExample/GridExample.xml index 7208836b36..fc1146dd28 100644 --- a/devel/grid_example/src/opnsense/mvc/app/models/OPNsense/GridExample/GridExample.xml +++ b/devel/grid_example/src/opnsense/mvc/app/models/OPNsense/GridExample/GridExample.xml @@ -7,12 +7,13 @@
- 1 + 1 Y Y +
diff --git a/devel/grid_example/src/opnsense/mvc/app/views/OPNsense/GridExample/index.volt b/devel/grid_example/src/opnsense/mvc/app/views/OPNsense/GridExample/index.volt index 0061bcb4f4..4f0ed8fd52 100644 --- a/devel/grid_example/src/opnsense/mvc/app/views/OPNsense/GridExample/index.volt +++ b/devel/grid_example/src/opnsense/mvc/app/views/OPNsense/GridExample/index.volt @@ -1,5 +1,5 @@ {# - # Copyright (c) 2019 Deciso B.V. + # Copyright (c) 2019-2025 Deciso B.V. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -25,43 +25,29 @@ #} - - - - - - - - - - - - - - - - - - -
{{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Email') }}{{ lang._('Commands') }}
- - -
- - -{{ partial("layout_partials/base_dialog",['fields':formDialogAddress,'id':'DialogAddress','label':lang._('Edit address')])}} +
+ + {{ partial('layout_partials/base_bootgrid_table', formGridAddress) }} +
+ +{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/gridexample/service/reconfigure'}) }} + +{{ partial("layout_partials/base_dialog",['fields':formDialogAddress,'id':formGridAddress['edit_dialog_id'],'label':lang._('Edit address')])}} diff --git a/devel/helloworld/Makefile b/devel/helloworld/Makefile index bf22cc416b..86a8690adb 100644 --- a/devel/helloworld/Makefile +++ b/devel/helloworld/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= helloworld -PLUGIN_VERSION= 1.3 +PLUGIN_VERSION= 1.4 PLUGIN_REVISION= 1 PLUGIN_COMMENT= A sample framework application PLUGIN_MAINTAINER= ad@opnsense.org diff --git a/devel/helloworld/src/opnsense/mvc/app/controllers/OPNsense/HelloWorld/Api/ServiceController.php b/devel/helloworld/src/opnsense/mvc/app/controllers/OPNsense/HelloWorld/Api/ServiceController.php index 6c7073da19..5b068bae43 100644 --- a/devel/helloworld/src/opnsense/mvc/app/controllers/OPNsense/HelloWorld/Api/ServiceController.php +++ b/devel/helloworld/src/opnsense/mvc/app/controllers/OPNsense/HelloWorld/Api/ServiceController.php @@ -46,13 +46,9 @@ public function reloadAction() { $status = "failed"; if ($this->request->isPost()) { - $backend = new Backend(); - $bckresult = trim($backend->configdRun('template reload OPNsense/HelloWorld')); - if ($bckresult == "OK") { - $status = "ok"; - } + $status = strtolower(trim((new Backend())->configdRun('template reload OPNsense/HelloWorld'))); } - return array("status" => $status); + return ["status" => $status]; } /** @@ -61,13 +57,12 @@ public function reloadAction() public function testAction() { if ($this->request->isPost()) { - $backend = new Backend(); - $bckresult = json_decode(trim($backend->configdRun("helloworld test")), true); + $bckresult = json_decode(trim((new Backend())->configdRun("helloworld test")), true); if ($bckresult !== null) { // only return valid json type responses return $bckresult; } } - return array("message" => "unable to run config action"); + return ["message" => "unable to run config action"]; } } diff --git a/devel/helloworld/src/opnsense/mvc/app/controllers/OPNsense/HelloWorld/Api/SettingsController.php b/devel/helloworld/src/opnsense/mvc/app/controllers/OPNsense/HelloWorld/Api/SettingsController.php index 750e1b986e..440ff3ce16 100644 --- a/devel/helloworld/src/opnsense/mvc/app/controllers/OPNsense/HelloWorld/Api/SettingsController.php +++ b/devel/helloworld/src/opnsense/mvc/app/controllers/OPNsense/HelloWorld/Api/SettingsController.php @@ -30,63 +30,14 @@ namespace OPNsense\HelloWorld\Api; -use OPNsense\Base\ApiControllerBase; -use OPNsense\HelloWorld\HelloWorld; -use OPNsense\Core\Config; +use OPNsense\Base\ApiMutableModelControllerBase; /** * Class SettingsController Handles settings related API actions for the HelloWorld module * @package OPNsense\Helloworld */ -class SettingsController extends ApiControllerBase +class SettingsController extends ApiMutableModelControllerBase { - /** - * retrieve HelloWorld general settings - * @return array general settings - * @throws \OPNsense\Base\ModelException - * @throws \ReflectionException - */ - public function getAction() - { - // define list of configurable settings - $result = array(); - if ($this->request->isGet()) { - $mdlHelloWorld = new HelloWorld(); - $result['helloworld'] = $mdlHelloWorld->getNodes(); - } - return $result; - } - - /** - * update HelloWorld settings - * @return array status - * @throws \OPNsense\Base\ModelException - * @throws \ReflectionException - */ - public function setAction() - { - $result = array("result" => "failed"); - if ($this->request->isPost()) { - // load model and update with provided data - $mdlHelloWorld = new HelloWorld(); - $mdlHelloWorld->setNodes($this->request->getPost("helloworld")); - - // perform validation - $valMsgs = $mdlHelloWorld->performValidation(); - foreach ($valMsgs as $field => $msg) { - if (!array_key_exists("validations", $result)) { - $result["validations"] = array(); - } - $result["validations"]["helloworld." . $msg->getField()] = $msg->getMessage(); - } - - // serialize model to config and save - if ($valMsgs->count() == 0) { - $mdlHelloWorld->serializeToConfig(); - Config::getInstance()->save(); - $result["result"] = "saved"; - } - } - return $result; - } + protected static $internalModelClass = 'OPNsense\HelloWorld\HelloWorld'; + protected static $internalModelName = 'helloworld'; } diff --git a/devel/helloworld/src/opnsense/mvc/app/controllers/OPNsense/HelloWorld/Api/SimplifiedSettingsController.php b/devel/helloworld/src/opnsense/mvc/app/controllers/OPNsense/HelloWorld/Api/SimplifiedSettingsController.php deleted file mode 100644 index 72167c2153..0000000000 --- a/devel/helloworld/src/opnsense/mvc/app/controllers/OPNsense/HelloWorld/Api/SimplifiedSettingsController.php +++ /dev/null @@ -1,43 +0,0 @@ - - 1 + 1 Y Y - sample@example.com + sample@example.com Y diff --git a/devel/helloworld/src/opnsense/mvc/app/views/OPNsense/HelloWorld/index.volt b/devel/helloworld/src/opnsense/mvc/app/views/OPNsense/HelloWorld/index.volt index 33255d209b..d09d70e1db 100644 --- a/devel/helloworld/src/opnsense/mvc/app/views/OPNsense/HelloWorld/index.volt +++ b/devel/helloworld/src/opnsense/mvc/app/views/OPNsense/HelloWorld/index.volt @@ -28,14 +28,13 @@ POSSIBILITY OF SUCH DAMAGE. @@ -63,6 +60,6 @@ POSSIBILITY OF SUCH DAMAGE.
- - + +
diff --git a/devel/helloworld/src/opnsense/scripts/OPNsense/HelloWorld/testConnection.py b/devel/helloworld/src/opnsense/scripts/helloworld/testConnection.py similarity index 100% rename from devel/helloworld/src/opnsense/scripts/OPNsense/HelloWorld/testConnection.py rename to devel/helloworld/src/opnsense/scripts/helloworld/testConnection.py diff --git a/devel/helloworld/src/opnsense/service/conf/actions.d/actions_helloworld.conf b/devel/helloworld/src/opnsense/service/conf/actions.d/actions_helloworld.conf index c5c5217933..1bb2afb595 100644 --- a/devel/helloworld/src/opnsense/service/conf/actions.d/actions_helloworld.conf +++ b/devel/helloworld/src/opnsense/service/conf/actions.d/actions_helloworld.conf @@ -1,5 +1,5 @@ [test] -command:/usr/local/opnsense/scripts/OPNsense/HelloWorld/testConnection.py +command:/usr/local/opnsense/scripts/helloworld/testConnection.py parameters: type:script_output message:hello world module test diff --git a/devel/helloworld/src/opnsense/service/templates/OPNsense/HelloWorld/helloworld.conf b/devel/helloworld/src/opnsense/service/templates/OPNsense/HelloWorld/helloworld.conf index 7ccefce079..28199b1465 100644 --- a/devel/helloworld/src/opnsense/service/templates/OPNsense/HelloWorld/helloworld.conf +++ b/devel/helloworld/src/opnsense/service/templates/OPNsense/HelloWorld/helloworld.conf @@ -1,4 +1,4 @@ -{% if helpers.exists('OPNsense.helloworld.general') and OPNsense.helloworld.general.Enabled|default("0") == "1" %} +{% if not helpers.empty('OPNsense.helloworld.general.Enabled') %} [general] SMTPHost={{ OPNsense.helloworld.general.SMTPHost|default("") }} FromEmail={{ OPNsense.helloworld.general.FromEmail|default("") }} diff --git a/dns/bind/Makefile b/dns/bind/Makefile index 025638dbf2..4d98f7a4b3 100644 --- a/dns/bind/Makefile +++ b/dns/bind/Makefile @@ -1,7 +1,8 @@ PLUGIN_NAME= bind -PLUGIN_VERSION= 1.19 +PLUGIN_VERSION= 1.34 +PLUGIN_REVISION= 2 PLUGIN_COMMENT= BIND domain name service -PLUGIN_DEPENDS= bind916 +PLUGIN_DEPENDS= bind920 PLUGIN_MAINTAINER= m.muenz@gmail.com .include "../../Mk/plugins.mk" diff --git a/dns/bind/pkg-descr b/dns/bind/pkg-descr index b92f8177fe..d3cbd8afa0 100644 --- a/dns/bind/pkg-descr +++ b/dns/bind/pkg-descr @@ -4,10 +4,88 @@ one computer can find another computer on the basis of its name. The BIND software distribution contains all of the software necessary for asking and answering name service questions. +WWW: https://www.isc.org Plugin Changelog ================ +1.34 + +* Add custom configuration include directory /usr/local/etc/namedb/named.conf.d (contributed by Nicholas Card) +* Add forward zones +* Fix primary zones grid and command column (contributed by benyamin-codez) + +1.33 + +* Add option to allow the rndc-key for zone transfers (contributed by Naomi Rennie-Waldock) +* Switch to Bind 9.20 + +1.32 + +* Fix handling of multiple ACLs in allow-query/allow-transfer (contributed by Nathan Rennie-Waldock) +* Fix multiple select on Recursion field and resulting multiple ACLs (contributed by Jordan Stacy) +* Add SSHFP record type (contributed by doktornotor) + +1.31 + +* Do not add the update-policy if the zone type is secondary (contributed by Brendan Bank) +* Adjust severity log levels (contributed by kulikov-a) + +1.30 + +* Add ability for RNDC key updates from other nameservers (contributed by Joachim Friberg) + +1.29 + +* Migrate General Log to Syslog +* Add Check & Preview button to Primary Zones grid + +1.28 + +* Add break-dnssec toggle when using filter-aaaa on IPv4/IPv6 clients (contributed by doktornotor) +* Fix template error if there is no 'records' in config at all (contributed by kulikov-a) + +1.27 + +* Add DNAME support (contributed by Simon Fischer) + +1.26 + +* Allow multiple ACLs to be selected for Transfers/Queries (contributed by Robbert Rijkse) +* Rename Master/Slave to Primary/Secondary (contributed by Robbert Rijkse) +* Add necessary hooks to allow the plugin to be used as a standalone core DNS server +* Changed default listening addresses to 0.0.0.0/:: for new users and interally translate these to "any" +* Prevent using a port being used by another DNS service + +1.25 + +* Ensure you can only add one ACL with the same name (contributed by Robbert Rijkse) +* Cleanup/Fix the Master/Slave domain dialogs (contributed by Robbert Rijkse) +* Revamp the logging page with proper columns (contributed by Robbert Rijkse) +* Add RNDC key configuration (contributed by Robbert Rijkse) +* Add PR record type (contributed by Robbert Rijkse) +* Update base to BIND 9.18 + +1.24 + +* Separate tables for master and slave zones in UI (contributed by Patrick M. Hausen and Manuel Faux) + +1.23 + +* Avoid errors with repeated primary servers and keys (contributed by Michael Newton) + +1.22 + +* Fix DNS Blacklist download + +1.21 + +* Add support for filter AAAA in DNS responses when A is present (contributed by Zane Chua) + +1.20 + +* Allow signed zone transfers (contributed by Michael Newton) + 1.19 * Add support for "Query Source [IP|IPv6]" options. @@ -104,6 +182,3 @@ Plugin Changelog 1.0 * Initial release - - -WWW: https://www.isc.org diff --git a/dns/bind/src/etc/inc/plugins.inc.d/bind.inc b/dns/bind/src/etc/inc/plugins.inc.d/bind.inc index 2b050aa2bb..e1d8b1b2cf 100644 --- a/dns/bind/src/etc/inc/plugins.inc.d/bind.inc +++ b/dns/bind/src/etc/inc/plugins.inc.d/bind.inc @@ -1,30 +1,31 @@ - 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. -*/ + * Copyright (C) 2018 Michael Muenz + * Copyright (C) 2023 Franco Fichtner + * 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. + */ function bind_enabled() { @@ -32,34 +33,82 @@ function bind_enabled() return (string)$model->enabled == '1'; } +function bind_configure() +{ + return [ + 'dns' => ['bind_configure_do'], + ]; +} + function bind_services() { - $services = array(); + $services = []; if (!bind_enabled()) { return $services; } - $services[] = array( + $model = new \OPNsense\Bind\General(); + + /* DNS service is eligable for core use when both 0.0.0.0 and :: are set */ + $any4 = false; + $any6 = false; + + foreach (explode(',', (string)$model->listenv4) as $addr) { + $any4 |= $addr === '0.0.0.0'; + } + + foreach (explode(',', (string)$model->listenv6) as $addr) { + $any6 |= $addr === '::'; + } + + $services[] = [ + /* the port may still be something other than 53, but it's safe to register a conflict for it */ + 'dns_ports' => ($any4 && $any6 ? [(string)$model->port] : []), 'description' => gettext('BIND Daemon'), - 'configd' => array( - 'restart' => array('bind restart'), - 'start' => array('bind start'), - 'stop' => array('bind stop'), - ), + 'configd' => [ + 'restart' => ['bind restart'], + 'start' => ['bind start'], + 'stop' => ['bind stop'], + ], + 'pidfile' => '/var/run/named/pid', 'name' => 'named', - 'pidfile' => '/var/run/named/pid' - ); + ]; return $services; } function bind_xmlrpc_sync() { - $result = array(); + $result = []; + $result['id'] = 'bind'; $result['section'] = 'OPNsense.bind'; $result['description'] = gettext('BIND domain name service'); $result['services'] = ['named']; - return array($result); + + return [$result]; +} + +function bind_configure_do($verbose) +{ + service_log('Starting BIND...', $verbose); + + configd_run('template reload OPNsense/Bind'); + configd_run('bind restart'); + + service_log("done.\n", $verbose); +} + +/** + * register syslog facilities + * @return array + */ +function bind_syslog() +{ + $syslogconf = []; + + $syslogconf['bind'] = ['facility' => ['named']]; + + return $syslogconf; } diff --git a/dns/bind/src/etc/namedb/named.conf.d/00-README.conf b/dns/bind/src/etc/namedb/named.conf.d/00-README.conf new file mode 100644 index 0000000000..fac2f0d6bc --- /dev/null +++ b/dns/bind/src/etc/namedb/named.conf.d/00-README.conf @@ -0,0 +1,7 @@ +# Custom BIND Configuration Directory +# +# Place your custom BIND directives in .conf files in this directory +# Files are included in alphabetical order +# +# Examples: +# server 192.168.1.100 { edns no; }; diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/AclController.php b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/AclController.php index e092f1f5ba..b7a213ef0e 100644 --- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/AclController.php +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/AclController.php @@ -1,31 +1,29 @@ +/* + * Copyright (C) 2018 Michael Muenz + * All rights reserved. * - * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: * - * 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. * - * 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. + * 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. */ namespace OPNsense\Bind\Api; @@ -39,25 +37,29 @@ class AclController extends ApiMutableModelControllerBase public function searchAclAction() { - return $this->searchBase('acls.acl', array("enabled", "name", "networks")); + return $this->searchBase('acls.acl', ['enabled', 'name', 'networks']); } + public function getAclAction($uuid = null) { - $this->sessionClose(); return $this->getBase('acl', 'acls.acl', $uuid); } + public function addAclAction() { return $this->addBase('acl', 'acls.acl'); } + public function delAclAction($uuid) { return $this->delBase('acls.acl', $uuid); } + public function setAclAction($uuid) { return $this->setBase('acl', 'acls.acl', $uuid); } + public function toggleAclAction($uuid) { return $this->toggleBase('acls.acl', $uuid); diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/DomainController.php b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/DomainController.php index df278dc53f..87c71db3f8 100644 --- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/DomainController.php +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/DomainController.php @@ -1,32 +1,30 @@ - * Copyright (C) 2019 Deciso B.V. +/* + * Copyright (C) 2019 Michael Muenz + * Copyright (C) 2019 Deciso B.V. + * All rights reserved. * - * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: * - * 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. * - * 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. + * 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. */ namespace OPNsense\Bind\Api; @@ -39,23 +37,72 @@ class DomainController extends ApiMutableModelControllerBase protected static $internalModelName = 'domain'; protected static $internalModelClass = '\OPNsense\Bind\Domain'; - public function searchDomainAction() + /* XXX backwards-compatibility for 22.7 and below */ + public function searchMasterDomainAction() + { + return $this->searchPrimaryDomainAction(); + } + + /* XXX backwards-compatibility for 22.7 and below */ + public function searchSlaveDomainAction() + { + return $this->searchSecondaryDomainAction(); + } + + public function searchPrimaryDomainAction() + { + return $this->searchBase( + 'domains.domain', + [ 'enabled', 'type', 'domainname', 'ttl', 'refresh', 'retry', 'expire', 'negative' ], + 'domainname', + function ($record) { + return $record->type->getNodeData()['primary']['selected'] === 1; + } + ); + } + + public function searchSecondaryDomainAction() { - return $this->searchBase('domains.domain', array( - "enabled", "type", "masterip", "domainname", "allowtransfer", "allowquery", "ttl", - "refresh", "retry", "expire", "negative", "mailadmin", "dnsserver" - )); + return $this->searchBase( + 'domains.domain', + [ 'enabled', 'type', 'domainname', 'primaryip' ], + 'domainname', + function ($record) { + return $record->type->getNodeData()['secondary']['selected'] === 1; + } + ); + } + + public function searchForwardDomainAction() + { + return $this->searchBase( + 'domains.domain', + [ 'enabled', 'type', 'domainname', 'forwardserver' ], + 'domainname', + function ($record) { + return $record->type->getNodeData()['forward']['selected'] === 1; + } + ); } public function getDomainAction($uuid = null) { - $this->sessionClose(); return $this->getBase('domain', 'domains.domain', $uuid); } - public function addDomainAction($uuid = null) + public function addPrimaryDomainAction($uuid = null) + { + return $this->addBase('domain', 'domains.domain', ['type' => 'primary']); + } + + public function addSecondaryDomainAction($uuid = null) + { + return $this->addBase('domain', 'domains.domain', ['type' => 'secondary']); + } + + public function addForwardDomainAction($uuid = null) { - return $this->addBase('domain', 'domains.domain'); + return $this->addBase('domain', 'domains.domain', ['type' => 'forward']); } public function delDomainAction($uuid) diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/GeneralController.php b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/GeneralController.php index d712f7fde9..b0d385cfb0 100644 --- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/GeneralController.php +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/GeneralController.php @@ -31,9 +31,32 @@ namespace OPNsense\Bind\Api; use OPNsense\Base\ApiMutableModelControllerBase; +use OPNsense\Core\Backend; class GeneralController extends ApiMutableModelControllerBase { protected static $internalModelClass = '\OPNsense\Bind\General'; protected static $internalModelName = 'general'; + + public function zonetestAction($zonename = null) + { + $response = "request error"; + if ($this->request->hasPost("zone")) { + $zonename = $this->request->getPost("zone"); + $backend = new Backend(); + $response = trim($backend->configdpRun("bind zone check", [$zonename])); + } + return array("response" => $response); + } + + public function zoneshowAction($zonename = null) + { + $response = "request error"; + if ($this->request->hasPost("zone")) { + $zonename = $this->request->getPost("zone"); + $backend = new Backend(); + $response = json_decode($backend->configdpRun("bind zone show", [$zonename]), true); + } + return $response; + } } diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/RecordController.php b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/RecordController.php index 49cac764ea..67dc5e30fb 100644 --- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/RecordController.php +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/RecordController.php @@ -1,32 +1,30 @@ - * Copyright (C) 2019 Deciso B.V. +/* + * Copyright (C) 2019 Michael Muenz + * Copyright (C) 2019 Deciso B.V. + * All rights reserved. * - * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: * - * 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. * - * 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. + * 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. */ namespace OPNsense\Bind\Api; @@ -77,7 +75,6 @@ public function searchRecordAction() public function getRecordAction($uuid = null) { - $this->sessionClose(); $domain = $this->request->get('domain'); $result = $this->getBase('record', 'records.record', $uuid); if ($uuid == null && !empty($result['record']['domain'])) { diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/ServiceController.php b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/ServiceController.php index a612a11216..8c4935bd0a 100644 --- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/ServiceController.php +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/Api/ServiceController.php @@ -48,10 +48,9 @@ class ServiceController extends ApiMutableServiceControllerBase public function dnsblAction() { - $this->sessionClose(); $mdl = new Dnsbl(); $backend = new Backend(); - $response = $backend->configdpRun('bind dnsbl', array((string)$mdl->type)); - return array("response" => $response); + $response = $backend->configdpRun('bind dnsbl', [(string)$mdl->type]); + return ['response' => $response]; } } diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/GeneralController.php b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/GeneralController.php index 3f0b046b82..cfa86649db 100644 --- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/GeneralController.php +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/GeneralController.php @@ -35,7 +35,9 @@ public function indexAction() $this->view->generalForm = $this->getForm("general"); $this->view->dnsblForm = $this->getForm("dnsbl"); $this->view->formDialogEditBindAcl = $this->getForm("dialogEditBindAcl"); - $this->view->formDialogEditBindDomain = $this->getForm("dialogEditBindDomain"); + $this->view->formDialogEditBindPrimaryDomain = $this->getForm("dialogEditBindPrimaryDomain"); + $this->view->formDialogEditBindSecondaryDomain = $this->getForm("dialogEditBindSecondaryDomain"); + $this->view->formDialogEditBindForwardDomain = $this->getForm("dialogEditBindForwardDomain"); $this->view->formDialogEditBindRecord = $this->getForm("dialogEditBindRecord"); $this->view->pick('OPNsense/Bind/general'); } diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/LogsController.php b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/LogsController.php new file mode 100644 index 0000000000..23fde619ef --- /dev/null +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/LogsController.php @@ -0,0 +1,46 @@ +view->pick('OPNsense/Bind/logs'); + } +} diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindDomain.xml b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindDomain.xml deleted file mode 100644 index afde58416d..0000000000 --- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindDomain.xml +++ /dev/null @@ -1,100 +0,0 @@ -
- - domain.enabled - - checkbox - This will enable or disable this zone. - - - domain.domainname - - text - Set the name for this zone. Both forward and reverse zones may be specified, i.e. example.com or 0.168.192.in-addr.arpa. - - - domain.allowtransfer - - dropdown - Define an ACL where you allow which server can retrieve this zone. - - - domain.allowquery - - dropdown - Define an ACL where you allow which client are allowed to query this zone. - - - domain.type - - dropdown - Set the type for this zone. - - - - header - - - - domain.masterip - - - select_multiple - true - Set the IP address of master server when using slave mode. - - - domain.allownotifyslave - - - select_multiple - true - A list of allowed IP addresses to receive notifies from. - - - - header - - - - domain.ttl - - text - Set the general Time To Live for this zone. - - - domain.refresh - - text - Set the time in seconds after which name servers should refresh the zone information. - - - domain.retry - - text - Set the time in seconds after which name servers should retry requests if the master does not respond. - - - domain.expire - - text - Set the time in seconds after which name servers should stop answering requests if the master does not respond. - - - domain.negative - - text - Set the time in seconds after which an entry for a non-existent record should expire from cache. - - - domain.mailadmin - - text - Set the mail address of zone admin. A @-sign will automatically be replaced with a dot in the zone data. - - - domain.dnsserver - - text - Set the DNS server hosting this file. This should usually be the FQDN of your firewall where the BIND plugin is installed. - -
diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindForwardDomain.xml b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindForwardDomain.xml new file mode 100644 index 0000000000..4ca30f87c6 --- /dev/null +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindForwardDomain.xml @@ -0,0 +1,22 @@ +
+ + domain.enabled + + checkbox + This will enable or disable this zone. + + + domain.domainname + + text + Set the name for this zone. Both forward and reverse zones may be specified, i.e. example.com or 0.168.192.in-addr.arpa. + + + domain.forwardserver + + + select_multiple + true + Set the IP address of server to forward requests to. + +
diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindPrimaryDomain.xml b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindPrimaryDomain.xml new file mode 100644 index 0000000000..8439403d18 --- /dev/null +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindPrimaryDomain.xml @@ -0,0 +1,80 @@ +
+ + domain.enabled + + checkbox + This will enable or disable this zone. + + + domain.domainname + + text + Set the name for this zone. Both forward and reverse zones may be specified, i.e. example.com or 0.168.192.in-addr.arpa. + + + domain.allowtransfer + + select_multiple + Define the ACLs where you allow which server can retrieve this zone. + + + domain.allowrndctransfer + + checkbox + Allow transfers via the RDNC key named "rndc-key". The key is shown in the general tab. + + + domain.allowquery + + select_multiple + Define the ACLs where you allow which client are allowed to query this zone. + + + domain.allowrndcupdate + + checkbox + Allow updates via the RDNC key named "rndc-key". The key is shown in the general tab. + + + domain.ttl + + text + Set the general Time To Live for this zone. + + + domain.refresh + + text + Set the time in seconds after which name servers should refresh the zone information. + + + domain.retry + + text + Set the time in seconds after which name servers should retry requests if the master does not respond. + + + domain.expire + + text + Set the time in seconds after which name servers should stop answering requests if the master does not respond. + + + domain.negative + + text + Set the time in seconds after which an entry for a non-existent record should expire from cache. + + + domain.mailadmin + + text + Set the mail address of zone admin. A @-sign will automatically be replaced with a dot in the zone data. + + + domain.dnsserver + + text + Set the DNS server hosting this file. This should usually be the FQDN of your firewall where the BIND plugin is installed. + +
diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindSecondaryDomain.xml b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindSecondaryDomain.xml new file mode 100644 index 0000000000..a6380c2837 --- /dev/null +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/dialogEditBindSecondaryDomain.xml @@ -0,0 +1,60 @@ +
+ + domain.enabled + + checkbox + This will enable or disable this zone. + + + domain.domainname + + text + Set the name for this zone. Both forward and reverse zones may be specified, i.e. example.com or 0.168.192.in-addr.arpa. + + + domain.allowtransfer + + select_multiple + Define the ACLs where you allow which server can retrieve this zone. + + + domain.allowquery + + select_multiple + Define the ACLs where you allow which client are allowed to query this zone. + + + domain.primaryip + + + select_multiple + true + Set the IP address of primary server. + + + domain.transferkeyalgo + + dropdown + Set the authentication algorithm for the TSIG key used to transfer domain data from the primary server. + + + domain.transferkeyname + + text + The name of the TSIG key, which must match the value on the primary server. + + + domain.transferkey + + text + The TSIG key used to transfer domain data from the master server in Base64 encoding. + + + domain.allownotifysecondary + + + select_multiple + true + A list of allowed IP addresses to receive notifies from (in addition to the primary server.) + +
diff --git a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/general.xml b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/general.xml index da2938c356..23e9c92026 100644 --- a/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/general.xml +++ b/dns/bind/src/opnsense/mvc/app/controllers/OPNsense/Bind/forms/general.xml @@ -69,11 +69,38 @@ true Set one or more hosts to send your DNS queries if the request is unknown. + + general.filteraaaav4 + + checkbox + This will filter AAAA records on IPv4 Clients. Set "DNSSEC Validation" to "No" and AAAA records will be omitted even if they are signed. + + + general.filteraaaav6 + + checkbox + This will filter AAAA records on IPv6 Clients. Set "DNSSEC Validation" to "No" and AAAA records will be omitted even if they are signed. + + + general.filteraaaaacl + + + select_multiple + true + Specifies a list of client addresses for which AAAA filtering is to be applied. + general.logsize text - Set the amount how big a logfile can growth. + Set the amount how big a logfile can growth. For Query and Blocked logs. + + + general.general_log_level + + + dropdown + Select General Log level. Log levels are listed in the order of increasing verbosity. Setting a certain log level will cause all messages of the specified and more severe log levels to be logged. general.maxcachesize @@ -84,14 +111,20 @@ general.recursion - dropdown + select_multiple Define an ACL where you allow which clients can resolve via this service. Usually use your local LAN. general.allowtransfer - dropdown - Define an ACL where you allow which server can retrieve zones. + select_multiple + Define the ACLs where you allow which server can retrieve zones. + + + general.allowquery + + select_multiple + Define the ACLs where you allow which client are allowed to query this server. general.dnssecvalidation @@ -143,4 +176,23 @@ true Except a list of IPs from rate-limiting like ::1 + + header + + true + + + general.rndcalgo + + dropdown + true + Set the authentication algorithm for the RNDC key. This requires a restart of the Bind Service. + + + general.rndcsecret + + text + true + The base64-encoded RNDC key. This requires a restart of the Bind Service. + diff --git a/dns/bind/src/opnsense/mvc/app/library/OPNsense/System/Status/BindOverrideStatus.php b/dns/bind/src/opnsense/mvc/app/library/OPNsense/System/Status/BindOverrideStatus.php new file mode 100644 index 0000000000..eb15970e6a --- /dev/null +++ b/dns/bind/src/opnsense/mvc/app/library/OPNsense/System/Status/BindOverrideStatus.php @@ -0,0 +1,57 @@ +internalPriority = 2; + $this->internalPersistent = true; + $this->internalIsBanner = true; + $this->internalTitle = gettext('BIND config override'); + $this->internalScope = [ + '/ui/bind/general/index' + ]; + } + + public function collectStatus() + { + if (count(glob('/usr/local/etc/namedb/named.conf.d/*')) > 1) { + $this->internalMessage = gettext( + 'The configuration contains manual overwrites, these may interfere with the settings configured here.' + ); + $this->internalStatus = SystemStatusCode::NOTICE; + } + } +} diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/ACL/ACL.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/ACL/ACL.xml index 83205c526b..76c52ef6c5 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/ACL/ACL.xml +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/ACL/ACL.xml @@ -4,6 +4,7 @@ ui/bind/* api/bind/* + api/diagnostics/log/named/* diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml index 474a0a0dfd..efa489b85b 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Acl.xml @@ -6,20 +6,23 @@ - 1 + 1 Y - Y - /^(?!any$|localhost$|localnets$|none$)[0-9a-zA-Z_\-]{1,32}$/u + /^(?!any$|localhost$|localnets$|none$)[0-9a-zA-Z_\-]{1,32}$/u Should be a string between 1 and 32 characters. Allowed characters are 0-9, a-z, A-Z, _ and -. Built-in ACL names must not be used: any, localhost, localnets, none. + + + An ACL with this name already exists. + UniqueConstraint + + - - , Y - Y + Y diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Dnsbl.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Dnsbl.xml index 119dbf76fc..103db84e29 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Dnsbl.xml +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Dnsbl.xml @@ -4,7 +4,7 @@ 1.0.5 - 0 + 0 Y @@ -41,19 +41,19 @@ N - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Domain.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Domain.xml index 8aad51f826..6743b66ae4 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Domain.xml +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Domain.xml @@ -1,34 +1,45 @@ //OPNsense/bind/domain BIND domain configuration - 1.0.1 + 1.1.2 - 1 + 1 Y - master + primary Y - master - slave + primary + secondary + forward - - N - , - Y - - - N - , - Y - + + Y + + + Y + + + + HMAC-SHA512 + HMAC-SHA384 + HMAC-SHA256 + HMAC-SHA224 + HMAC-SHA1 + HMAC-MD5 + + + + + + Y + - Y @@ -39,9 +50,9 @@ name - N - N + Y + - N - N + Y - - N - + + 1 + Y + + - 86400 + 86400 Y 60 86400 Set a value between 60 and 86400. - 21600 + 21600 Y 60 86400 Set a value between 60 and 86400. - 3600 + 3600 Y 60 86400 Set a value between 60 and 86400. - 3542400 + 3542400 Y 60 10000000 Set a value between 60 and 10000000. - 3600 + 3600 Y 60 86400 Set a value between 60 and 86400. - mail.opnsense.localdomain + mail.opnsense.localdomain Y - opnsense.localdomain + opnsense.localdomain Y diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.php b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.php index 4d6a5bd0c7..1c97234eaa 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.php +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.php @@ -1,35 +1,66 @@ - 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. -*/ + * Copyright (C) 2018 Michael Muenz + * Copyright (C) 2023 Deciso B.V. + * 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. + */ namespace OPNsense\Bind; use OPNsense\Base\BaseModel; +use OPNsense\Base\Messages\Message; +use OPNsense\Core\Backend; class General extends BaseModel { + public function performValidation($validateFullModel = false) + { + $messages = parent::performValidation($validateFullModel); + + if ( + ($validateFullModel || $this->enabled->isFieldChanged() || $this->port->isFieldChanged()) && + !empty((string)$this->enabled) + ) { + foreach (json_decode((new Backend())->configdpRun('service list'), true) as $service) { + if (empty($service['dns_ports'])) { + continue; + } + if (!is_array($service['dns_ports'])) { + syslog(LOG_ERR, sprintf('Service %s (%s) reported a faulty "dns_ports" entry.', $service['description'], $service['name'])); + continue; + } + if ($service['name'] != 'named' && in_array((string)$this->port, $service['dns_ports'])) { + $messages->appendMessage(new Message( + sprintf(gettext('%s is currently using this port.'), $service['description']), + $this->port->getInternalXMLTagName() + )); + break; + } + } + } + + return $messages; + } } diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml index 5d67e79318..238c9dc248 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/General.xml @@ -1,70 +1,86 @@ //OPNsense/bind/general BIND configuration - 1.0.7 + 1.0.12 - 0 + 0 Y - 0 + 0 Y - 1 + 1 Y - 127.0.0.1 - , + 0.0.0.0 Y - Y + Y - ::1 - , + :: Y - Y + Y - N ipv4 N - N ipv6 N - N ipv4 N - N ipv6 N - 53530 + 53530 Y - , - N - Y + Y + + 0 + Y + + + 0 + Y + + + Y + - 5 + 5 Y 1 1000 Choose a value between 1 and 1000. + + + Critical + Error + Warning + Notice + Informational + Debug + Dynamic + + Y + info + - 80 + 80 Y 1 99 @@ -78,8 +94,7 @@ name - N - N + Y Choose an ACL. @@ -90,45 +105,67 @@ name - N - N + Y + + + + + Y + No Auto - no - N + no Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - N 1 1000 Choose a value between 1 and 1000. - 127.0.0.1,::1 - , + 0.0.0.0,:: Y - Y + Y + + Y + hmac-sha256 + + HMAC-SHA512 + HMAC-SHA384 + HMAC-SHA256 + HMAC-SHA224 + HMAC-SHA1 + HMAC-MD5 + + + + Y + VxtIzJevSQXqnr7h2qerrcwjnZlMWSGGFBndKeNIDfw= + diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Menu/Menu.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Menu/Menu.xml index 13b24d1e01..06ed53ca81 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Menu/Menu.xml +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Menu/Menu.xml @@ -1,10 +1,8 @@ - - - - + + diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Migrations/M1_1_0.php b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Migrations/M1_1_0.php new file mode 100644 index 0000000000..c38c74facf --- /dev/null +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Migrations/M1_1_0.php @@ -0,0 +1,78 @@ +object(); + + /* checks to see if there is a bind config section, otherwise skips the rest of the migration */ + if (empty($config->OPNsense->bind)) { + return; + } + + $bindConfig = $config->OPNsense->bind; + + /* loops through the domains in the config */ + foreach ($bindConfig->domain->domains->domain as $domain) { + $domainModel = $model->getNodeByReference('domains.domain.' . $domain->attributes()['uuid']); + + /* migrates the domain type */ + if ($domain->type == 'master') { + $domainModel->type->setValue('primary'); + } else { + $domainModel->type->setValue('secondary'); + } + + /* migrates the Master IP to Primary IP field */ + if (!empty($domain->masterip)) { + $domainModel->primaryip->setValue($domain->masterip); + } + + /* migrates the AllowNotify Slave to AllowNotify Secondary field */ + if (!empty($domain->allownotifyslave)) { + $domainModel->allownotifysecondary->setValue($domain->allownotifyslave); + } + } + + parent::run($model); + } + } +} diff --git a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Record.xml b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Record.xml index 797b43b723..ccb589cbfa 100644 --- a/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Record.xml +++ b/dns/bind/src/opnsense/mvc/app/models/OPNsense/Bind/Record.xml @@ -1,12 +1,12 @@ //OPNsense/bind/record BIND record configuration - 1.0.0 + 1.0.1 - 1 + 1 Y @@ -19,25 +19,25 @@ - - - N - + - A + A Y A AAAA CAA CNAME + DNAME DNSKEY DS MX NS PTR + RP RRSIG SRV + SSHFP TLSA TXT diff --git a/dns/bind/src/opnsense/mvc/app/views/OPNsense/Bind/general.volt b/dns/bind/src/opnsense/mvc/app/views/OPNsense/Bind/general.volt index cc3a15c292..0d9b55eda9 100644 --- a/dns/bind/src/opnsense/mvc/app/views/OPNsense/Bind/general.volt +++ b/dns/bind/src/opnsense/mvc/app/views/OPNsense/Bind/general.volt @@ -1,46 +1,46 @@ {# - -OPNsense® is Copyright © 2014 – 2019 by Deciso B.V. -This file is Copyright © 2018 - 2019 by Michael Muenz -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. - -#} + # + # Copyright (c) 2014-2019 Deciso B.V. + # Copyright (c) 2018-2019 Michael Muenz + # 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':generalForm,'id':'frm_general_settings'])}} -
-
+
@@ -48,85 +48,86 @@ POSSIBILITY OF SUCH DAMAGE.
{{ partial("layout_partials/base_form",['fields':dnsblForm,'id':'frm_dnsbl_settings'])}} -
-
+
- - - - - - - - - - - - - - - - - - -
{{ lang._('Enabled') }}{{ lang._('Name') }}{{ lang._('Networks') }}{{ lang._('ID') }}{{ lang._('Commands') }}
- - -
+
+ + + + + + + + + + + + + + + + + + +
{{ lang._('Enabled') }}{{ lang._('Name') }}{{ lang._('Networks') }}{{ lang._('ID') }}{{ lang._('Commands') }}
+ + +
+



-
- {{ partial("layout_partials/base_dialog",['fields':formDialogEditBindAcl,'id':'dialogEditBindAcl','label':lang._('Edit ACL')])}} -{{ partial("layout_partials/base_dialog",['fields':formDialogEditBindDomain,'id':'dialogEditBindDomain','label':lang._('Edit Zone')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditBindPrimaryDomain,'id':'dialogEditBindPrimaryDomain','label':lang._('Edit Primary Zone')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditBindSecondaryDomain,'id':'dialogEditBindSecondaryDomain','label':lang._('Edit Secondary Zone')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditBindForwardDomain,'id':'dialogEditBindForwardDomain','label':lang._('Edit Forward Zone')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogEditBindRecord,'id':'dialogEditBindRecord','label':lang._('Edit Record')])}} + diff --git a/dns/bind/src/opnsense/mvc/app/views/OPNsense/Bind/logs.volt b/dns/bind/src/opnsense/mvc/app/views/OPNsense/Bind/logs.volt new file mode 100644 index 0000000000..4eab4cb5a9 --- /dev/null +++ b/dns/bind/src/opnsense/mvc/app/views/OPNsense/Bind/logs.volt @@ -0,0 +1,132 @@ +{# + # Copyright (c) 2022 Robbert Rijkse + # Copyright (c) 2019 Deciso B.V. + # 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("OPNsense/Diagnostics/log",['module':'core','scope':'named'])}} +
+ +
+
+
+ + + + + + + + + + + + +
{{ lang._('Date') }}{{ lang._('Client') }}{{ lang._('IP Address') }}{{ lang._('Record') }}{{ lang._('Query') }}
+
+
+
+ +
+
+
+ + + + + + + + + + + + +
{{ lang._('Date') }}{{ lang._('Client') }}{{ lang._('IP Address') }}{{ lang._('Record') }}{{ lang._('Query') }}
+
+
+
+
diff --git a/dns/bind/src/opnsense/scripts/OPNsense/Bind/dnsbl.sh b/dns/bind/src/opnsense/scripts/OPNsense/Bind/dnsbl.sh index aac1ad92ee..9324b6b2a3 100755 --- a/dns/bind/src/opnsense/scripts/OPNsense/Bind/dnsbl.sh +++ b/dns/bind/src/opnsense/scripts/OPNsense/Bind/dnsbl.sh @@ -38,70 +38,70 @@ mkdir -p ${WORKDIR} easylist() { # EasyList ${FETCH} https://justdomains.github.io/blocklists/lists/easylist-justdomains.txt -o ${WORKDIR}/easylist-raw - sed "/\.$/d" ${WORKDIR}/easylist-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/easylist + sed "/\.$/d" ${WORKDIR}/easylist-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/easylist rm ${WORKDIR}/easylist-raw } easyprivacy() { # EasyPrivacy ${FETCH} https://justdomains.github.io/blocklists/lists/easyprivacy-justdomains.txt -o ${WORKDIR}/easyprivacy-raw - sed "/\.$/d" ${WORKDIR}/easyprivacy-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/easyprivacy + sed "/\.$/d" ${WORKDIR}/easyprivacy-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/easyprivacy rm ${WORKDIR}/easyprivacy-raw } pornall() { # PornAll ${FETCH} https://raw.githubusercontent.com/chadmayfield/my-pihole-blocklists/master/lists/pi_blocklist_porn_all.list -o ${WORKDIR}/pornall-raw - sed "/\.$/d" ${WORKDIR}/pornall-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/pornall + sed "/\.$/d" ${WORKDIR}/pornall-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/pornall rm ${WORKDIR}/pornall-raw } porntop() { # PornTop1M ${FETCH} https://raw.githubusercontent.com/chadmayfield/pihole-blocklists/master/lists/pi_blocklist_porn_top1m.list -o ${WORKDIR}/porntop-raw - sed "/\.$/d" ${WORKDIR}/porntop-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/porntop + sed "/\.$/d" ${WORKDIR}/porntop-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/porntop rm ${WORKDIR}/porntop-raw } emdlist() { # EMD ${FETCH} https://hosts-file.net/emd.txt -o ${WORKDIR}/emdlist-raw - sed "/\.$/d" ${WORKDIR}/emdlist-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/emdlist + sed "/\.$/d" ${WORKDIR}/emdlist-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/emdlist rm ${WORKDIR}/emdlist-raw } adguard() { # AdGuard ${FETCH} https://justdomains.github.io/blocklists/lists/adguarddns-justdomains.txt -o ${WORKDIR}/adguard-raw - sed "/\.$/d" ${WORKDIR}/adguard-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/adguard + sed "/\.$/d" ${WORKDIR}/adguard-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/adguard rm ${WORKDIR}/adguard-raw } nocoin() { # NoCoin ${FETCH} https://justdomains.github.io/blocklists/lists/nocoin-justdomains.txt -o ${WORKDIR}/nocoin-raw - sed "/\.$/d" ${WORKDIR}/nocoin-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/nocoin + sed "/\.$/d" ${WORKDIR}/nocoin-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/nocoin rm ${WORKDIR}/nocoin-raw } rwtracker() { # RansomWare Tracker abuse.ch ${FETCH} https://ransomwaretracker.abuse.ch/downloads/RW_DOMBL.txt -o ${WORKDIR}/rwtracker-raw - sed "/\.$/d" ${WORKDIR}/rwtracker-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/rwtracker + sed "/\.$/d" ${WORKDIR}/rwtracker-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/rwtracker rm ${WORKDIR}/rwtracker-raw } mwdomains() { # MalwareDomains ${FETCH} http://malwaredomains.lehigh.edu/files/justdomains -o ${WORKDIR}/malwaredomains-raw - sed "/\.$/d" ${WORKDIR}/malwaredomains-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/malwaredomains + sed "/\.$/d" ${WORKDIR}/malwaredomains-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/malwaredomains rm ${WORKDIR}/malwaredomains-raw } windowsspyblockerspy() { # WindowsSpyBlocker (spy) ${FETCH} https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt -o ${WORKDIR}/windowsspyblockerspy-raw - sed "/\.$/d" ${WORKDIR}/windowsspyblockerspy-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/windowsspyblockerspy + sed "/\.$/d" ${WORKDIR}/windowsspyblockerspy-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/windowsspyblockerspy rm ${WORKDIR}/windowsspyblockerspy-raw } @@ -115,98 +115,98 @@ windowsspyblockerupdate() { windowsspyblockerextra() { # WindowsSpyBlocker (extra) ${FETCH} https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/extra.txt -o ${WORKDIR}/windowsspyblockerextra-raw - sed "/\.$/d" ${WORKDIR}/windowsspyblockerextra-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/windowsspyblockerextra + sed "/\.$/d" ${WORKDIR}/windowsspyblockerextra-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/windowsspyblockerextra rm ${WORKDIR}/windowsspyblockerextra-raw } cameleon() { # Cameleon List ${FETCH} http://sysctl.org/cameleon/hosts -o ${WORKDIR}/cameleon-raw - sed "/\.$/d" ${WORKDIR}/cameleon-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/cameleon + sed "/\.$/d" ${WORKDIR}/cameleon-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/cameleon rm ${WORKDIR}/cameleon-raw } adaway() { # AdAway List ${FETCH} https://adaway.org/hosts.txt -o ${WORKDIR}/adaway-raw - sed "/\.$/d" ${WORKDIR}/adaway-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/adaway + sed "/\.$/d" ${WORKDIR}/adaway-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/adaway rm ${WORKDIR}/adaway-raw } yoyo() { # YoYo List ${FETCH} "http://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext" -o ${WORKDIR}/yoyo-raw - sed "/\.$/d" ${WORKDIR}/yoyo-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/yoyo + sed "/\.$/d" ${WORKDIR}/yoyo-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/yoyo rm ${WORKDIR}/yoyo-raw } stevenblack() { # StevenBlack ${FETCH} https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts -o ${WORKDIR}/stevenblack-raw - sed "/\.$/d" ${WORKDIR}/stevenblack-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/stevenblack + sed "/\.$/d" ${WORKDIR}/stevenblack-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/stevenblack rm ${WORKDIR}/stevenblack-raw } blocklistads() { # Blocklist.site Ads ${FETCH} https://blocklistproject.github.io/Lists/ads.txt -o ${WORKDIR}/blocklistads-raw - sed "/\.$/d" ${WORKDIR}/blocklistads-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" > ${WORKDIR}/blocklistads + sed "/\.$/d" ${WORKDIR}/blocklistads-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" > ${WORKDIR}/blocklistads rm ${WORKDIR}/blocklistads-raw } blocklistfraud() { # Blocklist.site Fraud ${FETCH} https://blocklistproject.github.io/Lists/fraud.txt -o ${WORKDIR}/blocklistfraud-raw - sed "/\.$/d" ${WORKDIR}/blocklistfraud-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" > ${WORKDIR}/blocklistfraud + sed "/\.$/d" ${WORKDIR}/blocklistfraud-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" > ${WORKDIR}/blocklistfraud rm ${WORKDIR}/blocklistfraud-raw } blocklistphishing() { # Blocklist.site Phishing ${FETCH} https://blocklistproject.github.io/Lists/phishing.txt -o ${WORKDIR}/blocklistphishing-raw - sed "/\.$/d" ${WORKDIR}/blocklistphishing-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" > ${WORKDIR}/blocklistphishing + sed "/\.$/d" ${WORKDIR}/blocklistphishing-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" > ${WORKDIR}/blocklistphishing rm ${WORKDIR}/blocklistphishing-raw } hphosts-ads() { # hphosts-ads ${FETCH} https://hosts-file.net/ad_servers.txt -o ${WORKDIR}/hphosts-ads-raw - sed "/\.$/d" ${WORKDIR}/hphosts-ads-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/hphosts-ads + sed "/\.$/d" ${WORKDIR}/hphosts-ads-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/hphosts-ads rm ${WORKDIR}/hphosts-ads-raw } hphosts-fsa() { # hphosts-fsa ${FETCH} https://hosts-file.net/fsa.txt -o ${WORKDIR}/hphosts-fsa-raw - sed "/\.$/d" ${WORKDIR}/hphosts-fsa-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/hphosts-fsa + sed "/\.$/d" ${WORKDIR}/hphosts-fsa-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/hphosts-fsa rm ${WORKDIR}/hphosts-fsa-raw } hphosts-psh() { # hphosts-psh ${FETCH} https://hosts-file.net/psh.txt -o ${WORKDIR}/hphosts-psh-raw - sed "/\.$/d" ${WORKDIR}/hphosts-psh-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/hphosts-psh + sed "/\.$/d" ${WORKDIR}/hphosts-psh-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/hphosts-psh rm ${WORKDIR}/hphosts-psh-raw } hphosts-pup() { # hphosts-pup ${FETCH} https://hosts-file.net/pup.txt -o ${WORKDIR}/hphosts-pup-raw - sed "/\.$/d" ${WORKDIR}/hphosts-pup-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/hphosts-pup + sed "/\.$/d" ${WORKDIR}/hphosts-pup-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/hphosts-pup rm ${WORKDIR}/hphosts-pup-raw } simplead() { # Simple Ad List ${FETCH} https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt -o ${WORKDIR}/simplead-raw - sed "/\.$/d" ${WORKDIR}/simplead-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/simplead + sed "/\.$/d" ${WORKDIR}/simplead-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/simplead rm ${WORKDIR}/simplead-raw } simpletrack() { # Simple Tracking List ${FETCH} https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt -o ${WORKDIR}/simpletrack-raw - sed "/\.$/d" ${WORKDIR}/simpletrack-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/simpletrack + sed "/\.$/d" ${WORKDIR}/simpletrack-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/simpletrack rm ${WORKDIR}/simpletrack-raw } diff --git a/dns/bind/src/opnsense/scripts/OPNsense/Bind/setup.sh b/dns/bind/src/opnsense/scripts/OPNsense/Bind/setup.sh index e46b77b4bd..64b2fd7083 100755 --- a/dns/bind/src/opnsense/scripts/OPNsense/Bind/setup.sh +++ b/dns/bind/src/opnsense/scripts/OPNsense/Bind/setup.sh @@ -1,17 +1,7 @@ #!/bin/sh -mkdir -p /var/run/named -chown -R bind:bind /var/run/named -chmod 755 /var/run/named - -mkdir -p /var/dump -chown -R bind:bind /var/dump -chmod 755 /var/dump - -mkdir -p /var/stats -chown -R bind:bind /var/stats -chmod 755 /var/stats - -mkdir -p /var/log/named -chown -R bind:bind /var/log/named -chmod 755 /var/log/named +for DIR in /var/run/named /var/dump /var/stats /var/log/named /usr/local/etc/namedb/primary; do + mkdir -p ${DIR} + chown -R bind:bind ${DIR} + chmod 755 ${DIR} +done diff --git a/dns/bind/src/opnsense/scripts/OPNsense/Bind/zoneCheck.sh b/dns/bind/src/opnsense/scripts/OPNsense/Bind/zoneCheck.sh new file mode 100755 index 0000000000..db3e85dac8 --- /dev/null +++ b/dns/bind/src/opnsense/scripts/OPNsense/Bind/zoneCheck.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# check the primary zone file validity + +ZONENAME=${1} +ZONEPATH="/usr/local/etc/namedb/primary/${ZONENAME}.db" +if checkzone_errors=$(named-checkzone ${ZONENAME} ${ZONEPATH} 2>&1); then + echo "Zone check completed successfully" + echo "$checkzone_errors" +else + echo "$checkzone_errors" +fi + +exit 0 diff --git a/dns/bind/src/opnsense/scripts/OPNsense/Bind/zoneShow.py b/dns/bind/src/opnsense/scripts/OPNsense/Bind/zoneShow.py new file mode 100755 index 0000000000..38af91d4df --- /dev/null +++ b/dns/bind/src/opnsense/scripts/OPNsense/Bind/zoneShow.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +# send primary zone file content to stdout + +import sys +import os.path +import glob +import ujson + +zone_name = sys.argv[1] +result = dict() +zone_config = [] +zone_files_root = '/usr/local/etc/namedb/primary/' +zone_file = zone_files_root + zone_name + '.db' + +def load_db_file(zone_file): + """ load db-file + """ + for line in open(zone_file, 'r').read().split('\n'): + zone_config.append(line.rstrip()) + +if os.path.isfile(zone_file): + result['path'] = zone_file + result['time'] = os.path.getmtime(zone_file) + load_db_file(zone_file) + result['zone_content'] = zone_config +print(ujson.dumps(result)) diff --git a/dns/bind/src/opnsense/scripts/syslog/logformats/bind.py b/dns/bind/src/opnsense/scripts/syslog/logformats/bind.py new file mode 100755 index 0000000000..c4ef34892b --- /dev/null +++ b/dns/bind/src/opnsense/scripts/syslog/logformats/bind.py @@ -0,0 +1,112 @@ +""" + Copyright (c) 2022 Robbert Rijkse + 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. +""" +import datetime +import re +from . import NewBaseLogFormat + +class BindGeneralLogFormat(NewBaseLogFormat): + def __init__(self, filename): + super().__init__(filename) + self._priority = 1 + self._parts = list() + + def match(self, line): + return self._filename.find('named/named.log') > -1 + + def set_line(self, line): + super().set_line(line) + self._parts = self._line.split(maxsplit=4) + + @property + def timestamp(self): + # bind format return actual log data + ts = datetime.datetime.strptime(f"{self._parts[0]} {self._parts[1]}", "%d-%b-%Y %H:%M:%S.%f") + return ts.isoformat() + + @property + def severity(self): + # Grab the log level + severity = self._parts[3].strip(":") + options = { + "critical": 2, + "error": 3, + "warning": 4, + "notice": 5, + "info": 6, + "debug": 7, + "dynamic": 7 + } + if severity in options: + return options[severity] + return None + + @property + def process_name(self): + # Grab the type of log message + return self._parts[2].strip(":") + + @property + def line(self): + # Only grab the left over message + return self._parts[4].strip() + +class BindQueryLogFormat(NewBaseLogFormat): + def __init__(self, filename): + super().__init__(filename) + self._priority = 1 + self._parts = list() + + def match(self, line): + return self._filename.find('named/query.log') > -1 or self._filename.find('named/rpz.log') > -1 + + def set_line(self, line): + super().set_line(line) + self._parts = self._line.split(maxsplit=7) + + @property + def timestamp(self): + # bind format return actual log data + ts = datetime.datetime.strptime(f"{self._parts[0]} {self._parts[1]}", "%d-%b-%Y %H:%M:%S.%f") + return ts.isoformat() + + @property + def pid(self): + # Grab the IP and Port number of the client + # pid is used for this because you can't define custom names + return self._parts[4].strip() + + @property + def facility(self): + # Grab the record the query was for + # facility is used for this because you can't define custom names + return self._parts[5].strip()[1:-2] + + @property + def process_name(self): + # Grab the client memory ID + # process_name is used for this because you can't define custom names + return self._parts[3].strip() + + @property + def line(self): + # Only grab the left over message + return self._parts[7].strip() diff --git a/dns/bind/src/opnsense/service/conf/actions.d/actions_bind.conf b/dns/bind/src/opnsense/service/conf/actions.d/actions_bind.conf index e272a068fb..c3d4a47a5e 100644 --- a/dns/bind/src/opnsense/service/conf/actions.d/actions_bind.conf +++ b/dns/bind/src/opnsense/service/conf/actions.d/actions_bind.conf @@ -1,5 +1,5 @@ [start] -command:/usr/local/opnsense/scripts/OPNsense/Bind/setup.sh;/usr/local/etc/rc.d/named start +command:/usr/local/etc/rc.d/named start parameters: type:script message:starting BIND @@ -11,13 +11,13 @@ type:script message:stopping BIND [restart] -command:/usr/local/opnsense/scripts/OPNsense/Bind/setup.sh;/usr/local/etc/rc.d/named restart +command:/usr/local/etc/rc.d/named restart parameters: type:script message:restarting BIND [status] -command:/usr/local/etc/rc.d/named status;exit 0 +command:/usr/local/etc/rc.d/named status; exit 0 parameters: type:script_output message:request BIND status @@ -35,8 +35,18 @@ type:script message:fetching DNSBLs [dnsblcron] -command:/usr/local/opnsense/scripts/OPNsense/Bind/dnsbl.sh;/usr/local/etc/rc.d/named reload +command:/usr/local/opnsense/scripts/OPNsense/Bind/dnsbl.sh; /usr/local/etc/rc.d/named reload parameters: type:script message:fetching DNSBLs and restart description: Download BIND DNSBLs and restart + +[zone.check] +command:/usr/local/opnsense/scripts/OPNsense/Bind/zoneCheck.sh +parameters: %s +type:script_output + +[zone.show] +command:/usr/local/opnsense/scripts/OPNsense/Bind/zoneShow.py +parameters: %s +type:script_output diff --git a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/+TARGETS b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/+TARGETS index 33d000bba7..1e155e5a5b 100644 --- a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/+TARGETS +++ b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/+TARGETS @@ -1,11 +1,11 @@ -bing.db:/usr/local/etc/namedb/master/bing.db -blacklist.db:/usr/local/etc/namedb/master/blacklist.db -domain.db:/usr/local/etc/namedb/master/[OPNsense.bind.domain.domains.domain.%.domainname].db -duckduckgo.db:/usr/local/etc/namedb/master/duckduckgo.db -google.db:/usr/local/etc/namedb/master/google.db +bing.db:/usr/local/etc/namedb/primary/bing.db +blacklist.db:/usr/local/etc/namedb/primary/blacklist.db +domain.db:/usr/local/etc/namedb/primary/[OPNsense.bind.domain.domains.domain.%.domainname].db +duckduckgo.db:/usr/local/etc/namedb/primary/duckduckgo.db +google.db:/usr/local/etc/namedb/primary/google.db named:/etc/rc.conf.d/named named.conf:/usr/local/etc/namedb/named.conf rndc.conf:/usr/local/etc/namedb/rndc.conf -whitelist.db:/usr/local/etc/namedb/master/whitelist.db +whitelist.db:/usr/local/etc/namedb/primary/whitelist.db whitelist.inc:/usr/local/etc/namedb/whitelist.inc -youtube.db:/usr/local/etc/namedb/master/youtube.db +youtube.db:/usr/local/etc/namedb/primary/youtube.db diff --git a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/domain.db b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/domain.db index d24ecd3ee5..bce3a9ffdd 100644 --- a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/domain.db +++ b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/domain.db @@ -2,14 +2,16 @@ {% if helpers.exists('OPNsense.bind.domain.domains.domain') %} {% for domaindb in helpers.toList('OPNsense.bind.domain.domains.domain') %} {% if TARGET_FILTERS['OPNsense.bind.domain.domains.domain.' ~ loop.index0] or TARGET_FILTERS['OPNsense.bind.domain.domains.domain'] %} -{% if domaindb.enabled == '1' and domaindb.type == 'master' %} +{% if domaindb.enabled == '1' and domaindb.type == 'primary' %} $TTL {{ domaindb.ttl }} @ IN SOA {{ domaindb.dnsserver }}. {{ domaindb.mailadmin|replace('@', '.') }}. ( {{ domaindb.serial|trim }} {{ domaindb.refresh }} {{ domaindb.retry }} {{ domaindb.expire }} {{ domaindb.negative }} ) -{% for record in helpers.sortDictList(OPNsense.bind.record.records.record, 'name', 'type' ) %} -{% if record.domain == domaindb['@uuid'] %} +{% if helpers.exists('OPNsense.bind.record.records.record') %} +{% for record in helpers.sortDictList(OPNsense.bind.record.records.record, 'name', 'type' ) %} +{% if record.domain == domaindb['@uuid'] %} {{ record.name }} {{ record.type }} {{ record.value }} -{% endif %} -{% endfor %} +{% endif %} +{% endfor %} +{% endif %} {% endif %} {% endif %} {% endfor %} diff --git a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named index 1a4c02a000..34d82fcae3 100644 --- a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named +++ b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named @@ -1,5 +1,4 @@ {% if helpers.exists('OPNsense.bind.general.enabled') and OPNsense.bind.general.enabled == '1' %} -named_var_script="/usr/local/opnsense/scripts/OPNsense/Bind/setup.sh" {% if helpers.exists('OPNsense.bind.general.disablev6') and OPNsense.bind.general.disablev6 == '1' %} named_flags="-4" {% endif %} @@ -8,6 +7,7 @@ named_flags="-4" named_dnsbl="{{ OPNsense.bind.dnsbl.type }}" {% endif %} {% endif %} +named_setup="/usr/local/opnsense/scripts/OPNsense/Bind/setup.sh" named_enable="YES" {% else %} named_enable="NO" diff --git a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named.conf b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named.conf index 376efe90fa..9196b5de3e 100644 --- a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named.conf +++ b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/named.conf @@ -15,12 +15,12 @@ options { dump-file "/var/dump/named_dump.db"; statistics-file "/var/stats/named.stats"; -{% if helpers.exists('OPNsense.bind.general.listenv4') and OPNsense.bind.general.listenv4 != '' and helpers.exists('OPNsense.bind.general.port') and OPNsense.bind.general.port != '' %} - listen-on port {{ OPNsense.bind.general.port }} { {{ OPNsense.bind.general.listenv4.replace(',', '; ') }}; }; -{% endif %} -{% if helpers.exists('OPNsense.bind.general.listenv6') and OPNsense.bind.general.listenv6 != '' and helpers.exists('OPNsense.bind.general.port') and OPNsense.bind.general.port != '' %} - listen-on-v6 port {{ OPNsense.bind.general.port }} { {{ OPNsense.bind.general.listenv6.replace(',', '; ') }}; }; -{% endif -%} +{% for listenv4 in OPNsense.bind.general.listenv4.split(',') %} + listen-on port {{ OPNsense.bind.general.port }} { {% if listenv4 == '0.0.0.0' %}any{% else %}{{ listenv4 }}{% endif %}; }; +{% endfor %} +{% for listenv6 in OPNsense.bind.general.listenv6.split(',') %} + listen-on-v6 port {{ OPNsense.bind.general.port }} { {% if listenv6 == '::' %}any{% else %}{{ listenv6 }}{% endif %}; }; +{% endfor %} {% if helpers.exists('OPNsense.bind.general.querysource') and OPNsense.bind.general.querysource != '' %} query-source {{ OPNsense.bind.general.querysource }}; @@ -47,18 +47,31 @@ options { {% endif %} {% if helpers.exists('OPNsense.bind.general.recursion') and OPNsense.bind.general.recursion != '' %} -{% for list in helpers.toList('OPNsense.bind.general.recursion') %} -{% set recursionlist = helpers.getUUID(list) %} recursion yes; - allow-recursion { {{ recursionlist.name }}; }; -{% endfor %} + allow-recursion { +{% for acl in OPNsense.bind.general.recursion.split(',') %} +{% set recursion_acl = helpers.getUUID(acl) %} + {{ recursion_acl.name }}; +{% endfor %} + }; {% endif %} {% if helpers.exists('OPNsense.bind.general.allowtransfer') and OPNsense.bind.general.allowtransfer != '' %} -{% for list in helpers.toList('OPNsense.bind.general.allowtransfer') %} -{% set allowtransfer = helpers.getUUID(list) %} - allow-transfer { {{ allowtransfer.name }}; }; -{% endfor %} + allow-transfer { +{% for acl in OPNsense.bind.general.allowtransfer.split(',') %} +{% set transfer_acl = helpers.getUUID(acl) %} + {{ transfer_acl.name }}; +{% endfor %} + }; +{% endif %} + +{% if helpers.exists('OPNsense.bind.general.allowquery') and OPNsense.bind.general.allowquery != '' %} + allow-query { +{% for acl in OPNsense.bind.general.allowquery.split(',') %} +{% set query_acl = helpers.getUUID(acl) %} + {{ query_acl.name }}; +{% endfor %} + }; {% endif %} {% if helpers.exists('OPNsense.bind.general.maxcachesize') and OPNsense.bind.general.maxcachesize != '' %} @@ -88,54 +101,107 @@ options { {% endif %} }; +{% if helpers.exists('OPNsense.bind.general.rndcalgo') and helpers.exists('OPNsense.bind.general.rndcsecret') %} key "rndc-key" { - algorithm hmac-sha256; - secret "VxtIzJevSQXqnr7h2qerrcwjnZlMWSGGFBndKeNIDfw="; + algorithm "{{ OPNsense.bind.general.rndcalgo }}"; + secret "{{ OPNsense.bind.general.rndcsecret }}"; }; controls { inet 127.0.0.1 port 9530 allow { 127.0.0.1; } keys { "rndc-key"; }; }; +{% endif %} + +include "/usr/local/etc/namedb/named.conf.d/*.conf"; zone "." { type hint; file "/usr/local/etc/namedb/named.root"; }; -zone "localhost" { type master; file "/usr/local/etc/namedb/master/localhost-forward.db"; }; -zone "127.in-addr.arpa" { type master; file "/usr/local/etc/namedb/master/localhost-reverse.db"; }; -zone "0.ip6.arpa" { type master; file "/usr/local/etc/namedb/master/localhost-reverse.db"; }; +zone "localhost" { type primary; file "/usr/local/etc/namedb/primary/localhost-forward.db"; }; +zone "127.in-addr.arpa" { type primary; file "/usr/local/etc/namedb/primary/localhost-reverse.db"; }; +zone "0.ip6.arpa" { type primary; file "/usr/local/etc/namedb/primary/localhost-reverse.db"; }; {% if helpers.exists('OPNsense.bind.dnsbl.enabled') and OPNsense.bind.dnsbl.enabled == '1' %} {% if helpers.exists('OPNsense.bind.dnsbl.type') and OPNsense.bind.dnsbl.type != '' %} -zone "whitelist.localdomain" { type master; file "/usr/local/etc/namedb/master/whitelist.db"; notify no; check-names ignore; }; -zone "blacklist.localdomain" { type master; file "/usr/local/etc/namedb/master/blacklist.db"; notify no; check-names ignore; }; +zone "whitelist.localdomain" { type primary; file "/usr/local/etc/namedb/primary/whitelist.db"; notify no; check-names ignore; }; +zone "blacklist.localdomain" { type primary; file "/usr/local/etc/namedb/primary/blacklist.db"; notify no; check-names ignore; }; {% endif %} {% endif %} {% if helpers.exists('OPNsense.bind.dnsbl.enabled') and OPNsense.bind.dnsbl.enabled == '1' %} {% if helpers.exists('OPNsense.bind.dnsbl.forcesafegoogle') and OPNsense.bind.dnsbl.forcesafegoogle == '1' %} -zone "rpzgoogle" { type master; file "/usr/local/etc/namedb/master/google.db"; notify no; check-names ignore; }; +zone "rpzgoogle" { type primary; file "/usr/local/etc/namedb/primary/google.db"; notify no; check-names ignore; }; {% endif %} {% endif %} {% if helpers.exists('OPNsense.bind.dnsbl.enabled') and OPNsense.bind.dnsbl.enabled == '1' %} {% if helpers.exists('OPNsense.bind.dnsbl.forcesafeduckduckgo') and OPNsense.bind.dnsbl.forcesafeduckduckgo == '1' %} -zone "rpzduckduckgo" { type master; file "/usr/local/etc/namedb/master/duckduckgo.db"; notify no; check-names ignore; }; +zone "rpzduckduckgo" { type primary; file "/usr/local/etc/namedb/primary/duckduckgo.db"; notify no; check-names ignore; }; {% endif %} {% endif %} {% if helpers.exists('OPNsense.bind.dnsbl.enabled') and OPNsense.bind.dnsbl.enabled == '1' %} {% if helpers.exists('OPNsense.bind.dnsbl.forcesafeyoutube') and OPNsense.bind.dnsbl.forcesafeyoutube == '1' %} -zone "rpzyoutube" { type master; file "/usr/local/etc/namedb/master/youtube.db"; notify no; check-names ignore; }; +zone "rpzyoutube" { type primary; file "/usr/local/etc/namedb/primary/youtube.db"; notify no; check-names ignore; }; {% endif %} {% endif %} {% if helpers.exists('OPNsense.bind.dnsbl.enabled') and OPNsense.bind.dnsbl.enabled == '1' %} {% if helpers.exists('OPNsense.bind.dnsbl.forcestrictbing') and OPNsense.bind.dnsbl.forcestrictbing == '1' %} -zone "rpzbing" { type master; file "/usr/local/etc/namedb/master/bing.db"; notify no; check-names ignore; }; +zone "rpzbing" { type primary; file "/usr/local/etc/namedb/primary/bing.db"; notify no; check-names ignore; }; {% endif %} {% endif %} {% if helpers.exists('OPNsense.bind.domain.domains.domain') %} +{% set usedkeys = [] %} {% for domain in helpers.toList('OPNsense.bind.domain.domains.domain') %} {% if domain.enabled == '1' %} -{% set allow_transfer = helpers.getUUID(domain.allowtransfer) %} -{% set allow_query = helpers.getUUID(domain.allowquery) %} -zone "{{ domain.domainname }}" { type {{ domain.type }}; {% if domain.type == 'slave' %}masters { {{ domain.masterip.replace(',', '; ') }}; }; {% if domain.allownotifyslave is defined %} allow-notify { {{ domain.allownotifyslave.replace(',', '; ') }}; };{% endif %} file "/usr/local/etc/namedb/slave/{{ domain.domainname }}.db"; {% else %}file "/usr/local/etc/namedb/master/{{ domain.domainname }}.db"; {% endif %}{% if domain.allowtransfer is defined %} allow-transfer { {{ allow_transfer.name }}; };{% endif %}{% if domain.allowquery is defined %} allow-query { {{ allow_query.name }}; };{% endif %} }; +zone "{{ domain.domainname }}" { + type {{ domain.type }}; +{% if domain.type == 'forward' %} + forwarders { {{ domain.forwardserver.replace(',', '; ') }}; }; +{% elif domain.type == 'secondary' %} +{% if domain.transferkey is defined %} + primaries { {{ domain.primaryip.replace(',', ' key "' ~ domain.transferkeyname ~ '"; ') }} key "{{ domain.transferkeyname }}"; }; +{% else %} + primaries { {{ domain.primaryip.replace(',', '; ') }}; }; +{% endif %} +{% if domain.allownotifysecondary is defined %} + allow-notify { {{ domain.allownotifysecondary.replace(',', '; ') }}; }; +{% endif %} + file "/usr/local/etc/namedb/secondary/{{ domain.domainname }}.db"; +{% elif domain.type == 'primary' %} + file "/usr/local/etc/namedb/primary/{{ domain.domainname }}.db"; +{% endif %} +{% if domain.allowtransfer is defined or (domain.allowrndctransfer is defined and domain.allowrndctransfer == "1") %} + allow-transfer { +{% if domain.allowrndctransfer is defined and domain.allowrndctransfer == "1" %} + key "rndc-key"; +{% endif %} +{% if domain.allowtransfer is defined %} +{% for acl in domain.allowtransfer.split(',') %} +{% set transfer_acl = helpers.getUUID(acl) %} + {{ transfer_acl.name }}; +{% endfor %} +{% endif %} + }; +{% endif %} +{% if domain.allowquery is defined %} + allow-query { +{% for acl in domain.allowquery.split(',') %} +{% set query_acl = helpers.getUUID(acl) %} + {{ query_acl.name }}; +{% endfor %} + }; +{% endif %} +{% if domain.allowrndcupdate is defined and domain.allowrndcupdate == "1" and domain.type == 'primary' %} + update-policy { + grant rndc-key zonesub ANY; + }; +{% endif %} +}; +{% if domain.type == 'secondary' and domain.transferkey is defined and not(domain.transferkeyname in usedkeys) %} +{% do usedkeys.append(domain.transferkeyname) %} +key "{{ domain.transferkeyname }}" { + algorithm "{{ domain.transferkeyalgo }}"; + secret "{{ domain.transferkey }}"; +}; +{% endif %} {% endif %} {% endfor %} {% endif %} @@ -149,6 +215,14 @@ logging { print-category yes; }; + channel default_syslog { + print-time yes; + print-category yes; + print-severity yes; + syslog daemon; + severity {% if helpers.exists('OPNsense.bind.general.general_log_level') %}{{ OPNsense.bind.general.general_log_level }}{% else %}info{% endif %}; + }; + channel query_log { file "/var/log/named/query.log" versions 3 size {{ OPNsense.bind.general.logsize }}m; print-time yes; @@ -159,10 +233,35 @@ logging { print-time yes; }; - category default { default_log; }; - category general { default_log; }; + category default { default_syslog; }; + category general { default_syslog; }; + category config { default_syslog; }; + category dispatch { default_syslog; }; + category network { default_syslog; }; category queries { query_log; }; category rpz { rpz_log; }; category lame-servers { null; }; }; {% endif %} + +{% if helpers.exists('OPNsense.bind.general.filteraaaav4') and OPNsense.bind.general.filteraaaav4 == '1' or helpers.exists('OPNsense.bind.general.filteraaaav6') and OPNsense.bind.general.filteraaaav6 == '1' %} +plugin query "/usr/local/lib/bind/filter-aaaa.so" { +{% if helpers.exists('OPNsense.bind.general.filteraaaav4') and OPNsense.bind.general.filteraaaav4 == '1' %} +{% if OPNsense.bind.general.dnssecvalidation == 'no' %} + filter-aaaa-on-v4 break-dnssec; +{% else %} + filter-aaaa-on-v4 yes; +{% endif %} +{% endif %} +{% if helpers.exists('OPNsense.bind.general.filteraaaav6') and OPNsense.bind.general.filteraaaav6 == '1' %} +{% if OPNsense.bind.general.dnssecvalidation == 'no' %} + filter-aaaa-on-v6 break-dnssec; +{% else %} + filter-aaaa-on-v6 yes; +{% endif %} +{% endif %} +{% if helpers.exists('OPNsense.bind.general.filteraaaaacl') and OPNsense.bind.general.filteraaaaacl != '' %} + filter-aaaa { {{ OPNsense.bind.general.filteraaaaacl.replace(',', '; ') }}; }; +{% endif %} + }; +{% endif %} diff --git a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/rndc.conf b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/rndc.conf index ec98967398..d4800520f1 100644 --- a/dns/bind/src/opnsense/service/templates/OPNsense/Bind/rndc.conf +++ b/dns/bind/src/opnsense/service/templates/OPNsense/Bind/rndc.conf @@ -1,6 +1,7 @@ +{% if helpers.exists('OPNsense.bind.general.rndcalgo') and helpers.exists('OPNsense.bind.general.rndcsecret') %} key "rndc-key" { - algorithm hmac-sha256; - secret "VxtIzJevSQXqnr7h2qerrcwjnZlMWSGGFBndKeNIDfw="; + algorithm "{{ OPNsense.bind.general.rndcalgo }}"; + secret "{{ OPNsense.bind.general.rndcsecret }}"; }; options { @@ -8,3 +9,4 @@ options { default-server 127.0.0.1; default-port 9530; }; +{% endif %} diff --git a/dns/bind/src/opnsense/service/templates/OPNsense/Syslog/local/named.conf b/dns/bind/src/opnsense/service/templates/OPNsense/Syslog/local/named.conf new file mode 100644 index 0000000000..d301237e4a --- /dev/null +++ b/dns/bind/src/opnsense/service/templates/OPNsense/Syslog/local/named.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [named]. +################################################################### +filter f_local_named { + program("named"); +}; diff --git a/dns/ddclient/Makefile b/dns/ddclient/Makefile new file mode 100644 index 0000000000..469f49c666 --- /dev/null +++ b/dns/ddclient/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= ddclient +PLUGIN_VERSION= 1.29 +PLUGIN_REVISION= 1 +PLUGIN_DEPENDS= ddclient py${PLUGIN_PYTHON}-boto3 +PLUGIN_COMMENT= Dynamic DNS client +PLUGIN_MAINTAINER= ad@opnsense.org + +.include "../../Mk/plugins.mk" diff --git a/dns/ddclient/pkg-descr b/dns/ddclient/pkg-descr new file mode 100644 index 0000000000..7ead5a14f1 --- /dev/null +++ b/dns/ddclient/pkg-descr @@ -0,0 +1,192 @@ +This plugin offers dynamic DNS capabilities using a native backend +or ddclient. The native backend is the default implementation. +ddclient is a Perl client used to update dynamic DNS entries for +accounts on many dynamic DNS services. + +Plugin Changelog +================ + +1.29 + +* Add native backend support for Hetzner DNS (contributed by Michael J. Arcan) +* Add native backend support for dnspod.cn (contributed by Ansen) +* Add Cloudflare DNS IP check option (contributed by GTechAlpha) + +1.28 + +* Add native backend support for PowerDNS API (contributed by Oliver Traber) + +1.27 + +* Add support for altering IPv6 addresses in native backend (contributed by SaarLAN-Pissbeutel) +* Add Akamai to checkip providers (contributed by Rajiv Aaron Manglani) +* Fix Netcup host/domain recognition (contributed by SaarLAN-Pissbeutel) +* Empty IP send to DNS provider and replace dyndns by dynu (contributed by Meliox) +* Removed defunct ip4only.me and ip6only.me + +1.26 + +* Add ddclient TTL configuration in Gandi and GoDaddy (contributed by David PHAM-VAN) + +1.25 + +* Add DigitalOcean support to native backend (contributed by Olly Baker) + +1.24 + +* Refactored IP matching (contributed by Rob van Oostenrijk) + +1.23 + +* Add dashboard widget + +1.22 + +* Add gandi support +* Optionally support descriptive values for account selection when using native backend + +1.21 + +* Add Netcup support (contributed by Ingo Lafrenz) +* Use '==' instead of 'is' in Domeneshop Python support (contributed by ssmendon) +* Update DNS record instead of overwriting in Cloudflare Python support (contributed by lin-xianming) +* Improve service information fetch by avoiding creation of a model + +1.20 + +* Add system parameter to native dyndns2 requesto (contributed by Jakub Gargul) +* Add Digitalocean support (contributed by Mathias Schneuwly) +* Add Mythin Beasts support (contributed by PeterF) + +1.19 + +* Add Porkbun support (contributed by briandur) +* Add native service for Domeneshop (contributed by Bernhard Frenking) + +1.18 + +* Update to ddclient 3.11.2 FreeBSD ports version +* Default to native backend for new installs +* Fix permission of ddclient.json + +1.17 + +* Update to ddclient 3.11.1 (dnsexit legacy support removed) + +1.16 + +* Add custom GET/PUT protocols to native backend (contributed by DaCookie4u) +* Consider all 2xx status codes as success in native dyndns2 implementation + +1.15 + +* Add AWS Route53 and DuckDNS to native backend (contributed by Greg Glockner) +* Fix JSON output with disabled trailing accounts/escaping and empty stats in AccountField +* Rename Python-based "OPNsense" backend to "native" to prevent ambiguity +* Do not update on native backend when IP detection failed and emit a warning instead +* Add desec to native backend (contributed by Clemens Hardewig) +* Fix ClouDNS missing dynurl= parameter +* Clean up ddclient.conf template + +1.14 + +* Add "post" protocol in custom service type +* Add DNSExit API and regfish.de support to ddclient backend + +1.13 + +* Fix not returning IP address as a string in native backend (contributed by Sean Kelly) +* Fix PID file handling for native backend +* Use API token for cloudflare native backend if available (contributed by juantxorena) +* Read proxied attribute from cloudflare hostname and send it back to prevent it being removed +* Move accounting of "last accessed timestamp" to poller in native backend +* Change if= use to proper ifv4=/ifv6= use (contributed by Rhys Barrie) + +1.12 + +* Add cloudflare implementation for native backend (contributed by Thomas Cekal) +* Allow custom target hostname for dyndns2 protocol in native backend +* Adjust for missing ipv6= option including upstream patches for use=/usev4=/usev6= +* Require a selected interface through validation when interface check method is used + +1.11 + +* Add Python-based native backend support for custom ddclient-like implementation using the same input +* Add AzureDNS backende using OAuth 2.0 +* Add dyndns2 backend using said API + +1.10 + +* Update to ddclient 3.10.0 +* Add 1984 support (contributed by Luca Schoeneberg) +* Add ClouDNS support (contributed by Luca Schoeneberg) +* Add Dinahosting support (contributed by Luca Schoeneberg) +* Add DNSExit support (contributed by Luca Schoeneberg) +* Add DonDominio support (contributed by Luca Schoeneberg) +* Add Freemyip support (contributed by Luca Schoeneberg) +* Add godaddy support (contributed by Luca Schoeneberg) +* Add Hetzner support (contributed by Luca Schoeneberg) +* Add Key-Systems support (contributed by Luca Schoeneberg) +* Add NearlyFreeSpeech.NET support (contributed by Luca Schoeneberg) +* Add Njal.la support (contributed by satrapes) +* Add sitelutions support (contributed by Luca Schoeneberg) +* Add woima support (contributed by Luca Schoeneberg) +* Add Yandex support (contributed by Luca Schoeneberg) + +1.9 + +* Add icanhazip.com as a checkip provider (contributed by Matt Parnell) +* Configurable checkip (contributed by Christian Schulze) +* Allow % characters in usernames +* Fix parsing short IPv6 addresses from external service (contributed by Patrick Grupp) + +1.8 + +* Add a force action available via cron +* Fix expected permission on ddclient.conf +* Make service status and stop more reliable +* Time out checkip script after 10 seconds + +1.7 + +* Add current ip address and updated timestamp to search api and grid + +1.6 + +* Add Gandi support (contributed by Neozlag) + +1.5 + +* Add service control, XMLRPC registration and syslog target +* Add Servercow support (contributed by FreddleSpl0it) + +1.4 + +* Add advanced general setting to allow updates via IPv6 +* Enforce SSL on global level with account setting + +1.3 + +* Add checkip settings per account using selected source interface when provided +* Add OVH DynHost to the DynDNS providers (contributed by toxic0berliner) + +1.2 + +* Add Loopia (contributed by Johan Lilja) +* Add DNS Made Easy, FreeDNS and Dynu (contributed by Rene Schuster) +* Add root zone and wildcard support + +1.1 + +* Add spdyn, inwx and dns-o-matic (contributed by Rene Schuster) +* Add Hurricane Electric provider (contributed by Netboy3) +* Add option to force SSL, on by default (contributed by Robin Mueller) +* Add Cloudflare and custom service (contributed by Robin Mueller) +* Add STRATO provider (contributed by Alex Mi) +* Add use interface as IP source +* Fix ip6only.me (contributed by Robin Mueller) +* Fix uppercase use in usernames + +1.0 + +* Initial release diff --git a/dns/ddclient/src/etc/inc/plugins.inc.d/ddclient.inc b/dns/ddclient/src/etc/inc/plugins.inc.d/ddclient.inc new file mode 100644 index 0000000000..e41cb18354 --- /dev/null +++ b/dns/ddclient/src/etc/inc/plugins.inc.d/ddclient.inc @@ -0,0 +1,79 @@ +object(); + $is_enabled = false; + if ($cnf->OPNsense && $cnf->OPNsense->DynDNS && $cnf->OPNsense->DynDNS->general) { + $is_enabled = $cnf->OPNsense->DynDNS->general->enabled == '1'; + } + + if ($is_enabled) { + $service = [ + 'description' => gettext('ddclient'), + 'configd' => [ + 'restart' => ['ddclient restart'], + 'start' => ['ddclient start'], + 'stop' => ['ddclient stop'], + ], + 'name' => 'ddclient', + ]; + $service['pidfile'] = $cnf->OPNsense->DynDNS->general->backend != 'opnsense' ? '/var/run/ddclient.pid' : '/var/run/ddclient_opn.pid'; + $services[] = $service; + } + + return $services; +} + +function ddclient_xmlrpc_sync() +{ + $result = []; + + $result[] = [ + 'description' => gettext('ddclient'), + 'section' => 'OPNsense.DynDNS', + 'services' => ['ddclient'], + 'id' => 'ddclient', + ]; + + return $result; +} + +function ddclient_syslog() +{ + $logfacilities = []; + + $logfacilities['ddclient'] = [ + 'facility' => ['ddclient'], + ]; + + return $logfacilities; +} diff --git a/dns/ddclient/src/etc/rc.d/ddclient_opn b/dns/ddclient/src/etc/rc.d/ddclient_opn new file mode 100755 index 0000000000..791fbcb682 --- /dev/null +++ b/dns/ddclient/src/etc/rc.d/ddclient_opn @@ -0,0 +1,61 @@ +#!/bin/sh +# +# PROVIDE: ddclient_py +# REQUIRE: SERVERS +# KEYWORD: shutdown +# + +. /etc/rc.subr + +name=ddclient_opn +rcvar=ddclient_opn_enable +command=/usr/local/opnsense/scripts/ddclient/ddclient_opn.py +command_interpreter=/usr/local/bin/python3 +pidfile="/var/run/${name}.pid" +load_rc_config $name + +# Set defaults +: ${ddclient_opn_enable:=NO} + +start_postcmd=ddclient_opn_poststart +stop_cmd=ddclient_opn_stop + +ddclient_opn_poststart() +{ + # give the daemon some time to initialize its configuration + for i in 1 2 3 4 5; do + sleep 1 + + if [ -s ${rc_pid} ]; then + break + fi + done +} + +ddclient_opn_stop() +{ + if [ -z "$rc_pid" ]; then + [ -n "$rc_fast" ] && return 0 + _run_rc_notrunning + return 1 + fi + echo -n "Stopping ${name}." + kill -15 ${rc_pid} + # wait max 2 seconds for gentle exit + for i in $(seq 1 20); + do + if [ -z "`/bin/ps -ex | /usr/bin/awk '{print $1;}' | /usr/bin/grep "^${rc_pid}"`" ]; then + break + fi + sleep 0.1 + done + + for ddclient_pid in `/bin/ps -ex | grep 'ddclient_opn.py' | /usr/bin/awk '{print $1;}' ` + do + kill -9 $ddclient_pid >/dev/null 2>&1 + done + + echo "..done" +} + +run_rc_command $1 diff --git a/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/Api/AccountsController.php b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/Api/AccountsController.php new file mode 100644 index 0000000000..f15dab90c9 --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/Api/AccountsController.php @@ -0,0 +1,84 @@ +searchBase( + "accounts.account", + [ + 'enabled', 'service', 'description', 'username', 'hostnames', 'use_interface', + 'interface', 'protocol', 'current_ip', 'current_mtime' + ], + "description" + ); + foreach ($result['rows'] as &$row) { + if ($row['service'] == 'Custom') { + $row['service'] = 'Custom (' . $row['protocol'] . ')'; + } + unset($row['protocol']); + } + return $result; + } + + public function setItemAction($uuid) + { + return $this->setBase("account", "accounts.account", $uuid); + } + + public function addItemAction() + { + return $this->addBase("account", "accounts.account"); + } + + public function getItemAction($uuid = null) + { + return $this->getBase("account", "accounts.account", $uuid); + } + + public function delItemAction($uuid) + { + return $this->delBase("accounts.account", $uuid); + } + + public function toggleItemAction($uuid, $enabled = null) + { + return $this->toggleBase("accounts.account", $uuid, $enabled); + } +} diff --git a/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/Api/ServiceController.php b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/Api/ServiceController.php new file mode 100644 index 0000000000..1804fb3d82 --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/Api/ServiceController.php @@ -0,0 +1,44 @@ + [ + 'general' => $data['ddclient']['general'] + ] + ]; + } +} diff --git a/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/IndexController.php b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/IndexController.php new file mode 100644 index 0000000000..4214f012ff --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/IndexController.php @@ -0,0 +1,47 @@ +view->formDialogAccount = $this->getForm("dialogAccount"); + $this->view->formSettings = $this->getForm("settings"); + // choose template + $this->view->pick('OPNsense/DynDNS/index'); + } +} diff --git a/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml new file mode 100644 index 0000000000..0ffcf2040e --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/dialogAccount.xml @@ -0,0 +1,114 @@ +
+ + account.enabled + + checkbox + Enable this virtual server + + + account.description + + text + + + account.service + + dropdown + Select the service to use. + + + account.protocol + + dropdown + Select the protocol to use. + + + + account.server + + text + DynDNS Server hostname or uri to use (depending on the protocol). + When a URI is provided, the tag __MYIP__ will be replaced with the current detected address for this service + and __HOSTNAME__ will contain the (comma separated) list of hostnames provided. + + + + + account.resourceId + + text + true + + + account.username + + text + Username or login to use + + + account.password + + password + Password associated with this account + + + account.wildcard + + checkbox + + add a DNS wildcard CNAME record that points to the configured host. + + + account.zone + + text + + Zone containing the host entry. + + + account.hostnames + + select_multiple + + true + Hostname to update + + + account.ttl + + text + + Time to Live for the DNS entry + + + account.checkip + + dropdown + How to determine the address to use for this host + + + account.interface + + dropdown + + + account.dynipv6host + + text + true + Swap the interface identifier of the ipv6 address with the given partial ipv6 address (the least significant 64 bits of the address) + + + account.checkip_timeout + + text + How long to wait before the checkip process times out + + + account.force_ssl + + checkbox + Force update using HTTPS, please note setting this option will enforce https updates on all accounts + as ddclient only supports SSL=yes on a global level (the check ip service may still use HTTP on other services) + +
diff --git a/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/settings.xml b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/settings.xml new file mode 100644 index 0000000000..5c12c5c428 --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/controllers/OPNsense/DynDNS/forms/settings.xml @@ -0,0 +1,34 @@ +
+ + ddclient.general.enabled + + checkbox + Enable ddclient + + + ddclient.general.verbose + + checkbox + true + Enable verbose logging + + + ddclient.general.allowipv6 + + checkbox + true + Allow IPv6 for updates + + + ddclient.general.daemon_delay + + text + Interval in seconds to check for address changes + + + ddclient.general.backend + + dropdown + Select the backend to use. + +
diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/ACL/ACL.xml b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/ACL/ACL.xml new file mode 100644 index 0000000000..5e0cde48b2 --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/ACL/ACL.xml @@ -0,0 +1,11 @@ + + + Services: Dynamic DNS + + ui/dyndns/* + api/dyndns/accounts/* + api/dyndns/service/* + api/dyndns/settings/* + + + diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.php b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.php new file mode 100644 index 0000000000..a3cbb5a866 --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.php @@ -0,0 +1,91 @@ +getFlatNodes() as $key => $node) { + $tagName = $node->getInternalXMLTagName(); + $parentNode = $node->getParentNode(); + if ($validateFullModel || $node->isFieldChanged()) { + if ($parentNode->getInternalXMLTagName() === 'account' && in_array($tagName, ['protocol', 'server'])) { + $parentKey = $parentNode->__reference; + $validate_servers[$parentKey] = $parentNode; + } + } + } + foreach ($validate_servers as $key => $node) { + if ((string)$node->service == 'powerdns') { + if (empty($srv) || filter_var($srv, FILTER_VALIDATE_URL) === false) { + $messages->appendMessage( + new Message( + gettext("A valid URI is required."), + $key . ".server" + ) + ); + } + } + if ((string)$node->service != 'custom') { + continue; + } + $srv = (string)$node->server; + if (in_array((string)$node->protocol, ['get', 'post', 'put'])) { + if (empty($srv) || filter_var($srv, FILTER_VALIDATE_URL) === false) { + $messages->appendMessage( + new Message( + gettext("A valid URI is required."), + $key . ".server" + ) + ); + } + } else { + if (empty($srv) || filter_var($srv, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) { + $messages->appendMessage( + new Message( + gettext("A valid domain is required."), + $key . ".server" + ) + ); + } + } + } + return $messages; + } +} diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml new file mode 100644 index 0000000000..75c390ce1e --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml @@ -0,0 +1,214 @@ + + //OPNsense/DynDNS + 1.5.1 + Dynamic DNS client + + + + 1 + Y + + + 0 + Y + + + 0 + Y + + + 300 + Y + 1 + 86400 + + + Y + opnsense + A backend is required. + + ddclient + native + + + + + + + 1 + Y + + + Y + A service type is required. + + 1984 + Changeip + Cloudflare + ClouDNS + Digitalocean + dinahosting + DNS Made Easy (digicert) + DNS-O-Matic + DNSExit + DynDNS.com + DnsPark + dnspodcn + DSLReports + DonDominio + Duck DNS + Dynu + easyDNS + FreeDNS + freeMyIP + gandi.net + GoDaddy + Google + HE.net + HE.net TunnelBroker + Hetzner DNS Console + INWX + Key-Systems + Loopia + Mythic Beasts + NameCheap + NearlyFreeSpeech.net + Njalla + no-ip + nsupdate.info (IPv4) + nsupdate.info (IPv6) + OVHcloud DynHost + Porkbun + regfish.de + Servercow + Sitelutions + spDYN + STRATO + Woima + Yandex + Zoneedit + Custom + + + + N + A protocol type is required. + + DynDNS 1 + DynDNS 2 + Custom GET + Custom POST + Custom PUT + + + + N + + + N + /^([a-zA-Z0-9\-.@_:+\%])*$/u + The username contains invalid characters. + + + N + /^[^\n]*$/ + + + N + /^[^\n]*$/ + resourceId contains invalid characters. + + + Y + N + Y + Y + Y + Y + , + + + 0 + Y + + + N + N + + + Y + web_dyndns + An IP service type is required. + + akamai + akamai-ipv4 + akamai-ipv6 + cloudflare + cloudflare-ipv4 + cloudflare-ipv6 + cloudflare-dns + dynu-ipv4 + dynu-ipv6 + freedns + he + icanhazip + ipify-ipv4 + ipify-ipv6 + loopia + myonlineportal + noip-ipv4 + noip-ipv6 + nsupdate.info-ipv4 + nsupdate.info-ipv6 + zoneedit + Interface + + + + interface.check001 + + + + + N + /^::(([0-9a-fA-F]{1,4}:){0,3}[0-9a-fA-F]{1,4})?$/u + Entry is not a valid partial ipv6 address definition (e.g. ::1000). + + + 10 + Y + 10 + 60 + + + 1 + Y + + + 300 + Y + 1 + 604800 + + + N + + + An interface is required for the selected check method + SetIfConstraint + checkip + if + + + + + N + /^(.){1,255}$/u + Description should be a string between 1 and 255 characters + + + + + + + diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/AccountField.php b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/AccountField.php new file mode 100644 index 0000000000..0697684326 --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/AccountField.php @@ -0,0 +1,79 @@ +getAttribute('uuid')])) { + $stats = self::$current_stats[$node->getAttribute('uuid')]; + if (!empty($stats)) { + $node->current_ip->setValue($stats['ip']); + $node->current_mtime->setValue(date('c', (int)$stats['mtime'])); + } + } elseif (!empty((string)$node->hostnames)) { + foreach (explode(",", (string)$node->hostnames) as $hostname) { + if (!empty(self::$current_stats[$hostname]) && !empty(self::$current_stats[$hostname]['ip'])) { + $stats = self::$current_stats[$hostname]; + $node->current_ip->setValue($stats['ip']); + $node->current_mtime->setValue(date('c', $stats['mtime'])); + break; + } + } + } + } + + protected function actionPostLoadingEvent() + { + if (self::$current_stats === null) { + self::$current_stats = []; + $stats = json_decode((new Backend())->configdRun('ddclient statistics'), true); + if (!empty($stats) && !empty($stats['hosts'])) { + self::$current_stats = $stats['hosts']; + } elseif (!empty($stats)) { + self::$current_stats = $stats; + } + } + foreach ($this->internalChildnodes as $node) { + if (!$node->getInternalIsVirtual()) { + $this->addStatsFields($node); + } + } + return parent::actionPostLoadingEvent(); + } +} diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/CheckipField.php b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/CheckipField.php new file mode 100644 index 0000000000..83962e8054 --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/CheckipField.php @@ -0,0 +1,57 @@ +internalOptionList = self::$internalCacheOptionList; + return; + } + if (is_array($data)) { + $opn_backend = (string)$this->getParentModel()->general->backend == 'opnsense'; + foreach ($data as $key => $value) { + self::$internalCacheOptionList[$key] = gettext($value); + } + if ($opn_backend) { + // OPNsense backend, change interface label and add IPv6 option + self::$internalCacheOptionList['if'] = gettext("Interface [IPv4]"); + self::$internalCacheOptionList['if6'] = gettext("Interface [IPv6]"); + } + $this->internalOptionList = self::$internalCacheOptionList; + } + } +} diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/ServiceField.php b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/ServiceField.php new file mode 100644 index 0000000000..46798b5fa1 --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/FieldTypes/ServiceField.php @@ -0,0 +1,69 @@ +getParentModel()->general->backend == 'opnsense') { + $supported = json_decode((new Backend())->configdRun("ddclient opnbackend supported"), true); + if (!empty($supported)) { + self::$internalCacheOptionList = $supported; + asort(self::$internalCacheOptionList, SORT_NATURAL | SORT_FLAG_CASE); + } + } + } + $this->internalOptionList = self::$internalCacheOptionList; + } + + /** + * setter for option values + * @param $data + */ + public function setOptionValues($data) + { + if (!empty(self::$internalCacheOptionList) || (string)$this->getParentModel()->general->backend == 'opnsense') { + return; + } + if (is_array($data)) { + foreach ($data as $key => $value) { + self::$internalCacheOptionList[$key] = gettext($value); + } + $this->internalOptionList = self::$internalCacheOptionList; + } + } +} diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/Menu/Menu.xml b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/Menu/Menu.xml new file mode 100644 index 0000000000..0834a4ab75 --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/Menu/Menu.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/Migrations/M1_2_0.php b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/Migrations/M1_2_0.php new file mode 100644 index 0000000000..339c9c98d2 --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/Migrations/M1_2_0.php @@ -0,0 +1,70 @@ +object(); + + if (empty($config->OPNsense->DynDNS)) { + return; + } + + // migration will move these settings, extract datapoints from raw config + $checkip = (string)$config->OPNsense->DynDNS->general->checkip; + $interface = $checkip == "if" ? (string)$config->OPNsense->DynDNS->general->interface : ""; + $force_ssl = (string)$config->OPNsense->DynDNS->general->force_ssl; + $pre_account = []; + if (!empty($config->OPNsense->DynDNS->accounts->account)) { + foreach ($config->OPNsense->DynDNS->accounts->account as $account) { + $pre_account[(string)$account->attributes()['uuid']] = [ + "checkip" => !empty($account->use_interface) ? "if" : $checkip, + "interface" => !empty($account->use_interface) ? (string)$account->interface : $interface + ]; + } + } + + // update accounts + foreach ($model->accounts->account->iterateItems() as $account) { + $uuid = $account->getAttributes()['uuid']; + $account->checkip = $pre_account[$uuid]['checkip']; + $account->interface = $pre_account[$uuid]['interface']; + $account->force_ssl = $force_ssl; + } + } +} diff --git a/dns/ddclient/src/opnsense/mvc/app/views/OPNsense/DynDNS/index.volt b/dns/ddclient/src/opnsense/mvc/app/views/OPNsense/DynDNS/index.volt new file mode 100644 index 0000000000..88ee8897e5 --- /dev/null +++ b/dns/ddclient/src/opnsense/mvc/app/views/OPNsense/DynDNS/index.volt @@ -0,0 +1,155 @@ +{# + +OPNsense® is Copyright © 2021 by Deciso B.V. +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. + +#} + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Service') }}{{ lang._('Hostnames') }}{{ lang._('Username') }}{{ lang._('Interface') }}{{ lang._('Current IP') }}{{ lang._('Updated') }}{{ lang._('Description') }}{{ lang._('Commands') }}
+ + +
+
+
+ {{ partial("layout_partials/base_form",['fields':formSettings,'id':'frm_settings'])}} +
+
+ +
+
+
+
+ + +

+
+
+
+ +{# include dialogs #} +{{ partial("layout_partials/base_dialog",['fields':formDialogAccount,'id':'DialogAccount','label':lang._('Edit Account')])}} diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/checkip b/dns/ddclient/src/opnsense/scripts/ddclient/checkip new file mode 100755 index 0000000000..08d9edc6fe --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/checkip @@ -0,0 +1,43 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2022 Ad Schellevis + 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. +""" +import argparse +from lib import registered_services, checkip + + +if __name__ == '__main__': + # handle parameters + parser = argparse.ArgumentParser() + parser.add_argument('-s', '--service', help='service name', choices=registered_services(), required=True) + parser.add_argument('-i', '--interface', help='interface', type=str, default='') + parser.add_argument('-t', '--tls', help='enforce tls', choices=['0', '1'], default='1') + parser.add_argument('--timeout', help='timeout', type=str, default='10') + inputargs = parser.parse_args() + + proto = 'http' if inputargs.tls == "0" else 'https' + interface = inputargs.interface if inputargs.interface.strip() != "" else None + print(checkip(inputargs.service, proto, inputargs.timeout, interface)) diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/ddclient_opn.py b/dns/ddclient/src/opnsense/scripts/ddclient/ddclient_opn.py new file mode 100755 index 0000000000..ee1fb9e568 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/ddclient_opn.py @@ -0,0 +1,50 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2023 Ad Schellevis + 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. +""" +import argparse +import sys +import json +from lib import AccountFactory, Poller +sys.path.insert(0, "/usr/local/opnsense/site-python") +from daemonize import Daemonize + + +if __name__ == '__main__': + # handle parameters + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--config', help='config file [json]', default='/usr/local/etc/ddclient.json') + parser.add_argument('-s', '--status', help='status output file [json]', default='/var/tmp/ddclient_opn.status') + parser.add_argument('-f', '--foreground', help='run (log) in foreground', default=False, action='store_true') + parser.add_argument('-l', '--list', help='list known services and exit', default=False, action='store_true') + parser.add_argument('-p', '--pid', help='pid file location', default='/var/run/ddclient_opn.pid') + inputargs = parser.parse_args() + if inputargs.list: + print(json.dumps(AccountFactory().known_services())) + else: + cmd = lambda : Poller(inputargs.config, inputargs.status) + daemon = Daemonize(app="ddclient", pid=inputargs.pid, action=cmd, foreground=inputargs.foreground) + daemon.start() diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/__init__.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/__init__.py new file mode 100755 index 0000000000..762f022d13 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/__init__.py @@ -0,0 +1,61 @@ +""" + Copyright (c) 2022-2023 Ad Schellevis + 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. + + ------------------------------------------------------------------------------------------------------- + OPNsense ddclient alternative version. + + The base for the application is a configuration file /usr/local/etc/ddclient.json containing a `general` and + `account` section. Structured dictionaries per account settings determine the services to subscribe to, + general settings contain verbosity settings and poll intervals for example. + + Package structure: + * address.py + - contains all logic to figure out which address to use. + * poller.py + - The main poller class, which drives dyndns resolving. + * accounts + - Account/service type definitions, deriving from `BaseAccount`. + + The BaseAccount type: + + The lifetime of an account starts with the determination if an account object matches a requested configuration + account. Our AccountFactory() class is responsible for figuring our which classes are available and would + fit a provided definition using the `match(account)` method. + + Every account has an `atime` property, which determines when was the last time we compared if the stored address + matches the requested one and if needed was set accordingly at the remote service. + + Since every account likely has a dependency on an ip address, the base acccount implements an `execute()` method + which detects basic change (address, configuration changes) after which the implementation can do the actual + work and report if the address has really changed. This saves code and keeps the implementation simpler. + + The Poller class: + + Upon creation will start reading the configuration and merges the last known state (json), after each poll where something + changed (return status of `execute()`) the state is flushed to disk. + +""" +from .address import registered_services, checkip +from .poller import AccountFactory, Poller diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py new file mode 100755 index 0000000000..811733acee --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/__init__.py @@ -0,0 +1,140 @@ +""" + Copyright (c) 2023 Ad Schellevis + 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. +""" +import syslog +import hashlib +import uuid +import time +from ..address import checkip + + +class BaseAccount: + _priority = 255 + + def __init__(self, account: dict): + self._account = account + self._account['id'] = account.get('id', str(uuid.uuid4())) + self._account['description'] = account.get('description', '') + self._state = {} + self._last_accessed = 0 + self._current_address = None # last resolved address + + # calculate a hash so we can easily detect configuration changes + hash_list = [] + for fieldname in sorted(account.keys()): + if fieldname not in ['id', 'description', 'checkip', 'checkip_timeout', 'force_ssl']: + hash_list.append(str(account[fieldname])) + self._account['md5'] = hashlib.md5("|".join(hash_list).encode()).hexdigest() + + + def update_state(self, address, status='good'): + """ set ip[v4 or v6] address and update in state dict when address is provided. + """ + if address is not None: + self._state['ip'] = address + self._state['status'] = status + self._state['mtime'] = time.time() + self._state['md5'] = self.md5 + self._last_accessed = time.time() + + @staticmethod + def known_services(): + return {} + + @property + def id(self): + """ account unique id + """ + return self._account['id'] + + @property + def settings(self): + return self._account + + @staticmethod + def match(account): + """ Does this account fit for the provided specification + """ + return False + + @property + def description(self): + return ("%(id)s [%(service)s - %(description)s] " % self._account) + + @property + def state(self): + return self._state + + @state.setter + def state(self, value: dict): + self._state = value + + @property + def mtime(self): + return self._state.get('mtime', 0) + + @property + def atime(self): + return self._last_accessed + + @property + def md5(self): + return self._account.get('md5') + + @property + def is_verbose(self): + return self._account.get('verbose') is True + + @property + def current_address(self): + return self._current_address + + def execute(self): + """ execute account check/update sequence, return true if state changed + """ + self._current_address = checkip( + service = self.settings.get('checkip'), + proto = 'https' if self.settings.get('force_ssl', False) else 'http', + timeout = str(self.settings.get('checkip_timeout', '10')), + interface = self.settings['interface'] if self.settings.get('interface' ,'').strip() != '' else None, + dynipv6host = self.settings['dynipv6host'] if self.settings.get('dynipv6host' ,'').strip() != '' else None + ) + + if not self._current_address: + syslog.syslog( + syslog.LOG_WARNING, + "Account %s no global IP address detected, check config if warning persists" % (self.description) + ) + return False + elif ( + self._state.get('ip') is None or + self._current_address != self._state.get('ip') or + self.state.get('md5') != self.md5 + ): + # if current address doesn't equal the current state, propagate the fact + return True + else: + # unmodified, poller keeps track of last access timestamp + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/aws.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/aws.py new file mode 100755 index 0000000000..e80e4e0377 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/aws.py @@ -0,0 +1,95 @@ +""" + Copyright (c) 2023 Greg Glockner + 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. + ---------------------------------------------------------------------------------------------------- + AWS Route53 DNS provider + Usage: + AWS access key: username + AWS secret key: password + Route53 Hosted Zone ID: zone +""" +import syslog +import boto3 +from . import BaseAccount + + +class AWS(BaseAccount): + _services = ['aws'] + + def __init__(self, account: dict): + super().__init__(account) + + @staticmethod + def known_services(): + return AWS._services + + @staticmethod + def match(account): + return account.get('service') in AWS._services + + def execute(self): + """ AWS DNS update + """ + + if super().execute(): + if not self.current_address: + syslog.syslog( + syslog.LOG_WARNING, + f"No address found for {self.description}" + ) + return False + client = boto3.client('route53', + aws_access_key_id = self.settings.get('username'), + aws_secret_access_key = self.settings.get('password')) + ip = str(self.current_address) + if ':' in ip: + addrType = 'AAAA' + else: + addrType = 'A' + TTL = self.settings.get('ttl') + changeBatch = { + 'Changes': [{ + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'Name': host, + 'Type': addrType, + 'TTL': int(TTL), + 'ResourceRecords': [{'Value': ip}] + } + } for host in self.settings.get('hostnames').split(",")] + } + try: + response = client.change_resource_record_sets( + HostedZoneId = self.settings.get('zone'), + ChangeBatch = changeBatch) + except Exception as e: + syslog.syslog(syslog.LOG_ERR, str(e)) + return False + + syslog.syslog( + syslog.LOG_NOTICE, + f"Account {self.description} set new ip {self.current_address}, ID {response['ChangeInfo']['Id']}") + + self.update_state(address=self.current_address) + return True diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/azure.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/azure.py new file mode 100755 index 0000000000..e368b0f613 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/azure.py @@ -0,0 +1,192 @@ +""" + Copyright (c) 2023 Ad Schellevis + 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. + ---------------------------------------------------------------------------------------------------- + Azure DNS provider, inspired by https://github.com/opnsense/plugins/pull/1547 + + List DNS zones using Azure cloud shell + + #> az network dns zone list + + Returns a structure like: + + [ + { + "etag": "00000000-0000-0000-0000-0000000000000", + "id": "/subscriptions/00000000-0000-0000-0000-00000000000/resourceGroups/example/providers/Microsoft.Network/dnszones/example.com", <---- ResourceId + "location": "global", + "maxNumberOfRecordSets": 10000, + "maxNumberOfRecordsPerRecordSet": null, + "name": "test.deciso.com", + "nameServers": [ + "ns1-07.azure-dns.com.", + "ns2-07.azure-dns.net.", + "ns3-07.azure-dns.org.", + "ns4-07.azure-dns.info." + ], + "numberOfRecordSets": 3, + "registrationVirtualNetworks": null, + "resolutionVirtualNetworks": null, + "resourceGroup": "xxxxxx", + "tags": {}, + "type": "Microsoft.Network/dnszones", + "zoneType": "Public" + } + ] + + Next create a service principal (https://learn.microsoft.com/en-us/cli/azure/ad/sp?view=azure-cli-latest#az_ad_sp_create_for_rbac) + + #> az ad sp create-for-rbac --name "AcmeDnsValidator" --role "DNS Zone Contributor" --scopes /subscriptions/00000000-0000-0000-0000-00000000000/resourceGroups/example/providers/Microsoft.Network/dnszones/example.com + + Which returns a structure like: + + { + "appId": "00000000-0000-0000-0000-000000000000", <--- username + "displayName": "AcmeDnsValidator", + "password": "000000000000000000000000000000000000", <--- Password + "tenant": "00000000-0000-0000-0000-000000000000" + } +""" +import syslog +import requests +from requests.auth import HTTPBasicAuth +from . import BaseAccount + + +class Azure(BaseAccount): + _services = ['azure'] + + def __init__(self, account: dict): + super().__init__(account) + + @staticmethod + def known_services(): + return {'azure': 'Microsoft Azure'} + + @staticmethod + def match(account): + return account.get('service') in Azure._services + + def execute(self): + """ Azure DNS update, uses an oauth2 sequence to login, the following requests are being performed: + - https://management.azure.com/subscriptions/%s --> request target to authenticate against + - https://login.microsoftonline.com/%s/oauth2/token --> login using the tenantId received in the prev req + - https://management.azure.com/%s/XXX/%s --> set hostname address using the bearer token received + """ + if super().execute(): + resourceId = self.settings.get('resourceId', '') + if resourceId.find('subscriptions/') == -1: + syslog.syslog(syslog.LOG_ERR, 'No subscription id found for account %s' % self.description) + return + subscriptionId = resourceId.split('subscriptions/')[-1].split('/')[0] + req = requests.get('https://management.azure.com/subscriptions/%s?api-version=2016-09-01' % subscriptionId) + auth_target = req.headers.get('WWW-Authenticate', '').split(maxsplit=1) + if len(auth_target) < 2 or auth_target[0] != 'Bearer': + syslog.syslog(syslog.LOG_ERR, 'No Bearer token found for account %s' % self.description) + return + elif auth_target[1].find('https://login.windows.net/') == -1: + syslog.syslog( + syslog.LOG_ERR, + 'Unable to find Tenant ID for account %s (response: %s)' % (self.description, auth_target[1]) + ) + return + + tenantId = auth_target[1].split('https://login.windows.net/')[1].split('"')[0] + req_opts = { + 'url': 'https://login.microsoftonline.com/%s/oauth2/token' % tenantId, + 'data': { + 'resource': 'https://management.core.windows.net/', + 'grant_type': 'client_credentials', + 'client_id': self.settings.get('username'), + 'client_secret': self.settings.get('password') + }, + 'headers': { + 'User-Agent': 'OPNsense-dyndns' + } + } + req = requests.post(**req_opts) + try: + token_payload = req.json() + except requests.exceptions.JSONDecodeError: + token_payload = {} + if req.status_code != 200 or 'access_token' not in token_payload: + syslog.syslog( + syslog.LOG_ERR, + 'Unable to authenticate account %s (http_code: %d - %s)' % ( + self.description, + req.status_code, + req.text.replace('\n', '') + ) + ) + return + + for hostname in self.settings.get('hostnames', '').split(','): + req_opts = { + 'headers': { + 'Accept': 'application/json', + 'Authorization': 'Bearer %s' % token_payload['access_token'], + 'Content-Type': 'application/json' + } + } + if self.current_address.find(':') > 1: + # IPv6 + req_opts['url'] = 'https://management.azure.com/%s/AAAA/%s?api-version=2018-05-01' % ( + resourceId, hostname + ) + req_opts['json'] = { + 'properties': { + 'AAAARecords': [ + { + 'ipv6Address': self.current_address + } + ] + } + } + else: + #IPv4 + req_opts['url'] = 'https://management.azure.com/%s/A/%s?api-version=2018-05-01' % ( + resourceId, hostname + ) + req_opts['json'] = { + 'properties': { + 'ARecords': [ + { + 'ipv4Address': self.current_address + } + ] + } + } + + req = requests.patch(**req_opts) + if req.status_code == 200: + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s set new ip %s [%s]" % (self.description, self.current_address, req.text.strip()) + ) + + self.update_state(address=self.current_address) + return True + + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/cloudflare.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/cloudflare.py new file mode 100755 index 0000000000..6620e2431c --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/cloudflare.py @@ -0,0 +1,185 @@ +""" + Copyright (c) 2023 Thomas Cekal + Copyright (c) 2023 Ad Schellevis + 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. +""" +import json +import syslog +import requests +from . import BaseAccount + + +class Cloudflare(BaseAccount): + _priority = 65535 + + _services = { + 'cloudflare': 'api.cloudflare.com' + } + + def __init__(self, account: dict): + super().__init__(account) + + @staticmethod + def known_services(): + return {'cloudflare': 'Cloudflare'} + + @staticmethod + def match(account): + return account.get('service') in Cloudflare._services + + def execute(self): + if super().execute(): + # IPv4/IPv6 + recordType = "AAAA" if str(self.current_address).find(':') > 1 else "A" + + # get ZoneID + url = "https://%s/client/v4/zones" % self._services[self.settings.get('service')] + + headers = { + 'User-Agent': 'OPNsense-dyndns' + } + # switch between bearer and email/key authentication + if self.settings.get('username', '').find('@') == -1: + headers["Authorization"] = "Bearer " + self.settings.get('password') + else: + headers["X-Auth-Email"] = self.settings.get('username') + headers["X-Auth-Key"] = self.settings.get('password') + + req_opts = { + 'url': url, + 'params': { + 'name': self.settings.get('zone') + }, + 'headers': headers + } + response = requests.get(**req_opts) + try: + payload = response.json() + except requests.exceptions.JSONDecodeError: + payload = {} + if 'success' not in payload: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error parsing JSON response [ZoneID] %s" % (self.description, response.text) + ) + return False + if not payload.get('success', False): + syslog.syslog( + syslog.LOG_ERR, + "Account %s error receiving ZoneID [%s]" % (self.description, json.dumps(payload.get('errors', {}))) + ) + return False + + zone_id = payload['result'][0]['id'] + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s ZoneID for %s %s" % (self.description, self.settings.get('zone'), zone_id) + ) + + # Get record ID + req_opts = { + 'url': f"{req_opts['url']}/{zone_id}/dns_records", + 'params': { + 'name': self.settings.get('hostnames'), + 'type': recordType + }, + 'headers': req_opts['headers'] + } + response = requests.get(**req_opts) + try: + payload = response.json() + except requests.exceptions.JSONDecodeError: + payload = {} + if 'success' not in payload: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error parsing JSON response [RecordID] %s" % (self.description, response.text) + ) + return + if not payload.get('success', False): + syslog.syslog( + syslog.LOG_ERR, + "Account %s error receiving RecordID [%s]" % ( + self.description, json.dumps(payload.get('errors', {})) + ) + ) + return False + + if len(payload['result']) == 0: + syslog.syslog( + syslog.LOG_ERR, "Account %s error locating hostname %s [%s]" % ( + self.description, self.settings.get('hostnames'), recordType + ) + ) + return False + + record_id = payload['result'][0]['id'] + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s RecordID for %s %s" % (self.description, self.settings.get('hostnames'), record_id) + ) + + # Send IP address update + req_opts = { + 'url': f"{req_opts['url']}/{record_id}", + 'json': { + 'type': recordType, + 'name': self.settings.get('hostnames'), + 'content': str(self.current_address), + }, + 'headers': req_opts['headers'] + } + response = requests.patch(**req_opts) + try: + payload = response.json() + except requests.exceptions.JSONDecodeError: + payload = {} + if 'success' not in payload: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error parsing JSON response [UpdateIP] %s" % (self.description, response.text) + ) + return False + if payload.get('success', False): + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s set new ip %s [%s]" % ( + self.description, + self.current_address, + payload.get('result', {}).get('content', '') + ) + ) + + self.update_state(address=self.current_address) + return True + else: + syslog.syslog( + syslog.LOG_ERR, + "Account %s failed to set new ip %s [%s]" % (self.description, self.current_address, response.text) + ) + + + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/digitalocean.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/digitalocean.py new file mode 100755 index 0000000000..e6a62f25fa --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/digitalocean.py @@ -0,0 +1,177 @@ +""" + Copyright (c) 2023 Ad Schellevis + Copyright (c) 2024 Olly Baker + 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. +""" +import json +import syslog +import requests +from . import BaseAccount + + +class DigitalOcean(BaseAccount): + _priority = 65535 + + _services = {"digitalocean": "api.digitalocean.com"} + + def __init__(self, account: dict): + super().__init__(account) + + @staticmethod + def known_services(): + return {"digitalocean": "DigitalOcean"} + + @staticmethod + def match(account): + return account.get("service") in DigitalOcean._services + + def execute(self): + if super().execute(): + recordType = "AAAA" if str(self.current_address).find(":") > 1 else "A" + + headers = { + "User-Agent": "OPNsense-dyndns", + "Authorization": "Bearer " + self.settings.get("password"), + } + + url = "https://%s/v2/domains/%s/records" % ( + self._services[self.settings.get("service")], + self.settings.get("zone"), + ) + + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s current IP is %s (%s)" + % (self.description, self.current_address, recordType), + ) + + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s updating hostnames %s" + % (self.description, self.settings.get("hostnames", "")), + ) + + # Update each hostname + for hostname in self.settings.get("hostnames", "").split(","): + + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s getting record ID for hostname %s (%s)" + % (self.description, hostname, recordType), + ) + + request = { + "url": url, + "params": {"name": hostname, "type": recordType}, + "headers": headers, + } + + # Get record ID + response = requests.get(**request) + + try: + payload = response.json() + except requests.exceptions.JSONDecodeError: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error getting record ID for hostname %s (%s): failed to decode response as JSON. Response: %s" + % (self.description, hostname, recordType, response.text), + ) + continue + + if response.status_code != 200: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error getting record ID for hostname %s (%s): HTTP %s. Response: %s" + % ( + self.description, + hostname, + recordType, + response.status_code, + response.text, + ), + ) + continue + + if not payload.get("domain_records"): + syslog.syslog( + syslog.LOG_ERR, + "Account %s error getting record ID for hostname %s (%s): no results returned. Response: %s" + % (self.description, hostname, recordType, response.text), + ) + continue + + record_id = payload["domain_records"][0]["id"] + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s record ID for %s (%s) is %s" + % (self.description, hostname, recordType, record_id), + ) + + request = { + "url": "%s/%s" % (url, str(record_id)), + "json": { + "type": recordType, + "data": str(self.current_address), + }, + "headers": headers, + } + + # Update record IP + response = requests.patch(**request) + + if response.status_code == 200: + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s successfully updated hostname %s (%s) to IP %s" + % ( + self.description, + hostname, + recordType, + self.current_address, + ), + ) + + else: + syslog.syslog( + syslog.LOG_ERR, + "Account %s failed to set new IP (%s) for hostname %s (%s): HTTP %s. Response: %s" + % ( + self.description, + self.current_address, + self.description, + hostname, + recordType, + response.status_code, + response.text, + ), + ) + continue + self.update_state(address=self.current_address) + return True + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/dnspod_cn.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/dnspod_cn.py new file mode 100755 index 0000000000..cb4631fcc0 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/dnspod_cn.py @@ -0,0 +1,301 @@ +""" + Copyright (c) 2024 AnShen + Copyright (c) 2023 Ad Schellevis + 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. +""" +import json +import syslog +import time +import hashlib +import hmac +import requests +from datetime import datetime +from . import BaseAccount + + +# dnspod api 3.0 +# https://cloud.tencent.com/document/api/1427 + +class DNSPod_CN(BaseAccount): + _priority = 65535 + + _services = { + 'dnspodcn': 'dnspod.tencentcloudapi.com' + } + + def __init__(self, account: dict): + super().__init__(account) + self.service = 'dnspod' + + @staticmethod + def known_services(): + return {'dnspodcn': 'dnspodcn'} + + @staticmethod + def match(account): + return account.get('service') in DNSPod_CN._services + + + @staticmethod + def _sign(key, msg): + """ + Generate HMAC-SHA256 signature. + + Args: + key (bytes): Signing key + msg (str): Message to sign + + Returns: + bytes: Signature digest + """ + return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest() + + def generate_signature(self, action, payload="{}"): + """ + Generate signature and headers for a Tencent Cloud API request. + + Args: + action (str): API action name + payload (str or dict, optional): Request payload. Defaults to "{}". + + Returns: + tuple: Request headers and canonical request + """ + # Ensure payload is a string + payload = json.dumps(payload) if isinstance(payload, dict) else payload + + # Get current timestamp + timestamp = int(time.time()) + date = datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d") + + # Step 1: Create Canonical Request + http_request_method = "POST" + canonical_uri = "/" + canonical_querystring = "" + ct = "application/json; charset=utf-8" + canonical_headers = f"content-type:{ct}\nhost:{self._services[self.settings.get('service')]}\nx-tc-action:{action.lower()}\n" + signed_headers = "content-type;host;x-tc-action" + hashed_request_payload = hashlib.sha256(payload.encode("utf-8")).hexdigest() + + canonical_request = ( + f"{http_request_method}\n" + f"{canonical_uri}\n" + f"{canonical_querystring}\n" + f"{canonical_headers}\n" + f"{signed_headers}\n" + f"{hashed_request_payload}" + ) + + # Step 2: Create String to Sign + algorithm = "TC3-HMAC-SHA256" + credential_scope = f"{date}/{self.service}/tc3_request" + hashed_canonical_request = hashlib.sha256(canonical_request.encode("utf-8")).hexdigest() + + string_to_sign = ( + f"{algorithm}\n" + f"{timestamp}\n" + f"{credential_scope}\n" + f"{hashed_canonical_request}" + ) + + # Step 3: Calculate Signature + secret_date = self._sign(("TC3" + self.settings.get('password')).encode("utf-8"), date) + secret_service = self._sign(secret_date, self.service) + secret_signing = self._sign(secret_service, "tc3_request") + signature = hmac.new(secret_signing, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest() + + # Step 4: Create Authorization Header + authorization = ( + f"{algorithm} " + f"Credential={self.settings.get('username', '')}/{credential_scope}, " + f"SignedHeaders={signed_headers}, " + f"Signature={signature}" + ) + + # Prepare headers + headers = { + "Authorization": authorization, + "Content-Type": "application/json; charset=utf-8", + "Host": self._services[self.settings.get('service')], + "X-TC-Action": action, + "X-TC-Timestamp": str(timestamp), + "X-TC-Version": "2021-03-23", + 'User-Agent': 'OPNsense-dyndns', + } + + return headers, payload + + def send_request(self, action, payload="{}", region="", token=""): + """ + Send a request to the Tencent Cloud API. + + Args: + action (str): API action name + payload (str or dict, optional): Request payload. Defaults to "{}". + region (str, optional): Optional region parameter + token (str, optional): Optional token parameter + + Returns: + dict: API response JSON + """ + # Get headers and prepared payload + headers, payload = self.generate_signature(action, payload) + + # Add optional headers + if region: + headers["X-TC-Region"] = region + if token: + headers["X-TC-Token"] = token + + try: + # Send request using requests library + response = requests.post( + url=f"https://{self._services[self.settings.get('service')]}", + headers=headers, + data=payload, + timeout=10 + ) + + # Raise an exception for bad responses + response.raise_for_status() + + # Return JSON response + return response + + except requests.RequestException as err: + print(f"Request error: {err}") + + # If there's a response, print its content for debugging + if hasattr(err, 'response') and err.response is not None: + print(f"Response content: {err.response.text}") + + return None + + + def execute(self): + if super().execute(): + # IPv4/IPv6 + recordType = "AAAA" if str(self.current_address).find(':') > 1 else "A" + + subdomains = [] + hostnames = self.settings.get('hostnames').split(',') + for _subdomain in hostnames: + if _subdomain == self.settings.get('zone') or _subdomain == '@': + subdomains.append('@') + else: + subdomains.append(_subdomain.replace(f".{self.settings.get('zone')}", '')) + + if len(subdomains) < 1: + syslog.syslog( + syslog.LOG_ERR, + "Account %s hostnames format error" % self.description + ) + return False + + # Get record ID + response = self.send_request( + action='DescribeRecordList', + payload={ + 'RecordType': recordType, + 'Domain': self.settings.get('zone') + } + ) + try: + payload = response.json() + except requests.exceptions.JSONDecodeError: + payload = {} + if 'Response' in payload and 'Error' in payload['Response']: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error parsing JSON response [ZoneID] %s" % (self.description, payload['Response']['Error']['Code']) + ) + return False + if not payload['Response'].get('RecordList', False): + syslog.syslog( + syslog.LOG_ERR, + "Account %s error receiving ZoneID [%s]" % (self.description, response.text) + ) + return False + + record_id_list = [x['RecordId'] for x in payload['Response']['RecordList'] if x['Name'] in subdomains] + if len(record_id_list) < 1: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error Not Found Record [%s]" % (self.description, self.settings.get('hostnames')) + ) + return False + + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s ZoneID for %s %s" % (self.description, self.settings.get('zone'), record_id_list) + ) + + # Send IP address update + response = self.send_request( + action='ModifyRecordBatch', + payload={ + 'RecordIdList': record_id_list, + 'Change': 'value', + 'ChangeTo': str(self.current_address), + } + ) + try: + payload = response.json() + except requests.exceptions.JSONDecodeError: + payload = {} + if 'Response' in payload and 'Error' in payload['Response']: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error parsing JSON response [UpdateIP] %s" % (self.description, payload['Response']['Error']['Code']) + ) + return False + if len(payload['Response']['DetailList']) < 1: + syslog.syslog( + syslog.LOG_ERR, + "Account %s failed to set new ip %s [%s]" % (self.description, self.current_address, response.text) + ) + return False + + record_list = payload['Response']['DetailList'][0].get('RecordList', False) + if record_list and len(record_list) == len(subdomains): + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s set new ip %s %s" % ( + self.description, + self.current_address, + subdomains + ) + ) + + self.update_state(address=self.current_address) + return True + + syslog.syslog( + syslog.LOG_ERR, + "Account %s failed to set new ip %s %s" % (self.description, self.current_address, subdomains) + ) + + + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/domeneshop.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/domeneshop.py new file mode 100755 index 0000000000..91bbe27f58 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/domeneshop.py @@ -0,0 +1,93 @@ +""" + Copyright (c) 2023 Bernhard Frenking + Copyright (c) 2023 Ad Schellevis + 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. + ---------------------------------------------------------------------------------------------------- + Domeneshop DNS updater + Token should be set via the username field + Secret should be set via the password field +""" +import syslog +import requests +from requests.auth import HTTPBasicAuth +from . import BaseAccount + + +class Domeneshop(BaseAccount): + + _services = { + 'domeneshop': 'api.domeneshop.no' + } + + def __init__(self, account: dict): + super().__init__(account) + + @staticmethod + def known_services(): + return Domeneshop._services.keys() + + @staticmethod + def match(account): + return account.get('service') in Domeneshop._services + + def execute(self): + if super().execute(): + hostnames = self.settings.get('hostnames') + + # DNS update request using the "IP update protocol" + url = f'https://api.domeneshop.no/v0/dyndns/update?hostname={hostnames}&myip={str(self.current_address)}' + req_opts = { + 'url': url, + 'auth': HTTPBasicAuth(self.settings.get('username'), self.settings.get('password')), + 'headers': { + 'User-Agent': 'OPNsense-dyndns' + } + } + response = requests.get(**req_opts) + + # Parse response and update state and log + if response.status_code == 204: + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s set new ip %s [%s] for %s" % (self.description, self.current_address, response.text.strip(), hostnames) + ) + self.update_state(address=self.current_address, status=response.text.split()[0] if response.text else '') + return True + elif response.status_code == 404: + syslog.syslog( + syslog.LOG_ERR, + "Account %s failed to set new ip %s [%d - %s], because %s could not be found" % ( + self.description, self.current_address, response.status_code, response.text.replace('\n', ''), hostnames + ) + ) + else: + syslog.syslog( + syslog.LOG_ERR, + "Account %s failed to set new ip %s [%d - %s] for %s" % ( + self.description, self.current_address, response.status_code, response.text.replace('\n', ''), hostnames + ) + ) + + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/duckdns.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/duckdns.py new file mode 100755 index 0000000000..185daa112f --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/duckdns.py @@ -0,0 +1,80 @@ +""" + Copyright (c) 2023 Greg Glockner + 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. + ---------------------------------------------------------------------------------------------------- + DuckDNS updater + Token should be set via the password field +""" +import syslog +import requests +from . import BaseAccount + + +class duckdns(BaseAccount): + _services = ['duckdns'] + + def __init__(self, account: dict): + super().__init__(account) + + @staticmethod + def known_services(): + return duckdns._services + + @staticmethod + def match(account): + return account.get('service') in duckdns._services + + def execute(self): + """ Duck DNS update + """ + + if super().execute(): + data = { + 'domains': self.settings.get('hostnames'), + 'token': self.settings.get('password') + } + + ip = str(self.current_address) + if ':' in ip: + data['ipv6'] = ip + else: + data['ip'] = ip + + proto = 'https' if self.settings.get('force_ssl', False) else 'http' + + try: + response = requests.get(proto+'://www.duckdns.org/update', data) + if response.text.startswith('KO'): + raise RuntimeError( + f"DuckDNS update failed for {self.description} with ip {self.current_address} for domains {data['domains']}, response: {response.text}") + except Exception as e: + syslog.syslog(syslog.LOG_ERR, str(e)) + return False + + syslog.syslog( + syslog.LOG_NOTICE, + f"Account {self.description} set new ip {self.current_address} for domains {data['domains']}") + + self.update_state(address=self.current_address) + return True diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/dyndns2.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/dyndns2.py new file mode 100755 index 0000000000..e93d0e5abe --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/dyndns2.py @@ -0,0 +1,119 @@ +""" + Copyright (c) 2023 Ad Schellevis + 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. +""" +import syslog +import requests +from requests.auth import HTTPBasicAuth +from . import BaseAccount + + +class DynDNS2(BaseAccount): + _priority = 65535 + + _services = { + 'dyndns2': 'members.dyndns.org', + 'desec-v4': 'update.dedyn.io', + 'desec-v6': 'update6.dedyn.io', + 'dns-o-matic': 'updates.dnsomatic.com', + 'dynu': 'api.dynu.com', + 'he-net': 'dyn.dns.he.net', + 'he-net-tunnel': 'ipv4.tunnelbroker.net', + 'inwx': 'dyndns.inwx.com', + 'loopia': 'dyndns.loopia.se', + 'nsupdatev4': 'ipv4.nsupdate.info', + 'nsupdatev6': 'ipv6.nsupdate.info', + 'ovh': 'www.ovh.com', + 'spdyn': 'update.spdyn.de', + 'strato': 'dyndns.strato.com', + 'noip': 'dynupdate.no-ip.com' + } + + def __init__(self, account: dict): + super().__init__(account) + + @staticmethod + def known_services(): + return list(DynDNS2._services.keys()) + ['custom'] + + @staticmethod + def match(account): + if account.get('service') in DynDNS2.known_services(): + return True + else: + return False + + def execute(self): + if super().execute(): + protocol = self.settings.get('protocol', None) + if protocol in [ 'get', 'post', 'put' ]: + url = self.settings.get('server') + url = url.replace('__MYIP__', self.current_address) + url = url.replace('__HOSTNAME__', self.settings.get('hostnames')) + req = requests.request( + method=protocol, + url=url, + headers={'User-Agent': 'OPNsense-dyndns'}, + auth=HTTPBasicAuth(self.settings.get('username'), self.settings.get('password')) + ) + else: + uri_proto = 'https' if self.settings.get('force_ssl', False) else 'http' + if self.settings.get('service') in self._services: + url = "%s://%s/nic/update" % (uri_proto, self._services[self.settings.get('service')]) + else: + url = "%s://%s/nic/update" % (uri_proto, self.settings.get('server')) + + req_opts = { + 'url': url, + 'params': { + 'hostname': self.settings.get('hostnames'), + 'myip': self.current_address, + 'system': 'dyndns', + 'wildcard': 'ON' if self.settings.get('wildcard', False) else 'NOCHG' + }, + 'auth': HTTPBasicAuth(self.settings.get('username'), self.settings.get('password')), + 'headers': { + 'User-Agent': 'OPNsense-dyndns' + } + } + req = requests.get(**req_opts) + + if 200 <= req.status_code < 300: + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s set new ip %s [%s]" % (self.description, self.current_address, req.text.strip()) + ) + + self.update_state(address=self.current_address, status=req.text.split()[0] if req.text else '') + return True + else: + syslog.syslog( + syslog.LOG_ERR, + "Account %s failed to set new ip %s [%d - %s]" % ( + self.description, self.current_address, req.status_code, req.text.replace('\n', '') + ) + ) + + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/gandi.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/gandi.py new file mode 100755 index 0000000000..c92f1e2549 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/gandi.py @@ -0,0 +1,81 @@ +""" + Copyright (c) 2024 Thomas Cekal + Copyright (c) 2024 Ad Schellevis + 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. +""" +import json +import syslog +import requests +from . import BaseAccount + + +class Gandi(BaseAccount): + _services = { + 'gandi': 'api.gandi.net' + } + + def __init__(self, account: dict): + super().__init__(account) + + @staticmethod + def known_services(): + return Gandi._services.keys() + + @staticmethod + def match(account): + return account.get('service') in Gandi._services + + def execute(self): + if super().execute(): + # IPv4/IPv6 + recordType = "AAAA" if str(self.current_address).find(':') > 1 else "A" + + # Use bearer (api key) authentication + url = "https://api.gandi.net/v5/livedns/domains/" + self.settings.get('zone') + "/records/" + self.settings.get('hostnames') + "/" + recordType + payload = "{\"rrset_values\":[\"" + self.current_address + "\"],\"rrset_ttl\":300}" + headers = { + 'authorization': "Bearer " + self.settings.get('password'), + 'content-type': "application/json", + 'User-Agent': 'OPNsense-dyndns' + } + # Send IP address update + req = requests.request("PUT", url, data=payload, headers=headers) + if 200 <= req.status_code < 300: + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s set new ip %s [%s]" % (self.description, self.current_address, req.text.strip()) + ) + + self.update_state(address=self.current_address, status=req.text.split()[0] if req.text else '') + return True + else: + syslog.syslog( + syslog.LOG_ERR, + "Account %s failed to set new ip %s [%d - %s]" % ( + self.description, self.current_address, req.status_code, req.text.replace('\n', '') + ) + ) + + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/hetzner.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/hetzner.py new file mode 100755 index 0000000000..a066269ea5 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/hetzner.py @@ -0,0 +1,545 @@ +""" + Copyright (c) 2025 Arcan Consulting + 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. + + Hetzner DNS providers for OPNsense DynDNS via www.arcan-it.de + + Supports both APIs: + - Hetzner DNS (api.hetzner.cloud) - new Cloud API for migrated zones + - Hetzner DNS Legacy (dns.hetzner.com) - old API, shutting down May 2026 +""" +import syslog +import requests +from . import BaseAccount + + +class Hetzner(BaseAccount): + """ + Hetzner Cloud DNS API provider + Uses the new Cloud API (api.hetzner.cloud) + API Documentation: https://docs.hetzner.cloud/#dns + """ + _priority = 65535 + + _services = ['hetzner'] + + _api_base = "https://api.hetzner.cloud/v1" + + def __init__(self, account: dict): + super().__init__(account) + + @staticmethod + def known_services(): + return {'hetzner': 'Hetzner DNS'} + + @staticmethod + def match(account): + return account.get('service') in Hetzner._services + + def _get_headers(self): + return { + 'User-Agent': 'OPNsense-dyndns', + 'Authorization': 'Bearer ' + self.settings.get('password', ''), + 'Content-Type': 'application/json' + } + + def _get_zone_name(self): + """Get zone name from settings - try 'zone' field first, then 'username' as fallback""" + zone_name = self.settings.get('zone', '').strip() + if not zone_name: + zone_name = self.settings.get('username', '').strip() + return zone_name + + def _get_zone_id(self, headers): + """Get zone ID by zone name""" + zone_name = self._get_zone_name() + + url = f"{self._api_base}/zones" + params = {'name': zone_name} + + response = requests.get(url, headers=headers, params=params) + + if response.status_code != 200: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error fetching zones: HTTP %d - %s" % ( + self.description, response.status_code, response.text + ) + ) + return None + + try: + payload = response.json() + except requests.exceptions.JSONDecodeError: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error parsing JSON response: %s" % (self.description, response.text) + ) + return None + + zones = payload.get('zones', []) + if not zones: + syslog.syslog( + syslog.LOG_ERR, + "Account %s zone '%s' not found" % (self.description, zone_name) + ) + return None + + zone_id = zones[0].get('id') + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s found zone ID %s for %s" % (self.description, zone_id, zone_name) + ) + + return zone_id + + def _get_record(self, headers, zone_id, record_name, record_type): + """Get existing record by name and type""" + url = f"{self._api_base}/zones/{zone_id}/rrsets/{record_name}/{record_type}" + + response = requests.get(url, headers=headers) + + if response.status_code == 404: + return None + + if response.status_code != 200: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error fetching record: HTTP %d - %s" % ( + self.description, response.status_code, response.text + ) + ) + return None + + try: + payload = response.json() + return payload.get('rrset') + except requests.exceptions.JSONDecodeError: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error parsing JSON response: %s" % (self.description, response.text) + ) + return None + + def _update_record(self, headers, zone_id, record_name, record_type, address): + """Update existing record with new address""" + url = f"{self._api_base}/zones/{zone_id}/rrsets/{record_name}/{record_type}" + + data = { + 'records': [{'value': str(address)}], + 'ttl': int(self.settings.get('ttl', 300)) + } + + response = requests.put(url, headers=headers, json=data) + + if response.status_code != 200: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error updating record: HTTP %d - %s" % ( + self.description, response.status_code, response.text + ) + ) + return False + + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s updated %s %s to %s" % ( + self.description, record_name, record_type, address + ) + ) + + return True + + def _create_record(self, headers, zone_id, record_name, record_type, address): + """Create new record""" + url = f"{self._api_base}/zones/{zone_id}/rrsets" + + data = { + 'name': record_name, + 'type': record_type, + 'records': [{'value': str(address)}], + 'ttl': int(self.settings.get('ttl', 300)) + } + + response = requests.post(url, headers=headers, json=data) + + if response.status_code not in [200, 201]: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error creating record: HTTP %d - %s" % ( + self.description, response.status_code, response.text + ) + ) + return False + + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s created %s %s with %s" % ( + self.description, record_name, record_type, address + ) + ) + + return True + + def _extract_record_name(self, hostname, zone_name): + """Extract record name from hostname, handling FQDN format""" + hostname = hostname.rstrip('.') + + if hostname.endswith('.' + zone_name): + record_name = hostname[:-len(zone_name) - 1] + elif hostname == zone_name: + record_name = '@' + else: + record_name = hostname + + if not record_name or record_name == '@': + record_name = '@' + + return record_name + + def execute(self): + if super().execute(): + record_type = "AAAA" if ':' in str(self.current_address) else "A" + headers = self._get_headers() + + zone_id = self._get_zone_id(headers) + if not zone_id: + return False + + zone_name = self._get_zone_name() + + hostnames_raw = self.settings.get('hostnames', '') + hostnames = [h.strip() for h in hostnames_raw.split(',') if h.strip()] + + if not hostnames: + syslog.syslog( + syslog.LOG_ERR, + "Account %s no hostnames configured" % self.description + ) + return False + + all_success = True + for hostname in hostnames: + record_name = self._extract_record_name(hostname, zone_name) + + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s updating %s (record: %s, type: %s) to %s" % ( + self.description, hostname, record_name, record_type, self.current_address + ) + ) + + existing = self._get_record(headers, zone_id, record_name, record_type) + + if existing: + success = self._update_record( + headers, zone_id, record_name, record_type, self.current_address + ) + else: + success = self._create_record( + headers, zone_id, record_name, record_type, self.current_address + ) + + if success: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s set new IP %s for %s" % ( + self.description, self.current_address, hostname + ) + ) + else: + all_success = False + + if all_success: + self.update_state(address=self.current_address) + return True + + return False + + +class HetznerLegacy(BaseAccount): + """ + Hetzner DNS Console (Legacy) API provider + Uses the old API at dns.hetzner.com - will be shut down May 2026 + For zones not yet migrated to Hetzner Cloud Console + API Documentation: https://dns.hetzner.com/api-docs + """ + _priority = 65535 + + _services = ['hetzner-legacy'] + + _api_base = "https://dns.hetzner.com/api/v1" + + def __init__(self, account: dict): + super().__init__(account) + + @staticmethod + def known_services(): + return {'hetzner-legacy': 'Hetzner DNS Legacy (deprecated)'} + + @staticmethod + def match(account): + return account.get('service') in HetznerLegacy._services + + def _get_headers(self): + return { + 'User-Agent': 'OPNsense-dyndns', + 'Auth-API-Token': self.settings.get('password', ''), + 'Content-Type': 'application/json' + } + + def _get_zone_name(self): + """Get zone name from settings - try 'zone' field first, then 'username' as fallback""" + zone_name = self.settings.get('zone', '').strip() + if not zone_name: + zone_name = self.settings.get('username', '').strip() + return zone_name + + def _get_zone_id(self, headers): + """Get zone ID by zone name""" + zone_name = self._get_zone_name() + + url = f"{self._api_base}/zones" + response = requests.get(url, headers=headers) + + if response.status_code != 200: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error fetching zones: HTTP %d - %s" % ( + self.description, response.status_code, response.text + ) + ) + return None + + try: + payload = response.json() + except requests.exceptions.JSONDecodeError: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error parsing JSON response: %s" % (self.description, response.text) + ) + return None + + zones = payload.get('zones', []) + for zone in zones: + if zone.get('name') == zone_name: + zone_id = zone.get('id') + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s found zone ID %s for %s" % (self.description, zone_id, zone_name) + ) + return zone_id + + syslog.syslog( + syslog.LOG_ERR, + "Account %s zone '%s' not found" % (self.description, zone_name) + ) + return None + + def _get_record_id(self, headers, zone_id, record_name, record_type): + """Get record ID by name and type""" + url = f"{self._api_base}/records" + params = {'zone_id': zone_id} + + response = requests.get(url, headers=headers, params=params) + + if response.status_code != 200: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error fetching records: HTTP %d - %s" % ( + self.description, response.status_code, response.text + ) + ) + return None + + try: + payload = response.json() + except requests.exceptions.JSONDecodeError: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error parsing JSON response: %s" % (self.description, response.text) + ) + return None + + records = payload.get('records', []) + for record in records: + if record.get('name') == record_name and record.get('type') == record_type: + record_id = record.get('id') + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s found record ID %s for %s %s" % ( + self.description, record_id, record_name, record_type + ) + ) + return record_id + + return None + + def _update_record(self, headers, zone_id, record_id, record_name, record_type, address): + """Update existing record with new address""" + url = f"{self._api_base}/records/{record_id}" + + data = { + 'zone_id': zone_id, + 'type': record_type, + 'name': record_name, + 'value': str(address), + 'ttl': int(self.settings.get('ttl', 300)) + } + + response = requests.put(url, headers=headers, json=data) + + if response.status_code != 200: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error updating record: HTTP %d - %s" % ( + self.description, response.status_code, response.text + ) + ) + return False + + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s updated %s %s to %s" % ( + self.description, record_name, record_type, address + ) + ) + + return True + + def _create_record(self, headers, zone_id, record_name, record_type, address): + """Create new record""" + url = f"{self._api_base}/records" + + data = { + 'zone_id': zone_id, + 'type': record_type, + 'name': record_name, + 'value': str(address), + 'ttl': int(self.settings.get('ttl', 300)) + } + + response = requests.post(url, headers=headers, json=data) + + if response.status_code not in [200, 201]: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error creating record: HTTP %d - %s" % ( + self.description, response.status_code, response.text + ) + ) + return False + + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s created %s %s with %s" % ( + self.description, record_name, record_type, address + ) + ) + + return True + + def _extract_record_name(self, hostname, zone_name): + """Extract record name from hostname, handling FQDN format""" + hostname = hostname.rstrip('.') + + if hostname.endswith('.' + zone_name): + record_name = hostname[:-len(zone_name) - 1] + elif hostname == zone_name: + record_name = '@' + else: + record_name = hostname + + if not record_name or record_name == '@': + record_name = '@' + + return record_name + + def execute(self): + if super().execute(): + record_type = "AAAA" if ':' in str(self.current_address) else "A" + headers = self._get_headers() + + zone_id = self._get_zone_id(headers) + if not zone_id: + return False + + zone_name = self._get_zone_name() + + hostnames_raw = self.settings.get('hostnames', '') + hostnames = [h.strip() for h in hostnames_raw.split(',') if h.strip()] + + if not hostnames: + syslog.syslog( + syslog.LOG_ERR, + "Account %s no hostnames configured" % self.description + ) + return False + + all_success = True + for hostname in hostnames: + record_name = self._extract_record_name(hostname, zone_name) + + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s updating %s (record: %s, type: %s) to %s" % ( + self.description, hostname, record_name, record_type, self.current_address + ) + ) + + record_id = self._get_record_id(headers, zone_id, record_name, record_type) + + if record_id: + success = self._update_record( + headers, zone_id, record_id, record_name, record_type, self.current_address + ) + else: + success = self._create_record( + headers, zone_id, record_name, record_type, self.current_address + ) + + if success: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s set new IP %s for %s" % ( + self.description, self.current_address, hostname + ) + ) + else: + all_success = False + + if all_success: + self.update_state(address=self.current_address) + return True + + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/hostinger.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/hostinger.py new file mode 100755 index 0000000000..b9b004e359 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/hostinger.py @@ -0,0 +1,106 @@ +""" + Copyright (c) 2024 Thomas Cekal + Copyright (c) 2024 Ad Schellevis + Copyright (c) 2026 Leandro Scardua + 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. +""" + +import json +import syslog +import requests +from . import BaseAccount + +# Hostinger DNS API documentation: +# https://developers.hostinger.com/#tag/dns-zone/PUT/api/dns/v1/zones/{domain} + +class Hostinger(BaseAccount): + _services = { + 'hostinger': 'developers.hostinger.com' + } + + def __init__(self, account: dict): + super().__init__(account) + + @staticmethod + def known_services(): + return Hostinger._services.keys() + + @staticmethod + def match(account): + return account.get('service') in Hostinger._services + + def execute(self): + if super().execute(): + # IPv4/IPv6 + recordType = "AAAA" if str(self.current_address).find(':') > 1 else "A" + + # Validate TTL + ttl = int(self.settings.get('ttl', 300)) if (60 <= int(self.settings.get('ttl', 300)) <= 86400) else 300 + + # Use bearer authentication + url = "https://developers.hostinger.com/api/dns/v1/zones/" + self.settings.get('zone') + + # Build the zone update payload + payload = { + "overwrite": True, + "zone": [ + { + "name": self.settings.get('hostnames'), + "type": recordType, + "ttl": ttl, + "records": [ + { + "content": self.current_address + } + ] + } + ] + } + + headers = { + 'authorization': "Bearer " + self.settings.get('password'), + 'content-type': "application/json", + 'User-Agent': 'OPNsense-dyndns' + } + + # Send IP address update + req = requests.request("PUT", url, data=json.dumps(payload), headers=headers) + if 200 <= req.status_code < 300: + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s set new ip %s [%s]" % (self.description, self.current_address, req.text.strip()) + ) + + self.update_state(address=self.current_address, status='success') + return True + else: + syslog.syslog( + syslog.LOG_ERR, + "Account %s failed to set new ip %s [%d - %s]" % ( + self.description, self.current_address, req.status_code, req.text.replace('\n', '') + ) + ) + + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/netcup.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/netcup.py new file mode 100755 index 0000000000..10322a6309 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/netcup.py @@ -0,0 +1,180 @@ +""" + Copyright (c) 2023 Ingo Lafrenz + Copyright (c) 2023 Ad Schellevis + 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. + ---------------------------------------------------------------------------------------------------- + Netcup DNS provider, see https://ccp.netcup.net/run/webservice/servers/endpoint.php + +""" +import syslog +import requests +from . import BaseAccount + + +class Netcup(BaseAccount): + _services = ['netcup'] + + def __init__(self, account: dict): + super().__init__(account) + self.settings['APIPassword'] = None + self.settings['APIKey'] = None + # min TTL set to 300 + self.settings['ttl'] = max(int(self.settings['ttl']) if self.settings.get('ttl', '').isdigit() else 0, 300) + + + @staticmethod + def known_services(): + return Netcup._services + + @staticmethod + def match(account): + return account.get('service') in Netcup._services + + def execute(self): + if super().execute(): + if self.settings['hostnames'].find(',') > -1: + self.settings['hostnames'] = self.settings['hostnames'].split(',')[0] + syslog.syslog( + syslog.LOG_WARNING, + "Multiple hostnames detected, ignoring all except first. "+ + "Consider using CNAMEs or create separate DynDNS instances for each hostname." + ) + if self.settings['hostnames'].find('.') == -1: + syslog.syslog(syslog.LOG_ERR, "Incomplete FQDN offerred %s" % self.settings['hostnames']) + return False + + self.domain = self.settings['hostnames'].split('.', self.settings['hostnames'].count('.')-1)[-1] + self.hostname = self.settings['hostnames'].rsplit('.', 2)[0] if self.domain != self.settings['hostnames'] else '@' + + if self.settings['password'].count('|') == 1: + self.settings['APIPassword'], self.settings['APIKey'] = self.settings['password'].split('|') + + if self.settings['APIPassword'] is None or self.settings['APIKey'] is None: + syslog.syslog(syslog.LOG_ERR, "Unable to parse APIPassword|APIKey.") + return False + + self.netcupAPISessionID = self._login() + if not self.netcupAPISessionID: + return False + dnsZoneInfo = self._sendRequest(self._createRequestPayload('infoDnsZone')) + if not dnsZoneInfo: + return False + if str(self.settings['ttl']) != dnsZoneInfo['ttl']: + dnsZoneInfo['ttl'] = str(self.settings['ttl']) + self._sendRequest(self._createRequestPayload('updateDnsZone', {'dnszone': dnsZoneInfo})) + dnsRecordsInfo = self._sendRequest(self._createRequestPayload('infoDnsRecords')) + if not dnsRecordsInfo: + return False + recordType = 'AAAA' if ':' in self.current_address else 'A' + self._updateIpAddress(recordType, dnsRecordsInfo) + self._logout() + self.update_state(address=self.current_address) + return True + + def _login(self): + requestPayload = { + 'action': 'login', + 'param': { + 'customernumber': self.settings['username'], + 'apikey': self.settings['APIKey'], + 'apipassword': self.settings['APIPassword'] + } + } + return self._sendRequest(requestPayload).get('apisessionid', None) + + def _updateDnsRecords(self, hostRecord): + return self._sendRequest( + self._createRequestPayload( + 'updateDnsRecords', + {'dnsrecordset': {'dnsrecords': [hostRecord]}} + ) + ) + + def _logout(self): + requestPayload = { + 'action': 'logout', + 'param': { + 'customernumber': self._account['username'], + 'apikey': self.settings['APIKey'], + 'apisessionid': self.netcupAPISessionID + } + } + return self._sendRequest(requestPayload) + + def _updateIpAddress(self, recordType, dnsRecordsInfo): + matchingRecords = [ + r for r in dnsRecordsInfo['dnsrecords'] if r['type'] == recordType and r['hostname'] == self.hostname + ] + if len(matchingRecords) > 1: + raise Exception(f'Too many {recordType} records for hostname {self.hostname} in DNS zone {self.domain}.') + if matchingRecords: + hostRecord = matchingRecords[0] + else: + hostRecord = { + 'hostname': self.hostname, + 'type': recordType, + 'destination': None + } + currentNetcupIPAddress = hostRecord['destination'] + if self.current_address != currentNetcupIPAddress: + syslog.syslog( + syslog.LOG_NOTICE, + f'IP address change detected. Old IP: {currentNetcupIPAddress}, new IP: {self.current_address}' + ) + hostRecord['destination'] = self.current_address + if self._updateDnsRecords(hostRecord): + syslog.syslog( + syslog.LOG_NOTICE, + f'Successfully updated {recordType} record for {self.hostname}.{self.domain} to {self.current_address}' + ) + else: + syslog.syslog(syslog.LOG_NOTICE, 'IP address has not changed. Nothing to do.') + + def _createRequestPayload(self, action, extraParameters={}): + requestPayload = { + 'action': action, + 'param': { + 'domainname': self.domain, + 'customernumber': self._account['username'], + 'apikey': self.settings['APIKey'], + 'apisessionid': self.netcupAPISessionID, + } + } + requestPayload['param'].update(extraParameters) + return requestPayload + + def _sendRequest(self, payload): + req = requests.post(url='https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON', json=payload) + try: + resp = req.json() + except requests.exceptions.JSONDecodeError: + resp = {} + if resp.get('status', '') == 'success': + return resp.get('responsedata', {}) + else: + syslog.syslog( + syslog.LOG_ERR, + f"{payload['action']} failed with status {resp['status']}. response: {resp}" + ) + return {} diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/powerdns.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/powerdns.py new file mode 100755 index 0000000000..ea931a0cf2 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/account/powerdns.py @@ -0,0 +1,209 @@ +""" + Copyright (c) 2023 Ad Schellevis + Copyright (c) 2024 Olly Baker + Copyright (c) 2025 Oliver Traber + 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. +""" +import syslog +import requests +from . import BaseAccount + + +class PowerDNS(BaseAccount): + + def __init__(self, account: dict): + super().__init__(account) + # min TTL set to 300 + self.settings['ttl'] = max(int(self.settings["ttl"]) if self.settings.get("ttl", "").isdigit() else 0, 300) + + @staticmethod + def known_services(): + return {"powerdns": "PowerDNS API"} + + @staticmethod + def match(account): + return account.get("service") in ['powerdns'] + + + def _send_request(self, method, url, params=None, json=None): + headers = { + "User-Agent": "OPNsense-dyndns", + "X-API-Key": self.settings.get("password"), + } + + base_url = "%s/api/v1/servers/%s" % ( + self.settings.get('server'), + self.settings.get("server_id", "localhost") + ) + + url = base_url + url + return requests.request(method=method, url=url, headers=headers, params=params, json=json) + + + def _find_zone_id(self, hostname): + # Get the zone that a record belongs to + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s trying to get zone ID for hostname %s" + % (self.description, hostname), + ) + + zone = hostname + while (True): + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s checking if zone %s exists" + % (self.description, zone), + ) + + response = self._send_request(method="GET", url="/zones", params={"zone": zone}) + + if response.status_code == 200: + try: + payload = response.json() + # Check if a zone was found + if len(payload) == 0: + # Move one up in hierarchy + zone = '.'.join(zone.split('.')[1:]) + # Fail if root is reached + if zone == "": + syslog.syslog( + syslog.LOG_ERR, + "Account %s error getting zone ID for hostname %s - No matching zone found on server" + % (self.description, hostname), + ) + return None + else: + continue + else: + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s found zone %s for hostname %s" + % (self.description, zone, hostname), + ) + return payload[0].get("id") + except requests.exceptions.JSONDecodeError: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error getting zone ID for hostname %s - Failed to decode response as JSON. Response: %s" + % (self.description, hostname, response.text), + ) + return None + else: + syslog.syslog( + syslog.LOG_ERR, + "Account %s error getting zone ID for hostname %s HTTP %s. Response: %s" + % ( + self.description, + hostname, + response.status_code, + response.text, + ), + ) + return None + + def _replace_rrset(self, hostname, zone_id, record_type, content): + # Replace or create rrset for record + payload = { + "rrsets": [ + { + "name": hostname, + "type": record_type, + "ttl": int(self.settings.get("ttl")), + "changetype": "REPLACE", + "records": [ + {"content": content} + ] + } + ] + } + + response = self._send_request(method="PATCH", url=("/zones/" + zone_id), json=payload) + if response.status_code == 204: + # Success + return True + else: + # Failure + syslog.syslog( + syslog.LOG_ERR, + "Account %s error updating hostname %s in zone %s - HTTP %s Response: %s" + % ( + self.description, + hostname, + zone_id, + response.status_code, + response.text, + ), + ) + return False + + def execute(self): + if super().execute(): + record_type = "AAAA" if str(self.current_address).find(":") > 1 else "A" + + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s current IP is %s (%s)" + % (self.description, self.current_address, record_type), + ) + + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s updating hostnames %s" + % (self.description, self.settings.get("hostnames", "")), + ) + + # Update each hostname + for hostname in self.settings.get("hostnames", "").split(","): + + # Make hostname absolute + if not hostname.endswith("."): + hostname = hostname + "." + + # Get id of zone the hostname belongs to + zone_id = self._find_zone_id(hostname) + + # If zone can't be found, skip + if zone_id == None: + continue + + # Update record set + if self._replace_rrset(hostname, zone_id, record_type, content=self.current_address) and self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s successfully updated hostname %s (%s) to IP %s" + % ( + self.description, + hostname, + record_type, + self.current_address, + ), + ) + self.update_state(address=self.current_address) + return True + return False diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py new file mode 100755 index 0000000000..396b4720b1 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/address.py @@ -0,0 +1,153 @@ +""" + Copyright (c) 2022-2025 Ad Schellevis + 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. +""" +import subprocess +import re +import ipaddress +import dns.resolver +import dns.rdataclass +from urllib.parse import urlparse + +checkip_service_list = { + 'akamai': '%s://whatismyip.akamai.com', + 'akamai-ipv4': '%s://ipv4.whatismyip.akamai.com', + 'akamai-ipv6': '%s://ipv6.whatismyip.akamai.com', + 'cloudflare': '%s://one.one.one.one/cdn-cgi/trace', + 'cloudflare-ipv4': '%s://1.1.1.1/cdn-cgi/trace', + 'cloudflare-ipv6': '%s://[2606:4700:4700::1111]/cdn-cgi/trace', + 'dynu-ipv4': '%s://ipcheck.dynu.com/', + 'dynu-ipv6': '%s://ipcheckv6.dynu.com/', + 'freedns': '%s://freedns.afraid.org/dynamic/check.php', + 'he': '%s://checkip.dns.he.net/', + 'icanhazip': '%s://icanhazip.com/', + 'ipify-ipv4': '%s://api.ipify.org/', + 'ipify-ipv6': '%s://api6.ipify.org/', + 'loopia': '%s://dns.loopia.se/checkip/checkip.php', + 'myonlineportal': '%s://myonlineportal.net/checkip', + 'noip-ipv4': '%s://ip1.dynupdate.no-ip.com/', + 'noip-ipv6': '%s://ip1.dynupdate6.no-ip.com/', + 'nsupdate.info-ipv4': '%s://ipv4.nsupdate.info/myip', + 'nsupdate.info-ipv6': '%s://ipv6.nsupdate.info/myip', + 'zoneedit': '%s://dynamic.zoneedit.com/checkip.html' +} + +checkip_dns_list = { + 'cloudflare-dns': { + 'nameservers': ['1.1.1.1','1.0.0.1'], + 'resolve_params': { + 'qname': 'whoami.cloudflare', + 'rdtype': 'TXT', + 'rdclass': dns.rdataclass.from_text('CH') + } + } +} + +def registered_services(): + return list(checkip_service_list.keys()) + list(checkip_dns_list.keys()) + +def extract_address(host, txt): + """ Extract first IPv4 or IPv6 address from provided string + :param txt: text blob + :return: str + """ + for regexp in [r'(?:\d{1,3}\.){3}\d{1,3}', r'([a-f0-9:]+:+)+[a-f0-9]+']: + matches = re.finditer(regexp, txt) + for match in matches: + if match.group() != host: + try: + ipaddress.ip_address(match.group()) + return match.group() + except ValueError: + pass + return "" + + +def transform_ip(ip, ipv6host=None): + """ Changes ipv6 addresses if interface identifier is given + :param ip: ip address + :param ipv6host: 64 bit interface identifier + :return ipaddress.IPv4Address|ipaddress.IPv6Address + :raises ValueError: If the input can not be converted to an IPaddress + """ + if ipv6host and ip.find(':') > 0: + # extract 64 bit long prefix and add ipv6host [64]bits + return ipaddress.ip_address( + ipaddress.ip_network("%s/64" % ip, strict=False).network_address.exploded[0:19] + + ipaddress.ip_address(ipv6host).exploded[19:] + ) + else: + return ipaddress.ip_address(ip) + + +def checkip(service, proto='https', timeout='10', interface=None, dynipv6host=None): + """ find ip address using external web services defined in checkip_service_list + or dns services defined in checkip_dns_list + :param proto: protocol + :param timeout: timeout in seconds + :param interface: bind to interface + :param dynipv6host: optional partial ipv6 address + :return: str + """ + if service.lstrip('web_') in checkip_service_list: + # configuration name, strip web_ part + service = service.lstrip('web_') + params = ['/usr/local/bin/curl', '-m', timeout] + if interface is not None: + params.append("--interface") + params.append(interface) + url = checkip_service_list[service] % proto + params.append(url) + extracted_address = extract_address(urlparse(url).hostname, + subprocess.run(params, capture_output=True, text=True).stdout) + try: + return str(transform_ip(extracted_address, dynipv6host)) + except ValueError: + # invalid address + return "" + elif service in ['if', 'if6'] and interface is not None: + # return first non private IPv[4|6] interface address + ifcfg = subprocess.run(['/sbin/ifconfig', interface], capture_output=True, text=True).stdout + for line in ifcfg.split('\n'): + if line.startswith('\tinet'): + parts = line.split() + if (parts[0] == 'inet' and service == 'if') or (parts[0] == 'inet6' and service == 'if6'): + try: + address = transform_ip(parts[1], dynipv6host) + if address.is_global: + return str(address) + except ValueError: + continue + elif service.lstrip('dns_') in checkip_dns_list: + svc_info = checkip_dns_list[service.lstrip('dns_')] + resolve_params = svc_info['resolve_params'] + dns_resolver = dns.resolver.Resolver() + dns_resolver.nameservers = svc_info['nameservers'] + try: + dns_response = dns_resolver.resolve(**resolve_params) + return dns_response[0].to_text().strip('"') + except: + return "" + else: + return "" diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/lib/poller.py b/dns/ddclient/src/opnsense/scripts/ddclient/lib/poller.py new file mode 100755 index 0000000000..bb1d4900b1 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/lib/poller.py @@ -0,0 +1,175 @@ +""" + Copyright (c) 2023 Ad Schellevis + 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. +""" +import fcntl +import syslog +import glob +import importlib +import sys +import os +import time +import ujson +import ipaddress +from .account import BaseAccount + + +class AccountFactory: + def __init__(self): + self._account_classes = list() + self._register() + + def _register(self): + """ Register all account (type) classes. + These usually describe a protocol (like dyndns2) + """ + pkg_name = "%s.account" % __name__[:-len(os.path.splitext(os.path.basename(__file__))[0])-1] + all_account_classes = list() + for filename in glob.glob("%s/account/*.py" % os.path.dirname(__file__)): + importlib.import_module(".%s" % os.path.splitext(os.path.basename(filename))[0], pkg_name) + + for module_name in dir(sys.modules[pkg_name]): + for attribute_name in dir(getattr(sys.modules[pkg_name], module_name)): + cls = getattr(getattr(sys.modules[pkg_name], module_name), attribute_name) + if isinstance(cls, type) and issubclass(cls, BaseAccount) and cls != BaseAccount: + all_account_classes.append(cls) + + self._account_classes = sorted(all_account_classes, key=lambda k: k._priority) + + def get(self, account: dict): + for handler in self._account_classes: + if handler.match(account): + return handler(account) + + def known_services(self): + all_services = {} + for handler in self._account_classes: + data = handler.known_services() + if type(data) is dict: + all_services.update(data) + else: + for item in data: + all_services[item] = item + return all_services + + +class Poller: + def __init__(self, config_filename, status_filename): + self._config_filename = config_filename + self._status_filename = status_filename + self._accounts = {} + self._general_settings = {} + syslog.openlog('ddclient', facility=syslog.LOG_LOCAL4) + self.startup() + self.run() + + @property + def is_verbose(self): + return self._general_settings.get('verbose') is True + + @property + def is_enabled(self): + return self._general_settings.get('enabled') is True + + @property + def poll_interval(self): + return self._general_settings.get('daemon_delay', 60) + + def startup(self): + account_factory = AccountFactory() + with open(self._config_filename) as f: + cnf = ujson.load(f) + if type(cnf.get('general')) is dict: + self._general_settings = cnf.get('general') + if type(cnf.get('accounts')) is list: + for account in cnf.get('accounts'): + account['verbose'] = self.is_verbose + acc = account_factory.get(account) + if acc: + self._accounts[acc.id] = acc + if self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Account %s uses %s for service" % (acc.description, acc.__class__.__name__) + ) + elif self.is_verbose: + syslog.syslog( + syslog.LOG_NOTICE, + "Unable to find a suitable target for account %(id)s [%(description)s]" % account + ) + if len(self._accounts) > 0 and os.path.isfile(self._status_filename): + with open(self._status_filename) as f: + try: + state = ujson.load(f) + if type(state) is dict: + for sid in state: + if sid in self._accounts: + self._accounts[sid].state = state[sid] + except ValueError: + syslog.syslog(syslog.LOG_ERR, "Unable to read file %s" % self._status_filename) + + def flush_status(self): + fhandle = open(self._status_filename, 'a+') + try: + fcntl.flock(fhandle, fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError: + syslog.syslog(syslog.LOG_ERR, "Unable to flush status, %s already locked" % self._status_filename) + return + fhandle.seek(0) + fhandle.truncate() + data = {} + for acc_id in self._accounts: + data[acc_id] = self._accounts[acc_id].state + fhandle.write(ujson.dumps(data)) + fhandle.close() + + def run(self): + while True: + needs_flush = False + for acc in self._accounts.values(): + if time.time() - acc.atime > self.poll_interval: + if self.is_verbose: + syslog.syslog(syslog.LOG_NOTICE, "Account %s executing" % acc.description) + try: + if acc.execute(): + if self.is_verbose: + syslog.syslog(syslog.LOG_NOTICE, "Account %s updated" % acc.description) + needs_flush = True + else: + if self.is_verbose: + syslog.syslog(syslog.LOG_NOTICE, "Account %s not modified" % acc.description) + # update last accessed timestamp + acc.update_state(None) + except Exception as e: + # fatal exception, update atime so we're not going to retry too soon + acc.update_state(None) + syslog.syslog(syslog.LOG_ERR, "Account %s raised fatal error (%s)" % (acc.description, e)) + + if needs_flush: + if self.is_verbose: + syslog.syslog(syslog.LOG_NOTICE, "Flush dyndns status to disk") + self.flush_status() + + # XXX: needs better poll interval calculation + time.sleep(5) diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/setup.sh b/dns/ddclient/src/opnsense/scripts/ddclient/setup.sh new file mode 100755 index 0000000000..7cf7b8d4ad --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/setup.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +for CONF in /usr/local/etc/ddclient.conf /usr/local/etc/ddclient.json; do + chmod 0600 ${CONF} +done diff --git a/dns/ddclient/src/opnsense/scripts/ddclient/stats b/dns/ddclient/src/opnsense/scripts/ddclient/stats new file mode 100755 index 0000000000..750f16eb19 --- /dev/null +++ b/dns/ddclient/src/opnsense/scripts/ddclient/stats @@ -0,0 +1,65 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2022 Ad Schellevis + 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. +""" + +import os +import json + +filename = '/var/tmp/ddclient.cache' +filename_new = '/var/tmp/ddclient_opn.status' + +result = {"hosts": {}} + +# both ddclient and "opnsense" ddclient exist, only use old when currently in use +if os.path.isfile(filename) and os.path.isfile(filename_new): + if os.path.getmtime(filename) < os.path.getmtime(filename_new): + filename = None + +if filename is not None and os.path.isfile(filename): + with open(filename, "r") as fhandle: + for idx, row in enumerate(fhandle): + if idx == 0: + result['version'] = row.strip('#\n ') + elif idx == 1: + tmp = row.split('(')[-1].split(')')[0] + if tmp.isdigit(): + result['updated'] = int(tmp) + elif tmp.startswith('#') is False: + record = {} + for pair in row.split(','): + parts = pair.split('=') + if len(parts) == 2: + record[parts[0]] = parts[1] + if 'host' in record: + result['hosts'][record['host']] = record +elif os.path.isfile(filename_new): + # output will look completely different when our implementation is used, the model knows how to parse this + # (see AccountField.php) + with open(filename_new) as f: + result = json.load(f) + +print(json.dumps(result)) diff --git a/dns/ddclient/src/opnsense/service/conf/actions.d/actions_ddclient.conf b/dns/ddclient/src/opnsense/service/conf/actions.d/actions_ddclient.conf new file mode 100644 index 0000000000..8349ebdfec --- /dev/null +++ b/dns/ddclient/src/opnsense/service/conf/actions.d/actions_ddclient.conf @@ -0,0 +1,45 @@ +[start] +command: + /usr/local/etc/rc.d/ddclient start; + /usr/local/etc/rc.d/ddclient_opn start +type:script +message:starting ddclient + +[stop] +command:pkill -F /var/run/ddclient.pid 2> /dev/null; /usr/local/etc/rc.d/ddclient_opn onestop 2> /dev/null +errors:no +type:script +message:stopping ddclient + +[status] +command:/usr/local/sbin/pluginctl -s ddclient status +type:script_output +message:get ddclient status + +[restart] +command: + pkill -F /var/run/ddclient.pid 2> /dev/null; + /usr/local/etc/rc.d/ddclient restart; + /usr/local/etc/rc.d/ddclient_opn restart +type:script +message:restarting ddclient +description:Restart ddclient service + +[force] +command: + rm /var/tmp/ddclient_opn.status 2>/dev/null; + /usr/local/etc/rc.d/ddclient_opn restart 2>/dev/null; + /usr/local/sbin/ddclient -force +type:script +message:forcing ddclient update +description:Force ddclient update + +[statistics] +command:/usr/local/opnsense/scripts/ddclient/stats +type:script_output +message:get ddclient statistics + +[opnbackend.supported] +command:/usr/local/opnsense/scripts/ddclient/ddclient_opn.py -l +type:script_output +message:get ddclient statistics diff --git a/dns/ddclient/src/opnsense/service/templates/OPNsense/Syslog/local/ddclient.conf b/dns/ddclient/src/opnsense/service/templates/OPNsense/Syslog/local/ddclient.conf new file mode 100644 index 0000000000..7dc55b0775 --- /dev/null +++ b/dns/ddclient/src/opnsense/service/templates/OPNsense/Syslog/local/ddclient.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [ddclient]. +################################################################### +filter f_local_ddclient { + program("ddclient"); +}; diff --git a/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/+TARGETS b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/+TARGETS new file mode 100644 index 0000000000..63822fd4f2 --- /dev/null +++ b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/+TARGETS @@ -0,0 +1,4 @@ +rc.conf.d:/etc/rc.conf.d/ddclient +ddclient_opn.rc.conf.d:/etc/rc.conf.d/ddclient_opn +ddclient.conf:/usr/local/etc/ddclient.conf +ddclient.json:/usr/local/etc/ddclient.json diff --git a/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient.conf b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient.conf new file mode 100644 index 0000000000..d82eb19d9d --- /dev/null +++ b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient.conf @@ -0,0 +1,105 @@ +{% from 'OPNsense/Macros/interface.macro' import physical_interface %} +syslog=yes # log update msgs to syslog +pid=/var/run/ddclient.pid # record PID in file. +{% if not helpers.empty('OPNsense.DynDNS.general.verbose') %} +verbose=yes +{% endif %} +{% set accounts = [] %} +{% set force_ssl = [] %} +{% if helpers.exists('OPNsense.DynDNS.accounts.account') and OPNsense.DynDNS.general.backend == 'ddclient' %} +{% for account in helpers.toList('OPNsense.DynDNS.accounts.account') %} +{% if account.enabled|default('0') == '1' %} +{% do accounts.append(account) %} +{% if account.force_ssl|default('0') == '1' %} +{% do force_ssl.append(1) %} +{% endif %} +{% endif %} +{% endfor %} +{% endif %} +{% if force_ssl %} +ssl=yes +{% endif %} +{% for account in accounts %} + +{% if account.checkip == 'if' %} +{% if not helpers.empty('OPNsense.DynDNS.general.allowipv6') %} +usev6=ifv6, ifv6={{physical_interface(account.interface)}}, \ +{% endif %} +usev4=ifv4, ifv4={{physical_interface(account.interface)}}, \ +{% elif account.checkip.startswith('web_') %} +{% if account.interface %} +use=cmd, cmd="/usr/local/opnsense/scripts/ddclient/checkip -i {{physical_interface(account.interface)}} -t {{account.force_ssl}} -s {{account.checkip[4:]}} --timeout {{account.checkip_timeout|default('10')}}", \ +{% else %} +use=cmd, cmd="/usr/local/opnsense/scripts/ddclient/checkip -t {{account.force_ssl}} -s {{account.checkip[4:]}} --timeout {{account.checkip_timeout|default('10')}}", \ +{% endif %} +{% endif %} +{% if account.service == 'custom' %} +protocol={{account.protocol}}, \ +server={{account.server}}, \ +{% elif account.service in ['cloudflare', 'digitalocean', 'dnspodcn'] %} +protocol={{account.service}}, \ +zone={{account.zone}}, \ +{% elif account.service == 'cloudns' %} +protocol={{account.service}}, \ +dynurl=https://ipv4.cloudns.net/api/dynamicURL/?q={{account.password}}, \ +{% elif account.service == 'hosting1984' %} +protocol=1984, \ +{% elif account.service in ['godaddy', 'gandi'] %} +protocol={{account.service}}, \ +zone={{account.zone}}, \ +ttl={{account.ttl}}, \ +{% elif account.service == 'hetzner' %} +protocol={{account.service}}, \ +zone={{account.zone}}, \ +{% elif account.service == 'dns-o-matic' %} +protocol=dyndns2, \ +server=updates.dnsomatic.com, \ +{% elif account.service == 'dynu' %} +protocol=dyndns2, \ +server=api.dynu.com, \ +{% elif account.service == 'he-net' %} +protocol=dyndns2, \ +server=dyn.dns.he.net, \ +{% elif account.service == 'he-net-tunnel' %} +protocol=dyndns2, \ +server=ipv4.tunnelbroker.net, \ +{% elif account.service == 'inwx' %} +protocol=dyndns2, \ +server=dyndns.inwx.com, \ +{% elif account.service == 'loopia' %} +protocol=dyndns2, \ +server=dyndns.loopia.se, \ +{% elif account.service == 'nsupdatev4' %} +protocol=dyndns2, \ +server=ipv4.nsupdate.info, \ +{% elif account.service == 'nsupdatev6' %} +protocol=dyndns2, \ +server=ipv6.nsupdate.info, \ +{% elif account.service == 'servercow' %} +protocol=dyndns2, \ +server=dyndns.servercow.de, \ +{% elif account.service == 'spdyn' %} +protocol=dyndns2, \ +server=update.spdyn.de, \ +{% elif account.service == 'strato' %} +protocol=dyndns2, \ +server=dyndns.strato.com, \ +{% elif account.service == 'ovh' %} +protocol=dyndns2, \ +server=www.ovh.com, \ +{% elif account.service == 'porkbun' %} +protocol={{account.service}}, \ +apikey={{account.username}}, \ +secretapikey={{account.password}}, \ +{% else %} +protocol={{account.service}}, \ +{% endif %} +{% if account.wildcard|default('0') == '1' %} +wildcard=yes, \ +{% endif %} +{% if account.username %} +login={{account.username}}, \ +{% endif %} +password={{account.password}} \ +{{account.hostnames}} +{% endfor %} diff --git a/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient.json b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient.json new file mode 100644 index 0000000000..4a2cc7a852 --- /dev/null +++ b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient.json @@ -0,0 +1,34 @@ +{% from 'OPNsense/Macros/interface.macro' import physical_interface %} +{ + "general": { + "enabled": {{ "true" if not helpers.empty('OPNsense.DynDNS.general.enabled') else "false" }}, + "verbose": {{ "true" if not helpers.empty('OPNsense.DynDNS.general.verbose') else "false" }}, + "allowipv6": {{ "true" if not helpers.empty('OPNsense.DynDNS.general.allowipv6') else "false" }}, + "daemon_delay": {{OPNsense.DynDNS.general.daemon_delay|default('300')}} + }, + "accounts": [ +{% if helpers.exists('OPNsense.DynDNS.accounts.account') %} +{% for account in helpers.toList('OPNsense.DynDNS.accounts.account') | selectattr('enabled', 'equalto', '1') %} + { + "id": "{{ account['@uuid'] }}", + "service": "{{ account.service }}", + "protocol": "{{ account.protocol }}", + "server": {{ account.server | default('') | tojson }}, + "resourceId": {{ account.resourceId | default('') | tojson }}, + "username": {{ account.username | default('') | tojson }}, + "password": {{ account.password | default('') | tojson }}, + "hostnames": "{{ account.hostnames }}", + "wildcard": {{ "true" if account.wildcard == '1' else "false"}}, + "zone": "{{ account.zone }}", + "checkip": "{{ account.checkip }}", + "interface": "{% if account.interface %}{{physical_interface(account.interface)}}{% endif %}", + "dynipv6host": "{{ account.dynipv6host }}", + "checkip_timeout": {{ account.checkip_timeout }}, + "force_ssl": {{ "true" if account.force_ssl == '1' else "false"}}, + "ttl": "{{ account.ttl }}", + "description": {{ account.description | default('') | tojson }} + }{{ "," if not loop.last else ""}} +{% endfor %} +{% endif %} + ] +} diff --git a/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient_opn.rc.conf.d b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient_opn.rc.conf.d new file mode 100644 index 0000000000..b6a9cf0140 --- /dev/null +++ b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/ddclient_opn.rc.conf.d @@ -0,0 +1,6 @@ +{% if not helpers.empty('OPNsense.DynDNS.general.enabled') and OPNsense.DynDNS.general.backend == 'opnsense' %} +ddclient_opn_enable="YES" +ddclient_opn_setup="/usr/local/opnsense/scripts/ddclient/setup.sh" +{% else %} +ddclient_opn_enable="NO" +{% endif %} diff --git a/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/rc.conf.d b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/rc.conf.d new file mode 100644 index 0000000000..ecb315a717 --- /dev/null +++ b/dns/ddclient/src/opnsense/service/templates/OPNsense/ddclient/rc.conf.d @@ -0,0 +1,7 @@ +{% if not helpers.empty('OPNsense.DynDNS.general.enabled') and OPNsense.DynDNS.general.backend == 'ddclient' %} +ddclient_enable="YES" +ddclient_setup="/usr/local/opnsense/scripts/ddclient/setup.sh" +ddclient_flags="-daemon {{OPNsense.DynDNS.general.daemon_delay|default('300')}}" +{% else %} +ddclient_enable="NO" +{% endif %} diff --git a/dns/ddclient/src/opnsense/www/js/widgets/Dyndns.js b/dns/ddclient/src/opnsense/www/js/widgets/Dyndns.js new file mode 100644 index 0000000000..bc4c359abb --- /dev/null +++ b/dns/ddclient/src/opnsense/www/js/widgets/Dyndns.js @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 Deciso B.V. + * 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. + */ + +export default class Dyndns extends BaseTableWidget { + constructor() { + super(); + } + + getGridOptions() { + return { + // Trigger overflow-y:scroll after 650px height + sizeToContent: 650 + }; + } + + getMarkup() { + let $container = $('
'); + let $dyndnsTable = this.createTable('dyndnsTable', { + headerPosition: 'top', + headers: [ + this.translations.service, + this.translations.domains + ] + }); + + $container.append($dyndnsTable); + return $container; + } + + async onWidgetTick() { + // Check if DynDNS is enabled + const statusData = await this.ajaxCall(`/api/dyndns/service/${'status'}`); + if (!statusData || statusData.status !== "running") { + this.displayError(this.translations.servicedisabled); + return; + } + + // Fetch DynDNS account information + const accountData = await this.ajaxCall(`/api/dyndns/accounts/${'search_item'}`); + if (!accountData || !accountData.rows || accountData.rows.length === 0) { + this.displayError(this.translations.noaccount); + return; + } + + this.processAccounts(accountData.rows); + } + + // Utility function to display errors within the widget + displayError(message) { + const $error = $(` + + `); + $('#dyndnsTable').empty().append($error); + } + + processAccounts(accounts) { + if (!this.dataChanged('accounts', accounts)) { + return; + } + + $('.dyndns-tooltip').tooltip('hide'); + + let rows = []; + accounts.forEach(account => { + let colorClass = account.enabled === "1" ? 'text-success' : 'text-danger'; + let tooltipText = account.enabled === "1" ? this.translations.enabled : this.translations.disabled; + + let domainNames = account.hostnames.split(',') + .map(domain => `
${domain}
`) + .join(''); + + // Convert time to a localized format + let localizedTime = account.current_mtime ? new Date(account.current_mtime).toLocaleString() : this.translations.undefined; + let currentIp = account.current_ip || this.translations.undefined; + + let row = [ + ` + +
${this.translations.currentip}: ${currentIp}
+
${this.translations.currentmtime}: ${localizedTime}
+ `, + ` +
${domainNames}
+ ` + ]; + + rows.push(row); + }); + + // Update table with rows + super.updateTable('dyndnsTable', rows); + + // Initialize tooltips + $('.dyndns-tooltip').tooltip({container: 'body'}); + } + + onWidgetResize(elem, width, height) { + if (width < 320) { + $('#header_dyndnsTable').hide(); + $('.domain-names').parent().hide(); + } else { + $('#header_dyndnsTable').show(); + $('.domain-names').parent().show(); + } + return true; // Return true to force the grid to update its layout + } +} diff --git a/dns/ddclient/src/opnsense/www/js/widgets/Metadata/Dyndns.xml b/dns/ddclient/src/opnsense/www/js/widgets/Metadata/Dyndns.xml new file mode 100644 index 0000000000..17569ac10b --- /dev/null +++ b/dns/ddclient/src/opnsense/www/js/widgets/Metadata/Dyndns.xml @@ -0,0 +1,21 @@ + + + Dyndns.js + + /api/dyndns/accounts/* + /api/dyndns/service/* + + + Dynamic DNS + DynDNS service is not running + No DynDNS account information available + Enabled + Disabled + Current IP + Updated + Hostnames + Accounts + N/A + + + diff --git a/dns/dnscrypt-proxy/Makefile b/dns/dnscrypt-proxy/Makefile index 26d9d6d8df..b0e6868f11 100644 --- a/dns/dnscrypt-proxy/Makefile +++ b/dns/dnscrypt-proxy/Makefile @@ -1,5 +1,6 @@ PLUGIN_NAME= dnscrypt-proxy -PLUGIN_VERSION= 1.10 +PLUGIN_VERSION= 1.16 +PLUGIN_REVISION= 1 PLUGIN_COMMENT= Flexible DNS proxy supporting DNSCrypt and DoH PLUGIN_DEPENDS= dnscrypt-proxy2 PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/dns/dnscrypt-proxy/pkg-descr b/dns/dnscrypt-proxy/pkg-descr index 540b718146..f06697ce8b 100644 --- a/dns/dnscrypt-proxy/pkg-descr +++ b/dns/dnscrypt-proxy/pkg-descr @@ -1,10 +1,36 @@ A flexible DNS proxy, with support for modern encrypted DNS protocols such as DNSCrypt v2 and DNS-over-HTTPS. - Plugin Changelog ================ +1.16 + +* Fix ODoH servers not working (contributed by Pascal Herget) + +1.15 + +* Update plugin for dnscrypt-proxy 2.1 (contributed by Adrian Fedoreanu and Michael Muenz) + +1.14 + +* Fix display of the config with more than one disabled server in GUI (contributed by Evgeny Grin (karlson2k)) + +1.13 + +* Add necessary hooks to allow the plugin to be used as a standalone core DNS server +* Changed default listening addresses to 0.0.0.0 for new users +* Prevent using a port being used by another DNS service + +1.12 + +* Support specifying relays for anonymous DNS + +1.11 + +* Fix DNSBL update due to FreeBSD13 upgrade (sed syntax) +* Fix "manual disable of specific servers" when more than one server is specified (contributed by Evgeny Grin (karlson2k)) + 1.10 * Add option to enable/disable local query logs diff --git a/dns/dnscrypt-proxy/src/etc/inc/plugins.inc.d/dnscryptproxy.inc b/dns/dnscrypt-proxy/src/etc/inc/plugins.inc.d/dnscryptproxy.inc index 3f3588874e..e236cd6839 100644 --- a/dns/dnscrypt-proxy/src/etc/inc/plugins.inc.d/dnscryptproxy.inc +++ b/dns/dnscrypt-proxy/src/etc/inc/plugins.inc.d/dnscryptproxy.inc @@ -1,30 +1,31 @@ - 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. -*/ + * Copyright (C) 2018 Michael Muenz + * Copyright (C) 2023 Franco Fichtner + * 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. + */ function dnscryptproxy_enabled() { @@ -32,24 +33,55 @@ function dnscryptproxy_enabled() return (string)$model->enabled == '1'; } +function dnscryptproxy_configure() +{ + return [ + 'dns' => ['dnscryptproxy_configure_do'], + ]; +} + function dnscryptproxy_services() { - $services = array(); + $services = []; if (!dnscryptproxy_enabled()) { return $services; } - $services[] = array( + $model = new \OPNsense\Dnscryptproxy\General(); + $ports = []; + + /* DNS service is eligable for core use when either 0.0.0.0 or :: are set */ + foreach (explode(',', (string)$model->listen_addresses) as $addrport) { + if (preg_match('/^0\.0\.0\.0:([\d]+)$/', $addrport, $matches)) { + $ports[$matches[1]] = 1; + } elseif (preg_match('/^\[::\]:([\d]+)$/', $addrport, $matches)) { + $ports[$matches[1]] = 1; + } + } + + $services[] = [ + /* the port may still be something other than 53, but it's safe to register a conflict for it */ + 'dns_ports' => array_keys($ports), 'description' => gettext('DNSCrypt-Proxy'), - 'configd' => array( - 'restart' => array('dnscryptproxy restart'), - 'start' => array('dnscryptproxy start'), - 'stop' => array('dnscryptproxy stop'), - ), + 'configd' => [ + 'restart' => ['dnscryptproxy restart'], + 'start' => ['dnscryptproxy start'], + 'stop' => ['dnscryptproxy stop'], + ], + 'pid' => '/var/run/dnscrypt-proxy.pid', 'name' => 'dnscrypt-proxy', - 'pid' => '/var/run/dnscrypt-proxy.pid' - ); + ]; return $services; } + +function dnscryptproxy_configure_do($verbose) +{ + service_log('Starting DNSCrypt-Proxy...', $verbose); + + configd_run('template reload OPNsense/Dnscryptproxy'); + configd_run('dnscryptproxy restart'); + + service_log("done.\n", $verbose); +} diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/CloakController.php b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/CloakController.php index 98fc77a5b5..efc47624ea 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/CloakController.php +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/CloakController.php @@ -1,31 +1,29 @@ +/* + * Copyright (C) 2018 Michael Muenz + * All rights reserved. * - * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: * - * 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. * - * 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. + * 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. */ namespace OPNsense\Dnscryptproxy\Api; @@ -40,25 +38,29 @@ class CloakController extends ApiMutableModelControllerBase public function searchCloakAction() { - return $this->searchBase('cloaks.cloak', array("enabled", "name", "destination")); + return $this->searchBase('cloaks.cloak', ['enabled', 'name', 'destination']); } + public function getCloakAction($uuid = null) { - $this->sessionClose(); return $this->getBase('cloak', 'cloaks.cloak', $uuid); } + public function addCloakAction() { return $this->addBase('cloak', 'cloaks.cloak'); } + public function delCloakAction($uuid) { return $this->delBase('cloaks.cloak', $uuid); } + public function setCloakAction($uuid) { return $this->setBase('cloak', 'cloaks.cloak', $uuid); } + public function toggleCloakAction($uuid) { return $this->toggleBase('cloaks.cloak', $uuid); diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ForwardController.php b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ForwardController.php index cff952a76b..2d2c6dd16d 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ForwardController.php +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ForwardController.php @@ -1,31 +1,29 @@ +/* + * Copyright (C) 2018 Michael Muenz + * All rights reserved. * - * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: * - * 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. * - * 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. + * 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. */ namespace OPNsense\Dnscryptproxy\Api; @@ -40,25 +38,29 @@ class ForwardController extends ApiMutableModelControllerBase public function searchForwardAction() { - return $this->searchBase('forwards.forward', array("enabled", "domain", "dnsserver")); + return $this->searchBase('forwards.forward', ['enabled', 'domain', 'dnsserver']); } + public function getForwardAction($uuid = null) { - $this->sessionClose(); return $this->getBase('forward', 'forwards.forward', $uuid); } + public function addForwardAction() { return $this->addBase('forward', 'forwards.forward'); } + public function delForwardAction($uuid) { return $this->delBase('forwards.forward', $uuid); } + public function setForwardAction($uuid) { return $this->setBase('forward', 'forwards.forward', $uuid); } + public function toggleForwardAction($uuid) { return $this->toggleBase('forwards.forward', $uuid); diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ServerController.php b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ServerController.php index 778d31a421..58d27fa2aa 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ServerController.php +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ServerController.php @@ -38,25 +38,29 @@ class ServerController extends ApiMutableModelControllerBase public function searchServerAction() { - return $this->searchBase('servers.server', array("enabled", "name", "stamp")); + return $this->searchBase('servers.server', ['enabled', 'name', 'stamp']); } + public function getServerAction($uuid = null) { - $this->sessionClose(); return $this->getBase('server', 'servers.server', $uuid); } + public function addServerAction() { return $this->addBase('server', 'servers.server'); } + public function delServerAction($uuid) { return $this->delBase('servers.server', $uuid); } + public function setServerAction($uuid) { return $this->setBase('server', 'servers.server', $uuid); } + public function toggleServerAction($uuid) { return $this->toggleBase('servers.server', $uuid); diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ServiceController.php b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ServiceController.php index 8a00c2b4ac..835b974c5b 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ServiceController.php +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/ServiceController.php @@ -1,31 +1,29 @@ - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: +/* + * Copyright (C) 2018 Michael Muenz + * All rights reserved. * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: * - * 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. + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. * - * 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. + * 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. */ namespace OPNsense\Dnscryptproxy\Api; @@ -48,10 +46,9 @@ class ServiceController extends ApiMutableServiceControllerBase public function dnsblAction() { - $this->sessionClose(); $mdl = new Dnsbl(); $backend = new Backend(); - $response = $backend->configdpRun('dnscryptproxy dnsbl', array((string)$mdl->type)); - return array("response" => $response); + $response = $backend->configdpRun('dnscryptproxy dnsbl', [(string)$mdl->type]); + return ['response' => $response]; } } diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/WhitelistController.php b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/WhitelistController.php index 31b298fdc7..fbb05c043c 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/WhitelistController.php +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/Api/WhitelistController.php @@ -1,31 +1,29 @@ +/* + * Copyright (C) 2018 Michael Muenz + * All rights reserved. * - * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: * - * 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. * - * 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. + * 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. */ namespace OPNsense\Dnscryptproxy\Api; @@ -40,25 +38,29 @@ class WhitelistController extends ApiMutableModelControllerBase public function searchWhitelistAction() { - return $this->searchBase('whitelists.whitelist', array("enabled", "name", "description")); + return $this->searchBase('whitelists.whitelist', ['enabled', 'name', 'description']); } + public function getWhitelistAction($uuid = null) { - $this->sessionClose(); return $this->getBase('whitelist', 'whitelists.whitelist', $uuid); } + public function addWhitelistAction() { return $this->addBase('whitelist', 'whitelists.whitelist'); } + public function delWhitelistAction($uuid) { return $this->delBase('whitelists.whitelist', $uuid); } + public function setWhitelistAction($uuid) { return $this->setBase('whitelist', 'whitelists.whitelist', $uuid); } + public function toggleWhitelistAction($uuid) { return $this->toggleBase('whitelists.whitelist', $uuid); diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/forms/general.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/forms/general.xml index 41d4d57d26..02b73663a2 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/forms/general.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/controllers/OPNsense/Dnscryptproxy/forms/general.xml @@ -11,7 +11,7 @@ select_multiple true - Set the IP address and port combinations this service should listen on, e.g 127.0.0.1:5353 and/or [::1]:5353 + Set the IP address/port combinations this service should listen on. general.allowprivileged @@ -49,6 +49,12 @@ checkbox Let DNSCrypt-Proxy use servers with DNS-over-HTTPS protocol enabled. + + general.odoh_servers + + checkbox + Let DNSCrypt-Proxy use servers with Oblivious-DNS-over-HTTPS protocol enabled. Note: If checked you must provide ODoH target and relay servers manually! + general.require_dnssec @@ -179,4 +185,12 @@ true + + general.relaylist + + select_multiple + + true + relays. Will be used for relaying to all configured servers.]]> + diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Cloak.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Cloak.xml index fd1c125db5..a76121b983 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Cloak.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Cloak.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml index 95a89c1288..ba05d656e2 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml @@ -4,11 +4,10 @@ 1.0.0 - 0 + 0 Y - N Y AdAway List @@ -20,6 +19,7 @@ Easyprivacy List NoCoin List PornTop1M List + Q-Feeds (when installed) Simple Ad List Simple Tracker List Steven Black List diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Forward.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Forward.xml index 5632cc3a61..243cca945f 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Forward.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Forward.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.php b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.php index a10bb4ad9d..6f2a3d0659 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.php +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.php @@ -1,35 +1,89 @@ - 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. -*/ + * Copyright (C) 2018 Michael Muenz + * Copyright (C) 2023 Deciso B.V. + * 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. + */ namespace OPNsense\Dnscryptproxy; use OPNsense\Base\BaseModel; +use OPNsense\Base\Messages\Message; +use OPNsense\Core\Backend; class General extends BaseModel { + public function performValidation($validateFullModel = false) + { + $messages = parent::performValidation($validateFullModel); + + if ( + ($validateFullModel || $this->enabled->isFieldChanged() || $this->listen_addresses->isFieldChanged()) && + !empty((string)$this->enabled) + ) { + $any4 = []; + $any6 = []; + $ports = []; + + /* grab ALL ports to run a validation against, safer for user in the long run */ + foreach (explode(',', (string)$this->listen_addresses) as $addrport) { + if (preg_match('/(.*):([\d]+)$/', $addrport, $matches)) { + $ports[$matches[2]] = 1; + if ($matches[1] == '0.0.0.0') { + $any4[$matches[2]] = 1; + } elseif ($matches[1] == '[::]') { + $any6[$matches[2]] = 1; + } + } + } + + foreach (json_decode((new Backend())->configdpRun('service list'), true) as $service) { + if (empty($service['dns_ports'])) { + continue; + } + if (!is_array($service['dns_ports'])) { + syslog(LOG_ERR, sprintf('Service %s (%s) reported a faulty "dns_ports" entry.', $service['description'], $service['name'])); + continue; + } + if ($service['name'] != 'dnscrypt-proxy' && count(array_intersect(array_keys($ports), $service['dns_ports']))) { + $messages->appendMessage(new Message( + sprintf(gettext('%s is currently using one of these ports.'), $service['description']), + $this->listen_addresses->getInternalXMLTagName() + )); + break; + } + } + + if (count(array_keys(array_intersect_key($any4, $any6)))) { + $messages->appendMessage(new Message( + gettext('Cannot configure on both "0.0.0.0" and "::" as the first occurence will be treated as dual-stack.'), + $this->listen_addresses->getInternalXMLTagName() + )); + } + } + + return $messages; + } } diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.xml index 85826254e3..b942d30b08 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/General.xml @@ -1,151 +1,149 @@ //OPNsense/dnscryptproxy/general dnscrypt-proxy configuration - 0.1.1 + 0.1.3 - 0 + 0 Y - 127.0.0.1:5353,[::1]:5353 - N + 0.0.0.0:5353 + Y - 0 + 1 Y - 250 + 250 Y 1 10000 Choose a number between 1 and 10000. - 1 + 1 Y - 0 + 0 Y - 1 + 1 Y - 1 + 1 Y + + 0 + Y + - 0 + 0 Y - 1 + 1 Y - 0 + 0 Y - 0 + 0 Y - - N - + - 2500 + 2500 Y 100 10000 Choose a number between 100 and 10000. - 30 + 30 Y 1 600 Choose a number between 1 and 600. - 240 + 240 Y 1 3600 Choose a number between 1 and 3600. - 0 + 0 Y - 0 + 0 Y - 9.9.9.9:53 + 9.9.9.9:53 Y - 0 + 0 Y - 1 + 1 Y - 512 + 512 Y 1 20480 Choose a number between 1 and 20480. - 600 + 600 Y 1 3600 Choose a number between 1 and 3600. - 86400 + 86400 Y 1 86400 Choose a number between 1 and 86400. - 60 + 60 Y 1 3600 Choose a number between 1 and 3600. - 600 + 600 Y 1 86400 Choose a number between 1 and 86400. - - N - + - 1 + 1 Y - - /^([a-z0-9.,\-]{1,70})$/ - - N - , - Y + + /^[A-Za-z0-9\._\-]{1,70}(,[A-Za-z0-9\._\-]{1,70})*$/ + Please use valid server names. + diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Server.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Server.xml index 873cfaf6e6..2af1f6abb1 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Server.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Server.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Whitelist.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Whitelist.xml index f13e2463d3..c3d65113e7 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Whitelist.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Whitelist.xml @@ -6,15 +6,13 @@ - 1 + 1 Y Y - - N - + diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/views/OPNsense/Dnscryptproxy/general.volt b/dns/dnscrypt-proxy/src/opnsense/mvc/app/views/OPNsense/Dnscryptproxy/general.volt index c9106294bc..c73357a0bc 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/views/OPNsense/Dnscryptproxy/general.volt +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/views/OPNsense/Dnscryptproxy/general.volt @@ -1,31 +1,29 @@ {# - -OPNsense® is Copyright © 2014 – 2018 by Deciso B.V. -This file is Copyright © 2018 by Michael Muenz -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. - -#} + # Copyright (c) 2014-2018 Deciso B.V. + # Copyright (c) 2018 Michael Muenz + # 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. + #}
@@ -126,7 +123,7 @@ POSSIBILITY OF SUCH DAMAGE.

- +

@@ -154,15 +151,14 @@ POSSIBILITY OF SUCH DAMAGE.

- +

{{ partial("layout_partials/base_form",['fields':dnsblForm,'id':'frm_dnsbl_settings'])}} -
-
+
@@ -190,42 +186,42 @@ $( document ).ready(function() { }); $("#grid-forwards").UIBootgrid( - { 'search':'/api/dnscryptproxy/forward/searchForward', - 'get':'/api/dnscryptproxy/forward/getForward/', - 'set':'/api/dnscryptproxy/forward/setForward/', - 'add':'/api/dnscryptproxy/forward/addForward/', - 'del':'/api/dnscryptproxy/forward/delForward/', - 'toggle':'/api/dnscryptproxy/forward/toggleForward/' + { 'search':'/api/dnscryptproxy/forward/search_forward', + 'get':'/api/dnscryptproxy/forward/get_forward/', + 'set':'/api/dnscryptproxy/forward/set_forward/', + 'add':'/api/dnscryptproxy/forward/add_forward/', + 'del':'/api/dnscryptproxy/forward/del_forward/', + 'toggle':'/api/dnscryptproxy/forward/toggle_forward/' } ); $("#grid-cloaks").UIBootgrid( - { 'search':'/api/dnscryptproxy/cloak/searchCloak', - 'get':'/api/dnscryptproxy/cloak/getCloak/', - 'set':'/api/dnscryptproxy/cloak/setCloak/', - 'add':'/api/dnscryptproxy/cloak/addCloak/', - 'del':'/api/dnscryptproxy/cloak/delCloak/', - 'toggle':'/api/dnscryptproxy/cloak/toggleCloak/' + { 'search':'/api/dnscryptproxy/cloak/search_cloak', + 'get':'/api/dnscryptproxy/cloak/get_cloak/', + 'set':'/api/dnscryptproxy/cloak/set_cloak/', + 'add':'/api/dnscryptproxy/cloak/add_cloak/', + 'del':'/api/dnscryptproxy/cloak/del_cloak/', + 'toggle':'/api/dnscryptproxy/cloak/toggle_cloak/' } ); $("#grid-whitelists").UIBootgrid( - { 'search':'/api/dnscryptproxy/whitelist/searchWhitelist', - 'get':'/api/dnscryptproxy/whitelist/getWhitelist/', - 'set':'/api/dnscryptproxy/whitelist/setWhitelist/', - 'add':'/api/dnscryptproxy/whitelist/addWhitelist/', - 'del':'/api/dnscryptproxy/whitelist/delWhitelist/', - 'toggle':'/api/dnscryptproxy/whitelist/toggleWhitelist/' + { 'search':'/api/dnscryptproxy/whitelist/search_whitelist', + 'get':'/api/dnscryptproxy/whitelist/get_whitelist/', + 'set':'/api/dnscryptproxy/whitelist/set_whitelist/', + 'add':'/api/dnscryptproxy/whitelist/add_whitelist/', + 'del':'/api/dnscryptproxy/whitelist/del_whitelist/', + 'toggle':'/api/dnscryptproxy/whitelist/toggle_whitelist/' } ); $("#grid-servers").UIBootgrid( - { 'search':'/api/dnscryptproxy/server/searchServer', - 'get':'/api/dnscryptproxy/server/getServer/', - 'set':'/api/dnscryptproxy/server/setServer/', - 'add':'/api/dnscryptproxy/server/addServer/', - 'del':'/api/dnscryptproxy/server/delServer/', - 'toggle':'/api/dnscryptproxy/server/toggleServer/' + { 'search':'/api/dnscryptproxy/server/search_server', + 'get':'/api/dnscryptproxy/server/get_server/', + 'set':'/api/dnscryptproxy/server/set_server/', + 'add':'/api/dnscryptproxy/server/add_server/', + 'del':'/api/dnscryptproxy/server/del_server/', + 'toggle':'/api/dnscryptproxy/server/toggle_server/' } ); diff --git a/dns/dnscrypt-proxy/src/opnsense/scripts/OPNsense/Dnscryptproxy/dnsbl.sh b/dns/dnscrypt-proxy/src/opnsense/scripts/OPNsense/Dnscryptproxy/dnsbl.sh index 7f6153c238..77420eef0c 100755 --- a/dns/dnscrypt-proxy/src/opnsense/scripts/OPNsense/Dnscryptproxy/dnsbl.sh +++ b/dns/dnscrypt-proxy/src/opnsense/scripts/OPNsense/Dnscryptproxy/dnsbl.sh @@ -38,122 +38,129 @@ mkdir -p ${WORKDIR} easylist() { # EasyList ${FETCH} https://justdomains.github.io/blocklists/lists/easylist-justdomains.txt -o ${WORKDIR}/easylist-raw - sed "/\.$/d" ${WORKDIR}/easylist-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/easylist + sed "/\.$/d" ${WORKDIR}/easylist-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/easylist rm ${WORKDIR}/easylist-raw } easyprivacy() { # EasyPrivacy ${FETCH} https://justdomains.github.io/blocklists/lists/easyprivacy-justdomains.txt -o ${WORKDIR}/easyprivacy-raw - sed "/\.$/d" ${WORKDIR}/easyprivacy-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/easyprivacy + sed "/\.$/d" ${WORKDIR}/easyprivacy-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/easyprivacy rm ${WORKDIR}/easyprivacy-raw } pornall() { # PornAll ${FETCH} https://raw.githubusercontent.com/chadmayfield/my-pihole-blocklists/master/lists/pi_blocklist_porn_all.list -o ${WORKDIR}/pornall-raw - sed "/\.$/d" ${WORKDIR}/pornall-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/pornall + sed "/\.$/d" ${WORKDIR}/pornall-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/pornall rm ${WORKDIR}/pornall-raw } porntop() { # PornTop1M ${FETCH} https://raw.githubusercontent.com/chadmayfield/pihole-blocklists/master/lists/pi_blocklist_porn_top1m.list -o ${WORKDIR}/porntop-raw - sed "/\.$/d" ${WORKDIR}/porntop-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/porntop + sed "/\.$/d" ${WORKDIR}/porntop-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/porntop rm ${WORKDIR}/porntop-raw } adguard() { # AdGuard ${FETCH} https://justdomains.github.io/blocklists/lists/adguarddns-justdomains.txt -o ${WORKDIR}/adguard-raw - sed "/\.$/d" ${WORKDIR}/adguard-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/adguard + sed "/\.$/d" ${WORKDIR}/adguard-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/adguard rm ${WORKDIR}/adguard-raw } nocoin() { # NoCoin ${FETCH} https://justdomains.github.io/blocklists/lists/nocoin-justdomains.txt -o ${WORKDIR}/nocoin-raw - sed "/\.$/d" ${WORKDIR}/nocoin-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/nocoin + sed "/\.$/d" ${WORKDIR}/nocoin-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/nocoin rm ${WORKDIR}/nocoin-raw } windowsspyblockerspy() { # WindowsSpyBlocker (spy) ${FETCH} https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt -o ${WORKDIR}/windowsspyblockerspy-raw - sed "/\.$/d" ${WORKDIR}/windowsspyblockerspy-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/windowsspyblockerspy + sed "/\.$/d" ${WORKDIR}/windowsspyblockerspy-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/windowsspyblockerspy rm ${WORKDIR}/windowsspyblockerspy-raw } windowsspyblockerupdate() { # WindowsSpyBlocker (update) ${FETCH} https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/update.txt -o ${WORKDIR}/windowsspyblockerupdate-raw - sed "/\.$/d" ${WORKDIR}/windowsspyblockerupdate-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/windowsspyblockerupdate + sed "/\.$/d" ${WORKDIR}/windowsspyblockerupdate-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/windowsspyblockerupdate rm ${WORKDIR}/windowsspyblockerupdate-raw } windowsspyblockerextra() { # WindowsSpyBlocker (extra) ${FETCH} https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/extra.txt -o ${WORKDIR}/windowsspyblockerextra-raw - sed "/\.$/d" ${WORKDIR}/windowsspyblockerextra-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/windowsspyblockerextra + sed "/\.$/d" ${WORKDIR}/windowsspyblockerextra-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/windowsspyblockerextra rm ${WORKDIR}/windowsspyblockerextra-raw } adaway() { # AdAway List ${FETCH} https://adaway.org/hosts.txt -o ${WORKDIR}/adaway-raw - sed "/\.$/d" ${WORKDIR}/adaway-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/adaway + sed "/\.$/d" ${WORKDIR}/adaway-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/adaway rm ${WORKDIR}/adaway-raw } yoyo() { # YoYo List ${FETCH} "http://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext" -o ${WORKDIR}/yoyo-raw - sed "/\.$/d" ${WORKDIR}/yoyo-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/yoyo + sed "/\.$/d" ${WORKDIR}/yoyo-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/yoyo rm ${WORKDIR}/yoyo-raw } stevenblack() { # StevenBlack ${FETCH} https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts -o ${WORKDIR}/stevenblack-raw - sed "/\.$/d" ${WORKDIR}/stevenblack-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/stevenblack + sed "/\.$/d" ${WORKDIR}/stevenblack-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | tr -d '\r' | awk 'BEGIN{FS=OFS=" ";}{print $2;}' > ${WORKDIR}/stevenblack rm ${WORKDIR}/stevenblack-raw } blocklistads() { # Blocklist.site Ads ${FETCH} https://blocklistproject.github.io/Lists/ads.txt -o ${WORKDIR}/blocklistads-raw - sed "/\.$/d" ${WORKDIR}/blocklistads-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | awk '{print $2}' > ${WORKDIR}/blocklistads + sed "/\.$/d" ${WORKDIR}/blocklistads-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | awk '{print $2}' > ${WORKDIR}/blocklistads rm ${WORKDIR}/blocklistads-raw } blocklistfraud() { # Blocklist.site Fraud ${FETCH} https://blocklistproject.github.io/Lists/fraud.txt -o ${WORKDIR}/blocklistfraud-raw - sed "/\.$/d" ${WORKDIR}/blocklistfraud-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" |awk '{print $2}' > ${WORKDIR}/blocklistfraud + sed "/\.$/d" ${WORKDIR}/blocklistfraud-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" |awk '{print $2}' > ${WORKDIR}/blocklistfraud rm ${WORKDIR}/blocklistfraud-raw } blocklistphishing() { # Blocklist.site Phishing ${FETCH} https://blocklistproject.github.io/Lists/phishing.txt -o ${WORKDIR}/blocklistphishing-raw - sed "/\.$/d" ${WORKDIR}/blocklistphishing-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | awk '{print $2}' > ${WORKDIR}/blocklistphishing + sed "/\.$/d" ${WORKDIR}/blocklistphishing-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" | sed "/localhost/d" | sed "/127\.0\.0\.1/d" | sed "/255\.255\.255\.255/d" | sed "/\:\:1/d" | sed "/fe80\:\:1/d" | sed "/ff00\:\:/d" | sed "/ff02\:\:/d" | sed "/0\.0\.0\.0 0\.0\.0\.0/d" | awk '{print $2}' > ${WORKDIR}/blocklistphishing rm ${WORKDIR}/blocklistphishing-raw } simplead() { # Simple Ad List ${FETCH} https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt -o ${WORKDIR}/simplead-raw - sed "/\.$/d" ${WORKDIR}/simplead-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/simplead + sed "/\.$/d" ${WORKDIR}/simplead-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/simplead rm ${WORKDIR}/simplead-raw } simpletrack() { # Simple Tracking List ${FETCH} https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt -o ${WORKDIR}/simpletrack-raw - sed "/\.$/d" ${WORKDIR}/simpletrack-raw | sed "/^#/d" | sed "/\_/d" | sed "/^\s*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/simpletrack + sed "/\.$/d" ${WORKDIR}/simpletrack-raw | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/simpletrack rm ${WORKDIR}/simpletrack-raw } +qfeeds() { + # Q-Feeds List + if [ -f "/usr/local/etc/dnscrypt-proxy/blacklist-qfeeds.txt" ] && [ -s "/usr/local/etc/dnscrypt-proxy/blacklist-qfeeds.txt" ]; then + sed "/\.$/d" /usr/local/etc/dnscrypt-proxy/blacklist-qfeeds.txt | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/qfeeds + fi +} + install() { # Put all files in correct format for FILE in $(find ${WORKDIR} -type f); do @@ -222,6 +229,9 @@ for CAT in $(echo ${DNSBL} | tr ',' ' '); do yy) yoyo ;; + qf) + qfeeds + ;; esac done diff --git a/dns/dnscrypt-proxy/src/opnsense/service/conf/actions.d/actions_dnscryptproxy.conf b/dns/dnscrypt-proxy/src/opnsense/service/conf/actions.d/actions_dnscryptproxy.conf index 3eae8eadb0..ded64e9db8 100644 --- a/dns/dnscrypt-proxy/src/opnsense/service/conf/actions.d/actions_dnscryptproxy.conf +++ b/dns/dnscrypt-proxy/src/opnsense/service/conf/actions.d/actions_dnscryptproxy.conf @@ -1,5 +1,5 @@ [start] -command:/usr/local/opnsense/scripts/OPNsense/Dnscryptproxy/setup.sh;/usr/local/etc/rc.d/dnscrypt-proxy start +command:/usr/local/etc/rc.d/dnscrypt-proxy start parameters: type:script message:starting dnscrypt-proxy @@ -11,7 +11,7 @@ type:script message:stopping dnscrypt-proxy [restart] -command:/usr/local/opnsense/scripts/OPNsense/Dnscryptproxy/setup.sh;/usr/local/etc/rc.d/dnscrypt-proxy restart +command:/usr/local/etc/rc.d/dnscrypt-proxy restart parameters: type:script message:restarting dnscrypt-proxy @@ -29,7 +29,7 @@ type:script message:fetching DNSBLs [dnsblcron] -command:/usr/local/opnsense/scripts/OPNsense/Dnscryptproxy/dnsbl.sh;/usr/local/etc/rc.d/dnscrypt-proxy restart +command:/usr/local/opnsense/scripts/OPNsense/Dnscryptproxy/dnsbl.sh; /usr/local/etc/rc.d/dnscrypt-proxy restart parameters: type:script message:fetching DNSBLs and restart diff --git a/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt-proxy.toml b/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt-proxy.toml index 2207b162b1..84d98ff086 100644 --- a/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt-proxy.toml +++ b/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt-proxy.toml @@ -5,7 +5,7 @@ server_names = [{{ "'" + ("','".join(OPNsense.dnscryptproxy.general.serverlist.s {% endif %} {% if helpers.exists('OPNsense.dnscryptproxy.general.disabled_serverlist') and OPNsense.dnscryptproxy.general.disabled_serverlist != '' %} -disabled_server_names = ['{{OPNsense.dnscryptproxy.general.disabled_serverlist}}'] +disabled_server_names = [{{ "'" + ("','".join(OPNsense.dnscryptproxy.general.disabled_serverlist.split(','))) + "'" }}] {% endif %} {% if helpers.exists('OPNsense.dnscryptproxy.general.listen_addresses') and OPNsense.dnscryptproxy.general.listen_addresses != '' %} @@ -40,6 +40,12 @@ doh_servers = true doh_servers = false {% endif %} +{% if helpers.exists('OPNsense.dnscryptproxy.general.odoh_servers') and OPNsense.dnscryptproxy.general.odoh_servers == '1' %} +odoh_servers = true +{% else %} +odoh_servers = false +{% endif %} + {% if helpers.exists('OPNsense.dnscryptproxy.general.require_dnssec') and OPNsense.dnscryptproxy.general.require_dnssec == '1' %} require_dnssec = true {% else %} @@ -89,7 +95,7 @@ tls_disable_session_tickets = true tls_disable_session_tickets = false {% endif %} -fallback_resolver = '{{ OPNsense.dnscryptproxy.general.fallback_resolver }}' +bootstrap_resolvers = ['{{ OPNsense.dnscryptproxy.general.fallback_resolver }}'] {% if helpers.exists('OPNsense.dnscryptproxy.general.ignore_system_dns') and OPNsense.dnscryptproxy.general.ignore_system_dns == '1' %} ignore_system_dns = true @@ -146,12 +152,48 @@ cache = false [sources] [sources.'public-resolvers'] - urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md'] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/public-resolvers.md'] cache_file = 'public-resolvers.md' minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' refresh_delay = 72 prefix = '' + ## Anonymized DNS relays + + [sources.'relays'] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/relays.md'] + cache_file = 'relays.md' + minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + refresh_delay = 72 + prefix = '' + + ## Oblivious DoH servers + + [sources.'odoh-servers'] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-servers.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-servers.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/odoh-servers.md'] + cache_file = 'odoh-servers.md' + minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + refresh_delay = 72 + prefix = '' + + ## Oblivious DoH relays + + [sources.'odoh-relays'] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/odoh-relays.md'] + cache_file = 'odoh-relays.md' + minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + refresh_delay = 72 + prefix = '' + +[anonymized_dns] + +{% if helpers.exists('OPNsense.dnscryptproxy.general.relaylist') and OPNsense.dnscryptproxy.general.relaylist != '' %} + routes = [ + { server_name='*', via=[{{ "'" + ("','".join(OPNsense.dnscryptproxy.general.relaylist.split(','))) + "'" }}] } + ] +{% endif %} + + [static] {% if helpers.exists('OPNsense.dnscryptproxy.server.servers.server') %} {% for server_list in helpers.toList('OPNsense.dnscryptproxy.server.servers.server') %} diff --git a/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt_proxy b/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt_proxy index 96c051b26a..6dad9e923d 100644 --- a/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt_proxy +++ b/dns/dnscrypt-proxy/src/opnsense/service/templates/OPNsense/Dnscryptproxy/dnscrypt_proxy @@ -1,5 +1,5 @@ {% if helpers.exists('OPNsense.dnscryptproxy.general.enabled') and OPNsense.dnscryptproxy.general.enabled == '1' %} -dnscrypt_proxy_var_script="/usr/local/opnsense/scripts/OPNsense/Dnscryptproxy/setup.sh" +dnscrypt_proxy_setup="/usr/local/opnsense/scripts/OPNsense/Dnscryptproxy/setup.sh" dnscrypt_proxy_enable="YES" {% if helpers.exists('OPNsense.dnscryptproxy.general.allowprivileged') and OPNsense.dnscryptproxy.general.allowprivileged == '1' %} dnscrypt_proxy_suexec="YES" diff --git a/dns/dyndns/Makefile b/dns/dyndns/Makefile deleted file mode 100644 index 67afe795c0..0000000000 --- a/dns/dyndns/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -PLUGIN_NAME= dyndns -PLUGIN_VERSION= 1.27 -PLUGIN_COMMENT= Dynamic DNS Support -PLUGIN_MAINTAINER= franco@opnsense.org - -.include "../../Mk/plugins.mk" diff --git a/dns/dyndns/pkg-descr b/dns/dyndns/pkg-descr deleted file mode 100644 index 8f8dcb2326..0000000000 --- a/dns/dyndns/pkg-descr +++ /dev/null @@ -1,31 +0,0 @@ -Support for numerous Dynamic DNS services (DynDNS et al) - -Plugin Changelog -================ - -1.27 - -* Add missing digitalocean-v6 switch case (contributed by zarcjap) - -1.26 - -* Add support for 1984 Hosting Company (contributed by Adrian Fedoreanu) -* Add %HOST% support for custom dynamic DNS (contributed by Marc Collins) -* Add PAYLOAD log and debug support for missing code spots - -1.25 - -* Make regfish DynDNS work for IPv4 and IPv6 (contributed by DeepCoreSystem) -* Add STRATO IPv6 to DynDNS (contributed by Jan Wiesemann) -* Add desec.io wildcard support (contributed by Luca Zeug) -* Accept wildcard entry for Hetzner - -1.24 - -* Fix FreeDNS update in DynDNS plugin (contributed by rmrfus) -* Add hetzner dns console to supported dyndns services (contributed by schreibubi) -* Add support for deSEC.io (contributed by Michael Paul) -* Add All-Inkl v4 and v6 DynDNS support (contributed by polkhigh33) -* Add support for digitalocean v6 records (contributed by Jan Koppe) -* Update No-Ip client code to use APIv2 (contributed by Victor Kislov) -* Fix copy and paste unprotected JSON object access diff --git a/dns/dyndns/src/etc/inc/plugins.inc.d/dyndns.inc b/dns/dyndns/src/etc/inc/plugins.inc.d/dyndns.inc deleted file mode 100644 index 762549e53e..0000000000 --- a/dns/dyndns/src/etc/inc/plugins.inc.d/dyndns.inc +++ /dev/null @@ -1,251 +0,0 @@ - - * Copyright (C) 2010 Ermal Luçi - * Copyright (C) 2005-2006 Colin Smith - * Copyright (C) 2003-2004 Manuel Kasper - * 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. - */ - -require_once('plugins.inc.d/dyndns/phpDynDNS.inc'); -require_once('plugins.inc.d/dyndns/r53.inc'); - -function dyndns_configure() -{ - return array( - 'bootup' => array('dyndns_configure_do'), - 'local' => array('dyndns_configure_do'), - 'newwanip' => array('dyndns_configure_do:2'), - ); -} - -function dyndns_enabled() -{ - global $config; - - if (isset($config['dyndnses']['dyndns'])) { - foreach ($config['dyndnses']['dyndns'] as $conf) { - if (isset($conf['enable'])) { - return true; - } - } - } - - return false; -} - -function dyndns_services() -{ - global $config; - - $services = array(); - - if (dyndns_enabled()) { - $services[] = array( - 'description' => gettext('Dynamic DNS'), - 'configd' => array( - 'restart' => array('dyndns reload'), - ), - 'nocheck' => true, - 'name' => 'dyndns', - ); - } - - return $services; -} - -function dyndns_cron() -{ - $jobs = array(); - - if (dyndns_enabled()) { - $jobs[]['autocron'] = array('/usr/local/etc/rc.dyndns', '11', '1'); - } - - return $jobs; -} - -function dyndns_list() -{ - /* - * XXX something like this would be cool: - * - * https://github.com/openwrt/packages/blob/master/net/ddns-scripts/files/services - */ - - return array( - '1984-hosting' => '1984 Hosting Company', - '3322' => '3322', - 'all-inkl' => 'All-Inkl', - 'all-inkl-v6' => 'All-Inkl (v6)', - 'azure' => 'Azure DNS', - 'azurev6' => 'Azure DNS (v6)', - 'citynetwork' => 'City Network', - 'cloudflare' => 'Cloudflare', - 'cloudflare-token' => 'Cloudflare API token', - 'cloudflare-token-v6' => 'Cloudflare API token (v6)', - 'cloudflare-v6' => 'Cloudflare (v6)', - 'custom' => 'Custom', - 'custom-v6' => 'Custom (v6)', - 'desec' => 'deSEC', - 'desec-v4-v6' => 'deSEC (v4+v6)', - 'desec-v6' => 'deSEC (v6)', - 'dhs' => 'DHS', - 'digitalocean' => 'DigitalOcean', - 'digitalocean-v6' => 'DigitalOcean (v6)', - 'dnsexit' => 'DNSexit', - 'dnsomatic' => 'DNS-O-Matic', - 'duckdns' => 'Duck DNS', - 'dyndns' => 'DynDNS (dynamic)', - 'dyndns-custom' => 'DynDNS (custom)', - 'dyndns-static' => 'DynDNS (static)', - 'dyns' => 'DyNS', - 'dynv6' => 'dynv6', - 'dynv6-v6' => 'dynv6 (v6)', - 'easydns' => 'easyDNS', - 'eurodns' => 'EuroDNS', - 'freedns' => 'freeDNS', - 'gandi-livedns' => 'Gandi LiveDNS', - 'godaddy' => 'GoDaddy', - 'godaddy-v6' => 'GoDaddy (v6)', - 'googledomains' => 'Google Domains', - 'gratisdns' => 'GratisDNS', - 'he-net' => 'HE.net', - 'he-net-tunnelbroker' => 'HE.net Tunnelbroker', - 'he-net-v6' => 'HE.net (v6)', - 'hetzner' => 'Hetzner DNS Console', - 'hetzner-v6' => 'Hetzner DNS Console (v6)', - 'linode' => 'Linode', - 'linode-v6' => 'Linode (v6)', - 'loopia' => 'Loopia', - 'namecheap' => 'Namecheap', - 'noip' => 'No-IP', - 'noip-free' => 'No-IP (free)', - 'ods' => 'ODS.org', - 'oray' => 'Oray', - 'ovh-dynhost' => 'OVH DynHOST', - 'regfish' => 'regfish', - 'regfish-v6' => 'regfish (v6)', - 'route53' => 'Route 53', - 'route53-v6' => 'Route 53 (v6)', - 'selfhost' => 'SelfHost', - 'strato' => 'STRATO', - 'strato-v6' => 'STRATO (v6)', - 'zoneedit' => 'ZoneEdit', - ); -} - -function dyndns_cache_file($conf, $ipver = 4) -{ - $ipver = $ipver == 6 ? '_v6' : ''; - - return "/var/cache/dyndns_{$conf['interface']}_{$conf['host']}_{$conf['id']}{$ipver}.cache"; -} - -function dyndns_configure_client($conf) -{ - if (!isset($conf['enable'])) { - return; - } - - $dns = new updatedns( - $dnsService = $conf['type'], - $dnsHost = $conf['host'], - $dnsUser = $conf['username'], - $dnsPass = $conf['password'], - $dnsWilcard = $conf['wildcard'], - $dnsMX = $conf['mx'], - $dnsIf = "{$conf['interface']}", - $dnsBackMX = null, - $dnsServer = null, - $dnsPort = null, - $dnsUpdateURL = "{$conf['updateurl']}", - $forceUpdate = $conf['force'], - $dnsZoneID = $conf['zoneid'], - $dnsResourceID = $conf['resourceid'], - $dnsTTL = $conf['ttl'], - $dnsResultMatch = "{$conf['resultmatch']}", - $dnsRequestIf = "{$conf['requestif']}", - $dnsID = "{$conf['id']}", - $dnsVerboseLog = $conf['verboselog'], - $curlIpresolveV4 = $conf['curl_ipresolve_v4'], - $curlSslVerifypeer = $conf['curl_ssl_verifypeer'] - ); -} - -function dyndns_configure_do($verbose = false, $int = '') -{ - global $config; - - if (!dyndns_enabled()) { - return; - } - - $dyndnscfg = $config['dyndnses']['dyndns']; - $gwgroups = return_gateway_groups_array(); - - if ($verbose) { - echo 'Configuring dynamic DNS clients...'; - flush(); - } - - foreach ($dyndnscfg as $dyndns) { - if ((empty($int)) || ($int == $dyndns['interface']) || (is_array($gwgroups[$dyndns['interface']]))) { - $dyndns['verboselog'] = isset($dyndns['verboselog']); - $dyndns['curl_ipresolve_v4'] = isset($dyndns['curl_ipresolve_v4']); - $dyndns['curl_ssl_verifypeer'] = isset($dyndns['curl_ssl_verifypeer']); - dyndns_configure_client($dyndns); - sleep(1); - } - } - - if ($verbose) { - echo "done.\n"; - } -} - -function dyndns_failover_interface($interface, $family = 'all') -{ - global $config; - - /* shortcut for known interfaces */ - if (isset($config['interfaces'][$interface])) { - return get_real_interface($interface, $family); - } - - /* compare against gateway groups */ - $a_groups = return_gateway_groups_array(); - if (isset($a_groups[$interface])) { - /* we found a gateway group, fetch the interface or vip */ - if ($a_groups[$interface][0]['vip'] != '') { - return $a_groups[$interface][0]['vip']; - } else { - return $a_groups[$interface][0]['int']; - } - } - - /* fall through to get real interface the hard way */ - return get_real_interface($interface, $family); -} diff --git a/dns/dyndns/src/etc/inc/plugins.inc.d/dyndns/phpDynDNS.inc b/dns/dyndns/src/etc/inc/plugins.inc.d/dyndns/phpDynDNS.inc deleted file mode 100644 index d7b5582cfd..0000000000 --- a/dns/dyndns/src/etc/inc/plugins.inc.d/dyndns/phpDynDNS.inc +++ /dev/null @@ -1,2041 +0,0 @@ - $dnsHost, 'id' => $dnsID, 'interface' => $dnsIf); - $this->_cacheFile = dyndns_cache_file($conf, 4); - $this->_cacheFile_v6 = dyndns_cache_file($conf, 6); - $this->_debugFile = dyndns_cache_file($conf, 4) . '.debug'; - $this->_dnsServiceList = dyndns_list(); - - $this->_curlIpresolveV4 = $curlIpresolveV4; - $this->_curlSslVerifypeer = $curlSslVerifypeer; - $this->_dnsVerboseLog = $dnsVerboseLog; - if ($this->_dnsVerboseLog) { - log_error("Dynamic DNS: updatedns() starting"); - } - - $dyndnslck = lock("DDNS" . $dnsID, LOCK_EX); - - if (!$dnsService) { - $this->_error(2); - } - switch ($dnsService) { - case 'freedns': - if (!$dnsHost) { - $this->_error(5); - } - break; - case 'linode': - case 'linode-v6': - case 'namecheap': - case '1984-hosting': - if (!$dnsPass) { - $this->_error(4); - } elseif (!$dnsHost) { - $this->_error(5); - } - break; - case 'route53': - case 'route53-v6': - if (!$dnsZoneID) { - $this->_error(8); - } elseif (!$dnsTTL) { - $this->_error(9); - } - break; - case 'custom': - case 'custom-v6': - if (!$dnsUpdateURL) { - $this->_error(7); - } - break; - case 'duckdns': - case 'dynv6': - case 'dynv6-v6': - case 'regfish': - case 'regfish-v6': - if (!$dnsUser) { - $this->_error(3); - } elseif (!$dnsHost) { - $this->_error(5); - } - break; - case 'azure': - case 'azurev6': - if (!$dnsUser) { - $this->_error(3); - } elseif (!$dnsPass) { - $this->_error(4); - } elseif (!$dnsHost) { - $this->_error(5); - } elseif (!$dnsResourceID) { - $this->_error(8); - } elseif (!$dnsTTL) { - $this->_error(9); - } - break; - case 'cloudflare-token': - case 'cloudflare-token-v6': - case 'hetzner': - case 'hetzner-v6': - if (!$dnsPass) { - $this->_error(4); - } elseif (!$dnsHost) { - $this->_error(5); - } elseif (!$dnsTTL) { - $this->_error(9); - } - break; - case 'desec': - case 'desec-v4-v6': - case 'desec-v6': - if (!$dnsPass) { - $this->_error(4); - } elseif (!$dnsHost) { - $this->_error(5); - } - break; - default: - if (!$dnsUser) { - $this->_error(3); - } elseif (!$dnsPass) { - $this->_error(4); - } elseif (!$dnsHost) { - $this->_error(5); - } - break; - } - - switch ($dnsService) { - case 'all-inkl-v6': - case 'azurev6': - case 'cloudflare-token-v6': - case 'cloudflare-v6': - case 'custom-v6': - case 'desec-v4-v6': - case 'desec-v6': - case 'digitalocean-v6': - case 'dynv6-v6': - case 'godaddy-v6': - case 'he-net-v6': - case 'hetzner-v6': - case 'linode-v6': - case 'regfish-v6': - case 'route53-v6': - case 'strato-v6': - $this->_useIPv6 = true; - break; - default: - $this->_useIPv6 = false; - } - $this->_dnsService = strtolower($dnsService); - $this->_dnsUser = $dnsUser; - $this->_dnsPass = $dnsPass; - $this->_dnsHost = $dnsHost; - $this->_dnsServer = $dnsServer; - $this->_dnsPort = $dnsPort; - $this->_dnsWildcard = $dnsWildcard; - $this->_dnsMX = $dnsMX; - $this->_dnsZoneID = $dnsZoneID; - $this->_dnsResourceID = $dnsResourceID; - $this->_dnsTTL = $dnsTTL; - $this->_if = dyndns_failover_interface($dnsIf, $this->_useIPv6 ? 'inet6' : 'all'); - $this->_checkIP(); - $this->_dnsUpdateURL = $dnsUpdateURL; - $this->_dnsResultMatch = $dnsResultMatch; - $this->_dnsRequestIf = dyndns_failover_interface($dnsRequestIf, $this->_useIPv6 ? 'inet6' : 'all'); - if ($this->_dnsVerboseLog) { - log_error("Dynamic DNS ({$this->_dnsHost}): running dyndns_failover_interface for {$dnsRequestIf}. found {$this->_dnsRequestIf}"); - } - $this->_dnsRequestIfIP = $this->_useIPv6 ? get_interface_ipv6($this->_dnsRequestIf) : get_interface_ip($this->_dnsRequestIf); - $this->_dnsMaxCacheAgeDays = 25; - $this->_dnsDummyUpdateDone = false; - $this->_forceUpdateNeeded = $forceUpdate; - - // Ensure that we were able to lookup the IP - if (!is_ipaddr($this->_dnsIP)) { - log_error("Dynamic DNS ({$this->_dnsHost}) There was an error trying to determine the public IP for interface - {$dnsIf}({$this->_if}). Probably interface is not a WAN interface."); - unlock($dyndnslck); - return; - } - - $this->_debugID = rand(1000000, 9999999); - - if ($forceUpdate == false && $this->_detectChange() == false) { - $this->_error(10); - } else { - switch ($this->_dnsService) { - case '1984-hosting': - case '3322': - case 'all-inkl': - case 'all-inkl-v6': - case 'azure': - case 'azurev6': - case 'citynetwork': - case 'cloudflare': - case 'cloudflare-v6': - case 'cloudflare-token': - case 'cloudflare-token-v6': - case 'custom': - case 'custom-v6': - case 'desec': - case 'desec-v4-v6': - case 'desec-v6': - case 'dhs': - case 'digitalocean': - case 'digitalocean-v6': - case 'gandi-livedns': - case 'dnsexit': - case 'dnsomatic': - case 'duckdns': - case 'dyndns': - case 'dyndns-custom': - case 'dyndns-static': - case 'dyns': - case 'dynv6': - case 'dynv6-v6': - case 'easydns': - case 'eurodns': - case 'freedns': - case 'godaddy': - case 'godaddy-v6': - case 'googledomains': - case 'gratisdns': - case 'he-net': - case 'he-net-tunnelbroker': - case 'he-net-v6': - case 'hetzner': - case 'hetzner-v6': - case 'hn': - case 'linode': - case 'linode-v6': - case 'loopia': - case 'namecheap': - case 'noip': - case 'noip-free': - case 'ods': - case 'oray': - case 'ovh-dynhost': - case 'regfish': - case 'regfish-v6': - case 'route53': - case 'route53-v6': - case 'selfhost': - case 'staticcling': - case 'strato': - case 'strato-v6': - case 'zoneedit': - $this->_update(); - if ($this->_dnsDummyUpdateDone == true) { - // If a dummy update was needed, then sleep a while and do the update again to put the proper address back. - // Some providers (e.g. No-IP free accounts) need to have at least 1 address change every month. - // If the address has not changed recently, or the user did "Force Update", then the code does - // a dummy address change for providers like this. - sleep(10); - $this->_update(); - } - break; - default: - $this->_error(6); - break; - } - } - - unlock($dyndnslck); - } - - /* - * Private Function (added 12 July 05) [beta] - * Send Update To Selected Service. - */ - function _update() - { - if ($this->_dnsVerboseLog) { - log_error("Dynamic DNS ({$this->_dnsHost} via {$this->_dnsServiceList[$this->_dnsService]}): _update() starting."); - } - - if ($this->_dnsService != 'ods' and $this->_dnsService != 'route53' and $this->_dnsService != 'route53-v6') { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_USERAGENT, $this->_UserAgent); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_INTERFACE, $this->_dnsRequestIfIP); - curl_setopt($ch, CURLOPT_TIMEOUT, 15); - } - - switch ($this->_dnsService) { - case 'dyndns': - case 'dyndns-static': - case 'dyndns-custom': - if (isset($this->_dnsWildcard) && $this->_dnsWildcard != "OFF") { - $this->_dnsWildcard = "ON"; - } - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - $server = "https://members.dyndns.org/nic/update"; - $port = ""; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - curl_setopt($ch, CURLOPT_URL, $server . $port . '?system=dyndns&hostname=' . $this->_dnsHost . '&myip=' . $this->_dnsIP . '&wildcard=' . $this->_dnsWildcard . '&mx=' . $this->_dnsMX . '&backmx=NO'); - break; - case 'dhs': - $post_data['hostscmd'] = 'edit'; - $post_data['hostscmdstage'] = '2'; - $post_data['type'] = '4'; - $post_data['updatetype'] = 'Online'; - $post_data['mx'] = $this->_dnsMX; - $post_data['mx2'] = ''; - $post_data['txt'] = ''; - $post_data['offline_url'] = ''; - $post_data['cloak'] = 'Y'; - $post_data['cloak_title'] = ''; - $post_data['ip'] = $this->_dnsIP; - $post_data['domain'] = 'dyn.dhs.org'; - $post_data['hostname'] = $this->_dnsHost; - $post_data['submit'] = 'Update'; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - $server = "https://members.dhs.org/nic/hosts"; - $port = ""; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - curl_setopt($ch, CURLOPT_URL, '{$server}{$port}'); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); - break; - case 'noip': - case 'noip-free': - curl_setopt_array($ch, [ - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_USERPWD => $this->_dnsUser . ':' . $this->_dnsPass - ]); - $server = "https://dynupdate.no-ip.com/nic/update"; - $port = ""; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - if ( - ($this->_dnsService == "noip-free") && - ($this->_forceUpdateNeeded == true) && - ($this->_dnsDummyUpdateDone == false) - ) { - // Update the IP to a dummy value to force No-IP free accounts to see a change. - $iptoset = "192.168.1.1"; - $this->_dnsDummyUpdateDone = true; - log_error("Dynamic DNS ({$this->_dnsHost}): Processing dummy update on No-IP free account. IP temporarily set to " . $iptoset); - } else { - $iptoset = $this->_dnsIP; - } - curl_setopt($ch, CURLOPT_URL, $server . $port . '?hostname=' . $this->_dnsHost . '&myip=' . $iptoset); - break; - case 'easydns': - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - $server = "https://members.easydns.com/dyn/dyndns.php"; - $port = ""; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - curl_setopt($ch, CURLOPT_URL, $server . $port . '?hostname=' . $this->_dnsHost . '&myip=' . $this->_dnsIP . '&wildcard=' . $this->_dnsWildcard . '&mx=' . $this->_dnsMX . '&backmx=' . $this->_dnsBackMX); - break; - case 'hn': - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - $server = "http://dup.hn.org/vanity/update"; - $port = ""; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - curl_setopt($ch, CURLOPT_URL, $server . $port . '?ver=1&IP=' . $this->_dnsIP); - break; - case 'zoneedit': - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - - $server = "https://dynamic.zoneedit.com/auth/dynamic.html"; - $port = ""; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - curl_setopt($ch, CURLOPT_URL, "{$server}{$port}?host=" . $this->_dnsHost); - break; - case 'dyns': - /* XXX HTTPS is currently broken for them */ - $server = 'http://www.dyns.cx/postscript011.php'; - $port = ''; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - curl_setopt($ch, CURLOPT_URL, $server . $port . '?username=' . urlencode($this->_dnsUser) . '&password=' . $this->_dnsPass . '&host=' . $this->_dnsHost); - break; - case 'ods': - $misc_errno = 0; - $misc_error = ""; - $server = "ods.org"; - $port = ""; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - $this->con['socket'] = fsockopen("{$server}{$port}", "7070", $misc_errno, $misc_error, 30); - /* Check that we have connected */ - if (!$this->con['socket']) { - print "error! could not connect."; - break; - } - /* Here is the loop. Read the incoming data (from the socket connection) */ - while (!feof($this->con['socket'])) { - $this->con['buffer']['all'] = trim(fgets($this->con['socket'], 4096)); - $code = substr($this->con['buffer']['all'], 0, 3); - sleep(1); - switch ($code) { - case 100: - fputs($this->con['socket'], "LOGIN " . $this->_dnsUser . " " . $this->_dnsPass . "\n"); - break; - case 225: - fputs($this->con['socket'], "DELRR " . $this->_dnsHost . " A\n"); - break; - case 901: - fputs($this->con['socket'], "ADDRR " . $this->_dnsHost . " A " . $this->_dnsIP . "\n"); - break; - case 795: - fputs($this->con['socket'], "QUIT\n"); - break; - } - } - $this->_checkStatus(0, $code); - break; - case 'freedns': - curl_setopt($ch, CURLOPT_URL, 'https://sync.afraid.org/u/' . $this->_dnsPass . '/'); - break; - case 'dnsexit': - curl_setopt($ch, CURLOPT_URL, 'https://update.dnsexit.com/RemoteUpdate.sv?login=' . urlencode($this->_dnsUser) . '&password=' . $this->_dnsPass . '&host=' . $this->_dnsHost . '&myip=' . $this->_dnsIP); - break; - case 'loopia': - $this->_dnsWildcard = (isset($this->_dnsWildcard) && $this->_dnsWildcard == true) ? 'ON' : 'OFF'; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - curl_setopt($ch, CURLOPT_URL, 'https://dns.loopia.se/XDynDNSServer/XDynDNS.php?hostname=' . $this->_dnsHost . '&myip=' . $this->_dnsIP . '&wildcard=' . $this->_dnsWildcard); - break; - case 'staticcling': - curl_setopt($ch, CURLOPT_URL, 'https://www.staticcling.org/update.html?login=' . urlencode($this->_dnsUser) . '&pass=' . $this->_dnsPass); - break; - case 'dnsomatic': - /* Example syntax - https://username:password@updates.dnsomatic.com/nic/update?hostname=yourhostname&myip=ipaddress&wildcard=NOCHG&mx=NOCHG&backmx=NOCHG - */ - if (isset($this->_dnsWildcard) && $this->_dnsWildcard != "OFF") { - $this->_dnsWildcard = "ON"; - } - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - /* - Reference: https://www.dnsomatic.com/wiki/api - DNS-O-Matic usernames are 3-25 characters. - DNS-O-Matic passwords are 6-20 characters. - All ASCII letters and numbers accepted. - Dots, dashes, and underscores allowed, but not at the beginning or end of the string. - Required: "rawurlencode" http://www.php.net/manual/en/function.rawurlencode.php - Encodes the given string according to RFC 3986. - */ - $server = "https://" . rawurlencode($this->_dnsUser) . ":" . rawurlencode($this->_dnsPass) . "@updates.dnsomatic.com/nic/update?hostname="; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - curl_setopt($ch, CURLOPT_URL, $server . $this->_dnsHost . '&myip=' . $this->_dnsIP . '&wildcard=' . $this->_dnsWildcard . '&mx=' . $this->_dnsMX . '&backmx=NOCHG'); - break; - case 'namecheap': - /* Example: - https://dynamicdns.park-your-domain.com/update?host=[host_name]&domain=[domain.com]&password=[domain_password]&ip=[your_ip] - */ - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - $dparts = explode(".", trim($this->_dnsHost)); - $domain_part_count = ($dparts[count($dparts) - 1] == "uk") ? 3 : 2; - $domain_offset = count($dparts) - $domain_part_count; - $hostname = implode(".", array_slice($dparts, 0, $domain_offset)); - $domain = implode(".", array_slice($dparts, $domain_offset)); - $dnspass = trim($this->_dnsPass); - $server = "https://dynamicdns.park-your-domain.com/update?host={$hostname}&domain={$domain}&password={$dnspass}&ip={$this->_dnsIP}"; - curl_setopt($ch, CURLOPT_URL, $server); - break; - case 'he-net': - case 'he-net-v6': - $server = "https://dyn.dns.he.net/nic/update?"; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_URL, $server . 'hostname=' . $this->_dnsHost . '&password=' . $this->_dnsPass . '&myip=' . $this->_dnsIP); - break; - case 'he-net-tunnelbroker': - $server = "https://ipv4.tunnelbroker.net/nic/update?"; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - curl_setopt($ch, CURLOPT_URL, $server . 'hostname=' . $this->_dnsHost); - break; - case 'digitalocean': - case 'digitalocean-v6': - /* - * dnsHost should be the root domain - * dnsUser should be the record ID - * dnsPass should be the API key - */ - $server = "https://api.digitalocean.com/v2/domains/" . $this->_dnsHost . "/records/" . $this->_dnsUser; - $hostData = array("data" => "{$this->_dnsIP}"); - - /* - * DigitalOcean does not offer the API via IPv6, so we need - * to force sending the request using IPv4 when updating for IPv6 - */ - curl_setopt($ch, CURLOPT_INTERFACE, $this->_dnsRequestIf); - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_HTTPHEADER, array( - "Authorization: Bearer {$this->_dnsPass}", - 'Content-Type: application/json' - )); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($hostData)); - curl_setopt($ch, CURLOPT_URL, $server); - break; - case 'gandi-livedns': - /* - * https://github.com/vizion8-dan - * Tested on OPNsense 20.1.8_1-amd64 - IPv4 (A) - * dnsHost ("Hostname" field in OPNsense) should be the 2nd-level domain ("example.org") - * dnsUser ("Username" field in OPNsense) should be the subdomain / A-record ("myrecord" in 2nd-level domain) - * dnsPass should be the Gandi-API key - */ - $server = "https://dns.api.gandi.net/api/v5/domains/" . $this->_dnsHost . "/records/" . $this->_dnsUser . "/A"; - - $body = '{"rrset_ttl":"' . "300" . '", "rrset_values":["' . $this->_dnsIP . '"]}'; - - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_HTTPHEADER, array( - "X-Api-Key: {$this->_dnsPass}", - 'Content-Type: application/json' - )); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - curl_setopt($ch, CURLOPT_URL, $server); - break; - case 'selfhost': - if (isset($this->_dnsWildcard) && $this->_dnsWildcard != "OFF") { - $this->_dnsWildcard = "ON"; - } - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - $server = "https://carol.selfhost.de/nic/update"; - $port = ""; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - curl_setopt($ch, CURLOPT_URL, $server . $port . '?system=dyndns&hostname=' . $this->_dnsHost . '&myip=' . $this->_dnsIP . '&wildcard=' . $this->_dnsWildcard . '&mx=' . $this->_dnsMX . '&backmx=NO'); - break; - case 'route53': - case 'route53-v6': - /* Setting Variables */ - $hostname = "{$this->_dnsHost}."; - $ZoneID = $this->_dnsZoneID; - $AccessKeyId = $this->_dnsUser; - $SecretAccessKey = $this->_dnsPass; - $NewIP = $this->_dnsIP; - $NewTTL = $this->_dnsTTL; - $RecordType = ($this->_useIPv6) ? "AAAA" : "A"; - - /* Set Amazon AWS Credentials for this record */ - $r53 = new Route53($AccessKeyId, $SecretAccessKey); - - /* Function to find old values of records in Route 53 */ - if (!function_exists('Searchrecords')) { - function SearchRecords($records, $name) - { - if (!is_array($records)) { - return false; - } - $result = array(); - foreach ($records as $record) { - if (strtolower($record['Name']) == strtolower($name)) { - $result [] = $record; - } - } - return ($result) ? $result : false; - } - } - - $records = $r53->listResourceRecordSets("/hostedzone/$ZoneID"); - - /* Get IP for your hostname in Route 53 */ - if (false !== ($a_result = SearchRecords($records['ResourceRecordSets'], "$hostname"))) { - /** - * if hostname for ipv4 and ipv6 is the same, a_result contains more than 1 item - * we need to get the item that corresponds to the record type - */ - $oldTTLResult = null; - $oldIPResult = null; - foreach ($a_result as $resultItem) { - if ($RecordType === $resultItem['Type']) { - $oldTTLResult = $resultItem["TTL"]; - $oldIPResult = $resultItem["ResourceRecords"][0]; - } - } - - $OldTTL = $oldTTLResult; - $OldIP = $oldIPResult; - } else { - $OldIP = ""; - } - - /* Check if we need to update DNS Record */ - if ($OldIP !== $NewIP) { - if (!empty($OldIP)) { - /* Your Hostname already exists, deleting and creating it again */ - $changes = array(); - $changes[] = $r53->prepareChange("DELETE", $hostname, $RecordType, $OldTTL, $OldIP); - $changes[] = $r53->prepareChange("CREATE", $hostname, $RecordType, $NewTTL, $NewIP); - $result = $r53->changeResourceRecordSets("/hostedzone/$ZoneID", $changes); - } else { - /* Your Hostname does not exist yet, creating it */ - $changes = $r53->prepareChange("CREATE", $hostname, $RecordType, $NewTTL, $NewIP); - $result = $r53->changeResourceRecordSets("/hostedzone/$ZoneID", $changes); - } - } - $this->_checkStatus(0, $result); - break; - case 'custom': - case 'custom-v6': - if ($this->_curlIpresolveV4) { - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - } - if ($this->_curlSslVerifypeer) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - } else { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - } - if ($this->_dnsUser != '') { - curl_setopt($ch, CURLOPT_USERPWD, "{$this->_dnsUser}:{$this->_dnsPass}"); - } - $server = str_replace("%IP%", $this->_dnsIP, $this->_dnsUpdateURL); - $server = str_replace("%HOST%", $this->_dnsHost, $server); - curl_setopt($ch, CURLOPT_URL, $server); - break; - case 'cloudflare': - case 'cloudflare-v6': - case 'cloudflare-token': - case 'cloudflare-token-v6': - $baseUrl = 'https://api.cloudflare.com/client/v4'; - $fqdn = str_replace(' ', '', $this->_dnsHost); - $recordType = ($this->_useIPv6) ? 'AAAA' : 'A'; - $ttlData = intval($this->_dnsTTL) < 1 ? 1 : intval($this->_dnsTTL); - $hostData = array( - "content" => "{$this->_dnsIP}", - "type" => $recordType, - "name" => $fqdn, - "ttl" => $ttlData - ); - - // Determine if service is token based or user/password based and define appropriate header - if (strpos($this->_dnsService, 'token') !== false) { - $headerAuth = array( - "Authorization: Bearer {$this->_dnsPass}", - 'Content-Type: application/json' - ); - } else { - $headerAuth = array( - "X-Auth-Email: {$this->_dnsUser}", - "X-Auth-Key: {$this->_dnsPass}", - 'Content-Type: application/json' - ); - } - - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headerAuth); - - // Get all zone info - $zonesUrl = "$baseUrl/zones"; - curl_setopt($ch, CURLOPT_URL, $zonesUrl); - $output = json_decode(curl_exec($ch)); - $zoneId = null; // Set default value - - if (!empty($output->result)) { - // Iterate zone objects, check if $fqdn is equal to or ends with zone name - foreach ($output->result as $key => $zoneObj) { - if (preg_match("/^{$zoneObj->name}$|\.{$zoneObj->name}$/", $fqdn)) { - // Found matching zone - $zoneId = $zoneObj->id; - // Get $hostName from $fqdn, set $domainName - // These are only really used for log messages. - $hostName = preg_replace("/\.?{$zoneObj->name}$/", '', $fqdn); - $domainName = $zoneObj->name; - break; - } - } - } - - if ($zoneId) { // If zone ID was found get host ID - $dnsRecordsUrl = "$zonesUrl/$zoneId/dns_records"; - $getHostId = "$dnsRecordsUrl?name=$fqdn&type=$recordType"; - curl_setopt($ch, CURLOPT_URL, $getHostId); - $output = json_decode(curl_exec($ch)); - $recordCount = !empty($output->result) ? count($output->result) : 0; - if ($recordCount === 0) { - // create record - curl_setopt($ch, CURLOPT_URL, $dnsRecordsUrl); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); - } elseif ($recordCount >= 1) { - // update record - $recordId = $output->result[0]->id; - $hostData["proxied"] = $output->result[0]->proxied; - curl_setopt($ch, CURLOPT_URL, "$dnsRecordsUrl/$recordId"); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - } - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($hostData)); - if ($recordCount > 1) { - log_error("Dynamic DNS ($fqdn): Warning: multiple records for $hostName found"); - } - } - break; - case 'hetzner': - case 'hetzner-v6': - $baseUrl = 'https://dns.hetzner.com/api/v1'; - $fqdn = str_replace(' ', '', $this->_dnsHost); - $recordType = ($this->_useIPv6) ? 'AAAA' : 'A'; - $ttlData = intval($this->_dnsTTL) < 1 ? 120 : intval($this->_dnsTTL); - $hostData = [ - "value" => "{$this->_dnsIP}", - "type" => $recordType, - "name" => "", - "ttl" => $ttlData, - "zone_id" => "" - ]; - - $headerAuth = [ - "Auth-API-Token: {$this->_dnsPass}", - 'Content-Type: application/json' - ]; - - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headerAuth); - - // Get all zone info - $zonesUrl = "$baseUrl/zones"; - curl_setopt($ch, CURLOPT_URL, $zonesUrl); - $output = json_decode(curl_exec($ch)); - $zoneId = null; // Set default value - - if (!empty($output->zones)) { - // Iterate zone objects, check if $fqdn is equal to or ends with zone name - foreach ($output->zones as $key => $zoneObj) { - if (preg_match("/^{$zoneObj->name}$|\.{$zoneObj->name}$/", $fqdn)) { - // Found matching zone - $zoneId = $zoneObj->id; - // Get $hostName from $fqdn, set $domainName - // These are only really used for log messages. - $hostName = preg_replace("/\.?{$zoneObj->name}$/", '', $fqdn); - $domainName = $zoneObj->name; - - break; - } - } - } - - if ($zoneId) { // If zone ID was found get host ID - $dnsRecordsUrl = "$baseUrl/records?zone_id=$zoneId"; - curl_setopt($ch, CURLOPT_URL, $dnsRecordsUrl); - $output = json_decode(curl_exec($ch)); - $recordId = null; - - if (!empty($output->records)) { - // Iterate zone objects, check if $hostName exist of the same type - foreach ($output->records as $key => $recordObj) { - if (preg_match("/^{$recordObj->name}$/", $hostName)) { - if ($recordObj->type == $recordType) { - // Found matching host - $recordId = $recordObj->id; - break; - } - } - } - } - - if ($recordId) { // If record ID was found, update record - $setRecordUrl = "$baseUrl/records/$recordId"; - curl_setopt($ch, CURLOPT_URL, $setRecordUrl); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - } else { - $setRecordUrl = "$baseUrl/records"; - curl_setopt($ch, CURLOPT_URL, $setRecordUrl); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); - } - - $hostData["zone_id"] = $zoneId; - $hostData["name"] = $hostName; - - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($hostData)); - } - break; - case 'eurodns': - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - $server = "https://update.eurodyndns.org/update/"; - $port = ""; - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - curl_setopt($ch, CURLOPT_URL, $server . $port . '?hostname=' . $this->_dnsHost . '&myip=' . $this->_dnsIP); - break; - case 'gratisdns': - $server = "https://admin.gratisdns.com/ddns.php"; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - if (substr_count($this->_dnsHost, ".") < 2) { - $domain = $this->_dnsHost; - $hostname = $this->_dnsHost; - } else { - list($hostname, $domain) = explode(".", $this->_dnsHost, 2); - } - curl_setopt($ch, CURLOPT_URL, $server . '?u=' . urlencode($this->_dnsUser) . '&p=' . $this->_dnsPass . '&h=' . $this->_dnsHost . '&d=' . $domain . '&i=' . $this->_dnsIP); - break; - case 'ovh-dynhost': - if (isset($this->_dnsWildcard) && $this->_dnsWildcard != "OFF") { - $this->_dnsWildcard = "ON"; - } - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - $server = "https://www.ovh.com/nic/update"; - $port = ""; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - curl_setopt($ch, CURLOPT_URL, $server . $port . '?system=dyndns&hostname=' . $this->_dnsHost . '&myip=' . $this->_dnsIP . '&wildcard=' . $this->_dnsWildcard . '&mx=' . $this->_dnsMX . '&backmx=NO'); - break; - case 'citynetwork': - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - $server = 'https://dyndns.citynetwork.se/nic/update'; - $port = ""; - if ($this->_dnsServer) { - $server = $this->_dnsServer; - } - if ($this->_dnsPort) { - $port = ":" . $this->_dnsPort; - } - curl_setopt($ch, CURLOPT_URL, $server . $port . '?hostname=' . $this->_dnsHost . '&myip=' . $this->_dnsIP); - break; - case 'duckdns': - $server = "https://www.duckdns.org/update"; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_URL, $server . '?domains=' . str_replace('.duckdns.org', '', $this->_dnsHost) . '&token=' . urlencode($this->_dnsUser)); - break; - case 'dynv6': - $server = "https://ipv4.dynv6.com/api/update"; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_INTERFACE, $this->_dnsRequestIf); - curl_setopt($ch, CURLOPT_DNS_LOCAL_IP4, $this->_dnsIP); - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - curl_setopt($ch, CURLOPT_URL, $server . '?hostname=' . $this->_dnsHost . '&ipv4=' . $this->_dnsIP . '&token=' . $this->_dnsUser); - break; - case 'dynv6-v6': - $server = "https://ipv6.dynv6.com/api/update"; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_INTERFACE, $this->_dnsRequestIf); - curl_setopt($ch, CURLOPT_DNS_LOCAL_IP6, $this->_dnsIP); - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6); - curl_setopt($ch, CURLOPT_URL, $server . '?hostname=' . $this->_dnsHost . '&ipv6=' . $this->_dnsIP . '&token=' . $this->_dnsUser); - break; - case 'googledomains': - $server = "https://domains.google.com/nic/update"; - $post_data['hostname'] = $this->_dnsHost; - $post_data['myip'] = $this->_dnsIP; - $post_data['offline'] = 'no'; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); - curl_setopt($ch, CURLOPT_URL, $server); - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - break; - case 'strato': - case 'strato-v6': - $server = "https://dyndns.strato.com/nic/update?hostname={$this->_dnsHost}&myip={$this->_dnsIP}"; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - curl_setopt($ch, CURLOPT_URL, $server); - if ($this->_curlIpresolveV4) { - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - } - break; - case '1984-hosting': - $server = "https://api.1984.is/1.0/freedns/?apikey={$this->_dnsPass}&domain={$this->_dnsHost}&ip={$this->_dnsIP}"; - curl_setopt($ch, CURLOPT_URL, $server); - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - break; - case '3322': - $server = "http://members.3322.net/dyndns/update?hostname={$this->_dnsHost}&myip={$this->_dnsIP}"; - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - curl_setopt($ch, CURLOPT_URL, $server); - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - break; - case 'oray': - $server = "http://ddns.oray.com/ph/update?hostname={$this->_dnsHost}&myip={$this->_dnsIP}"; - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - curl_setopt($ch, CURLOPT_URL, $server); - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - break; - case 'regfish': - $server = "https://dyndns.regfish.de/?fqdn={$this->_dnsHost}&ipv4={$this->_dnsIP}&forcehost=1&token=" . urlencode($this->_dnsUser); - curl_setopt($ch, CURLOPT_URL, $server); - break; - case 'regfish-v6': - $server = "https://dyndns6.regfish.de/?fqdn={$this->_dnsHost}&ipv6={$this->_dnsIP}&forcehost=1&token=" . urlencode($this->_dnsUser); - curl_setopt($ch, CURLOPT_URL, $server); - break; - case 'linode': - case 'linode-v6': - $baseUrl = "https://api.linode.com/v4"; - $fqdn = trim($this->_dnsHost); - $recordType = ($this->_useIPv6) ? 'AAAA' : 'A'; - - if ($this->_dnsWildcard == 'ON') { - $fqdn = "*.$fqdn"; - } - - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_HTTPHEADER, array( - 'Accept: application/json', - 'Authorization: Bearer ' . $this->_dnsPass, - 'Content-Type: application/json' - )); - - $domainsUrl = "$baseUrl/domains"; - curl_setopt($ch, CURLOPT_URL, $domainsUrl); - $output = json_decode(curl_exec($ch)); - $domainId = null; - - if (!empty($output->data)) { - // Find matching domain and split the hostname part from it - foreach ($output->data as $key => $domainObj) { - if (preg_match("/^{$domainObj->domain}$|\.{$domainObj->domain}$/", $fqdn)) { - $domainId = $domainObj->id; - $hostName = preg_replace("/\.?{$domainObj->domain}$/", '', $fqdn); - $domainName = $domainObj->domain; - break; - } - } - } - - if ($domainId) { - if ($this->_dnsVerboseLog) { - log_error("Dynamic DNS ($fqdn): Found domain name: $domainName, ID: $domainId"); - } - - $dnsRecordsUrl = "$domainsUrl/$domainId/records"; - curl_setopt($ch, CURLOPT_URL, $dnsRecordsUrl); - $output = json_decode(curl_exec($ch)); - $recordId = null; - - if (!empty($output->data)) { - // Find matching record - foreach ($output->data as $key => $recordObj) { - if ($recordObj->type == $recordType && $recordObj->name == $hostName) { - $recordId = $recordObj->id; - break; - } - } - } - - $hostData = [ 'target' => "{$this->_dnsIP}" ]; - - if ($recordId) { - // Update record - if ($this->_dnsVerboseLog) { - log_error("Dynamic DNS ($fqdn): Updating existing record ID: $recordId"); - } - - curl_setopt($ch, CURLOPT_URL, "$dnsRecordsUrl/$recordId"); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - } else { - // Create record - if ($this->_dnsVerboseLog) { - log_error("Dynamic DNS ($fqdn): Creating new record"); - } - - $hostData['type'] = $recordType; - $hostData['name'] = $hostName; - // Linode will round up to the nearest valid TTL - $hostData['ttl_sec'] = 0; - - curl_setopt($ch, CURLOPT_URL, $dnsRecordsUrl); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); - } - - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($hostData)); - } else { - log_error("Dynamic DNS($fqdn): No zone found for domain"); - } - break; - case 'azurev6': - case 'azure': - $hostname = "{$this->_dnsHost}"; - $resourceid = trim($this->_dnsResourceID); - $app_id = $this->_dnsUser; - $client_secret = $this->_dnsPass; - $newip = $this->_dnsIP; - $newttl = $this->_dnsTTL; - // ensure resourceid starts with / and has no trailing / - $resourceid = '/' . trim($resourceid, '/'); - // extract subscription id from resource id - preg_match('/\\/subscriptions\\/(?[^\\/]*)/', $resourceid, $result); - $subscriptionid = isset($result['sid']) ? $result['sid'] : ''; - if (isset($result['sid'])) { - $subscriptionid = $result['sid']; - } else { - log_error("Azure subscription id not found in resource id ({$resourceid})"); - return false; - } - // find tenant id from subscription id - curl_setopt($ch, CURLOPT_URL, "https://management.azure.com/subscriptions/" . $subscriptionid . "?api-version=2016-09-01"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_HEADER, 1); - curl_setopt($ch, CURLOPT_NOBODY, 1); - $output = curl_exec($ch); - $pattern = '/Bearer authorization_uri="https:\\/\\/login.windows.net\\/(?[^"]*)/i'; - preg_match($pattern, $output, $result); - if (isset($result['tid'])) { - $tenantid = $result['tid']; - } else { - log_error("Tenant ID not found"); - return false; - } - // get an bearer token - curl_setopt($ch, CURLOPT_URL, "https://login.microsoftonline.com/" . $tenantid . "/oauth2/token"); - curl_setopt($ch, CURLOPT_POST, 1); - $body = "resource=" . urlencode("https://management.core.windows.net/") . "&grant_type=client_credentials&client_id=" . $app_id . "&client_secret=" . urlencode($client_secret); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $server_output = curl_exec($ch); - $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - preg_match("/\"access_token\":\"(?[^\"]*)\"/", $server_output, $result); - if (isset($result['tok'])) { - $bearertoken = $result['tok']; - } else { - log_error("no valid bearer token"); - return false; - } - // Update the DNS record - if ($this->_useIPv6) { - $url = "https://management.azure.com" . $resourceid . "/AAAA/" . $hostname . "?api-version=2017-09-01"; - $body = '{"properties":{"TTL":"' . $newttl . '", "AAAARecords":[{"ipv6Address":"' . $newip . '"}]}}'; - } else { - $url = "https://management.azure.com" . $resourceid . "/A/" . $hostname . "?api-version=2017-09-01"; - $body = '{"properties":{"TTL":"' . $newttl . '", "ARecords":[{"ipv4Address":"' . $newip . '"}]}}'; - } - $request_headers = array(); - $request_headers[] = 'Accept: application/json'; - $request_headers[] = 'Authorization: Bearer ' . $bearertoken; - $request_headers[] = 'Content-Type: application/json'; - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_USERAGENT, $this->_UserAgent); - curl_setopt($ch, CURLOPT_HTTPHEADER, $request_headers); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HEADER, 1); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - break; - case 'all-inkl': - case 'all-inkl-v6': - $server = "https://dyndns.kasserver.com/"; - $url = $server . '?myip=' . $this->_dnsIP; - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsUser . ':' . $this->_dnsPass); - // fix: All-Inkl dyndns.kasserver.com only supports v4 - $ipv4 = get_interface_ip($this->_dnsRequestIf); - if (!is_ipaddr($ipv4)) { - log_error("Dynamic DNS ({$this->_dnsHost}): (Error) Need a IPv4 address on $this->_dnsRequestIf!"); - return false; - } - curl_setopt($ch, CURLOPT_INTERFACE, $ipv4); - // fix end - break; - case 'godaddy': - case 'godaddy-v6': - /* Read https://developer.godaddy.com/ for API documentation */ - $baseApiUrl = 'https://api.godaddy.com/v1/domains/'; - $recordType = $this->_useIPv6 ? "AAAA" : "A"; - $splitHost = explode('.', trim($this->_dnsHost)); - $dnsDomain = '*'; - if ($this->_dnsWildcard != 'ON') { - $dnsDomain = array_shift($splitHost); - } - $dnsHost = implode('.', $splitHost); - - $url = $baseApiUrl . $dnsHost . '/records/' . $recordType . '/' . $dnsDomain; - - /* body can contain multiple options (data, port, priority, service, ttl, weight) */ - $data = array(); - $data[] = array('data' => $this->_dnsIP); - if ($this->_dnsTTL) { - $data[0]['ttl'] = $this->_dnsTTL; - } else { - // minimum allowed by GoDaddy - $data[0]['ttl'] = 600; - } - $jsonBody = json_encode($data); - if ($this->_dnsVerboseLog) { - log_error("Dynamic DNS: calling $url with body: $jsonBody"); - } - - /* PUT JSON /v1/domains/{domain}/records/{type}/{name} */ - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); - curl_setopt($ch, CURLOPT_HTTPHEADER, array( - 'Accept: application/json', - 'Content-Type: application/json', - 'Authorization: sso-key ' . $this->_dnsUser . ':' . $this->_dnsPass - )); - curl_setopt($ch, CURLOPT_URL, $url); - - curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonBody); - break; - case 'desec': - /* - * https://desec.readthedocs.io/en/latest/dyndns/update-api.html - * dnsHost should be the domain - * dnsPass should be the token, NOT the token id - * IPv6 is empty so deSEC API will not set this to the IPv6 of the sending interface if the connection is made via IPv6 - */ - $server = "https://update.dedyn.io/"; - $url = '?hostname=' . $this->_dnsHost . '&myipv4=' . $this->_dnsIP . '&myipv6=""'; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsHost . ':' . $this->_dnsPass); - curl_setopt($ch, CURLOPT_URL, $server . $url); - break; - case 'desec-v4-v6': - /* - * https://desec.readthedocs.io/en/latest/dyndns/update-api.html - * dnsHost should be the domain - * dnsPass should be the token, NOT the 36-character token id (https://forum.netgate.com/post/930114) - * IPv4 is determined by deSEC API via the sending interface - */ - - // temporarily disable useIPv6 to get IPv4 Address - $this->_useIPv6 = false; - $ipv4 = $this->_checkIP(); - $this->_useIPv6 = true; - - $server = "https://update.dedyn.io/"; - $url = '?hostname=' . $this->_dnsHost . '&myipv4=' . $ipv4 . '&myipv6=' . $this->_dnsIP; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsHost . ':' . $this->_dnsPass); - curl_setopt($ch, CURLOPT_URL, $server . $url); - break; - case 'desec-v6': - /* - * https://desec.readthedocs.io/en/latest/dyndns/update-api.html - * dnsHost should be the domain - * dnsPass should be the token, NOT the 36-character token id (https://forum.netgate.com/post/930114) - */ - $server = "https://update6.dedyn.io/"; - $url = '?hostname=' . $this->_dnsHost . '&myipv6=' . $this->_dnsIP; - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6); - curl_setopt($ch, CURLOPT_USERPWD, $this->_dnsHost . ':' . $this->_dnsPass); - curl_setopt($ch, CURLOPT_URL, $server . $url); - break; - default: - break; - } - if ($this->_dnsService != 'ods' and $this->_dnsService != 'route53' and $this->_dnsService != 'route53-v6') { - $data = curl_exec($ch); - $this->_checkStatus($ch, $data); - @curl_close($ch); - } - } - - /* - * Private Function (added 12 July 2005) [beta] - * Retrieve Update Status - */ - function _checkStatus($ch, $data) - { - if ($this->_dnsVerboseLog) { - log_error("Dynamic DNS ({$this->_dnsHost}): _checkStatus() starting."); - log_error("Dynamic DNS ({$this->_dnsHost}): Current Service: {$this->_dnsService}"); - } - $successful_update = false; - if ($this->_dnsService != 'ods' and $this->_dnsService != 'route53' and $this->_dnsService != 'route53-v6' && @curl_error($ch)) { - $status = "Curl error occurred: " . curl_error($ch); - log_error($status); - $this->status = $status; - return; - } - switch ($this->_dnsService) { - case 'dhs': - break; - case 'noip': - case 'noip-free': - $noIpPrc = explode(' ', $data); - $code = $noIpPrc[0]; - $ip = isset($noIpPrc[1]) ? $noIpPrc[1] : 'n/a'; - switch ($code) { - case 'good': - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) DNS hostname update successful."; - $successful_update = true; - break; - case 'nochg': - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) IP address is current, no update performed."; - $successful_update = true; - break; - case 'nohost': - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Hostname supplied does not exist."; - break; - case 'badauth': - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Invalid Username or Password."; - break; - case 'badagent': - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Client disabled. Client should exit and not perform any more updates without user intervention."; - break; - case '!donate': - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Requested update feature only available to Enhanced subscribers."; - break; - case '911': - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) No-IP servers currently experiencing outages. Retry no sooner than 30 minutes."; - break; - case 'abuse': - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Account disabled due to violation of No-IP terms of service."; - break; - default: - $status = "Dynamic DNS ({$this->_dnsHost}): (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - break; - } - break; - case 'easydns': - if (preg_match('/NOACCESS/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Authentication Failed: Username and/or Password was Incorrect."; - } elseif (preg_match('/NOSERVICE/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) No Service: Dynamic DNS Service has been disabled for this domain."; - } elseif (preg_match('/ILLEGAL INPUT/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Illegal Input: Self-Explanatory"; - } elseif (preg_match('/TOOSOON/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Too Soon: Not Enough Time Has Elapsed Since Last Update"; - } elseif (preg_match('/NOERROR/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) IP Updated Successfully!"; - $successful_update = true; - } else { - $status = "Dynamic DNS ({$this->_dnsHost}): (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'hn': - /* FIXME: add checks */ - break; - case 'zoneedit': - if (preg_match('/799/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error 799) Update Failed!"; - } elseif (preg_match('/700/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error 700) Update Failed!"; - } elseif (preg_match('/200/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) IP Address Updated Successfully!"; - $successful_update = true; - } elseif (preg_match('/201/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) IP Address Updated Successfully!"; - $successful_update = true; - } else { - $status = "Dynamic DNS ({$this->_dnsHost}): (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'dyns': - if (preg_match("/400/i", $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Bad Request - The URL was malformed. Required parameters were not provided."; - } elseif (preg_match('/402/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Update Too Soon - You have tried updating to quickly since last change."; - } elseif (preg_match('/403/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Database Error - There was a server-sided database error."; - } elseif (preg_match('/405/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Hostname Error - The hostname (" . $this->_dnsHost . ") doesn't belong to you."; - } elseif (preg_match('/200/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) IP Address Updated Successfully!"; - $successful_update = true; - } else { - $status = "Dynamic DNS ({$this->_dnsHost}): (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'ods': - if (preg_match("/299/i", $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) IP Address Updated Successfully!"; - $successful_update = true; - } else { - $status = "Dynamic DNS ({$this->_dnsHost}): (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'freedns': - if (preg_match("/No IP change detected.*skipping update/i", $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) No Change In IP Address"; - $successful_update = true; - } elseif (preg_match("/Updated/i", $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) IP Address Changed Successfully!"; - $successful_update = true; - } else { - $status = "Dynamic DNS ({$this->_dnsHost}): (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'dnsexit': - if (preg_match("/IP not changed/i", $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) No Change In IP Address"; - $successful_update = true; - } elseif (preg_match("/Success/i", $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) IP Address Changed Successfully!"; - $successful_update = true; - } else { - $status = "Dynamic DNS ({$this->_dnsHost}): (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'staticcling': - if (preg_match("/invalid ip/i", $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Bad Request - The IP provided was invalid."; - } elseif (preg_match('/required info missing/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Bad Request - Required parameters were not provided."; - } elseif (preg_match('/invalid characters/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Bad Request - Illegal characters in either the username or the password."; - } elseif (preg_match('/bad password/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Invalid password."; - } elseif (preg_match('/account locked/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) This account has been administratively locked."; - } elseif (preg_match('/update too frequent/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Updating too frequently."; - } elseif (preg_match('/DB error/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Error) Server side error."; - } elseif (preg_match('/success/i', $data)) { - $status = "Dynamic DNS ({$this->_dnsHost}): (Success) IP Address Updated Successfully!"; - $successful_update = true; - } else { - $status = "Dynamic DNS ({$this->_dnsHost}): (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'namecheap': - $tmp = str_replace("^M", "", $data); - $ncresponse = simplexml_load_string($tmp); - if (preg_match("/internal server error/i", $data)) { - $status = "Dynamic DNS: (Error) Server side error."; - } elseif (preg_match("/request is badly formed/i", $data)) { - $status = "Dynamic DNS: (Error) Badly Formed Request (check your settings)."; - } elseif ((string)$ncresponse->ErrCount === "0") { - $status = "Dynamic DNS: (Success) IP Address Updated Successfully!"; - $successful_update = true; - } elseif (isset($ncresponse->ErrCount) && is_numeric((string)$ncresponse->ErrCount) && (string)$ncresponse->ErrCount > 0) { - $status = "Dynamic DNS: (Error) "; - if (isset($ncresponse->errors)) { - foreach ($ncresponse->errors->children() as $err) { - $status .= (string)$err . " "; - } - } - $successful_update = true; - } else { - $status = "Dynamic DNS: (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'route53': - case 'route53-v6': - $successful_update = true; - break; - case 'custom': - case 'custom-v6': - $successful_update = false; - if ($this->_dnsResultMatch == "") { - $successful_update = true; - } else { - $this->_dnsResultMatch = str_replace("%IP%", $this->_dnsIP, $this->_dnsResultMatch); - $matches = preg_split("/(?_dnsResultMatch); - foreach ($matches as $match) { - $match = str_replace("\\|", "|", $match); - if (strcmp($match, trim($data, "\t\n\r")) == 0) { - $successful_update = true; - } - } - unset($matches); - } - if ($successful_update == true) { - $status = "Dynamic DNS: (Success) IP Address Updated Successfully!"; - } else { - $status = "Dynamic DNS: (Error) Result did not match."; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'cloudflare': - case 'cloudflare-v6': - case 'cloudflare-token': - case 'cloudflare-token-v6': - $output = json_decode($data); - if ($output->result->content === $this->_dnsIP) { - $status = "Dynamic DNS: (Success) {$this->_dnsHost} updated to {$this->_dnsIP}"; - $successful_update = true; - } elseif ($output->errors[0]->code === 9103) { - $status = "Dynamic DNS ({$this->_dnsHost}): ERROR - Invalid Credentials! Don't forget to use API Key for password field with Cloudflare."; - } elseif (($output->success) && (!$output->result[0]->id)) { - $status = "Dynamic DNS ({$this->_dnsHost}): ERROR - Zone ID was not found."; - } else { - $status = "Dynamic DNS ({$this->_dnsHost}): UNKNOWN ERROR - {$output->errors[0]->message}"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'hetzner': - case 'hetzner-v6': - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $output = json_decode($data); - if ($output->record->value === $this->_dnsIP) { - $status = "Dynamic DNS: (Success) {$this->_dnsHost} updated to {$this->_dnsIP}"; - $successful_update = true; - } elseif ($http_code == 401) { - $status = 'Dynamic DNS: (Error) Bad authentication attempt because of a wrong API Key.'; - } elseif ($http_code == 403) { - $status = 'Dynamic DNS: (Error) Access to the resource is denied. Mainly due to a lack of permissions to access it!'; - } else { - $status = 'Dynamic DNS: (Error) "Unknown Response"'; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'digitalocean': - case 'digitalocean-v6': - $output = json_decode($data); - if ($output->domain_record->data === $this->_dnsIP) { - $status = "Dynamic DNS: (Success) Record ID {$this->_dnsUser} updated to {$this->_dnsIP}"; - $successful_update = true; - } else { - $status = "Dynamic DNS Record ID ({$this->_dnsUser}): UNKNOWN ERROR"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'gandi-livedns': - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($http_code == 401) { - $status = 'Dynamic DNS: (Error) Bad authentication attempt because of a wrong API Key.'; - } elseif ($http_code == 403) { - $status = 'Dynamic DNS: (Error) Access to the resource is denied. Mainly due to a lack of permissions to access it!'; - } elseif ($http_code == 201) { - $status = 'Dynamic DNS: (Success) Record was created!'; - $successful_update = true; - } elseif ($http_code == 200) { - $status = 'Dynamic DNS: (Success) Same record already exists. Nothing was changed!'; - $successful_update = true; - } else { - $status = 'Dynamic DNS: (Error) "Unknown Response"'; - log_error("Dynamic DNS: HTTP Status: {$http_code} PAYLOAD: {$data}"); - $this->_debug("Unknown HTTP status: " . $http_code); - } - break; - case 'gratisdns': - if (preg_match('/Forkerte værdier/i', $data)) { - $status = "Dynamic DNS: (Error) Wrong values - Update could not be completed."; - } elseif (preg_match('/Bruger login: Bruger eksistere ikke/i', $data)) { - $status = "Dynamic DNS: (Error) Unknown username - User does not exist."; - } elseif (preg_match('/Bruger login: 1Fejl i kodeord/i', $data)) { - $status = "Dynamic DNS: (Error) Wrong password - Remember password is case sensitive."; - } elseif (preg_match('/Domæne kan IKKE administreres af bruger/i', $data)) { - $status = "Dynamic DNS: (Error) User unable to administer the selected domain."; - } elseif (preg_match('/OK/i', $data)) { - $status = "Dynamic DNS: (Success) IP Address Updated Successfully!"; - $successful_update = true; - } else { - $status = "Dynamic DNS: (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'duckdns': - if (preg_match('/OK/i', $data)) { - $status = "Dynamic DNS: (Success) IP Address Updated Successfully!"; - $successful_update = true; - } else { - $status = "Dynamic DNS: (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'dynv6': - case 'dynv6-v6': - /* API-Documentation: https://dynv6.com/docs/apis */ - if (preg_match('/addresses updated/i', $data)) { - $status = "Dynamic DNS: (Success) IP Address Updated Successfully!"; - $successful_update = true; - } elseif (preg_match('/addresses unchanged/i', $data)) { - $status = "Dynamic DNS: (Success) IP Address Unchanged!"; - $successful_update = true; - } else { - $status = "Dynamic DNS: (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case '1984-hosting': - $output = json_decode($data); - if ($output === null) { - $status = "Dynamic DNS ({$this->_dnsHost}): ERROR - empty response."; - } elseif ($output->ok === true) { - $status = "Dynamic DNS: (Success) {$this->_dnsHost} updated to {$this->_dnsIP}"; - $successful_update = true; - } elseif ($output->error) { - $status = "Dynamic DNS ({$this->_dnsHost}): ERROR - Invalid Credentials! Don't forget to use API Key for password field with 1984 Hosting Company."; - } elseif ($output->lookup) { - $status = "Dynamic DNS ({$this->_dnsHost}): ERROR - Zone ID was not found."; - } else { - $status = "Dynamic DNS ({$this->_dnsHost}): UNKNOWN ERROR - {$output->errors[0]->message}"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case '3322': - case 'citynetwork': - case 'dnsomatic': - case 'dyndns': - case 'dyndns-custom': - case 'dyndns-static': - case 'eurodns': - case 'googledomains': - case 'he-net': - case 'he-net-tunnelbroker': - case 'he-net-v6': - case 'loopia': - case 'oray': - case 'ovh-dynhost': - case 'selfhost': - case 'strato': - case 'strato-v6': - if (preg_match('/notfqdn/i', $data)) { - $status = "Dynamic DNS: (Error) Not a FQDN"; - } elseif (preg_match('/nochg/i', $data)) { - $status = "Dynamic DNS: (Success) No change in IP address"; - $successful_update = true; - } elseif (preg_match('/good/i', $data)) { - $status = "Dynamic DNS: (Success) IP address updated successfully ({$this->_dnsIP})"; - $successful_update = true; - } elseif (preg_match('/badauth/i', $data)) { - $status = "Dynamic DNS: (Error) Authentication failed"; - } elseif (preg_match("/badip/i", $data)) { - $status = "Dynamic DNS: (Error) IP address provided is invalid"; - } elseif (preg_match('/nohost/i', $data)) { - $status = "Dynamic DNS: (Error) Hostname does not exist or does not have dynamic DNS enabled"; - } elseif (preg_match('/numhost/i', $data)) { - $status = "Dynamic DNS: (Error) You may update up to 20 hosts only"; - } elseif (preg_match('/dnserr/i', $data)) { - $status = "Dynamic DNS: (Error) DNS error, stop updating for 30 minutes."; - } elseif (preg_match('/badagent/i', $data)) { - $status = "Dynamic DNS: (Error) Bad request"; - } elseif (preg_match('/abuse/i', $data)) { - $status = "Dynamic DNS: (Error) Access has been blocked for abuse"; - } elseif (preg_match('/911/i', $data)) { - $status = "Dynamic DNS: (Error) Server-side error or maintenance"; - } elseif (preg_match('/yours/i', $data)) { - $status = "Dynamic DNS: (Error) Specified hostname does not exist under this username"; - } else { - $status = "Dynamic DNS: (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'regfish': - case 'regfish-v6': - if (preg_match('/\|100\|/', $data)) { - $status = 'Dynamic DNS: (Success) Update successful'; - $successful_update = true; - } elseif (preg_match('/\|101\|/', $data)) { - $status = 'Dynamic DNS: (Success) Still up-to-date'; - $successful_update = true; - } elseif (preg_match('/\|401\|/', $data)) { - $status = 'Dynamic DNS: (Error) Standard authentication failed'; - } elseif (preg_match('/\|402\|/', $data)) { - $status = 'Dynamic DNS: (Error) Authentication failed'; - } elseif (preg_match('/\|406\|/', $data)) { - $status = 'Dynamic DNS: (Error) Invalid resource record'; - } elseif (preg_match('/\|407\|/', $data)) { - $status = 'Dynamic DNS: (Error) Invalid TTL range'; - } elseif (preg_match('/\|408\|/', $data)) { - $status = 'Dynamic DNS: (Error) Invalid IPv4'; - } elseif (preg_match('/\|409\|/', $data)) { - $status = 'Dynamic DNS: (Error) Invalid IPv6'; - } elseif (preg_match('/\|410\|/', $data)) { - $status = 'Dynamic DNS: (Error) Unknown authentication type'; - } elseif (preg_match('/\|412\|/', $data)) { - $status = 'Dynamic DNS: (Error) Domain format is wrong, missing trailing dot?'; - } elseif (preg_match('/\|414\|/', $data)) { - $status = 'Dynamic DNS: (Error) Unexpected error'; - } elseif (preg_match('/\|415\|/', $data)) { - $status = 'Dynamic DNS: (Error) Cannot update load balancer'; - } else { - $status = "Dynamic DNS: (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - case 'linode': - case 'linode-v6': - $fqdn = trim($this->_dnsHost); - if ($this->_dnsWildcard == 'ON') { - $fqdn = "*.$fqdn"; - } - - $output = json_decode($data); - if ($output->target === $this->_dnsIP) { - $status = "Dynamic DNS: (Success) $fqdn updated to {$this->_dnsIP}"; - $successful_update = true; - } elseif (!empty($output->errors)) { - $status = "Dynamic DNS ($fqdn): ERROR - Reason: {$output->errors[0]->reason}"; - } else { - $status = "Dynamic DNS ($fqdn): UNKNOWN ERROR"; - log_error("Dynamic DNS ($fqdn): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'azure': - case 'azurev6': - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($http_code == 401) { - $status = 'Dynamic DNS: (Error) User Authorization Failed'; - } elseif ($http_code == 201) { - $status = 'Dynamic DNS: (Success) IP Address Changed Successfully!'; - $successful_update = true; - } elseif ($http_code == 200) { - $status = 'Dynamic DNS: (Success) IP Address Changed Successfully!'; - $successful_update = true; - } else { - $status = 'Dynamic DNS: (Error) "Unknown Response"'; - log_error("Dynamic DNS: HTTP status: {$http_code} PAYLOAD: {$data}"); - $this->_debug("Unknown HTTP status: " . $http_code); - } - break; - case 'all-inkl': - case 'all-inkl-v6': - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if (preg_match('/good\s' . $this->_dnsIP . '/i', $data)) { - $status = "Dynamic DNS: (Success) IP Update Successfully!"; - $successful_update = true; - } elseif ($http_code == 401) { - $status = "Dynamic DNS: (Error) Authentication failed!"; - } elseif (preg_match('/bad\s\(dyndns_target_ip_syntax_incorrect\)/i', $data)) { - $status = "Dynamic DNS: (Error) IP Syntax incorrect ($this->_dnsIP)!"; - } else { - $status = "Dynamic DNS ({$this->_dnsHost}): (Unknown Response)"; - log_error("Dynamic DNS ({$this->_dnsHost}): PAYLOAD: {$data}"); - $this->_debug("Unknown Response: " . $data); - } - break; - case 'godaddy': - case 'godaddy-v6': - /* See https://developer.godaddy.com/ for API documentation, not all codes are handled. */ - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $successful_update = false; - if ($http_code == 200) { - $status = 'Dynamic DNS: (Success) IP Address Updated Successfully!'; - $successful_update = true; - } elseif ($http_code == 401) { - $status = 'Dynamic DNS: (Error) Authentication info not sent or invalid'; - } elseif ($http_code == 404) { - $status = 'Dynamic DNS: (Error) Resource not found'; - } else { - $status = "Dynamic DNS: (Error) Repsonse not handled check the following: {$data}"; - log_error("Dynamic DNS: HTTP status: {$http_code} PAYLOAD: {$data}"); - $this->_debug("Unknown HTTP status: " . $http_code); - } - break; - case 'desec': - case 'desec-v4-v6': - case 'desec-v6': - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - /* - * HTTP Code 404 should not be possible due to dnsUser == dnsHost, a wrong hostname should cause HTTP 401 Unauthorized. - */ - if ($http_code == 401) { - $status = 'Dynamic DNS: (Error) Bad authentication attempt because of a wrong Password.'; - } elseif ($http_code == 403) { - $status = 'Dynamic DNS: (Error) Access to the resource is denied. The selected hostname is not eligible for dynamic updates.'; - } elseif ($http_code == 429) { - $status = 'Dynamic DNS: (Error) Rate limit reached. Please don\'t try more than one request per minute.'; - } elseif ($http_code == 200 && preg_match('/good/i', $data)) { - $status = 'Dynamic DNS: (Success) IP Address Updated Successfully!'; - $successful_update = true; - } else { - $status = "Dynamic DNS: (Error) Repsonse not handled check the following: {$data}"; - log_error("Dynamic DNS: HTTP status: {$http_code} PAYLOAD: {$data}"); - $this->_debug("Unknown HTTP status: " . $http_code); - } - break; - default: - break; - } - - if ($successful_update == true) { - /* Write WAN IP to cache file */ - $wan_ip = $this->_checkIP(); - if ($this->_useIPv6 == false && $wan_ip > 0) { - $currentTime = time(); - log_error("Dynamic DNS: updating cache file {$this->_cacheFile}: {$wan_ip}"); - @file_put_contents($this->_cacheFile, "{$wan_ip}|{$currentTime}"); - } else { - @unlink($this->_cacheFile); - } - if ($this->_useIPv6 == true && $wan_ip > 0) { - $currentTime = time(); - log_error("Dynamic DNS: updating cache file {$this->_cacheFile_v6}: {$wan_ip}"); - @file_put_contents($this->_cacheFile_v6, "{$wan_ip}|{$currentTime}"); - } else { - @unlink($this->_cacheFile_v6); - } - } - $this->status = $status; - log_error($status); - } - - /* - * Private Function (added 12 July 05) [beta] - * Return Error, Set Last Error, and Die. - */ - function _error($errorNumber = '1') - { - switch ($errorNumber) { - case 0: - break; - case 2: - $error = 'Dynamic DNS: (ERROR!) No Dynamic DNS Service provider was selected.'; - break; - case 3: - $error = 'Dynamic DNS: (ERROR!) No Username Provided.'; - break; - case 4: - $error = 'Dynamic DNS: (ERROR!) No Password Provided.'; - break; - case 5: - $error = 'Dynamic DNS: (ERROR!) No Hostname Provided.'; - break; - case 6: - $error = 'Dynamic DNS: (ERROR!) The Dynamic DNS Service provided is not yet supported.'; - break; - case 7: - $error = 'Dynamic DNS: (ERROR!) No Update URL Provided.'; - break; - case 8: - $status = "Route 53: (Error) Invalid ZoneID"; - break; - case 9: - $status = "Route 53: (Error) Invalid TTL"; - break; - case 10: - $error = "Dynamic DNS ({$this->_dnsHost}): No change in my IP address and/or " . $this->_dnsMaxCacheAgeDays . " days has not passed. Not updating dynamic DNS entry."; - break; - default: - $error = "Dynamic DNS: (ERROR!) Unknown Response."; - /* FIXME: $data isn't in scope here */ - /* $this->_debug($data); */ - break; - } - $this->lastError = $error; - log_error($error); - } - - /* - * Private Function (added 12 July 05) [beta] - * - Detect whether or not IP needs to be updated. - * | Written Specifically for pfSense (https://www.pfsense.org) may - * | work with other systems. pfSense base is FreeBSD. - */ - function _detectChange() - { - $currentTime = time(); - - $wan_ip = $this->_checkIP(); - if ($wan_ip == 0) { - log_error("Dynamic DNS ({$this->_dnsHost}): Current WAN IP could not be determined, skipping update process."); - return false; - } - $log_error = "Dynamic DNS ({$this->_dnsHost}): Current WAN IP: {$wan_ip} "; - - if ($this->_useIPv6 == true) { - if (file_exists($this->_cacheFile_v6)) { - $contents = file_get_contents($this->_cacheFile_v6); - list($cacheIP,$cacheTime) = explode('|', $contents); - $this->_debug($cacheIP . '/' . $cacheTime); - $initial = false; - $log_error .= "Cached IPv6: {$cacheIP} "; - } else { - $cacheIP = '::'; - @file_put_contents($this->_cacheFile, "::|{$currentTime}"); - $cacheTime = $currentTime; - $initial = true; - $log_error .= "No Cached IPv6 found."; - } - } else { - if (file_exists($this->_cacheFile)) { - $contents = file_get_contents($this->_cacheFile); - list($cacheIP,$cacheTime) = explode('|', $contents); - $this->_debug($cacheIP . '/' . $cacheTime); - $initial = false; - $log_error .= "Cached IP: {$cacheIP} "; - } else { - $cacheIP = '0.0.0.0'; - @file_put_contents($this->_cacheFile, "0.0.0.0|{$currentTime}"); - $cacheTime = $currentTime; - $initial = true; - $log_error .= "No Cached IP found."; - } - } - if ($this->_dnsVerboseLog) { - log_error($log_error); - } - - // Convert seconds = days * hr/day * min/hr * sec/min - $maxCacheAgeSecs = $this->_dnsMaxCacheAgeDays * 24 * 60 * 60; - - $needs_updating = false; - /* lets determine if the item needs updating */ - if ($cacheIP != $wan_ip) { - $needs_updating = true; - $update_reason = "Dynamic DNS: cacheIP != wan_ip. Updating. "; - $update_reason .= "Cached IP: {$cacheIP} WAN IP: {$wan_ip} "; - } - if (($currentTime - $cacheTime) > $maxCacheAgeSecs) { - $needs_updating = true; - $this->_forceUpdateNeeded = true; - $update_reason = "Dynamic DNS: More than " . $this->_dnsMaxCacheAgeDays . " days. Updating. "; - $update_reason .= "{$currentTime} - {$cacheTime} > {$maxCacheAgeSecs} "; - } - if ($initial == true) { - $needs_updating = true; - $update_reason .= "Initial update. "; - } - - /* finally if we need updating then store the - * new cache value and return true - */ - if ($needs_updating == true) { - if ($this->_dnsVerboseLog) { - log_error("Dynamic DNS ({$this->_dnsHost}): {$update_reason}"); - } - return true; - } - - return false; - } - - /* - * Private Function (added 16 July 05) [beta] - * - Writes debug information to a file. - * - This function is only called when a unknown response - * - status is returned from a dynamic DNS service provider. - */ - function _debug($data) - { - $string = date('m-d-y h:i:s') . ' - (' . $this->_debugID . ') - [' . $this->_dnsService . '] - ' . $data . "\n"; - $file = fopen($this->_debugFile, 'a'); - fwrite($file, $string); - fclose($file); - } - - function _checkIP() - { - $ip_address = get_dyndns_ip($this->_if, $this->_useIPv6 ? 6 : 4); - if (!is_ipaddr($ip_address)) { - if ($this->_dnsVerboseLog) { - log_error("Dynamic DNS ({$this->_dnsHost}): IP address could not be extracted"); - } - - $ip_address = 0; - } else { - if ($this->_dnsVerboseLog) { - log_error("Dynamic DNS ({$this->_dnsHost}): {$ip_address} extracted"); - } - - $this->_dnsIP = $ip_address; - } - - return $ip_address; - } -} diff --git a/dns/dyndns/src/etc/inc/plugins.inc.d/dyndns/r53.inc b/dns/dyndns/src/etc/inc/plugins.inc.d/dyndns/r53.inc deleted file mode 100644 index d7ebc65fbc..0000000000 --- a/dns/dyndns/src/etc/inc/plugins.inc.d/dyndns/r53.inc +++ /dev/null @@ -1,753 +0,0 @@ -__accessKey; } - public function getSecretKey() { return $this->__secretKey; } - public function getHost() { return $this->__host; } - - protected $__verifyHost = 1; - protected $__verifyPeer = 1; - - // verifyHost and verifyPeer determine whether curl verifies ssl certificates. - // It may be necessary to disable these checks on certain systems. - // These only have an effect if SSL is enabled. - public function verifyHost() { return $this->__verifyHost; } - public function enableVerifyHost($enable = true) { $this->__verifyHost = $enable; } - - public function verifyPeer() { return $this->__verifyPeer; } - public function enableVerifyPeer($enable = true) { $this->__verifyPeer = $enable; } - - /** - * Constructor - * - * @param string $accessKey Access key - * @param string $secretKey Secret key - * @return void - */ - public function __construct($accessKey = null, $secretKey = null, $host = 'route53.amazonaws.com') { - if ($accessKey !== null && $secretKey !== null) { - $this->setAuth($accessKey, $secretKey); - } - $this->__host = $host; - } - - /** - * Set AWS access key and secret key - * - * @param string $accessKey Access key - * @param string $secretKey Secret key - * @return void - */ - public function setAuth($accessKey, $secretKey) { - $this->__accessKey = $accessKey; - $this->__secretKey = $secretKey; - } - - /** - * Lists the hosted zones on the account - * - * @param string marker A pagination marker returned by a previous truncated call - * @param int maxItems The maximum number of items per page. The service uses min($maxItems, 100). - * @return A list of hosted zones - */ - public function listHostedZones($marker = null, $maxItems = 100) { - $rest = new Route53Request($this, 'hostedzone', 'GET'); - - if($marker !== null) { - $rest->setParameter('marker', $marker); - } - if($maxItems !== 100) { - $rest->setParameter('maxitems', $maxItems); - } - - $rest = $rest->getResponse(); - if($rest->error === false && $rest->code !== 200) { - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - } - if($rest->error !== false) { - $this->__triggerError('listHostedZones', $rest->error); - return false; - } - - $response = array(); - if (!isset($rest->body)) - { - return $response; - } - - $zones = array(); - foreach($rest->body->HostedZones->HostedZone as $z) - { - $zones[] = $this->parseHostedZone($z); - } - $response['HostedZone'] = $zones; - - if(isset($rest->body->MaxItems)) { - $response['MaxItems'] = (string)$rest->body->MaxItems; - } - - if(isset($rest->body->IsTruncated)) { - $response['IsTruncated'] = (string)$rest->body->IsTruncated; - if($response['IsTruncated'] == 'true') { - $response['NextMarker'] = (string)$rest->body->NextMarker; - } - } - - return $response; - } - - /** - * Retrieves information on a specified hosted zone - * - * @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse - * In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9', - * then that full value should be passed here, including the '/hostedzone/' prefix. - * @return A data structure containing information about the specified zone - */ - public function getHostedZone($zoneId) { - // we'll strip off the leading forward slash, so we can use it as the action directly. - $zoneId = trim($zoneId, '/'); - - $rest = new Route53Request($this, $zoneId, 'GET'); - - $rest = $rest->getResponse(); - if($rest->error === false && $rest->code !== 200) { - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - } - if($rest->error !== false) { - $this->__triggerError('getHostedZone', $rest->error); - return false; - } - - $response = array(); - if (!isset($rest->body)) - { - return $response; - } - - $response['HostedZone'] = $this->parseHostedZone($rest->body->HostedZone); - $response['NameServers'] = $this->parseDelegationSet($rest->body->DelegationSet); - - return $response; - } - - /** - * Creates a new hosted zone - * - * @param string name The name of the hosted zone (e.g. "example.com.") - * @param string reference A user-specified unique reference for this request - * @param string comment An optional user-specified comment to attach to the zone - * @return A data structure containing information about the newly created zone - */ - public function createHostedZone($name, $reference, $comment = '') { - // hosted zone names must end with a period, but people will forget this a lot... - if(strrpos($name, '.') != (strlen($name) - 1)) { - $name .= '.'; - } - - $data = "\n"; - $data .= '\n"; - $data .= ''.$name."\n"; - $data .= ''.$reference."\n"; - if(strlen($comment) > 0) { - $data .= "\n"; - $data .= ''.$comment."\n"; - $data .= "\n"; - } - $data .= "\n"; - - $rest = new Route53Request($this, 'hostedzone', 'POST', $data); - - $rest = $rest->getResponse(); - - if($rest->error === false && !in_array($rest->code, array(200, 201, 202, 204)) ) { - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - } - if($rest->error !== false) { - $this->__triggerError('createHostedZone', $rest->error); - return false; - } - - $response = array(); - if (!isset($rest->body)) - { - return $response; - } - - $response['HostedZone'] = $this->parseHostedZone($rest->body->HostedZone); - $response['ChangeInfo'] = $this->parseChangeInfo($rest->body->ChangeInfo); - $response['NameServers'] = $this->parseDelegationSet($rest->body->DelegationSet); - - return $response; - } - - /** - * Retrieves information on a specified hosted zone - * - * @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse - * In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9', - * then that full value should be passed here, including the '/hostedzone/' prefix. - * @return The change request data corresponding to this delete - */ - public function deleteHostedZone($zoneId) { - // we'll strip off the leading forward slash, so we can use it as the action directly. - $zoneId = trim($zoneId, '/'); - - $rest = new Route53Request($this, $zoneId, 'DELETE'); - - $rest = $rest->getResponse(); - if($rest->error === false && $rest->code !== 200) { - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - } - if($rest->error !== false) { - $this->__triggerError('deleteHostedZone', $rest->error); - return false; - } - - if (!isset($rest->body)) - { - return array(); - } - - return $this->parseChangeInfo($rest->body->ChangeInfo); - } - - /** - * Retrieves a list of resource record sets for a given zone - * - * @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse - * In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9', - * then that full value should be passed here, including the '/hostedzone/' prefix. - * @param string type The type of resource record set to begin listing from. If this is specified, $name must also be specified. - * Must be one of: A, AAAA, CNAME, MX, NS, PTR, SOA, SPF, SRV, TXT - * @param string name The name at which to begin listing resource records (in the lexographic order of records). - * @param int maxItems The maximum number of results to return. The service uses min($maxItems, 100). - * @return The list of matching resource record sets - */ - public function listResourceRecordSets($zoneId, $type = '', $name = '', $maxItems = 100) { - // we'll strip off the leading forward slash, so we can use it as the action directly. - $zoneId = trim($zoneId, '/'); - - $rest = new Route53Request($this, $zoneId.'/rrset', 'GET'); - - if(strlen($type) > 0) { - $rest->setParameter('type', $type); - } - if(strlen($name) > 0) { - $rest->setParameter('name', $name); - } - if($maxItems != 100) { - $rest->setParameter('maxitems', $maxItems); - } - - $rest = $rest->getResponse(); - if($rest->error === false && $rest->code !== 200) { - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - } - if($rest->error !== false) { - $this->__triggerError('listResourceRecordSets', $rest->error); - return false; - } - - $response = array(); - if (!isset($rest->body)) - { - return $response; - } - - $recordSets = array(); - foreach($rest->body->ResourceRecordSets->ResourceRecordSet as $set) { - $recordSets[] = $this->parseResourceRecordSet($set); - } - - $response['ResourceRecordSets'] = $recordSets; - - if(isset($rest->body->MaxItems)) { - $response['MaxItems'] = (string)$rest->body->MaxItems; - } - - if(isset($rest->body->IsTruncated)) { - $response['IsTruncated'] = (string)$rest->body->IsTruncated; - if($response['IsTruncated'] == 'true') { - $response['NextRecordName'] = (string)$rest->body->NextRecordName; - $response['NextRecordType'] = (string)$rest->body->NextRecordType; - } - } - - return $response; - } - - /** - * Makes the specified resource record set changes (create or delete). - * - * @param string zoneId The id of the hosted zone, as returned by CreateHostedZoneResponse or ListHostedZoneResponse - * In other words, if ListHostedZoneResponse shows the zone's Id as '/hostedzone/Z1PA6795UKMFR9', - * then that full value should be passed here, including the '/hostedzone/' prefix. - * @param array changes An array of change objects, as they are returned by the prepareChange utility method. - * You may also pass a single change object. - * @param string comment An optional comment to attach to the change request - * @return The status of the change request - */ - public function changeResourceRecordSets($zoneId, $changes, $comment = '') { - // we'll strip off the leading forward slash, so we can use it as the action directly. - $zoneId = trim($zoneId, '/'); - - $data = "\n"; - $data .= '\n"; - $data .= "\n"; - - if(strlen($comment) > 0) { - $data .= ''.$comment."\n"; - } - - if(!is_array($changes)) { - $changes = array($changes); - } - - $data .= "\n"; - foreach($changes as $change) { - $data .= $change; - } - $data .= "\n"; - - $data .= "\n"; - $data .= "\n"; - - $rest = new Route53Request($this, $zoneId.'/rrset', 'POST', $data); - - $rest = $rest->getResponse(); - if($rest->error === false && !in_array($rest->code, array(200, 201, 202, 204))) { - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - } - if($rest->error !== false) { - $this->__triggerError('changeResourceRecordSets', $rest->error); - return false; - } - - if (!isset($rest->body)) - { - return array(); - } - - return $this->parseChangeInfo($rest->body->ChangeInfo); - } - - /** - * Retrieves information on a specified change request - * - * @param string changeId The id of the change, as returned by CreateHostedZoneResponse or ChangeResourceRecordSets - * In other words, if CreateHostedZoneResponse showed the change's Id as '/change/C2682N5HXP0BZ4', - * then that full value should be passed here, including the '/change/' prefix. - * @return The status of the change request - */ - public function getChange($changeId) { - // we'll strip off the leading forward slash, so we can use it as the action directly. - $zoneId = trim($changeId, '/'); - - $rest = new Route53Request($this, $changeId, 'GET'); - - $rest = $rest->getResponse(); - if($rest->error === false && $rest->code !== 200) { - $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); - } - if($rest->error !== false) { - $this->__triggerError('getChange', $rest->error); - return false; - } - - if (!isset($rest->body)) - { - return array(); - } - - return $this->parseChangeInfo($rest->body->ChangeInfo); - } - - - - /** - * Utility function to parse a HostedZone tag structure - */ - private function parseHostedZone($tag) { - $zone = array(); - $zone['Id'] = (string)$tag->Id; - $zone['Name'] = (string)$tag->Name; - $zone['CallerReference'] = (string)$tag->CallerReference; - - // these might always be set, but check just in case, since - // their values are option on CreateHostedZone requests - if(isset($tag->Config) && isset($tag->Config->Comment)) { - $zone['Config'] = array('Comment' => (string)$tag->Config->Comment); - } - - return $zone; - } - - /** - * Utility function to parse a ChangeInfo tag structure - */ - private function parseChangeInfo($tag) { - $info = array(); - $info['Id'] = (string)$tag->Id; - $info['Status'] = (string)$tag->Status; - $info['SubmittedAt'] = (string)$tag->SubmittedAt; - return $info; - } - - /** - * Utility function to parse a DelegationSet tag structure - */ - private function parseDelegationSet($tag) { - $servers = array(); - foreach($tag->NameServers->NameServer as $ns) { - $servers[] = (string)$ns; - } - return $servers; - } - - /** - * Utility function to parse a ResourceRecordSet tag structure - */ - private function parseResourceRecordSet($tag) { - $rrs = array(); - $rrs['Name'] = (string)$tag->Name; - $rrs['Type'] = (string)$tag->Type; - $rrs['TTL'] = (string)$tag->TTL; - $rrs['ResourceRecords'] = array(); - foreach($tag->ResourceRecords->ResourceRecord as $rr) { - $rrs['ResourceRecords'][] = (string)$rr->Value; - } - return $rrs; - } - - /** - * Utility function to prepare a Change object for ChangeResourceRecordSets requests. - * All fields are required. - * - * @param string action The action to perform. One of: CREATE, DELETE - * @param string name The name to perform the action on. - * If it does not end with '.', then AWS treats the name as relative to the zone root. - * @param string type The type of record being modified. - * Must be one of: A, AAAA, CNAME, MX, NS, PTR, SOA, SPF, SRV, TXT - * @param int ttl The time-to-live value for this record, in seconds. - * @param array records An array of resource records to attach to this change. - * Each member of this array can either be a string, or an array of strings. - * Passing an array of strings will attach multiple values to a single resource record. - * If a single string is passed as $records instead of an array, - * it will be treated as a single-member array. - * @return object An opaque object containing the change request. - * Do not write code that depends on the contents of this object, as it may change at any time. - */ - public function prepareChange($action, $name, $type, $ttl, $records) { - $change = "\n"; - $change .= ''.$action."\n"; - $change .= "\n"; - $change .= ''.$name."\n"; - $change .= ''.$type."\n"; - $change .= ''.$ttl."\n"; - $change .= "\n"; - - if(!is_array($records)) { - $records = array($records); - } - - foreach($records as $record) { - $change .= "\n"; - if(is_array($record)) { - foreach($record as $value) { - $change .= ''.$value."\n"; - } - } - else { - $change .= ''.$record."\n"; - } - $change .= "\n"; - } - - $change .= "\n"; - $change .= "\n"; - $change .= "\n"; - - return $change; - } - - /** - * Trigger an error message - * - * @internal Used by member functions to output errors - * @param array $error Array containing error information - * @return string - */ - public function __triggerError($functionname, $error) - { - if($error == false) { - log_error(sprintf("Route53::%s(): Encountered an error, but no description given", $functionname)); - } - else if(isset($error['curl']) && $error['curl']) - { - log_error(sprintf("Route53::%s(): %s %s", $functionname, $error['code'], $error['message'])); - } - else if(isset($error['Error'])) - { - $e = $error['Error']; - $message = sprintf("Route53::%s(): %s - %s: %s\nRequest Id: %s\n", $functionname, $e['Type'], $e['Code'], $e['Message'], $error['RequestId']); - log_error($message); - } - } - - /** - * Callback handler for 503 retries. - * - * @internal Used by SimpleDBRequest to call the user-specified callback, if set - * @param $attempt The number of failed attempts so far - * @return The retry delay in microseconds, or 0 to stop retrying. - */ - public function __executeServiceTemporarilyUnavailableRetryDelay($attempt) - { - if(is_callable($this->__serviceUnavailableRetryDelayCallback)) { - $callback = $this->__serviceUnavailableRetryDelayCallback; - return $callback($attempt); - } - return 0; - } -} - -final class Route53Request -{ - private $r53, $action, $verb, $data, $parameters = array(); - public $response; - - /** - * Constructor - * - * @param string $r53 The Route53 object making this request - * @param string $action SimpleDB action - * @param string $verb HTTP verb - * @param string $data For POST requests, the data being posted (optional) - * @return mixed - */ - function __construct($r53, $action, $verb, $data = '') { - $this->r53 = $r53; - $this->action = $action; - $this->verb = $verb; - $this->data = $data; - $this->response = new STDClass; - $this->response->error = false; - } - - /** - * Set request parameter - * - * @param string $key Key - * @param string $value Value - * @param boolean $replace Whether to replace the key if it already exists (default true) - * @return void - */ - public function setParameter($key, $value, $replace = true) { - if(!$replace && isset($this->parameters[$key])) - { - $temp = (array)($this->parameters[$key]); - $temp[] = $value; - $this->parameters[$key] = $temp; - } - else - { - $this->parameters[$key] = $value; - } - } - - /** - * Get the response - * - * @return object | false - */ - public function getResponse() { - - $params = array(); - foreach ($this->parameters as $var => $value) - { - if(is_array($value)) - { - foreach($value as $v) - { - $params[] = $var.'='.$this->__customUrlEncode($v); - } - } - else - { - $params[] = $var.'='.$this->__customUrlEncode($value); - } - } - - sort($params, SORT_STRING); - - $query = implode('&', $params); - - // must be in format 'Sun, 06 Nov 1994 08:49:37 GMT' - $date = gmdate('D, d M Y H:i:s e'); - - $headers = array(); - $headers[] = 'Date: '.$date; - $headers[] = 'Host: '.$this->r53->getHost(); - - $auth = 'AWS3-HTTPS AWSAccessKeyId='.$this->r53->getAccessKey(); - $auth .= ',Algorithm=HmacSHA256,Signature='.$this->__getSignature($date); - $headers[] = 'X-Amzn-Authorization: '.$auth; - - $url = 'https://'.$this->r53->getHost().'/'.Route53::API_VERSION.'/'.$this->action.'?'.$query; - - // Basic setup - $curl = curl_init(); - curl_setopt($curl, CURLOPT_USERAGENT, 'Route53/php'); - - curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, ($this->r53->verifyHost() ? 1 : 0)); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, ($this->r53->verifyPeer() ? 1 : 0)); - - curl_setopt($curl, CURLOPT_URL, $url); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); - curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); - - // Request types - switch ($this->verb) { - case 'GET': break; - case 'POST': - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); - if(strlen($this->data) > 0) { - curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data); - $headers[] = 'Content-Type: text/plain'; - $headers[] = 'Content-Length: '.strlen($this->data); - } - break; - case 'DELETE': - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); - break; - default: break; - } - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - curl_setopt($curl, CURLOPT_HEADER, false); - - // Execute, grab errors - if (curl_exec($curl)) { - $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); - } else { - $this->response->error = array( - 'curl' => true, - 'code' => curl_errno($curl), - 'message' => curl_error($curl), - 'resource' => $this->resource - ); - } - - @curl_close($curl); - - // Parse body into XML - if ($this->response->error === false && isset($this->response->body)) { - $this->response->body = simplexml_load_string($this->response->body); - - // Grab Route53 errors - if (!in_array($this->response->code, array(200, 201, 202, 204)) - && isset($this->response->body->Error)) { - $error = $this->response->body->Error; - $output = array(); - $output['curl'] = false; - $output['Error'] = array(); - $output['Error']['Type'] = (string)$error->Type; - $output['Error']['Code'] = (string)$error->Code; - $output['Error']['Message'] = (string)$error->Message; - $output['RequestId'] = (string)$this->response->body->RequestId; - - $this->response->error = $output; - unset($this->response->body); - } - } - - return $this->response; - } - - /** - * CURL write callback - * - * @param resource &$curl CURL resource - * @param string &$data Data - * @return integer - */ - private function __responseWriteCallback(&$curl, &$data) { - $this->response->body .= $data; - return strlen($data); - } - - /** - * Contributed by afx114 - * URL encode the parameters as per http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html?Query_QueryAuth.html - * PHP's rawurlencode() follows RFC 1738, not RFC 3986 as required by Amazon. The only difference is the tilde (~), so convert it back after rawurlencode - * See: http://www.morganney.com/blog/API/AWS-Product-Advertising-API-Requires-a-Signed-Request.php - * - * @param string $var String to encode - * @return string - */ - private function __customUrlEncode($var) { - return str_replace('%7E', '~', rawurlencode($var)); - } - - /** - * Generate the auth string using Hmac-SHA256 - * - * @internal Used by SimpleDBRequest::getResponse() - * @param string $string String to sign - * @return string - */ - private function __getSignature($string) { - return base64_encode(hash_hmac('sha256', $string, $this->r53->getSecretKey(), true)); - } -} diff --git a/dns/dyndns/src/etc/rc.dyndns b/dns/dyndns/src/etc/rc.dyndns deleted file mode 100755 index 5bdc013535..0000000000 --- a/dns/dyndns/src/etc/rc.dyndns +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/local/bin/php - - * 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. - */ - -require_once("config.inc"); -require_once("interfaces.inc"); -require_once("util.inc"); -require_once("filter.inc"); -require_once("plugins.inc.d/dyndns.inc"); - -if (isset($argv[1])) { - $argument = trim($argv[1], " \n"); -} else { - $argument = null; -} - -if (empty($argument)) { - dyndns_configure_do(true); -} else { - $interface = (new \OPNsense\Routing\Gateways(legacy_interfaces_details()))->getInterfaceName($argument); - if (empty($interface)) { - $interface = $argument; - } - dyndns_configure_do(true, $interface); -} diff --git a/dns/dyndns/src/etc/rc.syshook.d/monitor/50-dyndns b/dns/dyndns/src/etc/rc.syshook.d/monitor/50-dyndns deleted file mode 100755 index 77511becc1..0000000000 --- a/dns/dyndns/src/etc/rc.syshook.d/monitor/50-dyndns +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh - -# Copyright (c) 2019 Franco Fichtner -# -# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. - -GATEWAY="${1}" - -if [ -z "${GATEWAY}" ]; then - # require a gateway - exit 1 -fi - -echo -n "Reloading DynDNS for ${GATEWAY}: " -/usr/local/opnsense/service/configd_ctl.py dyndns reload ${GATEWAY} - -exit 0 diff --git a/dns/dyndns/src/opnsense/mvc/app/models/OPNsense/DynamicDNS/ACL/ACL.xml b/dns/dyndns/src/opnsense/mvc/app/models/OPNsense/DynamicDNS/ACL/ACL.xml deleted file mode 100644 index 108e7e6537..0000000000 --- a/dns/dyndns/src/opnsense/mvc/app/models/OPNsense/DynamicDNS/ACL/ACL.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - Services: Dynamic DNS clients - - services_dyndns.php* - services_dyndns_edit.php* - - - diff --git a/dns/dyndns/src/opnsense/mvc/app/models/OPNsense/DynamicDNS/Menu/Menu.xml b/dns/dyndns/src/opnsense/mvc/app/models/OPNsense/DynamicDNS/Menu/Menu.xml deleted file mode 100644 index 33f6a448cd..0000000000 --- a/dns/dyndns/src/opnsense/mvc/app/models/OPNsense/DynamicDNS/Menu/Menu.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/dns/dyndns/src/opnsense/service/conf/actions.d/actions_dyndns.conf b/dns/dyndns/src/opnsense/service/conf/actions.d/actions_dyndns.conf deleted file mode 100644 index 294b7da846..0000000000 --- a/dns/dyndns/src/opnsense/service/conf/actions.d/actions_dyndns.conf +++ /dev/null @@ -1,6 +0,0 @@ -[reload] -command:/usr/local/etc/rc.dyndns -description:Dynamic DNS Update -parameters:%s -type:script -message:updating dyndns %s diff --git a/dns/dyndns/src/www/services_dyndns.php b/dns/dyndns/src/www/services_dyndns.php deleted file mode 100644 index 30f5d259ce..0000000000 --- a/dns/dyndns/src/www/services_dyndns.php +++ /dev/null @@ -1,198 +0,0 @@ - - - - - -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- "> - "> - - - -%s', - $ipaddr != $cached_ip ? 'red' : 'green', - htmlspecialchars($cached_ip) - ); - } elseif (!empty($fdata6)) { - $cached_ipv6_s = explode('|', $fdata6); - $cached_ipv6 = $cached_ipv6_s[0]; - echo sprintf( - '%s', - $ipv6addr != $cached_ipv6 ? 'red' : 'green', - htmlspecialchars($cached_ipv6) - ); - } else { - echo sprintf('%s', gettext('N/A')); - }?> - - - -
-
-
-
-
-
-
-
- diff --git a/dns/dyndns/src/www/services_dyndns_edit.php b/dns/dyndns/src/www/services_dyndns_edit.php deleted file mode 100644 index 9eac454d85..0000000000 --- a/dns/dyndns/src/www/services_dyndns_edit.php +++ /dev/null @@ -1,500 +0,0 @@ - - - - - -
-
-
- 0) print_input_errors($input_errors); ?> -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- " /> -
- -
- -
- - -
- - -
- - -
- /> - -
- /> - -
- /> -
- /> - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- -
  - - - - - - -
- ', ''); ?> -
-
-
-
-
-
-
-
-%s', - $ipaddr != $cached_ip ? 'text-danger' : '', - htmlspecialchars($cached_ip) - ); - } elseif (!empty($fdata6)) { - $cached_ipv6_s = explode('|', $fdata6); - $cached_ipv6 = $cached_ipv6_s[0]; - echo sprintf( - '%s', - $ipv6addr != $cached_ipv6 ? 'text-danger' : '', - htmlspecialchars($cached_ipv6) - ); - } else { - echo gettext('N/A'); - } - } - exit; -} - -?> - - - - - - - - - - - - $dyndns) :?> - - - - - - - - -
> - $ifdesc) { - if ($dyndns['interface'] == $if) { - echo "{$ifdesc}"; - break; - } - } - foreach ($groupslist as $if => $group) { - if ($dyndns['interface'] == $if) { - echo "{$if}"; - break; - } - }?> - > - - > - - > -
- -
-
- diff --git a/dns/rfc2136/Makefile b/dns/rfc2136/Makefile index dc089e39b0..d589ee31a2 100644 --- a/dns/rfc2136/Makefile +++ b/dns/rfc2136/Makefile @@ -1,6 +1,6 @@ PLUGIN_NAME= rfc2136 -PLUGIN_VERSION= 1.6 -PLUGIN_REVISION= 2 +PLUGIN_VERSION= 1.9 +PLUGIN_REVISION= 5 PLUGIN_COMMENT= RFC-2136 Support PLUGIN_MAINTAINER= franco@opnsense.org PLUGIN_DEPENDS= bind-tools diff --git a/dns/rfc2136/pkg-descr b/dns/rfc2136/pkg-descr index defdef6d05..5ef614b97c 100644 --- a/dns/rfc2136/pkg-descr +++ b/dns/rfc2136/pkg-descr @@ -1 +1,8 @@ Support for RFC-2136 based dynamic DNS updates using Bind + +Plugin Changelog +================ + +1.8 + +* Add support for choosing hash algorithm used by nsupdate command diff --git a/dns/rfc2136/src/etc/inc/plugins.inc.d/rfc2136.inc b/dns/rfc2136/src/etc/inc/plugins.inc.d/rfc2136.inc index 8663b5f250..2f36beac2c 100644 --- a/dns/rfc2136/src/etc/inc/plugins.inc.d/rfc2136.inc +++ b/dns/rfc2136/src/etc/inc/plugins.inc.d/rfc2136.inc @@ -1,41 +1,41 @@ - Copyright (C) 2010 Ermal Luçi - Copyright (C) 2005-2006 Colin Smith - Copyright (C) 2003-2004 Manuel Kasper - 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. -*/ + * Copyright (C) 2014-2017 Franco Fichtner + * Copyright (C) 2010 Ermal Luçi + * Copyright (C) 2005-2006 Colin Smith + * Copyright (C) 2003-2004 Manuel Kasper + * 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. + */ function rfc2136_configure() { - return array( - 'bootup' => array('rfc2136_configure_do'), - 'local' => array('rfc2136_configure_do'), - 'newwanip' => array('rfc2136_configure_do:2'), - ); + return [ + 'bootup' => ['rfc2136_configure_do'], + 'local' => ['rfc2136_configure_do'], + 'newwanip' => ['rfc2136_configure_do:2'], + ]; } function rfc2136_enabled() @@ -55,19 +55,15 @@ function rfc2136_enabled() function rfc2136_services() { - global $config; - - $services = array(); + $services = []; if (rfc2136_enabled()) { - $services[] = array( + $services[] = [ 'description' => gettext('RFC 2136'), - 'configd' => array( - 'restart' => array('rfc2136 reload'), - ), + 'configd' => [ 'restart' => ['rfc2136 reload'] ], 'nocheck' => true, 'name' => 'rfc2136', - ); + ]; } return $services; @@ -75,10 +71,10 @@ function rfc2136_services() function rfc2136_cron() { - $jobs = array(); + $jobs = []; if (rfc2136_enabled()) { - $jobs[]['autocron'] = array('/usr/local/etc/rc.rfc2136', '16', '1'); + $jobs[]['autocron'] = ['/usr/local/etc/rc.rfc2136', '16', '1']; } return $jobs; @@ -91,23 +87,20 @@ function rfc2136_cache_file($dnsupdate, $ipver = 4) return "/var/cache/rfc2136_{$dnsupdate['interface']}_{$dnsupdate['host']}_{$dnsupdate['server']}{$ipver}.cache"; } -function rfc2136_configure_do($verbose = false, $int = '', $updatehost = '', $forced = false) +function rfc2136_configure_do($verbose = false, $int = null, $updatehost = '', $forced = false) { global $config; - if (!rfc2136_enabled()) { + if (!rfc2136_enabled() || !plugins_argument_map($int)) { return; } - if ($verbose) { - echo 'Configuring RFC 2136 clients...'; - flush(); - } + service_log('Configuring RFC 2136 clients...', $verbose); foreach ($config['dnsupdates']['dnsupdate'] as $i => $dnsupdate) { if (!isset($dnsupdate['enable'])) { continue; - } elseif (!empty($int) && $int != $dnsupdate['interface']) { + } elseif (!empty($int) && !in_array($dnsupdate['interface'], $int)) { continue; } elseif (!empty($updatehost) && ($updatehost != $dnsupdate['host'])) { continue; @@ -127,34 +120,17 @@ function rfc2136_configure_do($verbose = false, $int = '', $updatehost = '', $fo $hostname .= "."; } - /* write private key file - this is dumb - public and private keys are the same for HMAC-MD5, - but nsupdate insists on having both */ - $fd = fopen("/var/etc/K{$i}{$keyname}+157+00000.private", "w"); - $privkey = << $maxCacheAgeSecs) || $forced) { @@ -197,12 +173,12 @@ EOD; if (file_exists($cacheFile6)) { list($cachedipv6, $cacheTimev6) = explode('|', file_get_contents($cacheFile6)); } else { - list($cachedipv6, $cacheTimev6) = array('', ''); + list($cachedipv6, $cacheTimev6) = ['', '']; } if (isset($dnsupdate['usepublicip'])) { - $wanipv6 = get_dyndns_ip($dnsupdate['interface'], 6); + $wanipv6 = get_rfc2136_ip_address($dnsupdate['interface'], 6); } else { - $wanipv6 = get_interface_ipv6($dnsupdate['interface']); + list ($wanipv6) = interfaces_primary_address6($dnsupdate['interface']); } if (is_ipaddrv6($wanipv6)) { if (($wanipv6 != $cachedipv6) || (($currentTime - $cacheTimev6) > $maxCacheAgeSecs) || $forced) { @@ -222,20 +198,56 @@ EOD; $upinst .= "\n"; /* mind that trailing newline! */ if ($need_update) { - @file_put_contents("/var/etc/nsupdatecmds{$i}", $upinst); - unset($upinst); - /* invoke nsupdate */ - $cmd = "/usr/local/bin/nsupdate -k /var/etc/K{$i}{$keyname}+157+00000.key"; + file_safe("/var/etc/nsupdatecmds{$i}", $upinst); + + $frmt = ['/usr/local/bin/nsupdate -k %s']; + $args = [$keyfile]; + if (isset($dnsupdate['usetcp'])) { - $cmd .= " -v"; + $frmt[] = '-v'; } - $cmd .= " /var/etc/nsupdatecmds{$i}"; - mwexec_bg($cmd); - unset($cmd); + + $args[] = "/var/etc/nsupdatecmds{$i}"; + $frmt[] = '%s'; + + mwexecfb($frmt, $args); } } - if ($verbose) { - echo "done.\n"; + service_log("done.\n", $verbose); +} + +function get_rfc2136_ip_address($int, $ipver = 4) +{ + list ($ip_address) = $ipver == 6 ? interfaces_primary_address6($int) : interfaces_primary_address($int); + if (empty($ip_address)) { + log_error("Aborted IPv{$ipver} detection: no address for {$int}"); + return 'down'; + } + + if ($ipver != 6 && is_private_ipv4($ip_address)) { + $ip_ch = curl_init('http://checkip.dyndns.org'); + curl_setopt($ip_ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ip_ch, CURLOPT_INTERFACE, $ip_address); + curl_setopt($ip_ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ip_ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + $ip_result = curl_exec($ip_ch); + if ($ip_result !== false) { + preg_match('=Current IP Address: (.*)=siU', $ip_result, $matches); + $ip_address = trim($matches[1]); + } else { + log_error('Aborted IPv4 detection: ' . curl_error($ip_ch)); + $ip_address = ''; + } + curl_close($ip_ch); + } elseif ($ipver == 6 && is_linklocal($ip_address)) { + log_error('Aborted IPv6 detection: cannot bind to link-local address'); + $ip_address = ''; + } + + if (($ipver == 6 && !is_ipaddrv6($ip_address)) || ($ipver != 6 && !is_ipaddrv4($ip_address))) { + return 'down'; } + + return $ip_address; } diff --git a/dns/rfc2136/src/www/services_rfc2136.php b/dns/rfc2136/src/www/services_rfc2136.php index 16936da0a6..6acf4e7610 100644 --- a/dns/rfc2136/src/www/services_rfc2136.php +++ b/dns/rfc2136/src/www/services_rfc2136.php @@ -54,7 +54,7 @@ write_config(); system_cron_configure(); if (!empty($a_rfc2136[$_POST['id']]['enable'])) { - rfc2136_configure_do(false, '', $a_rfc2136[$_POST['id']]['host'], true); + rfc2136_configure_do(false, null, $a_rfc2136[$_POST['id']]['host'], true); } } exit; @@ -146,9 +146,9 @@ if (file_exists($filename) && !empty($rfc2136['enable']) && (empty($rfc2136['recordtype']) || $rfc2136['recordtype'] == 'A')) { echo "IPv4: "; if (isset($rfc2136['usepublicip'])) { - $ipaddr = get_dyndns_ip($rfc2136['interface'], 4); + $ipaddr = get_rfc2136_ip_address($rfc2136['interface'], 4); } else { - $ipaddr = get_interface_ip($rfc2136['interface']); + list ($ipaddr) = interfaces_primary_address($rfc2136['interface']); } $cached_ip_s = explode("|", file_get_contents($filename)); $cached_ip = $cached_ip_s[0]; @@ -167,9 +167,9 @@ if (file_exists($filename6) && !empty($rfc2136['enable']) && (empty($rfc2136['recordtype']) || $rfc2136['recordtype'] == 'AAAA')) { echo "IPv6: "; if (isset($rfc2136['usepublicip'])) { - $ipaddr = get_dyndns_ip($rfc2136['interface'], 6); + $ipaddr = get_rfc2136_ip_address($rfc2136['interface'], 6); } else { - $ipaddr = get_interface_ipv6($rfc2136['interface']); + list ($ipaddr) = interfaces_primary_address6($rfc2136['interface']); } $cached_ip_s = explode("|", file_get_contents($filename6)); $cached_ip = $cached_ip_s[0]; diff --git a/dns/rfc2136/src/www/services_rfc2136_edit.php b/dns/rfc2136/src/www/services_rfc2136_edit.php index d17c64c571..5dd4614f79 100644 --- a/dns/rfc2136/src/www/services_rfc2136_edit.php +++ b/dns/rfc2136/src/www/services_rfc2136_edit.php @@ -34,6 +34,15 @@ $a_rfc2136 = &config_read_array('dnsupdates', 'dnsupdate'); +$nsukeyalgos = [ + 'hmac-md5' => 'MD5', + 'hmac-sha1' => 'SHA-1', + 'hmac-sha224' => 'SHA-244', + 'hmac-sha256' => 'SHA-256', + 'hmac-sha384' => 'SHA-384', + 'hmac-sha512' => 'SHA-512', +]; + if ($_SERVER['REQUEST_METHOD'] === 'GET') { if (isset($_GET['id']) && !empty($a_rfc2136[$_GET['id']])) { $id = $_GET['id']; @@ -49,7 +58,7 @@ $pconfig['ttl'] = isset($id) &&!empty($a_rfc2136[$id]['ttl']) ? $a_rfc2136[$id]['ttl'] : 60; $pconfig['keydata'] = isset($id) &&!empty($a_rfc2136[$id]['keydata']) ? $a_rfc2136[$id]['keydata'] : null; $pconfig['keyname'] = isset($id) &&!empty($a_rfc2136[$id]['keyname']) ? $a_rfc2136[$id]['keyname'] : null; - $pconfig['keytype'] = isset($id) &&!empty($a_rfc2136[$id]['keytype']) ? $a_rfc2136[$id]['keytype'] : "zone"; + $pconfig['keyalgo'] = isset($id) &&!empty($a_rfc2136[$id]['keyalgo']) ? $a_rfc2136[$id]['keyalgo'] : null; $pconfig['server'] = isset($id) &&!empty($a_rfc2136[$id]['server']) ? $a_rfc2136[$id]['server'] : null; $pconfig['interface'] = isset($id) &&!empty($a_rfc2136[$id]['interface']) ? $a_rfc2136[$id]['interface'] : null; $pconfig['descr'] = isset($id) &&!empty($a_rfc2136[$id]['descr']) ? $a_rfc2136[$id]['descr'] : null; @@ -78,9 +87,12 @@ if (!empty($pconfig['ttl']) && !is_numericint($pconfig['ttl'])) { $input_errors[] = gettext("The DNS update TTL must be an integer."); } - if (!empty($pconfig['keyname']) && !is_domain($pconfig['keyname'])) { + if (!empty($pconfig['keyname']) && !is_domain(preg_replace('/\.$/', '', $pconfig['keyname']))) { $input_errors[] = gettext("The DNS update key name contains invalid characters."); } + if (!in_array($pconfig['keyalgo'] , array_keys($nsukeyalgos))) { + $input_errors[] = gettext('The DNS update key algorith is invalid.'); + } if (count($input_errors) == 0) { $rfc2136 = array(); @@ -88,8 +100,8 @@ $rfc2136['host'] = $pconfig['host']; $rfc2136['ttl'] = $pconfig['ttl']; $rfc2136['keyname'] = $pconfig['keyname']; - $rfc2136['keytype'] = $pconfig['keytype']; $rfc2136['keydata'] = $pconfig['keydata']; + $rfc2136['keyalgo'] = $pconfig['keyalgo']; $rfc2136['server'] = $pconfig['server']; $rfc2136['usetcp'] = !empty($pconfig['usetcp']); $rfc2136['usepublicip'] = !empty($pconfig['usepublicip']); @@ -110,7 +122,7 @@ system_cron_configure(); if (!empty($pconfig['force'])) { - rfc2136_configure_do(false, '', $rfc2136['host'], true); + rfc2136_configure_do(false, null, $rfc2136['host'], true); } header(url_safe('Location: /services_rfc2136.php')); @@ -151,14 +163,11 @@ @@ -198,11 +207,13 @@ - + - />   - />   - /> + @@ -210,7 +221,7 @@ diff --git a/dns/rfc2136/src/www/widgets/include/rfc2136.inc b/dns/rfc2136/src/www/widgets/include/rfc2136.inc deleted file mode 100644 index c143db6dd7..0000000000 --- a/dns/rfc2136/src/www/widgets/include/rfc2136.inc +++ /dev/null @@ -1,4 +0,0 @@ - - * Copyright (C) 2014-2016 Deciso B.V. - * Copyright (C) 2008 Ermal Luçi - * Copyright (C) 2013 Stanley P. Miller \ stan-qaz - * 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. - */ - -require_once("guiconfig.inc"); -require_once("widgets/include/rfc2136.inc"); -require_once("interfaces.inc"); -require_once("plugins.inc.d/rfc2136.inc"); - -$a_rfc2136 = &config_read_array('dnsupdates', 'dnsupdate'); - -if (!empty($_REQUEST['getrfc2136status'])) { - $first_entry = true; - foreach ($a_rfc2136 as $rfc2136) { - if ($first_entry) { - $first_entry = false; - } else { - // Put a vertical bar delimiter between the echoed HTML for each entry processed. - echo '|'; - } - - $filename = rfc2136_cache_file($rfc2136, 4); - $fdata = ''; - if (!empty($rfc2136['enable']) && (empty($rfc2136['recordtype']) || $rfc2136['recordtype'] == 'A') && file_exists($filename)) { - $ipaddr = get_dyndns_ip($rfc2136['interface'], 4); - $fdata = @file_get_contents($filename); - } - - $filename_v6 = rfc2136_cache_file($rfc2136, 6); - $fdata6 = ''; - if (!empty($rfc2136['enable']) && (empty($rfc2136['recordtype']) || $rfc2136['recordtype'] == 'AAAA') && file_exists($filename_v6)) { - $ipv6addr = get_dyndns_ip($rfc2136['interface'], 6); - $fdata6 = @file_get_contents($filename_v6); - } - - if (!empty($fdata)) { - $cached_ip_s = explode('|', $fdata); - $cached_ip = $cached_ip_s[0]; - echo sprintf( - 'IPv4: %s', - $ipaddr != $cached_ip ? 'red' : 'green', - htmlspecialchars($cached_ip) - ); - } else { - echo 'IPv4: ' . gettext('N/A'); - } - - echo '
'; - - if (!empty($fdata6)) { - $cached_ipv6_s = explode('|', $fdata6); - $cached_ipv6 = $cached_ipv6_s[0]; - echo sprintf( - 'IPv6: %s', - $ipv6addr != $cached_ipv6 ? 'red' : 'green', - htmlspecialchars($cached_ipv6) - ); - } else { - echo 'IPv6: ' . gettext('N/A'); - } - } - exit; -} - -?> - - - - - - - - - - - - $rfc2136) :?> - - - - - - - - -
> - $ifdesc) { - if ($rfc2136['interface'] == $if) { - echo "{$ifdesc}"; - break; - } - }?> - > - - > - - > -
- -
-
- diff --git a/emulators/qemu-guest-agent/Makefile b/emulators/qemu-guest-agent/Makefile index ff879738bc..62690e9e82 100644 --- a/emulators/qemu-guest-agent/Makefile +++ b/emulators/qemu-guest-agent/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= qemu-guest-agent -PLUGIN_VERSION= 1.1 +PLUGIN_VERSION= 1.3 PLUGIN_COMMENT= QEMU Guest Agent for OPNsense PLUGIN_DEPENDS= qemu-guest-agent PLUGIN_MAINTAINER= opnsense@moov.de diff --git a/emulators/qemu-guest-agent/pkg-descr b/emulators/qemu-guest-agent/pkg-descr index 12375095f5..656993fb20 100644 --- a/emulators/qemu-guest-agent/pkg-descr +++ b/emulators/qemu-guest-agent/pkg-descr @@ -6,6 +6,17 @@ WWW: http://wiki.qemu.org/Main_Page Plugin Changelog ================ +1.3 + +Fixed: +* fix disabled-rpcs break service start (#4287) +* service not working after upgrade to 24.7 (#4255) + +1.2 + +Changed: +* switch to most recent version of qemu guest agent + 1.1 Fixed: diff --git a/emulators/qemu-guest-agent/src/opnsense/mvc/app/models/OPNsense/QemuGuestAgent/QemuGuestAgent.xml b/emulators/qemu-guest-agent/src/opnsense/mvc/app/models/OPNsense/QemuGuestAgent/QemuGuestAgent.xml index 8c037740b5..59b056bbc7 100644 --- a/emulators/qemu-guest-agent/src/opnsense/mvc/app/models/OPNsense/QemuGuestAgent/QemuGuestAgent.xml +++ b/emulators/qemu-guest-agent/src/opnsense/mvc/app/models/OPNsense/QemuGuestAgent/QemuGuestAgent.xml @@ -5,16 +5,11 @@ - 1 + 1 Y - - 0 - N - + - N - Y Y diff --git a/emulators/qemu-guest-agent/src/opnsense/service/conf/actions.d/actions_qemuguestagent.conf b/emulators/qemu-guest-agent/src/opnsense/service/conf/actions.d/actions_qemuguestagent.conf index c236540b22..6a24b46a88 100644 --- a/emulators/qemu-guest-agent/src/opnsense/service/conf/actions.d/actions_qemuguestagent.conf +++ b/emulators/qemu-guest-agent/src/opnsense/service/conf/actions.d/actions_qemuguestagent.conf @@ -1,23 +1,17 @@ [start] -command:/usr/local/opnsense/scripts/OPNsense/QemuGuestAgent/setup.sh; /usr/local/etc/rc.d/qemu-guest-agent start +command:/usr/local/etc/rc.d/qemu-guest-agent start parameters: type:script message:starting qemu-guest-agent [stop] -command:/usr/local/etc/rc.d/qemu-guest-agent stop; exit 0 +command:/usr/local/etc/rc.d/qemu-guest-agent stop parameters: type:script message:stopping qemu-guest-agent [restart] -command:/usr/local/opnsense/scripts/OPNsense/QemuGuestAgent/setup.sh; /usr/local/etc/rc.d/qemu-guest-agent restart -parameters: -type:script -message:restarting qemu-guest-agent - -[reload] -command:/usr/local/opnsense/scripts/OPNsense/QemuGuestAgent/setup.sh; /usr/local/etc/rc.d/qemu-guest-agent restart +command:/usr/local/etc/rc.d/qemu-guest-agent restart parameters: type:script message:restarting qemu-guest-agent diff --git a/emulators/qemu-guest-agent/src/opnsense/service/templates/OPNsense/QemuGuestAgent/rc.conf.d b/emulators/qemu-guest-agent/src/opnsense/service/templates/OPNsense/QemuGuestAgent/rc.conf.d index ea9c80f8ce..4a14bee7e6 100644 --- a/emulators/qemu-guest-agent/src/opnsense/service/templates/OPNsense/QemuGuestAgent/rc.conf.d +++ b/emulators/qemu-guest-agent/src/opnsense/service/templates/OPNsense/QemuGuestAgent/rc.conf.d @@ -6,8 +6,9 @@ {% do optional_flags.append('-v') %} {% endif %} {% if helpers.exists('OPNsense.QemuGuestAgent.general.DisabledRPCs') and not helpers.empty('OPNsense.QemuGuestAgent.general.DisabledRPCs') %} -{% do optional_flags.append('--blacklist=' ~ OPNsense.QemuGuestAgent.general.DisabledRPCs) %} +{% do optional_flags.append('--block-rpcs=' ~ OPNsense.QemuGuestAgent.general.DisabledRPCs) %} {% endif %} +qemu_guest_agent_setup="/usr/local/opnsense/scripts/OPNsense/QemuGuestAgent/setup.sh" qemu_guest_agent_enable="YES" qemu_guest_agent_flags="{{optional_flags|join(' ')}}" {% else %} diff --git a/ftp/tftp/src/opnsense/mvc/app/models/OPNsense/Tftp/General.xml b/ftp/tftp/src/opnsense/mvc/app/models/OPNsense/Tftp/General.xml index 42284290b8..68a2ba78a7 100644 --- a/ftp/tftp/src/opnsense/mvc/app/models/OPNsense/Tftp/General.xml +++ b/ftp/tftp/src/opnsense/mvc/app/models/OPNsense/Tftp/General.xml @@ -4,11 +4,11 @@ 0.0.1 - 0 + 0 Y - 127.0.0.1 + 127.0.0.1 Y diff --git a/mail/fetchmail/Makefile b/mail/fetchmail/Makefile deleted file mode 100644 index db47f30b41..0000000000 --- a/mail/fetchmail/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -PLUGIN_NAME= fetchmail -PLUGIN_VERSION= 1.1 -PLUGIN_COMMENT= Remote-mail retrieval utility -PLUGIN_DEPENDS= fetchmail -PLUGIN_MAINTAINER= m.muenz@gmail.com - -.include "../../Mk/plugins.mk" diff --git a/mail/fetchmail/pkg-descr b/mail/fetchmail/pkg-descr deleted file mode 100644 index 41d9cfd416..0000000000 --- a/mail/fetchmail/pkg-descr +++ /dev/null @@ -1,19 +0,0 @@ -Fetchmail is a full-featured, robust, well-documented remote-mail retrieval and forwarding -utility intended to be used over on-demand TCP/IP links (such as SLIP or PPP connections). -It supports every remote-mail protocol now in use on the Internet: POP2, POP3, RPOP, APOP, -KPOP, all flavors of IMAP, ETRN, and ODMR. - -Plugin Changelog -================ - -1.1 - -* Fix "ssl" keyword syntax (contributed by mr44er) - -1.0 - -* Allow fetching IMAP mailboxes -* Allow fetching POP3 mailboxes - - -WWW: https://www.fetchmail.info/ diff --git a/mail/fetchmail/src/etc/inc/plugins.inc.d/fetchmail.inc b/mail/fetchmail/src/etc/inc/plugins.inc.d/fetchmail.inc deleted file mode 100644 index 1243a3e6ef..0000000000 --- a/mail/fetchmail/src/etc/inc/plugins.inc.d/fetchmail.inc +++ /dev/null @@ -1,49 +0,0 @@ - - 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. -*/ - -function fetchmail_services() -{ - global $config; - - $services = array(); - - if (isset($config['OPNsense']['fetchmail']['general']['enabled']) && $config['OPNsense']['fetchmail']['general']['enabled'] == 1) { - $services[] = array( - 'description' => gettext('Fetchmail'), - 'configd' => array( - 'restart' => array('fetchmail restart'), - 'start' => array('fetchmail start'), - 'stop' => array('fetchmail stop'), - ), - 'name' => 'fetchmail', - 'pidfile' => '/var/run/fetchmail/fetchmail.pid' - ); - } - - return $services; -} diff --git a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/Api/GeneralController.php b/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/Api/GeneralController.php deleted file mode 100644 index 3a9d3ba8f8..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/Api/GeneralController.php +++ /dev/null @@ -1,37 +0,0 @@ - - * 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. - */ - -namespace OPNsense\Fetchmail\Api; - -use OPNsense\Base\ApiMutableModelControllerBase; - -class GeneralController extends ApiMutableModelControllerBase -{ - protected static $internalModelClass = '\OPNsense\Fetchmail\General'; - protected static $internalModelName = 'general'; -} diff --git a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/Api/MailboxController.php b/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/Api/MailboxController.php deleted file mode 100644 index 403c36152b..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/Api/MailboxController.php +++ /dev/null @@ -1,67 +0,0 @@ - - * 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. - */ - -namespace OPNsense\Fetchmail\Api; - -use OPNsense\Base\ApiMutableModelControllerBase; - -class MailboxController extends ApiMutableModelControllerBase -{ - protected static $internalModelName = 'mailbox'; - protected static $internalModelClass = '\OPNsense\Fetchmail\Mailbox'; - - public function searchMailboxAction() - { - return $this->searchBase('mailboxes.mailbox', array("enabled", "host", "protocol", "user", "password", "destinationmail", "destination")); - } - - public function getMailboxAction($uuid = null) - { - return $this->getBase('mailbox', 'mailboxes.mailbox', $uuid); - } - - public function addMailboxAction() - { - return $this->addBase('mailbox', 'mailboxes.mailbox'); - } - - public function delMailboxAction($uuid) - { - return $this->delBase('mailboxes.mailbox', $uuid); - } - - public function setMailboxAction($uuid) - { - return $this->setBase('mailbox', 'mailboxes.mailbox', $uuid); - } - - public function toggleMailboxAction($uuid) - { - return $this->toggleBase('mailboxes.mailbox', $uuid); - } -} diff --git a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/Api/ServiceController.php b/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/Api/ServiceController.php deleted file mode 100644 index d3ea06f1aa..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/Api/ServiceController.php +++ /dev/null @@ -1,43 +0,0 @@ - - * 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. - */ - -namespace OPNsense\Fetchmail\Api; - -use OPNsense\Base\ApiMutableServiceControllerBase; - -/** - * Class ServiceController - * @package OPNsense\Fetchmail - */ -class ServiceController extends ApiMutableServiceControllerBase -{ - protected static $internalServiceClass = '\OPNsense\Fetchmail\General'; - protected static $internalServiceTemplate = 'OPNsense/Fetchmail'; - protected static $internalServiceEnabled = 'enabled'; - protected static $internalServiceName = 'fetchmail'; -} diff --git a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/GeneralController.php b/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/GeneralController.php deleted file mode 100644 index dde96e5211..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/GeneralController.php +++ /dev/null @@ -1,38 +0,0 @@ - - * 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. - */ - -namespace OPNsense\Fetchmail; - -class GeneralController extends \OPNsense\Base\IndexController -{ - public function indexAction() - { - $this->view->generalForm = $this->getForm("general"); - $this->view->pick('OPNsense/Fetchmail/general'); - } -} diff --git a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/MailboxController.php b/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/MailboxController.php deleted file mode 100644 index 1417158fdd..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/MailboxController.php +++ /dev/null @@ -1,38 +0,0 @@ - - * 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. - */ - -namespace OPNsense\Fetchmail; - -class MailboxController extends \OPNsense\Base\IndexController -{ - public function indexAction() - { - $this->view->formDialogEditFetchmailMailbox = $this->getForm("dialogEditFetchmailMailbox"); - $this->view->pick('OPNsense/Fetchmail/mailbox'); - } -} diff --git a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/forms/dialogEditFetchmailMailbox.xml b/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/forms/dialogEditFetchmailMailbox.xml deleted file mode 100644 index 98307b4d4e..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/forms/dialogEditFetchmailMailbox.xml +++ /dev/null @@ -1,56 +0,0 @@ -
- - mailbox.enabled - - checkbox - This will enable or disable the mailbox retrieving. - - - mailbox.host - - text - Hostname of the mail server where mailbox is located. - - - mailbox.protocol - - dropdown - Choose the protocol to use. - - - mailbox.user - - text - Username to authenticate against remote mail server. - - - mailbox.password - - text - Password to authenticate against remote mail server. - - - mailbox.destinationmail - - text - Set the mail address to deliver fetched mails to. - - - mailbox.destination - - text - Set the mail server hostname or IP to deliver fetched mails to. - - - mailbox.usessl - - checkbox - This will enable or disable usage of encrypted connections. For IMAP and POP3 it will switch the ports to 993 and 995. - - - mailbox.sslfingerprint - - text - If the server is using a self-signed certificate, the only option fetching mails is to enter the certficiate fingerprint here. - -
diff --git a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/forms/general.xml b/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/forms/general.xml deleted file mode 100644 index ac02481893..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/controllers/OPNsense/Fetchmail/forms/general.xml +++ /dev/null @@ -1,14 +0,0 @@ -
- - general.enabled - - checkbox - This will activate the Fetchmail daemon. - - - general.interval - - text - Interval in seconds how often to retrieve mails. - -
diff --git a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/ACL/ACL.xml b/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/ACL/ACL.xml deleted file mode 100644 index e3727a40ad..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/ACL/ACL.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - Services: Fetchmail - - ui/fetchmail/* - api/fetchmail/* - - - diff --git a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/General.php b/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/General.php deleted file mode 100644 index 5ec3f1dd2b..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/General.php +++ /dev/null @@ -1,34 +0,0 @@ - - * 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. - */ - -namespace OPNsense\Fetchmail; - -use OPNsense\Base\BaseModel; - -class General extends BaseModel -{ -} diff --git a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/General.xml b/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/General.xml deleted file mode 100644 index 09cb296c5f..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/General.xml +++ /dev/null @@ -1,15 +0,0 @@ - - //OPNsense/fetchmail/general - Fetchmail configuration - 0.1 - - - 0 - Y - - - 600 - Y - - - diff --git a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/Mailbox.php b/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/Mailbox.php deleted file mode 100644 index e843ede942..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/Mailbox.php +++ /dev/null @@ -1,34 +0,0 @@ - - * 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. - */ - -namespace OPNsense\Fetchmail; - -use OPNsense\Base\BaseModel; - -class Mailbox extends BaseModel -{ -} diff --git a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/Mailbox.xml b/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/Mailbox.xml deleted file mode 100644 index adc2e8a4ff..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/Mailbox.xml +++ /dev/null @@ -1,45 +0,0 @@ - - //OPNsense/fetchmail/mailbox - Fetchmail configuration - 0.1 - - - - - 1 - Y - - - Y - - - Y - - POP3 - IMAP - - - - Y - - - Y - - - Y - - - Y - - - 1 - Y - - - N - /^[A-Fa-f0-9\:]$/ - - - - - diff --git a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/Menu/Menu.xml b/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/Menu/Menu.xml deleted file mode 100644 index 59c8854d8c..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/models/OPNsense/Fetchmail/Menu/Menu.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/mail/fetchmail/src/opnsense/mvc/app/views/OPNsense/Fetchmail/general.volt b/mail/fetchmail/src/opnsense/mvc/app/views/OPNsense/Fetchmail/general.volt deleted file mode 100644 index 77f259c4c3..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/views/OPNsense/Fetchmail/general.volt +++ /dev/null @@ -1,61 +0,0 @@ -{# - # - # Copyright (C) 2020 Michael Muenz - # 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':generalForm,'id':'frm_general_settings'])}} -
-
- -
-
-
-
- - diff --git a/mail/fetchmail/src/opnsense/mvc/app/views/OPNsense/Fetchmail/mailbox.volt b/mail/fetchmail/src/opnsense/mvc/app/views/OPNsense/Fetchmail/mailbox.volt deleted file mode 100644 index 3be00f23d7..0000000000 --- a/mail/fetchmail/src/opnsense/mvc/app/views/OPNsense/Fetchmail/mailbox.volt +++ /dev/null @@ -1,91 +0,0 @@ -{# - # - # Copyright (C) 2020 Michael Muenz - # 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. - #} -
-
- - - - - - - - - - - - - - - - - - - - - -
{{ lang._('Enabled') }}{{ lang._('Host') }}{{ lang._('Protocol') }}{{ lang._('Username') }}{{ lang._('Password') }}{{ lang._('Destination') }}{{ lang._('ID') }}{{ lang._('Commands') }}
- - -
-
-
- -

-
-
-
- -{{ partial("layout_partials/base_dialog",['fields':formDialogEditFetchmailMailbox,'id':'dialogEditFetchmailMailbox','label':lang._('Edit Mailbox')])}} - - diff --git a/mail/fetchmail/src/opnsense/scripts/OPNsense/Fetchmail/setup.sh b/mail/fetchmail/src/opnsense/scripts/OPNsense/Fetchmail/setup.sh deleted file mode 100755 index feda292bc6..0000000000 --- a/mail/fetchmail/src/opnsense/scripts/OPNsense/Fetchmail/setup.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -mkdir -p /var/run/fetchmail -chown fetchmail:wheel /var/run/fetchmail -chmod 755 /var/run/fetchmail -chmod 700 /usr/local/etc/fetchmailrc diff --git a/mail/fetchmail/src/opnsense/service/conf/actions.d/actions_fetchmail.conf b/mail/fetchmail/src/opnsense/service/conf/actions.d/actions_fetchmail.conf deleted file mode 100644 index 9c38206dba..0000000000 --- a/mail/fetchmail/src/opnsense/service/conf/actions.d/actions_fetchmail.conf +++ /dev/null @@ -1,24 +0,0 @@ -[start] -command:/usr/local/opnsense/scripts/OPNsense/Fetchmail/setup.sh;/usr/local/etc/rc.d/fetchmail start -parameters: -type:script -message:starting Fetchmail - -[stop] -command:/usr/local/etc/rc.d/fetchmail stop; exit 0 -parameters: -type:script -message:stopping Fetchmail - -[restart] -command:/usr/local/opnsense/scripts/OPNsense/Fetchmail/setup.sh;/usr/local/etc/rc.d/fetchmail restart -parameters: -type:script -message:restarting Fetchmail -description:Restart Fetchmail service - -[status] -command:/usr/local/etc/rc.d/fetchmail status;exit 0 -parameters: -type:script_output -message:request Fetchmail status diff --git a/mail/fetchmail/src/opnsense/service/templates/OPNsense/Fetchmail/+TARGETS b/mail/fetchmail/src/opnsense/service/templates/OPNsense/Fetchmail/+TARGETS deleted file mode 100644 index 014ce2e157..0000000000 --- a/mail/fetchmail/src/opnsense/service/templates/OPNsense/Fetchmail/+TARGETS +++ /dev/null @@ -1,2 +0,0 @@ -fetchmailrc:/usr/local/etc/fetchmailrc -fetchmail:/etc/rc.conf.d/fetchmail diff --git a/mail/fetchmail/src/opnsense/service/templates/OPNsense/Fetchmail/fetchmail b/mail/fetchmail/src/opnsense/service/templates/OPNsense/Fetchmail/fetchmail deleted file mode 100644 index 4a21f3d9c9..0000000000 --- a/mail/fetchmail/src/opnsense/service/templates/OPNsense/Fetchmail/fetchmail +++ /dev/null @@ -1,6 +0,0 @@ -{% if helpers.exists('OPNsense.fetchmail.general.enabled') and OPNsense.fetchmail.general.enabled == '1' %} -fetchmail_var_script="/usr/local/opnsense/scripts/OPNsense/Fetchmail/setup.sh" -fetchmail_enable="YES" -{% else %} -fetchmail_enable="NO" -{% endif %} diff --git a/mail/fetchmail/src/opnsense/service/templates/OPNsense/Fetchmail/fetchmailrc b/mail/fetchmail/src/opnsense/service/templates/OPNsense/Fetchmail/fetchmailrc deleted file mode 100644 index 891cf3772e..0000000000 --- a/mail/fetchmail/src/opnsense/service/templates/OPNsense/Fetchmail/fetchmailrc +++ /dev/null @@ -1,14 +0,0 @@ -{% if helpers.exists('OPNsense.fetchmail.general.enabled') and OPNsense.fetchmail.general.enabled == '1' %} - -set daemon {{ OPNsense.fetchmail.general.interval }} -set syslog - -{% if helpers.exists('OPNsense.fetchmail.mailbox.mailboxes.mailbox') %} -{% for mailbox_list in helpers.toList('OPNsense.fetchmail.mailbox.mailboxes.mailbox') %} -{% if mailbox_list.enabled == '1' %} -poll {{ mailbox_list.host }} protocol {{ mailbox_list.protocol }} username "{{ mailbox_list.user }}" password "{{ mailbox_list.password }}" is {{ mailbox_list.destinationmail }} smtphost {{ mailbox_list.destination }} {% if mailbox_list.usessl == "0" %}ssl {% endif %} {% if mailbox_list.sslfingerprint|default('') != '' %} sslfingerprint "{{ mailbox_list.sslfingerprint }}" {% endif %} -{% endif %} -{% endfor %} -{% endif %} - -{% endif %} diff --git a/mail/postfix/Makefile b/mail/postfix/Makefile index 84c6f2441e..ca319ebf68 100644 --- a/mail/postfix/Makefile +++ b/mail/postfix/Makefile @@ -1,7 +1,7 @@ PLUGIN_NAME= postfix -PLUGIN_VERSION= 1.20 +PLUGIN_VERSION= 1.24.1 PLUGIN_COMMENT= SMTP mail relay -PLUGIN_DEPENDS= postfix35 +PLUGIN_DEPENDS= postfix PLUGIN_MAINTAINER= m.muenz@gmail.com .include "../../Mk/plugins.mk" diff --git a/mail/postfix/pkg-descr b/mail/postfix/pkg-descr index 49ff2b05a2..77caa08879 100644 --- a/mail/postfix/pkg-descr +++ b/mail/postfix/pkg-descr @@ -6,6 +6,27 @@ is completely different. Plugin Changelog ================ +1.24.1 + +* Align mailbox and message size + +1.24 + +* Disable broken, insecure, legacy NTLM authentication (contributed by Alfred Egger) + +1.23 + +* Add support for Opportunistic DANE as SMTP client security level +* Disable defunct GSSAPI authentication (contributed by Patrick M. Hausen) + +1.22 + +* Switch table format of header_checks from regexp_table to pcre_table (contributed by Starkstromkonsument) + +1.21 + +* Add static link to root certficiates + 1.20 * Make 'delay_warning_time' configurable in the UI diff --git a/mail/postfix/src/opnsense/data/OPNsense/Postfix/dh-parameters.2048.rfc7919 b/mail/postfix/src/opnsense/data/OPNsense/Postfix/dh-parameters.2048.rfc7919 new file mode 100644 index 0000000000..9b182b7201 --- /dev/null +++ b/mail/postfix/src/opnsense/data/OPNsense/Postfix/dh-parameters.2048.rfc7919 @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz ++8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a +87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 +YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi +7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD +ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== +-----END DH PARAMETERS----- diff --git a/mail/postfix/src/opnsense/mvc/app/controllers/OPNsense/Postfix/Api/ServiceController.php b/mail/postfix/src/opnsense/mvc/app/controllers/OPNsense/Postfix/Api/ServiceController.php index e0091751c1..60018990d5 100644 --- a/mail/postfix/src/opnsense/mvc/app/controllers/OPNsense/Postfix/Api/ServiceController.php +++ b/mail/postfix/src/opnsense/mvc/app/controllers/OPNsense/Postfix/Api/ServiceController.php @@ -64,9 +64,6 @@ public function checkrspamdAction() public function reconfigureAction() { if ($this->request->isPost()) { - // close session for long running action - $this->sessionClose(); - $mdlGeneral = new General(); $backend = new Backend(); diff --git a/mail/postfix/src/opnsense/mvc/app/controllers/OPNsense/Postfix/forms/general.xml b/mail/postfix/src/opnsense/mvc/app/controllers/OPNsense/Postfix/forms/general.xml index 9a4eade64a..35982885fa 100644 --- a/mail/postfix/src/opnsense/mvc/app/controllers/OPNsense/Postfix/forms/general.xml +++ b/mail/postfix/src/opnsense/mvc/app/controllers/OPNsense/Postfix/forms/general.xml @@ -123,7 +123,13 @@ general.smtpclient_security dropdown - Choose "none" to disable TLS for sending mail. Set encrypt to enforce TLS security, please do not use this for Internet wide communication as not every server supports TLS yet. Default is "may" which will use TLS when offered. + +
  • 'none' will disable TLS for sending mail.
  • +
  • 'may' will use TLS when offered (Opportunistic TLS)
  • +
  • 'encrypt' will enforce TLS on all connections. Please do not use this for Internet wide communication as not every server supports TLS yet.
  • +
  • 'dane' will enforce TLS if a TLSA-Record is published (Opportunistic DANE, RFC 7672). DNSSEC-capable resolver is required.
  • + ]]>
    general.relayhost diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Address.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Address.xml index ffaf8c949e..73baf4a493 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Address.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Address.xml @@ -6,7 +6,7 @@
    - 1 + 1 Y diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Antispam.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Antispam.xml index fdb3cac7fd..037907446a 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Antispam.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Antispam.xml @@ -4,11 +4,11 @@ 1.0.2 - 0 + 0 Y - accept + accept Y accept diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Domain.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Domain.xml index f74d697d70..a4b9f5c2fd 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Domain.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Domain.xml @@ -4,21 +4,18 @@ 1.0.1 - - - 1 - Y - - - - Y - - - - N - /^([0-9a-zA-Z.:\-\[\]]){1,64}$/u - Only 64 of the following characters are allowed: 0-9a-zA-Z.:-[] - + + + 1 + Y + + + Y + + + /^([0-9a-zA-Z.:\-\[\]]){1,64}$/u + Only 64 of the following characters are allowed: 0-9a-zA-Z.:-[] + diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/General.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/General.xml index 4be05625e7..ddc1928f9d 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/General.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/General.xml @@ -1,34 +1,25 @@ //OPNsense/postfix/general Postfix configuration - 1.2.6 + 1.2.7 - 0 + 0 Y - - - N - - - - N - - - - N - + + + - all + all Y - 25 + 25 Y - all + all Y All @@ -37,32 +28,26 @@ - N ipv4 - N ipv6 - 127.0.0.0/8,[::ffff:127.0.0.0]/104,[::1]/128 + 127.0.0.0/8,[::ffff:127.0.0.0]/104,[::1]/128 Y - - - N - + - 51200000 + 51200000 Y - N - /^([0-9a-z\.\-\_]{1,128})(,[0-9a-z\.\-\_]{1,128})*$/ui + /^([0-9a-z\.\-\_]{1,128})(,[0-9a-z\.\-\_]{1,128})*$/ui Only up to 128 of the following characters are allowed: 0-9a-zA-Z.-_ - intermediate + intermediate Y Modern @@ -71,7 +56,7 @@ - intermediate + intermediate Y Modern @@ -80,114 +65,104 @@ - 0 + 0 Y cert - N ca - N - may + may Y none may encrypt + dane - N - /^([0-9a-zA-Z.:\-\[\]]){1,64}$/u + /^([0-9a-zA-Z.:\-\[\]]){1,64}$/u Only 64 of the following characters are allowed: 0-9a-zA-Z.:-[] - 0 + 0 Y - - - N - - - - N - + + - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 1 + 1 Y - 0 + 0 Y - 0 - N 0 24 Choose a value between 1 and 24 to activate - 0 or empty to disable. diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Headerchecks.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Headerchecks.xml index 8e8b48377a..6846562502 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Headerchecks.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Headerchecks.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipient.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipient.xml index 40b5c48b46..e90726dea8 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipient.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipient.xml @@ -4,21 +4,21 @@ 1.0.0 - - - 1 - Y - -
    - Y -
    - - Y - - OK - REJECT - - + + + 1 + Y + +
    + Y +
    + + Y + + OK + REJECT + +
    diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipientbcc.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipientbcc.xml index f763871c34..1adf180358 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipientbcc.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Recipientbcc.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sender.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sender.xml index ad250f8027..d293eeaf72 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sender.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sender.xml @@ -4,21 +4,21 @@ 1.0.0 - - - 1 - Y - -
    - Y -
    - - Y - - OK - REJECT - - + + + 1 + Y + +
    + Y +
    + + Y + + OK + REJECT + +
    diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Senderbcc.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Senderbcc.xml index 7f2610a926..4d696bffd5 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Senderbcc.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Senderbcc.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sendercanonical.xml b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sendercanonical.xml index 46b342438f..0662d4d1f7 100644 --- a/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sendercanonical.xml +++ b/mail/postfix/src/opnsense/mvc/app/models/OPNsense/Postfix/Sendercanonical.xml @@ -6,7 +6,7 @@ - 1 + 1 Y diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/address.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/address.volt index c6bc57777d..7f1695ee1c 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/address.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/address.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-addresses").UIBootgrid( - { 'search':'/api/postfix/address/searchAddress', - 'get':'/api/postfix/address/getAddress/', - 'set':'/api/postfix/address/setAddress/', - 'add':'/api/postfix/address/addAddress/', - 'del':'/api/postfix/address/delAddress/', - 'toggle':'/api/postfix/address/toggleAddress/' + { 'search':'/api/postfix/address/search_address', + 'get':'/api/postfix/address/get_address/', + 'set':'/api/postfix/address/set_address/', + 'add':'/api/postfix/address/add_address/', + 'del':'/api/postfix/address/del_address/', + 'toggle':'/api/postfix/address/toggle_address/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/domain.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/domain.volt index bf7e0b1af2..a98dd06557 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/domain.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/domain.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-domains").UIBootgrid( - { 'search':'/api/postfix/domain/searchDomain', - 'get':'/api/postfix/domain/getDomain/', - 'set':'/api/postfix/domain/setDomain/', - 'add':'/api/postfix/domain/addDomain/', - 'del':'/api/postfix/domain/delDomain/', - 'toggle':'/api/postfix/domain/toggleDomain/' + { 'search':'/api/postfix/domain/search_domain', + 'get':'/api/postfix/domain/get_domain/', + 'set':'/api/postfix/domain/set_domain/', + 'add':'/api/postfix/domain/add_domain/', + 'del':'/api/postfix/domain/del_domain/', + 'toggle':'/api/postfix/domain/toggle_domain/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/headerchecks.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/headerchecks.volt index c33b317d44..3cbd1be086 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/headerchecks.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/headerchecks.volt @@ -34,12 +34,12 @@ *************************************************************************************************************/ $("#grid-headerchecks").UIBootgrid( - { 'search':'/api/postfix/headerchecks/searchHeaderchecks', - 'get':'/api/postfix/headerchecks/getHeadercheck/', - 'set':'/api/postfix/headerchecks/setHeadercheck/', - 'add':'/api/postfix/headerchecks/addHeadercheck/', - 'del':'/api/postfix/headerchecks/delHeadercheck/', - 'toggle':'/api/postfix/headerchecks/toggleHeadercheck/' + { 'search':'/api/postfix/headerchecks/search_headerchecks', + 'get':'/api/postfix/headerchecks/get_headercheck/', + 'set':'/api/postfix/headerchecks/set_headercheck/', + 'add':'/api/postfix/headerchecks/add_headercheck/', + 'del':'/api/postfix/headerchecks/del_headercheck/', + 'toggle':'/api/postfix/headerchecks/toggle_headercheck/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipient.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipient.volt index ed1e62be1d..e1bf839c7f 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipient.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipient.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-recipients").UIBootgrid( - { 'search':'/api/postfix/recipient/searchRecipient', - 'get':'/api/postfix/recipient/getRecipient/', - 'set':'/api/postfix/recipient/setRecipient/', - 'add':'/api/postfix/recipient/addRecipient/', - 'del':'/api/postfix/recipient/delRecipient/', - 'toggle':'/api/postfix/recipient/toggleRecipient/' + { 'search':'/api/postfix/recipient/search_recipient', + 'get':'/api/postfix/recipient/get_recipient/', + 'set':'/api/postfix/recipient/set_recipient/', + 'add':'/api/postfix/recipient/add_recipient/', + 'del':'/api/postfix/recipient/del_recipient/', + 'toggle':'/api/postfix/recipient/toggle_recipient/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipientbcc.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipientbcc.volt index 39bd7edc70..856734592d 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipientbcc.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/recipientbcc.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-recipientbccs").UIBootgrid( - { 'search':'/api/postfix/recipientbcc/searchRecipientbcc', - 'get':'/api/postfix/recipientbcc/getRecipientbcc/', - 'set':'/api/postfix/recipientbcc/setRecipientbcc/', - 'add':'/api/postfix/recipientbcc/addRecipientbcc/', - 'del':'/api/postfix/recipientbcc/delRecipientbcc/', - 'toggle':'/api/postfix/recipientbcc/toggleRecipientbcc/' + { 'search':'/api/postfix/recipientbcc/search_recipientbcc', + 'get':'/api/postfix/recipientbcc/get_recipientbcc/', + 'set':'/api/postfix/recipientbcc/set_recipientbcc/', + 'add':'/api/postfix/recipientbcc/add_recipientbcc/', + 'del':'/api/postfix/recipientbcc/del_recipientbcc/', + 'toggle':'/api/postfix/recipientbcc/toggle_recipientbcc/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sender.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sender.volt index 078e6d3869..217ce8186a 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sender.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sender.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-senders").UIBootgrid( - { 'search':'/api/postfix/sender/searchSender', - 'get':'/api/postfix/sender/getSender/', - 'set':'/api/postfix/sender/setSender/', - 'add':'/api/postfix/sender/addSender/', - 'del':'/api/postfix/sender/delSender/', - 'toggle':'/api/postfix/sender/toggleSender/' + { 'search':'/api/postfix/sender/search_sender', + 'get':'/api/postfix/sender/get_sender/', + 'set':'/api/postfix/sender/set_sender/', + 'add':'/api/postfix/sender/add_sender/', + 'del':'/api/postfix/sender/del_sender/', + 'toggle':'/api/postfix/sender/toggle_sender/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/senderbcc.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/senderbcc.volt index 7b22a077bd..96f324696f 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/senderbcc.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/senderbcc.volt @@ -35,12 +35,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-senderbccs").UIBootgrid( - { 'search':'/api/postfix/senderbcc/searchSenderbcc', - 'get':'/api/postfix/senderbcc/getSenderbcc/', - 'set':'/api/postfix/senderbcc/setSenderbcc/', - 'add':'/api/postfix/senderbcc/addSenderbcc/', - 'del':'/api/postfix/senderbcc/delSenderbcc/', - 'toggle':'/api/postfix/senderbcc/toggleSenderbcc/' + { 'search':'/api/postfix/senderbcc/search_senderbcc', + 'get':'/api/postfix/senderbcc/get_senderbcc/', + 'set':'/api/postfix/senderbcc/set_senderbcc/', + 'add':'/api/postfix/senderbcc/add_senderbcc/', + 'del':'/api/postfix/senderbcc/del_senderbcc/', + 'toggle':'/api/postfix/senderbcc/toggle_senderbcc/' } ); diff --git a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sendercanonical.volt b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sendercanonical.volt index 5324769fb7..85848ec9ac 100644 --- a/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sendercanonical.volt +++ b/mail/postfix/src/opnsense/mvc/app/views/OPNsense/Postfix/sendercanonical.volt @@ -36,12 +36,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-sendercanonicals").UIBootgrid( - { 'search':'/api/postfix/sendercanonical/searchSendercanonical', - 'get':'/api/postfix/sendercanonical/getSendercanonical/', - 'set':'/api/postfix/sendercanonical/setSendercanonical/', - 'add':'/api/postfix/sendercanonical/addSendercanonical/', - 'del':'/api/postfix/sendercanonical/delSendercanonical/', - 'toggle':'/api/postfix/sendercanonical/toggleSendercanonical/' + { 'search':'/api/postfix/sendercanonical/search_sendercanonical', + 'get':'/api/postfix/sendercanonical/get_sendercanonical/', + 'set':'/api/postfix/sendercanonical/set_sendercanonical/', + 'add':'/api/postfix/sendercanonical/add_sendercanonical/', + 'del':'/api/postfix/sendercanonical/del_sendercanonical/', + 'toggle':'/api/postfix/sendercanonical/toggle_sendercanonical/' } ); diff --git a/mail/postfix/src/opnsense/service/conf/actions.d/actions_postfix.conf b/mail/postfix/src/opnsense/service/conf/actions.d/actions_postfix.conf index 6fcd216fa5..b88e4e2e65 100644 --- a/mail/postfix/src/opnsense/service/conf/actions.d/actions_postfix.conf +++ b/mail/postfix/src/opnsense/service/conf/actions.d/actions_postfix.conf @@ -1,24 +1,24 @@ [start] -command:/usr/local/opnsense/scripts/OPNsense/Postfix/setup.sh;/usr/local/etc/rc.d/postfix start +command:/usr/local/etc/rc.d/postfix start parameters: type:script message:starting Postfix [stop] -command:/usr/local/etc/rc.d/postfix stop; exit 0 +command:/usr/local/etc/rc.d/postfix stop parameters: type:script message:stopping Postfix [restart] -command:/usr/local/opnsense/scripts/OPNsense/Postfix/setup.sh;/usr/local/etc/rc.d/postfix restart +command:/usr/local/etc/rc.d/postfix restart parameters: type:script message:restarting Postfix description:Restart Postfix service [status] -command:/usr/local/etc/rc.d/postfix status;exit 0 +command:/usr/local/etc/rc.d/postfix status; exit 0 parameters: type:script_output message:request Postfix status diff --git a/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/main.cf b/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/main.cf index 784b87b894..65be3ea0ff 100644 --- a/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/main.cf +++ b/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/main.cf @@ -35,8 +35,9 @@ virtual_alias_maps = hash:/usr/local/etc/postfix/virtual sender_bcc_maps = hash:/usr/local/etc/postfix/senderbcc recipient_bcc_maps = hash:/usr/local/etc/postfix/recipientbcc sender_canonical_maps = regexp:/usr/local/etc/postfix/sendercanonical -header_checks = regexp:/usr/local/etc/postfix/header_checks_receiving -smtp_header_checks = regexp:/usr/local/etc/postfix/header_checks_delivering +header_checks = pcre:/usr/local/etc/postfix/header_checks_receiving +smtp_header_checks = pcre:/usr/local/etc/postfix/header_checks_delivering +smtp_tls_CAfile = /usr/local/etc/ssl/cert.pem ########################## # END SYSTEM DEFAULTS ########################## @@ -79,6 +80,7 @@ smtpd_banner = $myhostname ESMTP Postfix {% endif %} {% if helpers.exists('OPNsense.postfix.general.message_size_limit') and OPNsense.postfix.general.message_size_limit != '' %} message_size_limit = {{ OPNsense.postfix.general.message_size_limit }} +mailbox_size_limit = {{ OPNsense.postfix.general.message_size_limit }} {% endif %} {% if helpers.exists('OPNsense.postfix.general.masquerade_domains') and OPNsense.postfix.general.masquerade_domains != '' %} masquerade_domains = {{ OPNsense.postfix.general.masquerade_domains }} @@ -88,6 +90,9 @@ smtp_tls_wrappermode = yes {% endif %} {% if helpers.exists('OPNsense.postfix.general.smtpclient_security') and OPNsense.postfix.general.smtpclient_security != '' %} +{% if OPNsense.postfix.general.smtpclient_security == 'dane' %} +smtp_dns_support_level = dnssec +{% endif %} smtp_tls_security_level = {{ OPNsense.postfix.general.smtpclient_security }} smtp_tls_loglevel = 1 {% endif %} @@ -115,17 +120,19 @@ smtpd_tls_cert_file = /usr/local/etc/postfix/cert_opn.pem {% endif %} {% if helpers.exists('OPNsense.postfix.general.ca') and OPNsense.postfix.general.ca != '' %} smtpd_tls_CAfile = /usr/local/etc/postfix/ca_opn.pem +{% else %} +smtpd_tls_CAfile = /usr/local/etc/ssl/cert.pem {% endif %} {% if helpers.exists('OPNsense.postfix.general.tls_server_compatibility') %} {% if OPNsense.postfix.general.tls_server_compatibility == 'modern' %} smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2 {% elif OPNsense.postfix.general.tls_server_compatibility == 'intermediate' %} smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 -smtpd_tls_dh1024_param_file = /usr/local/etc/dh-parameters.2048 +smtpd_tls_dh1024_param_file = /usr/local/opnsense/data/OPNsense/Postfix/dh-parameters.2048.rfc7919 smtpd_tls_mandatory_ciphers = medium {% elif OPNsense.postfix.general.tls_server_compatibility == 'old' %} smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3 -smtpd_tls_dh1024_param_file = /usr/local/etc/dh-parameters.2048 +smtpd_tls_dh1024_param_file = /usr/local/opnsense/data/OPNsense/Postfix/dh-parameters.2048.rfc7919 smtpd_tls_mandatory_ciphers = low {% endif %} smtpd_tls_protocols = $smtpd_tls_mandatory_protocols @@ -151,6 +158,7 @@ relayhost = {{ OPNsense.postfix.general.relayhost }} smtp_sasl_auth_enable = yes smtp_sasl_password_maps = hash:/usr/local/etc/postfix/smtp_auth smtp_sasl_security_options = +smtp_sasl_mechanism_filter = !gssapi, !ntlm, !external, static:all {% endif %} {% if helpers.exists('OPNsense.postfix.general.permit_sasl_authenticated') and OPNsense.postfix.general.permit_sasl_authenticated == '1' %} diff --git a/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/postfix b/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/postfix index 1d2cfd8551..65720e2cc6 100644 --- a/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/postfix +++ b/mail/postfix/src/opnsense/service/templates/OPNsense/Postfix/postfix @@ -1,5 +1,5 @@ {% if helpers.exists('OPNsense.postfix.general.enabled') and OPNsense.postfix.general.enabled == '1' %} -postfix_var_script="/usr/local/opnsense/scripts/OPNsense/Postfix/setup.sh" +postfix_setup="/usr/local/opnsense/scripts/OPNsense/Postfix/setup.sh" postfix_enable="YES" {% else %} postfix_enable="NO" diff --git a/mail/rspamd/Makefile b/mail/rspamd/Makefile index 01016fdafc..a1e2148ce0 100644 --- a/mail/rspamd/Makefile +++ b/mail/rspamd/Makefile @@ -1,5 +1,6 @@ PLUGIN_NAME= rspamd -PLUGIN_VERSION= 1.11 +PLUGIN_VERSION= 1.13 +PLUGIN_REVISION= 2 PLUGIN_COMMENT= Protect your network from spam PLUGIN_DEPENDS= rspamd PLUGIN_MAINTAINER= franz.fabian.94@gmail.com diff --git a/mail/rspamd/pkg-descr b/mail/rspamd/pkg-descr index 183897120d..a1d6d33b66 100644 --- a/mail/rspamd/pkg-descr +++ b/mail/rspamd/pkg-descr @@ -5,6 +5,16 @@ lua. Plugin Changelog ---------------- +1.13 + +* Make local whitelist by e-mail address possible (contributed by itNGO) +* Fix typo in "whitelisted_ip" option (contributed by Alexander Riedel) +* Add phishing exclusion (contributed by Makss39) + +1.12 + +* Adjusting the multimap setting to make the multimap whitelist work + 1.11 * Fix Milter Protocol by binding to Unix Sockets diff --git a/mail/rspamd/src/opnsense/mvc/app/controllers/OPNsense/Rspamd/forms/settings.xml b/mail/rspamd/src/opnsense/mvc/app/controllers/OPNsense/Rspamd/forms/settings.xml index b37c8b7806..b7a8d1b780 100644 --- a/mail/rspamd/src/opnsense/mvc/app/controllers/OPNsense/Rspamd/forms/settings.xml +++ b/mail/rspamd/src/opnsense/mvc/app/controllers/OPNsense/Rspamd/forms/settings.xml @@ -297,6 +297,14 @@ text Give a URL where to retrieve the Phishtank feed. + + rspamd.phishing.exclusion + + select_multiple + + true + The exclusion map should only contain a list of host names without a scheme and path. + diff --git a/mail/rspamd/src/opnsense/mvc/app/models/OPNsense/Rspamd/RSpamd.xml b/mail/rspamd/src/opnsense/mvc/app/models/OPNsense/Rspamd/RSpamd.xml index fccb94dfce..b327dff800 100644 --- a/mail/rspamd/src/opnsense/mvc/app/models/OPNsense/Rspamd/RSpamd.xml +++ b/mail/rspamd/src/opnsense/mvc/app/models/OPNsense/Rspamd/RSpamd.xml @@ -1,511 +1,497 @@ - //OPNsense/Rspamd - rspamd anti spam filter - 1.0.2 - - - - 0 - Y - - - 0 - Y - - - 0 - Y - - - N - 1 - 999 - - - This field must be bigger than the subject score. - ComparedToFieldConstraint - subjectscore - gt - - - This field must be bigger than the reject score. - ComparedToFieldConstraint - headerscore - gt - - - This field must be bigger than the greylist score. - ComparedToFieldConstraint - greylistscore - gt - - - - - N - 1 - 999 - - - This field must be lower than the subject score. - ComparedToFieldConstraint - subjectscore - lt - - - This field must be lower than the reject score. - ComparedToFieldConstraint - rejectscore - lt - - - This field must be bigger than the greylist score. - ComparedToFieldConstraint - greylistscore - gt - - - - - N - 1 - 999 - - - This field must be lower than the header score. - ComparedToFieldConstraint - headerscore - lt - - - This field must be lower than the reject score. - ComparedToFieldConstraint - rejectscore - lt - - - This field must be lower than the subject score. - ComparedToFieldConstraint - subjectscore - lt - - - - - N - 1 - 999 - - - This field must be bigger than the header score. - ComparedToFieldConstraint - headerscore - gt - - - This field must be lower than the reject score. - ComparedToFieldConstraint - rejectscore - lt - - - This field must be bigger than the subject score. - ComparedToFieldConstraint - greylistscore - gt - - - - - N - - - 200 - Y - 1 - 100000 - Choose a value between 1 and 100000. - - - 127.0.0.1 - Y - , - Y - - - - - - 0 - Y - - - 0 - Y - - - 0 - Y - - - 0 - Y - - - 0 - Y - - - 0 - Y - - - /[a-z0-9\.\-_@,]+/i - N - - - - - - N - 1 - - - N - 1 - - - N - - - 1 - N - 32 - A valid IPv4 mask must be between 1 and 32 bits. - 19 - - - 1 - N - 128 - 64 - A valid IPv6 mask must be between 1 and 128 bits. 64 bits are recommended as this is the recommended subnet size in IPv6. - - - N - /^([a-fA-F0-9\.:\[\]\/]*?,)*([a-fA-F0-9\.:\[\]\/]*)$/ - - - - - - 1 - N - A valid cache size must be set. - - - 1 - N - A valid cache expiration must be set. - - - 1 - N - A valid time jitter must be set. - - - 0 - Y - - - 0 - Y - - - - 1 - Y - - - 0 - Y - - - 0 - Y - - - 0 - Y - - - 1 - Y - - - 1 - Y - - - 0 - Y - - - header - Y - -
    Header
    - Envelope -
    -
    - - 1 - Y - -
    - - - - 0 - Y - - - 1 - N - 86400 - A valid cache expiration must be set. - - - - - - 0 - Y - - - - N - - - 0 - Y - - - 0 - Y - - - - N - - - - - - - 1 - N - The count value must be a positive number. - - - - m - Y - - Seconds - Minutes - Hours - - - - - - 1 - N - The count value must be a positive number. - - - - m - Y - - Seconds - Minutes - Hours - - - - - - 1 - N - The count value must be a positive number. - - - - m - Y - - Seconds - Minutes - Hours - - - - - - 1 - N - The count value must be a positive number. - - - - m - Y - - Seconds - Minutes - Hours - - - - - - 1 - N - The count value must be a positive number. - - - - m - Y - - Seconds - Minutes - Hours - - - - - - 1 - N - The count value must be a positive number. - - - - m - Y - - Seconds - Minutes - Hours - - - - - postmaster,mailer-daemon - - - 1 - Y - 20 - - - - - - 0 - Y - - - 0 - Y - - - 1 - Y - - - N - - - - - - 1 - N - 2 - A valid cache size in kilobytes must be set. - - - 1 - N - A valid expiration time must be set. - - - - - - 1 - Y - - - 1 - Y - - - 1 - 20000000 - N - A valid maximum size in bytes must be set. - - - N - - - - - - N - - - N - - - - - - N - exe,dll,scr,com,cmd,js,bat,vbs,ps1,bat,cpl,lnk,msi,msp,reg - - - N - - -
    + //OPNsense/Rspamd + rspamd anti spam filter + 1.0.2 + + + + 0 + Y + + + 0 + Y + + + 0 + Y + + + N + 1 + 999 + + + This field must be bigger than the subject score. + ComparedToFieldConstraint + subjectscore + gt + + + This field must be bigger than the reject score. + ComparedToFieldConstraint + headerscore + gt + + + This field must be bigger than the greylist score. + ComparedToFieldConstraint + greylistscore + gt + + + + + N + 1 + 999 + + + This field must be lower than the subject score. + ComparedToFieldConstraint + subjectscore + lt + + + This field must be lower than the reject score. + ComparedToFieldConstraint + rejectscore + lt + + + This field must be bigger than the greylist score. + ComparedToFieldConstraint + greylistscore + gt + + + + + N + 1 + 999 + + + This field must be lower than the header score. + ComparedToFieldConstraint + headerscore + lt + + + This field must be lower than the reject score. + ComparedToFieldConstraint + rejectscore + lt + + + This field must be lower than the subject score. + ComparedToFieldConstraint + subjectscore + lt + + + + + N + 1 + 999 + + + This field must be bigger than the header score. + ComparedToFieldConstraint + headerscore + gt + + + This field must be lower than the reject score. + ComparedToFieldConstraint + rejectscore + lt + + + This field must be bigger than the subject score. + ComparedToFieldConstraint + greylistscore + gt + + + + + N + + + 200 + Y + 1 + 100000 + Choose a value between 1 and 100000. + + + 127.0.0.1 + Y + , + Y + + + + + 0 + Y + + + 0 + Y + + + 0 + Y + + + 0 + Y + + + 0 + Y + + + 0 + Y + + + /[a-z0-9\.\-_@,]+/i + N + + + + + N + 1 + + + N + 1 + + + N + + + 1 + N + 32 + A valid IPv4 mask must be between 1 and 32 bits. + 19 + + + 1 + N + 128 + 64 + A valid IPv6 mask must be between 1 and 128 bits. 64 bits are recommended as this is the recommended subnet size in IPv6. + + + N + /^([a-fA-F0-9\.:\[\]\/]*?,)*([a-fA-F0-9\.:\[\]\/]*)$/ + + + + + 1 + N + A valid cache size must be set. + + + 1 + N + A valid cache expiration must be set. + + + 1 + N + A valid time jitter must be set. + + + 0 + Y + + + 0 + Y + + + + 1 + Y + + + 0 + Y + + + 0 + Y + + + 0 + Y + + + 1 + Y + + + 1 + Y + + + 0 + Y + + + header + Y + +
    Header
    + Envelope +
    +
    + + 1 + Y + +
    + + + 0 + Y + + + 1 + N + 86400 + A valid cache expiration must be set. + + + + + 0 + Y + + + + 0 + Y + + + 0 + Y + + + + N + + + + + + 1 + N + The count value must be a positive number. + + + + m + Y + + Seconds + Minutes + Hours + + + + + + 1 + N + The count value must be a positive number. + + + + m + Y + + Seconds + Minutes + Hours + + + + + + 1 + N + The count value must be a positive number. + + + + m + Y + + Seconds + Minutes + Hours + + + + + + 1 + N + The count value must be a positive number. + + + + m + Y + + Seconds + Minutes + Hours + + + + + + 1 + N + The count value must be a positive number. + + + + m + Y + + Seconds + Minutes + Hours + + + + + + 1 + N + The count value must be a positive number. + + + + m + Y + + Seconds + Minutes + Hours + + + + + postmaster,mailer-daemon + + + 1 + Y + 20 + + + + + 0 + Y + + + 0 + Y + + + 1 + Y + + + N + + + + + 1 + N + 2 + A valid cache size in kilobytes must be set. + + + 1 + N + A valid expiration time must be set. + + + + + 1 + Y + + + 1 + Y + + + 1 + 20000000 + N + A valid maximum size in bytes must be set. + + + N + + + + + N + + + N + + + + + N + exe,dll,scr,com,cmd,js,bat,vbs,ps1,bat,cpl,lnk,msi,msp,reg + + + N + + +
    diff --git a/mail/rspamd/src/opnsense/service/conf/actions.d/actions_rspamd.conf b/mail/rspamd/src/opnsense/service/conf/actions.d/actions_rspamd.conf index e4711138a0..1d8c575d90 100644 --- a/mail/rspamd/src/opnsense/service/conf/actions.d/actions_rspamd.conf +++ b/mail/rspamd/src/opnsense/service/conf/actions.d/actions_rspamd.conf @@ -1,23 +1,23 @@ [start] -command:/usr/local/opnsense/scripts/rspamd/setup.sh;/usr/local/etc/rc.d/rspamd start +command:/usr/local/etc/rc.d/rspamd start parameters: type:script message:starting rspamd [stop] -command:/usr/local/etc/rc.d/rspamd onestop +command:/usr/local/etc/rc.d/rspamd stop parameters: type:script message:stopping rspamd [restart] -command:/usr/local/opnsense/scripts/rspamd/setup.sh;/usr/local/etc/rc.d/rspamd restart +command:/usr/local/etc/rc.d/rspamd restart parameters: type:script message:restarting rspamd [status] -command:/usr/local/etc/rc.d/rspamd status;exit 0 +command:/usr/local/etc/rc.d/rspamd status; exit 0 parameters: type:script_output message:request rspamd status diff --git a/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/+TARGETS b/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/+TARGETS index 6337a9b543..dc9db3cbac 100644 --- a/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/+TARGETS +++ b/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/+TARGETS @@ -13,6 +13,7 @@ surbl-whitelist.inc.local:/var/db/rspamd/surbl-whitelist.inc.local greylist.conf:/usr/local/etc/rspamd/local.d/greylist.conf greylist_ip.wl:/usr/local/etc/rspamd/local.d/greylist_ip.wl phishing.conf:/usr/local/etc/rspamd/local.d/phishing.conf +phishing_whitelist.inc:/usr/local/etc/rspamd/local.d/phishing_whitelist.inc milter_headers.conf:/usr/local/etc/rspamd/local.d/milter_headers.conf multimap.conf:/usr/local/etc/rspamd/local.d/multimap.conf mx_check.conf:/usr/local/etc/rspamd/local.d/mx_check.conf diff --git a/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/greylist.conf b/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/greylist.conf index 42e19b416a..0d359054db 100644 --- a/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/greylist.conf +++ b/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/greylist.conf @@ -11,5 +11,5 @@ action = "soft reject"; # default greylisted action ipv4_mask = {{ OPNsense.Rspamd.graylist.ipv4mask|default('19') }}; ipv6_mask = {{ OPNsense.Rspamd.graylist.ipv6mask|default('64') }}; - whitelist_ip = "/usr/local/etc/rspamd/local.d/greylist_ip.wl"; + whitelisted_ip = "/usr/local/etc/rspamd/local.d/greylist_ip.wl"; {% endif %} diff --git a/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/multimap.conf b/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/multimap.conf index f357673dc1..8926ee13d2 100644 --- a/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/multimap.conf +++ b/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/multimap.conf @@ -8,13 +8,22 @@ extension_blacklist { filter = "extension"; map = "/${LOCAL_CONFDIR}/local.d/bad_file_extensions.map"; symbol = "FILENAME_BLACKLISTED"; - action = "reject"; -} + score 1000; + } WHITELIST_SENDER_DOMAIN { type = "from"; filter = "email:domain"; map = "/${LOCAL_CONFDIR}/local.d/whitelist_sender_domains.map"; - score = -50.0 + score = -1000; } + +local_wl_from { + type = "from"; + map = "$CONFDIR/maps.d/local_wl_from.inc"; + symbol = "LOCAL_WL_FROM"; + description = "Whitelist map for LOCAL_WL_FROM"; + score = -1000; + } + {% endif %} diff --git a/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/phishing_whitelist.inc b/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/phishing_whitelist.inc new file mode 100644 index 0000000000..a46347d158 --- /dev/null +++ b/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/phishing_whitelist.inc @@ -0,0 +1,5 @@ +{% if helpers.exists('OPNsense.Rspamd.general.enabled') and OPNsense.Rspamd.general.enabled == '1' and helpers.exists('OPNsense.Rspamd.phishing.exclusion') and OPNsense.Rspamd.phishing.exclusion != '' %} +{% for sender in OPNsense.Rspamd.phishing.exclusion.split(',') %} +{{ sender }} +{% endfor %} +{% endif %} diff --git a/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/rspamd b/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/rspamd index ca1a5ce4e4..e9cf0d6686 100644 --- a/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/rspamd +++ b/mail/rspamd/src/opnsense/service/templates/OPNsense/Rspamd/rspamd @@ -1,5 +1,5 @@ {% if helpers.exists('OPNsense.Rspamd.general.enabled') and OPNsense.Rspamd.general.enabled == '1' %} -rspamd_var_script="/usr/local/opnsense/scripts/rspamd/setup.sh" +rspamd_setup="/usr/local/opnsense/scripts/rspamd/setup.sh" rspamd_enable="YES" {% else %} rspamd_enable="NO" diff --git a/misc/theme-advanced/Makefile b/misc/theme-advanced/Makefile new file mode 100644 index 0000000000..2e92be9e10 --- /dev/null +++ b/misc/theme-advanced/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= theme-advanced +PLUGIN_VERSION= 1.1 +PLUGIN_COMMENT= Theme based on AdvancedTomato GUI +PLUGIN_MAINTAINER= jacky@prahec.com +PLUGIN_WWW= https://prahec.com/ +PLUGIN_NO_ABI= yes + +.include "../../Mk/plugins.mk" diff --git a/misc/theme-advanced/pkg-descr b/misc/theme-advanced/pkg-descr new file mode 100644 index 0000000000..10d6368848 --- /dev/null +++ b/misc/theme-advanced/pkg-descr @@ -0,0 +1 @@ +Advanced - Modern theme based on AdvancedTomato project by the original author diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.eot b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.eot new file mode 100644 index 0000000000..b93a4953ff Binary files /dev/null and b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.eot differ diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.svg b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.svg new file mode 100644 index 0000000000..94fb5490a2 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000000..1413fc609a Binary files /dev/null and b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf differ diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.woff b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.woff new file mode 100644 index 0000000000..9e612858f8 Binary files /dev/null and b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/fonts/bootstrap/glyphicons-halflings-regular.woff differ diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/_bootstrap-compass.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/_bootstrap-compass.scss new file mode 100644 index 0000000000..82706c47c5 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/_bootstrap-compass.scss @@ -0,0 +1,7 @@ +@function twbs-font-path($path) { + @return font-url($path, true); +} + +@function twbs-image-path($path) { + @return image-url($path, true); +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/_bootstrap-mincer.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/_bootstrap-mincer.scss new file mode 100644 index 0000000000..34132501f2 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/_bootstrap-mincer.scss @@ -0,0 +1,17 @@ +// Mincer asset helper functions +// +// This must be imported into a .css.ejs.scss file. +// Then, <% %>-interpolations will be parsed as strings by Sass, and evaluated by EJS after Sass compilation. + + +@function twbs-font-path($path) { + // do something like following + // from "path/to/font.ext#suffix" to "<%- asset_path(path/to/font.ext)) + #suffix %>" + // from "path/to/font.ext?#suffix" to "<%- asset_path(path/to/font.ext)) + ?#suffix %>" + // or from "path/to/font.ext" just "<%- asset_path(path/to/font.ext)) %>" + @return "<%- asset_path('#{$path}'.replace(/[#?].*$/, '')) + '#{$path}'.replace(/(^[^#?]*)([#?]?.*$)/, '$2') %>"; +} + +@function twbs-image-path($file) { + @return "<%- asset_path('#{$file}') %>"; +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/_bootstrap-sprockets.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/_bootstrap-sprockets.scss new file mode 100644 index 0000000000..7d3069280e --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/_bootstrap-sprockets.scss @@ -0,0 +1,7 @@ +@function twbs-font-path($path) { + @return font-path($path); +} + +@function twbs-image-path($path) { + @return image-path($path); +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-dialog.less b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-dialog.less new file mode 100644 index 0000000000..284a99402f --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-dialog.less @@ -0,0 +1,137 @@ +.modal-backdrop { + z-index: -1; +} + +.bootstrap-dialog { + + .modal-header { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } + .bootstrap-dialog-title { + color: #fff; + display: inline-block; + } + .bootstrap-dialog-button-icon { + margin-right: 3px; + } + .bootstrap-dialog-close-button { + float: right; + filter:alpha(opacity=90); + -moz-opacity:0.9; + -khtml-opacity: 0.9; + opacity: 0.9; + &:hover { + cursor: pointer; + filter: alpha(opacity=100); + -moz-opacity: 1; + -khtml-opacity: 1; + opacity: 1; + } + } + + /* dialog types */ + &.type-default { + .modal-header { + background-color: #fff; + } + .bootstrap-dialog-title { + color: #333; + } + } + + &.type-info { + .modal-header { + background-color: #B0CDDB; + } + } + + &.type-primary { + .modal-header { + background-color: #51aded; + } + } + + &.type-success { + .modal-header { + background-color: #7ebc59; + } + } + + &.type-warning { + .modal-header { + background-color: #f0ad4e; + } + } + + &.type-danger { + .modal-header { + background-color: #EE451F; + } + } + + &.size-large { + .bootstrap-dialog-title { + font-size: 24px; + } + .bootstrap-dialog-close-button { + font-size: 30px; + } + .bootstrap-dialog-message { + font-size: 18px; + } + } + + /** + * Icon animation + * Copied from font-awesome: http://fontawesome.io/ + **/ + .icon-spin { + display: inline-block; + -moz-animation: spin 2s infinite linear; + -o-animation: spin 2s infinite linear; + -webkit-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; + } + @-moz-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(359deg); + } + } + @-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + } + } + @-o-keyframes spin { + 0% { + -o-transform: rotate(0deg); + } + 100% { + -o-transform: rotate(359deg); + } + } + @-ms-keyframes spin { + 0% { + -ms-transform: rotate(0deg); + } + 100% { + -ms-transform: rotate(359deg); + } + } + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(359deg); + } + } + /** End of icon animation **/ +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/less/bootstrap-select.less b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/less/bootstrap-select.less new file mode 100644 index 0000000000..49f13a7021 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/less/bootstrap-select.less @@ -0,0 +1,348 @@ +@import "variables"; + +// Mixins +.cursor-disabled() { + cursor: not-allowed; +} + +// Rules +.bootstrap-select { + width: 348px \0; /*IE9 and below*/ + + // The selectpicker button + > .dropdown-toggle { + width: 100%; + padding-right: 25px; + z-index: 1; + } + + > select { + position: absolute !important; + bottom: 0; + left: 50%; + width: 0.11px !important; + height: 100% !important; + padding: 0 !important; + opacity: 0 !important; + border: none; + + &.mobile-device { + top: 0; + left: 0; + display: block !important; + width: 100% !important; + z-index: 2; + } + } + + // Error display + .has-error & .dropdown-toggle, + .error & .dropdown-toggle { + border-color: @color-red-error; + } + + &.fit-width { + width: auto !important; + } + + &:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) { + width: @width-default; + } + + .dropdown-toggle:focus { + outline: thin dotted #333333 !important; + outline: 5px auto -webkit-focus-ring-color !important; + outline-offset: -2px; + } +} + +.bootstrap-select.form-control { + margin-bottom: 0; + padding: 0; + border: none; + + &:not([class*="col-"]) { + width: 100%; + } + + &.input-group-btn { + z-index: auto; + } +} + +// The selectpicker components +.bootstrap-select.btn-group { + &:not(.input-group-btn), + &[class*="col-"] { + float: none; + display: inline-block; + margin-left: 0; + } + + // Forces the pull to the right, if necessary + &, + &[class*="col-"], + .row &[class*="col-"] { + &.dropdown-menu-right { + float: right; + } + } + + .form-inline &, + .form-horizontal &, + .form-group & { + margin-bottom: 0; + } + + .form-group-lg &.form-control, + .form-group-sm &.form-control { + padding: 0; + } + + // Set the width of the live search (and any other form control within an inline form) + // see https://github.com/silviomoreto/bootstrap-select/issues/685 + .form-inline & .form-control { + width: 100%; + } + + &.disabled, + > .disabled { + .cursor-disabled(); + + &:focus { + outline: none !important; + } + } + + &.bs-container { + position: absolute; + + .dropdown-menu { + z-index: @zindex-select-dropdown; + } + } + + // The selectpicker button + .dropdown-toggle { + .filter-option { + display: inline-block; + overflow: hidden; + width: 100%; + text-align: left; + } + + .caret { + position: absolute; + top: 50%; + right: 12px; + margin-top: -2px; + vertical-align: middle; + } + } + + &[class*="col-"] .dropdown-toggle { + width: 100%; + } + + // The selectpicker dropdown + .dropdown-menu { + min-width: 100%; + box-sizing: border-box; + + &.inner { + position: static; + float: none; + border: 0; + padding: 0; + margin: 0; + border-radius: 0; + box-shadow: none; + } + + li { + position: relative; + + &.active small { + color: #fff; + } + + &.disabled a { + .cursor-disabled(); + } + + a { + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + &.opt { + position: relative; + padding-left: 2.25em; + } + + span.check-mark { + display: none; + } + + span.text { + display: inline-block; + } + } + + small { + padding-left: 0.5em; + } + } + + .notify { + position: absolute; + bottom: 5px; + width: 96%; + margin: 0 2%; + min-height: 26px; + padding: 3px 5px; + background: rgb(245, 245, 245); + border: 1px solid rgb(227, 227, 227); + box-shadow: inset 0 1px 1px fade(rgb(0, 0, 0), 5%); + pointer-events: none; + opacity: 0.9; + box-sizing: border-box; + } + } + + .no-results { + padding: 3px; + background: #f5f5f5; + margin: 0 5px; + white-space: nowrap; + } + + &.fit-width .dropdown-toggle { + .filter-option { + position: static; + } + + .caret { + position: static; + top: auto; + margin-top: -1px; + } + } + + &.show-tick .dropdown-menu li { + &.selected a span.check-mark { + position: absolute; + display: inline-block; + right: 15px; + margin-top: 5px; + } + + a span.text { + margin-right: 34px; + } + } +} + +.bootstrap-select.show-menu-arrow { + &.open > .dropdown-toggle { + z-index: (@zindex-select-dropdown + 1); + } + + .dropdown-toggle { + &:before { + content: ''; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid @color-grey-arrow; + position: absolute; + bottom: -4px; + left: 9px; + display: none; + } + + &:after { + content: ''; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid white; + position: absolute; + bottom: -4px; + left: 10px; + display: none; + } + } + + &.dropup .dropdown-toggle { + &:before { + bottom: auto; + top: -3px; + border-top: 7px solid @color-grey-arrow; + border-bottom: 0; + } + + &:after { + bottom: auto; + top: -3px; + border-top: 6px solid white; + border-bottom: 0; + } + } + + &.pull-right .dropdown-toggle { + &:before { + right: 12px; + left: auto; + } + + &:after { + right: 13px; + left: auto; + } + } + + &.open > .dropdown-toggle { + &:before, + &:after { + display: block; + } + } +} + +.bs-searchbox, +.bs-actionsbox, +.bs-donebutton { + padding: 4px 8px; +} + +.bs-actionsbox { + width: 100%; + box-sizing: border-box; + + & .btn-group button { + width: 50%; + } +} + +.bs-donebutton { + float: left; + width: 100%; + box-sizing: border-box; + + & .btn-group button { + width: 100%; + } +} + +.bs-searchbox { + & + .bs-actionsbox { + padding: 0 8px 4px; + } + + & .form-control { + margin-bottom: 0; + width: 100%; + float: none; + } +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/less/variables.less b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/less/variables.less new file mode 100644 index 0000000000..97a510c23a --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/less/variables.less @@ -0,0 +1,7 @@ +@color-red-error: rgb(185, 74, 72); +@color-blue-hover: rgba(100, 177, 216, 0.4); +@color-grey-arrow: rgba(204, 204, 204, 0.2); + +@width-default: 348px; // 3 960px-grid columns + +@zindex-select-dropdown: 1035; // must be lower than a modal background (1040) but higher than the fixed navbar (1030) diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/sass/bootstrap-select.css b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/sass/bootstrap-select.css new file mode 100644 index 0000000000..b6c2903d8d --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/sass/bootstrap-select.css @@ -0,0 +1 @@ +@keyframes bs-notify-fadeOut{0%{opacity:0.9}100%{opacity:0}}select.bs-select-hidden,.bootstrap-select>select.bs-select-hidden,select.selectpicker{display:none !important}.bootstrap-select{width:220px \0;vertical-align:middle}.bootstrap-select>.dropdown-toggle{position:relative;width:100%;text-align:right;white-space:nowrap;display:inline-flex;align-items:center;justify-content:space-between}.bootstrap-select>.dropdown-toggle:after{margin-top:-1px}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:active{color:#999}.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:active{color:rgba(255,255,255,0.5)}.bootstrap-select>select{position:absolute !important;bottom:0;left:50%;display:block !important;width:0.5px !important;height:100% !important;padding:0 !important;opacity:0 !important;border:none;z-index:0 !important}.bootstrap-select>select.mobile-device{top:0;left:0;display:block !important;width:100% !important;z-index:2 !important}.has-error .bootstrap-select .dropdown-toggle,.error .bootstrap-select .dropdown-toggle,.bootstrap-select.is-invalid .dropdown-toggle,.was-validated .bootstrap-select select:invalid+.dropdown-toggle{border-color:#b94a48}.bootstrap-select.is-valid .dropdown-toggle,.was-validated .bootstrap-select select:valid+.dropdown-toggle{border-color:#28a745}.bootstrap-select.fit-width{width:auto !important}.bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn){width:220px}.bootstrap-select>select.mobile-device:focus+.dropdown-toggle{outline:thin dotted #333333 !important;outline:5px auto -webkit-focus-ring-color !important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none;height:auto}:not(.input-group)>.bootstrap-select.form-control:not([class*="col-"]){width:100%}.bootstrap-select.form-control.input-group-btn{float:none;z-index:auto}.form-inline .bootstrap-select,.form-inline .bootstrap-select.form-control:not([class*="col-"]){width:auto}.bootstrap-select:not(.input-group-btn),.bootstrap-select[class*="col-"]{float:none;display:inline-block;margin-left:0}.bootstrap-select.dropdown-menu-right,.bootstrap-select[class*="col-"].dropdown-menu-right,.row .bootstrap-select[class*="col-"].dropdown-menu-right{float:right}.form-inline .bootstrap-select,.form-horizontal .bootstrap-select,.form-group .bootstrap-select{margin-bottom:0}.form-group-lg .bootstrap-select.form-control,.form-group-sm .bootstrap-select.form-control{padding:0}.form-group-lg .bootstrap-select.form-control .dropdown-toggle,.form-group-sm .bootstrap-select.form-control .dropdown-toggle{height:100%;font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-sm .dropdown-toggle,.bootstrap-select.form-control-lg .dropdown-toggle{font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-sm .dropdown-toggle{padding:.25rem .5rem}.bootstrap-select.form-control-lg .dropdown-toggle{padding:.5rem 1rem}.form-inline .bootstrap-select .form-control{width:100%}.bootstrap-select.disabled,.bootstrap-select>.disabled{cursor:not-allowed}.bootstrap-select.disabled:focus,.bootstrap-select>.disabled:focus{outline:none !important}.bootstrap-select.bs-container{position:absolute;top:0;left:0;height:0 !important;padding:0 !important}.bootstrap-select.bs-container .dropdown-menu{z-index:9998}.bootstrap-select .dropdown-toggle .filter-option{position:static;top:0;left:0;float:left;height:100%;width:100%;text-align:left;overflow:hidden;flex:0 1 auto}.bs3.bootstrap-select .dropdown-toggle .filter-option{padding-right:inherit}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option{position:absolute;padding-top:inherit;padding-bottom:inherit;padding-left:inherit;float:none}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option .filter-option-inner{padding-right:inherit}.bootstrap-select .dropdown-toggle .filter-option-inner-inner{overflow:hidden}.bootstrap-select .dropdown-toggle .filter-expand{width:0 !important;float:left;opacity:0 !important;overflow:hidden}.bootstrap-select .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.input-group .bootstrap-select.form-control .dropdown-toggle{border-radius:inherit}.bootstrap-select[class*="col-"] .dropdown-toggle{width:100%}.bootstrap-select .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu>.inner:focus{outline:none !important}.bootstrap-select .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;box-shadow:none}.bootstrap-select .dropdown-menu li{position:relative}.bootstrap-select .dropdown-menu li.active small{color:rgba(255,255,255,0.5) !important}.bootstrap-select .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select .dropdown-menu li a{cursor:pointer;user-select:none}.bootstrap-select .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select .dropdown-menu li a span.check-mark{display:none}.bootstrap-select .dropdown-menu li a span.text{display:inline-block}.bootstrap-select .dropdown-menu li small{padding-left:0.5em}.bootstrap-select .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);pointer-events:none;opacity:0.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu .notify.fadeOut{animation:300ms linear 750ms forwards bs-notify-fadeOut}.bootstrap-select .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.fit-width .dropdown-toggle .filter-option{position:static;display:inline;padding:0}.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner,.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner{display:inline}.bootstrap-select.fit-width .dropdown-toggle .bs-caret:before{content:'\00a0'}.bootstrap-select.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark{position:absolute;display:inline-block;right:15px;top:5px}.bootstrap-select.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select .bs-ok-default:after{content:'';display:block;width:0.5em;height:1em;border-style:solid;border-width:0 0.26em 0.26em 0;transform:rotate(45deg)}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle{z-index:9999}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,0.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid white;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before{bottom:auto;top:-4px;border-top:7px solid rgba(204,204,204,0.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after{bottom:auto;top:-4px;border-top:6px solid white;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:before,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:before,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:after{display:block}.bs-searchbox,.bs-actionsbox,.bs-donebutton{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/sass/bootstrap-select.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/sass/bootstrap-select.scss new file mode 100644 index 0000000000..32b852e26f --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/sass/bootstrap-select.scss @@ -0,0 +1,519 @@ +@import "variables"; + +@keyframes bs-notify-fadeOut { + 0% {opacity: 0.9;} + 100% {opacity: 0;} +} + +// Mixins +@mixin cursor-disabled() { + cursor: not-allowed; +} + +@mixin box-sizing($fmt) { + -webkit-box-sizing: $fmt; + -moz-box-sizing: $fmt; + box-sizing: $fmt; +} + +@mixin box-shadow($fmt) { + -webkit-box-shadow: $fmt; + box-shadow: $fmt; +} + +@function fade($color, $amnt) { + @if $amnt > 1 { + $amnt: $amnt / 100; // convert to percentage if int + } + @return rgba($color, $amnt); +} + +// Rules +select.bs-select-hidden, +.bootstrap-select > select.bs-select-hidden, +select.selectpicker { + display: none !important; +} + +.bootstrap-select { + width: 220px \0; /*IE9 and below*/ + vertical-align: middle; + + // The selectpicker button + > .dropdown-toggle { + position: relative; + width: 100%; + // necessary for proper positioning of caret in Bootstrap 4 (pushes caret to the right) + text-align: right; + white-space: nowrap; + // force caret to be vertically centered for Bootstrap 4 multi-line buttons + display: inline-flex; + align-items: center; + justify-content: space-between; + + &:after { + margin-top: -1px; + } + + &.bs-placeholder { + &, + &:hover, + &:focus, + &:active { + color: $input-color-placeholder; + } + + &.btn-primary, + &.btn-secondary, + &.btn-success, + &.btn-danger, + &.btn-info, + &.btn-dark { + &, + &:hover, + &:focus, + &:active { + color: $input-alt-color-placeholder; + } + } + } + } + + > select { + position: absolute !important; + bottom: 0; + left: 50%; + display: block !important; + width: 0.5px !important; + height: 100% !important; + padding: 0 !important; + opacity: 0 !important; + border: none; + z-index: 0 !important; + + &.mobile-device { + top: 0; + left: 0; + display: block !important; + width: 100% !important; + z-index: 2 !important; + } + } + + // Error display + .has-error & .dropdown-toggle, + .error & .dropdown-toggle, + &.is-invalid .dropdown-toggle, + .was-validated & select:invalid + .dropdown-toggle { + border-color: $color-red-error; + } + + &.is-valid .dropdown-toggle, + .was-validated & select:valid + .dropdown-toggle { + border-color: $color-green-success; + } + + &.fit-width { + width: auto !important; + } + + &:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) { + width: $width-default; + } + + > select.mobile-device:focus + .dropdown-toggle { + outline: thin dotted #333333 !important; + outline: 5px auto -webkit-focus-ring-color !important; + outline-offset: -2px; + } +} + +// The selectpicker components +.bootstrap-select { + &.form-control { + margin-bottom: 0; + padding: 0; + border: none; + height: auto; + + :not(.input-group) > &:not([class*="col-"]) { + width: 100%; + } + + &.input-group-btn { + float: none; + z-index: auto; + } + } + + .form-inline &, + .form-inline &.form-control:not([class*="col-"]) { + width: auto; + } + + &:not(.input-group-btn), + &[class*="col-"] { + float: none; + display: inline-block; + margin-left: 0; + } + + // Forces the pull to the right, if necessary + &, + &[class*="col-"], + .row &[class*="col-"] { + &.dropdown-menu-right { + float: right; + } + } + + .form-inline &, + .form-horizontal &, + .form-group & { + margin-bottom: 0; + } + + .form-group-lg &.form-control, + .form-group-sm &.form-control { + padding: 0; + + .dropdown-toggle { + height: 100%; + font-size: inherit; + line-height: inherit; + border-radius: inherit; + } + } + + &.form-control-sm .dropdown-toggle, + &.form-control-lg .dropdown-toggle { + font-size: inherit; + line-height: inherit; + border-radius: inherit; + } + + &.form-control-sm .dropdown-toggle { + padding: $input-padding-y-sm $input-padding-x-sm; + } + + &.form-control-lg .dropdown-toggle { + padding: $input-padding-y-lg $input-padding-x-lg; + } + + // Set the width of the live search (and any other form control within an inline form) + // see https://github.com/silviomoreto/bootstrap-select/issues/685 + .form-inline & .form-control { + width: 100%; + } + + &.disabled, + > .disabled { + @include cursor-disabled(); + + &:focus { + outline: none !important; + } + } + + &.bs-container { + position: absolute; + top: 0; + left: 0; + height: 0 !important; + padding: 0 !important; + + .dropdown-menu { + z-index: $zindex-select-dropdown; + } + } + + // The selectpicker button + .dropdown-toggle { + .filter-option { + position: static; + top: 0; + left: 0; + float: left; + height: 100%; + width: 100%; + text-align: left; + overflow: hidden; + flex: 0 1 auto; // for IE10 + + @at-root .bs3#{&} { + padding-right: inherit; + } + + @at-root .input-group .bs3-has-addon#{&} { + position: absolute; + padding-top: inherit; + padding-bottom: inherit; + padding-left: inherit; + float: none; + + .filter-option-inner { + padding-right: inherit; + } + } + } + + .filter-option-inner-inner { + overflow: hidden; + } + + // used to expand the height of the button when inside an input group + .filter-expand { + width: 0 !important; + float: left; + opacity: 0 !important; + overflow: hidden; + } + + .caret { + position: absolute; + top: 50%; + right: 12px; + margin-top: -2px; + vertical-align: middle; + } + } + + .input-group &.form-control .dropdown-toggle { + border-radius: inherit; + } + + &[class*="col-"] .dropdown-toggle { + width: 100%; + } + + // The selectpicker dropdown + .dropdown-menu { + min-width: 100%; + @include box-sizing(border-box); + + > .inner:focus { + outline: none !important; + } + + &.inner { + position: static; + float: none; + border: 0; + padding: 0; + margin: 0; + border-radius: 0; + box-shadow: none; + } + + li { + position: relative; + + &.active small { + color: $input-alt-color-placeholder !important; + } + + &.disabled a { + @include cursor-disabled(); + } + + a { + cursor: pointer; + user-select: none; + + &.opt { + position: relative; + padding-left: 2.25em; + } + + span.check-mark { + display: none; + } + + span.text { + display: inline-block; + } + } + + small { + padding-left: 0.5em; + } + } + + .notify { + position: absolute; + bottom: 5px; + width: 96%; + margin: 0 2%; + min-height: 26px; + padding: 3px 5px; + background: rgb(245, 245, 245); + border: 1px solid rgb(227, 227, 227); + @include box-shadow(inset 0 1px 1px fade(rgb(0, 0, 0), 5)); + pointer-events: none; + opacity: 0.9; + @include box-sizing(border-box); + + &.fadeOut { + animation: 300ms linear 750ms forwards bs-notify-fadeOut; + } + } + } + + .no-results { + padding: 3px; + background: #f5f5f5; + margin: 0 5px; + white-space: nowrap; + } + + &.fit-width .dropdown-toggle { + .filter-option { + position: static; + display: inline; + padding: 0; + } + + .filter-option-inner, + .filter-option-inner-inner { + display: inline; + } + + .bs-caret:before { + content: '\00a0'; + } + + .caret { + position: static; + top: auto; + margin-top: -1px; + } + } + + &.show-tick .dropdown-menu { + .selected span.check-mark { + position: absolute; + display: inline-block; + right: 15px; + top: 5px; + } + + li a span.text { + margin-right: 34px; + } + } + + // default check mark for use without an icon font + .bs-ok-default:after { + content: ''; + display: block; + width: 0.5em; + height: 1em; + border-style: solid; + border-width: 0 0.26em 0.26em 0; + transform: rotate(45deg); + } +} + +.bootstrap-select.show-menu-arrow { + &.open > .dropdown-toggle, + &.show > .dropdown-toggle { + z-index: ($zindex-select-dropdown + 1); + } + + .dropdown-toggle .filter-option { + &:before { + content: ''; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid $color-grey-arrow; + position: absolute; + bottom: -4px; + left: 9px; + display: none; + } + + &:after { + content: ''; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid white; + position: absolute; + bottom: -4px; + left: 10px; + display: none; + } + } + + &.dropup .dropdown-toggle .filter-option { + &:before { + bottom: auto; + top: -4px; + border-top: 7px solid $color-grey-arrow; + border-bottom: 0; + } + + &:after { + bottom: auto; + top: -4px; + border-top: 6px solid white; + border-bottom: 0; + } + } + + &.pull-right .dropdown-toggle .filter-option { + &:before { + right: 12px; + left: auto; + } + + &:after { + right: 13px; + left: auto; + } + } + + &.open > .dropdown-toggle .filter-option, + &.show > .dropdown-toggle .filter-option { + &:before, + &:after { + display: block; + } + } +} + +.bs-searchbox, +.bs-actionsbox, +.bs-donebutton { + padding: 4px 8px; +} + +.bs-actionsbox { + width: 100%; + @include box-sizing(border-box); + + & .btn-group button { + width: 50%; + } +} + +.bs-donebutton { + float: left; + width: 100%; + @include box-sizing(border-box); + + & .btn-group button { + width: 100%; + } +} + +.bs-searchbox { + & + .bs-actionsbox { + padding: 0 8px 4px; + } + + & .form-control { + margin-bottom: 0; + width: 100%; + float: none; + } +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/sass/variables.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/sass/variables.scss new file mode 100644 index 0000000000..6e9c8af262 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap-select/sass/variables.scss @@ -0,0 +1,17 @@ +$color-red-error: rgb(185, 74, 72) !default; +$color-green-success: #28a745; +$color-grey-arrow: rgba(204, 204, 204, 0.2) !default; + +$width-default: 220px !default; // 3 960px-grid columns + +$zindex-select-dropdown: 9998 !default; // must be higher than a modal background (1050) + +//** Placeholder text color +$input-color-placeholder: #999 !default; +$input-alt-color-placeholder: rgba(255, 255, 255, 0.5) !default; + +$input-padding-y-sm: .25rem !default; +$input-padding-x-sm: .5rem !default; + +$input-padding-y-lg: 0.5rem !default; +$input-padding-x-lg: 1rem !default; diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_alerts.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_alerts.scss new file mode 100644 index 0000000000..e45de83058 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_alerts.scss @@ -0,0 +1,68 @@ +// +// Alerts +// -------------------------------------------------- + + +// Base styles +// ------------------------- + +.alert { + padding: $alert-padding; + margin-bottom: $line-height-computed; + border: 1px solid transparent; + border-radius: $alert-border-radius; + + // Headings for larger alerts + h4 { + margin-top: 0; + // Specified for the h4 to prevent conflicts of changing $headings-color + color: inherit; + } + // Provide class for links that match alerts + .alert-link { + font-weight: $alert-link-font-weight; + } + + // Improve alignment and spacing of inner content + > p, + > ul { + margin-bottom: 0; + } + > p + p { + margin-top: 5px; + } +} + +// Dismissible alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. +.alert-dismissible { + padding-right: ($alert-padding + 20); + + // Adjust close link position + .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; + } +} + +// Alternate styles +// +// Generate contextual modifier classes for colorizing the alert. + +.alert-success { + @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text); +} +.alert-info { + @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text); +} +.alert-warning { + @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text); +} +.alert-danger { + @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text); +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_badges.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_badges.scss new file mode 100644 index 0000000000..02394ae7fa --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_badges.scss @@ -0,0 +1,57 @@ +// +// Badges +// -------------------------------------------------- + + +// Base class +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: $font-size-small; + font-weight: $badge-font-weight; + color: $badge-color; + line-height: $badge-line-height; + vertical-align: baseline; + white-space: nowrap; + text-align: center; + background-color: $badge-bg; + border-radius: $badge-border-radius; + + // Empty badges collapse automatically (not available in IE8) + &:empty { + display: none; + } + + // Quick fix for badges in buttons + .btn & { + position: relative; + top: -1px; + } + .btn-xs & { + top: 0; + padding: 1px 5px; + } + + // [converter] extracted a& to a.badge + + // Account for badges in navs + a.list-group-item.active > &, + .nav-pills > .active > a > & { + color: $badge-active-color; + background-color: $badge-active-bg; + } + .nav-pills > li > a > & { + margin-left: 3px; + } +} + +// Hover state, but only for links +a.badge { + &:hover, + &:focus { + color: $badge-link-hover-color; + text-decoration: none; + cursor: pointer; + } +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_breadcrumbs.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_breadcrumbs.scss new file mode 100644 index 0000000000..3641e333b8 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_breadcrumbs.scss @@ -0,0 +1,26 @@ +// +// Breadcrumbs +// -------------------------------------------------- + + +.breadcrumb { + padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal; + margin-bottom: $line-height-computed; + list-style: none; + background-color: $breadcrumb-bg; + border-radius: $border-radius-base; + + > li { + display: inline-block; + + + li:before { + content: "#{$breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space + padding: 0 5px; + color: $breadcrumb-color; + } + } + + > .active { + color: $breadcrumb-active-color; + } +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_button-groups.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_button-groups.scss new file mode 100644 index 0000000000..63ccd927c8 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_button-groups.scss @@ -0,0 +1,240 @@ +// +// Button groups +// -------------------------------------------------- + +// Make the div behave like a button +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; // match .btn alignment given font-size hack above + > .btn { + position: relative; + float: left; + // Bring the "active" button to the front + &:hover, + &:focus, + &:active, + &.active { + z-index: 2; + } + &:focus { + // Remove focus outline when dropdown JS adds it after closing the menu + outline: 0; + } + } +} + +// Prevent double borders when buttons are next to each other +.btn-group { + .btn + .btn, + .btn + .btn-group, + .btn-group + .btn, + .btn-group + .btn-group { + margin-left: -1px; + } +} + +// Optional: Group multiple button groups together for a toolbar +.btn-toolbar { + margin-left: -5px; // Offset the first child's margin + @include clearfix(); + + .btn-group, + .input-group { + float: left; + } + > .btn, + > .btn-group, + > .input-group { + margin-left: 5px; + } +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match +.btn-group > .btn:first-child { + margin-left: 0; + &:not(:last-child):not(.dropdown-toggle) { + @include border-right-radius(0); + } +} +// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + @include border-left-radius(0); +} + +// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child { + > .btn:last-child, + > .dropdown-toggle { + @include border-right-radius(0); + } +} +.btn-group > .btn-group:last-child > .btn:first-child { + @include border-left-radius(0); +} + +// On active and open, don't show outline +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-xs > .btn { @extend .btn-xs; } +.btn-group-sm > .btn { @extend .btn-sm; } +.btn-group-lg > .btn { @extend .btn-lg; } + + +// Split button dropdowns +// ---------------------- + +// Give the line between buttons some depth +.btn-group > .btn + .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} + +// The clickable button for toggling the menu +// Remove the gradient and set the same inset shadow as the :active state +.btn-group.open .dropdown-toggle { + @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + @include box-shadow(none); + } +} + + +// Reposition the caret +.btn .caret { + margin-left: 0; +} +// Carets in other button sizes +.btn-lg .caret { + border-width: $caret-width-large $caret-width-large 0; + border-bottom-width: 0; +} +// Upside down carets for .dropup +.dropup .btn-lg .caret { + border-width: 0 $caret-width-large $caret-width-large; +} + + +// Vertical button groups +// ---------------------- + +.btn-group-vertical { + > .btn, + > .btn-group, + > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; + } + + // Clear floats so dropdown menus can be properly placed + > .btn-group { + @include clearfix(); + > .btn { + float: none; + } + } + + > .btn + .btn, + > .btn + .btn-group, + > .btn-group + .btn, + > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; + } +} + +.btn-group-vertical > .btn { + &:not(:first-child):not(:last-child) { + border-radius: 0; + } + &:first-child:not(:last-child) { + border-top-right-radius: $border-radius-base; + @include border-bottom-radius(0); + } + &:last-child:not(:first-child) { + border-bottom-left-radius: $border-radius-base; + @include border-top-radius(0); + } +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) { + > .btn:last-child, + > .dropdown-toggle { + @include border-bottom-radius(0); + } +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + @include border-top-radius(0); +} + + + +// Justified button groups +// ---------------------- + +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; + > .btn, + > .btn-group { + float: none; + display: table-cell; + width: 1%; + } + > .btn-group .btn { + width: 100%; + } + + > .btn-group .dropdown-menu { + left: auto; + } +} + + +// Checkbox and radio options +// +// In order to support the browser's form validation feedback, powered by the +// `required` attribute, we have to "hide" the inputs via `opacity`. We cannot +// use `display: none;` or `visibility: hidden;` as that also hides the popover. +// This way, we ensure a DOM element is visible to position the popover from. +// +// See https://github.com/twbs/bootstrap/pull/12794 for more. + +[data-toggle="buttons"] > .btn > input[type="radio"], +[data-toggle="buttons"] > .btn > input[type="checkbox"] { + position: absolute; + z-index: -1; + @include opacity(0); +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_buttons.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_buttons.scss new file mode 100644 index 0000000000..f2684c1b76 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_buttons.scss @@ -0,0 +1,159 @@ +// +// Buttons +// -------------------------------------------------- + + +// Base styles +// -------------------------------------------------- + +.btn { + display: inline-block; + margin-bottom: 0; // For input.btn + font-weight: $btn-font-weight; + text-align: center; + vertical-align: middle; + cursor: pointer; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid transparent; + white-space: nowrap; + @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $border-radius-base); + @include user-select(none); + + @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border); + + &, + &:active, + &.active { + &:focus { + @include tab-focus(); + } + } + + &:hover, + &:focus { + color: $btn-default-color; + text-decoration: none; + } + + &:active, + &.active { + outline: 0; + background-image: none; + @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + cursor: not-allowed; + pointer-events: none; // Future-proof disabling of clicks + @include opacity(.65); + @include box-shadow(none); + } +} + + +// Alternate buttons +// -------------------------------------------------- + +.btn-default { + @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border); +} +.btn-primary { + @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border); +} +// Success appears as green +.btn-success { + @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border); +} +// Info appears as blue-green +.btn-info { + @include button-variant($btn-info-color, $btn-info-bg, $btn-info-border); +} +// Warning appears as orange +.btn-warning { + @include button-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border); +} +// Danger and error appear as red +.btn-danger { + @include button-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border); +} + + +// Link buttons +// ------------------------- + +// Make a button look and behave like a link +.btn-link { + color: $link-color; + font-weight: normal; + cursor: pointer; + border-radius: 0; + + &, + &:active, + &[disabled], + fieldset[disabled] & { + background-color: transparent; + @include box-shadow(none); + } + &, + &:hover, + &:focus, + &:active { + border-color: transparent; + } + &:hover, + &:focus { + color: $link-hover-color; + text-decoration: underline; + background-color: transparent; + } + &[disabled], + fieldset[disabled] & { + &:hover, + &:focus { + color: $btn-link-disabled-color; + text-decoration: none; + } + } +} + + +// Button Sizes +// -------------------------------------------------- + +.btn-lg { + // line-height: ensure even-numbered height of button next to large input + @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $border-radius-large); +} +.btn-sm { + // line-height: ensure proper height of button next to small input + @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $border-radius-small); +} +.btn-xs { + @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $border-radius-small); +} + + +// Block button +// -------------------------------------------------- + +.btn-block { + display: block; + width: 100%; +} + +// Vertically space out multiple block buttons +.btn-block + .btn-block { + margin-top: 5px; +} + +// Specificity overrides +input[type="submit"], +input[type="reset"], +input[type="button"] { + &.btn-block { + width: 100%; + } +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_carousel.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_carousel.scss new file mode 100644 index 0000000000..e9e2f7ce1e --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_carousel.scss @@ -0,0 +1,243 @@ +// +// Carousel +// -------------------------------------------------- + + +// Wrapper for the slide container and indicators +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + overflow: hidden; + width: 100%; + + > .item { + display: none; + position: relative; + @include transition(.6s ease-in-out left); + + // Account for jankitude on images + > img, + > a > img { + @include img-responsive(); + line-height: 1; + } + } + + > .active, + > .next, + > .prev { + display: block; + } + + > .active { + left: 0; + } + + > .next, + > .prev { + position: absolute; + top: 0; + width: 100%; + } + + > .next { + left: 100%; + } + > .prev { + left: -100%; + } + > .next.left, + > .prev.right { + left: 0; + } + + > .active.left { + left: -100%; + } + > .active.right { + left: 100%; + } + +} + +// Left/right controls for nav +// --------------------------- + +.carousel-control { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: $carousel-control-width; + @include opacity($carousel-control-opacity); + font-size: $carousel-control-font-size; + color: $carousel-control-color; + text-align: center; + text-shadow: $carousel-text-shadow; + // We can't have this transition here because WebKit cancels the carousel + // animation if you trip this while in the middle of another animation. + + // Set gradients for backgrounds + &.left { + @include gradient-horizontal($start-color: rgba(0,0,0,.5), $end-color: rgba(0,0,0,.0001)); + } + &.right { + left: auto; + right: 0; + @include gradient-horizontal($start-color: rgba(0,0,0,.0001), $end-color: rgba(0,0,0,.5)); + } + + // Hover/focus state + &:hover, + &:focus { + outline: 0; + color: $carousel-control-color; + text-decoration: none; + @include opacity(.9); + } + + // Toggles + .icon-prev, + .icon-next, + .glyphicon-chevron-left, + .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + } + .icon-prev, + .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; + } + .icon-next, + .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; + } + .icon-prev, + .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + font-family: serif; + } + + + .icon-prev { + &:before { + content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) + } + } + .icon-next { + &:before { + content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) + } + } +} + +// Optional indicator pips +// +// Add an unordered list with the following class and add a list item for each +// slide your carousel holds. + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + margin-left: -30%; + padding-left: 0; + list-style: none; + text-align: center; + + li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + border: 1px solid $carousel-indicator-border-color; + border-radius: 10px; + cursor: pointer; + + // IE8-9 hack for event handling + // + // Internet Explorer 8-9 does not support clicks on elements without a set + // `background-color`. We cannot use `filter` since that's not viewed as a + // background color by the browser. Thus, a hack is needed. + // + // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we + // set alpha transparency for the best results possible. + background-color: #000 \9; // IE8 + background-color: rgba(0,0,0,0); // IE9 + } + .active { + margin: 0; + width: 12px; + height: 12px; + background-color: $carousel-indicator-active-bg; + } +} + +// Optional captions +// ----------------------------- +// Hidden by default for smaller viewports +.carousel-caption { + position: absolute; + left: 15%; + right: 15%; + bottom: 20px; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: $carousel-caption-color; + text-align: center; + text-shadow: $carousel-text-shadow; + & .btn { + text-shadow: none; // No shadow for button elements in carousel-caption + } +} + + +// Scale up controls for tablets and up +@media screen and (min-width: $screen-sm-min) { + + // Scale up the controls a smidge + .carousel-control { + .glyphicon-chevron-left, + .glyphicon-chevron-right, + .icon-prev, + .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + font-size: 30px; + } + .glyphicon-chevron-left, + .icon-prev { + margin-left: -15px; + } + .glyphicon-chevron-right, + .icon-next { + margin-right: -15px; + } + } + + // Show and left align the captions + .carousel-caption { + left: 20%; + right: 20%; + padding-bottom: 30px; + } + + // Move up the indicators + .carousel-indicators { + bottom: 20px; + } +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_close.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_close.scss new file mode 100644 index 0000000000..62ce30fa37 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_close.scss @@ -0,0 +1,35 @@ +// +// Close icons +// -------------------------------------------------- + + +.close { + float: right; + font-size: ($font-size-base * 1.5); + font-weight: $close-font-weight; + line-height: 1; + color: $close-color; + text-shadow: $close-text-shadow; + @include opacity(.2); + + &:hover, + &:focus { + color: $close-color; + text-decoration: none; + cursor: pointer; + @include opacity(.5); + } + + // [converter] extracted button& to button.close +} + +// Additional properties for button version +// iOS requires the button element instead of an anchor tag. +// If you want the anchor version, it requires `href="#"`. +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_code.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_code.scss new file mode 100644 index 0000000000..3a434b946b --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_code.scss @@ -0,0 +1,68 @@ +// +// Code (inline and block) +// -------------------------------------------------- + + +// Inline and block code styles +code, +kbd, +pre, +samp { + font-family: $font-family-monospace; +} + +// Inline code +code { + padding: 2px 4px; + font-size: 90%; + color: $code-color; + background-color: $code-bg; + border-radius: $border-radius-base; +} + +// User input typically entered via keyboard +kbd { + padding: 2px 4px; + font-size: 90%; + color: $kbd-color; + background-color: $kbd-bg; + border-radius: $border-radius-small; + box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); + + kbd { + padding: 0; + font-size: 100%; + box-shadow: none; + } +} + +// Blocks of code +pre { + display: block; + padding: calc(($line-height-computed - 100%) / 2); + margin: 0 0 calc($line-height-computed / 2); + font-size: ($font-size-base - 1); // 14px to 13px + line-height: $line-height-base; + word-break: break-all; + word-wrap: break-word; + color: $pre-color; + background-color: $pre-bg; + border: 1px solid $pre-border-color; + border-radius: $border-radius-base; + + // Account for some code outputs that place code tags in pre tags + code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; + } +} + +// Enable scrollable blocks of code +.pre-scrollable { + max-height: $pre-scrollable-max-height; + overflow-y: scroll; +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_component-animations.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_component-animations.scss new file mode 100644 index 0000000000..8c3fd07a27 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_component-animations.scss @@ -0,0 +1,35 @@ +// +// Component animations +// -------------------------------------------------- + +// Heads up! +// +// We don't use the `.opacity()` mixin here since it causes a bug with text +// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. + +.fade { + opacity: 0; + @include transition(opacity .15s linear); + &.in { + opacity: 1; + } +} + +.collapse { + display: none; + + &.in { display: block; } + // [converter] extracted tr&.in to tr.collapse.in + // [converter] extracted tbody&.in to tbody.collapse.in +} + +tr.collapse.in { display: table-row; } + +tbody.collapse.in { display: table-row-group; } + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + @include transition(height .35s ease); +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_dropdowns.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_dropdowns.scss new file mode 100644 index 0000000000..df50ec0cbc --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_dropdowns.scss @@ -0,0 +1,214 @@ +// +// Dropdown menus +// -------------------------------------------------- + + +// Dropdown arrow/caret +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: $caret-width-base solid; + border-right: $caret-width-base solid transparent; + border-left: $caret-width-base solid transparent; +} + +// The dropdown wrapper (div) +.dropdown { + position: relative; +} + +// Prevent the focus on the dropdown toggle when closing dropdowns +.dropdown-toggle:focus { + outline: 0; +} + +// The dropdown menu (ul) +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: $zindex-dropdown; + display: none; // none by default, but block on "open" of the menu + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; // override default ul + list-style: none; + font-size: $font-size-base; + text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) + background-color: $dropdown-bg; + border: 1px solid $dropdown-fallback-border; // IE8 fallback + border: 1px solid $dropdown-border; + border-radius: $border-radius-base; + @include box-shadow(0 6px 12px rgba(0,0,0,.175)); + background-clip: padding-box; + + // Aligns the dropdown menu to right + // + // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` + &.pull-right { + right: 0; + left: auto; + } + + // Dividers (basically an hr) within the dropdown + .divider { + @include nav-divider($dropdown-divider-bg); + } + + // Links within the dropdown menu + > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: $line-height-base; + color: $dropdown-link-color; + white-space: nowrap; // prevent links from randomly breaking onto new lines + } +} + +// Hover/Focus state +.dropdown-menu > li > a { + &:hover, + &:focus { + text-decoration: none; + color: $dropdown-link-hover-color; + background-color: $dropdown-link-hover-bg; + } +} + +// Active state +.dropdown-menu > .active > a { + &, + &:hover, + &:focus { + color: $dropdown-link-active-color; + text-decoration: none; + outline: 0; + background-color: $dropdown-link-active-bg; + } +} + +// Disabled state +// +// Gray out text and ensure the hover/focus state remains gray + +.dropdown-menu > .disabled > a { + &, + &:hover, + &:focus { + color: $dropdown-link-disabled-color; + } +} +// Nuke hover/focus effects +.dropdown-menu > .disabled > a { + &:hover, + &:focus { + text-decoration: none; + background-color: transparent; + background-image: none; // Remove CSS gradient + @include reset-filter(); + cursor: not-allowed; + } +} + +// Open state for the dropdown +.open { + // Show the menu + > .dropdown-menu { + display: block; + } + + // Remove the outline when :focus is triggered + > a { + outline: 0; + } +} + +// Menu positioning +// +// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown +// menu with the parent. +.dropdown-menu-right { + left: auto; // Reset the default from `.dropdown-menu` + right: 0; +} +// With v3, we enabled auto-flipping if you have a dropdown within a right +// aligned nav component. To enable the undoing of that, we provide an override +// to restore the default dropdown menu alignment. +// +// This is only for left-aligning a dropdown menu within a `.navbar-right` or +// `.pull-right` nav component. +.dropdown-menu-left { + left: 0; + right: auto; +} + +// Dropdown section headers +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: $font-size-small; + line-height: $line-height-base; + color: $dropdown-header-color; + white-space: nowrap; // as with > li > a +} + +// Backdrop to catch body clicks on mobile, etc. +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: ($zindex-dropdown - 10); +} + +// Right aligned dropdowns +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// +// Just add .dropup after the standard .dropdown class and you're set, bro. +// TODO: abstract this so that the navbar fixed styles are not placed here? + +.dropup, +.navbar-fixed-bottom .dropdown { + // Reverse the caret + .caret { + border-top: 0; + border-bottom: $caret-width-base solid; + content: ""; + } + // Different positioning for bottom up menu + .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; + } +} + + +// Component alignment +// +// Reiterate per navbar.less and the modified component alignment there. + +@media (min-width: $grid-float-breakpoint) { + .navbar-right { + .dropdown-menu { + right: 0; left: auto; + } + // Necessary for overrides of the default right aligned menu. + // Will remove come v4 in all likelihood. + .dropdown-menu-left { + left: 0; right: auto; + } + } +} diff --git a/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_forms.scss b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_forms.scss new file mode 100644 index 0000000000..cd5e5f1bf4 --- /dev/null +++ b/misc/theme-advanced/src/opnsense/www/themes/advanced/assets/stylesheets/bootstrap/_forms.scss @@ -0,0 +1,541 @@ +// +// Forms +// -------------------------------------------------- + + +// Normalize non-controls +// +// Restyle and baseline non-control form elements. + +fieldset { + padding: 0; + margin: 0; + border: 0; + // Chrome and Firefox set a min-width: min-content; on fieldsets, + // so we reset that to ensure it behaves more like a standard block element. + // See https://github.com/twbs/bootstrap/issues/12359. + min-width: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: $line-height-computed; + font-size: ($font-size-base * 1.5); + line-height: inherit; + color: $legend-color; + border: 0; + border-bottom: 1px solid $legend-border-color; +} + +label { + display: inline-block; + max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141) + margin-bottom: 5px; + font-weight: normal; +} + + +// Normalize form controls +// +// While most of our form styles require extra classes, some basic normalization +// is required to ensure optimum display with or without those classes to better +// address browser inconsistencies. + +// Override content-box in Normalize (isnt specific enough) +input[type="search"] { + @include box-sizing(border-box); +} + +// Position radios and checkboxes better +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; // IE8-9 + line-height: normal; +} + +// Set the height of file controls to match text inputs +input[type="file"] { + display: block; +} + +// Make range inputs behave like textual form controls +input[type="range"] { + display: block; + width: 100%; +} + +// Make multiple select elements height not fixed +select[multiple], +select[size] { + height: auto; +} + +// Focus for file, radio, and checkbox +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + @include tab-focus(); +} + +// Adjust output element +output { + display: block; + padding-top: ($padding-base-vertical + 1); + font-size: $font-size-base; + line-height: $line-height-base; + color: $input-color; +} + + +// Common form controls +// +// Shared size and type resets for form controls. Apply `.form-control` to any +// of the following form controls: +// +select, +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"] +/* .form-control */ +{ + display: block; + width: 100%; + height: $input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) + padding: $padding-base-vertical $padding-base-horizontal; + font-size: $font-size-base; + line-height: $line-height-base; + color: $input-color; + background-color: $input-bg; + background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 + border: 1px solid $input-border; + border-radius: $input-border-radius; + text-overflow: ellipsis; + max-width:450px; + transition: none; + //@include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); + @include transition(border-color ease-in-out .1s, box-shadow ease-in-out .1s); + + // Customize the `:focus` state to imitate native WebKit styles. + @include form-control-focus(); + + // Placeholder + @include placeholder(); + + // Disabled and read-only inputs + // + // HTML5 says that controls under a fieldset > legend:first-child wont be + // disabled if the fieldset is disabled. Due to implementation difficulty, we + // dont honor that edge case; we style them as disabled anyway. + &[disabled], + &[readonly], + fieldset[disabled] & { + cursor: not-allowed; + background-color: $input-bg-disabled; + opacity: 1; // iOS fix for unreadable disabled content + } + + // [converter] extracted textarea& to textarea.form-control +} + +// Reset height for `textarea`s +textarea{ + height: auto; +} + + +// Search inputs in iOS +// +// This overrides the extra rounded corners on search inputs in iOS so that our +// `.form-control` class can properly style them. Note that this cannot simply +// be added to `.form-control` as its not specific enough. For details, see +// https://github.com/twbs/bootstrap/issues/11586. + +input[type="search"] { + -webkit-appearance: none; +} + + +// Special styles for iOS temporal inputs +// +// In Mobile Safari, setting `display: block` on temporal inputs causes the +// text within the input to become vertically misaligned. +// As a workaround, we set a pixel line-height that matches the +// given height of the input. Since this fucks up everything else, we have to +// appropriately reset it for Internet Explorer and the size variations. + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + line-height: $input-height-base; + // IE8+ misaligns the text within date inputs, so we reset + line-height: $line-height-base #{\0}; + + &.input-sm { + line-height: $input-height-small; + } + &.input-lg { + line-height: $input-height-large; + } +} + + +// Form groups +// +// Designed to help with the organization and spacing of vertical forms. For +// horizontal forms, use the predefined grid classes. + +.form-group { + margin-bottom: 15px; +} + + +// Checkboxes and radios +// +// Indent the labels to position radios/checkboxes as hanging controls. + +.radio, +.checkbox { + position: relative; + display: block; + min-height: $line-height-computed; // clear the floating input if there is no label text + margin-top: 10px; + margin-bottom: 10px; + + label { + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; + } +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-left: -20px; + margin-top: 4px \9; +} + +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing +} + +// Radios and checkboxes on same line +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + vertical-align: middle; + font-weight: normal; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; // space out consecutive inline controls +} + +// Apply same disabled cursor tweak as for inputs +// Some special care is needed because
    + {% endfor %} +
    diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsbgp.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsbgp.volt deleted file mode 100644 index 60c7d72f17..0000000000 --- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsbgp.volt +++ /dev/null @@ -1,145 +0,0 @@ -{# - -OPNsense® is Copyright © 2014 – 2017 by Deciso B.V. -Copyright (C) 2017 Fabian Franz -Copyright (C) 2017 Michael Muenz -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':diagnosticsForm,'id':'frm_diagnostics_settings'])}} -#} - - - - - - - - - -
    -
    -
    -
    -
    
    -  
    -
    -
    
    -  
    -
    diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsgeneral.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsgeneral.volt deleted file mode 100644 index 16c1470190..0000000000 --- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsgeneral.volt +++ /dev/null @@ -1,142 +0,0 @@ -{# - -OPNsense® is Copyright © 2014 – 2017 by Deciso B.V. -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. - -#} - - - - - - - - -
    -
    -
    -
    -
    
    -  
    -
    diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospf.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospf.volt deleted file mode 100644 index 80cf75ff06..0000000000 --- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospf.volt +++ /dev/null @@ -1,475 +0,0 @@ -{# - -OPNsense® is Copyright © 2014 – 2017 by Deciso B.V. -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. - -#} - - - - - - - - - - - - -
    -
    -
    -
    -
    -
    -
    diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospfv3.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospfv3.volt deleted file mode 100644 index 4f4662dbb9..0000000000 --- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/diagnosticsospfv3.volt +++ /dev/null @@ -1,382 +0,0 @@ -{# - -OPNsense® is Copyright © 2014 – 2017 by Deciso B.V. -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. - -#} - - - - - - - - - - - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/general.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/general.volt index 01e1746bd9..0421762229 100644 --- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/general.volt +++ b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/general.volt @@ -1,6 +1,6 @@ {# -OPNsense® is Copyright © 2014 – 2017 by Deciso B.V. +OPNsense® is Copyright © 2014 – 2025 by Deciso B.V. This file is Copyright © 2017 by Fabian Franz All rights reserved. @@ -26,32 +26,36 @@ 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':generalForm,'id':'frm_general_settings'])}} -
    -
    - -
    -
    + +
    + {{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_general_settings'])}} +
    +{{ partial( + 'layout_partials/base_apply_button', + { + 'data_endpoint': '/api/quagga/service/reconfigure', + 'data_service_widget': 'quagga', + 'data_change_message_content': lang._('Apply will reload the service without causing interruptions. Some changes will need a full restart with the available service control buttons.') + } +) }} diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/ospf.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/ospf.volt index 9a25d49132..51d069d388 100644 --- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/ospf.volt +++ b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/ospf.volt @@ -1,6 +1,6 @@ {# -OPNsense® is Copyright © 2014 – 2017 by Deciso B.V. +OPNsense® is Copyright © 2014 – 2025 by Deciso B.V. This file is Copyright © 2017 by Fabian Franz All rights reserved. @@ -27,216 +27,190 @@ POSSIBILITY OF SUCH DAMAGE. #} - - -
    -
    -
    - {{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_ospf_settings'])}} -
    -
    - -
    -
    -
    + - $("#grid-networks").UIBootgrid( - { 'search':'/api/quagga/ospfsettings/searchNetwork', - 'get':'/api/quagga/ospfsettings/getNetwork/', - 'set':'/api/quagga/ospfsettings/setNetwork/', - 'add':'/api/quagga/ospfsettings/addNetwork/', - 'del':'/api/quagga/ospfsettings/delNetwork/', - 'toggle':'/api/quagga/ospfsettings/toggleNetwork/', - 'options':{selection:false, multiSelect:false} + + + + +
    + +
    + {{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_ospf_settings'])}} + {{ partial('layout_partials/base_bootgrid_table', formGridEditRedistribution)}} +
    + +
    + {{ partial('layout_partials/base_bootgrid_table', formGridEditOSPFNeighbor)}} +
    + +
    + {{ partial('layout_partials/base_bootgrid_table', formGridEditOSPFArea)}} +
    + +
    + {{ partial('layout_partials/base_bootgrid_table', formGridEditNetwork)}} +
    + +
    + {{ partial('layout_partials/base_bootgrid_table', formGridEditInterface)}} +
    + +
    + {{ partial('layout_partials/base_bootgrid_table', formGridEditPrefixLists)}} +
    + +
    + {{ partial('layout_partials/base_bootgrid_table', formGridEditRouteMaps)}} +
    +
    +{{ partial( + 'layout_partials/base_apply_button', + { + 'data_endpoint': '/api/quagga/service/reconfigure', + 'data_service_widget': 'quagga', + 'data_change_message_content': lang._('Apply will reload the service without causing interruptions. Some changes will need a full restart with the available service control buttons.') } - ); -}); - - -{{ partial("layout_partials/base_dialog",['fields':formDialogEditNetwork,'id':'DialogEditNetwork','label':lang._('Edit Network')])}} -{{ partial("layout_partials/base_dialog",['fields':formDialogEditInterface,'id':'DialogEditInterface','label':lang._('Edit Interface')])}} -{{ partial("layout_partials/base_dialog",['fields':formDialogEditPrefixLists,'id':'DialogEditPrefixLists','label':lang._('Edit Prefix Lists')])}} -{{ partial("layout_partials/base_dialog",['fields':formDialogEditRouteMaps,'id':'DialogEditRouteMaps','label':lang._('Edit Route Maps')])}} +) }} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditOSPFArea,'id':formGridEditOSPFArea['edit_dialog_id'],'label':lang._('Edit Area')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditOSPFNeighbor,'id':formGridEditOSPFNeighbor['edit_dialog_id'],'label':lang._('Edit Neighbor')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditNetwork,'id':formGridEditNetwork['edit_dialog_id'],'label':lang._('Edit Network')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditInterface,'id':formGridEditInterface['edit_dialog_id'],'label':lang._('Edit Interface')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditPrefixLists,'id':formGridEditPrefixLists['edit_dialog_id'],'label':lang._('Edit Prefix Lists')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditRouteMaps,'id':formGridEditRouteMaps['edit_dialog_id'],'label':lang._('Edit Route Maps')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditRedistribution,'id':formGridEditRedistribution['edit_dialog_id'],'label':lang._('Edit Route Redistribution')])}} diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/ospf6.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/ospf6.volt index e2a8ed98e4..fda09a55d0 100644 --- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/ospf6.volt +++ b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/ospf6.volt @@ -1,125 +1,169 @@ {# + # Copyright (c) 2014-2025 Deciso B.V. + # Copyright (c) 2017 Fabian Franz + # Copyright (c) 2017 Michael Muenz + # 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. + #} -OPNsense® is Copyright © 2014 – 2017 by Deciso B.V. -This file is Copyright © 2017 by Fabian Franz -This file is Copyright © 2017 by Michael Muenz -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: + + + + + +
    + +
    + {{ partial("layout_partials/base_form",['fields':ospf6Form,'id':'frm_ospf6_settings'])}} + {{ partial('layout_partials/base_bootgrid_table', formGridEditRedistribution)}} +
    + +
    + {{ partial('layout_partials/base_bootgrid_table', formGridEditNetwork)}} +
    - - - - - - - - - - - - - - - - - - - -
    {{ lang._('Enabled') }}{{ lang._('Interface Name') }}{{ lang._('Area') }}{{ lang._('Network Type') }}{{ lang._('ID') }}{{ lang._('Commands') }}
    - - - -
    + {{ partial('layout_partials/base_bootgrid_table', formGridEditInterface)}}
    - + +
    + {{ partial('layout_partials/base_bootgrid_table', formGridEditPrefixLists)}}
    - - - -{{ partial("layout_partials/base_dialog",['fields':formDialogEditInterface,'id':'DialogEditInterface','label':lang._('Edit Interface')])}} +) }} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditNetwork,'id':formGridEditNetwork['edit_dialog_id'],'label':lang._('Edit Network')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditInterface,'id':formGridEditInterface['edit_dialog_id'],'label':lang._('Edit Interface')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditPrefixLists,'id':formGridEditPrefixLists['edit_dialog_id'],'label':lang._('Edit Prefix Lists')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditRouteMaps,'id':formGridEditRouteMaps['edit_dialog_id'],'label':lang._('Edit Route Maps')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditRedistribution,'id':formGridEditRedistribution['edit_dialog_id'],'label':lang._('Edit Route Redistribution')])}} diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/rip.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/rip.volt index 8abc7a3f51..332666f2f7 100644 --- a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/rip.volt +++ b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/rip.volt @@ -1,6 +1,6 @@ {# -OPNsense® is Copyright © 2014 – 2017 by Deciso B.V. +OPNsense® is Copyright © 2014 – 2025 by Deciso B.V. This file is Copyright © 2017 by Fabian Franz All rights reserved. @@ -27,33 +27,35 @@ POSSIBILITY OF SUCH DAMAGE. #} -
    - {{ partial("layout_partials/base_form",['fields':ripForm,'id':'frm_rip_settings'])}} -
    -
    - -
    -
    - + +
    + {{ partial("layout_partials/base_form",['fields':ripForm,'id':'frm_rip_settings'])}} +
    +{{ partial( + 'layout_partials/base_apply_button', + { + 'data_endpoint': '/api/quagga/service/reconfigure', + 'data_service_widget': 'quagga', + 'data_change_message_content': lang._('Apply will reload the service without causing interruptions. Some changes will need a full restart with the available service control buttons.') + } +) }} diff --git a/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/static.volt b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/static.volt new file mode 100644 index 0000000000..33c01a198d --- /dev/null +++ b/net/frr/src/opnsense/mvc/app/views/OPNsense/Quagga/static.volt @@ -0,0 +1,82 @@ +{# + # Copyright (c) 2024 Deciso B.V. + # Copyright (c) 2024 Mike Shuey + # 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':staticForm,'id':'frm_static_settings'])}} +
    + +
    + {{ partial('layout_partials/base_bootgrid_table', formGridEditSTATICRoute)}} +
    +
    +{{ partial( + 'layout_partials/base_apply_button', + { + 'data_endpoint': '/api/quagga/service/reconfigure', + 'data_service_widget': 'quagga', + 'data_change_message_content': lang._('Apply will reload the service without causing interruptions. Some changes will need a full restart with the available service control buttons.') + } +) }} +{{ partial("layout_partials/base_dialog",['fields':formDialogEditSTATICRoute,'id':formGridEditSTATICRoute['edit_dialog_id'],'label':lang._('Edit Routes')])}} diff --git a/net/frr/src/opnsense/scripts/frr/carp_event_handler b/net/frr/src/opnsense/scripts/frr/carp_event_handler index 3256478b8b..5baa2f63a3 100755 --- a/net/frr/src/opnsense/scripts/frr/carp_event_handler +++ b/net/frr/src/opnsense/scripts/frr/carp_event_handler @@ -31,7 +31,7 @@ import lib.events from lib import InterfaceStatus, VtySH if __name__ == '__main__': - syslog.openlog('frr_carp', logoption=syslog.LOG_DAEMON, facility=syslog.LOG_LOCAL1) + syslog.openlog('frr_carp', facility=syslog.LOG_LOCAL1) syslog.syslog(syslog.LOG_NOTICE, 'FRR received carp configuration event.') ifstatus = InterfaceStatus() vtysh = VtySH() diff --git a/net/frr/src/opnsense/scripts/frr/frr_wrapper.sh b/net/frr/src/opnsense/scripts/frr/frr_wrapper.sh new file mode 100755 index 0000000000..5b3a812991 --- /dev/null +++ b/net/frr/src/opnsense/scripts/frr/frr_wrapper.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +# Copyright (c) 2025 Andy Binder +# +# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. + +# Service wrapper for starting/restarting frr service +# This wrapper is needed to react on specific service interactions through watchfrr. +# Startup details with watchfrr enabled (default): +# 1. "service frr start" calls "service frr start watchfrr" +# 2. watchfrr once started calls "service frr restart all" +# 3. "restart all" need to loop the list of $frr_daemons to start each +# of then +# 4. vtysh -b is executed to load boot startup configuration + +ACTION="$1" +COMMAND="$2" + +/usr/sbin/service frr "$ACTION" "$COMMAND" +SERVICE_EXIT_CODE=$? + +# If frr starts/restarts ospfd, e.g. on process error (parameter: start/restart ospfd) +if [ "$2" = "ospfd" ]; then + logger -t frr_wrapper "WATCHFRR - OSPFD - Starting CARP event handler now" + /usr/local/opnsense/scripts/frr/carp_event_handler +fi +# If frr starts up (parameter: restart all) +if [ "$2" = "all" ]; then + ( + /usr/bin/logger -t frr_wrapper "WATCHFRR - STARTUP - Starting CARP event handler now" + /usr/local/opnsense/scripts/frr/carp_event_handler + ) & +fi +exit $SERVICE_EXIT_CODE diff --git a/net/frr/src/opnsense/scripts/frr/legacy-diagnostics.py b/net/frr/src/opnsense/scripts/frr/legacy-diagnostics.py deleted file mode 100755 index 73cb70d5ef..0000000000 --- a/net/frr/src/opnsense/scripts/frr/legacy-diagnostics.py +++ /dev/null @@ -1,445 +0,0 @@ -#!/usr/local/bin/python3 -""" - Copyright (c) 2020 Marc Leuser - Copyright (c) 2020 Ad Schellevis - 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. - -""" - -import argparse -import ujson -import re -from typing import List, Dict -from lib import VtySH - - -class Re: - """ Custom regex helper class (source https://stackoverflow.com/a/4980181) - Use to conveniently build switch-case like constructs using if/elif - """ - - def __init__(self): - self.last_match = None - - def match(self, pattern, text): - self.last_match = re.match(pattern, text) - return self.last_match - - def search(self, pattern, text): - self.last_match = re.search(pattern, text) - return self.last_match - - -class FRRTableReader: - def __init__(self, titles: List[str] = None): - self.titles = titles if titles is not None else list() - self.columns = list() - - def read_header(self, line: str, start_without_title: bool = False, start_without_title_name: str = 'status'): - # we're going to create a list of columns. each entry contains title, start index and end index for easy parsing - self.columns = [] - - # the first column's title may sometimes be empty in FRR's output. we'll have to give it a name though - if start_without_title: - self.columns.append({ - 'title': start_without_title_name, - 'start_index': 0, - 'end_index': None - }) - - # fill the list with subsequent columns - for title in self.titles: - try: - # find the start index of the current title in the header - start_index = line.index(title) - except ValueError: - # just skip the column if it can't be found - continue - - # last column's end index is this column's start index - if self.columns: - self.columns[-1]['end_index'] = start_index - - # add the current column to the list - self.columns.append({ - 'title': title, - 'start_index': start_index, - 'end_index': None - }) - - def read_line(self, line: str) -> Dict[str, str]: - result = {} - - # sanity check: the line has to be long enough to contain all columns, - # so its length must be greater than the last column's start index - if len(line) > self.columns[-1]['start_index']: - for column in self.columns: - # use the column's name as dict key and just extract the data from start to end index - result[column['title'].strip()] = line[column['start_index']:column['end_index']].strip() - - return result - - -class DaemonError(Exception): - pass - - -class Daemon: - def __init__(self, vtysh: VtySH): - self.vtysh = vtysh - self.myre = Re() - - def _show(self, suffix: str): - # execute the command and filter out empty lines so subsequent iterations don't have to deal with them - return list(filter(None, self.vtysh.execute(command='show ' + suffix, translate=bytes.decode).split('\n'))) - - -class OSPF(Daemon): - def _show(self, suffix: str): - return super()._show('ip ospf ' + suffix) - - def database(self): - db = {} - # table reader for Route Link States - rltr = FRRTableReader(titles=['Link ID', 'ADV Router', 'Age', 'Seq#', 'CkSum', 'Link count']) - # table reader for Net Link States - nltr = FRRTableReader(titles=['Link ID', 'ADV Router', 'Age', 'Seq#', 'CkSum']) - # table reader for Summary Link States - sltr = FRRTableReader(titles=['Link ID', 'ADV Router', 'Age', 'Seq#', 'CkSum', 'Route\n']) - # table reader for AS External Link States (columns are identical to Summary Link States) - eltr = sltr - - # get the FRR output - lines = self._show('database') - - # placeholder for the active table reader for the coming line - tr = None - # whether or not the header for the current section has already been parsed - header_parsed = False - # the current router - router = None - # the current area - area = None - # the current mode - mode = None - - for line in lines: - if line.startswith(' '): - # this is a heading - heading = line.strip() - header_parsed = False - - # this is going to be dirty - if self.myre.search(r'OSPF Router with ID \(([\.\d]+)\)', heading): - router = self.myre.last_match.group(1) - if router not in db: - db[router] = {} - mode = 'router' - elif self.myre.search(r'Router Link States \(Area ([\.\d]+)\)', heading): - mode = 'router_link_state_area' - area = self.myre.last_match.group(1) - if mode not in db[router]: - db[router][mode] = {} - if area not in db[router][mode]: - db[router][mode][area] = [] - tr = rltr - elif self.myre.search(r'Net Link States \(Area ([\.\d]+)\)', heading): - mode = 'net_link_state_area' - area = self.myre.last_match.group(1) - if mode not in db[router]: - db[router][mode] = {} - if area not in db[router][mode]: - db[router][mode][area] = [] - tr = nltr - elif self.myre.search(r'Summary Link States \(Area ([\.\d]+)\)', heading): - mode = 'summary_link_state_area' - area = self.myre.last_match.group(1) - if mode not in db[router]: - db[router][mode] = {} - if area not in db[router][mode]: - db[router][mode][area] = [] - tr = sltr - elif heading == 'AS External Link States': - mode = 'external_states' - if mode not in db[router]: - db[router][mode] = [] - tr = eltr - else: - raise DaemonError('failed to parse heading: ' + heading) - # told you. - else: - if not header_parsed: - if mode in ['summary_link_state_area', 'external_states']: - # workaround because "Route" matches on "Router" and breaks the offset parsing logic - # stay consistent with the previous script, add a trailing newline - line += '\n' - tr.read_header(line) - header_parsed = True - else: - if mode == 'router': - raise DaemonError( - 'attempting to parse a table row but the mode is \'router\'. ' - 'please debug the FRR output:\n\n\n' + line - ) - elif mode == 'external_states': - db[router][mode].append(tr.read_line(line)) - else: - db[router][mode][area].append(tr.read_line(line)) - - return db - - -class OSPFv3(Daemon): - def _show(self, suffix: str): - return super()._show('ipv6 ospf6 ' + suffix) - - def database(self): - database = {} - lines = map(str.strip, self._show('database')) - - # table reader - tr = FRRTableReader(titles=["Type", "LSId", "AdvRouter", " Age", " SeqNum", " Payload"]) - header_parsed = False - interface = None - area = None - mode = None - - for line in lines: - if self.myre.search(r'Area Scoped Link State Database \(Area (.*)\)', line): - header_parsed = False - mode = 'scoped_link_db' - area = self.myre.last_match.group(1) - if mode not in database: - database[mode] = {} - if area not in database[mode]: - database[mode][area] = [] - elif self.myre.search(r'I\/F Scoped Link State Database \(I\/F (\S+) in Area (.*)\)', line): - header_parsed = False - mode = 'if_scoped_link_state' - interface = self.myre.last_match.group(1) - area = self.myre.last_match.group(2) - if mode not in database: - database[mode] = {} - if interface not in database[mode]: - database[mode][interface] = {} - if area not in database[mode][interface]: - database[mode][interface][area] = [] - elif line == 'AS Scoped Link State Database': - header_parsed = False - mode = 'as_scoped' - if mode not in database: - database[mode] = [] - else: - if not header_parsed: - tr.read_header(line) - header_parsed = True - else: - if mode == 'scoped_link_db': - database[mode][area].append(tr.read_line(line)) - elif mode == 'if_scoped_link_state': - database[mode][interface][area].append(tr.read_line(line)) - elif mode == 'as_scoped': - database[mode].append(tr.read_line(line)) - else: - raise DaemonError('invalid mode, failed to parse line: ' + line) - - return database - - def route(self): - route = [] - lines = map(str.strip, self._show('route')) - - for line in lines: - columns = re.split(r'\s+', line) - route.append({ - 'f1': columns[0], - 'f2': columns[1], - 'network': columns[2], - 'gateway': columns[3], - 'interface': columns[4], - 'time': columns[5], - }) - - return route - - def interface(self): - interface = {} - lines = map(str.strip, self._show('interface')) - - current_if = None - for line in lines: - if self.myre.search(r'(\S+) is (down|up), type ([A-Z]+)', line): - current_if = self.myre.last_match.group(1) - interface[current_if] = { - 'up': True if self.myre.last_match.group(2) == 'up' else False, - 'type': self.myre.last_match.group(3), - 'enabled': True - } - elif self.myre.search(r'Interface ID: (\d+)', line): - interface[current_if]['id'] = self.myre.last_match.group(1) - elif self.myre.search(r'OSPF not enabled on this interface', line): - interface[current_if]['enabled'] = False - elif self.myre.search(r'Instance ID (\d+), Interface MTU (\d+) \(autodetect: (\d+)\)', line): - interface[current_if]['instance_id'] = int(self.myre.last_match.group(1)) - interface[current_if]['interface_mtu'] = int(self.myre.last_match.group(2)) - interface[current_if]['interface_mtu_autodetect'] = int(self.myre.last_match.group(3)) - elif self.myre.search(r'(inet |inet6): (\S+)', line): - family = 'IPv6' if self.myre.last_match.group(1) == 'inet6' else 'IPv4' - if family not in interface[current_if]: - interface[current_if][family] = [] - interface[current_if][family].append(self.myre.last_match.group(2)) - elif self.myre.search(r'MTU mismatch detection: (en|dis)abled', line): - interface[current_if]['mtu_mismatch_detection'] = True if self.myre.last_match.group( - 1) == 'en' else False - elif self.myre.search(r'DR: (\S+) BDR: (\S+)', line): - interface[current_if]['designated_router'] = self.myre.last_match.group(1) - interface[current_if]['backup_designated_router'] = self.myre.last_match.group(2) - elif self.myre.search(r'State (\S+), Transmit Delay (\d+) sec, Priority (\d+)', line): - interface[current_if]['state'] = self.myre.last_match.group(1) - interface[current_if]['transmit_delay'] = int(self.myre.last_match.group(2)) - interface[current_if]['priority'] = int(self.myre.last_match.group(3)) - elif self.myre.search(r'Number of I\/F scoped LSAs is (\d+)', line): - interface[current_if]['number_if_scoped_lsas'] = int(self.myre.last_match.group(1)) - elif self.myre.search(r'(\d+) Pending LSAs for (\S+) in Time ([\d:]+)(?: (.*))', line): - if 'pending_lsas' not in interface[current_if]: - interface[current_if]['pending_lsas'] = {} - interface[current_if]['pending_lsas'][self.myre.last_match.group(2)] = { - 'time': self.myre.last_match.group(3), - 'count': self.myre.last_match.group(1), - 'flags': self.myre.last_match.group(4) - } - elif self.myre.search(r'Hello (\d+), Dead (\d+), Retransmit (\d+)', line): - interface[current_if]['timers'] = { - 'hello': int(self.myre.last_match.group(1)), - 'dead': int(self.myre.last_match.group(2)), - 'retransmit': int(self.myre.last_match.group(3)) - } - elif self.myre.search(r'Area ID (\S+), Cost (\d+)', line): - if 'area_cost' not in interface[current_if]: - interface[current_if]['area_cost'] = [] - interface[current_if]['area_cost'].append({ - 'area': self.myre.last_match.group(1), - 'cost': int(self.myre.last_match.group(2)) - }) - elif line in ['Internet Address:', 'Timer intervals configured:']: - # ignore these strings - pass - else: - raise DaemonError('failed to parse line: ' + line) - - return interface - - def neighbor(self): - neighbors = [] - lines = self._show('neighbor') - - tr = FRRTableReader(titles=['Neighbor ID', 'Pri', 'DeadTime', 'State/IfState', 'Duration I/F[State]']) - ll = lines.pop(0) - tr.read_header(ll) - - for line in lines: - neighbor = tr.read_line(line) - neighbor['Pri'] = int(neighbor['Pri']) - neighbors.append(neighbor) - - return neighbors - - def overview(self): - overview = {'areas': {}} - lines = map(str.strip, self._show('')) - - current_area = None - - for line in lines: - if self.myre.search(r'OSPFv3 Routing Process \((\d+)\) with Router-ID ([\d\.]+)', line): - overview['router_id'] = self.myre.last_match.group(2) - overview['routing_process'] = int(self.myre.last_match.group(1)) - elif self.myre.search(r'Initial SPF scheduling delay (\d+) millisec\(s\)', line): - overview['initial_spf_scheduling_delay'] = int(self.myre.last_match.group(1)) - elif self.myre.search(r'(Min|Max)imum hold time between consecutive SPFs (\d+) milli?second\(s\)', line): - if 'hold_time' not in overview: - overview['hold_time'] = {} - overview['hold_time'][self.myre.last_match.group(1).lower()] = int(self.myre.last_match.group(2)) - elif line == 'This router is an ASBR (injecting external routing information)': - overview['asbr'] = True - elif self.myre.search(r'SPF timer is (.*)', line): - overview['spf_timer'] = self.myre.last_match.group(1) - elif self.myre.search(r'Running (.*)', line): - overview['running_time'] = self.myre.last_match.group(1) - elif self.myre.search(r'Number of AS scoped LSAs is (\d+)', line): - overview['number_as_scoped'] = int(self.myre.last_match.group(1)) - elif self.myre.search(r'Hold time multiplier is currently (\d+)', line): - overview['current_hold_time_multipier'] = int(self.myre.last_match.group(1)) - elif self.myre.search(r'Number of areas in this router is (\d+)', line): - overview['number_of_areas'] = int(self.myre.last_match.group(1)) - elif self.myre.search(r'^Area ([\d\.]*)', line): - current_area = self.myre.last_match.group(1) - overview['areas'][current_area] = {} - elif self.myre.search(r'Interface attached to this area: (.*)', line): - overview['areas'][current_area]['interfaces'] = self.myre.last_match.group(1).split(' ') - elif self.myre.search(r'Number of Area scoped LSAs is (.*)', line): - overview['areas'][current_area]['number_lsas'] = int(self.myre.last_match.group(1)) - elif self.myre.search(r'LSA minimum arrival (.*)', line) or \ - self.myre.search(r'SPF algorithm last executed (.*)', line) or \ - self.myre.search(r'Last SPF duration (.*)', line) or \ - self.myre.search(r'SPF last executed (.*)', line) or \ - self.myre.search(r'Number of Area scoped LSAs is (.*)', line): - # skip these lines - pass - else: - raise DaemonError('failed to parse line: ' + line) - - return overview - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Python port of the OPNsense FRR output parser.') - parser.add_argument('-d', '--ospf-database', help='Prints the OSPF Database', action='store_true') - parser.add_argument('-D', '--ospfv3-database', help='Prints the OSPFv3 Database', action='store_true') - parser.add_argument('-t', '--ospfv3-route', help='Prints the OSPFv3 routing table', action='store_true') - parser.add_argument('-I', '--ospfv3-interface', help='Prints OSPFv3 interface information', action='store_true') - parser.add_argument('-N', '--ospfv3-neighbor', help='Prints OSPFv3 neighbor information', action='store_true') - parser.add_argument('-O', '--ospfv3-overview', help='Prints an OSPFv3 Summary', action='store_true') - args = parser.parse_args() - - # initialize VtySH and parser objects - main_vtysh = VtySH() - ospf = OSPF(main_vtysh) - ospfv3 = OSPFv3(main_vtysh) - - main_result = {} - if args.ospf_database: - main_result['ospf_database'] = ospf.database() - elif args.ospfv3_database: - main_result['ospfv3_database'] = ospfv3.database() - elif args.ospfv3_route: - main_result['ospfv3_route'] = ospfv3.route() - elif args.ospfv3_interface: - main_result['ospfv3_interface'] = ospfv3.interface() - elif args.ospfv3_neighbor: - main_result['ospfv3_neighbors'] = ospfv3.neighbor() - elif args.ospfv3_overview: - main_result['ospfv3_overview'] = ospfv3.overview() - - print(ujson.dumps(main_result, escape_forward_slashes=False)) diff --git a/net/frr/src/opnsense/scripts/frr/lib/events/ospf6d.py b/net/frr/src/opnsense/scripts/frr/lib/events/ospf6d.py new file mode 100755 index 0000000000..42d86c9d9d --- /dev/null +++ b/net/frr/src/opnsense/scripts/frr/lib/events/ospf6d.py @@ -0,0 +1,107 @@ +""" + Copyright (c) 2022 Ad Schellevis + 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. + +""" +import os +import syslog +from configparser import ConfigParser +from ..base import BaseEventHandler + + +class Ospf6dEventHandler(BaseEventHandler): + _config = '/usr/local/etc/frr/ospf6d_carp.conf' + + @property + def should_run(self): + return self.vtysh.is_running('ospf6d') + + def _read_config(self): + result = dict() + if os.path.isfile(self._config): + cnf = ConfigParser() + cnf.read(self._config) + not_empty = lambda x, y: cnf.has_option(x, y) and cnf.get(x, y) != '' and cnf.get(x, y) != '0' + for section in cnf.sections(): + if not_empty(section, 'interface') and not_empty(section, 'interface') \ + and not_empty(section, 'demoted_cost') and not_empty(section, 'carp_depend_on'): + default_cost = cnf.getint(section, 'default_cost') if not_empty(section, 'default_cost') else None + result[cnf.get(section, 'interface')] = { + 'demoted_cost': cnf.getint(section, 'demoted_cost'), + 'carp_depend_on': cnf.get(section, 'carp_depend_on'), + 'default_cost': default_cost, + } + + return result + + def execute(self): + if os.path.isfile(self._config): + # parse ospf6 interface data, keep structure similar to what ospf offers when using json output + ospf_interfaces = { + 'interfaces': {} + } + this_interface = None + for line in self.vtysh.execute('show ipv6 ospf6 interface', translate=None).decode().split('\n'): + if len(line) > 0 and line[0] != ' ': + this_interface = line.split()[0] + ospf_interfaces['interfaces'][this_interface] = {} + elif this_interface is not None: + if line.find('Area ID') > 0 and line.split()[-1].isdigit(): + # Area ID X.X.X.X, Cost XXXX + ospf_interfaces['interfaces'][this_interface]['cost'] = int(line.split()[-1]) + + config_interfaces = self._read_config() + for intf in config_interfaces: + if 'interfaces' in ospf_interfaces and intf in ospf_interfaces['interfaces']: + ospf_intf_cost = ospf_interfaces['interfaces'][intf]['cost'] + is_intf_master = self.ifstatus.address_status(config_interfaces[intf]['carp_depend_on']) == 'master' + is_ospf_dem = ospf_intf_cost == config_interfaces[intf]['demoted_cost'] + if is_intf_master and is_ospf_dem: + # promote ospf6 interface + conf_cost = config_interfaces[intf]['default_cost'] + if conf_cost is None: + syslog.syslog( + syslog.LOG_NOTICE, 'ospf6d promote interface %s (no default cost configured).' % intf + ) + self.vtysh.execute( + ['interface %s' % intf, 'no ipv6 ospf6 cost'], translate=None, configure=True + ) + elif conf_cost != ospf_intf_cost: + syslog.syslog( + syslog.LOG_NOTICE, 'ospf6d promote interface %s (cost %d).' % (intf, conf_cost) + ) + self.vtysh.execute( + ['interface %s' % intf, 'ipv6 ospf6 cost %d' % conf_cost], + translate=None, configure=True + ) + elif not is_intf_master and not is_ospf_dem: + # demote ospf6 interface + conf_cost = config_interfaces[intf]['demoted_cost'] + syslog.syslog( + syslog.LOG_NOTICE, 'ospf6d demote interface %s (cost %d).' % (intf, conf_cost) + ) + self.vtysh.execute( + ['interface %s' % intf, 'ipv6 ospf6 cost %d' % conf_cost], + translate=None, configure=True + ) diff --git a/net/frr/src/opnsense/scripts/frr/lib/events/ospfd.py b/net/frr/src/opnsense/scripts/frr/lib/events/ospfd.py index d692747088..7eabadb9b7 100755 --- a/net/frr/src/opnsense/scripts/frr/lib/events/ospfd.py +++ b/net/frr/src/opnsense/scripts/frr/lib/events/ospfd.py @@ -59,8 +59,6 @@ def execute(self): if os.path.isfile(self._config): ospf_interfaces = self.vtysh.execute('show ip ospf interface json') config_interfaces = self._read_config() - cnf = ConfigParser() - cnf.read(self._config) for intf in config_interfaces: if 'interfaces' in ospf_interfaces and intf in ospf_interfaces['interfaces']: ospf_intf_cost = ospf_interfaces['interfaces'][intf]['cost'] diff --git a/net/frr/src/opnsense/scripts/frr/register_sas b/net/frr/src/opnsense/scripts/frr/register_sas new file mode 100755 index 0000000000..33d4fcee7c --- /dev/null +++ b/net/frr/src/opnsense/scripts/frr/register_sas @@ -0,0 +1,68 @@ +#!/usr/local/bin/python3 +""" + Copyright (c) 2022 Ad Schellevis + 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. + +""" +import os +import subprocess +import tempfile +from configparser import ConfigParser + +if __name__ == '__main__': + frr_sad = {} + frr_sa_database = "/usr/local/etc/frr/sa_policies.conf" + # stage 1, read required FRR policies + if os.path.exists(frr_sa_database): + cnf = ConfigParser() + cnf.read(frr_sa_database) + for section in cnf.sections(): + if cnf.has_option(section, 'src') and cnf.has_option(section, 'dst'): + policy_key = "%s %s" % (cnf.get(section, 'src'), cnf.get(section, 'dst')) + frr_sad[policy_key] = {} + for prop in cnf.items(section): + frr_sad[policy_key][prop[0]] = prop[1] + + # stage 2, red current installed policies which seems to originate from FRR + registered_policies = [] + current_policy = None + for line in subprocess.run(["/sbin/setkey", "-D"], capture_output=True, text=True).stdout.split('\n'): + parts = line.strip().split() + if not line.startswith('\t') and len(parts) > 1: + current_policy = {"src": parts[0], "dst": parts[1]} + elif len(parts) > 2 and parts[0] == 'A:' and parts[1] == 'tcp-md5': + # Let's assume we're the only ones registering these types of entries + registered_policies.append(current_policy) + + # flush changes to temp file and load with setkey + temp_filename = None + with tempfile.NamedTemporaryFile(mode='wt', delete=False) as fo: + temp_filename = fo.name + for policy in registered_policies: + fo.write("delete -n %(src)s %(dst)s tcp 0x1000;\n" % policy) + for new_policy in frr_sad: + fo.write('add -n %(src)s %(dst)s %(protocol)s %(spi)s -A %(aalgo)s "%(key)s";\n' % frr_sad[new_policy]) + + if temp_filename: + subprocess.run(["/sbin/setkey", "-f", fo.name], capture_output=True, text=True) diff --git a/net/frr/src/opnsense/scripts/frr/setup.sh b/net/frr/src/opnsense/scripts/frr/setup.sh new file mode 100755 index 0000000000..bda37d2c33 --- /dev/null +++ b/net/frr/src/opnsense/scripts/frr/setup.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +user=frr +group=frr + +mkdir -p /var/run/frr +chown $user:$group /var/run/frr +chmod 750 /var/run/frr + +mkdir -p /usr/local/etc/frr +chown $user:$group /usr/local/etc/frr +chmod 750 /usr/local/etc/frr + +# ensure that frr can read the configuration files +chown -R $user:$group /usr/local/etc/frr +chown -R $user:$group /var/run/frr + +# logfile (if used) +touch /var/log/frr.log +chown $user:$group /var/log/frr.log + +# register Security Associations +/usr/local/opnsense/scripts/frr/register_sas + +# delete stale configuration files from frr.conf migration +files_to_delete=" + /etc/rc.d/watchfrr + /usr/local/etc/frr/bfdd.conf + /usr/local/etc/frr/bgpd.conf + /usr/local/etc/frr/ospfd.conf + /usr/local/etc/frr/ospf6d.conf + /usr/local/etc/frr/ripd.conf + /usr/local/etc/frr/staticd.conf + /usr/local/etc/frr/zebra.conf +" + +rm -f $files_to_delete diff --git a/net/frr/src/opnsense/scripts/quagga/setup.sh b/net/frr/src/opnsense/scripts/quagga/setup.sh deleted file mode 100755 index 5cb140a6bc..0000000000 --- a/net/frr/src/opnsense/scripts/quagga/setup.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -user=frr -group=frr - -mkdir -p /var/run/frr -chown $user:$group /var/run/frr -chmod 750 /var/run/frr - -mkdir -p /usr/local/etc/frr -chown $user:$group /usr/local/etc/frr -chmod 750 /usr/local/etc/frr - -# ensure that frr can read the configuration files -chown -R $user:$group /usr/local/etc/frr -chown -R $user:$group /var/run/frr - -# logfile (if used) -touch /var/log/frr.log -chown $user:$group /var/log/frr.log diff --git a/net/frr/src/opnsense/service/conf/actions.d/actions_quagga.conf b/net/frr/src/opnsense/service/conf/actions.d/actions_quagga.conf index 0544b9ab57..86170cd9ba 100644 --- a/net/frr/src/opnsense/service/conf/actions.d/actions_quagga.conf +++ b/net/frr/src/opnsense/service/conf/actions.d/actions_quagga.conf @@ -1,281 +1,192 @@ [start] -command:/usr/local/opnsense/scripts/quagga/setup.sh;/usr/local/etc/rc.d/frr start +command:/usr/local/etc/rc.d/frr start parameters: type:script -message:starting quagga +message:starting frr [stop] -command:/usr/local/etc/rc.d/frr stop; exit 0 +command:/usr/local/etc/rc.d/frr stop parameters: type:script -message:stopping quagga +message:stopping frr [restart] -command:/usr/local/opnsense/scripts/quagga/setup.sh;/usr/local/etc/rc.d/frr restart +command:/usr/local/etc/rc.d/frr restart; /usr/local/opnsense/scripts/frr/carp_event_handler parameters: type:script -message:restarting quagga +message:restarting frr +description:Restart FRR + +[reload] +command:service frr reload; /usr/local/opnsense/scripts/frr/carp_event_handler +parameters: +type:script +message:reloading frr +description:Reload FRR [status] -command:/usr/local/etc/rc.d/frr status;exit 0 +command:/usr/local/etc/rc.d/frr status; exit 0 parameters: type:script_output -message:request quagga +message:request frr [diagnostics.general_running-config] command:/usr/local/bin/vtysh -c "show running-config" parameters: +errors:no type:script_output message:FRR diagnosticts "show running-config" [diagnostics.general_route4] -command:/usr/local/bin/vtysh -c "show ip route" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show ip route %s' +errors:no type:script_output message:FRR diagnosticts "show ip route" -[diagnostics.general_route4_json] -command:/usr/local/bin/vtysh -c "show ip route json" -parameters: -type:script_output -message:FRR diagnosticts "show ip route json" - [diagnostics.general_route6] -command:/usr/local/bin/vtysh -c "show ipv6 route" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show ipv6 route %s' +errors:no type:script_output message:FRR diagnosticts "show ipv6 route" -[diagnostics.general_route6_json] -command:/usr/local/bin/vtysh -c "show ipv6 route json" -parameters: -type:script_output -message:FRR diagnosticts "show ipv6 route json" - -[diagnostics.bgp_route] -command:exit 1 -parameters: -type:script_output -message:FRR diagnostics "show bgp all" (not implemented) - -[diagnostics.bgp_route_json] -command:/usr/local/bin/vtysh -c "show bgp all json" -parameters: -type:script_output -message:FRR diagnostics "show bgp all json" (not implemented) - [diagnostics.bgp_route4] -command:/usr/local/bin/vtysh -c "show bgp ipv4" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show bgp ipv4 %s' +errors:no type:script_output -message:FRR diagnostics "show bgp ipv4" - -[diagnostics.bgp_route4_json] -command:/usr/local/bin/vtysh -c "show bgp ipv4 json" -parameters: -type:script_output -message:FRR diagnostics "show bgp ipv4 json" +message:FRR diagnostics "show bgp ipv4 %s" [diagnostics.bgp_route6] -command:/usr/local/bin/vtysh -c "show bgp ipv6" -parameters: -type:script_output -message:FRR diagnostics "show bgp ipv6" - -[diagnostics.bgp_route6_json] -command:/usr/local/bin/vtysh -c "show bgp ipv6 json" -parameters: +command:/usr/local/bin/vtysh +parameters:-c 'show bgp ipv6 %s' +errors:no type:script_output -message:FRR diagnostics "show bgp ipv6 json" +message:FRR diagnostics "show bgp ipv6 %s" [diagnostics.bgp_summary] -command:/usr/local/bin/vtysh -c "show bgp summary" -parameters: -type:script_output -message:FRR diagnostics "show bgp summary" - -[diagnostics.bgp_summary_json] -command:/usr/local/bin/vtysh -c "show bgp summary json" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show bgp summary %s' +errors:no type:script_output -message:FRR diagnostics "show bgp summary json" +message:FRR diagnostics "show bgp summary %s" [diagnostics.bgp_summary4] -command:/usr/local/bin/vtysh -c "show bgp ipv4 summary" -parameters: -type:script_output -message:FRR diagnostics "show bgp ipv4 summary" - -[diagnostics.bgp_summary4_json] -command:/usr/local/bin/vtysh -c "show bgp ipv4 summary json" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show bgp ipv4 summary %s' +errors:no type:script_output -message:FRR diagnostics "show bgp ipv4 summary json" +message:FRR diagnostics "show bgp ipv4 summary %s" [diagnostics.bgp_summary6] -command:/usr/local/bin/vtysh -c "show bgp ipv6 summary" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show bgp ipv6 summary %s' +errors:no type:script_output -message:FRR diagnostics "show bgp ipv6 summary" - -[diagnostics.bgp_summary6_json] -command:/usr/local/bin/vtysh -c "show bgp ipv6 summary json" -parameters: -type:script_output -message:FRR diagnostics "show bgp ipv6 summary json" +message:FRR diagnostics "show bgp ipv6 summary %s" [diagnostics.bgp_neighbors] -command:/usr/local/bin/vtysh -c "show bgp neighbors" -parameters: -type:script_output -message:FRR diagnostics "show bgp neighbors" - -[diagnostics.bgp_neighbors_json] -command:/usr/local/bin/vtysh -c "show bgp neighbors json" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show bgp neighbors %s' +errors:no type:script_output -message:FRR diagnostics "show bgp neighbors json" +message:FRR diagnostics "show bgp neighbors %s" [diagnostics.bgp_neighbors4] -command:/usr/local/bin/vtysh -c "show bgp ipv4 neighbors" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show bgp ipv4 neighbors %s' +errors:no type:script_output -message:FRR diagnostics "show bgp ipv4 neighbors" - -[diagnostics.bgp_neighbors4_json] -command:/usr/local/bin/vtysh -c "show bgp ipv4 neighbors json" -parameters: -type:script_output -message:FRR diagnostics "show bgp ipv4 neighbors json" +message:FRR diagnostics "show bgp ipv4 neighbors %s" [diagnostics.bgp_neighbors6] -command:/usr/local/bin/vtysh -c "show bgp ipv6 neighbors" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show bgp ipv6 neighbors %s' +errors:no type:script_output -message:FRR diagnostics "show bgp ipv6 neighbors" - -[diagnostics.bgp_neighbors6_json] -command:/usr/local/bin/vtysh -c "show bgp ipv6 neighbors json" -parameters: -type:script_output -message:FRR diagnostics "show bgp ipv6 neighbors json" +message:FRR diagnostics "show bgp ipv6 neighbors %s" [diagnostics.ospf_overview] -command:/usr/local/bin/vtysh -c "show ip ospf" -parameters: -type:script_output -message:FRR diagnostics "show ip ospf" - -[diagnostics.ospf_overview_json] -command:/usr/local/bin/vtysh -c "show ip ospf json" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show ip ospf %s' +errors:no type:script_output -message:FRR diagnostics "show ip ospf json" +message:FRR diagnostics "show ip ospf %s" [diagnostics.ospf_neighbor] -command:/usr/local/bin/vtysh -c "show ip ospf neighbor" -parameters: -type:script_output -message:FRR diagnostics "show ip ospf neighbor" - -[diagnostics.ospf_neighbor_json] -command:/usr/local/bin/vtysh -c "show ip ospf neighbor json" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show ip ospf neighbor %s' +errors:no type:script_output -message:FRR diagnostics "show ip ospf neighbor json" +message:FRR diagnostics "show ip ospf neighbor %s" [diagnostics.ospf_route] -command:/usr/local/bin/vtysh -c "show ip ospf route" -parameters: -type:script_output -message:FRR diagnostics "show ip ospf route" - -[diagnostics.ospf_route_json] -command:/usr/local/bin/vtysh -c "show ip ospf route json" -parameters: -type:script_output -message:FRR diagnostics "show ip ospf route json" - -[diagnostics.ospf_database] -command:/usr/local/bin/vtysh -c "show ip ospf database" -parameters: -type:script_output -message:FRR diagnostics "show ip ospf database" - -[diagnostics.ospf_database_json] -command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospf-database -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show ip ospf route %s' +errors:no type:script_output -message:FRR diagnostics "show ip ospf database json" +message:FRR diagnostics "show ip ospf route %s" [diagnostics.ospf_interface] -command:/usr/local/bin/vtysh -c "show ip ospf interface" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show ip ospf interface %s' +errors:no type:script_output -message:FRR diagnostics "show ip ospf interface" +message:FRR diagnostics "show ip ospf interface %s" -[diagnostics.ospf_interface_json] -command:/usr/local/bin/vtysh -c "show ip ospf interface json" -parameters: +[diagnostics.bfd_neighbors] +command:/usr/local/bin/vtysh +parameters: -c 'show bfd peers %s' +errors:no type:script_output -message:FRR diagnostics "show ip ospf interface json" +message:FRR diagnostics "show bfd peers json %s" -[diagnostics.ospfv3_overview] -command:/usr/local/bin/vtysh -c "show ipv6 ospf6" -parameters: +[diagnostics.bfd_summary] +command:/usr/local/bin/vtysh +parameters: -c 'show bfd peers brief %s' +errors:no type:script_output -message:FRR diagnostics "show ipv6 ospf6" +message:FRR diagnostics "show bfd peers brief %s" -[diagnostics.ospfv3_overview_json] -command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospfv3-overview -parameters: +[diagnostics.bfd_counters] +command:/usr/local/bin/vtysh +parameters: -c 'show bfd peers counters %s' +errors:no type:script_output -message:FRR diagnostics "show ipv6 ospf6 json" +message:FRR diagnostics "show bfd peers counters %s" -[diagnostics.ospfv3_neighbor] -command:/usr/local/bin/vtysh -c "show ipv6 ospf6 neighbor" -parameters: +[diagnostics.ospf_database] +command:/usr/local/bin/vtysh +parameters: -c 'show ip ospf database %s' +errors:no type:script_output -message:FRR diagnostics "show ipv6 ospf6 neighbor" +message:FRR diagnostics "show ip ospf database" -[diagnostics.ospfv3_neighbor_json] -command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospfv3-neighbor -parameters: +[diagnostics.ospfv3_overview] +command:/usr/local/bin/vtysh +parameters: -c 'show ipv6 ospf6 %s' +errors:no type:script_output -message:FRR diagnostics "show ipv6 ospf6 neighbor json" +message:FRR diagnostics "show ipv6 ospf6 %s" [diagnostics.ospfv3_route] -command:/usr/local/bin/vtysh -c "show ipv6 ospf6 route" -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show ipv6 ospf6 route %s' +errors:no type:script_output -message:FRR diagnostics "show ipv6 ospf6 route" - -[diagnostics.ospfv3_route_json] -command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospfv3-route -parameters: -type:script_output -message:FRR diagnostics "show ipv6 ospf6 route json" +message:FRR diagnostics "show ipv6 ospf6 route %s" [diagnostics.ospfv3_database] -command:/usr/local/bin/vtysh -c "show ipv6 ospf6 database" -parameters: -type:script_output -message:FRR diagnostics "show ipv6 ospf6 database" - -[diagnostics.ospfv3_database_json] -command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospfv3-database -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show ipv6 ospf6 database %s' +errors:no type:script_output message:FRR diagnostics "show ipv6 ospf6 database json" [diagnostics.ospfv3_interface] -command:/usr/local/bin/vtysh -c "show ipv6 ospf6 interface" -parameters: -type:script_output -message:FRR diagnostics "show ipv6 ospf6 interface" - -[diagnostics.ospfv3_interface_json] -command:/usr/local/opnsense/scripts/frr/legacy-diagnostics.py --ospfv3-interface -parameters: +command:/usr/local/bin/vtysh +parameters: -c 'show ipv6 ospf6 interface %s' +errors:no type:script_output -message:FRR diagnostics "show ipv6 ospf6 interface json" +message:FRR diagnostics "show ipv6 ospf6 interface %s" diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/+TARGETS b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/+TARGETS index 30b11bedbf..9ee342bb4c 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/+TARGETS +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/+TARGETS @@ -1,10 +1,7 @@ -bfdd.conf:/usr/local/etc/frr/bfdd.conf -bgpd.conf:/usr/local/etc/frr/bgpd.conf -ospfd.conf:/usr/local/etc/frr/ospfd.conf -ospfd_carp.conf:/usr/local/etc/frr/ospfd_carp.conf -ospf6d.conf:/usr/local/etc/frr/ospf6d.conf -ripd.conf:/usr/local/etc/frr/ripd.conf frr:/etc/rc.conf.d/frr -zebra.conf:/usr/local/etc/frr/zebra.conf +frr.conf:/usr/local/etc/frr/frr.conf vtysh.conf:/usr/local/etc/frr/vtysh.conf +ospf6d_carp.conf:/usr/local/etc/frr/ospf6d_carp.conf +ospfd_carp.conf:/usr/local/etc/frr/ospfd_carp.conf +sa_policies.conf:/usr/local/etc/frr/sa_policies.conf syslog-ng-frr-events.conf:/usr/local/etc/syslog-ng.conf.d/frr-events.conf diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/bfdd.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/bfdd.conf index 8b13652630..58db29a339 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/bfdd.conf +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/bfdd.conf @@ -1,29 +1,14 @@ +{# included in frr.conf #} {% if helpers.exists('OPNsense.quagga.bfd.enabled') and OPNsense.quagga.bfd.enabled == '1' %} -! -! Zebra configuration saved from vty -! 2017/03/03 20:21:04 -! -{% if helpers.exists('OPNsense.quagga.general') %} -{% if helpers.exists('OPNsense.quagga.general.enablesyslog') and OPNsense.quagga.general.enablesyslog == '1' %} -log syslog {{ OPNsense.quagga.general.sysloglevel }} -{% endif %} -{% if helpers.exists('OPNsense.quagga.general.profile') %} -frr defaults {{ OPNsense.quagga.general.profile }} -{% endif %} -{% endif %} -! -! -! -line vty -! -! bfd {% if helpers.exists('OPNsense.quagga.bfd.neighbors.neighbor') %} {% for neighbor in helpers.toList('OPNsense.quagga.bfd.neighbors.neighbor') %} {% if neighbor.enabled == '1' %} - peer {{ neighbor.address }} + peer {{ neighbor.address }} {% if neighbor.multihop|default('0') == '1' %}multihop{% endif +%} + detect-multiplier {{ neighbor.detect_multiplier }} + receive-interval {{ neighbor.receive_interval }} + transmit-interval {{ neighbor.transmit_interval }} {% endif %} {% endfor %} {% endif %} -! {% endif %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/bgpd.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/bgpd.conf index a9c4b993ee..c280332f21 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/bgpd.conf +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/bgpd.conf @@ -1,3 +1,4 @@ +{# included in frr.conf #} {% if helpers.exists('OPNsense.quagga.bgp.enabled') and OPNsense.quagga.bgp.enabled == '1' %} {% from 'OPNsense/Macros/interface.macro' import physical_interface %} {% set addressFamilies = ['ipv4', 'ipv6' ] %} @@ -26,37 +27,111 @@ {% endif %} {% endfor %} {% endif %} -! -! Zebra configuration saved from vty -! 2017/03/03 20:21:04 -! -{% if helpers.exists('OPNsense.quagga.general') %} -{% if helpers.exists('OPNsense.quagga.general.enablesyslog') and OPNsense.quagga.general.enablesyslog == '1' %} -log syslog {{ OPNsense.quagga.general.sysloglevel }} -{% endif %} -{% if helpers.exists('OPNsense.quagga.general.profile') %} -frr defaults {{ OPNsense.quagga.general.profile }} -{% endif %} -{% endif %} -! -! -! {% if helpers.exists('OPNsense.quagga.bgp.asnumber') and OPNsense.quagga.bgp.asnumber != '' %} router bgp {{ OPNsense.quagga.bgp.asnumber }} +{% if not helpers.empty('OPNsense.quagga.bgp.logneighborchanges') %} + bgp log-neighbor-changes +{% endif %} +{% if OPNsense.quagga.bgp.enforce_first_as == '0' %} + no bgp enforce-first-as +{% endif %} + no bgp default ipv4-unicast no bgp ebgp-requires-policy +{% if helpers.exists('OPNsense.quagga.bgp.networkimportcheck') and OPNsense.quagga.bgp.networkimportcheck == '1' %} + bgp network import-check +{% else %} + no bgp network import-check +{% endif %} {% if helpers.exists('OPNsense.quagga.bgp.graceful') and OPNsense.quagga.bgp.graceful == '1' %} bgp graceful-restart {% endif %} +{% if OPNsense.quagga.bgp.bestpath %} +{% for option in OPNsense.quagga.bgp.bestpath.split(',') %} + bgp bestpath {{ option }} +{% endfor %} +{% endif %} {% if helpers.exists('OPNsense.quagga.bgp.routerid') and OPNsense.quagga.bgp.routerid != '' %} bgp router-id {{ OPNsense.quagga.bgp.routerid }} {% endif %} +{% if helpers.exists('OPNsense.quagga.bgp.distance') and OPNsense.quagga.bgp.distance != '' %} + distance bgp {{ OPNsense.quagga.bgp.distance }} {{ OPNsense.quagga.bgp.distance }} {{ OPNsense.quagga.bgp.distance }} +{% endif %} +{% for peergroup in helpers.toList('OPNsense.quagga.bgp.peergroups.peergroup') %} +{% if peergroup.enabled == '1' %} + neighbor {{ peergroup.name }} peer-group +{% if 'remoteas' in peergroup and peergroup.remoteas and not peergroup.remote_as_mode %} + neighbor {{ peergroup.name }} remote-as {{ peergroup.remoteas }} +{% else %} + neighbor {{ peergroup.name }} remote-as {{ peergroup.remote_as_mode }} +{% endif %} +{% if peergroup.updatesource %} + neighbor {{ peergroup.name }} update-source {{ physical_interface(peergroup.updatesource) }} +{% endif %} +{% if peergroup.nexthopself|default('0') == '1' %} + neighbor {{ peergroup.name }} next-hop-self +{% endif %} +{% if peergroup.defaultoriginate|default('0') == '1' %} + neighbor {{ peergroup.name }} default-originate +{% endif %} +{% if peergroup.linkedPrefixlistIn|default("") != "" %} +{% for prefixlist in peergroup.linkedPrefixlistIn.split(",") %} +{% set prefixlist2_data = helpers.getUUID(prefixlist) %} +{% if prefixlist2_data != {} and prefixlist2_data.enabled == '1' %} + neighbor {{ peergroup.name }} prefix-list {{ prefixlist2_data.name }} in +{% endif %} +{% endfor %} +{% endif %} +{% if peergroup.linkedPrefixlistOut|default("") != "" %} +{% for prefixlist in peergroup.linkedPrefixlistOut.split(",") %} +{% set prefixlist_data = helpers.getUUID(prefixlist) %} +{% if prefixlist_data != {} and prefixlist_data.enabled == '1' %} + neighbor {{ peergroup.name }} prefix-list {{ prefixlist_data.name }} out +{% endif %} +{% endfor %} +{% endif %} +{% if peergroup.linkedRoutemapIn|default("") != "" %} +{% for aspath in peergroup.linkedRoutemapIn.split(",") %} +{% set routemap2_data = helpers.getUUID(aspath) %} +{% if routemap2_data != {} and routemap2_data.enabled == '1' %} + neighbor {{ peergroup.name }} route-map {{ routemap2_data.name }} in +{% endif %} +{% endfor %} +{% endif %} +{% if peergroup.linkedRoutemapOut|default("") != "" %} +{% for aspath in peergroup.linkedRoutemapOut.split(",") %} +{% set routemap_data = helpers.getUUID(aspath) %} +{% if routemap_data != {} and routemap_data.enabled == '1' %} + neighbor {{ peergroup.name }} route-map {{ routemap_data.name }} out +{% endif %} +{% endfor %} +{% endif %} +{% if peergroup.listenranges %} +{% for prefix in peergroup.listenranges.split(',') %} + bgp listen range {{ prefix }} peer-group {{ peergroup.name }} +{% endfor %} +{% endif %} +{% endif %} +{% endfor %} {% if helpers.exists('OPNsense.quagga.bgp.neighbors.neighbor') %} {% for neighbor in helpers.toList('OPNsense.quagga.bgp.neighbors.neighbor') %} {% if neighbor.enabled == '1' %} +{% if 'remoteas' in neighbor and neighbor.remoteas and not neighbor.remote_as_mode %} neighbor {{ neighbor.address }} remote-as {{ neighbor.remoteas }} -{% if 'bfd' in neighbor and neighbor.bfd == '1' %} +{% else %} + neighbor {{ neighbor.address }} remote-as {{ neighbor.remote_as_mode }} +{% endif %} +{% if neighbor.bfd|default('') == '1' %} neighbor {{ neighbor.address }} bfd {% endif %} +{% if 'password' in neighbor and neighbor.password != '' %} + neighbor {{ neighbor.address }} password {{ neighbor.password }} +{% endif %} +{% if 'weight' in neighbor and neighbor.weight != '' %} + neighbor {{ neighbor.address }} weight {{ neighbor.weight }} +{% endif %} +{% if 'disable_connected_check' in neighbor and neighbor.disable_connected_check == '1' %} + neighbor {{ neighbor.address }} disable-connected-check +{% endif %} {% if ':' not in neighbor.address and 'updatesource' in neighbor and neighbor.updatesource != '' %} neighbor {{ neighbor.address }} update-source {{ physical_interface(neighbor.updatesource) }} {% endif %} @@ -64,7 +139,7 @@ router bgp {{ OPNsense.quagga.bgp.asnumber }} neighbor {{ neighbor.address }} interface {{ physical_interface(neighbor.linklocalinterface) }} {% endif %} {% if 'multihop' in neighbor and neighbor.multihop == '1' %} - neighbor {{ neighbor.address }} ebgp-multihop + neighbor {{ neighbor.address }} ebgp-multihop 255 {% endif %} {% if 'keepalive' in neighbor and neighbor.keepalive != '' %} {% if 'holddown' in neighbor and neighbor.holddown != '' %} @@ -74,34 +149,61 @@ router bgp {{ OPNsense.quagga.bgp.asnumber }} {% if 'connecttimer' in neighbor and neighbor.connecttimer != '' %} neighbor {{ neighbor.address }} timers connect {{ neighbor.connecttimer }} {% endif %} +{% if 'capabilities' in neighbor and neighbor.capabilities %} +{% for capability in neighbor.capabilities.split(',') %} + neighbor {{ neighbor.address }} capability {{ capability }} +{% endfor %} +{% endif %} +{% if 'attributeunchanged' in neighbor and neighbor.attributeunchanged != '' %} + neighbor {{ neighbor.address }} attribute-unchanged {{ neighbor.attributeunchanged }} +{% endif %} +{% if neighbor.peergroup|default('') != '' %} +{% set pgname = helpers.getUUID(neighbor.peergroup) %} + neighbor {{ neighbor.address }} peer-group {{ pgname.name }} +{% endif %} {% endif %} {% endfor %} {% endif %} {% for addressFamily in addressFamilies %} address-family {{ addressFamily }} unicast -{% if helpers.exists('OPNsense.quagga.bgp.redistribute') and OPNsense.quagga.bgp.redistribute != '' %} -{% for bgp_redistribute in OPNsense.quagga.bgp.redistribute.split(',') %} - redistribute {{ bgp_redistribute }} -{% endfor %} -{% endif %} +{% for redistribution in helpers.toList('OPNsense.quagga.bgp.redistributions.redistribution') %} +{% if redistribution.enabled == '1' %} + redistribute {{ redistribution.redistribute }}{% if redistribution.linkedRoutemap %} route-map {{ helpers.getUUID(redistribution.linkedRoutemap).name }}{% endif +%} +{% endif %} +{% endfor %} {% for network in networks[addressFamily] %} network {{ network }} {% endfor %} +{% for peergroup in helpers.toList('OPNsense.quagga.bgp.peergroups.peergroup') %} +{% if peergroup.enabled == '1' and (addressFamily in peergroup.family.split(',')) %} + neighbor {{ peergroup.name }} activate +{% endif %} +{% endfor %} {% for neighbor in neighbors[addressFamily] %} neighbor {{ neighbor.address }} activate {% if 'nexthopself' in neighbor and neighbor.nexthopself == '1' %} - neighbor {{ neighbor.address }} next-hop-self + neighbor {{ neighbor.address }} next-hop-self {% if 'nexthopselfall' in neighbor and neighbor.nexthopselfall == '1' %}all{% endif %} + {% endif %} {% if 'rrclient' in neighbor and neighbor.rrclient == '1' %} neighbor {{ neighbor.address }} route-reflector-client {% endif %} +{% if neighbor.soft_reconfiguration_inbound|default('0') == '1' %} + neighbor {{ neighbor.address }} soft-reconfiguration inbound +{% endif %} {% if 'defaultoriginate' in neighbor and neighbor.defaultoriginate == '1' %} neighbor {{ neighbor.address }} default-originate {% endif %} {% if 'asoverride' in neighbor and neighbor.asoverride == '1' %} neighbor {{ neighbor.address }} as-override {% endif %} +{% if 'removeprivateas' in neighbor and neighbor.removeprivateas %} + neighbor {{ neighbor.address }} {{ neighbor.removeprivateas }} +{% endif %} +{% if 'allowas_in' in neighbor and neighbor.allowas_in %} + neighbor {{ neighbor.address }} allowas-in {{ neighbor.allowas_in }} +{% endif %} {% if neighbor.linkedPrefixlistIn|default("") != "" %} {% for prefixlist in neighbor.linkedPrefixlistIn.split(",") %} {% set prefixlist2_data = helpers.getUUID(prefixlist) %} @@ -136,7 +238,6 @@ router bgp {{ OPNsense.quagga.bgp.asnumber }} {% endif %} {% endfor %} exit-address-family -! {% endfor %} {% if helpers.exists('OPNsense.quagga.bgp.prefixlists.prefixlist') %} @@ -144,13 +245,11 @@ router bgp {{ OPNsense.quagga.bgp.asnumber }} {% if prefixlist.enabled == '1' and prefixlist.version == 'IPv4' %} ip prefix-list {{ prefixlist.name }} seq {{ prefixlist.seqnumber }} {{ prefixlist.action }} {{ prefixlist.network }} {% endif %} -! {% if prefixlist.enabled == '1' and prefixlist.version == 'IPv6' %} ipv6 prefix-list {{ prefixlist.name }} seq {{ prefixlist.seqnumber }} {{ prefixlist.action }} {{ prefixlist.network }} {% endif %} {% endfor %} {% endif %} -! {% if helpers.exists('OPNsense.quagga.bgp.aspaths.aspath') %} {% for aspath in helpers.sortDictList(OPNsense.quagga.bgp.aspaths.aspath, 'number' ) %} {% if aspath.enabled == '1' %} @@ -158,7 +257,6 @@ bgp as-path access-list {{ aspath.number }} {{ aspath.action }} {{ aspath.as }} {% endif %} {% endfor %} {% endif %} -! {% if helpers.exists('OPNsense.quagga.bgp.communitylists.communitylist') %} {% for communitylist in helpers.sortDictList(OPNsense.quagga.bgp.communitylists.communitylist, 'number' ) %} {% if communitylist.enabled == '1' %} @@ -166,7 +264,6 @@ bgp community-list {{ communitylist.number }} seq {{ communitylist.seqnumber }} {% endif %} {% endfor %} {% endif %} -! {% if helpers.exists('OPNsense.quagga.bgp.routemaps.routemap') %} {% for routemap in helpers.sortDictList(OPNsense.quagga.bgp.routemaps.routemap, 'name', 'id' ) %} {% if routemap.enabled == '1' %} @@ -179,7 +276,7 @@ route-map {{ routemap.name }} {{ routemap.action }} {{ routemap.id }} {% endif %} {% endfor %} {% endif %} -{% if routemap.set|default("") != '' and routemap.match|default("") != '' %} +{% if routemap.set|default("") != '' %} set {{ routemap.set }} {% endif %} {% if routemap.match2|default("") != "" %} @@ -206,13 +303,8 @@ route-map {{ routemap.name }} {{ routemap.action }} {{ routemap.id }} {% endif %} {% endfor %} {% endif %} -! {% endif %} -! {% if helpers.exists('OPNsense.quagga.bgpd.enabled') and OPNsense.quagga.general.enablesnmp == '1' %} agentx {% endif %} -! -line vty -! {% endif %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/frr b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/frr index 9c8ee85256..97d1f02c09 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/frr +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/frr @@ -1,25 +1,30 @@ {% if helpers.exists('OPNsense.quagga.general.enabled') and OPNsense.quagga.general.enabled == '1' %} -frr_var_script="/usr/local/opnsense/scripts/quagga/setup.sh" +# XXX rc.d/frr splits up defunct "frr" service into frr_daemons +# and we always start "zebra" so for now this works: +zebra_setup="/usr/local/opnsense/scripts/frr/setup.sh" frr_enable="YES" {% if helpers.exists('OPNsense.quagga.general.enablecarp') and OPNsense.quagga.general.enablecarp == '1' %} start_precmd="ifconfig | grep 'carp: MASTER'" {% endif %} -frr_daemons="zebra{% +frr_daemons="mgmtd zebra{% if helpers.exists('OPNsense.quagga.ospf.enabled') and OPNsense.quagga.ospf.enabled == '1' %} ospfd{% endif %}{% if helpers.exists('OPNsense.quagga.rip.enabled') and OPNsense.quagga.rip.enabled == '1' %} ripd{% endif %}{% if helpers.exists('OPNsense.quagga.bfd.enabled') and OPNsense.quagga.bfd.enabled == '1' %} bfdd{% endif %}{% if helpers.exists('OPNsense.quagga.bgp.enabled') and OPNsense.quagga.bgp.enabled == '1' %} bgpd{% endif %}{% if helpers.exists('OPNsense.quagga.ospf6.enabled') and OPNsense.quagga.ospf6.enabled == '1' %} ospf6d{% endif %}{% if helpers.exists('OPNsense.quagga.ripng.enabled') and OPNsense.quagga.ripng.enabled == '1' %} ripngd{% endif %}{% -if helpers.exists('OPNsense.quagga.isis.enabled') and OPNsense.quagga.isis.enabled == '1' %} isisd{% endif %}" -frr_carp_demote="{% if not helpers.empty('OPNsense.quagga.ospf.carp_demote') %} ospfd{% endif %}" -start_postcmd="/usr/local/opnsense/scripts/frr/carp_event_handler" -{% else %} -frr_enable="NO" -{% endif %} +if helpers.exists('OPNsense.quagga.static.enabled') and OPNsense.quagga.static.enabled == '1' %} staticd{% endif %}" +frr_carp_demote="{% + if not helpers.empty('OPNsense.quagga.ospf.carp_demote') %} ospfd{% endif %}{% + if not helpers.empty('OPNsense.quagga.ospf6.carp_demote') %} ospf6d{% endif +%}" +watchfrr_flags="-r /usr/local/opnsense/scripts/frr/frr_wrapper.shbBrestartbB%s -s /usr/local/opnsense/scripts/frr/frr_wrapper.shbBstartbB%s -k /usr/sbin/servicebBfrrbBstopbB%s -b bB -t 30" {% if OPNsense.quagga.general.enablesnmp == '1' %} zebra_flags="${zebra_flags} -M snmp" bgpd_flags="${bgpd_flags} -M snmp" -ospf_flags="${ospf_flags} -M snmp" -ospf6_flags="${ospf6_flags} -M snmp" +ospfd_flags="${ospfd_flags} -M snmp" +ospf6d_flags="${ospf6d_flags} -M snmp" +{% endif %} +{% else %} +frr_enable="NO" {% endif %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/frr.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/frr.conf new file mode 100644 index 0000000000..b8da1a348f --- /dev/null +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/frr.conf @@ -0,0 +1,8 @@ +{# Main configuration file #} +{% include "OPNsense/Quagga/zebra.conf" %} +{% include "OPNsense/Quagga/ripd.conf" %} +{% include "OPNsense/Quagga/ospfd.conf" %} +{% include "OPNsense/Quagga/ospf6d.conf" %} +{% include "OPNsense/Quagga/bgpd.conf" %} +{% include "OPNsense/Quagga/bfdd.conf" %} +{% include "OPNsense/Quagga/staticd.conf" %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospf6d.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospf6d.conf index 92fb578d97..66896f7a7c 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospf6d.conf +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospf6d.conf @@ -1,30 +1,21 @@ +{# included in frr.conf #} {% macro cline(directive, modelname) -%}{% if modelname %} ipv6 ospf6 {{ directive }} {{ modelname }} {% endif %}{%- endmacro %} {% from 'OPNsense/Macros/interface.macro' import physical_interface %} -{% if helpers.exists('OPNsense.quagga.ospf6.enabled') and OPNsense.quagga.ospf6.enabled == '1' %} -! -! Zebra configuration saved from vty -! 2017/03/03 20:21:04 -! +{% if not helpers.empty('OPNsense.quagga.ospf6.enabled') %} {% if helpers.exists('OPNsense.quagga.general') %} -{% if helpers.exists('OPNsense.quagga.general.enablesyslog') and OPNsense.quagga.general.enablesyslog == '1' %} -log syslog {{ OPNsense.quagga.general.sysloglevel }} -{% endif %} -{% if helpers.exists('OPNsense.quagga.general.profile') %} -frr defaults {{ OPNsense.quagga.general.profile }} -{% endif %} {% if OPNsense.quagga.general.enablesnmp == '1' %} agentx {% endif %} {% endif %} -! -! -! -{% if helpers.exists('OPNsense.quagga.ospf6.interfaces.interface') %} -{% for interface in helpers.toList('OPNsense.quagga.ospf6.interfaces.interface') %} -{% if interface.enabled == '1' %} +{% for interface in helpers.toList('OPNsense.quagga.ospf6.interfaces.interface') %} +{% if interface.enabled == '1' %} interface {{ physical_interface(interface.interfacename) }} + ipv6 ospf6 area {{ interface.area }} +{% if interface.bfd|default('') == '1' %} + ipv6 ospf6 bfd +{% endif %} {% if interface.networktype %} ipv6 ospf6 network {{ interface.networktype }} {% endif %} @@ -36,27 +27,71 @@ interface {{ physical_interface(interface.interfacename) }} }}{{ cline("hello-interval",interface.hellointerval) }}{{ cline("priority",interface.priority) }}{{ cline("retransmit-interval",interface.retransmitinterval) -}}! -{% endif %} -{% endfor %} -{% endif %} -! +}} +{% endif %} +{% endfor %} router ospf6 -{% if helpers.exists('OPNsense.quagga.ospf6.routerid') and OPNsense.quagga.ospf6.routerid != '' %} +{% if not helpers.empty('OPNsense.quagga.ospf6.routerid') %} ospf6 router-id {{ OPNsense.quagga.ospf6.routerid }} {% endif %} -{% if helpers.exists('OPNsense.quagga.ospf6.redistribute') and OPNsense.quagga.ospf6.redistribute != '' %} -{% for line in OPNsense.quagga.ospf6.redistribute.split(',') %} - redistribute {{ line }} -{% endfor %}{% endif %} -{% if helpers.exists('OPNsense.quagga.ospf6.interfaces.interface') %} -{% for interface in helpers.toList('OPNsense.quagga.ospf6.interfaces.interface') %} -{% if interface.enabled == '1' %} - interface {{ physical_interface(interface.interfacename) }} area {{ interface.area }} +{% if not helpers.empty('OPNsense.quagga.ospf6.originate') %} + default-information originate{% if not helpers.empty('OPNsense.quagga.ospf6.originatealways') %} always {% endif %}{% if OPNsense.quagga.ospf6.originatemetric|default('') != '' %} metric {{ OPNsense.quagga.ospf6.originatemetric }}{% endif %} + +{% endif %} +{% for redistribution in helpers.toList('OPNsense.quagga.ospf6.redistributions.redistribution') %} +{% if redistribution.enabled == '1' %} + redistribute {{ redistribution.redistribute }}{% if redistribution.linkedRoutemap %} route-map {{ helpers.getUUID(redistribution.linkedRoutemap).name }}{% endif +%} +{% endif %} +{% endfor %} +{% if helpers.exists('OPNsense.quagga.ospf6.networks.network') %} +{% for network in helpers.toList('OPNsense.quagga.ospf6.networks.network') %} +{% if network.enabled == '1' %} + network {{ network.ipaddr }}/{{ network.netmask }} area {{ network.area }} +{% endif %} +{% if network.arearange|default("") != "" %} + area {{ network.area }} range {{ network.arearange }} +{% endif %} +{% if network.linkedPrefixlistIn|default("") != "" %} +{% for prefixlist in network.linkedPrefixlistIn.split(",") %} +{% set prefixlist2_data = helpers.getUUID(prefixlist) %} +{% if prefixlist2_data != {} and prefixlist2_data.enabled == '1' %} + area {{ network.area }} filter-list prefix {{ prefixlist2_data.name }} in +{% endif %} +{% endfor %} +{% endif %} +{% if network.linkedPrefixlistOut|default("") != "" %} +{% for prefixlist in network.linkedPrefixlistOut.split(",") %} +{% set prefixlist_data = helpers.getUUID(prefixlist) %} +{% if prefixlist_data != {} and prefixlist_data.enabled == '1' %} + area {{ network.area }} filter-list prefix {{ prefixlist_data.name }} out +{% endif %} +{% endfor %} {% endif %} {% endfor %} {% endif %} -! -line vty -! +{% if helpers.exists('OPNsense.quagga.ospf6.prefixlists.prefixlist') %} +{% for prefixlist in helpers.sortDictList(OPNsense.quagga.ospf6.prefixlists.prefixlist, 'name', 'seqnumber' ) %} +{% if prefixlist.enabled == '1' %} +ipv6 prefix-list {{ prefixlist.name }} seq {{ prefixlist.seqnumber }} {{ prefixlist.action }} {{ prefixlist.network }} +{% endif %} +{% endfor %} +{% endif %} +{% if helpers.exists('OPNsense.quagga.ospf6.routemaps.routemap') %} +{% for routemap in helpers.sortDictList(OPNsense.quagga.ospf6.routemaps.routemap, 'name', 'id' ) %} +{% if routemap.enabled == '1' %} +route-map {{ routemap.name }} {{ routemap.action }} {{ routemap.id }} +{% if routemap.match2|default("") != "" %} +{% for prefixlist in routemap.match2.split(",") %} +{% set prefixlist_data = helpers.getUUID(prefixlist) %} +{% if 'match2' in routemap and routemap.match2 != '' %} + match ipv6 address prefix-list {{ prefixlist_data.name }} +{% endif %} +{% endfor %} +{% endif %} +{% if routemap.set|default("") != "" %} + set {{ routemap.set }} +{% endif %} +{% endif %} +{% endfor %} +{% endif %} {% endif %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospf6d_carp.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospf6d_carp.conf new file mode 100644 index 0000000000..3d03df677f --- /dev/null +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospf6d_carp.conf @@ -0,0 +1,14 @@ +{# consumed by ospf6d.py #} +{% from 'OPNsense/Macros/interface.macro' import physical_interface %} +{% if helpers.exists('OPNsense.quagga.ospf6.interfaces.interface') %} +{% for interface in helpers.toList('OPNsense.quagga.ospf6.interfaces.interface') %} +{% if interface.enabled == '1' %} +[{{ interface['@uuid'] }}] +enabled={{interface.enabled|default('0')}} +interface={{physical_interface(interface.interfacename)}} +default_cost={{interface.cost|default('')}} +demoted_cost={{interface.cost_demoted|default('')}} +carp_depend_on={{interface.carp_depend_on|default('')}} +{% endif %} +{% endfor %} +{% endif %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd.conf index 350f4925cb..bbb3d4b0a2 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd.conf +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd.conf @@ -1,64 +1,83 @@ +{# included in frr.conf #} {% macro cline(directive, modelname) -%}{% if modelname %} ip ospf {{ directive }} {{ modelname }} {% endif %}{%- endmacro %} {% from 'OPNsense/Macros/interface.macro' import physical_interface %} {% if helpers.exists('OPNsense.quagga.ospf.enabled') and OPNsense.quagga.ospf.enabled == '1' %} -! -! Zebra configuration saved from vty -! 2017/03/03 20:21:04 -! {% if helpers.exists('OPNsense.quagga.general') %} -{% if helpers.exists('OPNsense.quagga.general.enablesyslog') and OPNsense.quagga.general.enablesyslog == '1' %} -log syslog {{ OPNsense.quagga.general.sysloglevel }} -{% endif %} -{% if helpers.exists('OPNsense.quagga.general.profile') %} -frr defaults {{ OPNsense.quagga.general.profile }} -{% endif %} {% if OPNsense.quagga.general.enablesnmp == '1' %} agentx {% endif %} {% endif %} -! -! -! +{% if OPNsense.quagga.ospf.passiveinterfaces %} +{% for iface in OPNsense.quagga.ospf.passiveinterfaces.split(',') %} +interface {{ helpers.physical_interface(iface) }} + ip ospf passive +{% endfor %} +{% endif %} +{# vtysh automatically merges passive interfaces with interfaces below #} {% if helpers.exists('OPNsense.quagga.ospf.interfaces.interface') %} -{% for interface in helpers.toList('OPNsense.quagga.ospf.interfaces.interface') %} -{% if interface.enabled == '1' %} -interface {{ physical_interface(interface.interfacename) }} -{% if interface.networktype %} -{{ cline("network",interface.networktype) -}}{% endif %} -{{ cline("authentication",interface.authtype) -}}{% if interface.authtype and interface.authtype == 'message-digest' -%}{{ cline("message-digest-key " + interface.authkey_id + " md5",interface.authkey) -}}{% endif -%}{{ cline("area",interface.area) -}}{{ cline("cost",interface.cost) -}}{{ cline("dead-interval",interface.deadinterval) -}}{{ cline("hello-interval",interface.hellointerval) -}}{{ cline("priority",interface.priority) -}}{{ cline("retransmit-interval",interface.retransmitinterval) -}}! -{% endif %} -{% endfor %} +{% for interface in helpers.toList('OPNsense.quagga.ospf.interfaces.interface') %} +{% set iface = physical_interface(interface.interfacename) %} +{% if interface.enabled == '1' %} +interface {{ iface }} +{% if interface.bfd|default('') == '1' %} + ip ospf bfd +{% endif %} +{% if interface.networktype %} +{% if interface.networktype == 'point-to-multipoint' %} +{% if interface.p2mpoptions %} + {{ cline("network", "point-to-multipoint " ~ interface.p2mpoptions) }} +{% else %} + {{ cline("network", "point-to-multipoint") }} +{% endif %} +{% else %} + {{ cline("network", interface.networktype) }} +{% endif %} +{% endif %} +{% if interface.authtype and interface.authtype == 'message-digest' %} + {{ cline("authentication", interface.authtype) }} + {{ cline("message-digest-key " + interface.authkey_id + " md5", interface.authkey) }} +{% elif interface.authtype and interface.authtype == 'plain' %} + {{ cline("authentication", ' ') }} + {{ cline("authentication-key", interface.authkey) }} +{% endif %} + {{ cline("area", interface.area) }} + {{ cline("cost", interface.cost) }} + {{ cline("dead-interval", interface.deadinterval) }} + {{ cline("hello-interval", interface.hellointerval) }} + {{ cline("priority", interface.priority) }} + {{ cline("retransmit-interval", interface.retransmitinterval) }} +{% endif %} +{% endfor %} {% endif %} -! router ospf +{% if helpers.exists('OPNsense.quagga.ospf.logadjacencychanges') and OPNsense.quagga.ospf.logadjacencychanges == '1' %} + log-adjacency-changes +{% endif %} {% if helpers.exists('OPNsense.quagga.ospf.costreference') and OPNsense.quagga.ospf.costreference != '' %} auto-cost reference-bandwidth {{ OPNsense.quagga.ospf.costreference }} {% endif %} {% if helpers.exists('OPNsense.quagga.ospf.routerid') and OPNsense.quagga.ospf.routerid != '' %} ospf router-id {{ OPNsense.quagga.ospf.routerid }} {% endif %} -{% if helpers.exists('OPNsense.quagga.ospf.redistribute') and OPNsense.quagga.ospf.redistribute != '' %} -{% for line in OPNsense.quagga.ospf.redistribute.split(',') %} -{% if helpers.exists('OPNsense.quagga.ospf.redistributemap') and OPNsense.quagga.ospf.redistributemap != '' %}{% set line = line + " route-map " + helpers.getUUID(OPNsense.quagga.ospf.redistributemap).name %}{% endif %} - redistribute {{ line }} -{% endfor %}{% endif %} -{% if helpers.exists('OPNsense.quagga.ospf.passiveinterfaces') and OPNsense.quagga.ospf.passiveinterfaces != '' %} -{% for line in OPNsense.quagga.ospf.passiveinterfaces.split(',') %} - passive-interface {{ physical_interface(line) }} -{% endfor %}{% endif %} +{% for redistribution in helpers.toList('OPNsense.quagga.ospf.redistributions.redistribution') %} +{% if redistribution.enabled == '1' %} + redistribute {{ redistribution.redistribute }}{% if redistribution.linkedRoutemap %} route-map {{ helpers.getUUID(redistribution.linkedRoutemap).name }}{% endif +%} +{% endif %} +{% endfor %} +{% for area in helpers.toList('OPNsense.quagga.ospf.areas.area') %} +{% if area.enabled == '1' %} + area {{ area.id }} {{ area.type }} +{% endif %} +{% endfor %} +{% if helpers.exists('OPNsense.quagga.ospf.neighbors.neighbor') %} +{% for neighbor in helpers.toList('OPNsense.quagga.ospf.neighbors.neighbor') %} +{% if neighbor.enabled == '1' %} + neighbor {{ neighbor.address }}{% if 'pollinterval' in neighbor and neighbor.pollinterval != '' %} poll-interval {{ neighbor.pollinterval }} {% endif %}{% if 'priority' in neighbor and neighbor.priority != '' %} priority {{ neighbor.priority }} {% endif +%} +{% endif %} +{% endfor %} +{% endif %} {% if helpers.exists('OPNsense.quagga.ospf.networks.network') %} {% for network in helpers.toList('OPNsense.quagga.ospf.networks.network') %} {% if network.enabled == '1' %} @@ -89,7 +108,6 @@ router ospf default-information originate{% if helpers.exists('OPNsense.quagga.ospf.originatealways') and OPNsense.quagga.ospf.originatealways == '1' %} always {% endif %}{% if helpers.exists('OPNsense.quagga.ospf.originatemetric') and OPNsense.quagga.ospf.originatemetric != '' %} metric {{ OPNsense.quagga.ospf.originatemetric }}{% endif %} {% endif %} -! {% if helpers.exists('OPNsense.quagga.ospf.prefixlists.prefixlist') %} {% for prefixlist in helpers.sortDictList(OPNsense.quagga.ospf.prefixlists.prefixlist, 'name', 'seqnumber' ) %} {% if prefixlist.enabled == '1' %} @@ -97,7 +115,6 @@ ip prefix-list {{ prefixlist.name }} seq {{ prefixlist.seqnumber }} {{ prefixlis {% endif %} {% endfor %} {% endif %} -! {% if helpers.exists('OPNsense.quagga.ospf.routemaps.routemap') %} {% for routemap in helpers.sortDictList(OPNsense.quagga.ospf.routemaps.routemap, 'name', 'id' ) %} {% if routemap.enabled == '1' %} @@ -116,7 +133,4 @@ route-map {{ routemap.name }} {{ routemap.action }} {{ routemap.id }} {% endif %} {% endfor %} {% endif %} -! -line vty -! {% endif %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd_carp.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd_carp.conf index 724d7cb3c1..d67fdde4a3 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd_carp.conf +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd_carp.conf @@ -1,3 +1,4 @@ +{# consumed by ospfd.py #} {% from 'OPNsense/Macros/interface.macro' import physical_interface %} {% if helpers.exists('OPNsense.quagga.ospf.interfaces.interface') %} {% for interface in helpers.toList('OPNsense.quagga.ospf.interfaces.interface') %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ripd.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ripd.conf index 70532d4f1a..70d29a8359 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ripd.conf +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ripd.conf @@ -1,18 +1,6 @@ +{# included in frr.conf #} {% if helpers.exists('OPNsense.quagga.rip.enabled') and OPNsense.quagga.rip.enabled == '1' %} {% from 'OPNsense/Macros/interface.macro' import physical_interface %} -! -! Zebra configuration saved from vty -! 2017/03/26 22:40:16 -! -{% if helpers.exists('OPNsense.quagga.general') %} -{% if helpers.exists('OPNsense.quagga.general.enablesyslog') and OPNsense.quagga.general.enablesyslog == '1' %} -log syslog {{ OPNsense.quagga.general.sysloglevel }} -{% endif %} -{% if helpers.exists('OPNsense.quagga.general.profile') %} -frr defaults {{ OPNsense.quagga.general.profile }} -{% endif %} -{% endif %} -! router rip version {{ OPNsense.quagga.rip.version }} {% if helpers.exists('OPNsense.quagga.rip.redistribute') and OPNsense.quagga.rip.redistribute != '' %} @@ -31,7 +19,4 @@ router rip {% if helpers.exists('OPNsense.quagga.rip.defaultmetric') and OPNsense.quagga.rip.defaultmetric != '' %} default-metric {{ OPNsense.quagga.rip.defaultmetric }} {% endif %} -! -line vty -! {% endif %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/sa_policies.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/sa_policies.conf new file mode 100644 index 0000000000..9f0df54223 --- /dev/null +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/sa_policies.conf @@ -0,0 +1,25 @@ +{# consumed by scripts/frr/register_sas #} +{% if helpers.exists('OPNsense.quagga.bgp.enabled') and OPNsense.quagga.bgp.enabled == '1' %} +{% if helpers.exists('OPNsense.quagga.bgp.neighbors.neighbor') %} +{% for neighbor in helpers.toList('OPNsense.quagga.bgp.neighbors.neighbor') %} +{% if neighbor.enabled == '1' and neighbor.password|default('') != '' %} +[policy_{{neighbor['@uuid']}}_in] +src={{ neighbor.address }} +dst={{ neighbor.localip }} +protocol=tcp +spi=0x1000 +aalgo=tcp-md5 +key={{ neighbor.password }} + +[policy_{{neighbor['@uuid']}}_out] +src={{ neighbor.localip }} +dst={{ neighbor.address }} +protocol=tcp +spi=0x1000 +aalgo=tcp-md5 +key={{ neighbor.password }} + +{% endif %} +{% endfor %} +{% endif %} +{% endif %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/staticd.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/staticd.conf new file mode 100644 index 0000000000..43a810beb4 --- /dev/null +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/staticd.conf @@ -0,0 +1,19 @@ +{# included in frr.conf #} +{% if not helpers.empty('OPNsense.quagga.static.enabled') %} +{% for route in helpers.toList('OPNsense.quagga.static.routes.route') %} +{% if route.enabled == '1' %} +{%- if ':' in route.network %} +ipv6 +{%- else %} +ip +{%- endif %} + route {{ route.network }} +{%- if route.gateway %} + {{ route.gateway}} +{%- endif %} +{%- if route.interfacename %} + {{ helpers.physical_interface(route.interfacename) }} +{%- endif +%} +{% endif %} +{% endfor %} +{% endif %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/syslog-ng-frr-events.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/syslog-ng-frr-events.conf index 9644313b81..309d47627c 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/syslog-ng-frr-events.conf +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/syslog-ng-frr-events.conf @@ -6,7 +6,8 @@ destination d_frr_event { }; filter f_frr_ospf { - program("ospfd") and ( + (program("ospfd") or program("ospf6d")) + and ( ( level("info") or level("notice") ) or ( diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/vtysh.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/vtysh.conf index e69de29bb2..57d0b081de 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/vtysh.conf +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/vtysh.conf @@ -0,0 +1 @@ +{# file is empty on purpose since vtysh requires a configuration file to exist #} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/zebra.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/zebra.conf index fbc685f72b..1822f47f1f 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/zebra.conf +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/zebra.conf @@ -1,28 +1,14 @@ +{# included in frr.conf #} {% if helpers.exists('OPNsense.quagga.general') %} -! -! Zebra configuration saved from vty -! 2017/03/03 20:21:04 -! {% if helpers.exists('OPNsense.quagga.general.profile') %} frr defaults {{ OPNsense.quagga.general.profile }} {% endif %} {% if helpers.exists('OPNsense.quagga.general.enablesyslog') and OPNsense.quagga.general.enablesyslog == '1' %} log syslog {{ OPNsense.quagga.general.sysloglevel }} {% endif %} -! -! -! -! -! {% if OPNsense.quagga.general.enablesnmp == '1' %} agentx {% endif %} -! -! ip forwarding ipv6 forwarding -! -! -line vty -! {% endif %} diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Syslog/local/routing_frr.conf b/net/frr/src/opnsense/service/templates/OPNsense/Syslog/local/routing_frr.conf index ad0869d3c7..61a95d16ac 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Syslog/local/routing_frr.conf +++ b/net/frr/src/opnsense/service/templates/OPNsense/Syslog/local/routing_frr.conf @@ -2,5 +2,5 @@ # Local syslog-ng configuration filter definition [FRR]. ################################################################### filter f_local_routing_frr { - program("bgpd") or program("ospfd") or program("ospf6d") or program("ripd") or program("zebra") or program("frr_carp"); + program("bgpd") or program("ospfd") or program("ospf6d") or program("ripd") or program("zebra") or program("frr_carp") or program("frr_wrapper"); }; diff --git a/net/frr/src/opnsense/www/js/quagga/lodash.js b/net/frr/src/opnsense/www/js/quagga/lodash.js deleted file mode 100644 index 71226cfa05..0000000000 --- a/net/frr/src/opnsense/www/js/quagga/lodash.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * @license - * Lodash lodash.com/license | Underscore.js 1.8.3 underscorejs.org/LICENSE - */ -;(function(){function n(n,t,r){switch(r.length){case 0:return n.call(t);case 1:return n.call(t,r[0]);case 2:return n.call(t,r[0],r[1]);case 3:return n.call(t,r[0],r[1],r[2])}return n.apply(t,r)}function t(n,t,r,e){for(var u=-1,i=null==n?0:n.length;++u"']/g,G=RegExp(V.source),H=RegExp(K.source),J=/<%-([\s\S]+?)%>/g,Y=/<%([\s\S]+?)%>/g,Q=/<%=([\s\S]+?)%>/g,X=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,nn=/^\w*$/,tn=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,rn=/[\\^$.*+?()[\]{}|]/g,en=RegExp(rn.source),un=/^\s+|\s+$/g,on=/^\s+/,fn=/\s+$/,cn=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,an=/\{\n\/\* \[wrapped with (.+)\] \*/,ln=/,? & /,sn=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,hn=/\\(\\)?/g,pn=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,_n=/\w*$/,vn=/^[-+]0x[0-9a-f]+$/i,gn=/^0b[01]+$/i,dn=/^\[object .+?Constructor\]$/,yn=/^0o[0-7]+$/i,bn=/^(?:0|[1-9]\d*)$/,xn=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,jn=/($^)/,wn=/['\n\r\u2028\u2029\\]/g,mn="[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?(?:\\u200d(?:[^\\ud800-\\udfff]|(?:\\ud83c[\\udde6-\\uddff]){2}|[\\ud800-\\udbff][\\udc00-\\udfff])[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?)*",An="(?:[\\u2700-\\u27bf]|(?:\\ud83c[\\udde6-\\uddff]){2}|[\\ud800-\\udbff][\\udc00-\\udfff])"+mn,En="(?:[^\\ud800-\\udfff][\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]?|[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|(?:\\ud83c[\\udde6-\\uddff]){2}|[\\ud800-\\udbff][\\udc00-\\udfff]|[\\ud800-\\udfff])",kn=RegExp("['\u2019]","g"),Sn=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]","g"),On=RegExp("\\ud83c[\\udffb-\\udfff](?=\\ud83c[\\udffb-\\udfff])|"+En+mn,"g"),In=RegExp(["[A-Z\\xc0-\\xd6\\xd8-\\xde]?[a-z\\xdf-\\xf6\\xf8-\\xff]+(?:['\u2019](?:d|ll|m|re|s|t|ve))?(?=[\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000]|[A-Z\\xc0-\\xd6\\xd8-\\xde]|$)|(?:[A-Z\\xc0-\\xd6\\xd8-\\xde]|[^\\ud800-\\udfff\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\d+\\u2700-\\u27bfa-z\\xdf-\\xf6\\xf8-\\xffA-Z\\xc0-\\xd6\\xd8-\\xde])+(?:['\u2019](?:D|LL|M|RE|S|T|VE))?(?=[\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000]|[A-Z\\xc0-\\xd6\\xd8-\\xde](?:[a-z\\xdf-\\xf6\\xf8-\\xff]|[^\\ud800-\\udfff\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\d+\\u2700-\\u27bfa-z\\xdf-\\xf6\\xf8-\\xffA-Z\\xc0-\\xd6\\xd8-\\xde])|$)|[A-Z\\xc0-\\xd6\\xd8-\\xde]?(?:[a-z\\xdf-\\xf6\\xf8-\\xff]|[^\\ud800-\\udfff\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\d+\\u2700-\\u27bfa-z\\xdf-\\xf6\\xf8-\\xffA-Z\\xc0-\\xd6\\xd8-\\xde])+(?:['\u2019](?:d|ll|m|re|s|t|ve))?|[A-Z\\xc0-\\xd6\\xd8-\\xde]+(?:['\u2019](?:D|LL|M|RE|S|T|VE))?|\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])|\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])|\\d+",An].join("|"),"g"),Rn=RegExp("[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]"),zn=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,Wn="Array Buffer DataView Date Error Float32Array Float64Array Function Int8Array Int16Array Int32Array Map Math Object Promise RegExp Set String Symbol TypeError Uint8Array Uint8ClampedArray Uint16Array Uint32Array WeakMap _ clearTimeout isFinite parseInt setTimeout".split(" "),Bn={}; -Bn["[object Float32Array]"]=Bn["[object Float64Array]"]=Bn["[object Int8Array]"]=Bn["[object Int16Array]"]=Bn["[object Int32Array]"]=Bn["[object Uint8Array]"]=Bn["[object Uint8ClampedArray]"]=Bn["[object Uint16Array]"]=Bn["[object Uint32Array]"]=true,Bn["[object Arguments]"]=Bn["[object Array]"]=Bn["[object ArrayBuffer]"]=Bn["[object Boolean]"]=Bn["[object DataView]"]=Bn["[object Date]"]=Bn["[object Error]"]=Bn["[object Function]"]=Bn["[object Map]"]=Bn["[object Number]"]=Bn["[object Object]"]=Bn["[object RegExp]"]=Bn["[object Set]"]=Bn["[object String]"]=Bn["[object WeakMap]"]=false; -var Ln={};Ln["[object Arguments]"]=Ln["[object Array]"]=Ln["[object ArrayBuffer]"]=Ln["[object DataView]"]=Ln["[object Boolean]"]=Ln["[object Date]"]=Ln["[object Float32Array]"]=Ln["[object Float64Array]"]=Ln["[object Int8Array]"]=Ln["[object Int16Array]"]=Ln["[object Int32Array]"]=Ln["[object Map]"]=Ln["[object Number]"]=Ln["[object Object]"]=Ln["[object RegExp]"]=Ln["[object Set]"]=Ln["[object String]"]=Ln["[object Symbol]"]=Ln["[object Uint8Array]"]=Ln["[object Uint8ClampedArray]"]=Ln["[object Uint16Array]"]=Ln["[object Uint32Array]"]=true, -Ln["[object Error]"]=Ln["[object Function]"]=Ln["[object WeakMap]"]=false;var Un={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Cn=parseFloat,Dn=parseInt,Mn=typeof global=="object"&&global&&global.Object===Object&&global,Tn=typeof self=="object"&&self&&self.Object===Object&&self,$n=Mn||Tn||Function("return this")(),Fn=typeof exports=="object"&&exports&&!exports.nodeType&&exports,Nn=Fn&&typeof module=="object"&&module&&!module.nodeType&&module,Pn=Nn&&Nn.exports===Fn,Zn=Pn&&Mn.process,qn=function(){ -try{var n=Nn&&Nn.f&&Nn.f("util").types;return n?n:Zn&&Zn.binding&&Zn.binding("util")}catch(n){}}(),Vn=qn&&qn.isArrayBuffer,Kn=qn&&qn.isDate,Gn=qn&&qn.isMap,Hn=qn&&qn.isRegExp,Jn=qn&&qn.isSet,Yn=qn&&qn.isTypedArray,Qn=b("length"),Xn=x({"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I", -"\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss","\u0100":"A","\u0102":"A","\u0104":"A","\u0101":"a","\u0103":"a","\u0105":"a","\u0106":"C", -"\u0108":"C","\u010a":"C","\u010c":"C","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\u010e":"D","\u0110":"D","\u010f":"d","\u0111":"d","\u0112":"E","\u0114":"E","\u0116":"E","\u0118":"E","\u011a":"E","\u0113":"e","\u0115":"e","\u0117":"e","\u0119":"e","\u011b":"e","\u011c":"G","\u011e":"G","\u0120":"G","\u0122":"G","\u011d":"g","\u011f":"g","\u0121":"g","\u0123":"g","\u0124":"H","\u0126":"H","\u0125":"h","\u0127":"h","\u0128":"I","\u012a":"I","\u012c":"I","\u012e":"I","\u0130":"I","\u0129":"i", -"\u012b":"i","\u012d":"i","\u012f":"i","\u0131":"i","\u0134":"J","\u0135":"j","\u0136":"K","\u0137":"k","\u0138":"k","\u0139":"L","\u013b":"L","\u013d":"L","\u013f":"L","\u0141":"L","\u013a":"l","\u013c":"l","\u013e":"l","\u0140":"l","\u0142":"l","\u0143":"N","\u0145":"N","\u0147":"N","\u014a":"N","\u0144":"n","\u0146":"n","\u0148":"n","\u014b":"n","\u014c":"O","\u014e":"O","\u0150":"O","\u014d":"o","\u014f":"o","\u0151":"o","\u0154":"R","\u0156":"R","\u0158":"R","\u0155":"r","\u0157":"r","\u0159":"r", -"\u015a":"S","\u015c":"S","\u015e":"S","\u0160":"S","\u015b":"s","\u015d":"s","\u015f":"s","\u0161":"s","\u0162":"T","\u0164":"T","\u0166":"T","\u0163":"t","\u0165":"t","\u0167":"t","\u0168":"U","\u016a":"U","\u016c":"U","\u016e":"U","\u0170":"U","\u0172":"U","\u0169":"u","\u016b":"u","\u016d":"u","\u016f":"u","\u0171":"u","\u0173":"u","\u0174":"W","\u0175":"w","\u0176":"Y","\u0177":"y","\u0178":"Y","\u0179":"Z","\u017b":"Z","\u017d":"Z","\u017a":"z","\u017c":"z","\u017e":"z","\u0132":"IJ","\u0133":"ij", -"\u0152":"Oe","\u0153":"oe","\u0149":"'n","\u017f":"s"}),nt=x({"&":"&","<":"<",">":">",'"':""","'":"'"}),tt=x({"&":"&","<":"<",">":">",""":'"',"'":"'"}),rt=function x(mn){function An(n){if(yu(n)&&!ff(n)&&!(n instanceof Un)){if(n instanceof On)return n;if(oi.call(n,"__wrapped__"))return Fe(n)}return new On(n)}function En(){}function On(n,t){this.__wrapped__=n,this.__actions__=[],this.__chain__=!!t,this.__index__=0,this.__values__=T}function Un(n){this.__wrapped__=n, -this.__actions__=[],this.__dir__=1,this.__filtered__=false,this.__iteratees__=[],this.__takeCount__=4294967295,this.__views__=[]}function Mn(n){var t=-1,r=null==n?0:n.length;for(this.clear();++t=t?n:t)),n}function _t(n,t,e,u,i,o){var f,c=1&t,a=2&t,l=4&t;if(e&&(f=i?e(n,u,i,o):e(n)),f!==T)return f;if(!du(n))return n;if(u=ff(n)){if(f=me(n),!c)return Ur(n,f)}else{var s=vo(n),h="[object Function]"==s||"[object GeneratorFunction]"==s;if(af(n))return Ir(n,c);if("[object Object]"==s||"[object Arguments]"==s||h&&!i){if(f=a||h?{}:Ae(n),!c)return a?Mr(n,lt(f,n)):Dr(n,at(f,n))}else{if(!Ln[s])return i?n:{};f=Ee(n,s,c)}}if(o||(o=new Zn), -i=o.get(n))return i;o.set(n,f),pf(n)?n.forEach(function(r){f.add(_t(r,t,e,r,n,o))}):sf(n)&&n.forEach(function(r,u){f.set(u,_t(r,t,e,u,n,o))});var a=l?a?ve:_e:a?Bu:Wu,p=u?T:a(n);return r(p||n,function(r,u){p&&(u=r,r=n[u]),ot(f,u,_t(r,t,e,u,n,o))}),f}function vt(n){var t=Wu(n);return function(r){return gt(r,n,t)}}function gt(n,t,r){var e=r.length;if(null==n)return!e;for(n=Qu(n);e--;){var u=r[e],i=t[u],o=n[u];if(o===T&&!(u in n)||!i(o))return false}return true}function dt(n,t,r){if(typeof n!="function")throw new ti("Expected a function"); -return bo(function(){n.apply(T,r)},t)}function yt(n,t,r,e){var u=-1,i=o,a=true,l=n.length,s=[],h=t.length;if(!l)return s;r&&(t=c(t,k(r))),e?(i=f,a=false):200<=t.length&&(i=O,a=false,t=new Nn(t));n:for(;++ut}function Rt(n,t){return null!=n&&oi.call(n,t)}function zt(n,t){return null!=n&&t in Qu(n)}function Wt(n,t,r){for(var e=r?f:o,u=n[0].length,i=n.length,a=i,l=Ku(i),s=1/0,h=[];a--;){var p=n[a];a&&t&&(p=c(p,k(t))),s=Ci(p.length,s), -l[a]=!r&&(t||120<=u&&120<=p.length)?new Nn(a&&p):T}var p=n[0],_=-1,v=l[0];n:for(;++_r.length?t:kt(t,hr(r,0,-1)),r=null==t?t:t[Me(Ve(r))],null==r?T:n(r,t,e)}function Ut(n){return yu(n)&&"[object Arguments]"==Ot(n)}function Ct(n){ -return yu(n)&&"[object ArrayBuffer]"==Ot(n)}function Dt(n){return yu(n)&&"[object Date]"==Ot(n)}function Mt(n,t,r,e,u){if(n===t)t=true;else if(null==n||null==t||!yu(n)&&!yu(t))t=n!==n&&t!==t;else n:{var i=ff(n),o=ff(t),f=i?"[object Array]":vo(n),c=o?"[object Array]":vo(t),f="[object Arguments]"==f?"[object Object]":f,c="[object Arguments]"==c?"[object Object]":c,a="[object Object]"==f,o="[object Object]"==c;if((c=f==c)&&af(n)){if(!af(t)){t=false;break n}i=true,a=false}if(c&&!a)u||(u=new Zn),t=i||_f(n)?se(n,t,r,e,Mt,u):he(n,t,f,r,e,Mt,u);else{ -if(!(1&r)&&(i=a&&oi.call(n,"__wrapped__"),f=o&&oi.call(t,"__wrapped__"),i||f)){n=i?n.value():n,t=f?t.value():t,u||(u=new Zn),t=Mt(n,t,r,e,u);break n}if(c)t:if(u||(u=new Zn),i=1&r,f=_e(n),o=f.length,c=_e(t).length,o==c||i){for(a=o;a--;){var l=f[a];if(!(i?l in t:oi.call(t,l))){t=false;break t}}if((c=u.get(n))&&u.get(t))t=c==t;else{c=true,u.set(n,t),u.set(t,n);for(var s=i;++at?r:0,Se(t,r)?n[t]:T}function Xt(n,t,r){var e=-1;return t=c(t.length?t:[$u],k(ye())),n=Gt(n,function(n){return{ -a:c(t,function(t){return t(n)}),b:++e,c:n}}),w(n,function(n,t){var e;n:{e=-1;for(var u=n.a,i=t.a,o=u.length,f=r.length;++e=f?c:c*("desc"==r[e]?-1:1);break n}}e=n.b-t.b}return e})}function nr(n,t){return tr(n,t,function(t,r){return zu(n,r)})}function tr(n,t,r){for(var e=-1,u=t.length,i={};++et||9007199254740991t&&(t=-t>u?0:u+t),r=r>u?u:r,0>r&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0,r=Ku(u);++e=u){for(;e>>1,o=n[i];null!==o&&!wu(o)&&(r?o<=t:ot.length?n:kt(n,hr(t,0,-1)),null==n||delete n[Me(Ve(t))]}function jr(n,t,r,e){for(var u=n.length,i=e?u:-1;(e?i--:++ie)return e?br(n[0]):[];for(var u=-1,i=Ku(e);++u=e?n:hr(n,t,r)}function Ir(n,t){if(t)return n.slice();var r=n.length,r=gi?gi(r):new n.constructor(r);return n.copy(r),r}function Rr(n){var t=new n.constructor(n.byteLength);return new vi(t).set(new vi(n)), -t}function zr(n,t){return new n.constructor(t?Rr(n.buffer):n.buffer,n.byteOffset,n.length)}function Wr(n,t){if(n!==t){var r=n!==T,e=null===n,u=n===n,i=wu(n),o=t!==T,f=null===t,c=t===t,a=wu(t);if(!f&&!a&&!i&&n>t||i&&o&&c&&!f&&!a||e&&o&&c||!r&&c||!u)return 1;if(!e&&!i&&!a&&nu?T:i,u=1),t=Qu(t);++eo&&f[0]!==a&&f[o-1]!==a?[]:L(f,a), -o-=c.length,or?r?or(t,n):t:(r=or(t,Oi(n/D(t))),Rn.test(t)?Or(M(r),0,n).join(""):r.slice(0,n))}function te(t,r,e,u){function i(){for(var r=-1,c=arguments.length,a=-1,l=u.length,s=Ku(l+c),h=this&&this!==$n&&this instanceof i?f:t;++at||e)&&(1&n&&(i[2]=h[2],t|=1&r?0:4),(r=h[3])&&(e=i[3],i[3]=e?Br(e,r,h[4]):r,i[4]=e?L(i[3],"__lodash_placeholder__"):h[4]),(r=h[5])&&(e=i[5],i[5]=e?Lr(e,r,h[6]):r,i[6]=e?L(i[5],"__lodash_placeholder__"):h[6]),(r=h[7])&&(i[7]=r),128&n&&(i[8]=null==i[8]?h[8]:Ci(i[8],h[8])),null==i[9]&&(i[9]=h[9]),i[0]=h[0],i[1]=t),n=i[0], -t=i[1],r=i[2],e=i[3],u=i[4],f=i[9]=i[9]===T?c?0:n.length:Ui(i[9]-a,0),!f&&24&t&&(t&=-25),Ue((h?co:yo)(t&&1!=t?8==t||16==t?Kr(n,t,f):32!=t&&33!=t||u.length?Jr.apply(T,i):te(n,t,r,e):Pr(n,t,r),i),n,t)}function ce(n,t,r,e){return n===T||lu(n,ei[r])&&!oi.call(e,r)?t:n}function ae(n,t,r,e,u,i){return du(n)&&du(t)&&(i.set(t,n),Yt(n,t,T,ae,i),i.delete(t)),n}function le(n){return xu(n)?T:n}function se(n,t,r,e,u,i){var o=1&r,f=n.length,c=t.length;if(f!=c&&!(o&&c>f))return false;if((c=i.get(n))&&i.get(t))return c==t; -var c=-1,a=true,l=2&r?new Nn:T;for(i.set(n,t),i.set(t,n);++cr&&(r=Ui(e+r,0)),_(n,ye(t,3),r)):-1}function Pe(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var u=e-1;return r!==T&&(u=Eu(r),u=0>r?Ui(e+u,0):Ci(u,e-1)), -_(n,ye(t,3),u,true)}function Ze(n){return(null==n?0:n.length)?wt(n,1):[]}function qe(n){return n&&n.length?n[0]:T}function Ve(n){var t=null==n?0:n.length;return t?n[t-1]:T}function Ke(n,t){return n&&n.length&&t&&t.length?er(n,t):n}function Ge(n){return null==n?n:$i.call(n)}function He(n){if(!n||!n.length)return[];var t=0;return n=i(n,function(n){if(hu(n))return t=Ui(n.length,t),true}),A(t,function(t){return c(n,b(t))})}function Je(t,r){if(!t||!t.length)return[];var e=He(t);return null==r?e:c(e,function(t){ -return n(r,T,t)})}function Ye(n){return n=An(n),n.__chain__=true,n}function Qe(n,t){return t(n)}function Xe(){return this}function nu(n,t){return(ff(n)?r:uo)(n,ye(t,3))}function tu(n,t){return(ff(n)?e:io)(n,ye(t,3))}function ru(n,t){return(ff(n)?c:Gt)(n,ye(t,3))}function eu(n,t,r){return t=r?T:t,t=n&&null==t?n.length:t,fe(n,128,T,T,T,T,t)}function uu(n,t){var r;if(typeof t!="function")throw new ti("Expected a function");return n=Eu(n),function(){return 0<--n&&(r=t.apply(this,arguments)),1>=n&&(t=T), -r}}function iu(n,t,r){return t=r?T:t,n=fe(n,8,T,T,T,T,T,t),n.placeholder=iu.placeholder,n}function ou(n,t,r){return t=r?T:t,n=fe(n,16,T,T,T,T,T,t),n.placeholder=ou.placeholder,n}function fu(n,t,r){function e(t){var r=c,e=a;return c=a=T,_=t,s=n.apply(e,r)}function u(n){var r=n-p;return n-=_,p===T||r>=t||0>r||g&&n>=l}function i(){var n=Go();if(u(n))return o(n);var r,e=bo;r=n-_,n=t-(n-p),r=g?Ci(n,l-r):n,h=e(i,r)}function o(n){return h=T,d&&c?e(n):(c=a=T,s)}function f(){var n=Go(),r=u(n);if(c=arguments, -a=this,p=n,r){if(h===T)return _=n=p,h=bo(i,t),v?e(n):s;if(g)return lo(h),h=bo(i,t),e(p)}return h===T&&(h=bo(i,t)),s}var c,a,l,s,h,p,_=0,v=false,g=false,d=true;if(typeof n!="function")throw new ti("Expected a function");return t=Su(t)||0,du(r)&&(v=!!r.leading,l=(g="maxWait"in r)?Ui(Su(r.maxWait)||0,t):l,d="trailing"in r?!!r.trailing:d),f.cancel=function(){h!==T&&lo(h),_=0,c=p=a=h=T},f.flush=function(){return h===T?s:o(Go())},f}function cu(n,t){function r(){var e=arguments,u=t?t.apply(this,e):e[0],i=r.cache; -return i.has(u)?i.get(u):(e=n.apply(this,e),r.cache=i.set(u,e)||i,e)}if(typeof n!="function"||null!=t&&typeof t!="function")throw new ti("Expected a function");return r.cache=new(cu.Cache||Fn),r}function au(n){if(typeof n!="function")throw new ti("Expected a function");return function(){var t=arguments;switch(t.length){case 0:return!n.call(this);case 1:return!n.call(this,t[0]);case 2:return!n.call(this,t[0],t[1]);case 3:return!n.call(this,t[0],t[1],t[2])}return!n.apply(this,t)}}function lu(n,t){return n===t||n!==n&&t!==t; -}function su(n){return null!=n&&gu(n.length)&&!_u(n)}function hu(n){return yu(n)&&su(n)}function pu(n){if(!yu(n))return false;var t=Ot(n);return"[object Error]"==t||"[object DOMException]"==t||typeof n.message=="string"&&typeof n.name=="string"&&!xu(n)}function _u(n){return!!du(n)&&(n=Ot(n),"[object Function]"==n||"[object GeneratorFunction]"==n||"[object AsyncFunction]"==n||"[object Proxy]"==n)}function vu(n){return typeof n=="number"&&n==Eu(n)}function gu(n){return typeof n=="number"&&-1=n; -}function du(n){var t=typeof n;return null!=n&&("object"==t||"function"==t)}function yu(n){return null!=n&&typeof n=="object"}function bu(n){return typeof n=="number"||yu(n)&&"[object Number]"==Ot(n)}function xu(n){return!(!yu(n)||"[object Object]"!=Ot(n))&&(n=di(n),null===n||(n=oi.call(n,"constructor")&&n.constructor,typeof n=="function"&&n instanceof n&&ii.call(n)==li))}function ju(n){return typeof n=="string"||!ff(n)&&yu(n)&&"[object String]"==Ot(n)}function wu(n){return typeof n=="symbol"||yu(n)&&"[object Symbol]"==Ot(n); -}function mu(n){if(!n)return[];if(su(n))return ju(n)?M(n):Ur(n);if(wi&&n[wi]){n=n[wi]();for(var t,r=[];!(t=n.next()).done;)r.push(t.value);return r}return t=vo(n),("[object Map]"==t?W:"[object Set]"==t?U:Uu)(n)}function Au(n){return n?(n=Su(n),n===$||n===-$?1.7976931348623157e308*(0>n?-1:1):n===n?n:0):0===n?n:0}function Eu(n){n=Au(n);var t=n%1;return n===n?t?n-t:n:0}function ku(n){return n?pt(Eu(n),0,4294967295):0}function Su(n){if(typeof n=="number")return n;if(wu(n))return F;if(du(n)&&(n=typeof n.valueOf=="function"?n.valueOf():n, -n=du(n)?n+"":n),typeof n!="string")return 0===n?n:+n;n=n.replace(un,"");var t=gn.test(n);return t||yn.test(n)?Dn(n.slice(2),t?2:8):vn.test(n)?F:+n}function Ou(n){return Cr(n,Bu(n))}function Iu(n){return null==n?"":yr(n)}function Ru(n,t,r){return n=null==n?T:kt(n,t),n===T?r:n}function zu(n,t){return null!=n&&we(n,t,zt)}function Wu(n){return su(n)?qn(n):Vt(n)}function Bu(n){if(su(n))n=qn(n,true);else if(du(n)){var t,r=ze(n),e=[];for(t in n)("constructor"!=t||!r&&oi.call(n,t))&&e.push(t);n=e}else{if(t=[], -null!=n)for(r in Qu(n))t.push(r);n=t}return n}function Lu(n,t){if(null==n)return{};var r=c(ve(n),function(n){return[n]});return t=ye(t),tr(n,r,function(n,r){return t(n,r[0])})}function Uu(n){return null==n?[]:S(n,Wu(n))}function Cu(n){return $f(Iu(n).toLowerCase())}function Du(n){return(n=Iu(n))&&n.replace(xn,Xn).replace(Sn,"")}function Mu(n,t,r){return n=Iu(n),t=r?T:t,t===T?zn.test(n)?n.match(In)||[]:n.match(sn)||[]:n.match(t)||[]}function Tu(n){return function(){return n}}function $u(n){return n; -}function Fu(n){return qt(typeof n=="function"?n:_t(n,1))}function Nu(n,t,e){var u=Wu(t),i=Et(t,u);null!=e||du(t)&&(i.length||!u.length)||(e=t,t=n,n=this,i=Et(t,Wu(t)));var o=!(du(e)&&"chain"in e&&!e.chain),f=_u(n);return r(i,function(r){var e=t[r];n[r]=e,f&&(n.prototype[r]=function(){var t=this.__chain__;if(o||t){var r=n(this.__wrapped__);return(r.__actions__=Ur(this.__actions__)).push({func:e,args:arguments,thisArg:n}),r.__chain__=t,r}return e.apply(n,a([this.value()],arguments))})}),n}function Pu(){} -function Zu(n){return Ie(n)?b(Me(n)):rr(n)}function qu(){return[]}function Vu(){return false}mn=null==mn?$n:rt.defaults($n.Object(),mn,rt.pick($n,Wn));var Ku=mn.Array,Gu=mn.Date,Hu=mn.Error,Ju=mn.Function,Yu=mn.Math,Qu=mn.Object,Xu=mn.RegExp,ni=mn.String,ti=mn.TypeError,ri=Ku.prototype,ei=Qu.prototype,ui=mn["__core-js_shared__"],ii=Ju.prototype.toString,oi=ei.hasOwnProperty,fi=0,ci=function(){var n=/[^.]+$/.exec(ui&&ui.keys&&ui.keys.IE_PROTO||"");return n?"Symbol(src)_1."+n:""}(),ai=ei.toString,li=ii.call(Qu),si=$n._,hi=Xu("^"+ii.call(oi).replace(rn,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),pi=Pn?mn.Buffer:T,_i=mn.Symbol,vi=mn.Uint8Array,gi=pi?pi.g:T,di=B(Qu.getPrototypeOf,Qu),yi=Qu.create,bi=ei.propertyIsEnumerable,xi=ri.splice,ji=_i?_i.isConcatSpreadable:T,wi=_i?_i.iterator:T,mi=_i?_i.toStringTag:T,Ai=function(){ -try{var n=je(Qu,"defineProperty");return n({},"",{}),n}catch(n){}}(),Ei=mn.clearTimeout!==$n.clearTimeout&&mn.clearTimeout,ki=Gu&&Gu.now!==$n.Date.now&&Gu.now,Si=mn.setTimeout!==$n.setTimeout&&mn.setTimeout,Oi=Yu.ceil,Ii=Yu.floor,Ri=Qu.getOwnPropertySymbols,zi=pi?pi.isBuffer:T,Wi=mn.isFinite,Bi=ri.join,Li=B(Qu.keys,Qu),Ui=Yu.max,Ci=Yu.min,Di=Gu.now,Mi=mn.parseInt,Ti=Yu.random,$i=ri.reverse,Fi=je(mn,"DataView"),Ni=je(mn,"Map"),Pi=je(mn,"Promise"),Zi=je(mn,"Set"),qi=je(mn,"WeakMap"),Vi=je(Qu,"create"),Ki=qi&&new qi,Gi={},Hi=Te(Fi),Ji=Te(Ni),Yi=Te(Pi),Qi=Te(Zi),Xi=Te(qi),no=_i?_i.prototype:T,to=no?no.valueOf:T,ro=no?no.toString:T,eo=function(){ -function n(){}return function(t){return du(t)?yi?yi(t):(n.prototype=t,t=new n,n.prototype=T,t):{}}}();An.templateSettings={escape:J,evaluate:Y,interpolate:Q,variable:"",imports:{_:An}},An.prototype=En.prototype,An.prototype.constructor=An,On.prototype=eo(En.prototype),On.prototype.constructor=On,Un.prototype=eo(En.prototype),Un.prototype.constructor=Un,Mn.prototype.clear=function(){this.__data__=Vi?Vi(null):{},this.size=0},Mn.prototype.delete=function(n){return n=this.has(n)&&delete this.__data__[n], -this.size-=n?1:0,n},Mn.prototype.get=function(n){var t=this.__data__;return Vi?(n=t[n],"__lodash_hash_undefined__"===n?T:n):oi.call(t,n)?t[n]:T},Mn.prototype.has=function(n){var t=this.__data__;return Vi?t[n]!==T:oi.call(t,n)},Mn.prototype.set=function(n,t){var r=this.__data__;return this.size+=this.has(n)?0:1,r[n]=Vi&&t===T?"__lodash_hash_undefined__":t,this},Tn.prototype.clear=function(){this.__data__=[],this.size=0},Tn.prototype.delete=function(n){var t=this.__data__;return n=ft(t,n),!(0>n)&&(n==t.length-1?t.pop():xi.call(t,n,1), ---this.size,true)},Tn.prototype.get=function(n){var t=this.__data__;return n=ft(t,n),0>n?T:t[n][1]},Tn.prototype.has=function(n){return-1e?(++this.size,r.push([n,t])):r[e][1]=t,this},Fn.prototype.clear=function(){this.size=0,this.__data__={hash:new Mn,map:new(Ni||Tn),string:new Mn}},Fn.prototype.delete=function(n){return n=be(this,n).delete(n),this.size-=n?1:0,n},Fn.prototype.get=function(n){return be(this,n).get(n); -},Fn.prototype.has=function(n){return be(this,n).has(n)},Fn.prototype.set=function(n,t){var r=be(this,n),e=r.size;return r.set(n,t),this.size+=r.size==e?0:1,this},Nn.prototype.add=Nn.prototype.push=function(n){return this.__data__.set(n,"__lodash_hash_undefined__"),this},Nn.prototype.has=function(n){return this.__data__.has(n)},Zn.prototype.clear=function(){this.__data__=new Tn,this.size=0},Zn.prototype.delete=function(n){var t=this.__data__;return n=t.delete(n),this.size=t.size,n},Zn.prototype.get=function(n){ -return this.__data__.get(n)},Zn.prototype.has=function(n){return this.__data__.has(n)},Zn.prototype.set=function(n,t){var r=this.__data__;if(r instanceof Tn){var e=r.__data__;if(!Ni||199>e.length)return e.push([n,t]),this.size=++r.size,this;r=this.__data__=new Fn(e)}return r.set(n,t),this.size=r.size,this};var uo=Fr(mt),io=Fr(At,true),oo=Nr(),fo=Nr(true),co=Ki?function(n,t){return Ki.set(n,t),n}:$u,ao=Ai?function(n,t){return Ai(n,"toString",{configurable:true,enumerable:false,value:Tu(t),writable:true})}:$u,lo=Ei||function(n){ -return $n.clearTimeout(n)},so=Zi&&1/U(new Zi([,-0]))[1]==$?function(n){return new Zi(n)}:Pu,ho=Ki?function(n){return Ki.get(n)}:Pu,po=Ri?function(n){return null==n?[]:(n=Qu(n),i(Ri(n),function(t){return bi.call(n,t)}))}:qu,_o=Ri?function(n){for(var t=[];n;)a(t,po(n)),n=di(n);return t}:qu,vo=Ot;(Fi&&"[object DataView]"!=vo(new Fi(new ArrayBuffer(1)))||Ni&&"[object Map]"!=vo(new Ni)||Pi&&"[object Promise]"!=vo(Pi.resolve())||Zi&&"[object Set]"!=vo(new Zi)||qi&&"[object WeakMap]"!=vo(new qi))&&(vo=function(n){ -var t=Ot(n);if(n=(n="[object Object]"==t?n.constructor:T)?Te(n):"")switch(n){case Hi:return"[object DataView]";case Ji:return"[object Map]";case Yi:return"[object Promise]";case Qi:return"[object Set]";case Xi:return"[object WeakMap]"}return t});var go=ui?_u:Vu,yo=Ce(co),bo=Si||function(n,t){return $n.setTimeout(n,t)},xo=Ce(ao),jo=function(n){n=cu(n,function(n){return 500===t.size&&t.clear(),n});var t=n.cache;return n}(function(n){var t=[];return 46===n.charCodeAt(0)&&t.push(""),n.replace(tn,function(n,r,e,u){ -t.push(e?u.replace(hn,"$1"):r||n)}),t}),wo=fr(function(n,t){return hu(n)?yt(n,wt(t,1,hu,true)):[]}),mo=fr(function(n,t){var r=Ve(t);return hu(r)&&(r=T),hu(n)?yt(n,wt(t,1,hu,true),ye(r,2)):[]}),Ao=fr(function(n,t){var r=Ve(t);return hu(r)&&(r=T),hu(n)?yt(n,wt(t,1,hu,true),T,r):[]}),Eo=fr(function(n){var t=c(n,Er);return t.length&&t[0]===n[0]?Wt(t):[]}),ko=fr(function(n){var t=Ve(n),r=c(n,Er);return t===Ve(r)?t=T:r.pop(),r.length&&r[0]===n[0]?Wt(r,ye(t,2)):[]}),So=fr(function(n){var t=Ve(n),r=c(n,Er);return(t=typeof t=="function"?t:T)&&r.pop(), -r.length&&r[0]===n[0]?Wt(r,T,t):[]}),Oo=fr(Ke),Io=pe(function(n,t){var r=null==n?0:n.length,e=ht(n,t);return ur(n,c(t,function(n){return Se(n,r)?+n:n}).sort(Wr)),e}),Ro=fr(function(n){return br(wt(n,1,hu,true))}),zo=fr(function(n){var t=Ve(n);return hu(t)&&(t=T),br(wt(n,1,hu,true),ye(t,2))}),Wo=fr(function(n){var t=Ve(n),t=typeof t=="function"?t:T;return br(wt(n,1,hu,true),T,t)}),Bo=fr(function(n,t){return hu(n)?yt(n,t):[]}),Lo=fr(function(n){return mr(i(n,hu))}),Uo=fr(function(n){var t=Ve(n);return hu(t)&&(t=T), -mr(i(n,hu),ye(t,2))}),Co=fr(function(n){var t=Ve(n),t=typeof t=="function"?t:T;return mr(i(n,hu),T,t)}),Do=fr(He),Mo=fr(function(n){var t=n.length,t=1=t}),of=Ut(function(){return arguments}())?Ut:function(n){return yu(n)&&oi.call(n,"callee")&&!bi.call(n,"callee")},ff=Ku.isArray,cf=Vn?k(Vn):Ct,af=zi||Vu,lf=Kn?k(Kn):Dt,sf=Gn?k(Gn):Tt,hf=Hn?k(Hn):Nt,pf=Jn?k(Jn):Pt,_f=Yn?k(Yn):Zt,vf=ee(Kt),gf=ee(function(n,t){return n<=t}),df=$r(function(n,t){ -if(ze(t)||su(t))Cr(t,Wu(t),n);else for(var r in t)oi.call(t,r)&&ot(n,r,t[r])}),yf=$r(function(n,t){Cr(t,Bu(t),n)}),bf=$r(function(n,t,r,e){Cr(t,Bu(t),n,e)}),xf=$r(function(n,t,r,e){Cr(t,Wu(t),n,e)}),jf=pe(ht),wf=fr(function(n,t){n=Qu(n);var r=-1,e=t.length,u=2--n)return t.apply(this,arguments)}},An.ary=eu,An.assign=df,An.assignIn=yf,An.assignInWith=bf,An.assignWith=xf,An.at=jf,An.before=uu,An.bind=Ho,An.bindAll=Nf,An.bindKey=Jo,An.castArray=function(){if(!arguments.length)return[];var n=arguments[0];return ff(n)?n:[n]},An.chain=Ye,An.chunk=function(n,t,r){if(t=(r?Oe(n,t,r):t===T)?1:Ui(Eu(t),0),r=null==n?0:n.length,!r||1>t)return[];for(var e=0,u=0,i=Ku(Oi(r/t));et?0:t,e)):[]},An.dropRight=function(n,t,r){var e=null==n?0:n.length;return e?(t=r||t===T?1:Eu(t),t=e-t,hr(n,0,0>t?0:t)):[]},An.dropRightWhile=function(n,t){return n&&n.length?jr(n,ye(t,3),true,true):[]; -},An.dropWhile=function(n,t){return n&&n.length?jr(n,ye(t,3),true):[]},An.fill=function(n,t,r,e){var u=null==n?0:n.length;if(!u)return[];for(r&&typeof r!="number"&&Oe(n,t,r)&&(r=0,e=u),u=n.length,r=Eu(r),0>r&&(r=-r>u?0:u+r),e=e===T||e>u?u:Eu(e),0>e&&(e+=u),e=r>e?0:ku(e);r>>0,r?(n=Iu(n))&&(typeof t=="string"||null!=t&&!hf(t))&&(t=yr(t),!t&&Rn.test(n))?Or(M(n),0,r):n.split(t,r):[]},An.spread=function(t,r){if(typeof t!="function")throw new ti("Expected a function");return r=null==r?0:Ui(Eu(r),0), -fr(function(e){var u=e[r];return e=Or(e,0,r),u&&a(e,u),n(t,this,e)})},An.tail=function(n){var t=null==n?0:n.length;return t?hr(n,1,t):[]},An.take=function(n,t,r){return n&&n.length?(t=r||t===T?1:Eu(t),hr(n,0,0>t?0:t)):[]},An.takeRight=function(n,t,r){var e=null==n?0:n.length;return e?(t=r||t===T?1:Eu(t),t=e-t,hr(n,0>t?0:t,e)):[]},An.takeRightWhile=function(n,t){return n&&n.length?jr(n,ye(t,3),false,true):[]},An.takeWhile=function(n,t){return n&&n.length?jr(n,ye(t,3)):[]},An.tap=function(n,t){return t(n), -n},An.throttle=function(n,t,r){var e=true,u=true;if(typeof n!="function")throw new ti("Expected a function");return du(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),fu(n,t,{leading:e,maxWait:t,trailing:u})},An.thru=Qe,An.toArray=mu,An.toPairs=zf,An.toPairsIn=Wf,An.toPath=function(n){return ff(n)?c(n,Me):wu(n)?[n]:Ur(jo(Iu(n)))},An.toPlainObject=Ou,An.transform=function(n,t,e){var u=ff(n),i=u||af(n)||_f(n);if(t=ye(t,4),null==e){var o=n&&n.constructor;e=i?u?new o:[]:du(n)&&_u(o)?eo(di(n)):{}; -}return(i?r:mt)(n,function(n,r,u){return t(e,n,r,u)}),e},An.unary=function(n){return eu(n,1)},An.union=Ro,An.unionBy=zo,An.unionWith=Wo,An.uniq=function(n){return n&&n.length?br(n):[]},An.uniqBy=function(n,t){return n&&n.length?br(n,ye(t,2)):[]},An.uniqWith=function(n,t){return t=typeof t=="function"?t:T,n&&n.length?br(n,T,t):[]},An.unset=function(n,t){return null==n||xr(n,t)},An.unzip=He,An.unzipWith=Je,An.update=function(n,t,r){return null==n?n:lr(n,t,kr(r)(kt(n,t)),void 0)},An.updateWith=function(n,t,r,e){ -return e=typeof e=="function"?e:T,null!=n&&(n=lr(n,t,kr(r)(kt(n,t)),e)),n},An.values=Uu,An.valuesIn=function(n){return null==n?[]:S(n,Bu(n))},An.without=Bo,An.words=Mu,An.wrap=function(n,t){return nf(kr(t),n)},An.xor=Lo,An.xorBy=Uo,An.xorWith=Co,An.zip=Do,An.zipObject=function(n,t){return Ar(n||[],t||[],ot)},An.zipObjectDeep=function(n,t){return Ar(n||[],t||[],lr)},An.zipWith=Mo,An.entries=zf,An.entriesIn=Wf,An.extend=yf,An.extendWith=bf,Nu(An,An),An.add=Qf,An.attempt=Ff,An.camelCase=Bf,An.capitalize=Cu, -An.ceil=Xf,An.clamp=function(n,t,r){return r===T&&(r=t,t=T),r!==T&&(r=Su(r),r=r===r?r:0),t!==T&&(t=Su(t),t=t===t?t:0),pt(Su(n),t,r)},An.clone=function(n){return _t(n,4)},An.cloneDeep=function(n){return _t(n,5)},An.cloneDeepWith=function(n,t){return t=typeof t=="function"?t:T,_t(n,5,t)},An.cloneWith=function(n,t){return t=typeof t=="function"?t:T,_t(n,4,t)},An.conformsTo=function(n,t){return null==t||gt(n,t,Wu(t))},An.deburr=Du,An.defaultTo=function(n,t){return null==n||n!==n?t:n},An.divide=nc,An.endsWith=function(n,t,r){ -n=Iu(n),t=yr(t);var e=n.length,e=r=r===T?e:pt(Eu(r),0,e);return r-=t.length,0<=r&&n.slice(r,e)==t},An.eq=lu,An.escape=function(n){return(n=Iu(n))&&H.test(n)?n.replace(K,nt):n},An.escapeRegExp=function(n){return(n=Iu(n))&&en.test(n)?n.replace(rn,"\\$&"):n},An.every=function(n,t,r){var e=ff(n)?u:bt;return r&&Oe(n,t,r)&&(t=T),e(n,ye(t,3))},An.find=Fo,An.findIndex=Ne,An.findKey=function(n,t){return p(n,ye(t,3),mt)},An.findLast=No,An.findLastIndex=Pe,An.findLastKey=function(n,t){return p(n,ye(t,3),At); -},An.floor=tc,An.forEach=nu,An.forEachRight=tu,An.forIn=function(n,t){return null==n?n:oo(n,ye(t,3),Bu)},An.forInRight=function(n,t){return null==n?n:fo(n,ye(t,3),Bu)},An.forOwn=function(n,t){return n&&mt(n,ye(t,3))},An.forOwnRight=function(n,t){return n&&At(n,ye(t,3))},An.get=Ru,An.gt=ef,An.gte=uf,An.has=function(n,t){return null!=n&&we(n,t,Rt)},An.hasIn=zu,An.head=qe,An.identity=$u,An.includes=function(n,t,r,e){return n=su(n)?n:Uu(n),r=r&&!e?Eu(r):0,e=n.length,0>r&&(r=Ui(e+r,0)),ju(n)?r<=e&&-1r&&(r=Ui(e+r,0)),v(n,t,r)):-1},An.inRange=function(n,t,r){return t=Au(t),r===T?(r=t,t=0):r=Au(r),n=Su(n),n>=Ci(t,r)&&n=n},An.isSet=pf,An.isString=ju,An.isSymbol=wu,An.isTypedArray=_f,An.isUndefined=function(n){return n===T},An.isWeakMap=function(n){return yu(n)&&"[object WeakMap]"==vo(n)},An.isWeakSet=function(n){return yu(n)&&"[object WeakSet]"==Ot(n)},An.join=function(n,t){return null==n?"":Bi.call(n,t)},An.kebabCase=Lf,An.last=Ve,An.lastIndexOf=function(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var u=e;if(r!==T&&(u=Eu(r),u=0>u?Ui(e+u,0):Ci(u,e-1)), -t===t){for(r=u+1;r--&&n[r]!==t;);n=r}else n=_(n,d,u,true);return n},An.lowerCase=Uf,An.lowerFirst=Cf,An.lt=vf,An.lte=gf,An.max=function(n){return n&&n.length?xt(n,$u,It):T},An.maxBy=function(n,t){return n&&n.length?xt(n,ye(t,2),It):T},An.mean=function(n){return y(n,$u)},An.meanBy=function(n,t){return y(n,ye(t,2))},An.min=function(n){return n&&n.length?xt(n,$u,Kt):T},An.minBy=function(n,t){return n&&n.length?xt(n,ye(t,2),Kt):T},An.stubArray=qu,An.stubFalse=Vu,An.stubObject=function(){return{}},An.stubString=function(){ -return""},An.stubTrue=function(){return true},An.multiply=rc,An.nth=function(n,t){return n&&n.length?Qt(n,Eu(t)):T},An.noConflict=function(){return $n._===this&&($n._=si),this},An.noop=Pu,An.now=Go,An.pad=function(n,t,r){n=Iu(n);var e=(t=Eu(t))?D(n):0;return!t||e>=t?n:(t=(t-e)/2,ne(Ii(t),r)+n+ne(Oi(t),r))},An.padEnd=function(n,t,r){n=Iu(n);var e=(t=Eu(t))?D(n):0;return t&&et){var e=n;n=t,t=e}return r||n%1||t%1?(r=Ti(),Ci(n+r*(t-n+Cn("1e-"+((r+"").length-1))),t)):ir(n,t)},An.reduce=function(n,t,r){var e=ff(n)?l:j,u=3>arguments.length;return e(n,ye(t,4),r,u,uo)},An.reduceRight=function(n,t,r){var e=ff(n)?s:j,u=3>arguments.length; -return e(n,ye(t,4),r,u,io)},An.repeat=function(n,t,r){return t=(r?Oe(n,t,r):t===T)?1:Eu(t),or(Iu(n),t)},An.replace=function(){var n=arguments,t=Iu(n[0]);return 3>n.length?t:t.replace(n[1],n[2])},An.result=function(n,t,r){t=Sr(t,n);var e=-1,u=t.length;for(u||(u=1,n=T);++en||9007199254740991=i)return n;if(i=r-D(e),1>i)return e;if(r=o?Or(o,0,i).join(""):n.slice(0,i),u===T)return r+e;if(o&&(i+=r.length-i),hf(u)){if(n.slice(i).search(u)){ -var f=r;for(u.global||(u=Xu(u.source,Iu(_n.exec(u))+"g")),u.lastIndex=0;o=u.exec(f);)var c=o.index;r=r.slice(0,c===T?i:c)}}else n.indexOf(yr(u),i)!=i&&(u=r.lastIndexOf(u),-1e.__dir__?"Right":"")}),e},Un.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse()}}),r(["filter","map","takeWhile"],function(n,t){ -var r=t+1,e=1==r||3==r;Un.prototype[n]=function(n){var t=this.clone();return t.__iteratees__.push({iteratee:ye(n,3),type:r}),t.__filtered__=t.__filtered__||e,t}}),r(["head","last"],function(n,t){var r="take"+(t?"Right":"");Un.prototype[n]=function(){return this[r](1).value()[0]}}),r(["initial","tail"],function(n,t){var r="drop"+(t?"":"Right");Un.prototype[n]=function(){return this.__filtered__?new Un(this):this[r](1)}}),Un.prototype.compact=function(){return this.filter($u)},Un.prototype.find=function(n){ -return this.filter(n).head()},Un.prototype.findLast=function(n){return this.reverse().find(n)},Un.prototype.invokeMap=fr(function(n,t){return typeof n=="function"?new Un(this):this.map(function(r){return Lt(r,n,t)})}),Un.prototype.reject=function(n){return this.filter(au(ye(n)))},Un.prototype.slice=function(n,t){n=Eu(n);var r=this;return r.__filtered__&&(0t)?new Un(r):(0>n?r=r.takeRight(-n):n&&(r=r.drop(n)),t!==T&&(t=Eu(t),r=0>t?r.dropRight(-t):r.take(t-n)),r)},Un.prototype.takeRightWhile=function(n){ -return this.reverse().takeWhile(n).reverse()},Un.prototype.toArray=function(){return this.take(4294967295)},mt(Un.prototype,function(n,t){var r=/^(?:filter|find|map|reject)|While$/.test(t),e=/^(?:head|last)$/.test(t),u=An[e?"take"+("last"==t?"Right":""):t],i=e||/^find/.test(t);u&&(An.prototype[t]=function(){function t(n){return n=u.apply(An,a([n],f)),e&&h?n[0]:n}var o=this.__wrapped__,f=e?[1]:arguments,c=o instanceof Un,l=f[0],s=c||ff(o);s&&r&&typeof l=="function"&&1!=l.length&&(c=s=false);var h=this.__chain__,p=!!this.__actions__.length,l=i&&!h,c=c&&!p; -return!i&&s?(o=c?o:new Un(this),o=n.apply(o,f),o.__actions__.push({func:Qe,args:[t],thisArg:T}),new On(o,h)):l&&c?n.apply(this,f):(o=this.thru(t),l?e?o.value()[0]:o.value():o)})}),r("pop push shift sort splice unshift".split(" "),function(n){var t=ri[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:pop|shift)$/.test(n);An.prototype[n]=function(){var n=arguments;if(e&&!this.__chain__){var u=this.value();return t.apply(ff(u)?u:[],n)}return this[r](function(r){return t.apply(ff(r)?r:[],n)}); -}}),mt(Un.prototype,function(n,t){var r=An[t];if(r){var e=r.name+"";oi.call(Gi,e)||(Gi[e]=[]),Gi[e].push({name:t,func:r})}}),Gi[Jr(T,2).name]=[{name:"wrapper",func:T}],Un.prototype.clone=function(){var n=new Un(this.__wrapped__);return n.__actions__=Ur(this.__actions__),n.__dir__=this.__dir__,n.__filtered__=this.__filtered__,n.__iteratees__=Ur(this.__iteratees__),n.__takeCount__=this.__takeCount__,n.__views__=Ur(this.__views__),n},Un.prototype.reverse=function(){if(this.__filtered__){var n=new Un(this); -n.__dir__=-1,n.__filtered__=true}else n=this.clone(),n.__dir__*=-1;return n},Un.prototype.value=function(){var n,t=this.__wrapped__.value(),r=this.__dir__,e=ff(t),u=0>r,i=e?t.length:0;n=i;for(var o=this.__views__,f=0,c=-1,a=o.length;++c=this.__values__.length;return{done:n,value:n?T:this.__values__[this.__index__++]}},An.prototype.plant=function(n){ -for(var t,r=this;r instanceof En;){var e=Fe(r);e.__index__=0,e.__values__=T,t?u.__wrapped__=e:t=e;var u=e,r=r.__wrapped__}return u.__wrapped__=n,t},An.prototype.reverse=function(){var n=this.__wrapped__;return n instanceof Un?(this.__actions__.length&&(n=new Un(this)),n=n.reverse(),n.__actions__.push({func:Qe,args:[Ge],thisArg:T}),new On(n,this.__chain__)):this.thru(Ge)},An.prototype.toJSON=An.prototype.valueOf=An.prototype.value=function(){return wr(this.__wrapped__,this.__actions__)},An.prototype.first=An.prototype.head, -wi&&(An.prototype[wi]=Xe),An}();typeof define=="function"&&typeof define.amd=="object"&&define.amd?($n._=rt, define(function(){return rt})):Nn?((Nn.exports=rt)._=rt,Fn._=rt):$n._=rt}).call(this); diff --git a/net/ftp-proxy/Makefile b/net/ftp-proxy/Makefile index 78c8fc10ba..4f655e4bac 100644 --- a/net/ftp-proxy/Makefile +++ b/net/ftp-proxy/Makefile @@ -1,6 +1,6 @@ PLUGIN_NAME= ftp-proxy PLUGIN_VERSION= 1.0 -PLUGIN_REVISION= 3 +PLUGIN_REVISION= 4 PLUGIN_COMMENT= Control ftp-proxy processes PLUGIN_MAINTAINER= frank.brendel@eurolog.com diff --git a/net/ftp-proxy/src/etc/rc.d/os-ftp-proxy b/net/ftp-proxy/src/etc/rc.d/os-ftp-proxy index 76c45ddc8b..b462b6cade 100755 --- a/net/ftp-proxy/src/etc/rc.d/os-ftp-proxy +++ b/net/ftp-proxy/src/etc/rc.d/os-ftp-proxy @@ -1,8 +1,5 @@ #!/bin/sh # -# $FreeBSD$ -# - # PROVIDE: os-ftp-proxy # REQUIRE: DAEMON pf # KEYWORD: shutdown diff --git a/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/Api/ServiceController.php b/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/Api/ServiceController.php index 6bc2f58537..dec43eeac0 100644 --- a/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/Api/ServiceController.php +++ b/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/Api/ServiceController.php @@ -41,9 +41,6 @@ class ServiceController extends ApiControllerBase public function statusAction($uuid) { $result = array("result" => "failed", "function" => "status"); - if ($this->request->isPost()) { - $this->sessionClose(); - } if ($uuid != null) { $mdlFtpProxy = new FtpProxy(); $node = $mdlFtpProxy->getNodeByReference('ftpproxy.' . $uuid); @@ -62,9 +59,6 @@ public function statusAction($uuid) public function startAction($uuid) { $result = array("result" => "failed", "function" => "start"); - if ($this->request->isPost()) { - $this->sessionClose(); - } if ($uuid != null) { $mdlFtpProxy = new FtpProxy(); $node = $mdlFtpProxy->getNodeByReference('ftpproxy.' . $uuid); @@ -83,9 +77,6 @@ public function startAction($uuid) public function stopAction($uuid) { $result = array("result" => "failed", "function" => "stop"); - if ($this->request->isPost()) { - $this->sessionClose(); - } if ($uuid != null) { $mdlFtpProxy = new FtpProxy(); $node = $mdlFtpProxy->getNodeByReference('ftpproxy.' . $uuid); @@ -103,9 +94,6 @@ public function stopAction($uuid) */ public function restartAction($uuid) { - if ($this->request->isPost()) { - $this->sessionClose(); - } if ($uuid != null) { $mdlFtpProxy = new FtpProxy(); $node = $mdlFtpProxy->getNodeByReference('ftpproxy.' . $uuid); @@ -123,9 +111,6 @@ public function restartAction($uuid) public function configAction() { $result = array("result" => "failed", "function" => "config"); - if ($this->request->isPost()) { - $this->sessionClose(); - } $result['result'] = $this->callBackend('template'); return $result; } @@ -136,9 +121,6 @@ public function configAction() */ public function reloadAction() { - if ($this->request->isPost()) { - $this->sessionClose(); - } $result = $this->configAction(); if ($result['result'] == 'OK') { $result['function'] = "reload"; diff --git a/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/Api/SettingsController.php b/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/Api/SettingsController.php index 4e570b30d2..1b25ff5e04 100644 --- a/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/Api/SettingsController.php +++ b/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/Api/SettingsController.php @@ -1,31 +1,29 @@ sessionClose(); $fields = array( "enabled", "listenaddress", diff --git a/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/ItemController.php b/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/ItemController.php index 679fc52da1..a9f30d596b 100644 --- a/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/ItemController.php +++ b/net/ftp-proxy/src/opnsense/mvc/app/controllers/OPNsense/FtpProxy/ItemController.php @@ -36,5 +36,4 @@ */ class ItemController extends \OPNsense\Base\IndexController { - } diff --git a/net/ftp-proxy/src/opnsense/mvc/app/models/OPNsense/FtpProxy/FtpProxy.xml b/net/ftp-proxy/src/opnsense/mvc/app/models/OPNsense/FtpProxy/FtpProxy.xml index 34d241a977..f105108f07 100644 --- a/net/ftp-proxy/src/opnsense/mvc/app/models/OPNsense/FtpProxy/FtpProxy.xml +++ b/net/ftp-proxy/src/opnsense/mvc/app/models/OPNsense/FtpProxy/FtpProxy.xml @@ -1,77 +1,77 @@ - //OPNsense/ftpproxies - 1.0.0 - Ftp Proxy settings - - - - 1 - Y - - - Y - 127.0.0.1 - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-4]|2[0-5][0-9]|[01]?[0-9][0-9]?)$/ - Listen address must be a valid IPv4 address - - - 8021 - Y - 1 - 65535 - Listen port needs to be an integer value between 1 and 65535 - - - N - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-4]|2[0-5][0-9]|[01]?[0-9][0-9]?)$/ - Source address must be a valid IPv4 address - - - 0 - N - - - 86400 - N - 1 - 86400 - Idle timeout needs to be an integer value between 1 and 86400 - - - 100 - N - 1 - 500 - Maximum number of concurrent FTP sessions needs to be an integer value between 1 and 500 - - - N - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-4]|2[0-5][0-9]|[01]?[0-9][0-9]?)$/ - Reverse address must be a valid IPv4 address - - - 21 - N - 1 - 65535 - Reverse port needs to be an integer value between 1 and 65535 - - - 0 - N - - - 5 - N - 0 - 7 - Debug level needs to be an integer value between 0 and 7 - - - N - /^([\t\n\v\f\r 0-9a-zA-Z.,_\x{00A0}-\x{FFFF}]){1,255}$/u - Enter a description. - - - + //OPNsense/ftpproxies + 1.0.0 + Ftp Proxy settings + + + + 1 + Y + + + Y + 127.0.0.1 + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-4]|2[0-5][0-9]|[01]?[0-9][0-9]?)$/ + Listen address must be a valid IPv4 address + + + 8021 + Y + 1 + 65535 + Listen port needs to be an integer value between 1 and 65535 + + + N + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-4]|2[0-5][0-9]|[01]?[0-9][0-9]?)$/ + Source address must be a valid IPv4 address + + + 0 + N + + + 86400 + N + 1 + 86400 + Idle timeout needs to be an integer value between 1 and 86400 + + + 100 + N + 1 + 500 + Maximum number of concurrent FTP sessions needs to be an integer value between 1 and 500 + + + N + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-4]|2[0-5][0-9]|[01]?[0-9][0-9]?)$/ + Reverse address must be a valid IPv4 address + + + 21 + N + 1 + 65535 + Reverse port needs to be an integer value between 1 and 65535 + + + 0 + N + + + 5 + N + 0 + 7 + Debug level needs to be an integer value between 0 and 7 + + + N + /^([\t\n\v\f\r 0-9a-zA-Z.,_\x{00A0}-\x{FFFF}]){1,255}$/u + Enter a description. + + + diff --git a/net/ftp-proxy/src/opnsense/mvc/app/views/OPNsense/FtpProxy/index.volt b/net/ftp-proxy/src/opnsense/mvc/app/views/OPNsense/FtpProxy/index.volt index 58f543270e..7a51ca9e53 100644 --- a/net/ftp-proxy/src/opnsense/mvc/app/views/OPNsense/FtpProxy/index.volt +++ b/net/ftp-proxy/src/opnsense/mvc/app/views/OPNsense/FtpProxy/index.volt @@ -34,8 +34,8 @@ POSSIBILITY OF SUCH DAMAGE. */ function openDialog(uuid) { var editDlg = "DialogEdit"; - var setUrl = "/api/ftpproxy/settings/setProxy/"; - var getUrl = "/api/ftpproxy/settings/getProxy/"; + var setUrl = "/api/ftpproxy/settings/set_proxy/"; + var getUrl = "/api/ftpproxy/settings/get_proxy/"; var urlMap = {}; urlMap['frm_' + editDlg] = getUrl + uuid; mapDataToFormUI(urlMap).done(function () { @@ -56,12 +56,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-proxies").UIBootgrid( - { 'search':'/api/ftpproxy/settings/searchProxy', - 'get':'/api/ftpproxy/settings/getProxy/', - 'set':'/api/ftpproxy/settings/setProxy/', - 'add':'/api/ftpproxy/settings/addProxy/', - 'del':'/api/ftpproxy/settings/delProxy/', - 'toggle':'/api/ftpproxy/settings/toggleProxy/', + { 'search':'/api/ftpproxy/settings/search_proxy', + 'get':'/api/ftpproxy/settings/get_proxy/', + 'set':'/api/ftpproxy/settings/set_proxy/', + 'add':'/api/ftpproxy/settings/add_proxy/', + 'del':'/api/ftpproxy/settings/del_proxy/', + 'toggle':'/api/ftpproxy/settings/toggle_proxy/', 'options':{selection:false, multiSelect:false} } ); diff --git a/net/haproxy/Makefile b/net/haproxy/Makefile index 36875ce938..15a0d1ec64 100644 --- a/net/haproxy/Makefile +++ b/net/haproxy/Makefile @@ -1,7 +1,8 @@ PLUGIN_NAME= haproxy -PLUGIN_VERSION= 3.7 +PLUGIN_VERSION= 4.6 +PLUGIN_REVISION= 2 PLUGIN_COMMENT= Reliable, high performance TCP/HTTP load balancer -PLUGIN_DEPENDS= haproxy22 +PLUGIN_DEPENDS= haproxy30 py${PLUGIN_PYTHON}-haproxy-cli PLUGIN_MAINTAINER= opnsense@moov.de .include "../../Mk/plugins.mk" diff --git a/net/haproxy/pkg-descr b/net/haproxy/pkg-descr index 0609ea6f36..13a4f948b6 100644 --- a/net/haproxy/pkg-descr +++ b/net/haproxy/pkg-descr @@ -6,6 +6,142 @@ very high loads while needing persistence or Layer7 processing. Plugin Changelog ================ +4.6 + +Changed: +* improve help text for "http-request redirect" rules (#4650) +* rename "http-request redirect" input field (#4650) + +4.5 + +Changed: +* upgrade to HAProxy 3.0 release series (#4411) +* migrate cert export to Trust MVC + +4.4 + +Fixed: +* Cron job "Sync SSL certificate changes" not working (#4035) +* Template error with empty user group (#3364) + +4.3 + +Added: +* Add new global parameter: DNS prefer IP family (#3779) + +Fixed: +* SNI not working when automatic OCSP updates are enabled (#3779) +* HAProxy error: has an OCSP URI but an error occurred (#3779) + +Changed: +* prefer IPv4 results when resolving DNS names (#3779) +* disable OCSP updates if cert contains no OCSP data (#3779) + +4.2 + +Added: +* add support for built-in OCSP update feature +* add support for forwarded header (RFC7239) +* add option "X-Forwarded-For Header" to backend settings +* add options for HTTP/2 performance tuning + +Fixed: +* fix SSL sync cron job (bulk sync was never working properly) + +Changed: +* upgrade to HAProxy 2.8 release series (#3459) +* change default for HTTP/2 to enabled (only new frontends/backends) +* add "no-alpn" option if HTTP/2 is not enabled (only TLS-enabled frontends) +* move OCSP settings from "Service" to "Global" section +* replace bundled haproxyctl library with haproxy-cli + +Deprecated: +* frontend option "X-Forwarded-For Header" (the backend option should be used) + +Removed: +* remove OSCP update cron job + +4.1 + +Fixed: +* fix SSL preferences in health checks (#3221) + +4.0 + +Added: +* add new service option "Gradual connection close time" (close-spread-time) (#3026) +* add new frontend option "shards" (#3026) + +Changed: +* upgrade to HAProxy 2.6 release series (#3026) +* rename frontend option "Type" to "Connection Mode" (#3026) +* migrate options "http-tunnel" and "forceclose" to "http-keep-alive" (#3026) +* replace "process" with "threads" bind keyword for CPU Affinity (#3026) +* no longer duplicate global defaults in backends/frontends (#2642) + +Removed: +* remove Processes/nbproc option (use Threads/nbthread instead) (#3026) +* remove "Process ID" from CPU Affinity settings (now always 1) (#3026) +* remove "bind-process" option (replaced by the "threads" bind keyword) (#3026) +* remove options "http-tunnel" and "forceclose" from "Connection Mode" (#3026) + +3.12 + +Added: +* add support for req.ssl_hello_type (#2311) +* add support for Prometheus exporter (#2764) +* add support for FastCGI applications (#2769) +* add server option to override the multiplexer protocol + +Fixed: +* fix unix sockets in chrooted environment (#3093) +* fix peers by automatically configuring the local peer (#3114) + +Changed: +* update HAProxy documentation URLs + +3.11 + +Added: +* add support for cache parameter (#2908) + +3.10 + +WARNING: This release switches to the HAProxy 2.4 release series, +which may result in incompatible changes for some users. + +Added: +* add support for DNS resolution over TCP (#2644) + +Changed: +* upgrade to HAProxy 2.4 release series (#2644) +* disable strict-limits for safekeeping (#2644) + +Removed: +* remove deprecated option tune.chksize (#2644) + +3.9 + +Added: +* add SSL SNI setting to servers and health checks (#2388) + +Fixed: +* fix custom TCP health checks (#2653) + +Changed: +* replace "force SSL" setting with "SSL preferences" in health checks (#2388) +* health check port is no longer an advanced option + +3.8 + +Added: +* add support for unix sockets (#2040) +* add "max connections" option to servers (#2641) + +Changed: +* allow setting "max connections" to "0" (unlimited) +* raise maximum value for "max connections" to 10000000 + 3.7 Added: @@ -95,6 +231,7 @@ Fixed: * prevent the deletion of items that are still referenced elsewhere (core/#1897) Changed: +* upgrade to HAProxy 2.2 release series (#2092) * change default SSL version to TLSv1.2 (ssl-min-ver) * remove weak ciphers from (default) SSL settings * remove default SSL bind options that would conflict with ssl-min-ver diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/ServiceController.php b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/ServiceController.php index 524597aef0..0632cef861 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/ServiceController.php +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/ServiceController.php @@ -56,8 +56,6 @@ public function configtestAction() $backend = new Backend(); // first generate template based on current configuration $backend->configdRun('template reload OPNsense/HAProxy'); - // now export all the required files (or syntax check will fail) - $backend->configdRun("haproxy setup"); // finally run the syntax check $response = $backend->configdRun("haproxy configtest"); return array("result" => $response); diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/SettingsController.php b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/SettingsController.php index 44ccd006c2..4231ca119b 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/SettingsController.php +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/Api/SettingsController.php @@ -1,7 +1,7 @@ searchBase('luas.lua', array('enabled', 'name', 'description'), 'name'); } + public function getFcgiAction($uuid = null) + { + return $this->getBase('fcgi', 'fcgis.fcgi', $uuid); + } + + public function setFcgiAction($uuid) + { + return $this->setBase('fcgi', 'fcgis.fcgi', $uuid); + } + + public function addFcgiAction() + { + return $this->addBase('fcgi', 'fcgis.fcgi'); + } + + public function delFcgiAction($uuid) + { + return $this->delBase('fcgis.fcgi', $uuid); + } + + public function searchFcgisAction() + { + return $this->searchBase('fcgis.fcgi', array('name', 'description'), 'name'); + } + public function getErrorfileAction($uuid = null) { return $this->getBase('errorfile', 'errorfiles.errorfile', $uuid); @@ -318,7 +343,7 @@ public function toggleCpuAction($uuid, $enabled = null) public function searchCpusAction() { - return $this->searchBase('cpus.cpu', array('enabled', 'name', 'process_id', 'thread_id', 'cpu_id'), 'name'); + return $this->searchBase('cpus.cpu', array('enabled', 'name', 'thread_id', 'cpu_id'), 'name'); } public function getGroupAction($uuid = null) diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/IndexController.php b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/IndexController.php index 8e2b52f65b..791f6bfe67 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/IndexController.php +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/IndexController.php @@ -1,7 +1,7 @@ view->formDialogBackend = $this->getForm("dialogBackend"); $this->view->formDialogCpu = $this->getForm("dialogCpu"); $this->view->formDialogErrorfile = $this->getForm("dialogErrorfile"); + $this->view->formDialogFcgi = $this->getForm("dialogFcgi"); $this->view->formDialogFrontend = $this->getForm("dialogFrontend"); $this->view->formDialogGroup = $this->getForm("dialogGroup"); $this->view->formDialogHealthcheck = $this->getForm("dialogHealthcheck"); diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAcl.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAcl.xml index 4643f99a87..895bd6e4b2 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAcl.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAcl.xml @@ -278,6 +278,17 @@ text + + + header + + + + acl.ssl_hello_type + + dropdown + + header diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAction.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAction.xml index f0d17e77e2..0386b9f1ae 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAction.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogAction.xml @@ -87,9 +87,9 @@ action.http_request_redirect - + text - HAProxy's documentation for further details and examples.]]> + HAProxy's documentation for further details and examples.]]> @@ -128,7 +128,7 @@ action.http_request_add_header_content text - HAProxy's documentation for further details and examples.]]> + HAProxy's documentation for further details and examples.]]> @@ -145,7 +145,7 @@ action.http_request_set_header_content text - HAProxy's documentation for further details and examples.]]> + HAProxy's documentation for further details and examples.]]> @@ -251,7 +251,7 @@ action.http_response_add_header_content text - HAProxy's documentation for further details and examples.]]> + HAProxy's documentation for further details and examples.]]> @@ -268,7 +268,7 @@ action.http_response_set_header_content text - HAProxy's documentation for further details and examples.]]> + HAProxy's documentation for further details and examples.]]> @@ -448,4 +448,26 @@ dropdown + + + header + + + + action.fcgi_pass_header + + text + + + + + header + + + + action.fcgi_set_param + + text + Custom Log format rules. With this directive, it is possible to overwrite the value of default FastCGI parameters.]]> + diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml index 31e0eda988..014b959aeb 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogBackend.xml @@ -28,7 +28,7 @@ backend.algorithm dropdown - HAProxy documentation for a full description.]]> + HAProxy documentation for a full description.]]> Choose a load balancing algorithm. @@ -42,7 +42,7 @@ backend.proxyProtocol dropdown - HAProxy documentation for a full description.]]> + HAProxy documentation for a full description.]]> true @@ -54,6 +54,12 @@ Type server name or choose from list. + + backend.linkedFcgi + + dropdown + + backend.linkedResolver @@ -167,6 +173,27 @@ true + + backend.forwardedHeader + + checkbox + + + + backend.forwardedHeaderParameters + + select_multiple + + true + true + HAProxy documentation for a full description.]]> + + + backend.forwardFor + + checkbox + + header @@ -186,7 +213,7 @@ backend.persistence_cookiemode dropdown - HAProxy documentation for a full description.]]> + HAProxy documentation for a full description.]]> backend.persistence_cookiename @@ -208,14 +235,14 @@ backend.stickiness_pattern dropdown - HAProxy documentation for a full description.
    NOTE: Consider not using this feature in multi-process mode, it can result in random behaviours.
    ]]>
    + HAProxy documentation for a full description.
    NOTE: Consider not using this feature in multi-process mode, it can result in random behaviours.
    ]]>
    Choose a persistence type.
    backend.stickiness_dataTypes select_multiple - HAProxy documentation for a full description.]]> + HAProxy documentation for a full description.]]> backend.stickiness_expire diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogCpu.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogCpu.xml index a1d97f46ee..8e0b705960 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogCpu.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogCpu.xml @@ -11,12 +11,6 @@ text Choose a name for this CPU affinity rule. - - cpu.process_id - - dropdown - Process ID that should bind to a specific CPU set. Any process IDs above nbproc are ignored. - cpu.thread_id diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFcgi.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFcgi.xml new file mode 100644 index 0000000000..4ad9d1e94e --- /dev/null +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFcgi.xml @@ -0,0 +1,77 @@ +
    + + fcgi.enabled + + checkbox + Enable this FastCGI application. + + + fcgi.name + + text + Name to identify this FastCGI application. + + + fcgi.description + + text + Description for this FastCGI application. + + + fcgi.docroot + + text + + + + fcgi.index + + text + + + + fcgi.path_info + + text + HAProxy's documentation for further details and examples.]]> + + + fcgi.log_stderr + + checkbox + + + + fcgi.keep_conn + + checkbox + + + + fcgi.get_values + + checkbox + + + + fcgi.mpxs_conns + + checkbox + + + + fcgi.max_reqs + + text + + + + fcgi.linkedActions + + select_multiple + + true + + Choose rules. + +
    diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml index 14e6448499..ded45f08c2 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml @@ -23,7 +23,7 @@ select_multiple true - + Enter address:port here. Finish with TAB.
    @@ -207,9 +207,30 @@ frontend.forwardFor - + checkbox - + + + + frontend.prometheus_enabled + + checkbox + + true + + + frontend.prometheus_path + + text + + true + + + frontend.connectionBehaviour + + dropdown + keep-alive mode with regards to persistent connections. Option "httpclose" configures HAProxy to close connections with the server and the client as soon as the request and the response are received. It will also check if a "Connection: close" header is already set in each direction, and will add one if missing. Option "http-server-close" enables HTTP connection-close mode on the server side while keeping the ability to support HTTP keep-alive and pipelining on the client side.]]> + true @@ -242,7 +263,7 @@ frontend.tuning_maxConnections - + text @@ -276,6 +297,13 @@ Choose CPU affinity rules. true + + frontend.tuning_shards + + text + + true + header @@ -322,14 +350,14 @@ frontend.stickiness_pattern dropdown - HAProxy documentation for further information.]]> + HAProxy documentation for further information.]]> Choose a stick-table type. frontend.stickiness_dataTypes select_multiple - HAProxy documentation for a full description.]]> + HAProxy documentation for a full description.]]> frontend.stickiness_expire @@ -356,7 +384,7 @@ frontend.stickiness_counter_key text - HAProxy documentation for a full description.]]> + HAProxy documentation for a full description.]]> true @@ -412,13 +440,6 @@ header - - frontend.connectionBehaviour - - dropdown - keep-alive mode with regards to persistent connections. Option "http-tunnel" disables any HTTP processing past the first request and the first response. Option "httpclose" configures HAProxy to work in HTTP tunnel mode and check if a "Connection: close" header is already set in each direction, and will add one if missing. Option "http-server-close" enables HTTP connection-close mode on the server side while keeping the ability to support HTTP keep-alive and pipelining on the client side. With Option "forceclose" HAProxy will actively close the outgoing server channel as soon as the server has finished to respond and release some resources earlier.]]> - true - frontend.customOptions diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogHealthcheck.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogHealthcheck.xml index a3d23ffbc6..179ef4b8e0 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogHealthcheck.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogHealthcheck.xml @@ -17,24 +17,29 @@ dropdown + + healthcheck.ssl + + dropdown + + + + healthcheck.sslSNI + + text + + healthcheck.interval text - - healthcheck.force_ssl - - checkbox - - healthcheck.checkport text - - true + @@ -91,7 +96,7 @@ healthcheck.http_value text -
    NOTE: It is important to note that the responses will be limited to a certain size defined by the global "tune.chksize" option, which defaults to 16384 bytes.
    ]]>
    +
    @@ -107,7 +112,7 @@ healthcheck.tcp_sendValue text -
    NOTE: It is important to note that the responses will be limited to a certain size defined by the global "tune.chksize" option, which defaults to 16384 bytes.
    ]]>
    +
    healthcheck.tcp_matchType diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogMapfile.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogMapfile.xml index f571ce69c3..554246fa45 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogMapfile.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogMapfile.xml @@ -15,6 +15,6 @@ mapfile.content textbox - HAProxy documentation for a full description.]]> + HAProxy documentation for a full description.]]> diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogResolver.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogResolver.xml index a769aead4c..73ca63cdce 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogResolver.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogResolver.xml @@ -24,7 +24,7 @@ true true - + Enter ip:port here. Finish with TAB. @@ -55,7 +55,7 @@ resolver.accepted_payload_size text - + true diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogServer.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogServer.xml index 1436580cf5..99db8ffab0 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogServer.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogServer.xml @@ -67,9 +67,21 @@ Type option name or choose from list. + + + header + + + + server.unix_socket + + dropdown + Select the frontend that provides the UNIX socket. This UNIX socket will be used as the server's address, making it possible to send connections to this frontend. Only frontends that provide the unix@ pattern as listen address can be selected. + header + server.port @@ -83,6 +95,13 @@ dropdown + + server.multiplexer_protocol + + dropdown + + true + server.resolvePrefer @@ -96,6 +115,12 @@ checkbox + + server.sslSNI + + text + + server.sslVerify @@ -107,7 +132,7 @@ select_multiple true - To import additional CAs, go to Certificate Manager.]]> + To import additional CAs, go to Authority Manager.]]> Type CA name or choose from list. @@ -126,6 +151,13 @@ Type certificate name or choose from list. true + + server.maxConnections + + text + + true + server.weight diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalCache.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalCache.xml index d9fd948b39..b38486734b 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalCache.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalCache.xml @@ -27,4 +27,16 @@ text + + haproxy.general.cache.processVary + + checkbox + + + + haproxy.general.cache.maxSecondaryEntries + + text + + diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalDefaults.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalDefaults.xml index 4a44e0aee1..81a9700f7e 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalDefaults.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalDefaults.xml @@ -5,9 +5,15 @@ haproxy.general.defaults.maxConnections - + text - + + + + haproxy.general.defaults.maxConnectionsServers + + text + haproxy.general.defaults.timeoutClient diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalPeers.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalPeers.xml index f1091f4f35..7dba7f9f8c 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalPeers.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalPeers.xml @@ -10,14 +10,14 @@ - + header haproxy.general.peers.name1 text - + system hostname, then this peer is automatically configured as local peer.]]> haproxy.general.peers.listen1 @@ -32,14 +32,14 @@ - + header haproxy.general.peers.name2 text - + system hostname, then this peer is automatically configured as local peer.]]> haproxy.general.peers.listen2 diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalSettings.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalSettings.xml index aa3f791ff0..6c6a1a22e6 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalSettings.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalSettings.xml @@ -21,18 +21,18 @@ text + + haproxy.general.closeSpreadTime + + text + + haproxy.general.seamlessReload checkbox - - haproxy.general.storeOcsp - - checkbox - - haproxy.general.showIntro diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalStats.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalStats.xml index 6832e1e173..ad63e778ef 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalStats.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalStats.xml @@ -5,7 +5,7 @@ haproxy.general.stats.enabled - + checkbox @@ -57,4 +57,29 @@
    NOTE: The syntax will not be checked, use at your own risk!
    ]]>
    true + + + header + + + haproxy.general.stats.prometheus_enabled + + checkbox + + + + haproxy.general.stats.prometheus_bind + + select_multiple + + true + + Enter address:port here. Finish with TAB. + + + haproxy.general.stats.prometheus_path + + text + + diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalTuning.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalTuning.xml index ea55662de4..069dea4ad8 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalTuning.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/generalTuning.xml @@ -10,13 +10,6 @@
    NOTE: Running as user root could be a security issue but it may be required by some features.
    ]]>
    true - - haproxy.general.tuning.nbproc - - text -
    WARNING: This option is deprecated and will be removed in a future version of HAProxy, threads should be used instead.
    ]]>
    - true -
    haproxy.general.tuning.nbthread @@ -27,7 +20,13 @@ haproxy.general.tuning.maxConnections text -
    NOTE: HAProxy will not be able to allocate enough memory if you set this value too high. Consider raising the settings for kern.maxfiles and kern.maxfilesperproc if you need to specify a non-default value.
    ]]>
    +
    NOTE: Consider raising the settings for kern.maxfiles and kern.maxfilesperproc in System: Settings: Tunables, otherwise HAProxy will fail to open the specified number of connections.
    ]]>
    +
    + + haproxy.general.tuning.resolversPrefer + + dropdown + haproxy.general.tuning.sslServerVerify @@ -48,13 +47,6 @@
    NOTE: It is strongly recommended not to change this from the default value, as very low values will break some services such as statistics, and values larger than default size will increase memory usage, possibly causing the system to run out of memory.
    ]]>
    true
    - - haproxy.general.tuning.checkBufferSize - - text - - true - haproxy.general.tuning.luaMaxMem @@ -81,6 +73,28 @@
    NOTE: The syntax will not be checked, use at your own risk!
    ]]>
    true
    + + + header + + + haproxy.general.tuning.ocspUpdateEnabled + + checkbox + + + + haproxy.general.tuning.ocspUpdateMinDelay + + text + + + + haproxy.general.tuning.ocspUpdateMaxDelay + + text + + header @@ -124,4 +138,44 @@ true + + + header + + + haproxy.general.tuning.h2_initialWindowSize + + text + + + + haproxy.general.tuning.h2_initialWindowSizeOutgoing + + text + + + + haproxy.general.tuning.h2_initialWindowSizeIncoming + + text + + + + haproxy.general.tuning.h2_maxConcurrentStreams + + text + + + + haproxy.general.tuning.h2_maxConcurrentStreamsOutgoing + + text + + + + haproxy.general.tuning.h2_maxConcurrentStreamsIncoming + + text + + diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/maintenanceCronjobs.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/maintenanceCronjobs.xml index f9c4edf83f..1924980d29 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/maintenanceCronjobs.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/maintenanceCronjobs.xml @@ -10,17 +10,6 @@ checkbox Automation instead of this cron job.]]> - - - header - - - - haproxy.maintenance.cronjobs.updateOcsp - - checkbox - HAProxy service settings.]]> - header diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.php b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.php index 1028b45b36..7dade59ea6 100644 --- a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.php +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.php @@ -138,19 +138,19 @@ public function getByAclID($uuid) /** * create a new ACL * @param string $name - * @param string $description * @param string $expression + * @param string $description * @param string $negate * @param hash $parameters * @return string */ - public function newAcl($name, $description = "", $expression, $negate = "0", $parameters = array()) + public function newAcl($name, $expression, $description = "", $negate = "0", $parameters = array()) { $acl = $this->acls->acl->Add(); $uuid = $acl->getAttributes()['uuid']; $acl->name = $name; - $acl->description = $description; $acl->expression = $expression; + $acl->description = $description; $acl->negate = $negate; foreach ($parameters as $key => $value) { $acl->$key = $value; @@ -161,11 +161,11 @@ public function newAcl($name, $description = "", $expression, $negate = "0", $pa /** * create a new action * @param string $name - * @param string $description * @param string $testType + * @param string $type + * @param string $description * @param string $linkedAcls * @param string $operator - * @param string $type * @param string $useBackend * @param string $useServer * @param string $actionName @@ -173,16 +173,16 @@ public function newAcl($name, $description = "", $expression, $negate = "0", $pa * @param string $actionValue * @return string */ - public function newAction($name, $description = "", $testType, $linkedAcls = "", $operator = "and", $type, $parameters = array()) + public function newAction($name, $testType, $type, $description = "", $linkedAcls = "", $operator = "and", $parameters = array()) { $action = $this->actions->action->Add(); $uuid = $action->getAttributes()['uuid']; $action->name = $name; - $action->description = $description; $action->testType = $testType; + $action->type = $type; + $action->description = $description; $action->linkedAcls = $linkedAcls; $action->operator = $operator; - $action->type = $type; foreach ($parameters as $key => $value) { $action->$key = $value; } @@ -192,24 +192,24 @@ public function newAction($name, $description = "", $testType, $linkedAcls = "", /** * create a new server * @param string $name - * @param string $description * @param string $address * @param string $port * @param string $mode + * @param string $description * @param string $ssl * @param string $sslVerify * @param string $weight * @return string */ - public function newServer($name, $description = "", $address, $port, $mode, $ssl = "0", $sslVerify = "1", $weight = "") + public function newServer($name, $address, $port, $mode, $description = "", $ssl = "0", $sslVerify = "1", $weight = "") { $srv = $this->servers->server->Add(); $uuid = $srv->getAttributes()['uuid']; $srv->name = $name; - $srv->description = $description; $srv->address = $address; $srv->port = $port; $srv->mode = $mode; + $srv->description = $description; $srv->ssl = $ssl; $srv->sslVerify = $sslVerify; $srv->weight = $weight; @@ -218,24 +218,24 @@ public function newServer($name, $description = "", $address, $port, $mode, $ssl /** * create a new backend - * @param string $enabled * @param string $name - * @param string $description * @param string $mode * @param string $algorithm + * @param string $enabled + * @param string $description * @param string $linkedServers * @param string $linkedActions * @return string */ - public function newBackend($enabled = "0", $name, $description = "", $mode, $algorithm, $linkedServers = "", $linkedActions = "") + public function newBackend($name, $mode, $algorithm, $enabled = "0", $description = "", $linkedServers = "", $linkedActions = "") { $backend = $this->backends->backend->Add(); $uuid = $backend->getAttributes()['uuid']; - $backend->enabled = $enabled; $backend->name = $name; - $backend->description = $description; $backend->mode = $mode; $backend->algorithm = $algorithm; + $backend->enabled = $enabled; + $backend->description = $description; $backend->linkedServers = $linkedServers; $backend->linkedActions = $linkedActions; return $uuid; @@ -269,7 +269,7 @@ public function linkAclToAction($acl_uuid, $action_uuid, $replace = false) return $acl_uuid; } else { // Extend existing string. - $linkedAcls .= ",${acl_uuid}"; + $linkedAcls .= ",{$acl_uuid}"; } } else { $linkedAcls = $acl_uuid; @@ -309,7 +309,7 @@ public function linkServerToBackend($server_uuid, $backend_uuid, $replace = fals return $server_uuid; } else { // Extend existing string. - $linkedServers .= ",${server_uuid}"; + $linkedServers .= ",{$server_uuid}"; } } else { $linkedServers = $server_uuid; @@ -349,7 +349,7 @@ public function linkActionToFrontend($action_uuid, $frontend_uuid, $replace = fa return $action_uuid; } else { // Extend existing string. - $linkedActions .= ",${action_uuid}"; + $linkedActions .= ",{$action_uuid}"; } } else { $linkedActions = $action_uuid; diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml index 89cfe9e0f9..e1a40e1c57 100644 --- a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml @@ -1,37 +1,43 @@ //OPNsense/HAProxy - 3.3.0 + 4.1.0 the HAProxy load balancer - 0 + 0 Y - 0 + 0 Y - 60s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 60s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N + + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". + N + - 0 + 0 Y + - 0 + 0 N - 1 + 1 - 0 + 0 Y @@ -41,7 +47,7 @@ N - 1024 + 1024 1 65535 Please specify a value between 1 and 65535. @@ -54,7 +60,7 @@ N - 1024 + 1024 1 65535 Please specify a value between 1 and 65535. @@ -63,32 +69,33 @@ - 0 + 0 Y - 1 - 500000 - Please specify a value between 1 and 500000. + 0 + 10000000 + Please specify a value between 0 and 10000000. N - - 1 - 1 - 128 - Please specify a value between 1 and 128. - Y - - 1 + 1 1 1024 Please specify a value between 1 and 1024. N + + N + ipv4 + + IPv4 + IPv6 + + Y - ignore + ignore no preference [default] enforce verify @@ -96,39 +103,32 @@ - 2048 + 2048 1024 16384 Please specify a value between 1024 and 16384. Y - 16384 + 16384 1024 1048576 Please specify a value between 1024 and 1048576. N - - 16384 - 1024 - 1048576 - Please specify a value between 1024 and 1048576. - N - - 2 + 2 0 50 Please specify a value between 0 and 50. Y - 0 + 0 Y - 0 + 0 0 1024 Please specify a value between 0 and 1024. @@ -137,13 +137,31 @@ N + + 0 + Y + + + 300 + 1 + 86400 + Please specify a value between 1 and 86400. + N + + + 3600 + 1 + 86400 + Please specify a value between 1 and 86400. + N + - 0 + 0 Y N - prefer-client-ciphers + prefer-client-ciphers Y Y @@ -164,7 +182,7 @@ N - TLSv1.2 + TLSv1.2 SSLv3 TLSv1.0 @@ -184,54 +202,96 @@ - ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256 + ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256 N - TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 N + + 0 + 10000000 + Please specify a value between 0 and 10000000. + N + + + 0 + 10000000 + Please specify a value between 0 and 10000000. + N + + + 0 + 10000000 + Please specify a value between 0 and 10000000. + N + + + 0 + 10000000 + Please specify a value between 0 and 10000000. + N + + + 0 + 10000000 + Please specify a value between 0 and 10000000. + N + + + 0 + 10000000 + Please specify a value between 0 and 10000000. + N + - 1 - 500000 - Please specify a value between 1 and 500000. + 0 + 10000000 + Please specify a value between 0 and 10000000. N + + 0 + 10000000 + Please specify a value between 0 and 10000000. + N + - 30s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 30s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 30s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 30s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 30s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 30s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N 0 100 - 3 + 3 Please specify a value between 0 and 100. Y N - x-1 + x-1 redispatch on every 3rd retry redispatch on every 2nd retry @@ -244,7 +304,7 @@ N - last,libc + last,libc Y Y @@ -259,15 +319,15 @@ - 127.0.0.1 - /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u + 127.0.0.1 + /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u lower Please specify a valid servername or IP address. Y Y - local0 + local0 alert audit @@ -297,7 +357,7 @@ N - info + info alert crit @@ -318,36 +378,36 @@ - 0 + 0 N 1024 65535 - 8822 + 8822 Please specify a value between 1024 and 65535. Y - 0 + 0 N N - Y - /^((([0-9a-zA-Z._\-]+:[0-9a-zA-Z._\-]+)([,]){0,1}))*/u + Y + /^((([0-9a-zA-Z._\-]+:[0-9a-zA-Z._\-]+)([,]){0,1}))*/u lower Please provide a valid listen address, i.e. 10.0.0.1:8080 or haproxy.example.com:8999. - 0 + 0 N N - Y - /^((([0-9a-zA-Z._\-]+:[0-9a-zA-Z._\-]+)([,]){0,1}))*/u + Y + /^((([0-9a-zA-Z._\-]+:[0-9a-zA-Z._\-]+)([,]){0,1}))*/u Please provide a valid user and password, i.e. user:secret123. @@ -359,7 +419,7 @@ Related user not found - Y + Y N @@ -371,29 +431,47 @@ Related group not found - Y + Y N N + + 0 + N + + + *:8404 + N + Y + /^((([0-9a-zA-Z._\-\*:\[\]]+:+[0-9]+(-[0-9]+)?|unix@[0-9a-z_\-]+)([,]){0,1}))*/u + lower + Please provide a valid listen address, i.e. 10.0.0.1:8404 or haproxy.example.com:8404. + + + /metrics + N + /^.{1,2048}$/u + Should be a string between 1 and 2048 characters. + - 0 + 0 N 1 4095 - 4 + 4 Y Please specify a value between 1 and 4095. 1 3600 - 60 + 60 N Please specify a value between 1 and 3600. @@ -403,6 +481,16 @@ N Please specify a value between 1 and 2146435072. + + 0 + N + + + 10 + 1 + N + Please specify a positive integer value. + @@ -411,32 +499,32 @@ N - 1 + 1 Y Y - /^([0-9a-zA-Z._\-]){1,255}$/u + /^([0-9a-zA-Z._\-]){1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. Y - Y - /^((([0-9a-zA-Z._\-\*:\[\]]+:[0-9]+(-[0-9]+)?)([,]){0,1}))*/u + Y + /^((([0-9a-zA-Z._\-\*:\[\]]+:+[0-9]+(-[0-9]+)?|unix@[0-9a-z_\-]+)([,]){0,1}))*/u lower - Please provide a valid listen address, i.e. 127.0.0.1:8080, [::1]:8080 or www.example.com:443. Port range as start-end, i.e. 127.0.0.1:1220-1240. + Please provide a valid listen address, i.e. 127.0.0.1:8080, [::1]:8080, www.example.com:443 or unix@socket-name. Port range as start-end, i.e. 127.0.0.1:1220-1240. N Y - http + http HTTP / HTTPS (SSL offloading) [default] SSL / HTTPS (TCP mode) @@ -455,7 +543,7 @@ N - 0 + 0 Y @@ -472,12 +560,12 @@ N - 0 + 0 Y N - prefer-client-ciphers + prefer-client-ciphers Y Y @@ -498,7 +586,7 @@ N - TLSv1.2 + TLSv1.2 SSLv3 TLSv1.0 @@ -518,39 +606,39 @@ - ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256 + ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256 N - TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 N - 1 + 1 Y - 0 + 0 N - 0 + 0 N - 15768000 + 15768000 1 1000000000 Please specify a value between 1 and 1000000000. Y - 0 + 0 N N - required + required none optional @@ -570,7 +658,7 @@ Please select a valid CA from the list. - 0 + 0 N @@ -582,7 +670,7 @@ Related user not found - Y + Y N @@ -594,27 +682,27 @@ Related group not found - Y + Y N - 1 - 500000 - Please specify a value between 1 and 500000. + 0 + 10000000 + Please specify a value between 0 and 10000000. N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N @@ -630,24 +718,30 @@ Y N + + 2 + 1000 + Please specify a value between 2 and 1000. + N + - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y - 0 + 0 Y @@ -681,26 +775,26 @@ Y - 30m - /^([0-9]{1,5}(?:ms|s|m|h|d)?)/u + 30m + /^([0-9]{1,5}(?:ms|s|m|h|d)?)/u lower Should be a number between 1 and 5 characters followed by either "d", "h", "m", "s" or "ms". Y - 50k - /^([0-9]{1,5}[k|m|g]{1})*/u + 50k + /^([0-9]{1,5}[k|m|g]{1})*/u lower Should be a number between 1 and 5 characters followed by either "k", "m" or "g". - 1 + 1 N - src + src N - /^([0-9a-zA-Z._]){1,32}$/u + /^([0-9a-zA-Z._]){1,32}$/u Should be a string between 1 and 32 characters. @@ -710,52 +804,52 @@ N - 10s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 10s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 10s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 10s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 10s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 10s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 10s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 10s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 1m - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 1m + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 1m - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 1m + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 0 + 1 N - 0 + 0 N N - h2,http11 + h2,http11 Y Y @@ -764,19 +858,28 @@ HTTP/1.0 + - 0 + 0 Y + + 0 + N + + + /metrics + N + /^.{1,2048}$/u + Should be a string between 1 and 2048 characters. + Y - http-keep-alive + http-keep-alive http-keep-alive [default] - http-tunnel httpclose http-server-close - forceclose @@ -792,7 +895,7 @@ Related action item not found Y - Y + Y N @@ -804,7 +907,7 @@ Related error file item not found - Y + Y N @@ -815,22 +918,22 @@ N - 1 + 1 Y Y - /^([0-9a-zA-Z._\-]){1,255}$/u + /^([0-9a-zA-Z._\-]){1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. Y - http + http HTTP (Layer 7) [default] TCP (Layer 4) @@ -838,7 +941,7 @@ Y - source + source Source-IP Hash [default] Round Robin @@ -850,7 +953,7 @@ Y - 2 + 2 2 1000 Please specify a value between 2 and 1000. @@ -874,6 +977,18 @@ Y N + + + + + Related fcgi item not found + N + N + Related resolver not found - N + N N @@ -905,13 +1020,13 @@ - /^((([0-9a-zA-Z._\-\*:]+)))*/u + /^((([0-9a-zA-Z._\-\*:]+)))*/u lower Please specify a valid source address, i.e. 10.0.0.1 or loadbalancer.example.com:50000. Port range as start-end, i.e. 10.0.0.1:50000-60000. N - 1 + 1 Y @@ -927,16 +1042,16 @@ N - 0 + 0 N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N @@ -961,20 +1076,20 @@ Related mailer not found - N + N N - 0 + 1 N - 0 + 0 N N - h2,http11 + h2,http11 Y Y @@ -983,9 +1098,30 @@ HTTP/1.0 + + 0 + N + + + 0 + N + + + N + Y + Y + + proto + host + by + by_port + for + for_port + + N - sticktable + sticktable Stick-table persistence [default] Cookie-based persistence (HTTP/HTTPS only) @@ -993,25 +1129,25 @@ Y - piggyback + piggyback Piggyback on existing cookie Insert new cookie - SRVCOOKIE + SRVCOOKIE N - /^([0-9a-zA-Z\.,_\-:]){1,1024}$/u + /^([0-9a-zA-Z\.,_\-:]){1,1024}$/u Does not look like a valid cookie name (most special characters are not allowed). - 1 + 1 Y N - sourceipv4 + sourceipv4 Source-IP [default] Source-IPv6 @@ -1040,21 +1176,21 @@ Y - 30m - /^([0-9]{1,5}(?:ms|s|m|h|d)?)/u + 30m + /^([0-9]{1,5}(?:ms|s|m|h|d)?)/u lower Should be a number between 1 and 5 characters followed by either "d", "h", "m", "s" or "ms". Y - 50k - /^([0-9]{1,5}[k|m|g]{1})*/u + 50k + /^([0-9]{1,5}[k|m|g]{1})*/u lower Should be a number between 1 and 5 characters followed by either "k", "m" or "g". N - /^([0-9a-zA-Z\.,_\-:]){1,1024}$/u + /^([0-9a-zA-Z\.,_\-:]){1,1024}$/u Does not look like a valid cookie name (most special characters are not allowed). @@ -1064,43 +1200,43 @@ N - 10s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 10s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 10s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 10s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 10s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 10s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 10s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 10s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 1m - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 1m + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 1m - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 1m + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 0 + 0 N @@ -1112,7 +1248,7 @@ Related user not found - Y + Y N @@ -1124,21 +1260,21 @@ Related group not found - Y + Y N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N @@ -1155,12 +1291,12 @@ N - 0 + 0 Y N - safe + safe Never Safe [default] @@ -1169,7 +1305,7 @@ - 0 + 0 N @@ -1182,7 +1318,7 @@ Related action item not found Y - Y + Y N @@ -1194,7 +1330,7 @@ Related error file item not found - Y + Y N @@ -1205,21 +1341,21 @@ Y - 1 + 1 Y - /^([0-9a-zA-Z._\-]){1,255}$/u + /^([0-9a-zA-Z._\-]){1,255}$/u Should be a string between 1 and 255 characters. Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N
    - /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u + /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u Please specify a valid servername or IP address. N
    @@ -1230,36 +1366,45 @@ N - 1 65535 Please specify a value between 1 and 65535. - N N - active + active active [default] backup disabled + + N + unspecified + + auto-selection [recommended] + FastCGI +

    HTTP/2

    +

    HTTP/1.1

    +
    +
    Y - static + static static + unix socket - /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u + /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u Please specify a valid service name. N - /^[0-9]+(-[0-9]+)?/u + /^[0-9]+(-[0-9]+)?/u Please specify a valid number or range. N @@ -1272,7 +1417,7 @@ Related resolver not found - N + N N @@ -1294,11 +1439,16 @@
    - 0 + 0 Y + + /^([0-9a-zA-Z._\-]){1,255}$/u + Should be a string between 1 and 255 characters. + N + - 1 + 1 Y @@ -1317,6 +1467,12 @@ cert Please select a valid certificate from the list. + + 0 + 10000000 + Please specify a value between 0 and 10000000. + N + 0 256 @@ -1324,17 +1480,17 @@ N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^((([0-9a-zA-Z._\-\*:]+)))*/u + /^((([0-9a-zA-Z._\-\*:]+)))*/u lower Please specify a valid source address, i.e. 10.0.0.1 or loadbalancer.example.com:50000. Port range as start-end, i.e. 10.0.0.1:50000-60000. N @@ -1342,23 +1498,38 @@ N + + + + + Related frontend item not found + N + N + - /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u Should be a string between 1 and 255 characters. Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N Y - http + http TCP HTTP [default] @@ -1373,25 +1544,39 @@ - 2s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 2s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". Y + + N + nopref + + Use server settings + Force SSL for health checks + Force SSL+SNI for health checks + Force no SSL for health checks + + + + /^([0-9a-zA-Z._\-]){1,255}$/u + Should be a string between 1 and 255 characters. + N + + - 0 + 0 N - 1 65535 Please specify a value between 1 and 65535. - N N - options + options OPTIONS [default] HEAD @@ -1403,14 +1588,14 @@ - / - /^(.*){1,255}$/u + / + /^(.*){1,255}$/u Should be a string between 1 and 255 characters. N N - http10 + http10 HTTP/1.0 [default] HTTP/1.1 @@ -1418,13 +1603,13 @@ - localhost - /^([0-9a-zA-Z._\-]){1,255}$/u + localhost + /^([0-9a-zA-Z._\-]){1,255}$/u Should be a string between 1 and 255 characters. N - 0 + 0 N @@ -1437,14 +1622,14 @@
    - 0 + 0 N N - 0 + 0 N @@ -1452,7 +1637,7 @@ N - string + string test the exact string match in the response buffer [default] test a regular expression on the response buffer @@ -1460,7 +1645,7 @@ - 0 + 0 N @@ -1473,26 +1658,26 @@ N - /^([0-9a-zA-Z._\-]){1,255}$/u + /^([0-9a-zA-Z._\-]){1,255}$/u Should be a string between 1 and 255 characters. N - 0 + 0 N - /^([0-9a-zA-Z._\-]){1,255}$/u + /^([0-9a-zA-Z._\-]){1,255}$/u Should be a string between 1 and 255 characters. N - /^([0-9a-zA-Z._\-]){1,255}$/u + /^([0-9a-zA-Z._\-]){1,255}$/u Should be a string between 1 and 255 characters. N - /^([0-9a-zA-Z._\-]){1,255}$/u + /^([0-9a-zA-Z._\-]){1,255}$/u Should be a string between 1 and 255 characters. N @@ -1514,12 +1699,12 @@ N - /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u Should be a string between 1 and 255 characters. Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N @@ -1547,6 +1732,7 @@ SSL Client certificate is valid SSL Client certificate verify error result SSL Client certificate issued by CA common-name + SSL Hello Type Source IP matches specified IP Source IP is local Source IP: TCP source port @@ -1582,124 +1768,124 @@ - 0 + 0 Y - 0 + 0 N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N @@ -1709,16 +1895,25 @@ N - /^.{1,4096}$/u + /^.{1,4096}$/u N + + N + x1 + + 0 - no client hello + 1 - client hello + 2 - server hello + + - /^.{1,4096}$/u + /^.{1,4096}$/u N N - gt + gt greater than greater equal @@ -1732,7 +1927,7 @@ N - gt + gt greater than greater equal @@ -1746,7 +1941,7 @@ N - gt + gt greater than greater equal @@ -1760,7 +1955,7 @@ N - gt + gt greater than greater equal @@ -1774,7 +1969,7 @@ N - gt + gt greater than greater equal @@ -1788,7 +1983,7 @@ N - gt + gt greater than greater equal @@ -1802,7 +1997,7 @@ N - gt + gt greater than greater equal @@ -1816,7 +2011,7 @@ N - gt + gt greater than greater equal @@ -1830,7 +2025,7 @@ N - gt + gt greater than greater equal @@ -1844,7 +2039,7 @@ N - gt + gt greater than greater equal @@ -1858,7 +2053,7 @@ N - gt + gt greater than greater equal @@ -1872,7 +2067,7 @@ N - gt + gt greater than greater equal @@ -1886,7 +2081,7 @@ N - gt + gt greater than greater equal @@ -1900,7 +2095,7 @@ N - gt + gt greater than greater equal @@ -1930,31 +2125,31 @@ N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N @@ -1985,7 +2180,7 @@ Related user not found - Y + Y N @@ -1997,7 +2192,7 @@ Related group not found - Y + Y N @@ -2005,18 +2200,18 @@ - /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u Should be a string between 1 and 255 characters. Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N Y - if + if IF [default] UNLESS @@ -2036,7 +2231,7 @@ N - and + and AND [default] OR @@ -2048,6 +2243,8 @@ Use specified Backend Pool Override server in Backend Pool Map domains to backend pools using a map file + FastCGI pass-header + FastCGI set-param http-request allow http-request deny http-request tarpit @@ -2112,66 +2309,76 @@ N N + + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + N + + + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + N + - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N N - txn + txn variable is shared with the whole process variable is shared with the whole session @@ -2181,51 +2388,51 @@ - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N @@ -2235,12 +2442,12 @@ N - /^.{1,4096}$/u + /^.{1,4096}$/u N N - txn + txn variable is shared with the whole process variable is shared with the whole session @@ -2250,39 +2457,39 @@ - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,32}$/u + /^.{1,32}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N - /^.{1,32}$/u + /^.{1,32}$/u N - /^.{1,4096}$/u + /^.{1,4096}$/u N @@ -2311,13 +2518,13 @@ N - N + N - N + N - N + N @@ -2351,26 +2558,26 @@ Y - 1 + 1 Y - /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u Should be a string between 1 and 255 characters. Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - 1 + 1 Y Y - id + id Use a random ID for the filename [default] Use the specified name as filename @@ -2381,24 +2588,98 @@ + + + + Y + + + 1 + Y + + + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + Should be a string between 1 and 255 characters. + Y + + + /^.{1,255}$/u + Should be a string between 1 and 255 characters. + N + + + /^.{1,4096}$/u + Should be a string between 1 and 4096 characters. + Y + + + /^.{1,4096}$/u + Should be a string between 1 and 4096 characters. + N + + + /^.{1,4096}$/u + Should be a string between 1 and 4096 characters. + N + + + 0 + N + + + 1 + N + + + 0 + N + + + 0 + N + + + 1 + 100000 + Please specify a value between 1 and 100000. + N + + + + + + Related action item not found + Y + Y + N + + + Y - /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u Should be a string between 1 and 255 characters. Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N Y - 503 + 503 200 400 @@ -2423,12 +2704,12 @@ Y - /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u Should be a string between 1 and 255 characters. Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N @@ -2443,16 +2724,16 @@ Y - 1 + 1 Y - /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u Should be a string between 1 and 255 characters. Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N @@ -2469,7 +2750,7 @@ N - 0 + 0 N @@ -2480,21 +2761,21 @@ Y - 1 + 1 Y - /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u Should be a string between 1 and 255 characters. Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,512}$/u + /^.{1,512}$/u Should be a string between 1 and 512 characters. Y @@ -2506,89 +2787,18 @@ Y - 1 + 1 Y - /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u Should be a string between 1 and 255 characters. Y - - Y - - All HAProxy processes - Processes with odd ID - Processes with even ID - Process 1 - Process 2 - Process 3 - Process 4 - Process 5 - Process 6 - Process 7 - Process 8 - Process 9 - Process 10 - Process 11 - Process 12 - Process 13 - Process 14 - Process 15 - Process 16 - Process 17 - Process 18 - Process 19 - Process 20 - Process 21 - Process 22 - Process 23 - Process 24 - Process 25 - Process 26 - Process 27 - Process 28 - Process 29 - Process 30 - Process 31 - Process 32 - Process 33 - Process 34 - Process 35 - Process 36 - Process 37 - Process 38 - Process 39 - Process 40 - Process 41 - Process 42 - Process 43 - Process 44 - Process 45 - Process 46 - Process 47 - Process 48 - Process 49 - Process 50 - Process 51 - Process 52 - Process 53 - Process 54 - Process 55 - Process 56 - Process 57 - Process 58 - Process 59 - Process 60 - Process 61 - Process 62 - Process 63 - - Y - All HAProxy threads + All HAProxy threads Threads with odd ID Threads with even ID Thread 1 @@ -2737,83 +2947,83 @@ Y - 1 + 1 Y - /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u Should be a string between 1 and 255 characters. Y N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N Y - Y - /^((([0-9a-zA-Z._\-\*:\[\]]+:[0-9]+(-[0-9]+)?)([,]){0,1}))*/u + Y + /^((((udp@|tcp@)?[0-9a-zA-Z._\-\*:\[\]]+:[0-9]+(-[0-9]+)?)([,]){0,1}))*/u lower - Please provide a valid nameserver address, i.e. 127.0.0.1:53, [::1]:53 or 192.168.1.1:53. + Please provide a valid nameserver address, i.e. 127.0.0.1:53, [::1]:53 or tcp@192.168.1.1:53. - 0 + 0 Y - 3 + 3 0 100000 Please specify a value between 0 and 100000. N - 1s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 1s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - 1s - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + 1s + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N 0 - 8192 - Should be a number between 0 and 8192 + 65535 + Should be a number between 0 and 65535 N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N - /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u + /^([0-9]{1,8}(?:us|ms|s|m|h|d)?)/u Should be a number between 1 and 8 characters, optionally followed by either "d", "h", "m", "s", "ms" or "us". N @@ -2825,24 +3035,24 @@ Y - 1 + 1 Y - /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u + /^[^\t^,^;^\.^\[^\]^\{^\}]{1,255}$/u Should be a string between 1 and 255 characters. Y N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. Y Y - Y - /^((([0-9a-zA-Z._\-\*:\[\]]+:[0-9]+(-[0-9]+)?)([,]){0,1}))*/u + Y + /^((([0-9a-zA-Z._\-\*:\[\]]+:[0-9]+(-[0-9]+)?)([,]){0,1}))*/u lower Please provide mailserver addresses, i.e. 192.168.1.1:25. @@ -2854,7 +3064,7 @@ Y - alert + alert emerg alert @@ -2867,7 +3077,7 @@ - 30 + 30 4 10000 Please specify a value between 4 and 10000 seconds. @@ -2881,7 +3091,7 @@ - 0 + 0 N @@ -2898,8 +3108,9 @@ Related cron not found. N + - 0 + 0 N @@ -2917,7 +3128,7 @@ N - 0 + 0 N @@ -2935,7 +3146,7 @@ N - 0 + 0 N diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml index 0d78bf3eb3..d95844a34c 100644 --- a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Menu/Menu.xml @@ -18,6 +18,7 @@ + diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M3_5_0.php b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M3_5_0.php new file mode 100644 index 0000000000..24306366e0 --- /dev/null +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M3_5_0.php @@ -0,0 +1,50 @@ +getNodeByReference('healthchecks.healthcheck')->iterateItems() as $healthcheck) { + switch ((string)$healthcheck->force_ssl) { + case '0': + $healthcheck->ssl = 'nopref'; + break; + case '1': + $healthcheck->ssl = 'ssl'; + break; + } + } + } +} diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M4_0_0.php b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M4_0_0.php new file mode 100644 index 0000000000..c13544da51 --- /dev/null +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M4_0_0.php @@ -0,0 +1,50 @@ +getNodeByReference('frontends.frontend')->iterateItems() as $frontend) { + switch ((string)$frontend->connectionBehaviour) { + case 'http-tunnel': + $frontend->connectionBehaviour = 'http-keep-alive'; + break; + case 'forceclose': + $frontend->connectionBehaviour = 'http-keep-alive'; + break; + } + } + } +} diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M4_1_0.php b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M4_1_0.php new file mode 100644 index 0000000000..22f91c8fe4 --- /dev/null +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/Migrations/M4_1_0.php @@ -0,0 +1,59 @@ +general->storeOcsp; + $model->general->tuning->ocspUpdateEnabled = $old_ocsp; + + // Remove obsolete OCSP cron job + if ((string)$model->maintenance->cronjobs->updateOcspCron != "") { + $cron_uuid = (string)$model->maintenance->cronjobs->updateOcspCron; + $model->maintenance->cronjobs->updateOcspCron = ""; + + // Delete the cronjob item + $mdlCron = new Cron(); + if ($mdlCron->jobs->job->del($cron_uuid)) { + $mdlCron->serializeToConfig(); + $model->serializeToConfig($validateFullModel = false, $disable_validation = true); + Config::getInstance()->save(); + } + } + } +} diff --git a/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt b/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt index ba0cf32f02..cb6eea31a4 100644 --- a/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt +++ b/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/index.volt @@ -49,178 +49,175 @@ POSSIBILITY OF SUCH DAMAGE. **********************************************************************/ $("#grid-frontends").UIBootgrid( - { search:'/api/haproxy/settings/searchFrontends', - get:'/api/haproxy/settings/getFrontend/', - set:'/api/haproxy/settings/setFrontend/', - add:'/api/haproxy/settings/addFrontend/', - del:'/api/haproxy/settings/delFrontend/', - toggle:'/api/haproxy/settings/toggleFrontend/', + { search:'/api/haproxy/settings/search_frontends', + get:'/api/haproxy/settings/get_frontend/', + set:'/api/haproxy/settings/set_frontend/', + add:'/api/haproxy/settings/add_frontend/', + del:'/api/haproxy/settings/del_frontend/', + toggle:'/api/haproxy/settings/toggle_frontend/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-backends").UIBootgrid( - { search:'/api/haproxy/settings/searchBackends', - get:'/api/haproxy/settings/getBackend/', - set:'/api/haproxy/settings/setBackend/', - add:'/api/haproxy/settings/addBackend/', - del:'/api/haproxy/settings/delBackend/', - toggle:'/api/haproxy/settings/toggleBackend/', + { search:'/api/haproxy/settings/search_backends', + get:'/api/haproxy/settings/get_backend/', + set:'/api/haproxy/settings/set_backend/', + add:'/api/haproxy/settings/add_backend/', + del:'/api/haproxy/settings/del_backend/', + toggle:'/api/haproxy/settings/toggle_backend/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-servers").UIBootgrid( - { search:'/api/haproxy/settings/searchServers', - get:'/api/haproxy/settings/getServer/', - set:'/api/haproxy/settings/setServer/', - add:'/api/haproxy/settings/addServer/', - del:'/api/haproxy/settings/delServer/', - toggle:'/api/haproxy/settings/toggleServer/', + { search:'/api/haproxy/settings/search_servers', + get:'/api/haproxy/settings/get_server/', + set:'/api/haproxy/settings/set_server/', + add:'/api/haproxy/settings/add_server/', + del:'/api/haproxy/settings/del_server/', + toggle:'/api/haproxy/settings/toggle_server/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-healthchecks").UIBootgrid( - { search:'/api/haproxy/settings/searchHealthchecks', - get:'/api/haproxy/settings/getHealthcheck/', - set:'/api/haproxy/settings/setHealthcheck/', - add:'/api/haproxy/settings/addHealthcheck/', - del:'/api/haproxy/settings/delHealthcheck/', + { search:'/api/haproxy/settings/search_healthchecks', + get:'/api/haproxy/settings/get_healthcheck/', + set:'/api/haproxy/settings/set_healthcheck/', + add:'/api/haproxy/settings/add_healthcheck/', + del:'/api/haproxy/settings/del_healthcheck/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-actions").UIBootgrid( - { search:'/api/haproxy/settings/searchActions', - get:'/api/haproxy/settings/getAction/', - set:'/api/haproxy/settings/setAction/', - add:'/api/haproxy/settings/addAction/', - del:'/api/haproxy/settings/delAction/', + { search:'/api/haproxy/settings/search_actions', + get:'/api/haproxy/settings/get_action/', + set:'/api/haproxy/settings/set_action/', + add:'/api/haproxy/settings/add_action/', + del:'/api/haproxy/settings/del_action/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-acls").UIBootgrid( - { search:'/api/haproxy/settings/searchAcls', - get:'/api/haproxy/settings/getAcl/', - set:'/api/haproxy/settings/setAcl/', - add:'/api/haproxy/settings/addAcl/', - del:'/api/haproxy/settings/delAcl/', + { search:'/api/haproxy/settings/search_acls', + get:'/api/haproxy/settings/get_acl/', + set:'/api/haproxy/settings/set_acl/', + add:'/api/haproxy/settings/add_acl/', + del:'/api/haproxy/settings/del_acl/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-users").UIBootgrid( - { search:'/api/haproxy/settings/searchUsers', - get:'/api/haproxy/settings/getUser/', - set:'/api/haproxy/settings/setUser/', - add:'/api/haproxy/settings/addUser/', - del:'/api/haproxy/settings/delUser/', - toggle:'/api/haproxy/settings/toggleUser/', + { search:'/api/haproxy/settings/search_users', + get:'/api/haproxy/settings/get_user/', + set:'/api/haproxy/settings/set_user/', + add:'/api/haproxy/settings/add_user/', + del:'/api/haproxy/settings/del_user/', + toggle:'/api/haproxy/settings/toggle_user/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-groups").UIBootgrid( - { search:'/api/haproxy/settings/searchGroups', - get:'/api/haproxy/settings/getGroup/', - set:'/api/haproxy/settings/setGroup/', - add:'/api/haproxy/settings/addGroup/', - del:'/api/haproxy/settings/delGroup/', - toggle:'/api/haproxy/settings/toggleGroup/', + { search:'/api/haproxy/settings/search_groups', + get:'/api/haproxy/settings/get_group/', + set:'/api/haproxy/settings/set_group/', + add:'/api/haproxy/settings/add_group/', + del:'/api/haproxy/settings/del_group/', + toggle:'/api/haproxy/settings/toggle_group/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-luas").UIBootgrid( - { search:'/api/haproxy/settings/searchLuas', - get:'/api/haproxy/settings/getLua/', - set:'/api/haproxy/settings/setLua/', - add:'/api/haproxy/settings/addLua/', - del:'/api/haproxy/settings/delLua/', - toggle:'/api/haproxy/settings/toggleLua/', + { search:'/api/haproxy/settings/search_luas', + get:'/api/haproxy/settings/get_lua/', + set:'/api/haproxy/settings/set_lua/', + add:'/api/haproxy/settings/add_lua/', + del:'/api/haproxy/settings/del_lua/', + toggle:'/api/haproxy/settings/toggle_lua/', + options: { + } + } + ); + + $("#grid-fcgis").UIBootgrid( + { search:'/api/haproxy/settings/search_fcgis', + get:'/api/haproxy/settings/get_fcgi/', + set:'/api/haproxy/settings/set_fcgi/', + add:'/api/haproxy/settings/add_fcgi/', + del:'/api/haproxy/settings/del_fcgi/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-errorfiles").UIBootgrid( - { search:'/api/haproxy/settings/searchErrorfiles', - get:'/api/haproxy/settings/getErrorfile/', - set:'/api/haproxy/settings/setErrorfile/', - add:'/api/haproxy/settings/addErrorfile/', - del:'/api/haproxy/settings/delErrorfile/', + { search:'/api/haproxy/settings/search_errorfiles', + get:'/api/haproxy/settings/get_errorfile/', + set:'/api/haproxy/settings/set_errorfile/', + add:'/api/haproxy/settings/add_errorfile/', + del:'/api/haproxy/settings/del_errorfile/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-mapfiles").UIBootgrid( - { search:'/api/haproxy/settings/searchMapfiles', - get:'/api/haproxy/settings/getMapfile/', - set:'/api/haproxy/settings/setMapfile/', - add:'/api/haproxy/settings/addMapfile/', - del:'/api/haproxy/settings/delMapfile/', + { search:'/api/haproxy/settings/search_mapfiles', + get:'/api/haproxy/settings/get_mapfile/', + set:'/api/haproxy/settings/set_mapfile/', + add:'/api/haproxy/settings/add_mapfile/', + del:'/api/haproxy/settings/del_mapfile/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-cpus").UIBootgrid( - { search:'/api/haproxy/settings/searchCpus', - get:'/api/haproxy/settings/getCpu/', - set:'/api/haproxy/settings/setCpu/', - add:'/api/haproxy/settings/addCpu/', - del:'/api/haproxy/settings/delCpu/', - toggle:'/api/haproxy/settings/toggleCpu/', + { search:'/api/haproxy/settings/search_cpus', + get:'/api/haproxy/settings/get_cpu/', + set:'/api/haproxy/settings/set_cpu/', + add:'/api/haproxy/settings/add_cpu/', + del:'/api/haproxy/settings/del_cpu/', + toggle:'/api/haproxy/settings/toggle_cpu/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-resolvers").UIBootgrid( - { search:'/api/haproxy/settings/searchResolvers', - get:'/api/haproxy/settings/getResolver/', - set:'/api/haproxy/settings/setResolver/', - add:'/api/haproxy/settings/addResolver/', - del:'/api/haproxy/settings/delResolver/', - toggle:'/api/haproxy/settings/toggleResolver/', + { search:'/api/haproxy/settings/search_resolvers', + get:'/api/haproxy/settings/get_resolver/', + set:'/api/haproxy/settings/set_resolver/', + add:'/api/haproxy/settings/add_resolver/', + del:'/api/haproxy/settings/del_resolver/', + toggle:'/api/haproxy/settings/toggle_resolver/', options: { - rowCount:[10,25,50,100,500,1000] } } ); $("#grid-mailers").UIBootgrid( - { search:'/api/haproxy/settings/searchMailers', - get:'/api/haproxy/settings/getMailer/', - set:'/api/haproxy/settings/setMailer/', - add:'/api/haproxy/settings/addMailer/', - del:'/api/haproxy/settings/delMailer/', - toggle:'/api/haproxy/settings/toggleMailer/', + { search:'/api/haproxy/settings/search_mailers', + get:'/api/haproxy/settings/get_mailer/', + set:'/api/haproxy/settings/set_mailer/', + add:'/api/haproxy/settings/add_mailer/', + del:'/api/haproxy/settings/del_mailer/', + toggle:'/api/haproxy/settings/toggle_mailer/', options: { - rowCount:[10,25,50,100,500,1000] } } ); @@ -587,6 +584,12 @@ POSSIBILITY OF SUCH DAMAGE. + +

    {{ lang._('Please be aware that you need to %smanually%s add the required firewall rules for all configured services.') | format('', '') }}

    -

    {{ lang._('Further information is available in our %sHAProxy plugin documentation%s and of course in the %sofficial HAProxy documentation%s. Be sure to report bugs and request features on our %sGitHub issue page%s. Code contributions are also very welcome!') | format('', '', '', '', '', '') }}

    +

    {{ lang._('Further information is available in the %sofficial HAProxy documentation%s. Be sure to report bugs and request features on our %sGitHub issue page%s. Code contributions are also very welcome!') | format('', '', '', '') }}


    @@ -740,7 +744,7 @@ POSSIBILITY OF SUCH DAMAGE.
  • {{ lang._('%sConditions:%s HAProxy is capable of extracting data from requests, responses and other connection data and match it against predefined patterns. Use these powerful patterns to compose a condition that may be used in multiple Rules.') | format('', '') }}
  • {{ lang._('%sRules:%s Perform a large set of actions if one or more %sConditions%s match. These Rules may be used in %sBackend Pools%s as well as %sPublic Services%s.') | format('', '', '', '', '', '', '', '') }}
  • -

    {{ lang._("For more information on HAProxy's %sACL feature%s see the %sofficial documentation%s.") | format('', '', '', '') }}

    +

    {{ lang._("For more information on HAProxy's %sACL feature%s see the %sofficial documentation%s.") | format('', '', '', '') }}

    {{ lang._('Note that it is possible to directly add options to the HAProxy configuration by using the "option pass-through", a setting that is available for several configuration items. It allows you to implement configurations that are currently not officially supported by this plugin. It is strongly discouraged to rely on this feature. Please report missing features on our GitHub page!') | format('', '') }}


    @@ -755,7 +759,7 @@ POSSIBILITY OF SUCH DAMAGE.
  • {{ lang._('%sGroup:%s A optional list containing one or more users. Groups usually make it easier to manage permissions for a large number of users') | format('', '') }}
  • {{ lang._('Note that users and groups must be selected from the Backend Pool or Public Service configuration in order to be used for authentication. In addition to this users and groups may also be used in Rules/Conditions.') }}

    -

    {{ lang._("For more information on HAProxy's %suser/group management%s see the %sofficial documentation%s.") | format('', '', '', '') }}

    +

    {{ lang._("For more information on HAProxy's %suser/group management%s see the %sofficial documentation%s.") | format('', '', '', '') }}


    @@ -773,7 +777,7 @@ POSSIBILITY OF SUCH DAMAGE.
  • {{ lang._("%sCache:%s HAProxy's cache which was designed to perform cache on small objects (favicon, css, etc.). This is a minimalist low-maintenance cache which runs in RAM.") | format('', '', '', '') }}
  • {{ lang._("%sPeers:%s Configure a communication channel between two HAProxy instances. This will propagate entries of any data-types in stick-tables between these HAProxy instances over TCP connections in a multi-master fashion. Useful when aiming for a seamless failover in a HA setup.") | format('', '', '', '') }}
  • -

    {{ lang._("For more details visit HAProxy's official documentation regarding the %sStatistics%s, %sCache%s and %sPeers%s features.") | format('', '', '', '', '', '') }}

    +

    {{ lang._("For more details visit HAProxy's official documentation regarding the %sStatistics%s, %sCache%s and %sPeers%s features.") | format('', '', '', '', '', '') }}


    @@ -784,13 +788,14 @@ POSSIBILITY OF SUCH DAMAGE.

    {{ lang._("Most of the time these features are not required, but in certain situations they will be handy:") }}

    • {{ lang._("%sError Messages:%s Return a custom message instead of errors generated by HAProxy. Useful to overwrite HAProxy's internal error messages. The message must represent the full HTTP response and include required HTTP headers.") | format('', '') }}
    • +
    • {{ lang._("%sFastCGI Applications:%s HAProxy can be configured to send requests to FastCGI applications. After configuring a FastCGI application, it needs to be enabled in a %sBackend Pool%s.") | format('', '', '', '') }}
    • {{ lang._("%sLua scripts:%s Include your own Lua code/scripts to extend HAProxy's functionality. The Lua code can be used in certain %sRules%s, for example.") | format('', '', '', '') }}
    • {{ lang._("%sMap Files:%s A map allows to map a data in input to an other one on output. For example, this makes it possible to map a large number of domains to backend pools without using the GUI. Map files need to be used in %sRules%s, otherwise they are ignored.") | format('', '', '', '') }}
    • {{ lang._("%sCPU Affinity Rules:%s This feature makes it possible to bind HAProxy's processes/threads to a specific CPU (or a CPU set). Furthermore it is possible to select CPU Affinity Rules in %sPublic Services%s to restrict them to a certain set of processes/threads/CPUs.") | format('', '', '', '') }}
    • {{ lang._("%sResolvers:%s This feature allows in-depth configuration of how HAProxy handles name resolution and interacts with name resolvers (DNS). Each resolver configuration can be used in %sBackend Pools%s to apply individual name resolution configurations.") | format('', '', '', '') }}
    • {{ lang._("%sE-Mail Alerts:%s It is possible to send email alerts when the state of servers changes. Each configuration can be used in %sBackend Pools%s to send e-mail alerts to the configured recipient.") | format('', '', '', '') }}
    -

    {{ lang._("For more details visit HAProxy's official documentation regarding the %sError Messages%s, %sLua Script%s and the %sMap Files%s features. More information on HAProxy's CPU Affinity is also available %shere%s, %shere%s and %shere%s. A detailed explanation of the resolvers feature can be found %shere%s.") | format('', '', '', '', '', '' ,'', '' ,'', '' ,'', '','', '') }}

    +

    {{ lang._("For more details visit HAProxy's official documentation regarding the %sError Messages%s, %sLua Script%s and the %sMap Files%s features. More information on HAProxy's CPU Affinity is also available %shere%s, %shere%s and %shere%s. A detailed explanation of the resolvers feature can be found %shere%s.") | format('', '', '', '', '', '' ,'', '' ,'', '' ,'', '','', '') }}


    @@ -1029,6 +1034,31 @@ POSSIBILITY OF SUCH DAMAGE. +
    + + + + + + + + + + + + + + + + + + +
    {{ lang._('FastCGI ID') }}{{ lang._('FastCGI Application Name') }}{{ lang._('Description') }}{{ lang._('Commands') }}{{ lang._('ID') }}
    + + +
    +
    +
    @@ -1086,7 +1116,6 @@ POSSIBILITY OF SUCH DAMAGE. - @@ -1264,6 +1293,7 @@ POSSIBILITY OF SUCH DAMAGE. {{ partial("layout_partials/base_dialog",['fields':formDialogGroup,'id':'DialogGroup','label':lang._('Edit Group')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogLua,'id':'DialogLua','label':lang._('Edit Lua Script')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogErrorfile,'id':'DialogErrorfile','label':lang._('Edit Error Message')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogFcgi,'id':'DialogFcgi','label':lang._('Edit FastCGI Application')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogMapfile,'id':'DialogMapfile','label':lang._('Edit Map File')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogCpu,'id':'DialogCpu','label':lang._('Edit CPU Affinity Rule')])}} {{ partial("layout_partials/base_dialog",['fields':formDialogResolver,'id':'DialogResolver','label':lang._('Edit Resolver')])}} diff --git a/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/maintenance.volt b/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/maintenance.volt index 50eb005110..0094f996c9 100644 --- a/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/maintenance.volt +++ b/net/haproxy/src/opnsense/mvc/app/views/OPNsense/HAProxy/maintenance.volt @@ -79,7 +79,7 @@ POSSIBILITY OF SUCH DAMAGE. // save data for this tab saveFormToEndpoint(url="/api/haproxy/maintenance/set",formid=frm_id,callback_ok=function(){ // Handle cron integration - ajaxCall(url="/api/haproxy/maintenance/fetchCronIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/haproxy/maintenance/fetch_cron_integration", sendData={}, callback=function(data,status) { }); // when done, disable progress animation @@ -127,7 +127,7 @@ POSSIBILITY OF SUCH DAMAGE. } function showDiffDialog(payload) { - $.post('/api/haproxy/maintenance/certDiff', payload, function(data) { + $.post('/api/haproxy/maintenance/cert_diff', payload, function(data) { BootstrapDialog.show({ type: BootstrapDialog.TYPE_INFO, title: "{{ lang._('Diff between configured and active SSL certificates') }}", @@ -143,7 +143,7 @@ POSSIBILITY OF SUCH DAMAGE. } function applyDiffDialog(payload, requested_count) { - $.post('/api/haproxy/maintenance/certActions', payload, function(data_actions) { + $.post('/api/haproxy/maintenance/cert_actions', payload, function(data_actions) { question = '' question += `
    ${data_actions}
    `; question += '{{ lang._('Apply SSL certificates to HAProxy?') }}

    '; @@ -151,7 +151,7 @@ POSSIBILITY OF SUCH DAMAGE. stdDialogConfirm('{{ lang._('Confirmation Required') }}', question, '{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() { - $.post('/api/haproxy/maintenance/certSync', payload, function(data) { + $.post('/api/haproxy/maintenance/cert_sync', payload, function(data) { modified_count = data.result.add_count + data.result.remove_count + data.result.update_count; if (requested_count != modified_count) { @@ -176,13 +176,12 @@ POSSIBILITY OF SUCH DAMAGE. $("#grid-certificates").bootgrid('destroy'); var grid_certificates = $("#grid-certificates").UIBootgrid({ - search: '/api/haproxy/maintenance/searchCertificateDiff', + search: '/api/haproxy/maintenance/search_certificate_diff', options: { ajax: true, selection: true, multiSelect: true, keepSelection: true, - rowCount:[10,25,50,100,500,1000], searchSettings: { delay: 250, characters: 1 @@ -262,7 +261,7 @@ POSSIBILITY OF SUCH DAMAGE. }); var payload = {}; - $.post('/api/haproxy/maintenance/certSyncBulk', payload, function(data) { + $.post('/api/haproxy/maintenance/cert_sync_bulk', payload, function(data) { modified_count = data.result.add_count + data.result.remove_count + data.result.update_count; if (requested_count != modified_count) { var error_msg = syncErrorMessage(data.result.modified, data.result.deleted); @@ -286,13 +285,12 @@ POSSIBILITY OF SUCH DAMAGE. // grid-status $("#grid-status").bootgrid('destroy'); var grid_status = $("#grid-status").UIBootgrid({ - search: '/api/haproxy/maintenance/searchServer', + search: '/api/haproxy/maintenance/search_server', options: { ajax: true, selection: true, multiSelect: true, keepSelection: true, - rowCount:[10,25,50,100,500,1000], searchSettings: { delay: 250, characters: 1 @@ -328,7 +326,7 @@ POSSIBILITY OF SUCH DAMAGE. stdDialogConfirm('{{ lang._('Confirmation Required') }}', question, '{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() { - $.post('/api/haproxy/maintenance/serverState', payload, function(data) { + $.post('/api/haproxy/maintenance/server_state', payload, function(data) { if (data.status != 'ok') { BootstrapDialog.show({ type: BootstrapDialog.TYPE_DANGER, @@ -372,7 +370,7 @@ POSSIBILITY OF SUCH DAMAGE. 'weight': $("#newWeight").val() }; - $.post('/api/haproxy/maintenance/serverWeight', payload, function(data) { + $.post('/api/haproxy/maintenance/server_weight', payload, function(data) { if (data.status != 'ok') { BootstrapDialog.show({ type: BootstrapDialog.TYPE_DANGER, @@ -415,7 +413,7 @@ POSSIBILITY OF SUCH DAMAGE. stdDialogConfirm('{{ lang._('Confirmation Required') }}', question, '{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() { - $.post('/api/haproxy/maintenance/serverStateBulk', payload, function(data) { + $.post('/api/haproxy/maintenance/server_state_bulk', payload, function(data) { if (data.status != 'ok') { BootstrapDialog.show({ type: BootstrapDialog.TYPE_DANGER, @@ -465,7 +463,7 @@ POSSIBILITY OF SUCH DAMAGE. 'weight': $("#newBulkWeight").val() }; - $.post('/api/haproxy/maintenance/serverWeightBulk', payload, function(data) { + $.post('/api/haproxy/maintenance/server_weight_bulk', payload, function(data) { if (data.status != 'ok') { BootstrapDialog.show({ type: BootstrapDialog.TYPE_DANGER, diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportCerts.php b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportCerts.php index b28a84ecd4..cccff6408d 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportCerts.php +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportCerts.php @@ -2,7 +2,7 @@ ['ssl_certificates', 'ssl_clientAuthCAs', 'ssl_clientAuthCRLs', 'ssl_default_certificate'], @@ -68,19 +78,21 @@ foreach ($configObj->$type as $cert) { if ($cert_refid == (string)$cert->refid) { $pem_content = ''; + $ocsp_conf = ''; // CRLs require special export if ($type == 'crl') { - $crl =& lookup_crl($cert_refid); - crl_update($crl); - $pem_content = base64_decode($crl['text']); + if (isset($cert->text)) { + $pem_content = base64_decode($cert->text); + } } else { $pem_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->crt))); $pem_content .= "\n" . str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->prv))); + // Get OCSP status + $ocsp_conf = hasOcspInfo($pem_content) ? ' [ocsp-update on]' : ''; // check if a CA is linked if (!empty((string)$cert->caref)) { - $cert = (array)$cert; - $ca = ca_chain($cert); // append the CA to the certificate data + $ca = CertStore::getCaChain((string)$cert->caref); $pem_content .= "\n" . $ca; // additionally export CA to it's own file, // not required for HAProxy, but makes OCSP handling easier @@ -96,7 +108,12 @@ file_put_contents($output_pem_filename, $pem_content); chmod($output_pem_filename, 0600); echo "exported $type to " . $output_pem_filename . "\n"; - $crtlist[] = $output_pem_filename; + // Check if automatic OCSP updates are enabled. + if (isset($configObj->OPNsense->HAProxy->general->tuning->ocspUpdateEnabled) and ($configObj->OPNsense->HAProxy->general->tuning->ocspUpdateEnabled == '1')) { + $crtlist[] = $output_pem_filename . $ocsp_conf; + } else { + $crtlist[] = $output_pem_filename; + } } else { // In contrast to certificates, CA/CRL content needs to be put in a single file. // A list of individual files is not supported by HAproxy. @@ -121,7 +138,20 @@ // check if a default certificate is configured if (($type == 'cert') and isset($child->ssl_default_certificate) and (string)$child->ssl_default_certificate != "") { $default_cert = (string)$child->ssl_default_certificate; - $default_cert_filename = $export_path . $default_cert . ".pem"; + // Get OCSP status + $ocsp_conf = ''; + foreach ($configObj->cert as $cert) { + if ($default_cert == (string)$cert->refid) { + $pem_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->crt))); + $ocsp_conf = hasOcspInfo($pem_content) ? ' [ocsp-update on]' : ''; + } + } + // Check if automatic OCSP updates are enabled. + if (isset($configObj->OPNsense->HAProxy->general->tuning->ocspUpdateEnabled) and ($configObj->OPNsense->HAProxy->general->tuning->ocspUpdateEnabled == '1')) { + $default_cert_filename = $export_path . $default_cert . ".pem" . $ocsp_conf; + } else { + $default_cert_filename = $export_path . $default_cert . ".pem"; + } // ensure that the default certificate is the first entry on the list $crtlist = array_diff($crtlist, [$default_cert_filename]); array_unshift($crtlist, $default_cert_filename); diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportErrorFiles.php b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportErrorFiles.php index 9ffaa95d99..f5417c754e 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportErrorFiles.php +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportErrorFiles.php @@ -30,8 +30,6 @@ // Use legacy code to export certificates to the filesystem. require_once("config.inc"); -require_once("certs.inc"); -require_once("legacy_bindings.inc"); use OPNsense\Core\Config; diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportLuaScripts.php b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportLuaScripts.php index 8406572815..30e8bd3b31 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportLuaScripts.php +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportLuaScripts.php @@ -30,8 +30,6 @@ // Use legacy code to export certificates to the filesystem. require_once("config.inc"); -require_once("certs.inc"); -require_once("legacy_bindings.inc"); use OPNsense\Core\Config; diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportMapFiles.php b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportMapFiles.php index ee502e4783..fe2e682f78 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportMapFiles.php +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/exportMapFiles.php @@ -30,8 +30,6 @@ // Use legacy code to export certificates to the filesystem. require_once("config.inc"); -require_once("certs.inc"); -require_once("legacy_bindings.inc"); use OPNsense\Core\Config; diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/__init__.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/__init__.py deleted file mode 100755 index c7f37eafa1..0000000000 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""haproxy lib for socket commands. -Based on: https://github.com/neurogeek/haproxyctl""" -__version__ = "1.0" diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py deleted file mode 100755 index 733daf113b..0000000000 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/cmds.py +++ /dev/null @@ -1,335 +0,0 @@ -"""cmds.py - Implementations of the different HAProxy commands""" -import re -import csv -import json -from collections import OrderedDict -from io import StringIO - -class Cmd(): - """Cmd - Command base class""" - req_args = [] - args = {} - cmdTxt = "" - helpTxt = "" - - # pylint: disable=unused-argument - def __init__(self, *args, **kwargs): - """Argument to the command are given in kwargs only. We ignore *args.""" - self.args = kwargs - valid_kwargs = [k for (k, v) in kwargs.items() if v is not None] - - if not all([a in valid_kwargs for a in self.req_args]): - raise Exception(f"Wrong number of arguments. Required arguments are: {self.WhatArgs()}") - - def WhatArgs(self): - """Returns a formatted string of arguments to this command.""" - return ",".join(self.req_args) - - @classmethod - def getHelp(cls): - """Get formatted help string for this command.""" - txtArgs = ",".join(cls.req_args) - - if not txtArgs: - txtArgs = "None" - return " ".join((cls.helpTxt, "Arguments: %s" % txtArgs)) - - def getCmd(self): - """Gets the command line for this command. - The default behavior is to apply the args dict to cmdTxt - """ - return self.cmdTxt % self.args - - def getBootstrapOutput(self, resObj): - """ Returns results gathered from HAProxy as jquery bootstrap output """ - args = { - "rows": resObj, - "page": int(self.args['page']) if self.args['page'] != None else 1, - "page_rows": int(self.args['page_rows']) if self.args['page_rows'] != None else len(rows), - "search": self.args['search'], - "sort_col": self.args['sort_col'] if self.args['sort_col'] else 'id', - "sort_dir": self.args['sort_dir'], - } - rows = args['rows'] - # search - if args['search']: - filtered_rows = [] - for row in rows: - def inner(row): - for k, v in row.items(): - if args['search'] in v: - return row - return None - - match = inner(row) - if match: - filtered_rows.append(match) - rows = filtered_rows - - # sort - rows.sort(key=lambda k: k[args['sort_col']], reverse=True if args['sort_dir'] == 'desc' else False) - - # pager - total = len(rows) - pages = [rows[i:i + args['page_rows']] for i in range(0, total, args['page_rows'])] - if pages and (args['page'] > len(pages) or args['page'] < 1): - raise KeyError(f"Current page {args['page']} does not exist. Available pages: {len(pages)}") - page = pages[args['page'] - 1] if pages else [] - - return json.dumps({ - "rows": page, - "total": total, - "rowCount": args['page_rows'], - "current": args['page'] - }) - - def getJsonOutput(self, resObj): - """Returns results gathered from HAProxy as json""" - return json.dumps(resObj) - - def getResult(self, res): - """Returns raw results gathered from HAProxy""" - if res == '\n': - res = None - - if self.args['output'] == 'json': - return self.getJsonOutput(self.getResultObj(res)) - - if self.args['output'] == 'bootstrap': - return self.getBootstrapOutput(self.getResultObj(res)) - - return res - - def getResultObj(self, res): - """Returns refined output from HAProxy, packed inside a Python obj i.e. a dict()""" - return res - -class setServerAgent(Cmd): - cmdTxt = "set server %(backend)s/%(server)s agent %(value)s\r\n" - req_args = ['backend', 'server', 'value'] - helpTxt = "Force a server's agent to a new state." - -class setServerHealth(Cmd): - cmdTxt = "set server %(backend)s/%(server)s health %(value)s\r\n" - req_args = ['backend', 'server', 'value'] - helpTxt = "Force a server's health to a new state." - -class setServerState(Cmd): - cmdTxt = "set server %(backend)s/%(server)s state %(value)s\r\n" - req_args = ['backend', 'server', 'value'] - helpTxt = "Force a server's administrative state to a new state." - -class setServerWeight(Cmd): - cmdTxt = "set server %(backend)s/%(server)s weight %(value)s\r\n" - req_args = ['backend', 'server', 'value'] - helpTxt = "Force a server's weight to a new state." - -class showSslCrtLists(Cmd): - cmdTxt = "show ssl crt-list\r\n" - helpTxt = "Show the list of crt-lists." - - def getResultObj(self, res): - result = { "crt_lists": []} - for line in res.split("\n"): - if line.startswith('/'): - result["crt_lists"].append(line) - return result - -class showSslCrtList(Cmd): - cmdTxt = "show ssl crt-list -n %(crt_list)s\r\n" - req_args = ['crt_list'] - helpTxt = "Show the the content of a crt-list." - - def getResultObj(self, res): - result = {} - list_id = None - for line in res.split("\n"): - if line.startswith('# '): - list_id = line.split("# ")[1] - result["certs"] = [] - - if list_id and line.startswith('/'): - result["certs"].append(line) - - if result: - return result - - return {"error": res.strip()} - -class showSslCerts(Cmd): - cmdTxt = "show ssl cert\r\n" - helpTxt = "Display the SSL certificates used in memory." - - def getResultObj(self, res): - result = { - "transaction": [], - "filename": [] - } - for line in res.split("\n"): - if line.startswith('*'): - result['transaction'].append(line) - elif line.startswith('/'): - result['filename'].append(line) - return result - -class showSslCert(Cmd): - cmdTxt = "show ssl cert %(certfile)s\r\n" - req_args = ['certfile'] - helpTxt = "Display the details of a SSL certificate used in memory." - - def getResultObj(self, res): - result = {} - cert_id = None - for line in res.split("\n"): - if line: - key = line.split(":")[0] - val = line.split(":")[1].strip() - - if key == 'Filename': - cert_id = val - - if cert_id: - result[key] = val - - if result: - return result - - return {"error": res.strip()} - -class addToSslCrtList(Cmd): - cmdTxt = "add ssl crt-list %(crt_list)s %(certfile)s\r\n" - req_args = ['crt_list', 'certfile'] - helpTxt = "Add a ssl cert to a crt-list." - -class delFromSslCrtList(Cmd): - cmdTxt = "del ssl crt-list %(crt_list)s %(certfile)s\r\n" - req_args = ['crt_list', 'certfile'] - helpTxt = "Delete a ssl cert from a crt-list." - -class newSslCrt(Cmd): - """" Create an empty slot for the certificate in HAProxy’s memory """ - cmdTxt = "new ssl cert %(certfile)s\r\n" - req_args = ['certfile'] - helpTxt = "Create a new certificate file to be used in a crt-list or a directory." - -class updateSslCrt(Cmd): - """" Begin a transaction to upload the certificate into a slot in HAProxy’s memory """ - cmdTxt = "set ssl cert %(certfile)s <<\n%(payload)s\r\n" - req_args = ['certfile', 'payload'] - helpTxt = "Replace a certificate file." - -class delSslCrt(Cmd): - """" Begin a transaction to remove the certificate from a slot in HAProxy’s memory """ - cmdTxt = "del ssl cert %(certfile)s\r\n" - req_args = ['certfile'] - helpTxt = "Delete delete an unused certificate file." - -class commitSslCrt(Cmd): - """ Commit the transaction so HAProxy detects the change. """ - cmdTxt = "commit ssl cert %(certfile)s\r\n" - req_args = ['certfile'] - helpTxt = "Commit a certificate file." - -class abortSslCrt(Cmd): - cmdTxt = "abort ssl cert %(certfile)s\r\n" - req_args = ['certfile'] - helpTxt = "Abort a transaction for a certificate file." - -class showFBEnds(Cmd): - """Base class for getting a listing Frontends and Backends""" - switch = "" - cmdTxt = "show stat\r\n" - - def getResult(self, res): - return "\n".join(self._getResult(res)) - - def getResultObj(self, res): - return self._getResult(res) - - def _getResult(self, res): - """Show Frontend/Backends. To do this, we extract info from - the stat command and filter out by a specific - switch (FRONTEND/BACKEND)""" - - if not self.switch: - raise Exception("No action specified") - - result = [] - lines = res.split('\n') - cl = re.compile("^[^,].+," + self.switch.upper() + ",.*$") - - for e in lines: - me = re.match(cl, e) - if me: - print(e) - result.append(e.split(",")[0]) - return result - -class showFrontends(showFBEnds): - """Show frontends command.""" - switch = "frontend" - helpTxt = "List all Frontends." - -class showBackends(showFBEnds): - """Show backends command.""" - switch = "backend" - helpTxt = "List all Backends." - -class showInfo(Cmd): - """Show info HAProxy command""" - cmdTxt = "show info\r\n" - helpTxt = "Show info on HAProxy instance." - - def getResultObj(self, res): - resDict = {} - for line in res.split('\n'): - k, v = line.split(':') - resDict[k] = v - - return resDict - -class showSessions(Cmd): - """Show sess HAProxy command""" - cmdTxt = "show sess\r\n" - helpTxt = "Show HAProxy sessions." - - def getResultObj(self, res): - return res.split('\n') - -class baseStat(Cmd): - """Base class for stats commands.""" - - def getDict(self, res): - # clean response - res = re.sub(r'^# ', '', res, re.MULTILINE) - res = re.sub(r',\n', '\n', res, re.MULTILINE) - res = re.sub(r',\n\n', '\n', res, re.MULTILINE) - - csv_string = StringIO(res) - return csv.DictReader(csv_string, delimiter=',') - -class showServers(baseStat): - """Show all servers. If backend is given, show only servers for this backend. """ - cmdTxt = "show stat\r\n" - helpTxt = "Lists all servers. Filter for servers in backend, if set." - - def getResultObj(self, res): - servers = [] - - reader = self.getDict(res) - for row in reader: - row = OrderedDict(row) - # show only server - if row['svname'] in ['BACKEND', 'FRONTEND']: - continue - - # filter server for given backend - if self.args['backend'] and row['pxname'] != self.args['backend']: - continue - - # add id - row['id'] = f"{row['pxname']}/{row['svname']}" - row.move_to_end('id', last=False) - servers.append(dict(row)) - - return servers diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/conn.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/conn.py deleted file mode 100755 index 0c38673c16..0000000000 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/conn.py +++ /dev/null @@ -1,83 +0,0 @@ -# pylint: disable=locally-disabled, too-few-public-methods, no-self-use, invalid-name -"""conn.py - Connection module.""" -import re -from socket import socket, AF_INET, AF_UNIX, SOCK_STREAM -from haproxy import const - -class HapError(Exception): - """Generic exception for haproxyctl.""" - pass - -class HaPConn(object): - """HAProxy Socket object. - This class abstract the socket interface so - commands can be sent to HAProxy and results received and - parse by the command objects""" - - def __init__(self, sfile, socket_module=socket): - """Initializes an HAProxy and opens a connection to it - (sfile, type) -> Path for the UNIX socket""" - - self.sock = None - sfile = sfile.strip() - stype = AF_UNIX - self.socket_module = socket_module - - mobj = re.match( - '(?Punix://|tcp://)(?P[^:]+):*(?P[0-9]*)$', sfile) - - if mobj: - proto = mobj.groupdict().get('proto', None) - addr = mobj.groupdict().get('addr', None) - port = mobj.groupdict().get('port', '') - - if not addr or not proto: - raise HapError('Could not determine type of socket.') - - if proto == const.HAP_TCP_PATH: - if not port: - raise HapError('When using a tcp socket, a port is needed.') - stype = AF_INET - sfile = (addr, int(port)) - - if proto == const.HAP_UNIX_PATH: - stype = AF_UNIX - sfile = addr - - # Fallback should be sfile/AF_UNIX by default - self.sfile = (sfile, stype) - self.open() - - def open(self): - """Opens a connection for the socket. - This function should only be called if - self.closed() method was called""" - - sfile, stype = self.sfile - self.sock = self.socket_module(stype, SOCK_STREAM) - self.sock.connect(sfile) - - def sendCmd(self, cmd, objectify=False): - """Receives a command obj and sends it to the socket. Receives the output and passes it - through the command to parse it. - objectify -> Return an object instead of plain text""" - - res = "" - try: - self.sock.send(cmd.getCmd()) - except TypeError: - self.sock.send(bytearray(cmd.getCmd(), 'ASCII')) - output = self.sock.recv(const.HAP_BUFSIZE) - - while output: - res += output.decode('UTF-8') - output = self.sock.recv(const.HAP_BUFSIZE) - - if objectify: - return cmd.getResultObj(res) - - return cmd.getResult(res) - - def close(self): - """Closes the socket""" - self.sock.close() diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/const.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/const.py deleted file mode 100755 index ebd60d8c89..0000000000 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/const.py +++ /dev/null @@ -1,7 +0,0 @@ -"""const.py - Constants for haproxyctl.""" -HAP_OK = 1 -HAP_ERR = 2 -HAP_SOCK_ERR = 3 -HAP_BUFSIZE = 8192 -HAP_UNIX_PATH = 'unix://' -HAP_TCP_PATH = 'tcp://' diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/__init__.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/__init__.py deleted file mode 100755 index e69de29bb2..0000000000 diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py deleted file mode 100755 index 3277871636..0000000000 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_cmds.py +++ /dev/null @@ -1,291 +0,0 @@ -# pylint: disable=star-args, locally-disabled, too-few-public-methods, no-self-use, invalid-name -"""test_cmds.py - Unittests related to command implementations.""" -import sys, os, unittest - -sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) -from haproxy import cmds - - -class TestCommands(unittest.TestCase): - """Tests all of the commands.""" - - def setUp(self): - self.maxDiff = None - self.pem_cert_content = """ - -----BEGIN CERTIFICATE----- - MIIGNjCCBR6gAwIBAgITAPoWnilNUBNcAb8iJ2dgK1eXeTANBgkqhkiG9w0BAQsF - ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0yMTAyMDMw - ODQ2MTBaFw0yMTA1MDQwODQ2MTBaMBoxGDAWBgNVBAMTD3Rlc3QuYW5kZW1hbi5k - ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL7DSlOfRdoKZdX825O4 - Q+uEN85NYR/SJtSLDfaaRebanbDzxp90PEIHCqZyf0q7Zz5eF6qd2ycldtJSVk8b - lVOyJjPIOLUrUAeF6I07b/AOBO/8DU9G3lARSOQkPmC80ahGAW3F1eaccf08qncW - CGxKKXmeL9mbAsA4k6+6pIq8YRBqMCE2bkRQ/scAa8pL7ms5hceONWfqjHC12zIp - yavvnfNVZ6z7QlwHEh3Rajk1IaHLyE7+9+oQ3zXqFtM6sBvXlvVhwsizgkH3ZodN - 81ycvHoP1MWqHGHX0klREQ9qRrHuSuqHsjJHX8gtbqI2Z9DVOUUEunbIkImTwqYj - e5tp7g4RQJUgAdsauyN02NTdeUeci+JDvA3FHJpAtA7tDXIeNcyPjRho17i4VUIc - Yasu5JDF0iSPDT/Srxt6EsDntDFDco1HXMsFqUhMbY2+gUWC3P0n98VWSO+BCtAd - Fbc4+N3QEM8RnQKI86WHR/vnVDoigOhALupXa6czjLGMjaSLDI0nyJ5M81r8ZuBZ - Wu2Q6HTikNmoWl3w6x+9WvY6TQd9OpCjQUu13UMVAco8CGEOj0ZqhhLTccX8dxPK - /01bXMtFRivJfe6vML+O0N54JbI5caXmaEdcEuazAVJWt1ZPGFTMjiw/O0S6Hb0V - YJKXqjJs9t95O5MpL9W4YvGxAgMBAAGjggJrMIICZzAOBgNVHQ8BAf8EBAMCBaAw - HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD - VR0OBBYEFHQLXiD/GxQD11ocGiFauejS5RRmMB8GA1UdIwQYMBaAFMDMA0a5WCDM - XHJw8+EuyyCm9Wg6MHcGCCsGAQUFBwEBBGswaTAyBggrBgEFBQcwAYYmaHR0cDov - L29jc3Auc3RnLWludC14MS5sZXRzZW5jcnlwdC5vcmcwMwYIKwYBBQUHMAKGJ2h0 - dHA6Ly9jZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAaBgNVHREEEzAR - gg90ZXN0LmFuZGVtYW4uZGUwTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC - 3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcw - ggEDBgorBgEEAdZ5AgQCBIH0BIHxAO8AdQAW6GnB0ZXq18P4lxrj8HYB94zhtp0x - qFIYtoN/MagVCAAAAXdnSPbpAAAEAwBGMEQCICAST5iJD7DVrcKRvu9rvNVVnkOW - hAYUgihWr/1Gu6VdAiAcRcZYBP0hIHmFExM9ehJ+J7YmqM35SyiC7s0chsNdHQB2 - AN2ZNPyl5ySAyVZofYE0mQhJskn3tWnYx7yrP1zB825kAAABd2dI+N0AAAQDAEcw - RQIgaaUndm8O3+nCl5OHTf6rOdi9VF9szVckdgDargdWKkgCIQCAjW4UvuMIv4Bt - c6auowPcpdqHjL8XRcztJA3XUGRGHTANBgkqhkiG9w0BAQsFAAOCAQEABza4/ocY - J/XwN8PP+Ane7fVerqL7mRfhzJhxz4mbCPfv4Drq3kUu9fnhR/vaGgdaNdnO83a9 - PUBCm6FCPMcVwX0uKDJ9J4Xj+SVjnVu4+7uhS5LyygtaegoBZyMb5ppxWH1n5r47 - 10ug+KptERFf1datb8/jsEVF7rYCtPXBygjfGAbGuCxViakr4BNcOBPNL+MusfvP - qpH8kEyPAIwHX02XvvpLTy77qiyTpQSuFOusOJptNNqBUeBehqpf8FHn01fnKkcW - pKmFJ2e2VSnTZIBJvD58HMR+WNAEp7tHffHk2z/mPPtdRdxW5Zieoe5+6+HDtwgG - +VCAIWMkC36Dvg== - -----END CERTIFICATE----- - - -----BEGIN RSA PRIVATE KEY----- - MIIJKgIBAAKCAgEAvsNKU59F2gpl1fzbk7hD64Q3zk1hH9Im1IsN9ppF5tqdsPPG - n3Q8QgcKpnJ/SrtnPl4Xqp3bJyV20lJWTxuVU7ImM8g4tStQB4XojTtv8A4E7/wN - T0beUBFI5CQ+YLzRqEYBbcXV5pxx/TyqdxYIbEopeZ4v2ZsCwDiTr7qkirxhEGow - ITZuRFD+xwBrykvuazmFx441Z+qMcLXbMinJq++d81VnrPtCXAcSHdFqOTUhocvI - Tv736hDfNeoW0zqwG9eW9WHCyLOCQfdmh03zXJy8eg/UxaocYdfSSVERD2pGse5K - 6oeyMkdfyC1uojZn0NU5RQS6dsiQiZPCpiN7m2nuDhFAlSAB2xq7I3TY1N15R5yL - 4kO8DcUcmkC0Du0Nch41zI+NGGjXuLhVQhxhqy7kkMXSJI8NP9KvG3oSwOe0MUNy - jUdcywWpSExtjb6BRYLc/Sf3xVZI74EK0B0Vtzj43dAQzxGdAojzpYdH++dUOiKA - 6EAu6ldrpzOMsYyNpIsMjSfInkzzWvxm4Fla7ZDodOKQ2ahaXfDrH71a9jpNB306 - kKNBS7XdQxUByjwIYQ6PRmqGEtNxxfx3E8r/TVtcy0VGK8l97q8wv47Q3nglsjlx - peZoR1wS5rMBUla3Vk8YVMyOLD87RLodvRVgkpeqMmz233k7kykv1bhi8bECAwEA - AQKCAgEAswbSPXJPetahRdcdNyAKVgBq4ykJinSOTpAF1bZo/cOTlFrjwAe0+X5k - R1tTDQ6dURG7AjtNTgrB3Za6O1m2paqeYaB5X8U7QSQx4EG0xsRRa+vPjeQDhX8D - OmCtTdpGpLa2Zo/xM5EFBVUm4cYCt6ZOED4dyAnK5hzytUvjWfR6343Yh4LurxyY - TqidgGgMZALDA0n54wFjNe/lu8kt5Ddns9MmDlhrqbRVEzjSiMfNPWvjHAf7IGcf - JBkBvNDqL+b/XGCYDgUxrLkDNt44E2VhGOi8lZkVM9n5FyeGbEIgAKKTGlGpMbh8 - MoA4wPFwMrO5IIXUfN+zjfnnBkZsnAomGQYDh/hrsQPwU7MoyfO0Wzw+RzLWK8JH - EnjR7O/Lgh+A2AdLhCLiRC5td2uuJ2yLRIRUlcQPsCsYnCCL6Ip9IwK1idmQySGw - bG83decXNSJUv5h3qF6f3fl+JPrHnAbviBzEJ67xAf1MdHbFxwYvRFVfEHj9RZ3W - z+cw7ofD8XVHTfXn0XipvYqI/bVsitMXI35pOt+/ZV8rjJlXopw+IV6U9/60cBkk - BXC7ONDyH2pNwxPbRgcLm2sEK0L9qhxRzCj0iD1WyOAiFJX4ytVbJhR7pt0goiun - i2XDh2l8hoK1lKZNS/yJ+VhnbX595mdqScmIXD8utlgK8f0bLfECggEBAORXimSK - gzegnsBjieTtzC6MmRRxxN46vnMZ2LCeLMxhs3vM7LBcBfsQYqbt/FVFtYBRpr+d - TGTmfPXqKuSqbtAbghxAMo/lECXzALa0nQSsz1fFhX8B7slFarsDmmCb1GmXF/kG - ku/Uoa7jmY3htBj5rjVHjDKPZFVetU+2wbuwlU17Bj4nlSzqud4NMlu56pm3FZ/1 - BAhMxm3z6dLnOgqJzpN1QmKZHNkjLmi8fza/HQM5pP3DpQcPiyuLzywGIqHaO1qT - OIdpZfLEvNpMV7bJ2bagv5nX3TVRWWsBkh0HCAuH30qqaVPpQvkPem1zsM3x+D5q - +PhMIPGpbQiUyCUCggEBANXefd0ZcJymG15WJyO44eFwzgMz9ezfdB8INa+vCOiZ - Y7FtYDgEKu4uzBxtMjO4mQO6DCkfi7JwTJFN4ag3dJEJNGmrf7Xe84IAImJQk0Of - BojAXCFAuNf1Xl3prkvnvtzNirwQMHCUbv5wYzOqglgj2i/hjIj3/Wbt91riq5j+ - 4qQT4kkw/XgCtbQ27HohKIcC/mXbHchEi7NtXrGoM1xqmu1mGH1uul3LQ6p5VwHc - ZFiIAC0awsx9Qe9khZ5EGpZuS0tqJsREcv8ygYMvWcPJEv8aMQM7Nj4biA5rKEgo - L+66ibpntldvbz2qntEvJ2rKzGci0RDUQHy4sW8/d50CggEBAKCZaX7ZZPzk/YL2 - /2+CSQ+cV7ZnZj2fN4Ag96UROxTsyp4SPY60yogQuDIMRGN9SfDcfNlcOvTkn5Me - hdiafqHkFxjjlixawYbPaPsYAS/ek156UDBKHbZ2GmE6YYP9VeKGIJhHpWUFOkqV - TdTaoB7IzVwv3E1bSQg6Om+8bHoj8n6yPmvMz0DuPpgM1BRrqLNAb/c3DwT/ari+ - ywBJHSt4TVCtMmnCouWdtvB3U0ogFLnF+2N4DUPwDMQt6yJdllIb+Y706NdkrA2Z - jfJDq5WmVnf6i4gaqTzs4GVAj5HW9jOV9ti/DqGz+CTQXB1LN1lCDIVqG34XnTwb - G9LjQfkCggEAZwYAt4tTtgJGWNFDlW+wT/sZIm3bX7ncpD4+Ll0w+2s4nPXFTfaj - /4zHgkIP1t5rx2HODdlGYDS8jZpow7HDE0LN3sFgienWf5808QtDhWWLrkCLoPEe - mdl3FeJFtgby6EaTODjMPM8kEKlvACp5E6BhsIMEQc7EYNrtNvjOFKtj3go+DWfu - EeusQB3dGI/0h+UnS0WcOSbb7RkYbphJ9ZDdBNMTpQi7+ga6l9pP0XOrWwJYo2Gq - yPrl0j4oJ69C54hF+RQvjIg0pT5dKSacJTYtUnn5dkcFwDFe/yMbinbhcCynwAXJ - zqC9g4U3cCk44bbDdENPVr4IOox13NND+QKCAQEAilm2oMZoP3WGkBMTSzJl6OGd - F8NnE95noleknNFYuThhCT6T4Z1s28VpxXV7d0DTNOtXj+TzeZq4jrwkgOSZbif0 - 8ky4gRZmm0iFwvAu8ZXk1olHbhMZnCOfh0Qhd4bU2tSoWgWVIAQWEHUhDI7Q1rsX - s4sCjYHKuNMEKdfYvxtKeiunoFqdmT65hwM9o3TfvJfm/RChb7i/nVruXQ6IhPEM - 9WYZS7hlKyqVBESJuonR15biy7Xov5ELl6A821cskZO3vTwtlBSeCDiqaeVLpKR3 - aYwf5YZo7v+N8KBSLEdLNjoKK4PfXUdczD7uOUllbd4/MRgCn4EmFvmpljGiEQ== - -----END RSA PRIVATE KEY----- - - -----BEGIN CERTIFICATE----- - MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw - GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 - MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw - ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 - 8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym - oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 - ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN - xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 - dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 - AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw - HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 - BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu - b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu - Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq - hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF - UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 - AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp - DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 - IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf - zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI - PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w - SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em - 2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 - WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt - n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= - -----END CERTIFICATE----- - """ - - self.Resp = { - "disable": "disable server redis-ro/redis-ro0", - "set-server-agent": "set server redis-ro/redis-ro0 agent up", - "set-server-health": "set server redis-ro/redis-ro0 health stopping", - "set-server-state": "set server redis-ro/redis-ro0 state drain", - "set-server-weight": "set server redis-ro/redis-ro0 weight 10", - "frontends": "show stat", - "info": "show info", - "sessions": "show sess", - "servers": "show stat", - "show-ssl-crt-lists": "show ssl crt-list", - "show-ssl-crt-list": "show ssl crt-list -n /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", - "show-ssl-certs": "show ssl cert", - "show-ssl-cert": "show ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", - "add-to-crt-list": "add ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist /tmp/haproxy/ssl/601a70e4844b0.pem", - "del-from-crt-list": "del ssl crt-list /tmp/haproxy/ssl/601a7392cc9984.99301413.certlist /tmp/haproxy/ssl/601a70e4844b0.pem", - "new-ssl-cert": "new ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", - "update-ssl-cert": "set ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem <<\n%s" % self.pem_cert_content, - "del-ssl-cert": "del ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", - "commit-ssl-cert": "commit ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", - "abort-ssl-cert": "abort ssl cert /tmp/haproxy/ssl/601a70e4844b0.pem", - } - - self.Resp = dict([(k, v + "\r\n") for k, v in self.Resp.items()]) - - def test_setServerAgent(self): - """Test 'set server agent' command""" - args = {"backend": "redis-ro", "server": "redis-ro0", "value": "up"} - cmdOutput = cmds.setServerAgent(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["set-server-agent"]) - - def test_setServerHealth(self): - """Test 'set server health' command""" - args = {"backend": "redis-ro", "server": "redis-ro0", "value": "stopping"} - cmdOutput = cmds.setServerHealth(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["set-server-health"]) - - def test_setServerState(self): - """Test 'set server state' command""" - args = {"backend": "redis-ro", "server": "redis-ro0", "value": "drain"} - cmdOutput = cmds.setServerState(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["set-server-state"]) - - def test_setServerWeight(self): - """Test 'set server weight' command""" - args = {"backend": "redis-ro", "server": "redis-ro0", "value": "10"} - cmdOutput = cmds.setServerWeight(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["set-server-weight"]) - - def test_showFrontends(self): - """Test 'frontends/backends' commands""" - args = {} - cmdOutput = cmds.showFrontends(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["frontends"]) - - def test_showInfo(self): - """Test 'show info' command""" - cmdOutput = cmds.showInfo().getCmd() - self.assertEqual(cmdOutput, self.Resp["info"]) - - def test_showSessions(self): - """Test 'show sess' command""" - cmdOutput = cmds.showSessions().getCmd() - self.assertEqual(cmdOutput, self.Resp["sessions"]) - - def test_showServers(self): - """Test 'show stat' command""" - args = {"backend": "redis-ro"} - cmdOutput = cmds.showServers(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["servers"]) - - def test_showSslCrtLists(self): - """Test 'show ssl crt-list' command""" - cmdOutput = cmds.showSslCrtLists().getCmd() - self.assertEqual(cmdOutput, self.Resp["show-ssl-crt-lists"]) - - def test_showSslCrtList(self): - """Test 'show ssl crt-list ' command""" - args = { - "crt_list": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", - } - cmdOutput = cmds.showSslCrtList(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["show-ssl-crt-list"]) - - def test_showSslCerts(self): - """Test 'show ssl cert' command""" - cmdOutput = cmds.showSslCerts().getCmd() - self.assertEqual(cmdOutput, self.Resp["show-ssl-certs"]) - - def test_showSslCert(self): - """Test 'show ssl cert ' command""" - args = { - "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem" - } - cmdOutput = cmds.showSslCert(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["show-ssl-cert"]) - - def test_addToSslCrtList(self): - """Test 'add ssl crt-list ' command""" - args = { - "crt_list": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", - "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem" - } - cmdOutput = cmds.addToSslCrtList(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["add-to-crt-list"]) - - def test_delFromSslCrtList(self): - """Test 'del ssl crt-list ' command""" - args = { - "crt_list": "/tmp/haproxy/ssl/601a7392cc9984.99301413.certlist", - "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem" - } - cmdOutput = cmds.delFromSslCrtList(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["del-from-crt-list"]) - - def test_newSslCrt(self): - """Test 'new ssl cert ' command""" - args = { - "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem", - } - cmdOutput = cmds.newSslCrt(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["new-ssl-cert"]) - - def test_updateSslCrt(self): - """Test 'set ssl cert ' command""" - args = { - "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem", - "payload": "%s" % self.pem_cert_content - } - cmdOutput = cmds.updateSslCrt(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["update-ssl-cert"]) - - def test_delSslCrt(self): - """Test 'del ssl cert ' command""" - args = { - "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem", - } - cmdOutput = cmds.delSslCrt(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["del-ssl-cert"]) - - def test_commitSslCrt(self): - """Test 'commit ssl cert ' command""" - args = { - "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem", - } - cmdOutput = cmds.commitSslCrt(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["commit-ssl-cert"]) - - def test_abortSslCrt(self): - """Test 'abort ssl cert ' command""" - args = { - "certfile": "/tmp/haproxy/ssl/601a70e4844b0.pem", - } - cmdOutput = cmds.abortSslCrt(**args).getCmd() - self.assertEqual(cmdOutput, self.Resp["abort-ssl-cert"]) - - -if __name__ == '__main__': - unittest.main() diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_conn.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_conn.py deleted file mode 100755 index ea8c15f607..0000000000 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/lib/haproxy/tests/test_conn.py +++ /dev/null @@ -1,59 +0,0 @@ -# pylint: disable=locally-disabled, too-few-public-methods, no-self-use, invalid-name, broad-except -"""test_conn.py - Unittests related to connections to HAProxy.""" -import sys, os -sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) -from haproxy import conn -import unittest -from socket import AF_INET, AF_UNIX - -class SimpleConnMock(object): - """Simple socket mock.""" - def __init__(self, stype, stream): - self.stype = stype - self.stream = stream - - def connect(self, addr): - """Mocked socket.connect method.""" - pass - -class TestConnection(unittest.TestCase): - """Tests different aspects of haproxyctl's connections to HAProxy.""" - - def testConnSimple(self): - """Tests that connection to non-protocol path works and fallsback to UNIX socket.""" - sfile = "/some/path/to/socket.sock" - c = conn.HaPConn(sfile, socket_module=SimpleConnMock) - addr, stype = c.sfile - self.assertEqual(sfile, addr) - self.assertEqual(stype, AF_UNIX) - - def testConnUnixString(self): - """Tests that unix:// protocol works and connects to a socket.""" - sfile = "unix:///some/path/to/socket.socket" - c = conn.HaPConn(sfile, socket_module=SimpleConnMock) - addr, stype = c.sfile - self.assertEqual("/some/path/to/socket.socket", addr) - self.assertEqual(stype, AF_UNIX) - - def testConnTCPString(self): - """Tests that tcp:// protocol works and connects to an IP.""" - sfile = "tcp://1.2.3.4:8080" - c = conn.HaPConn(sfile, socket_module=SimpleConnMock) - addr, stype = c.sfile - ip, port = addr - self.assertEqual("1.2.3.4", ip) - self.assertEqual(8080, port) - self.assertEqual(stype, AF_INET) - - def testConnTCPStringNoPort(self): - """Tests that passing a tcp:// address with no port, raises an Exception.""" - sfile = "tcp://1.2.3.4" - # Not using assertRaises because we still support 2.6 - try: - conn.HaPConn(sfile, socket_module=SimpleConnMock) - raise Exception('Connection should have thrown an exception') - except conn.HapError: - pass - -if __name__ == '__main__': - unittest.main() diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/setup.sh b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/setup.sh index 8dc393461a..5c74fd2792 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/setup.sh +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/setup.sh @@ -5,7 +5,7 @@ if [ -f /etc/rc.conf.d/haproxy ]; then fi # NOTE: Keep /var/haproxy on this list, see GH issue opnsense/plugins #39. -HAPROXY_DIRS="/var/haproxy /var/haproxy/var/run /tmp/haproxy /tmp/haproxy/ssl /tmp/haproxy/lua /tmp/haproxy/errorfiles /tmp/haproxy/mapfiles" +HAPROXY_DIRS="/var/haproxy /var/haproxy/sockets /var/haproxy/var/run /tmp/haproxy /tmp/haproxy/ssl /tmp/haproxy/lua /tmp/haproxy/errorfiles /tmp/haproxy/mapfiles /tmp/haproxy/sockets" for directory in ${HAPROXY_DIRS}; do mkdir -p ${directory} @@ -22,11 +22,6 @@ find /var/haproxy -type d -exec chmod 550 {} \; /usr/local/opnsense/scripts/OPNsense/HAProxy/exportErrorFiles.php > /dev/null 2>&1 /usr/local/opnsense/scripts/OPNsense/HAProxy/exportMapFiles.php > /dev/null 2>&1 -# update OCSP data -if [ "${haproxy_ocsp}" == "YES" ]; then - /usr/local/opnsense/scripts/OPNsense/HAProxy/updateOcsp.sh > /dev/null 2>&1 -fi - # deploy new config case "$1" in deploy) diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py index 554db684aa..bcf407c36f 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/socketCommand.py @@ -4,7 +4,6 @@ import argparse import traceback -sys.path.append(os.path.join(os.path.dirname(__file__), 'lib')) from haproxy.conn import HaPConn from haproxy import cmds diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py index cf8ac67975..8df8543f3b 100755 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py +++ b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/syncCerts.py @@ -9,7 +9,6 @@ import json from typing import List -sys.path.append(os.path.join(os.path.dirname(__file__), 'lib')) from haproxy.conn import HaPConn from haproxy import cmds @@ -357,7 +356,7 @@ def sync(self): for message in cert['messages']: print(" " + repr(message)) - for cert in sync['delete']: + for cert in sync['deleted']: print(f"\n DEL: {cert['cert']}") for message in cert['messages']: print(" " + repr(message)) diff --git a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/updateOcsp.sh b/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/updateOcsp.sh deleted file mode 100755 index 6bf8af9283..0000000000 --- a/net/haproxy/src/opnsense/scripts/OPNsense/HAProxy/updateOcsp.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/sh -# This file is based on: -# https://github.com/acmesh-official/acme.sh/blob/master/deploy/haproxy.sh -# -# Copyright (C) 2021 Neil Pang -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -HAPROXY_DIR="/tmp/haproxy/ssl" -HAPROXY_SOCKET="/var/run/haproxy.socket" - -for _pem in "$HAPROXY_DIR"/*.pem; do - cert_file="$(basename "$_pem")" - _issuer="${HAPROXY_DIR}/${cert_file%.pem}.issuer" - _ocsp="${_pem}.ocsp" - cert_cn="$(openssl x509 -in "$_pem" -noout -text | sed -nE 's/.*Subject:.*CN = ([^,]*)(,.*)?$/\1/p')" - - if [ ! -f "$_issuer" ]; then - continue - fi - - if [ -r "${_issuer}" ]; then - _ocsp_url="$(openssl x509 -noout -ocsp_uri -in "$_pem")" - if [ -n "$_ocsp_url" ]; then - _ocsp_host="$(echo "$_ocsp_url" | cut -d/ -f3)" - subjectdn="$(openssl x509 -in "$_issuer" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)" - issuerdn="$(openssl x509 -in "$_issuer" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)" - if [ "$subjectdn" = "$issuerdn" ]; then - _cafile_argument="-CAfile \"${_issuer}\"" - else - _cafile_argument="" - fi - _openssl_version=$(openssl version | cut -d' ' -f2) - _openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1) - _openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2) - if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then - _header_sep="=" - else - _header_sep=" " - fi - - _openssl_ocsp_cmd="openssl ocsp \ - -issuer \"${_issuer}\" \ - -cert \"${_pem}\" \ - -url \"${_ocsp_url}\" \ - -header Host${_header_sep}\"${_ocsp_host}\" \ - -respout \"${_ocsp}\" \ - -verify_other \"${_issuer}\" \ - ${_cafile_argument} \ - | grep -q \"${_pem}: good\"" - - eval "${_openssl_ocsp_cmd}" - _ret=$? - - if [ "${_ret}" != "0" ]; then - echo "Updating OCSP stapling failed with return code ${_ret}" - else - _update="$(openssl enc -base64 -A -in "${_ocsp}")" - if ! echo "set ssl ocsp-response ${_update}" | socat stdio $HAPROXY_SOCKET; then - echo "Updating haproxy OCSP stapling via socket failed" - fi - fi - fi - fi -done diff --git a/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf b/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf index a9f3c5b25b..407dad535e 100644 --- a/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf +++ b/net/haproxy/src/opnsense/service/conf/actions.d/actions_haproxy.conf @@ -1,9 +1,3 @@ -[setup] -command:/usr/local/opnsense/scripts/OPNsense/HAProxy/setup.sh -parameters: -type:script_output -message:setup haproxy service requirements - [start] command:/usr/local/opnsense/scripts/OPNsense/HAProxy/setup.sh deploy; /usr/local/opnsense/scripts/OPNsense/HAProxy/rc-wrapper.sh start parameters: @@ -31,7 +25,7 @@ description:Reload HAProxy service message:reloading haproxy [configtest] -command:/usr/local/sbin/haproxy -c -f /usr/local/etc/haproxy.conf.staging 2>&1 || exit 0 +command:/usr/local/opnsense/scripts/OPNsense/HAProxy/setup.sh; /usr/local/sbin/haproxy -c -f /usr/local/etc/haproxy.conf.staging 2>&1 || exit 0 parameters: type:script_output message:testing haproxy configuration @@ -97,7 +91,7 @@ type:script_output message:Show diff between configured ssl certificates and certs from HAProxy memory for multiple frontends [cert_sync] -command:configctl template reload OPNsense/HAProxy 2 > /dev/null; /usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py +command:configctl -q template reload OPNsense/HAProxy; /usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py parameters: sync --frontend-ids %s --output json type:script_output message:Sync ssl certificates into HAProxy memory for multiple frontends @@ -109,7 +103,7 @@ type:script_output message:Show diff between configured ssl certificates and certs from HAProxy memory for all frontends [cert_sync_bulk] -command:configctl template reload OPNsense/HAProxy 2 > /dev/null; /usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py sync --output json +command:configctl -q template reload OPNsense/HAProxy; /usr/local/opnsense/scripts/OPNsense/HAProxy/syncCerts.py sync --output json parameters: type:script_output message:Sync ssl certificates into HAProxy memory for all frontends @@ -132,10 +126,3 @@ command:/usr/bin/diff -Naur /usr/local/etc/haproxy.conf /usr/local/etc/haproxy.c parameters: type:script_output message:diff haproxy config - -[update_ocsp] -command:/usr/local/opnsense/scripts/OPNsense/HAProxy/updateOcsp.sh -parameters: -type:script_output -description:Update HAProxy OCSP data -message:update haproxy ocsp data diff --git a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf index 4ce226be3b..642a84e4a6 100644 --- a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf +++ b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf @@ -284,6 +284,8 @@ {% set acl_enabled = '0' %} # ERROR: missing parameters {% endif %} +{% elif acl_data.expression == 'ssl_hello_type' %} +{% do acl_options.append('req.ssl_hello_type ' ~ acl_data.ssl_hello_type|replace('x', '')) %} {% elif acl_data.expression == 'src' %} {% if acl_data.src|default("") != "" %} {% do acl_options.append('src ' ~ acl_data.src) %} @@ -463,6 +465,20 @@ {% set action_enabled = '0' %} # ERROR: missing parameters {% endif %} +{% elif action_data.type == 'fcgi_pass_header' %} +{% if action_data.fcgi_pass_header|default('') != '' %} +{% do action_options.append('pass-header ' ~ action_data.fcgi_pass_header) %} +{% else %} +{% set action_enabled = '0' %} + # ERROR: missing parameters +{% endif %} +{% elif action_data.type == 'fcgi_set_param' %} +{% if action_data.fcgi_set_param|default('') != '' %} +{% do action_options.append('set-param ' ~ action_data.fcgi_set_param) %} +{% else %} +{% set action_enabled = '0' %} + # ERROR: missing parameters +{% endif %} {% elif action_data.type == 'http-request_allow' %} {% do action_options.append('http-request allow') %} {% elif action_data.type == 'http-request_deny' %} @@ -694,7 +710,7 @@ {% set comment_lines = comment_lines + [' # NOTE: actions with no ACLs/conditions will always match'] %} {% endif %} {% if action_options|length > 0 %} -{% do global_action_options.append(comment_lines|join('\n')) %} +{% do global_action_options.append(comment_lines|join('\n')) -%} {% do global_action_options.append(([action_options|join(' '), acl_line]|join(' '))) %} {% endif %} {% else %} @@ -870,22 +886,27 @@ {% if group_data == {} %} # WARNING: group data not found ({{group}}) {% else %} -{# # extract user list from group object #} -{% for user in group_data.members.split(",") %} -{% set user_data = helpers.getUUID(user) %} -{% if user_data.name in users_seen %} +{# # check if group has any member #} +{% if group_data.members is defined %} +{# # extract user list from group object #} +{% for user in group_data.members.split(",") %} +{% set user_data = helpers.getUUID(user) %} +{% if user_data.name in users_seen %} # WARNING: skipping duplicate username ({{user_data.name}}) -{% else %} -{% do users_seen.append(user_data.name) %} -{# # check if using an encrypted password #} -{% if user_data.password|default("")|truncate(1, False, '', 0) == '$' %} -{% set user_pwsec = 'password' %} {% else %} -{% set user_pwsec = 'insecure-password' %} -{% endif %} +{% do users_seen.append(user_data.name) %} +{# # check if using an encrypted password #} +{% if user_data.password|default("")|truncate(1, False, '', 0) == '$' %} +{% set user_pwsec = 'password' %} +{% else %} +{% set user_pwsec = 'insecure-password' %} +{% endif %} user {{user_data.name}} {{user_pwsec}} {{user_data.password}} -{% endif %} -{% endfor %} +{% endif %} +{% endfor %} +{% else %} + # WARNING: ignoring group with no members ({{group_data.name}}) +{% endif %} {% endif %} {% endfor %} {% else %} @@ -944,23 +965,42 @@ global {% else %} stats socket /var/run/haproxy.socket group proxy mode 775 level admin {% endif %} - nbproc {{OPNsense.HAProxy.general.tuning.nbproc}} {% if OPNsense.HAProxy.general.tuning.nbthread|default('') != '' %} nbthread {{OPNsense.HAProxy.general.tuning.nbthread}} {% endif %} {% if helpers.exists('OPNsense.HAProxy.cpus.cpu') %} {% for cpu_map in helpers.toList('OPNsense.HAProxy.cpus.cpu') %} {% if cpu_map.enabled == '1' %} - cpu-map {{cpu_map.process_id|replace('x', '')}}/{{cpu_map.thread_id|replace('x', '')}} {{cpu_map.cpu_id|replace('x', '')|replace(',', ' ')}} + cpu-map 1/{{cpu_map.thread_id|replace('x', '')}} {{cpu_map.cpu_id|replace('x', '')|replace(',', ' ')}} {% endif %} {% endfor %} {% endif %} {% if OPNsense.HAProxy.general.hardStopAfter|default('') != '' %} hard-stop-after {{OPNsense.HAProxy.general.hardStopAfter}} {% endif %} +{% if OPNsense.HAProxy.general.closeSpreadTime|default('') != '' %} + close-spread-time {{OPNsense.HAProxy.general.closeSpreadTime}} +{% endif %} +{# # Disable strict-limits because a syntax check will not reveal #} +{# # whether kern.maxfilesperproc or kern.maxfiles are too low. #} + no strict-limits {% if helpers.exists('OPNsense.HAProxy.general.tuning.maxConnections') %} maxconn {{OPNsense.HAProxy.general.tuning.maxConnections}} {% endif %} +{# # check if OCSP is enabled #} +{% if OPNsense.HAProxy.general.tuning.ocspUpdateEnabled|default('') == '1' %} +{% if helpers.exists('OPNsense.HAProxy.general.tuning.ocspUpdateMinDelay') %} + ocsp-update.mindelay {{OPNsense.HAProxy.general.tuning.ocspUpdateMinDelay}} +{% endif %} +{% if helpers.exists('OPNsense.HAProxy.general.tuning.ocspUpdateMaxDelay') %} + ocsp-update.maxdelay {{OPNsense.HAProxy.general.tuning.ocspUpdateMaxDelay}} +{% endif %} +{% endif %} +{% if helpers.exists('OPNsense.HAProxy.general.tuning.resolversPrefer') %} + httpclient.resolvers.prefer {{OPNsense.HAProxy.general.tuning.resolversPrefer}} +{% else %} + httpclient.resolvers.prefer ipv4 +{% endif %} {% if helpers.exists('OPNsense.HAProxy.general.tuning.maxDHSize') %} tune.ssl.default-dh-param {{OPNsense.HAProxy.general.tuning.maxDHSize}} {% endif %} @@ -975,15 +1015,30 @@ global {% if OPNsense.HAProxy.general.tuning.bogusProxyEnabled|default("") == '1' %} pp2-never-send-local {% endif %} -{% if OPNsense.HAProxy.general.tuning.checkBufferSize|default("") != "" %} - tune.chksize {{OPNsense.HAProxy.general.tuning.checkBufferSize}} -{% endif %} {% if OPNsense.HAProxy.general.tuning.bufferSize|default("") != "" %} tune.bufsize {{OPNsense.HAProxy.general.tuning.bufferSize}} {% endif %} {% if OPNsense.HAProxy.general.tuning.luaMaxMem|default("") != "" %} tune.lua.maxmem {{OPNsense.HAProxy.general.tuning.luaMaxMem}} {% endif %} +{% if OPNsense.HAProxy.general.tuning.h2_initialWindowSize|default("") != "" %} + tune.h2.initial-window-size {{OPNsense.HAProxy.general.tuning.h2_initialWindowSize}} +{% endif %} +{% if OPNsense.HAProxy.general.tuning.h2_initialWindowSizeOutgoing|default("") != "" %} + tune.h2.be.initial-window-size {{OPNsense.HAProxy.general.tuning.h2_initialWindowSizeOutgoing}} +{% endif %} +{% if OPNsense.HAProxy.general.tuning.h2_initialWindowSizeIncoming|default("") != "" %} + tune.h2.fe.initial-window-size {{OPNsense.HAProxy.general.tuning.h2_initialWindowSizeIncoming}} +{% endif %} +{% if OPNsense.HAProxy.general.tuning.h2_maxConcurrentStreams|default("") != "" %} + tune.h2.max-concurrent-streams {{OPNsense.HAProxy.general.tuning.h2_maxConcurrentStreams}} +{% endif %} +{% if OPNsense.HAProxy.general.tuning.h2_maxConcurrentStreamsOutgoing|default("") != "" %} + tune.h2.be.max-concurrent-streams {{OPNsense.HAProxy.general.tuning.h2_maxConcurrentStreamsOutgoing}} +{% endif %} +{% if OPNsense.HAProxy.general.tuning.h2_maxConcurrentStreamsIncoming|default("") != "" %} + tune.h2.fe.max-concurrent-streams {{OPNsense.HAProxy.general.tuning.h2_maxConcurrentStreamsIncoming}} +{% endif %} {# # logging configuration #} {% set logging = [] %} {% if OPNsense.HAProxy.general.logging.host != '127.0.0.1' %} @@ -1032,13 +1087,13 @@ global ssl-default-bind-ciphers {{ OPNsense.HAProxy.general.tuning.ssl_cipherList }} {% endif %} {% if OPNsense.HAProxy.general.tuning.ssl_cipherSuites|default("") != "" %} -{% if helpers.exists('system.firmware.flavour') and not(helpers.empty('system.firmware.flavour')) and system.firmware.flavour|default('') == 'libressl' %} - # WARNING: ssl-default-bind-ciphersuites cannot be used with flavour {{ system.firmware.flavour}}. -{% else %} ssl-default-bind-ciphersuites {{ OPNsense.HAProxy.general.tuning.ssl_cipherSuites }} -{% endif %} {% endif %} {% endif %} +{# # specify local peer #} +{% if peers_enabled is defined %} + localpeer {{ system.hostname|lower }}.{{ system.domain|lower }} +{% endif %} {# # pass-through options #} {% if OPNsense.HAProxy.general.tuning.customOptions|default("") != "" %} # WARNING: pass through options below this line @@ -1062,6 +1117,14 @@ cache opnsense-haproxy-cache {% if OPNsense.HAProxy.general.cache.maxObjectSize|default("") != "" %} max-object-size {{OPNsense.HAProxy.general.cache.maxObjectSize}} {% endif %} +{% if OPNsense.HAProxy.general.cache.processVary|default("") == "1" %} + process-vary on + {% if OPNsense.HAProxy.general.cache.maxSecondaryEntries|default("") != "" %} + max-secondary-entries {{OPNsense.HAProxy.general.cache.maxSecondaryEntries}} + {% endif %} +{% else %} + process-vary off +{% endif %} {%- endif -%} {# ############################### #} @@ -1095,6 +1158,9 @@ defaults {% if OPNsense.HAProxy.general.defaults.init_addr|default("") != "" %} default-server init-addr {{OPNsense.HAProxy.general.defaults.init_addr}} {% endif %} +{% if OPNsense.HAProxy.general.defaults.maxConnectionsServers|default("") != "" %} + default-server maxconn {{OPNsense.HAProxy.general.defaults.maxConnectionsServers}} +{% endif %} {% if OPNsense.HAProxy.general.defaults.customOptions|default("") != "" %} # WARNING: pass through options below this line {% for customOpt in OPNsense.HAProxy.general.defaults.customOptions.split("\n") %} @@ -1171,7 +1237,8 @@ userlist stats_auth resolvers {{resolver.id}} {% if resolver.nameservers|default("") != "" %} {% for nameserver in resolver.nameservers.split(",") %} - nameserver {{nameserver}} {{nameserver}} +{# # special characters are not supported in server names #} + nameserver {{nameserver|replace('@', '_')}} {{nameserver}} {% endfor %} {% endif %} {% if resolver.parse_resolv_conf|default("") == "1" %} @@ -1293,11 +1360,7 @@ frontend {{frontend.name}} {% do ssl_options.append('ciphers ' ~ frontend.ssl_cipherList) %} {% endif %} {% if frontend.ssl_cipherSuites|default("") != "" %} -{% if helpers.exists('system.firmware.flavour') and not(helpers.empty('system.firmware.flavour')) and system.firmware.flavour|default('') == 'libressl' %} - # WARNING: ciphersuites cannot be used with flavour {{ system.firmware.flavour}}. -{% else %} {% do ssl_options.append('ciphersuites ' ~ frontend.ssl_cipherSuites) %} -{% endif %} {% endif %} {# # HSTS #} {% if frontend.ssl_hstsEnabled|default("") == '1' and frontend.mode == 'http' %} @@ -1334,31 +1397,43 @@ frontend {{frontend.name}} {# # convert protocols to HAProxy-compatible format #} {% set alpn_options = frontend.advertised_protocols|replace('http10', 'http/1.0')|replace('http11', 'http/1.1') %} {% do ssl_options.append('alpn ' ~ alpn_options) %} +{% else %} +{# # disable ALPN to enforce the GUI settings #} +{% do ssl_options.append('no-alpn') %} {% endif %} {# # HTTP/2 without TLS #} {% elif frontend.http2Enabled|default("") == '1' and frontend.http2Enabled_nontls|default("") == '1' %} {% do adv_options.append('proto h2') %} {% endif %} {# # CPU affinity configuration #} -{% set bind_process = [] %} {% if frontend.linkedCpuAffinityRules|default('') != '' %} {% for cpu_map in frontend.linkedCpuAffinityRules.split(',') %} {% set cpu_map_data = helpers.getUUID(cpu_map) %} {% if cpu_map_data.enabled == '1' %} -{# # Limit visibility to a certain set of processes #} -{% do bind_process.append(cpu_map_data.process_id|replace('x', '')) %} -{# # Restrict the list of processes/threads on which this listener is allowed to run #} -{% do adv_options.append('process ' ~ cpu_map_data.process_id|replace('x', '') ~ '/' ~ cpu_map_data.thread_id|replace('x', '')) %} +{# # Restrict the list of threads on which this listener is allowed to run #} +{% do adv_options.append('thread ' ~ cpu_map_data.thread_id|replace('x', '')) %} {% endif %} {% endfor %} -{% if bind_process|length > 0 %} - bind-process {{bind_process|join(' ')}} -{% endif %} +{% endif %} +{# # shards / multiple listeners on the same IP:port #} +{% if frontend.tuning_shards|default('') != '' %} +{% do adv_options.append('shards ' ~ frontend.tuning_shards) %} {% endif %} {# # bind/listen configuration #} {% if frontend.bind|default("") != "" %} {% for bind in frontend.bind.split(",") %} - bind {{bind}} name {{bind}} {% if frontend.bindOptions|default("") != "" %}{{ frontend.bindOptions }} {% endif %}{% if frontend.ssl_enabled == '1' and ssl_certs|default("") != "" %}ssl {{ ssl_options|join(' ') }} {{ ssl_certs|join(' ') }} {% endif %}{% if adv_options|length > 0 %} {{ adv_options|join(' ') }} {% endif %} +{# # check if this is a unix socket #} +{% set unix_bind = bind | regex_replace ("^unix@.*","TRUE") %} +{% if unix_bind == "TRUE" %} +{# # extract socket name and add full path #} +{% set socket_name = bind | regex_replace ("^unix@","") %} +{% set bind_address = "/var/haproxy/sockets/" ~ socket_name ~ " user www" %} +{% set bind_name = "unix@" ~ socket_name %} +{% else %} +{% set bind_address = bind %} +{% set bind_name = bind %} +{% endif %} + bind {{bind_address}} name {{bind_name}} {% if frontend.bindOptions|default("") != "" %}{{ frontend.bindOptions }} {% endif %}{% if frontend.ssl_enabled == '1' and ssl_certs|default("") != "" %}ssl {{ ssl_options|join(' ') }} {{ ssl_certs|join(' ') }} {% endif %}{% if adv_options|length > 0 %} {{ adv_options|join(' ') }} {% endif %} {% endfor %} {% endif %} @@ -1383,14 +1458,15 @@ frontend {{frontend.name}} {% if frontend.forwardFor == '1' and frontend.mode == 'http' %} option forwardfor {% endif %} - # tuning options +{% if frontend.prometheus_enabled == '1' and frontend.mode == 'http' and frontend.prometheus_path|default("") != "" %} + http-request use-service prometheus-exporter if { path {{frontend.prometheus_path}} } +{% endif %} +{# # tuning options #} {% if frontend.tuning_maxConnections is defined %} maxconn {{frontend.tuning_maxConnections}} {% endif %} {% if frontend.tuning_timeoutClient is defined %} timeout client {{frontend.tuning_timeoutClient}} -{% elif OPNsense.HAProxy.general.defaults.timeoutClient is defined %} - timeout client {{OPNsense.HAProxy.general.defaults.timeoutClient}} {% endif %} {% if frontend.tuning_timeoutHttpReq|default("") != "" and frontend.mode == 'http' %} timeout http-request {{frontend.tuning_timeoutHttpReq}} @@ -1475,25 +1551,29 @@ backend {{backend.name}} {% set healthcheck_enabled = '0' %} {% elif healthcheck_data.type == 'tcp' %} {# # custom TCP health check option #} +{# # TODO: add support for multiple send/expect steps, see plugins/#2653 #} {% if healthcheck_data.tcp_enabled|default("") == '1' %} -{# # validate options: both must not be disabled at the same time #} +{# # check if any of the required values can be found #} {% if healthcheck_data.tcp_sendValue|default("") == "" and healthcheck_data.tcp_matchValue|default("") == "" %} - # ERROR: invalid custom TCP health check, missing "sendValue" or "matchValue" + # ERROR: invalid custom TCP health check, missing send/expect data {% else %} + option tcp-check +{# # check for "send" value #} +{% if healthcheck_data.tcp_sendValue|default("") != "" %} + tcp-check send {{healthcheck_data.tcp_sendValue}} +{% endif %} {% set healthcheck_customtcp = [] %} -{% do healthcheck_customtcp.append('send ' ~ healthcheck_data.tcp_sendValue) if healthcheck_data.tcp_sendValue|default("") != "" %} {% if healthcheck_data.tcp_matchValue|default("") != "" %} -{% do healthcheck_customtcp.append('expect ' ~ healthcheck_data.tcp_matchType) %} +{% do healthcheck_customtcp.append(healthcheck_data.tcp_matchType) %} {% if healthcheck_data.tcp_negate == '1' and healthcheck_data.tcp_matchType|default("") != 'binary' %} {% do healthcheck_customtcp.append('!') %} {% endif %} {% do healthcheck_customtcp.append(healthcheck_data.tcp_matchValue) %} {% endif %} -{# # XXX: some values (send/match) must be properly escaped (whitespace) #} -{# # TODO: add support for multiple send/expect steps #} - option tcp-check - tcp-check connect - tcp-check {{healthcheck_customtcp|join(' ')}} +{# # check for "expect" value #} +{% if (healthcheck_customtcp|length > 0 ) %} + tcp-check expect {{healthcheck_customtcp|join(' ')}} +{% endif %} {% endif %} {% endif %} {% elif healthcheck_data.type == 'http' %} @@ -1590,23 +1670,28 @@ backend {{backend.name}} {% else %} balance {{backend.algorithm}} {% endif %} +{# # FastCGI application #} +{% if backend.linkedFcgi|default("") != "" %} +{% set fcgi_data = helpers.getUUID(backend.linkedFcgi) %} +{% if fcgi_data == {} %} +# ERROR: FastCGI data not found ({{backend.linkedFcgi}}) +{% elif fcgi_data.enabled == '0' %} +# NOTE: specified FastCGI application is disabled ({{fcgi_data.name}}) +{% else %} + use-fcgi-app {{fcgi_data.name}} +{% endif %} +{% endif %} {# # call macro to evaluate stickiness config #} {{ StickTableConfig(backend,true) }} - # tuning options +{# # tuning options #} {% if backend.tuning_timeoutConnect|default("") != "" %} timeout connect {{backend.tuning_timeoutConnect}} -{% elif OPNsense.HAProxy.general.defaults.timeoutConnect is defined %} - timeout connect {{OPNsense.HAProxy.general.defaults.timeoutConnect}} {% endif %} {% if backend.tuning_timeoutCheck|default("") != "" %} timeout check {{backend.tuning_timeoutCheck}} -{% elif OPNsense.HAProxy.general.defaults.timeoutCheck is defined %} - timeout check {{OPNsense.HAProxy.general.defaults.timeoutCheck}} {% endif %} {% if backend.tuning_timeoutServer|default("") != "" %} timeout server {{backend.tuning_timeoutServer}} -{% elif OPNsense.HAProxy.general.defaults.timeoutServer is defined %} - timeout server {{OPNsense.HAProxy.general.defaults.timeoutServer}} {% endif %} {% if backend.tuning_retries|default("") != "" %} retries {{backend.tuning_retries}} @@ -1632,6 +1717,18 @@ backend {{backend.name}} {% if backend.tuning_httpreuse|default("") != "" and backend.mode == "http" %} http-reuse {{backend.tuning_httpreuse}} {% endif %} +{% if backend.forwardedHeader == '1' and backend.mode == 'http' %} +{% set forwarded_params = [] %} +{% if backend.forwardedHeaderParameters|default("") != "" %} +{% for fwd_param in backend.forwardedHeaderParameters.split(",") %} +{% do forwarded_params.append(fwd_param) %} +{% endfor %} +{% endif %} + option forwarded {{forwarded_params|join(' ')}} +{% endif %} +{% if backend.forwardFor == '1' and backend.mode == 'http' %} + option forwardfor +{% endif %} {% if helpers.exists('OPNsense.HAProxy.general.cache') and OPNsense.HAProxy.general.cache.enabled|default("") == "1" and backend.tuning_caching|default("") == "1" and backend.mode == "http" %} http-request cache-use opnsense-haproxy-cache http-response cache-store opnsense-haproxy-cache @@ -1648,13 +1745,29 @@ backend {{backend.name}} # ERROR: server data not found ({{server}}) {% else %} {# # check if all required server parameters are set #} -{% if (server_data.type|default("") == 'static' and server_data.address|default("") == '') or (server_data.type|default("") == 'template' and (server_data.serviceName|default("") == '' or server_data.number|default("") == '')) %} +{% if (server_data.type|default("") == 'static' and server_data.address|default("") == '') or (server_data.type|default("") == 'template' and (server_data.serviceName|default("") == '' or server_data.number|default("") == '')) or (server_data.type|default("") == 'unix' and server_data.unix_socket|default("") == '') %} # ERROR: server is invalid, required parameters not set ({{server_data.name}}) {% else %} {# # server type #} {% set server_basics = [] %} {% if server_data.type|default("") == 'template' %} {% do server_basics.append('server-template ' ~ server_data.name ~ ' ' ~ server_data.number ~ ' ' ~ server_data.serviceName) %} +{% elif server_data.type|default("") == 'unix' %} +{# # extract unix socket information from frontend #} +{% set frontend_data = helpers.getUUID(server_data.unix_socket) %} +{% set socket_path = "" %} +{% for bind in frontend_data.bind.split(",") %} +{# # check if this is a unix socket #} +{% set unix_bind = bind | regex_replace ("^unix@.*","TRUE") %} +{% if unix_bind == "TRUE" %} +{# # extract socket name and add full path #} +{% set socket_name = bind | regex_replace ("^unix@","") %} +{% set socket_path = "/sockets/" ~ socket_name %} +{% do server_basics.append('server ' ~ server_data.name ~ ' ' ~ socket_path) %} +{# # only the first unix socket is considered #} +{% break %} +{% endif %} +{% endfor %} {% else %} {% do server_basics.append('server ' ~ server_data.name ~ ' ' ~ server_data.address) %} {% endif %} @@ -1694,21 +1807,44 @@ backend {{backend.name}} {% do server_options.append('port ' ~ server_data.checkport) %} {% endif %} {# # force SSL encryption for health checks #} -{% if healthcheck_data.force_ssl|default('') == '1' %} -{% do server_options.append('check-ssl ') %} +{% if healthcheck_data.ssl|default('') == 'ssl' or healthcheck_data.ssl|default('') == 'sslsni' %} +{% do server_options.append('check-ssl') %} +{% elif healthcheck_data.ssl|default('') == 'nossl' %} +{% do server_options.append('no-check-ssl') %} +{% endif %} +{# # add SNI header for health checks #} +{% if healthcheck_data.ssl|default('') == 'sslsni' and healthcheck_data.sslSNI|default('') != '' %} +{% do server_options.append('check-sni ' ~ healthcheck_data.sslSNI) %} +{# # fallback to server SNI value #} +{% elif healthcheck_data.ssl|default('') == 'sslsni' and server_data.sslSNI|default('') != '' %} +{% do server_options.append('check-sni ' ~ server_data.sslSNI) %} +{% elif healthcheck_data.ssl|default('') == 'sslsni' and (healthcheck_data.sslSNI|default('') == '' and server_data.sslSNI|default('') == '') %} +# ERROR: missing SSL SNI value {% endif %} {# # add all additions from healthchecks here #} {% do server_options.append(healthcheck_additions|join(' ')) if healthcheck_additions.length != '0' %} {% endif %} +{# # server maxconn #} +{% do server_options.append('maxconn ' ~ server_data.maxConnections) if server_data.maxConnections|default("") != "" %} {# # server weight #} {% do server_options.append('weight ' ~ server_data.weight) if server_data.weight|default("") != "" %} {# # server role/mode #} {% if server_data.mode|default("") != 'active' %} {% do server_options.append(server_data.mode) %} {% endif %} +{# # force multiplexer protocol if FastCGI is configured for this backend #} +{% if backend.linkedFcgi|default("") != "" and fcgi_data.enabled == '1' %} +{% do server_options.append('proto fcgi') %} +{% elif server_data.multiplexer_protocol|default('') != '' and server_data.multiplexer_protocol|default('') != 'unspecified' %} +{% do server_options.append('proto ' ~ server_data.multiplexer_protocol) %} +{% endif %} {# # server ssl communication #} {% if server_data.ssl|default("") == '1' %} {% do server_options.append('ssl') %} +{# # SNI #} +{% if server_data.sslSNI|default('') != '' %} +{% do server_options.append('sni str(' ~ server_data.sslSNI ~ ')') %} +{% endif %} {# # HTTP/2 #} {% if backend.http2Enabled|default("") == '1' and backend.ba_advertised_protocols|default("") != "" %} {# # convert protocols to HAProxy-compatible format #} @@ -1716,7 +1852,8 @@ backend {{backend.name}} {% do server_options.append('alpn ' ~ alpn_options) %} {% endif %} {# # HTTP/2 without TLS #} -{% elif backend.http2Enabled|default("") == '1' and backend.http2Enabled_nontls|default("") == '1' %} +{# # Must be ignored when a FastCGI application is configured #} +{% elif backend.http2Enabled|default("") == '1' and backend.http2Enabled_nontls|default("") == '1' and (server_data.multiplexer_protocol|default('') == '' or server_data.multiplexer_protocol|default('') == 'unspecified') and (backend.linkedFcgi|default('') == '' or fcgi_data.enabled == '0') %} {% do server_options.append('proto h2') %} {% endif %} {# # ssl verification can be enabled for two reasons: #} @@ -1724,7 +1861,7 @@ backend {{backend.name}} {# # 2. in health checks: to verify *only* health check communication to this server #} {# # When 1. is enabled, health checks are automatically secured. #} {# # Use-case for 2: when using TCP for server communication, but HTTPS for health checks. #} -{% if server_data.ssl|default("") == '1' or (healthcheck_enabled == '1' and healthcheck_data.force_ssl|default('') == '1') %} +{% if server_data.ssl|default("") == '1' or (healthcheck_enabled == '1' and (healthcheck_data.ssl|default('') == 'ssl' or healthcheck_data.ssl|default('') == 'sslsni')) %} {# # get status of ssl verification #} {% set ssl_verify_enabled = '0' %} {% if helpers.exists('OPNsense.HAProxy.general.tuning.sslServerVerify') and OPNsense.HAProxy.general.tuning.sslServerVerify|default("") != 'ignore' %} @@ -1742,7 +1879,7 @@ backend {{backend.name}} {% do server_options.append('ca-file /tmp/haproxy/ssl/' ~ server_data.id ~ '.calist') %} {% else %} {# # fallback to system CA Root Certificates #} -{% do server_options.append('ca-file /etc/ssl/cert.pem') %} +{% do server_options.append('ca-file /usr/local/etc/ssl/cert.pem') %} {% endif %} {# # check for SSL CRL #} {% if server_data.sslCRL|default("") != "" %} @@ -1826,11 +1963,55 @@ backend {{backend.name}} {% endfor %} {%- endif -%} +{# ############################### #} +{# FASTCGI #} +{# ############################### #} + +{%- if helpers.exists('OPNsense.HAProxy.fcgis') %} +{% for fcgi in helpers.toList('OPNsense.HAProxy.fcgis.fcgi') %} +{# # ignore disabled fcgis #} +{% if fcgi.enabled == '1' %} +# FastCGI: {{fcgi.name}} ({{fcgi.description}}) +fcgi-app {{fcgi.name}} + docroot {{fcgi.docroot}} +{% if fcgi.log_stderr|default('') == '1' %} + log-stderr global +{% endif -%} +{% if fcgi.index|default('') != '' %} + index {{fcgi.index}} +{% endif -%} +{% if fcgi.path_info|default('') != '' %} + path-info {{fcgi.path_info}} +{% endif -%} +{% if fcgi.keep_conn|default('') == '1' %} + option keep-conn +{% endif -%} +{% if fcgi.get_values|default('') == '1' %} + option get-values +{% endif -%} +{% if fcgi.mpxs_conns|default('') == '1' %} + option mpxs-conns +{% endif -%} +{% if fcgi.max_reqs|default('') != '' %} + option max-reqs {{fcgi.max_reqs}} +{% endif -%} +{# # action and ACL configuration #} +{% if fcgi.linkedActions|default("") != "" -%} +{# # call macro to evaluate ACLs and actions #} +{{ AclsAndActions(fcgi.linkedActions) }} +{%- endif %} + +{% else %} +# FastCGI (DISABLED): {{fcgi.name}} ({{fcgi.description}}) +{% endif %} +{% endfor %} +{%- endif %} + {# ############################### #} {# PEERS #} {# ############################### #} -{%- if helpers.exists('OPNsense.HAProxy.general.peers') and OPNsense.HAProxy.general.peers.enabled|default("") == "1" %} +{%- if peers_enabled is defined %} {# # ensure that no value is missing #} {% if OPNsense.HAProxy.general.peers.name1|default("") != '' and OPNsense.HAProxy.general.peers.listen1|default("") != '' and @@ -1839,8 +2020,10 @@ backend {{backend.name}} OPNsense.HAProxy.general.peers.listen2|default("") != '' and OPNsense.HAProxy.general.peers.port2|default("") != '' %} peers {{peers_name}} - peer {{OPNsense.HAProxy.general.peers.name1}} {{OPNsense.HAProxy.general.peers.listen1}}:{{OPNsense.HAProxy.general.peers.port1}} - peer {{OPNsense.HAProxy.general.peers.name2}} {{OPNsense.HAProxy.general.peers.listen2}}:{{OPNsense.HAProxy.general.peers.port2}} + peer {{OPNsense.HAProxy.general.peers.name1|lower}} {{OPNsense.HAProxy.general.peers.listen1}}:{{OPNsense.HAProxy.general.peers.port1}} + peer {{OPNsense.HAProxy.general.peers.name2|lower}} {{OPNsense.HAProxy.general.peers.listen2}}:{{OPNsense.HAProxy.general.peers.port2}} +{% else %} +# ERROR: peers configuration is incomplete {% endif %} {%- endif -%} @@ -1848,7 +2031,7 @@ peers {{peers_name}} {# STATISTICS #} {# ############################### #} -{%- if helpers.exists('OPNsense.HAProxy.general.stats') and OPNsense.HAProxy.general.stats.enabled|default("") == "1" %} +{% if helpers.exists('OPNsense.HAProxy.general.stats') and OPNsense.HAProxy.general.stats.enabled|default("") == "1" %} {# # enable local stats #} listen local_statistics bind 127.0.0.1:{{OPNsense.HAProxy.general.stats.port}} @@ -1863,7 +2046,7 @@ listen local_statistics {% endfor %} {% endif %} -{# # remote stats are optional #} +{# # remote stats are optional #} {% if OPNsense.HAProxy.general.stats.remoteEnabled|default("") == "1" %} {% if OPNsense.HAProxy.general.stats.remoteBind|default("") != "" %} listen remote_statistics @@ -1889,8 +2072,18 @@ listen remote_statistics # ERROR: remote statistics disabled, because no listen address was specified {% endif %} {% else %} -# statistics are DISABLED +# remote statistics are DISABLED {% endif %} +{% else %} +# statistics are DISABLED +{% endif %} +{% if helpers.exists('OPNsense.HAProxy.general.stats') and OPNsense.HAProxy.general.stats.prometheus_enabled|default("") == "1" %} +{# # enable prometheus exporter #} +frontend prometheus_exporter + bind {{OPNsense.HAProxy.general.stats.prometheus_bind}} + mode http + http-request use-service prometheus-exporter if { path {{OPNsense.HAProxy.general.stats.prometheus_path}} } {% endif %} + {%- endif -%} diff --git a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/rc.conf.d b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/rc.conf.d index 261881284a..1aa2b759cb 100644 --- a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/rc.conf.d +++ b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/rc.conf.d @@ -1,13 +1,8 @@ {% if helpers.exists('OPNsense.HAProxy.general.enabled') and OPNsense.HAProxy.general.enabled|default("0") == "1" %} haproxy_enable=YES -haproxy_var_script="/usr/local/opnsense/scripts/OPNsense/HAProxy/setup.sh" +haproxy_setup="/usr/local/opnsense/scripts/OPNsense/HAProxy/setup.sh" haproxy_pidfile="/var/run/haproxy.pid" haproxy_config="/usr/local/etc/haproxy.conf" -{% if helpers.exists('OPNsense.HAProxy.general.storeOcsp') and OPNsense.HAProxy.general.storeOcsp|default("0") == "1" %} -haproxy_ocsp=YES -{% else %} -haproxy_ocsp=NO -{% endif %} {% if helpers.exists('OPNsense.HAProxy.general.gracefulStop') and OPNsense.HAProxy.general.gracefulStop|default("0") == "1" %} haproxy_hardstop=NO {% else %} diff --git a/net/igmp-proxy/Makefile b/net/igmp-proxy/Makefile index e322484ae3..036669b4f0 100644 --- a/net/igmp-proxy/Makefile +++ b/net/igmp-proxy/Makefile @@ -1,8 +1,7 @@ PLUGIN_NAME= igmp-proxy PLUGIN_VERSION= 1.5 -PLUGIN_REVISION= 2 +PLUGIN_REVISION= 6 PLUGIN_DEPENDS= igmpproxy PLUGIN_COMMENT= IGMP-Proxy Service -PLUGIN_MAINTAINER= franco@opnsense.org .include "../../Mk/plugins.mk" diff --git a/net/igmp-proxy/src/etc/inc/plugins.inc.d/igmpproxy.inc b/net/igmp-proxy/src/etc/inc/plugins.inc.d/igmpproxy.inc index 42d6f63ffc..6a50501857 100644 --- a/net/igmp-proxy/src/etc/inc/plugins.inc.d/igmpproxy.inc +++ b/net/igmp-proxy/src/etc/inc/plugins.inc.d/igmpproxy.inc @@ -38,17 +38,17 @@ function igmpproxy_enabled() function igmpproxy_services() { - $services = array(); + $services = []; if (!igmpproxy_enabled()) { return $services; } - $pconfig = array(); + $pconfig = []; $pconfig['name'] = 'igmpproxy'; $pconfig['description'] = gettext('IGMP Proxy'); - $pconfig['php']['restart'] = array('igmpproxy_configure_do'); - $pconfig['php']['start'] = array('igmpproxy_configure_do'); + $pconfig['php']['restart'] = ['igmpproxy_configure_do']; + $pconfig['php']['start'] = ['igmpproxy_configure_do']; $services[] = $pconfig; return $services; @@ -56,10 +56,10 @@ function igmpproxy_services() function igmpproxy_configure() { - return array( - 'bootup' => array('igmpproxy_configure_do'), - 'newwanip' => array('igmpproxy_configure_do'), - ); + return [ + 'bootup' => ['igmpproxy_configure_do'], + 'newwanip' => ['igmpproxy_configure_do'], + ]; } function igmpproxy_configure_do($verbose = false) @@ -72,10 +72,7 @@ function igmpproxy_configure_do($verbose = false) return; } - if ($verbose) { - echo 'Starting IGMP Proxy...'; - flush(); - } + service_log('Starting IGMP Proxy...', $verbose); $iflist = get_configured_interface_with_descr(); @@ -90,13 +87,13 @@ EOD; foreach ($config['igmpproxy']['igmpentry'] as $igmpcf) { unset($iflist[$igmpcf['ifname']]); - $realif = get_real_interface($igmpcf['ifname']); + $device = get_real_interface($igmpcf['ifname']); if (empty($igmpcf['threshold'])) { $threshld = 1; } else { $threshld = $igmpcf['threshold']; } - $igmpconf .= "phyint {$realif} {$igmpcf['type']} ratelimit 0 threshold {$threshld}\n"; + $igmpconf .= "phyint {$device} {$igmpcf['type']} ratelimit 0 threshold {$threshld}\n"; if ($igmpcf['address'] <> "") { $item = explode(" ", $igmpcf['address']); @@ -107,15 +104,13 @@ EOD; $igmpconf .= "\n"; } foreach ($iflist as $ifn => $unused) { - $realif = get_real_interface($ifn); - $igmpconf .= "phyint {$realif} disabled\n"; + $device = get_real_interface($ifn); + $igmpconf .= "phyint {$device} disabled\n"; } $igmpconf .= "\n"; - file_put_contents('/usr/local/etc/igmpproxy.conf', $igmpconf); - mwexec('/usr/local/etc/rc.d/igmpproxy onestart'); + file_safe('/usr/local/etc/igmpproxy.conf', $igmpconf); + mwexecf('/usr/local/etc/rc.d/igmpproxy onestart'); - if ($verbose) { - print "done.\n"; - } + service_log("done.\n", $verbose); } diff --git a/net/isc-dhcp/Makefile b/net/isc-dhcp/Makefile new file mode 100644 index 0000000000..970944acbe --- /dev/null +++ b/net/isc-dhcp/Makefile @@ -0,0 +1,9 @@ +PLUGIN_NAME= isc-dhcp +PLUGIN_VERSION= 1.0 +PLUGIN_REVISION= 3 +PLUGIN_COMMENT= ISC DHCPv4/v6 server +PLUGIN_DEPENDS= isc-dhcp44-server +PLUGIN_MAINTAINER= franco@opnsense.org +PLUGIN_TIER= 2 + +.include "../../Mk/plugins.mk" diff --git a/net/isc-dhcp/pkg-descr b/net/isc-dhcp/pkg-descr new file mode 100644 index 0000000000..36f4461845 --- /dev/null +++ b/net/isc-dhcp/pkg-descr @@ -0,0 +1,18 @@ +The ISC Dynamic Host Configuration Protocol Distribution provides a +freely redistributable reference implementation of all aspects of the +DHCP protocol. + +This plugin implements the DHCPv4 and DHCPv6 server in OPNsense, which +used to be the standard implementation of DHCP until better alternatives +such as Dnsmasq and Kea were supplied. + +Plugin Changelog +================ + +1.0 + +* First release resembling the core state of 25.7.11 +* Minor changes due to "radvd" extraction out of DHCPv6 configuration +* Minor changes regarding "track6" and "idassoc6" mode handling +* Safeguard dhcpd_staticarp() from nonexistent interface configuration +* Added DHCPv6 static mappings export diff --git a/net/isc-dhcp/src/etc/dhcpd.opnsense.d/README b/net/isc-dhcp/src/etc/dhcpd.opnsense.d/README new file mode 100644 index 0000000000..236e6b01b3 --- /dev/null +++ b/net/isc-dhcp/src/etc/dhcpd.opnsense.d/README @@ -0,0 +1 @@ +OPNsense: automatically included dhcpd.conf files for IPv4. diff --git a/net/isc-dhcp/src/etc/dhcpd6.opnsense.d/README b/net/isc-dhcp/src/etc/dhcpd6.opnsense.d/README new file mode 100644 index 0000000000..1bf81ac229 --- /dev/null +++ b/net/isc-dhcp/src/etc/dhcpd6.opnsense.d/README @@ -0,0 +1 @@ +OPNsense: automatically included dhcpd.conf files for IPv6. diff --git a/net/isc-dhcp/src/etc/inc/plugins.inc.d/dhcpd.inc b/net/isc-dhcp/src/etc/inc/plugins.inc.d/dhcpd.inc new file mode 100644 index 0000000000..df22829e0d --- /dev/null +++ b/net/isc-dhcp/src/etc/inc/plugins.inc.d/dhcpd.inc @@ -0,0 +1,1327 @@ + + * Copyright (C) 2010 Ermal Luçi + * Copyright (C) 2005-2006 Colin Smith + * Copyright (C) 2003-2004 Manuel Kasper + * 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. + */ + +function dhcpd_configure() +{ + return [ + 'dhcp' => ['dhcpd_dhcp_configure:3'], + 'local' => ['dhcpd_dhcp_configure'], + ]; +} + +function dhcpd_run() +{ + return [ + 'static_mapping' => 'dhcpd_staticmap', + ]; +} + +function dhcpd_syslog() +{ + return ['dhcpd' => ['facility' => ['dhcpd']]]; +} + +function dhcpd_dhcpv6_enabled() +{ + global $config; + + $explicit_off = []; + + /* handle manually configured DHCP6 server settings first */ + foreach (config_read_array('dhcpdv6') as $dhcpv6if => $dhcpv6ifconf) { + if (isset($config['interfaces'][$dhcpv6if]['enable']) && isset($dhcpv6ifconf['enable'])) { + if ($dhcpv6ifconf['enable'] == '-1') { + $explicit_off[] = $dhcpv6if; + } else { + return true; + } + } + } + + /* handle DHCP-PD prefixes and 6RD dynamic interfaces */ + foreach (legacy_config_get_interfaces(['virtual' => false]) as $ifnm => $ifcfg) { + if (in_array($ifnm, $explicit_off)) { + continue; + } + if (isset($ifcfg['enable']) && ($ifcfg['ipaddrv6'] ?? 'none') == 'track6' && !isset($ifcfg['dhcpd6track6allowoverride'])) { + return true; + } + } + + return false; +} + +function dhcpd_dhcpv4_enabled() +{ + global $config; + + foreach (config_read_array('dhcpd') as $dhcpif => $dhcpifconf) { + if (isset($dhcpifconf['enable']) && !empty($config['interfaces'][$dhcpif])) { + return true; + } + } + + return false; +} + +function dhcpd_services() +{ + $services = []; + + if (dhcpd_dhcpv4_enabled()) { + $pconfig = []; + $pconfig['name'] = 'dhcpd'; + $pconfig['description'] = gettext('ISC DHCPv4 Server'); + $pconfig['php']['restart'] = ['dhcpd_dhcp4_configure']; + $pconfig['php']['start'] = ['dhcpd_dhcp4_configure']; + $pconfig['pidfile'] = '/var/dhcpd/var/run/dhcpd.pid'; + $services[] = $pconfig; + } + + if (dhcpd_dhcpv6_enabled()) { + $pconfig = []; + $pconfig['name'] = 'dhcpd6'; + $pconfig['description'] = gettext('ISC DHCPv6 Server'); + $pconfig['php']['restart'] = ['dhcpd_dhcp6_configure']; + $pconfig['php']['start'] = ['dhcpd_dhcp6_configure']; + $pconfig['pidfile'] = '/var/dhcpd/var/run/dhcpdv6.pid'; + $services[] = $pconfig; + } + + return $services; +} + +function dhcpd_xmlrpc_sync() +{ + $result = []; + + $result[] = [ + 'description' => gettext('ISC DHCPv4'), + 'help' => gettext('Synchronize the DHCP Server settings over to the other HA host.'), + 'section' => 'dhcpd', + 'id' => 'dhcpd', + 'services' => ['dhcpd'], + ]; + + $result[] = [ + 'description' => gettext('ISC DHCPv6'), + 'help' => gettext('Synchronize DHCPv6 Server settings over to the other HA host.'), + 'section' => 'dhcpdv6', + 'id' => 'dhcpdv6', + 'services' => ['dhcpdv6'], + ]; + + return $result; +} + +function dhcpd_dhcp_configure($verbose = false, $family = null, $ignorelist = []) +{ + $dirs = ['/dev', '/etc', '/lib', '/run', '/usr', '/usr/local/sbin', '/var/db', '/var/run']; + + foreach ($dirs as $dir) { + mwexecf('/bin/mkdir -p %s', "/var/dhcpd{$dir}"); + } + + if (mwexecfm('/sbin/mount -uw %s', '/var/dhcpd/dev')) { + mwexecf('/sbin/mount -t devfs devfs %s', '/var/dhcpd/dev'); + } + + mwexecf('/usr/sbin/chown -R dhcpd:dhcpd %s', '/var/dhcpd'); + + if ($family == null || $family == 'inet') { + dhcpd_dhcp4_configure($verbose); + } + + if ($family == null || $family == 'inet6') { + dhcpd_dhcp6_configure($verbose, $ignorelist); + } +} + +function dhcpd_dhcp4_configure($verbose = false) +{ + global $config; + + $need_ddns_updates = false; + $ddns_zones = []; + + if (!dhcpd_dhcpv4_enabled()) { + /* XXX dhcpd_staticarp() does not care to disable either way */ + killbypid('/var/dhcpd/var/run/dhcpd.pid'); + return; + } + + service_log('Starting DHCPv4 service...', $verbose); + + killbypid('/var/dhcpd/var/run/dhcpd.pid'); + + /* Only consider DNS servers with IPv4 addresses for the IPv4 DHCP server. */ + $dns_arrv4 = []; + if (!empty($config['system']['dnsserver'][0])) { + foreach ($config['system']['dnsserver'] as $dnsserver) { + if (is_ipaddrv4($dnsserver)) { + $dns_arrv4[] = $dnsserver; + } + } + } + + $custoptions = ""; + $ifconfig_details = legacy_interfaces_details(); + foreach ($config['dhcpd'] as $dhcpif => $dhcpifconf) { + if (isset($dhcpifconf['numberoptions']['item'])) { + foreach ($dhcpifconf['numberoptions']['item'] as $itemidx => $item) { + if (!empty($item['type'])) { + $itemtype = $item['type']; + } else { + $itemtype = "text"; + } + $custoptions .= "option custom-{$dhcpif}-{$itemidx} code {$item['number']} = {$itemtype};\n"; + } + } + } + $dhcpdconf = << $dhcpifconf) { + dhcpd_staticarp($dhcpif, $ifconfig_details); + + if (!isset($dhcpifconf['enable'])) { + continue; + } + + if (!empty($dhcpifconf['failover_peerip'])) { + $intip = get_interface_ip($dhcpif, $ifconfig_details); + $failover_primary = false; + if (!empty($config['virtualip']['vip'])) { + foreach ($config['virtualip']['vip'] as $vipent) { + if ($vipent['interface'] == $dhcpif) { + $carp_nw = gen_subnet($vipent['subnet'], $vipent['subnet_bits']); + if (ip_in_subnet($dhcpifconf['failover_peerip'], "{$carp_nw}/{$vipent['subnet_bits']}")) { + /* this is the interface! */ + if (is_numeric($vipent['advskew']) && (intval($vipent['advskew']) < 20)) { + $failover_primary = true; + } + break; + } + } + } + } else { + log_msg("DHCP failover set up on {$dhcpif} but no CARP virtual IPs defined!", LOG_WARNING); + } + $dhcpdconf_pri = ""; + if ($failover_primary) { + $my_port = "519"; + $peer_port = "520"; + $type = "primary"; + $dhcpdconf_pri = "split 128;\n"; + if (isset($dhcpifconf['failover_split'])) { + $dhcpdconf_pri = "split {$dhcpifconf['failover_split']};\n"; + } + $dhcpdconf_pri .= " mclt 600;\n"; + } else { + $type = "secondary"; + $my_port = "520"; + $peer_port = "519"; + } + + if (is_ipaddrv4($intip)) { + $dhcpdconf .= << $dhcpifconf) { + if (!isset($dhcpifconf['enable']) || !isset($iflist[$dhcpif])) { + continue; + } + + list ($ifcfgip, $ifcfgnet, $ifcfgsn) = interfaces_primary_address($dhcpif, $ifconfig_details); + if (!is_ipaddrv4($ifcfgip) || !is_subnetv4($ifcfgnet)) { + $device = get_real_interface($dhcpif); + log_msg("dhcpd_dhcp4_configure() found no suitable IPv4 address on {$dhcpif}({$device})", LOG_WARNING); + continue; + } + + $subnetmask = gen_subnet_mask($ifcfgsn); + $subnet = explode('/', $ifcfgnet)[0]; + + $all_pools = []; + $all_pools[] = $dhcpifconf; + if (!empty($dhcpifconf['pool'])) { + $all_pools = array_merge($all_pools, $dhcpifconf['pool']); + } + + $dnscfg = ""; + + if (!empty($dhcpifconf['domain'])) { + $dnscfg .= " option domain-name \"{$dhcpifconf['domain']}\";\n"; + } + + if (!empty($dhcpifconf['domainsearchlist'])) { + $dnscfg .= " option domain-search \"" . join("\",\"", preg_split("/[ ;]+/", $dhcpifconf['domainsearchlist'])) . "\";\n"; + } + + $newzone = []; + + if (isset($dhcpifconf['ddnsupdate'])) { + $need_ddns_updates = true; + if (!empty($dhcpifconf['ddnsdomain'])) { + $newzone['domain-name'] = $dhcpifconf['ddnsdomain']; + $dnscfg .= " ddns-domainname \"{$dhcpifconf['ddnsdomain']}\";\n"; + } else { + $newzone['domain-name'] = $config['system']['domain']; + } + $revsubnet = array_reverse(explode(".", $subnet)); + $subnetmask_rev = array_reverse(explode('.', $subnetmask)); + foreach ($subnetmask_rev as $octet) { + if ($octet == "0") { + array_shift($revsubnet); + } + } + $newzone['ptr-domain'] = implode(".", $revsubnet) . ".in-addr.arpa"; + } + + if (!empty($dhcpifconf['dnsserver'][0])) { + $dnscfg .= " option domain-name-servers " . join(",", $dhcpifconf['dnsserver']) . ";"; + if (!empty($newzone['domain-name'])) { + $newzone['dns-servers'] = $dhcpifconf['dnsserver']; + } + } elseif (!empty(service_by_filter(['dns_ports' => '53']))) { + $dnscfg .= " option domain-name-servers {$ifcfgip};"; + if (!empty($newzone['domain-name'])) { + $newzone['dns-servers'] = [$ifcfgip]; + } + } elseif (!empty($dns_arrv4)) { + $dnscfg .= " option domain-name-servers " . join(",", $dns_arrv4) . ";"; + if (!empty($newzone['domain-name'])) { + $newzone['dns-servers'] = $dns_arrv4; + } + } + + /* + * Create classes - These all contain comma-separated lists. + * Join them into one big comma-separated string then split + * them all up. + */ + $all_mac_strings = []; + foreach ($all_pools as $poolconf) { + if (!empty($poolconf['mac_allow'])) { + $all_mac_strings[] = $poolconf['mac_allow']; + } + if (!empty($poolconf['mac_deny'])) { + $all_mac_strings[] = $poolconf['mac_deny']; + } + } + $all_mac_list = array_unique(explode(',', implode(',', $all_mac_strings))); + foreach ($all_mac_list as $mac) { + if (!empty($mac)) { + $dhcpdconf .= 'class "' . str_replace(':', '', $mac) . '" {' . "\n"; + // Skip the first octet of the MAC address - for media type, typically Ethernet ("01") and match the rest. + $dhcpdconf .= ' match if substring (hardware, 1, ' . (substr_count($mac, ':') + 1) . ') = ' . $mac . ';' . "\n"; + $dhcpdconf .= '}' . "\n"; + } + } + + $dhcpdconf .= "\nsubnet {$subnet} netmask {$subnetmask} {\n"; + + // Setup pool options + foreach ($all_pools as $poolconf) { + $dhcpdconf .= " pool {\n"; + if (!empty($poolconf['dnsserver'][0])) { + $dhcpdconf .= ' option domain-name-servers ' . join(',', $poolconf['dnsserver']) . ";\n"; + } + + /* allow/deny MACs */ + if (!empty($poolconf['mac_allow'])) { + $mac_allow_list = array_unique(explode(',', $poolconf['mac_allow'])); + foreach ($mac_allow_list as $mac) { + if (!empty($mac)) { + $dhcpdconf .= " allow members of \"" . str_replace(':', '', $mac) . "\";\n"; + } + } + } + if (!empty($poolconf['mac_deny'])) { + $mac_deny_list = array_unique(explode(',', $poolconf['mac_deny'])); + foreach ($mac_deny_list as $mac) { + if (!empty($mac)) { + $dhcpdconf .= " deny members of \"" . str_replace(':', '', $mac) . "\";\n"; + } + } + } + + if (!empty($poolconf['failover_peerip'])) { + $dhcpdconf .= " deny dynamic bootp clients;\n"; + } + + if (isset($poolconf['denyunknown'])) { + $dhcpdconf .= " deny unknown-clients;\n"; + } + + if (isset($poolconf['ignoreuids'])) { + $dhcpdconf .= " ignore-client-uids true;\n"; + } + + if ( + !empty($poolconf['gateway']) && $poolconf['gateway'] != "none" + && (empty($dhcpifconf['gateway']) || $poolconf['gateway'] != $dhcpifconf['gateway']) + ) { + $dhcpdconf .= " option routers {$poolconf['gateway']};\n"; + } + + if (!empty($dhcpifconf['failover_peerip'])) { + $dhcpdconf .= " failover peer \"dhcp_{$dhcpif}\";\n"; + } + + if ( + !empty($poolconf['domain']) + && (empty($dhcpifconf['domain']) || $poolconf['domain'] != $dhcpifconf['domain']) + ) { + $dhcpdconf .= " option domain-name \"{$poolconf['domain']}\";\n"; + } + + if ( + !empty($poolconf['domainsearchlist']) + && (empty($dhcpifconf['domainsearchlist']) || $poolconf['domainsearchlist'] != $dhcpifconf['domainsearchlist']) + ) { + $dhcpdconf .= " option domain-search \"" . join("\",\"", preg_split("/[ ;]+/", $poolconf['domainsearchlist'])) . "\";\n"; + } + + if (isset($poolconf['ddnsupdate'])) { + if ( + !empty($poolconf['ddnsdomain']) + && (empty($dhcpifconf['ddnsdomain']) || $poolconf['ddnsdomain'] != $dhcpifconf['ddnsdomain']) + ) { + $dhcpdconf .= " ddns-domainname \"{$poolconf['ddnsdomain']}\";\n"; + + $newddnszone = []; + $newddnszone['domain-name'] = $poolconf['ddnsdomain']; + $newddnszone['dns-servers'] = array($poolconf['ddnsdomainprimary']); + $newddnszone['ddnsdomainkeyname'] = $poolconf['ddnsdomainkeyname'] ?? ''; + $newddnszone['ddnsdomainkey'] = $poolconf['ddnsdomainkey'] ?? ''; + $newddnszone['ddnsdomainalgorithm'] = !empty($poolconf['ddnsdomainalgorithm']) ? $poolconf['ddnsdomainalgorithm'] : "hmac-md5"; + $ddns_zones[] = $newddnszone; + } + } + + // default-lease-time + if ( + !empty($poolconf['defaultleasetime']) + && (empty($dhcpifconf['defaultleasetime']) || $poolconf['defaultleasetime'] != $dhcpifconf['defaultleasetime']) + ) { + $dhcpdconf .= " default-lease-time {$poolconf['defaultleasetime']};\n"; + } + + // max-lease-time + if ( + !empty($poolconf['maxleasetime']) + && (empty($dhcpifconf['maxleasetime']) || $poolconf['maxleasetime'] != $dhcpifconf['maxleasetime']) + ) { + $dhcpdconf .= " max-lease-time {$poolconf['maxleasetime']};\n"; + } + // interface MTU + if ( + !empty($poolconf['interface_mtu']) + && (empty($dhcpifconf['interface_mtu']) || $poolconf['interface_mtu'] != $dhcpifconf['interface_mtu']) + ) { + $dhcpdconf .= " option interface-mtu {$poolconf['interface_mtu']};\n"; + } + + // netbios-name* + if ( + !empty($poolconf['winsserver'][0]) + && (empty($dhcpifconf['winsserver'][0]) || $poolconf['winsserver'][0] != $dhcpifconf['winsserver'][0]) + ) { + $dhcpdconf .= " option netbios-name-servers " . join(",", $poolconf['winsserver']) . ";\n"; + $dhcpdconf .= " option netbios-node-type 8;\n"; + } + + // ntp-servers + if ( + !empty($poolconf['ntpserver'][0]) + && (empty($dhcpifconf['ntpserver'][0]) || $poolconf['ntpserver'][0] != $dhcpifconf['ntpserver'][0]) + ) { + $dhcpdconf .= " option ntp-servers " . join(",", $poolconf['ntpserver']) . ";\n"; + } + + // tftp-server-name + if (!empty($poolconf['tftp']) && (empty($dhcpifconf['tftp']) || $poolconf['tftp'] != $dhcpifconf['tftp'])) { + $dhcpdconf .= " option tftp-server-name \"{$poolconf['tftp']}\";\n"; + + // bootfile-name + if (!empty($poolconf['bootfilename']) && (empty($dhcpifconf['bootfilename']) || $poolconf['bootfilename'] != $dhcpifconf['bootfilename'])) { + $dhcpdconf .= " option bootfile-name \"{$poolconf['bootfilename']}\";\n"; + } + } + + // ldap-server + if (!empty($poolconf['ldap']) && (empty($dhcpifconf['ldap']) || $poolconf['ldap'] != $dhcpifconf['ldap'])) { + $dhcpdconf .= " option ldap-server \"{$poolconf['ldap']}\";\n"; + } + + // net boot information + if (isset($poolconf['netboot'])) { + if (!empty($poolconf['nextserver']) && (empty($dhcpifconf['nextserver']) || $poolconf['nextserver'] != $dhcpifconf['nextserver'])) { + $dhcpdconf .= " next-server {$poolconf['nextserver']};\n"; + } + if (!empty($poolconf['filename']) && (empty($dhcpifconf['filename']) || $poolconf['filename'] != $dhcpifconf['filename'])) { + $dhcpdconf .= " filename \"{$poolconf['filename']}\";\n"; + } + if (!empty($poolconf['rootpath']) && (empty($dhcpifconf['rootpath']) || $poolconf['rootpath'] != $dhcpifconf['rootpath'])) { + $dhcpdconf .= " option root-path \"{$poolconf['rootpath']}\";\n"; + } + } + $dhcpdconf .= " range {$poolconf['range']['from']} {$poolconf['range']['to']};\n"; + $dhcpdconf .= " }\n\n"; + } + // End of settings inside pools + + if (!empty($dhcpifconf['gateway'])) { + $routers = $dhcpifconf['gateway'] != "none" ? $dhcpifconf['gateway'] : null; + } else { + // by default, add interface address in "option routers" + $routers = $ifcfgip; + } + if (!empty($routers)) { + $dhcpdconf .= " option routers {$routers};\n"; + } + + $dhcpdconf .= << $item) { + if (empty($item['type']) || $item['type'] == "text") { + $dhcpdconf .= " option custom-{$dhcpif}-{$itemidx} \"{$item['value']}\";\n"; + } else { + $dhcpdconf .= " option custom-{$dhcpif}-{$itemidx} {$item['value']};\n"; + } + } + } + + // ldap-server + if (!empty($dhcpifconf['ldap'])) { + $dhcpdconf .= " option ldap-server \"{$dhcpifconf['ldap']}\";\n"; + } + + // net boot information + if (isset($dhcpifconf['netboot'])) { + if (!empty($dhcpifconf['nextserver'])) { + $dhcpdconf .= " next-server {$dhcpifconf['nextserver']};\n"; + } + + $conditional = false; + $filemap = [ + '00:06' => 'filename32', + '00:07' => 'filename64', + '00:09' => 'filename64', + '00:0a' => 'filename32arm', + '00:0b' => 'filename64arm', + ]; + + if (!empty($dhcpifconf['filenameipxe'])) { + $dhcpdconf .= " if exists user-class and option user-class = \"iPXE\" {\n"; + $dhcpdconf .= " filename \"{$dhcpifconf['filenameipxe']}\";\n"; + $dhcpdconf .= " }"; + $conditional = true; + } + + foreach ($filemap as $arch => $file) { + if (empty($dhcpifconf[$file])) { + continue; + } + + $dhcpdconf .= $conditional ? ' else ' : ' '; + $dhcpdconf .= "if option arch = {$arch} {\n"; + $dhcpdconf .= " filename \"{$dhcpifconf[$file]}\";\n"; + $dhcpdconf .= " }"; + + $conditional = true; + } + + if ($conditional) { + if (!empty($dhcpifconf['filename'])) { + $dhcpdconf .= " else {\n"; + $dhcpdconf .= " filename \"{$dhcpifconf['filename']}\";\n"; + $dhcpdconf .= " }"; + } + $dhcpdconf .= "\n"; + } elseif (!empty($dhcpifconf['filename'])) { + $dhcpdconf .= " filename \"{$dhcpifconf['filename']}\";\n"; + } + + if (!empty($dhcpifconf['rootpath'])) { + $dhcpdconf .= " option root-path \"{$dhcpifconf['rootpath']}\";\n"; + } + } + + $dhcpdconf .= << $sm) { + $dhcpdconf .= "\nhost s_{$dhcpif}_{$i} {\n"; + if (!empty($sm['mac'])) { + $dhcpdconf .= " hardware ethernet {$sm['mac']};\n"; + } + + if (!empty($sm['cid'])) { + $dhcpdconf .= " option dhcp-client-identifier \"{$sm['cid']}\";\n"; + } + + if (!empty($sm['ipaddr'])) { + $dhcpdconf .= " fixed-address {$sm['ipaddr']};\n"; + } + + if (!empty($sm['hostname'])) { + $dhhostname = str_replace(" ", "_", $sm['hostname']); + $dhhostname = str_replace(".", "_", $dhhostname); + $dhcpdconf .= " option host-name \"{$dhhostname}\";\n"; + if ($need_ddns_updates) { + $dhcpdconf .= " ddns-hostname \"{$dhhostname}\";\n"; + } + $dhcpdconf .= " set hostname-override = config-option host-name;\n"; + } + if (!empty($sm['filename'])) { + $dhcpdconf .= " filename \"{$sm['filename']}\";\n"; + } + + if (!empty($sm['rootpath'])) { + $dhcpdconf .= " option root-path \"{$sm['rootpath']}\";\n"; + } + + if (!empty($sm['gateway']) && $sm['gateway'] != "none" && (empty($dhcpifconf['gateway']) || $sm['gateway'] != $dhcpifconf['gateway'])) { + $dhcpdconf .= " option routers {$sm['gateway']};\n"; + } + + $smdnscfg = ""; + if (!empty($sm['domain']) && ($sm['domain'] != $dhcpifconf['domain'])) { + $smdnscfg .= " option domain-name \"{$sm['domain']}\";\n"; + } + + if (!empty($sm['domainsearchlist']) && ($sm['domainsearchlist'] != $dhcpifconf['domainsearchlist'])) { + $smdnscfg .= " option domain-search \"" . join("\",\"", preg_split("/[ ;]+/", $sm['domainsearchlist'])) . "\";\n"; + } + + if ($need_ddns_updates) { + if (!empty($sm['ddnsdomain']) && ($sm['ddnsdomain'] != $dhcpifconf['ddnsdomain'])) { + $smdnscfg .= " ddns-domainname \"{$sm['ddnsdomain']}\";\n"; + } + } + + if (!empty($sm['dnsserver']) && ($sm['dnsserver'][0]) && ($sm['dnsserver'][0] != $dhcpifconf['dnsserver'][0])) { + $smdnscfg .= " option domain-name-servers " . join(",", $sm['dnsserver']) . ";\n"; + } + $dhcpdconf .= "{$smdnscfg}"; + + // default-lease-time + if (!empty($sm['defaultleasetime']) && ($sm['defaultleasetime'] != $dhcpifconf['defaultleasetime'])) { + $dhcpdconf .= " default-lease-time {$sm['defaultleasetime']};\n"; + } + + // max-lease-time + if (!empty($sm['maxleasetime']) && ($sm['maxleasetime'] != $dhcpifconf['maxleasetime'])) { + $dhcpdconf .= " max-lease-time {$sm['maxleasetime']};\n"; + } + + // netbios-name* + if (!empty($sm['winsserver']) && $sm['winsserver'][0] && ($sm['winsserver'][0] != $dhcpifconf['winsserver'][0])) { + $dhcpdconf .= " option netbios-name-servers " . join(",", $sm['winsserver']) . ";\n"; + $dhcpdconf .= " option netbios-node-type 8;\n"; + } + + // ntp-servers + if (!empty($sm['ntpserver']) && $sm['ntpserver'][0] && ($sm['ntpserver'][0] != $dhcpifconf['ntpserver'][0])) { + $dhcpdconf .= " option ntp-servers " . join(",", $sm['ntpserver']) . ";\n"; + } + + // tftp-server-name + if (!empty($sm['tftp']) && ($sm['tftp'] != $dhcpifconf['tftp'])) { + $dhcpdconf .= " option tftp-server-name \"{$sm['tftp']}\";\n"; + + // bootfile-name + if (!empty($sm['bootfilename']) && ($sm['bootfilename'] != $dhcpifconf['bootfilename'])) { + $dhcpdconf .= " option bootfile-name \"{$sm['bootfilename']}\";\n"; + } + } + + $dhcpdconf .= "}\n"; + } + } + + if (!empty($newzone['domain-name']) && isset($dhcpifconf['ddnsupdate']) && is_ipaddrv4($dhcpifconf['ddnsdomainprimary'])) { + $newzone['dns-servers'] = array($dhcpifconf['ddnsdomainprimary']); + $newzone['ddnsdomainkeyname'] = $dhcpifconf['ddnsdomainkeyname'] ?? ''; + $newzone['ddnsdomainkey'] = $dhcpifconf['ddnsdomainkey'] ?? ''; + $newzone['ddnsdomainalgorithm'] = !empty($dhcpifconf['ddnsdomainalgorithm']) ? $dhcpifconf['ddnsdomainalgorithm'] : "hmac-md5"; + $ddns_zones[] = $newzone; + } + + if (isset($dhcpifconf['omapi']) && !$omapi_added) { + $dhcpdconf .= "\nomapi-port {$dhcpifconf['omapiport']};\n"; + if (isset($dhcpifconf['omapialgorithm']) && isset($dhcpifconf['omapikey'])) { + $dhcpdconf .= "key omapi_key {\n"; + $dhcpdconf .= " algorithm {$dhcpifconf['omapialgorithm']};\n"; + $dhcpdconf .= " secret \"{$dhcpifconf['omapikey']}\";\n"; + $dhcpdconf .= "};\nomapi-key omapi_key;\n\n"; + + /* make sure we only add this OMAPI block once */ + $omapi_added = true; + } + } + + $dhcpdifs[] = escapeshellarg(get_real_interface($dhcpif)); + } + + if ($need_ddns_updates) { + $dhcpdconf .= "\nddns-update-style interim;\n"; + $dhcpdconf .= "update-static-leases on;\n"; + $dhcpdconf .= dhcpd_zones($ddns_zones); + } + + foreach (glob('/usr/local/etc/dhcpd.opnsense.d/*.conf') as $file) { + $dhcpdconf .= "\n\n# including custom file {$file}\n" . file_get_contents($file); + } + + @file_put_contents('/var/dhcpd/etc/dhcpd.conf', $dhcpdconf); + @touch('/var/dhcpd/var/db/dhcpd.leases'); + + if (count($dhcpdifs) > 0) { + mwexecf('/usr/local/opnsense/scripts/dhcp/cleanup_leases4.php -m'); + mwexecf('/usr/local/sbin/dhcpd -user dhcpd -group dhcpd -chroot /var/dhcpd -cf /etc/dhcpd.conf -pf /var/run/dhcpd.pid ' . implode(' ', $dhcpdifs)); + } + + service_log("done.\n", $verbose); +} + +function dhcpd_zones($ddns_zones, $ipproto = 'inet') +{ + $dhcpdconf = ''; + if (is_array($ddns_zones)) { + $added_zones = []; + $added_keys = []; + foreach ($ddns_zones as $zone) { + $versionsuffix = $ipproto == "inet6" ? "6" : ""; + // We don't need to add zones multiple times. + foreach ([$zone['domain-name'], $zone['ptr-domain']] as $domain) { + if (!empty($domain) && !in_array($domain, $added_zones)) { + /* dhcpdconf2 is injected *after* the key */ + $dhcpdconf2 = "zone {$domain}. {\n"; + // XXX: $zone['dns-servers'] only contains one item, ref $newzone['dns-servers'] + $dhcpdconf2 .= " primary{$versionsuffix} {$zone['dns-servers'][0]};\n"; + if (!empty($zone['ddnsdomainkeyname']) && !empty($zone['ddnsdomainkey'])) { + if (!in_array($zone['ddnsdomainkeyname'], $added_keys)) { + $dhcpdconf .= "\nkey {$zone['ddnsdomainkeyname']} {\n"; + $dhcpdconf .= " algorithm {$zone['ddnsdomainalgorithm']};\n"; + $dhcpdconf .= " secret {$zone['ddnsdomainkey']};\n"; + $dhcpdconf .= "}\n"; + $added_keys[] = $zone['ddnsdomainkeyname']; + } + $dhcpdconf2 .= " key {$zone['ddnsdomainkeyname']};\n"; + } + $dhcpdconf2 .= "}\n"; + $dhcpdconf .= $dhcpdconf2; + $added_zones[] = $domain; + } + } + } + } + + return $dhcpdconf; +} + +function dhcpd_dhcp6_configure($verbose = false, $ignorelist = []) +{ + global $config; + + if (!dhcpd_dhcpv6_enabled()) { + killbypid('/var/dhcpd/var/run/dhcpdv6.pid'); + killbypid('/var/run/dhcpleases6.pid'); + return; + } + + service_log('Starting DHCPv6 service...', $verbose); + + killbypid('/var/dhcpd/var/run/dhcpdv6.pid'); + killbypid('/var/run/dhcpleases6.pid'); + + $iflist = get_configured_interface_with_descr(); + $ifconfig_details = legacy_interfaces_details(); + $dhcpdv6cfg = config_read_array('dhcpdv6'); + + /* Only consider DNS servers with IPv6 addresses for the IPv6 DHCP server. */ + $dns_arrv6 = array(); + if (!empty($config['system']['dnsserver'][0])) { + foreach ($config['system']['dnsserver'] as $dnsserver) { + if (is_ipaddrv6($dnsserver)) { + $dns_arrv6[] = $dnsserver; + } + } + } + + /* we add a fake entry for interfaces that are set to track6 another WAN */ + foreach (array_keys($iflist) as $ifname) { + /* Do not put in the config an interface which is down */ + if (isset($ignorelist[$ifname])) { + continue; + } + if (isset($config['interfaces'][$ifname]['track6-interface'])) { + list ($ifcfgipv6) = interfaces_primary_address6($ifname, $ifconfig_details); + if (!is_ipaddrv6($ifcfgipv6)) { + continue; + } + + $ifcfgipv6 = Net_IPv6::getNetmask($ifcfgipv6, 64); + $ifcfgipv6arr = explode(':', $ifcfgipv6); + + if ( + ($config['interfaces'][$ifname]['ipaddrv6'] ?? 'none') == 'track6' && + !isset($config['interfaces'][$ifname]['dhcpd6track6allowoverride']) + ) { + /* mock a real server */ + if (!empty($dhcpdv6cfg[$ifname]['enable']) && $dhcpdv6cfg[$ifname]['enable'] == '-1') { + /* tracking, but dhcpv6 disabled */ + $dhcpdv6cfg[$ifname] = []; + } else { + $dhcpdv6cfg[$ifname] = ['enable' => true]; + } + + /* fixed range */ + $ifcfgipv6arr[7] = '1000'; + $dhcpdv6cfg[$ifname]['range'] = array(); + $dhcpdv6cfg[$ifname]['range']['from'] = Net_IPv6::compress(implode(':', $ifcfgipv6arr)); + $ifcfgipv6arr[7] = '2000'; + $dhcpdv6cfg[$ifname]['range']['to'] = Net_IPv6::compress(implode(':', $ifcfgipv6arr)); + + /* with enough room we can add dhcp6 prefix delegation */ + $pdlen = calculate_ipv6_delegation_length($config['interfaces'][$ifname]['track6-interface']); + if ($pdlen > 2) { + /* XXX calculation is probably out of whack, please fix */ + + $pdlenmax = $pdlen; + $pdlenhalf = $pdlenmax - 1; + $pdlenmin = 64 - ceil($pdlenhalf / 4); + $dhcpdv6cfg[$ifname]['prefixrange'] = array(); + $dhcpdv6cfg[$ifname]['prefixrange']['prefixlength'] = $pdlenmin; + + /* set the delegation start to half the current address block */ + $range = Net_IPv6::parseAddress($ifcfgipv6, (64 - $pdlenmax)); + $range['start'] = Net_IPv6::getNetmask($range['end'], (64 - $pdlenhalf)); + + /* set the end range to a multiple of the prefix delegation size, required by dhcpd */ + $range = Net_IPv6::parseAddress($range['end'], (64 - $pdlenhalf)); + $range['end'] = Net_IPv6::getNetmask($range['end'], (64 - round($pdlen / 2))); + + $dhcpdv6cfg[$ifname]['prefixrange']['from'] = Net_IPv6::compress($range['start']); + $dhcpdv6cfg[$ifname]['prefixrange']['to'] = Net_IPv6::compress($range['end']); + } + } else { + if (!empty($dhcpdv6cfg[$ifname]['range']['from']) && !empty($dhcpdv6cfg[$ifname]['range']['to'])) { + /* get config entry and marry it to the live prefix */ + $dhcpdv6cfg[$ifname]['range']['from'] = merge_ipv6_address($ifcfgipv6, $dhcpdv6cfg[$ifname]['range']['from']); + $dhcpdv6cfg[$ifname]['range']['to'] = merge_ipv6_address($ifcfgipv6, $dhcpdv6cfg[$ifname]['range']['to']); + } + + if (!empty($dhcpdv6cfg[$ifname]['prefixrange']['from']) && !empty($dhcpdv6cfg[$ifname]['prefixrange']['to'])) { + /* XXX $pdlen is never validated against prefixlenght setting, but must be smaller or equal */ + $pdlen = 64 - calculate_ipv6_delegation_length($config['interfaces'][$ifname]['track6-interface']); + + $range_from = $dhcpdv6cfg[$ifname]['prefixrange']['from']; + if (merge_ipv6_address($range_from, '::') == '::') { + log_msg("'{$dhcpdv6cfg[$ifname]['prefixrange']['from']}' is not a valid prefix range value", LOG_WARNING); + /* XXX previously it was suggested to use suffix but it was actually infix so shift 64 bits if possible */ + $range_from = $dhcpdv6cfg[$ifname]['prefixrange']['from'] . ':0:0:0:0'; + } + + $range_to = $dhcpdv6cfg[$ifname]['prefixrange']['to']; + if (merge_ipv6_address($range_to, '::') == '::') { + log_msg("'{$dhcpdv6cfg[$ifname]['prefixrange']['to']}' is not a valid prefix range value", LOG_WARNING); + /* XXX previously it was suggested to use suffix but it was actually infix so shift 64 bits if possible */ + $range_to = $dhcpdv6cfg[$ifname]['prefixrange']['to'] . ':0:0:0:0'; + } + + $dhcpdv6cfg[$ifname]['prefixrange']['from'] = merge_ipv6_address($ifcfgipv6, $range_from, $pdlen); + $dhcpdv6cfg[$ifname]['prefixrange']['to'] = merge_ipv6_address($ifcfgipv6, $range_to, $pdlen); + } + } + } + } + + $custoptionsv6 = ""; + foreach ($dhcpdv6cfg as $dhcpv6if => $dhcpv6ifconf) { + if (isset($dhcpv6ifconf['numberoptions']['item'])) { + foreach ($dhcpv6ifconf['numberoptions']['item'] as $itemv6idx => $itemv6) { + if (!empty($itemv6['type'])) { + $itemtype = $itemv6['type']; + } else { + $itemtype = "text"; + } + $custoptionsv6 .= "option custom-{$dhcpv6if}-{$itemv6idx} code {$itemv6['number']} = {$itemtype};\n"; + } + } + } + + if (isset($dhcpv6ifconf['netboot']) && !empty($dhcpv6ifconf['bootfile_url'])) { + $custoptionsv6 .= "option dhcp6.bootfile-url code 59 = string;\n"; + } + + $dhcpdv6conf = << $dhcpv6ifconf) { + if (!isset($dhcpv6ifconf['enable']) || !isset($iflist[$dhcpv6if])) { + continue; + } + + if (isset($ignorelist[$dhcpv6if])) { + continue; + } + + list ($ifcfgipv6, $networkv6) = interfaces_primary_address6($dhcpv6if, $ifconfig_details); + if (!is_ipaddrv6($ifcfgipv6) || !is_subnetv6($networkv6)) { + $device = get_real_interface($dhcpv6if, 'inet6'); + log_msg("dhcpd_dhcp6_configure() found no suitable IPv6 address on {$dhcpv6if}({$device})", LOG_WARNING); + continue; + } + + $dnscfgv6 = ""; + + if (!empty($dhcpv6ifconf['domainsearchlist'])) { + $dnscfgv6 .= " option dhcp6.domain-search \"" . join("\",\"", preg_split("/[ ;]+/", $dhcpv6ifconf['domainsearchlist'])) . "\";\n"; + } + + $newzone = array(); + + if (isset($dhcpv6ifconf['ddnsupdate'])) { + $need_ddns_updates = true; + if (!empty($dhcpv6ifconf['ddnsdomain'])) { + $dnscfgv6 .= " ddns-domainname \"{$dhcpv6ifconf['ddnsdomain']}\";\n"; + $newzone['domain-name'] = $dhcpv6ifconf['ddnsdomain']; + } else { + $newzone['domain-name'] = $config['system']['domain']; + } + + $subnetv6 = explode("/", $networkv6)[0]; + $addr = inet_pton($subnetv6); + $addr_unpack = unpack('H*hex', $addr); + $addr_hex = $addr_unpack['hex']; + $revsubnet = array_reverse(str_split($addr_hex)); + foreach ($revsubnet as $octet) { + if ($octet == "0") { + array_shift($revsubnet); + } else { + break; + } + } + + $newzone['ptr-domain'] = implode(".", $revsubnet) . ".ip6.arpa"; + } + + if (isset($dhcpv6ifconf['dnsserver'][0])) { + $dnscfgv6 .= " option dhcp6.name-servers " . join(",", $dhcpv6ifconf['dnsserver']) . ";"; + } elseif (!empty(service_by_filter(['dns_ports' => '53']))) { + $dnscfgv6 .= " option dhcp6.name-servers {$ifcfgipv6};"; + } elseif (!empty($dns_arrv6)) { + $dnscfgv6 .= " option dhcp6.name-servers " . join(",", $dns_arrv6) . ";"; + } + + $dhcpdv6conf .= "\nsubnet6 {$networkv6} {\n"; + + if (!empty($dhcpv6ifconf['range']['from'])) { + $dhcpdv6conf .= " range6 {$dhcpv6ifconf['range']['from']} {$dhcpv6ifconf['range']['to']};\n"; + } + + $dhcpdv6conf .= "{$dnscfgv6}\n"; + + if (!empty($dhcpv6ifconf['prefixrange']['from']) && is_ipaddrv6($dhcpv6ifconf['prefixrange']['from']) && is_ipaddrv6($dhcpv6ifconf['prefixrange']['to'])) { + $dhcpdv6conf .= " prefix6 {$dhcpv6ifconf['prefixrange']['from']} {$dhcpv6ifconf['prefixrange']['to']}/{$dhcpv6ifconf['prefixrange']['prefixlength']};\n"; + } + + // default-lease-time + if (!empty($dhcpv6ifconf['defaultleasetime'])) { + $dhcpdv6conf .= " default-lease-time {$dhcpv6ifconf['defaultleasetime']};\n"; + } + + // max-lease-time + if (!empty($dhcpv6ifconf['maxleasetime'])) { + $dhcpdv6conf .= " max-lease-time {$dhcpv6ifconf['maxleasetime']};\n"; + } + + // min-secs + if (!empty($dhcpv6ifconf['minsecs'])) { + $dhcpdv6conf .= " min-secs {$dhcpv6ifconf['minsecs']};\n"; + } + + // ntp-servers + if (isset($dhcpv6ifconf['ntpserver'][0])) { + $ntpservers = array(); + foreach ($dhcpv6ifconf['ntpserver'] as $ntpserver) { + if (is_ipaddrv6($ntpserver)) { + $ntpservers[] = $ntpserver; + } + } + if (count($ntpservers) > 0) { + $dhcpdv6conf .= " option dhcp6.sntp-servers " . join(",", $dhcpv6ifconf['ntpserver']) . ";\n"; + } + } + + // Handle option, number rowhelper values + if (isset($dhcpv6ifconf['numberoptions']['item'])) { + $dhcpdv6conf .= "\n"; + foreach ($dhcpv6ifconf['numberoptions']['item'] as $itemv6idx => $itemv6) { + if (empty($itemv6['type']) || $itemv6['type'] == "text") { + $dhcpdv6conf .= " option custom-{$dhcpv6if}-{$itemv6idx} \"{$itemv6['value']}\";\n"; + } else { + $dhcpdv6conf .= " option custom-{$dhcpv6if}-{$itemv6idx} {$itemv6['value']};\n"; + } + } + } + + // net boot information + if (isset($dhcpv6ifconf['netboot'])) { + if (!empty($dhcpv6ifconf['bootfile_url'])) { + $dhcpdv6conf .= " option dhcp6.bootfile-url \"{$dhcpv6ifconf['bootfile_url']}\";\n"; + } + } + + $dhcpdv6conf .= "}\n"; + + /* add static mappings */ + /* Needs to use DUID */ + if (isset($dhcpv6ifconf['staticmap'])) { + $i = 0; + foreach ($dhcpv6ifconf['staticmap'] as $sm) { + $dhcpdv6conf .= << 0) { + mwexecf('/usr/local/sbin/dhcpd -6 -user dhcpd -group dhcpd -chroot /var/dhcpd -cf /etc/dhcpdv6.conf -pf /var/run/dhcpdv6.pid ' . implode(' ', $dhcpdv6ifs)); + mwexecf('/usr/sbin/daemon -m0 -f -p %s %s', ['/var/run/dhcpleases6.pid', '/usr/local/opnsense/scripts/dhcp/prefixes.sh']); + } + + service_log("done.\n", $verbose); +} + +function dhcpd_staticmap($proto = null, $valid_addresses = true, $ifconfig_details = null, $allow_disabled = false, $for_export = false) +{ + $staticmap = []; + foreach (empty($proto) ? [4, 6] : [$proto] as $inet) { + $ipaddr = $inet == 6 ? 'ipaddrv6' : 'ipaddr'; + + foreach (config_read_array($inet == 6 ? 'dhcpdv6' : 'dhcpd') as $dhcpif => $dhcpifconf) { + if (!isset($dhcpifconf['staticmap']) || (!isset($dhcpifconf['enable']) && !$allow_disabled)) { + continue; + } + + $ifconf = config_read_array('interfaces', $dhcpif); + list ($ipaddrv6) = $inet == 6 && isset($ifconf['track6-interface']) ? + interfaces_primary_address6($dhcpif, $ifconfig_details) : [null]; + + foreach ($dhcpifconf['staticmap'] as $host) { + if (empty($host[$ipaddr]) && $valid_addresses) { + /* we only return proper entries with an IP address */ + continue; + } + + if (!empty($ipaddrv6)) { + /* expand IPv6 suffix address, but only allow user-given compressed suffix */ + $host['ipaddrv6'] = merge_ipv6_address($ipaddrv6, $host['ipaddrv6']); + } + + if (!empty($host['ipaddrv6'])) { + /* avoid sloppy input by recompressing the address correctly */ + $host['ipaddrv6'] = Net_IPv6::compress(Net_IPv6::uncompress($host['ipaddrv6'])); + } + + $domain = null; + + // XXX: dhcpdv6 domain entries have been superseded by domainsearchlist, + // for backward compatibility support both here. + if ($inet == 6 && !empty($host['domainsearchlist'])) { + $domain = $host['domainsearchlist']; + } elseif (!empty($host['domain'])) { + $domain = $host['domain']; + } elseif ($inet == 6 && !empty($dhcpifconf['domainsearchlist'])) { + $domain = $dhcpifconf['domainsearchlist']; + } elseif (!empty($dhcpifconf['domain'])) { + $domain = $dhcpifconf['domain']; + } + + if ($domain !== null) { + /* first entry only unless requested */ + $domain = !$for_export ? preg_split('/[ ;]+/', $domain)[0] : + preg_replace('/\s*;\s*/', ',', trim($domain)); + } + + $entry = [ + 'descr' => $host['descr'] ?? null, + 'domain' => $domain, + 'hostname' => $host['hostname'] ?? null, + 'interface' => $dhcpif, + $ipaddr => $host[$ipaddr], + ]; + + foreach (['mac', 'duid'] as $property) { + if (isset($host[$property])) { + $entry[$property] = $host[$property]; + } + } + + $staticmap[] = $entry; + } + } + } + + return $staticmap; +} + +function dhcpd_parse_duid($duid_string) +{ + $parsed_duid = []; + + for ($i = 0; $i < strlen($duid_string); $i++) { + $s = substr($duid_string, $i, 1); + if ($s == '\\') { + $n = substr($duid_string, $i + 1, 1); + if ($n == '\\' || $n == '"') { + $parsed_duid[] = sprintf('%02x', ord($n)); + $i += 1; + } elseif (is_numeric($n)) { + $parsed_duid[] = sprintf('%02x', octdec(substr($duid_string, $i + 1, 3))); + $i += 3; + } + } else { + $parsed_duid[] = sprintf('%02x', ord($s)); + } + } + + $iaid = array_slice($parsed_duid, 0, 4); + $duid = array_slice($parsed_duid, 4); + + return [$iaid, $duid]; +} + +function dhcpd_staticarp($interface, $ifconfig_details) +{ + global $config; + + $ifcfg = $config['interfaces'][$interface]; + if (empty($ifcfg['if']) || !isset($ifcfg['enable'])) { + return; + } + + $device = $ifcfg['if']; + + if (empty($ifconfig_details[$device])) { + return; + } + + $have = in_array('staticarp', $ifconfig_details[$device]['flags']); + $want = isset($config['dhcpd'][$interface]['staticarp']); + + if ($have !== $want) { + mwexecf('/sbin/ifconfig %s %sstaticarp', [$device, $want ? '' : '-']); + mwexecf('/usr/sbin/arp -d -i %s -a', [$device]); + + /* call this only when the ARP cache was flushed */ + interfaces_neighbors_configure($interface, $ifconfig_details); + } +} diff --git a/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv4/Api/LeasesController.php b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv4/Api/LeasesController.php new file mode 100644 index 0000000000..0b3db93c07 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv4/Api/LeasesController.php @@ -0,0 +1,205 @@ +request->get('inactive'); + $selected_interfaces = $this->request->get('selected_interfaces'); + $backend = new Backend(); + $config = Config::getInstance()->object(); + $online = []; + $if_devs = []; + $if_descrs = []; + $ip_ranges = []; + $interfaces = []; + + /* get ARP data to match online clients */ + $arp_data = json_decode($backend->configdRun('dhcpd list arp'), true) ?? []; + /* get static leases */ + $sleases = json_decode($backend->configdRun('dhcpd list static 0'), true) ?? []; + /* get dynamic leases, include inactive leases if requested */ + $leases = json_decode($backend->configdpRun('dhcpd list leases', [$inactive]), true) ?? []; + /* get manufacturer info */ + $mac_man = json_decode($backend->configdRun('interface list macdb'), true) ?? []; + /* get ifconfig info to match IPs to interfaces */ + $ifconfig = json_decode($backend->configdRun('interface list ifconfig'), true) ?? []; + + /* get all device names and their associated interface names */ + foreach ($config->interfaces->children() as $if => $if_props) { + $if_devs[$if] = (string)$if_props->if; + $if_descrs[$if] = (string)$if_props->descr ?: strtoupper($if); + } + + /* list online IPs and MACs */ + if (is_array($arp_data) && isset($arp_data['arp']) && !empty($arp_data['arp']['arp-cache'])) { + foreach ($arp_data['arp']['arp-cache'] as $arp_entry) { + if (!isset($arp_entry['expired'])) { + array_push($online, $arp_entry['mac-address'], $arp_entry['ip-address']); + } + } + } + + /* gather ip ranges from ifconfig */ + foreach ($ifconfig as $if => $data) { + if (!empty($data['ipv4'])) { + foreach ($data['ipv4'] as $ip) { + if (!empty($ip['ipaddr']) && !empty($ip['subnetbits'])) { + $ip_ranges[$ip['ipaddr'] . '/' . $ip['subnetbits']] = $if; + } + } + } + } + + /* parse dynamic leases */ + foreach ($leases as $idx => $lease) { + $leases[$idx]['type'] = 'dynamic'; + $leases[$idx]['status'] = 'offline'; + $leases[$idx]['descr'] = ''; + $leases[$idx]['mac'] = ''; + $leases[$idx]['starts'] = ''; + $leases[$idx]['ends'] = ''; + $leases[$idx]['hostname'] = ''; + $leases[$idx]['state'] = $lease['binding'] == 'free' ? 'expired' : $lease['binding']; + + if (array_key_exists('hardware', $lease)) { + $mac = $lease['hardware']['mac-address']; + $leases[$idx]['mac'] = $mac; + $leases[$idx]['status'] = in_array(strtolower($lease['address']), $online) ? 'online' : 'offline'; + unset($leases[$idx]['hardware']); + } + + if (array_key_exists('starts', $lease)) { + $leases[$idx]['starts'] = date('Y/m/d H:i:s', $lease['starts']); + } + + if (array_key_exists('ends', $lease)) { + $leases[$idx]['ends'] = date('Y/m/d H:i:s', $lease['ends']); + } + + if (array_key_exists('client-hostname', $lease)) { + $leases[$idx]['hostname'] = $lease['client-hostname']; + } + } + + /* parse static leases */ + $statics = []; + if ($sleases) { + foreach ($sleases["dhcpd"] as $slease) { + $static = []; + $static['address'] = $slease['ipaddr'] ?? ''; + $static['type'] = 'static'; + $static['mac'] = $slease['mac'] ?? ''; + $static['starts'] = ''; + $static['ends'] = ''; + $static['hostname'] = $slease['hostname'] ?? ''; + $static['descr'] = $slease['descr'] ?? ''; + $static['if_descr'] = ''; + $static['if'] = $slease['interface'] ?? ''; + $static['state'] = 'active'; + $static['status'] = in_array(strtolower($static['mac']), $online) ? 'online' : 'offline'; + $statics[] = $static; + } + } + + /* merge dynamic and static leases */ + $leases = array_merge($leases, $statics); + + foreach ($leases as $idx => $lease) { + /* include manufacturer info */ + $leases[$idx]['man'] = ''; + if ($lease['mac'] != '') { + $mac_hi = strtoupper(substr(str_replace(':', '', $lease['mac']), 0, 6)); + if (array_key_exists($mac_hi, $mac_man)) { + $leases[$idx]['man'] = $mac_man[$mac_hi]; + } + } + + /* include interface */ + $intf = ''; + $intf_descr = ''; + + if (!empty($lease['if'])) { + /* interface already included */ + $intf = $lease['if']; + $intf_descr = $if_descrs[$intf]; + } else { + /* interface not known, check range */ + foreach ($ip_ranges as $cidr => $if_dev) { + if (!empty($lease['address']) && Util::isIPInCIDR($lease['address'], $cidr)) { + $intf = array_search($if_dev, $if_devs); + $intf_descr = $if_descrs[$intf]; + break; + } + } + } + + $leases[$idx]['if'] = $intf; + $leases[$idx]['if_descr'] = $intf_descr; + + if (!empty($intf_descr) && !array_key_exists($intf, $interfaces)) { + $interfaces[$intf] = $intf_descr; + } + } + + $response = $this->searchRecordsetBase($leases, null, 'address', function ($key) use ($selected_interfaces) { + if (empty($selected_interfaces) || in_array($key['if'], $selected_interfaces)) { + return true; + } + + return false; + }); + + /* present relevant interfaces to the view so they can be filtered on */ + $response['interfaces'] = $interfaces; + return $response; + } + + public function delLeaseAction($ip) + { + $result = ["result" => "failed"]; + + if ($this->request->isPost()) { + $response = json_decode((new Backend())->configdpRun("dhcpd remove lease", [$ip]), true); + if ($response["removed_leases"] != "0") { + $result["result"] = "deleted"; + } + } + + + return $result; + } +} diff --git a/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv4/Api/ServiceController.php b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv4/Api/ServiceController.php new file mode 100644 index 0000000000..050af775e1 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv4/Api/ServiceController.php @@ -0,0 +1,50 @@ +object(); + + foreach ($config->dhcpd->children() as $dhcpifconf) { + if (!empty((string)$dhcpifconf->enable) && (string)$dhcpifconf->enable == '1') { + return true; + } + } + + return false; + } +} diff --git a/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv4/LeasesController.php b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv4/LeasesController.php new file mode 100644 index 0000000000..c53538f83d --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv4/LeasesController.php @@ -0,0 +1,39 @@ +view->pick('OPNsense/DHCPv4/leases'); + } +} diff --git a/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/LeasesController.php b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/LeasesController.php new file mode 100644 index 0000000000..bc4edd4248 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/LeasesController.php @@ -0,0 +1,272 @@ +request->get('inactive'); + $selected_interfaces = $this->request->get('selected_interfaces'); + $backend = new Backend(); + $config = Config::getInstance()->object(); + $online = []; + $if_devs = []; + $if_descrs = []; + $ip_ranges = []; + $leases = []; + $interfaces = []; + + /* get NDP data to match online clients */ + $ndp_data = json_decode($backend->configdRun('interface list ndp json'), true); + /* get static leases */ + $sleases = json_decode($backend->configdRun('dhcpd6 list static 0'), true); + /* get dynamic leases, inactive leases if requested */ + $raw_leases = json_decode($backend->configdpRun('dhcpd6 list leases', [$inactive]), true); + /* get manufacturer info */ + $mac_man = json_decode($backend->configdRun('interface list macdb'), true); + /* get ifconfig info to match IPs to interfaces */ + $ifconfig = json_decode($backend->configdRun('interface list ifconfig'), true); + + /* get all device names and their associated interface names */ + foreach ($config->interfaces->children() as $if => $if_props) { + $if_devs[$if] = (string)$if_props->if; + $if_descrs[$if] = (string)$if_props->descr ?: strtoupper($if); + } + + /* list online IPs and MACs */ + foreach ($ndp_data as $ndp_entry) { + array_push($online, $ndp_entry['mac'], $ndp_entry['ip']); + } + + /* gather ip ranges from ifconfig */ + foreach ($ifconfig as $if => $data) { + if (!empty($data['ipv6'])) { + foreach ($data['ipv6'] as $ip) { + if (!empty($ip['ipaddr']) && !empty($ip['subnetbits'])) { + $ip_ranges[$ip['ipaddr'] . '/' . $ip['subnetbits']] = $if; + } + } + } + } + + foreach ($raw_leases as $raw_lease) { + if (!array_key_exists('addresses', $raw_lease)) { + continue; + } + + /* set defaults */ + $lease = []; + $lease['type'] = 'dynamic'; + $lease['lease_type'] = $raw_lease['lease_type']; + $lease['iaid'] = $raw_lease['iaid']; + $lease['duid'] = $raw_lease['duid']; + $lease['iaid_duid'] = $raw_lease['iaid_duid']; + $lease['descr'] = ''; + $lease['if'] = ''; + + if (array_key_exists('cltt', $raw_lease)) { + $lease['cltt'] = date('Y/m/d H:i:s', $raw_lease['cltt']); + } + + /* XXX we pick the first address, this will be fine for a typical deployment + * according to RFC8415 section 6.6, but it should be noted that the protocol + * (and isc-dhcpv6) is capable of handing over multiple addresses/prefixes to + * a single client within a single lease. The backend accounts for this. + */ + $seg = $raw_lease['addresses'][0]; + $lease['state'] = $seg['binding'] == 'free' ? 'expired' : $seg['binding']; + if (array_key_exists('ends', $seg)) { + $lease['ends'] = date('Y/m/d H:i:s', $seg['ends']); + } + + $lease['address'] = $seg['iaaddr']; + $lease['status'] = in_array(strtolower($lease['address']), $online) ? 'online' : 'offline'; + $leases[] = $lease; + } + + $statics = []; + if ($sleases) { + foreach ($sleases['dhcpd'] as $slease) { + $static = [ + 'address' => $slease['ipaddrv6'] ?? '', + 'type' => 'static', + 'cltt' => '', + 'ends' => '', + 'descr' => $slease['descr'] ?? '', + 'iaid' => '', + 'duid' => $slease['duid'] ?? '', + 'if_descr' => '', + 'if' => $slease['interface'] ?? '', + 'state' => 'active', + 'status' => in_array(strtolower($slease['ipaddrv6']), $online) ? 'online' : 'offline' + ]; + $statics[] = $static; + } + } + + /* merge dynamic and static leases */ + $leases = array_merge($leases, $statics); + + foreach ($leases as $idx => $lease) { + $leases[$idx]['man'] = ''; + $leases[$idx]['mac'] = ''; + $done = false; + /* We infer the MAC from NDP data if available, otherwise we extract it out + * of the DUID. However, RFC8415 section 11 states that an attempt to parse + * a DUID to obtain a client's link-layer address is unreliable, as there is no + * guarantee that the client is still using the same link-layer address as when + * it generated its DUID. Therefore, if we can link it to a manufacturer, chances + * are fairly high that this is a valid MAC address, otherwise we omit the MAC + * address. + */ + if (!empty(['address'])) { + foreach ($ndp_data as $ndp) { + if ($ndp['ip'] == $lease['address']) { + $leases[$idx]['mac'] = $ndp['mac']; + $leases[$idx]['man'] = empty($ndp['manufacturer']) ? '' : $ndp['manufacturer']; + $leases[$idx]['if'] = array_search($ndp['intf'], $if_devs); + $leases[$idx]['if_descr'] = $if_descrs[$leases[$idx]['if']]; + if (!empty($leases[$idx]['if_descr'])) { + $interfaces[$leases[$idx]['if']] = $leases[$idx]['if_descr']; + } + $done = true; + break; + } + } + if ($done) { + continue; + } + } + + /* include MAC */ + if (!empty($lease['duid'])) { + $mac = ''; + $duid_type = substr($lease['duid'], 0, 5); + if ($duid_type === "00:01" || $duid_type === "00:03") { + /* DUID generated based on LL addr with or without timestamp */ + $hw_type = substr($lease['duid'], 6, 5); + if ($hw_type == "00:01") { /* HW type ethernet */ + $mac = substr($lease['duid'], -17, 17); + } + } + + if (!empty($mac)) { + $mac_hi = strtoupper(substr(str_replace(':', '', $mac), 0, 6)); + if (array_key_exists($mac_hi, $mac_man)) { + $leases[$idx]['mac'] = $mac; + $leases[$idx]['man'] = $mac_man[$mac_hi]; + } + } + } + + /* include interface */ + $intf = ''; + $intf_descr = ''; + if (!empty($lease['if'])) { + $intf = $lease['if']; + $intf_descr = $if_descrs[$intf]; + } else { + foreach ($ip_ranges as $cidr => $if) { + if (!empty($lease['address']) && Util::isIPInCIDR($lease['address'], $cidr)) { + $intf = array_search($if, $if_devs); + $intf_descr = $if_descrs[$intf]; + break; + } + } + } + + $leases[$idx]['if'] = $intf; + $leases[$idx]['if_descr'] = $intf_descr; + + if (!empty($intf_descr) && !array_key_exists($intf, $interfaces)) { + $interfaces[$intf] = $intf_descr; + } + } + + $response = $this->searchRecordsetBase($leases, null, 'address', function ($key) use ($selected_interfaces) { + if (empty($selected_interfaces) || in_array($key['if'], $selected_interfaces)) { + return true; + } + + return false; + }, SORT_REGULAR); + + $response['interfaces'] = $interfaces; + return $response; + } + + public function searchPrefixAction() + { + $backend = new Backend(); + $prefixes = []; + + $raw_leases = json_decode($backend->configdpRun('dhcpd6 list leases 1'), true); + foreach ($raw_leases as $raw_lease) { + if ($raw_lease['lease_type'] === 'ia-pd' && array_key_exists('prefixes', $raw_lease)) { + $prefix = []; + $prefix['lease_type'] = $raw_lease['lease_type']; + $prefix['iaid'] = $raw_lease['iaid']; + $prefix['duid'] = $raw_lease['duid']; + if (array_key_exists('cltt', $raw_lease)) { + $prefix['cltt'] = date('Y/m/d H:i:s', $raw_lease['cltt']); + } + + $prefix_raw = $raw_lease['prefixes'][0]; + $prefix['prefix'] = $prefix_raw['iaprefix']; + if (array_key_exists('ends', $prefix_raw)) { + $prefix['ends'] = date('Y/m/d H:i:s', $prefix_raw['ends']); + } + $prefix['state'] = $prefix_raw['binding'] == 'free' ? 'expired' : $prefix_raw['binding']; + $prefixes[] = $prefix; + } + } + + return $this->searchRecordsetBase($prefixes, null, 'prefix', null, SORT_REGULAR); + } + + public function delLeaseAction($ip) + { + $result = ["result" => "failed"]; + + if ($this->request->isPost()) { + $response = json_decode((new Backend())->configdpRun("dhcpd6 remove lease", [$ip]), true); + if ($response["removed_leases"] != "0") { + $result["result"] = "deleted"; + } + } + + return $result; + } +} diff --git a/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/ServiceController.php b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/ServiceController.php new file mode 100644 index 0000000000..e063439791 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/ServiceController.php @@ -0,0 +1,50 @@ +object(); + + foreach ($config->dhcpdv6->children() as $dhcpifconf) { + if (!empty((string)$dhcpifconf->enable) && (string)$dhcpifconf->enable == '1') { + return true; + } + } + + return false; + } +} diff --git a/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/LeasesController.php b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/LeasesController.php new file mode 100644 index 0000000000..872d5d7de4 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/LeasesController.php @@ -0,0 +1,39 @@ +view->pick('OPNsense/DHCPv6/leases'); + } +} diff --git a/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv4/ACL/ACL.xml b/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv4/ACL/ACL.xml new file mode 100644 index 0000000000..1be674d634 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv4/ACL/ACL.xml @@ -0,0 +1,28 @@ + + + Services: ISC DHCPv4: Log File + + ui/diagnostics/log/core/dhcpd/* + api/diagnostics/log/core/dhcpd/* + + + + Services: ISC DHCPv4: Leases + + ui/dhcpv4/leases + api/dhcpv4/leases/* + + + + Services: ISC DHCPv4: Edit + + services_dhcp_edit.php* + + + + Services: ISC DHCPv4 + + services_dhcp.php* + + + diff --git a/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv4/Menu/Menu.xml b/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv4/Menu/Menu.xml new file mode 100644 index 0000000000..336effdbc2 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv4/Menu/Menu.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv6/ACL/ACL.xml b/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv6/ACL/ACL.xml new file mode 100644 index 0000000000..85aba9a1e4 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv6/ACL/ACL.xml @@ -0,0 +1,21 @@ + + + Services: ISC DHCPv6: Edit + + services_dhcpv6_edit.php* + + + + Services: ISC DHCPv6 + + services_dhcpv6.php* + + + + Status: ISC DHCPv6: Leases + + ui/dhcpv6/leases + api/dhcpv6/leases/* + + + diff --git a/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv6/Menu/Menu.xml b/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv6/Menu/Menu.xml new file mode 100644 index 0000000000..13e0eaa73c --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/models/OPNsense/DHCPv6/Menu/Menu.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/net/isc-dhcp/src/opnsense/mvc/app/views/OPNsense/DHCPv4/leases.volt b/net/isc-dhcp/src/opnsense/mvc/app/views/OPNsense/DHCPv4/leases.volt new file mode 100644 index 0000000000..f0b0d860c4 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/views/OPNsense/DHCPv4/leases.volt @@ -0,0 +1,182 @@ +{# + # Copyright (c) 2023 Deciso B.V. + # 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. + #} + + + + + + +
    +
    + +
    +
    + +
    +
    {{ lang._('CPU Rule ID') }} {{ lang._('Enabled') }} {{ lang._('Name') }}{{ lang._('Process ID') }} {{ lang._('Thread ID') }} {{ lang._('CPU ID') }} {{ lang._('Commands') }}
    + + + + + + + + + + + + + + + + + + + + + +
    {{ lang._('Interface') }}{{ lang._('IP Address') }}{{ lang._('MAC Address') }}{{ lang._('Hostname') }}{{ lang._('Description') }}{{ lang._('Start') }}{{ lang._('End') }}{{ lang._('Status') }}{{ lang._('State') }}{{ lang._('Lease Type') }}
    +
    diff --git a/net/isc-dhcp/src/opnsense/mvc/app/views/OPNsense/DHCPv6/leases.volt b/net/isc-dhcp/src/opnsense/mvc/app/views/OPNsense/DHCPv6/leases.volt new file mode 100644 index 0000000000..50fbb44f3b --- /dev/null +++ b/net/isc-dhcp/src/opnsense/mvc/app/views/OPNsense/DHCPv6/leases.volt @@ -0,0 +1,221 @@ +{# + # Copyright (c) 2023 Deciso B.V. + # 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. + #} + + + + + + +
    +
    +
    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    {{ lang._('Interface') }}{{ lang._('IP Address') }}{{ lang._('IAID') }}{{ lang._('DUID') }}{{ lang._('MAC Address') }}{{ lang._('Description') }}{{ lang._('Last Transaction Time') }}{{ lang._('End') }}{{ lang._('Status') }}{{ lang._('State') }}{{ lang._('Lease Type') }}
    +
    +
    + + + + + + + + + + + + + + + + + +
    {{ lang._('IPv6 Prefix') }}{{ lang._('IAID') }}{{ lang._('DUID') }}{{ lang._('Last Transaction Time') }}{{ lang._('End') }}{{ lang._('State') }}
    +
    +
    diff --git a/net/isc-dhcp/src/opnsense/scripts/dhcp/cleanup_leases4.php b/net/isc-dhcp/src/opnsense/scripts/dhcp/cleanup_leases4.php new file mode 100755 index 0000000000..ed10d5548e --- /dev/null +++ b/net/isc-dhcp/src/opnsense/scripts/dhcp/cleanup_leases4.php @@ -0,0 +1,117 @@ +#!/usr/local/bin/php + $dhcpifconf) { + if (!empty($dhcpifconf['staticmap']) && !empty($dhcpifconf['enable'])) { + foreach ($dhcpifconf['staticmap'] as $static) { + if (!empty($static['mac']) && !empty($static['ipaddr'])) { + $addresses[$static['mac']] = $static['ipaddr']; + } + } + } + } +} +if (!empty($opts['d'])) { + $addresses[] = $opts['d']; +} + +if (isset($opts['s'])) { + killbypid('/var/dhcpd/var/run/dhcpd.pid'); +} elseif (isvalidpid('/var/dhcpd/var/run/dhcpd.pid')) { + echo "dhcpd active, can't update lease file"; + exit(1); +} + + + +$removed_leases = 0; +$fin = @fopen($dhcp_lease_file, 'r+'); +$fout = @fopen($dhcp_lease_file . '.new', 'w'); +if ($fin && flock($fin, LOCK_EX)) { + $lease = ''; + $lease_ip = ''; + $lease_mac = ';'; + while (($line = fgets($fin, 4096)) !== false) { + $fields = explode(' ', trim($line)); + if (strpos($line, 'lease ') === 0) { + $lease_ip = trim($fields[1]); + } elseif (strpos($line, 'hardware ethernet ') > 0) { + $lease_mac = strtolower(trim($fields[2], ' \n;')); + } + $lease .= $line; + + if ($line == "}\n") { + // end of segment, flush when relevant + $exact_match = isset($addresses[$lease_mac]) && $addresses[$lease_mac] == $lease_ip; + if ((!isset($addresses[$lease_mac]) && !in_array($lease_ip, $addresses)) || $exact_match) { + fputs($fout, $lease); + } else { + $removed_leases++; + } + $lease = ''; + $lease_ip = ''; + $lease_mac = ';'; + } + } + flock($fin, LOCK_UN); + fclose($fin); + fclose($fout); + @unlink($dhcp_lease_file); + @rename($dhcp_lease_file . '.new', $dhcp_lease_file); +} +if (isset($opts['s'])) { + dhcpd_dhcp4_configure(); +} + +echo json_encode(["removed_leases" => $removed_leases]); diff --git a/net/isc-dhcp/src/opnsense/scripts/dhcp/cleanup_leases6.php b/net/isc-dhcp/src/opnsense/scripts/dhcp/cleanup_leases6.php new file mode 100755 index 0000000000..c8a9447ab0 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/scripts/dhcp/cleanup_leases6.php @@ -0,0 +1,108 @@ +#!/usr/local/bin/php + 0) { + $content_to_flush[] = $line; + } else { + // output data directly if we're not in a "ia-na" section + fputs($fout, $line); + } + + if ($line == "}\n") { + if ($iaaddr != $ip_to_remove) { + // write ia-na section + foreach ($content_to_flush as $cached_line) { + fputs($fout, $cached_line); + } + } else { + $removed_leases++; + // skip empty line + fgets($fin, 4096); + } + // end of segment + $content_to_flush = array(); + $iaaddr = ""; + } + } + flock($fin, LOCK_UN); + fclose($fin); + fclose($fout); + @unlink($dhcp_lease_file); + @rename($dhcp_lease_file . ".new", $dhcp_lease_file); +} + +if (isset($opts['s'])) { + dhcpd_dhcp6_configure(); +} + +echo json_encode(["removed_leases" => $removed_leases]); diff --git a/net/isc-dhcp/src/opnsense/scripts/dhcp/get_leases.py b/net/isc-dhcp/src/opnsense/scripts/dhcp/get_leases.py new file mode 100755 index 0000000000..8b20a86289 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/scripts/dhcp/get_leases.py @@ -0,0 +1,53 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2017-2019 Ad Schellevis + 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. + + -------------------------------------------------------------------------------------- + list dhcp leases +""" +import sys +sys.path.insert(0, "/usr/local/opnsense/site-python") +import watchers.dhcpd +import time +import argparse +import ujson + +parser = argparse.ArgumentParser() +parser.add_argument('--inactive', help='include inactive leases', default='0', type=str) +args = parser.parse_args() + +last_leases = dict() +result = list() +dhcpdleases = watchers.dhcpd.DHCPDLease() +for lease in dhcpdleases.watch(): + # only the last entries for a given IP are relevant + last_leases[lease['address']] = lease + +for lease in last_leases.values(): + if ('ends' in lease and lease['ends'] is not None and lease['ends'] > time.time()) or args.inactive == '1': + result.append(lease) + +print (ujson.dumps(result)) diff --git a/net/isc-dhcp/src/opnsense/scripts/dhcp/get_leases6.py b/net/isc-dhcp/src/opnsense/scripts/dhcp/get_leases6.py new file mode 100755 index 0000000000..5315c06ab6 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/scripts/dhcp/get_leases6.py @@ -0,0 +1,171 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2023 Deciso B.V. + 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. + + -------------------------------------------------------------------------------------- + list dhcpv6 leases +""" +import ujson +import calendar +import datetime +import time +import argparse + +def parse_date(dt): + try: + return calendar.timegm(datetime.datetime.strptime(dt.strip(), "%Y/%m/%d %H:%M:%S;").timetuple()) + except ValueError: + return None + +def parse_iaaddr_iaprefix(input): + """ + parse either an iaaddr or iaprefix segment. return a tuple + containing the type parsed and the corresponding segment + """ + segment = dict() + for idx, line in enumerate(input): + parts = line.split(maxsplit=1) + if idx == 0: + segment[parts[0]] = parts[1].split()[0] + elif not parts[0]: + continue + elif parts[0] == 'binding': + segment[parts[0]] = parts[1].split()[1].strip(';') + elif parts[0] in ('preferred-life', 'max-life'): + segment[parts[0]] = parts[1].strip(';')[0] + elif parts[0] == 'ends': + segment[parts[0]] = parse_date(parts[1].split(maxsplit=1)[1]) + + return segment + +def parse_iaid_duid(input): + """ + parse the combined IAID_DUID value. This is provided in octal format. + non-printable characters are provided as octal escapes. + We return the hex representation of the raw IAID_DUID value, the IAID integer, + as well as the separated DUID value in a dict. The IAID_DUID value is + used to uniquely identify a lease, so this value should be used to determine the last + relevant entry in the leases file. + """ + parsed = [] + i = 0 + while i < len(input): + c = input[i] + if c == '\\': + next_c = input[i + 1] + if next_c == '\\' or next == '"': + parsed.append("%02x" % ord(next_c)) + i += 1 + elif next_c.isnumeric(): + octal_to_decimal = int(input[i+1:i+4], 8) + parsed.append("%02x" % octal_to_decimal) + i += 3 + else: + parsed.append("%02x" % ord(c)) + i += 1 + + return { + 'iaid': int(''.join(reversed(parsed[0:4])), 16), + 'duid': ":".join([str(a) for a in parsed[4:]]), + 'iaid_duid': ":".join([str(a) for a in parsed]) + } + +def parse_lease(lines): + """ + Parse a DHCPv6 lease. We return a two-tuple containing the combined iaid_duid + and the lease. a single lease may contain multiple addresses/prefixes. + """ + lease = dict() + cur_segment = [] + addresses = [] + prefixes = [] + + for idx, line in enumerate(lines): + parts = line.split(maxsplit=1) + if idx == 0: + lease['lease_type'] = parts[0] + lease.update(parse_iaid_duid(parts[1][parts[1].index('"')+1:parts[1].rfind('"')])) + elif parts[0] == 'cltt' and len(parts) >= 2: + cltt = parse_date(parts[1].split(maxsplit=1)[1]) + lease['cltt'] = cltt + + if len(line) > 1 and line[0] == ' ' and '}' in line and len(cur_segment) > 0: + cur_segment.append(line) + segment = parse_iaaddr_iaprefix(cur_segment) + if 'iaaddr' in segment: + addresses.append(segment) + elif 'iaprefix' in segment: + prefixes.append(segment) + cur_segment = [] + elif len(cur_segment) > 0 or parts[0] in ['iaaddr', 'iaprefix']: + cur_segment.append(line) + + # ia_ta/ia_na (addresses) and ia_pd (prefixes) are mutually exclusive. + if addresses: + lease['addresses'] = addresses + elif prefixes: + lease['prefixes'] = prefixes + + return lease + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--inactive', help='include inactive leases', default='0', type=str) + args = parser.parse_args() + leasefile = '/var/dhcpd/var/db/dhcpd6.leases' + result = [] + cur_lease = [] + last_leases = dict() + + try: + with open(leasefile, 'r') as leasef: + for line in leasef: + if len(line) > 5 and (line[0:5] == 'ia-ta' or line[0:5] == 'ia-na' or line[0:5] == 'ia-pd'): + cur_lease.append(line) + elif len(line) > 1 and line[0] == '}' and len(cur_lease) > 0: + cur_lease.append(line) + parsed_lease = parse_lease(cur_lease) + last_leases["%s,%s" % (parsed_lease['lease_type'], parsed_lease['iaid_duid'])] = parsed_lease + cur_lease = [] + elif len(cur_lease) > 0: + cur_lease.append(line) + except IOError: + pass + + for lease in last_leases.values(): + if args.inactive == '1': + result.append(lease) + else: + for key in ('addresses', 'prefixes'): + if key in lease: + for i in range(len(lease[key])): + segment = lease[key][i] + if not ('ends' in segment and segment['ends'] is not None and segment['ends'] > time.time()): + del lease[key][i] + if key in lease and lease[key]: + result.append(lease) + + print(ujson.dumps(result)) diff --git a/net/isc-dhcp/src/opnsense/scripts/dhcp/prefixes.php b/net/isc-dhcp/src/opnsense/scripts/dhcp/prefixes.php new file mode 100755 index 0000000000..92880ea10f --- /dev/null +++ b/net/isc-dhcp/src/opnsense/scripts/dhcp/prefixes.php @@ -0,0 +1,138 @@ +#!/usr/local/bin/php + + * Copyright (C) 2012 Seth Mos + * 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. + */ + +require_once 'config.inc'; +require_once 'interfaces.inc'; +require_once 'util.inc'; +require_once 'plugins.inc.d/dhcpd.inc'; + +$leases_file = '/var/dhcpd/var/db/dhcpd6.leases'; +if (!file_exists($leases_file)) { + exit(1); +} + +$duid_arr = []; +foreach (new SplFileObject($leases_file) as $line) { + if (preg_match("/^(ia-[np][ad])[ ]+\"(.*?)\"/i ", $line, $duidmatch)) { + $type = $duidmatch[1]; + $duid = $duidmatch[2]; + } elseif (preg_match("/iaaddr[ ]+([0-9a-f:]+)[ ]+/i", $line, $addressmatch)) { + $ia_na = $addressmatch[1]; + } elseif (preg_match("/iaprefix[ ]+([0-9a-f:\/]+)[ ]+/i", $line, $prefixmatch)) { + $ia_pd = $prefixmatch[1]; + } elseif (preg_match("/binding state active/i", $line, $activematch)) { + $active = true; + } elseif (preg_match("/^}/i ", $line)) { + $iaid_duid = dhcpd_parse_duid($duid); + $duid = implode(':', $iaid_duid[1]); + + switch ($type) { + case 'ia-na': + if (!empty($ia_na) && !empty($active)) { + $duid_arr[$duid]['address'] = $ia_na; + } + break; + case 'ia-pd': + if (!empty($ia_pd) && !empty($active)) { + if (empty($duid_arr[$duid]['prefix'])) { + $duid_arr[$duid]['prefix'] = []; + } + $duid_arr[$duid]['prefix'][] = $ia_pd; + } + break; + } + + unset($active); + unset($duid); + unset($ia_na); + unset($ia_pd); + unset($type); + } +} + +/* since a route requires a gateway address try to derive it from static mapping as well */ +foreach (plugins_run('static_mapping:dhcpd') as $map) { + foreach ($map as $host) { + if (empty($host['duid'])) { + continue; + } + + if (empty($duid_arr[$host['duid']])) { + continue; + } + + if (!empty($host['ipaddrv6'])) { + $ipaddrv6 = $host['ipaddrv6']; + + /* although link-local is not a real static mapping use it to reach the downstream router */ + if (is_linklocal($ipaddrv6) && strpos($ipaddrv6, '%') === false) { + $ipaddrv6 .= '%' . get_real_interface($host['interface'], 'inet6'); + } + + /* we want static mapping to have a higher priority */ + $duid_arr[$host['duid']]['address'] = $ipaddrv6; + } + } +} + +$routes = []; + +/* collect expired leases */ +$dhcpd_log = shell_safe('opnsense-log -n dhcpd'); +if (!empty($dhcpd_log)) { + foreach (new SplFileObject($dhcpd_log) as $line) { + if (preg_match('/releases prefix ([0-9a-f:]+\/[0-9]+)/i', $line, $expire)) { + /* expire first, overwritten later when active */ + $routes[$expire[1]] = null; + } + } +} + +/* collect active leases */ +foreach ($duid_arr as $entry) { + if (!empty($entry['prefix']) && !empty($entry['address'])) { + foreach ($entry['prefix'] as $prefix) { + /* new or reassigned takes priority */ + $routes[$prefix] = $entry['address']; + } + } +} + +/* expire all first */ +foreach (array_keys($routes) as $prefix) { + mwexecfm('/sbin/route delete -inet6 %s', [$prefix]); +} + +/* active route apply */ +foreach ($routes as $prefix => $address) { + if (!empty($address)) { + mwexecf('/sbin/route add -inet6 %s %s', [$prefix, $address]); + } +} diff --git a/net/isc-dhcp/src/opnsense/scripts/dhcp/prefixes.sh b/net/isc-dhcp/src/opnsense/scripts/dhcp/prefixes.sh new file mode 100755 index 0000000000..00aeda5873 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/scripts/dhcp/prefixes.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# Copyright (C) 2022 Franco Fichtner +# 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. + +CHECKSUM="md5 -q" +INTERVAL=${1:-20} +LEASES="/var/dhcpd/var/db/dhcpd6.leases" +PREVIOUS= + +while :; do + CURRENT=$(${CHECKSUM} ${LEASES}) + if [ "${CURRENT}" != "${PREVIOUS}" ]; then + configctl dhcpd6 update prefixes + PREVIOUS=${CURRENT} + fi + + sleep "${INTERVAL}" +done diff --git a/net/isc-dhcp/src/opnsense/service/conf/actions.d/actions_dhcpd.conf b/net/isc-dhcp/src/opnsense/service/conf/actions.d/actions_dhcpd.conf new file mode 100644 index 0000000000..00ec71db8c --- /dev/null +++ b/net/isc-dhcp/src/opnsense/service/conf/actions.d/actions_dhcpd.conf @@ -0,0 +1,50 @@ +[list.leases] +command:/usr/local/opnsense/scripts/dhcp/get_leases.py +parameters:--inactive %s +type:script_output +message:list dhcp leases %s + +[list.arp] +command:/usr/sbin/arp +parameters:-an --libxo json +type:script_output +message:list arp table + +[list.static] +command:/usr/local/sbin/pluginctl -r static_mapping:dhcpd 4 +parameters:%s +type:script_output +message: list dhcp static mappings %s + +[start] +command:/usr/local/sbin/pluginctl -s dhcpd start +parameters: +type:script +message:Starting dhcpd +description:Start DHCPd + +[stop] +command:/usr/local/sbin/pluginctl -s dhcpd stop +parameters: +type:script +message:Stopping dhcpd +description:Stop DHCPd + +[restart] +command:/usr/local/sbin/pluginctl -s dhcpd restart +parameters:%s +type:script +message:Restarting %s dhcpd +description:Restart DHCPd + +[status] +command:/usr/local/sbin/pluginctl -s dhcpd status +parameters: +type:script_output +message:Request DHCPd status + +[remove.lease] +command:/usr/local/opnsense/scripts/dhcp/cleanup_leases4.php +parameters:-d=%s -s +type:script_output +message:remove lease for %s diff --git a/net/isc-dhcp/src/opnsense/service/conf/actions.d/actions_dhcpd6.conf b/net/isc-dhcp/src/opnsense/service/conf/actions.d/actions_dhcpd6.conf new file mode 100644 index 0000000000..6dbcbc49f5 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/service/conf/actions.d/actions_dhcpd6.conf @@ -0,0 +1,50 @@ +[list.leases] +command:/usr/local/opnsense/scripts/dhcp/get_leases6.py +parameters:--inactive %s +type:script_output +message:list dhcpv6 leases %s + +[list.static] +command:/usr/local/sbin/pluginctl -r static_mapping:dhcpd 6 +parameters:%s +type:script_output +message: list dhcpv6 static mappings %s + +[update.prefixes] +command:/usr/local/opnsense/scripts/dhcp/prefixes.php +parameters: +type:script +message:update IPv6 prefixes + +[start] +command:/usr/local/sbin/pluginctl -s dhcpd6 start +parameters: +type:script +message:Starting dhcpd6 +description:Start DHCPd6 + +[stop] +command:/usr/local/sbin/pluginctl -s dhcpd6 stop +parameters: +type:script +message:Stopping dhcpd6 +description:Stop DHCPd6 + +[restart] +command:/usr/local/sbin/pluginctl -s dhcpd6 restart +parameters:%s +type:script +message:Restarting %s dhcpd6 +description:Restart DHCPd6 + +[status] +command:/usr/local/sbin/pluginctl -s dhcpd6 status +parameters: +type:script_output +message:Request DHCPd6 status + +[remove.lease] +command:/usr/local/opnsense/scripts/dhcp/cleanup_leases6.php +parameters:-d=%s -s +type:script_output +message:remove lease for %s diff --git a/net/isc-dhcp/src/opnsense/service/templates/OPNsense/Syslog/local/dhcpd.conf b/net/isc-dhcp/src/opnsense/service/templates/OPNsense/Syslog/local/dhcpd.conf new file mode 100644 index 0000000000..1a6a37b0d8 --- /dev/null +++ b/net/isc-dhcp/src/opnsense/service/templates/OPNsense/Syslog/local/dhcpd.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [dhcpd]. +################################################################### +filter f_local_dhcpd { + facility(local7) or program("dhcpd"); +}; diff --git a/net/isc-dhcp/src/opnsense/service/templates/OPNsense/Syslog/sources/005-dhcpd.conf b/net/isc-dhcp/src/opnsense/service/templates/OPNsense/Syslog/sources/005-dhcpd.conf new file mode 100644 index 0000000000..b66b33993c --- /dev/null +++ b/net/isc-dhcp/src/opnsense/service/templates/OPNsense/Syslog/sources/005-dhcpd.conf @@ -0,0 +1 @@ + unix-dgram("/var/dhcpd/var/run/log" dir_perm(0755) flags(syslog-protocol)); diff --git a/net/isc-dhcp/src/www/services_dhcp.php b/net/isc-dhcp/src/www/services_dhcp.php new file mode 100644 index 0000000000..6b00cc16ef --- /dev/null +++ b/net/isc-dhcp/src/www/services_dhcp.php @@ -0,0 +1,1218 @@ + + * 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. + */ + +require_once("guiconfig.inc"); +require_once("filter.inc"); +require_once("system.inc"); +require_once("interfaces.inc"); +require_once("plugins.inc.d/dhcpd.inc"); + +function validate_partial_mac_list($maclist) +{ + $macs = explode(',', $maclist); + + foreach ($macs as $mac) { + if (!is_macaddr($mac, true)) { + return false; + } + } + + return true; +} + +function reconfigure_dhcpd() +{ + system_resolver_configure(); + dhcpd_dhcp4_configure(); + clear_subsystem_dirty('staticmaps'); +} + +$config_copy_fieldsnames = array('enable', 'staticarp', 'failover_peerip', 'failover_split', 'descr', + 'defaultleasetime', 'maxleasetime', 'gateway', 'domain', 'domainsearchlist', 'denyunknown','ignoreuids', 'ddnsdomain', + 'ddnsdomainprimary', 'ddnsdomainkeyname', 'ddnsdomainkey', 'ddnsdomainalgorithm', 'ddnsupdate', 'mac_allow', + 'mac_deny', 'tftp', 'bootfilename', 'ldap', 'netboot', 'nextserver', 'filename', 'filename32', 'filename64', + 'filename32arm', 'filename64arm', 'filenameipxe', 'rootpath', 'netmask', 'numberoptions', 'interface_mtu', 'wpad', 'omapi', 'omapiport', + 'omapialgorithm', 'omapikey', 'minsecs'); + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // handle identifiers and action + if (!empty($_GET['if']) && !empty($config['interfaces'][$_GET['if']]) && + isset($config['interfaces'][$_GET['if']]['enable']) && + is_ipaddr($config['interfaces'][$_GET['if']]['ipaddr'])) { + $if = $_GET['if']; + if (isset($_GET['pool']) && !empty($config['dhcpd'][$_GET['if']]['pool'][$_GET['pool']])) { + $pool = $_GET['pool']; + } + } else { + /* if no interface is provided this invoke is invalid */ + header(url_safe('Location: /index.php')); + exit; + } + + $a_pools = &config_read_array('dhcpd', $if, 'pool'); + + if (!empty($_GET['act'])) { + $act = $_GET['act']; + } else { + $act = null; + } + + // point to source of data (pool or main dhcp section) + if (isset($pool)) { + $dhcpdconf = &$a_pools[$pool]; + } elseif ($act == "newpool") { + $dhcpdconf = array(); + } else { + $dhcpdconf = &config_read_array('dhcpd', $if); + } + $pconfig = array(); + // simple 1-on-1 copy + foreach ($config_copy_fieldsnames as $fieldname) { + if (isset($dhcpdconf[$fieldname])) { + $pconfig[$fieldname] = $dhcpdconf[$fieldname]; + } else { + $pconfig[$fieldname] = null; + } + } + // handle booleans + $pconfig['enable'] = isset($dhcpdconf['enable']); + $pconfig['wpad'] = isset($dhcpdconf['wpad']); + $pconfig['staticarp'] = isset($dhcpdconf['staticarp']); + $pconfig['denyunknown'] = isset($dhcpdconf['denyunknown']); + $pconfig['ignoreuids'] = isset($dhcpdconf['ignoreuids']); + $pconfig['ddnsupdate'] = isset($dhcpdconf['ddnsupdate']); + $pconfig['netboot'] = isset($dhcpdconf['netboot']); + + // array conversions + $pconfig['numberoptions'] = !empty($pconfig['numberoptions']) ? $pconfig['numberoptions'] : array(); + + // list items + $pconfig['range_from'] = !empty($dhcpdconf['range']['from']) ? $dhcpdconf['range']['from'] : ""; + $pconfig['range_to'] = !empty($dhcpdconf['range']['to']) ? $dhcpdconf['range']['to'] : ""; + $pconfig['wins1'] = !empty($dhcpdconf['winsserver'][0]) ? $dhcpdconf['winsserver'][0] : ""; + $pconfig['wins2'] = !empty($dhcpdconf['winsserver'][1]) ? $dhcpdconf['winsserver'][1] : ""; + $pconfig['dns1'] = !empty($dhcpdconf['dnsserver'][0]) ? $dhcpdconf['dnsserver'][0] : ""; + $pconfig['dns2'] = !empty($dhcpdconf['dnsserver'][1]) ? $dhcpdconf['dnsserver'][1] : ""; + $pconfig['ntp1'] = !empty($dhcpdconf['ntpserver'][0]) ? $dhcpdconf['ntpserver'][0] : ""; + $pconfig['ntp2'] = !empty($dhcpdconf['ntpserver'][1]) ? $dhcpdconf['ntpserver'][1] : ""; +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + // handle identifiers and actions + if (!empty($_POST['if']) && !empty($config['interfaces'][$_POST['if']])) { + $if = $_POST['if']; + if (isset($_POST['pool']) && !empty($config['dhcpd'][$_POST['if']]['pool'][$_POST['pool']])) { + $pool = $_POST['pool']; + } + } + + $a_pools = &config_read_array('dhcpd', $if, 'pool'); + + if (!empty($_POST['act'])) { + $act = $_POST['act']; + } else { + $act = null; + } + $pconfig = $_POST; + $input_errors = array(); + + if (isset($_POST['submit'])) { + // transform Additional BOOTP/DHCP Options + $pconfig['numberoptions'] = array(); + if (isset($pconfig['numberoptions_number'])) { + $pconfig['numberoptions']['item'] = array(); + foreach ($pconfig['numberoptions_number'] as $opt_seq => $opt_number) { + if (!empty($opt_number)) { + $pconfig['numberoptions']['item'][] = array('number' => $opt_number, + 'type' => $pconfig['numberoptions_type'][$opt_seq], + 'value' => $pconfig['numberoptions_value'][$opt_seq] + ); + } + } + } + + /* input validation */ + $reqdfields = explode(" ", "range_from range_to"); + $reqdfieldsn = array(gettext("Range begin"),gettext("Range end")); + + do_input_validation($pconfig, $reqdfields, $reqdfieldsn, $input_errors); + + if (!is_ipaddrv4($pconfig['range_from'])) { + $input_errors[] = gettext("A valid range must be specified."); + } + if (!is_ipaddrv4($pconfig['range_to'])) { + $input_errors[] = gettext("A valid range must be specified."); + } + if (!empty($pconfig['gateway']) && $pconfig['gateway'] != "none" && !is_ipaddrv4($pconfig['gateway'])) { + $input_errors[] = gettext("A valid IP address must be specified for the gateway."); + } + if ((!empty($pconfig['wins1']) && !is_ipaddrv4($pconfig['wins1'])) || (!empty($pconfig['wins2']) && !is_ipaddrv4($pconfig['wins2']))) { + $input_errors[] = gettext("A valid IP address must be specified for the primary/secondary WINS servers."); + } + list (, $parent_net) = interfaces_primary_address($pconfig['if']); + if (is_subnetv4($parent_net) && $pconfig['gateway'] && $pconfig['gateway'] != "none") { + if (!ip_in_subnet($pconfig['gateway'], $parent_net) && !ip_in_interface_alias_subnet($pconfig['if'], $pconfig['gateway'])) { + $input_errors[] = sprintf(gettext("The gateway address %s does not lie within the chosen interface's subnet."), $pconfig['gateway']); + } + } + if ((!empty($pconfig['dns1']) && !is_ipaddrv4($pconfig['dns1'])) || (!empty($pconfig['dns2']) && !is_ipaddrv4($pconfig['dns2']))) { + $input_errors[] = gettext("A valid IP address must be specified for the primary/secondary DNS servers."); + } + + if (!empty($pconfig['defaultleasetime']) && (!is_numeric($pconfig['defaultleasetime']) || ($pconfig['defaultleasetime'] < 60))) { + $input_errors[] = gettext("The default lease time must be at least 60 seconds."); + } + + if (!empty($pconfig['maxleasetime']) && (!is_numeric($pconfig['maxleasetime']) || ($pconfig['maxleasetime'] < 60) || ($pconfig['maxleasetime'] <= $pconfig['defaultleasetime']))) { + $input_errors[] = gettext("The maximum lease time must be at least 60 seconds and higher than the default lease time."); + } + + if (!empty($pconfig['minsecs']) && (!is_numeric($pconfig['minsecs']) || ($pconfig['minsecs'] < 0) || ($pconfig['minsecs'] > 255))) { + $input_errors[] = gettext("The response delay must be higher than 0 and no more than 255 seconds."); + } + + if ((!empty($pconfig['ddnsdomain']) && !is_domain($pconfig['ddnsdomain']))) { + $input_errors[] = gettext("A valid domain name must be specified for the dynamic DNS registration."); + } + if ((!empty($pconfig['ddnsdomainprimary']) && !is_ipaddrv4($pconfig['ddnsdomainprimary']))) { + $input_errors[] = gettext("A valid primary domain name server IP address must be specified for the dynamic domain name."); + } + if (!empty($pconfig['ddnsdomainkey']) && base64_encode(base64_decode($pconfig['ddnsdomainkey'], true)) !== $pconfig['ddnsdomainkey']) { + $input_errors[] = gettext('You must specify a Base64-encoded domain key.'); + } + if ((!empty($pconfig['ddnsdomainkey']) && empty($pconfig['ddnsdomainkeyname'])) || + (!empty($pconfig['ddnsdomainkeyname']) && empty($pconfig['ddnsdomainkey'])) + ) { + $input_errors[] = gettext("You must specify both a valid domain key and key name."); + } + if (!empty($pconfig['domainsearchlist'])) { + $domain_array=preg_split("/[ ;]+/",$pconfig['domainsearchlist']); + foreach ($domain_array as $curdomain) { + if (!is_domain($curdomain, true)) { + $input_errors[] = gettext("A valid domain search list must be specified."); + break; + } + } + } + + // Validate MACs + if (!empty($pconfig['mac_allow']) && !validate_partial_mac_list($pconfig['mac_allow'])) { + $input_errors[] = gettext("If you specify a mac allow list, it must contain only valid partial MAC addresses."); + } + if (!empty($pconfig['mac_deny']) && !validate_partial_mac_list($pconfig['mac_deny'])) { + $input_errors[] = gettext("If you specify a mac deny list, it must contain only valid partial MAC addresses."); + } + + if ((!empty($pconfig['ntp1']) && !is_ipaddrv4($pconfig['ntp1'])) || (!empty($pconfig['ntp2']) && !is_ipaddrv4($pconfig['ntp2']))) { + $input_errors[] = gettext("A valid IP address must be specified for the primary/secondary NTP servers."); + } + if (!empty($pconfig['domain']) && !is_domain($pconfig['domain'], true)) { + $input_errors[] = gettext("A valid domain name must be specified for the DNS domain."); + } + if (!empty($pconfig['tftp']) && !is_ipaddrv4($pconfig['tftp']) && !is_domain($pconfig['tftp']) && !is_URL($pconfig['tftp'])) { + $input_errors[] = gettext("A valid IP address or hostname must be specified for the TFTP server."); + } + if (!empty($pconfig['nextserver']) && !is_ipaddrv4($pconfig['nextserver'])) { + $input_errors[] = gettext("A valid IP address must be specified for the network boot server."); + } + + if (gen_subnet($config['interfaces'][$if]['ipaddr'], $config['interfaces'][$if]['subnet']) == $pconfig['range_from']) { + $input_errors[] = gettext("You cannot use the network address in the starting subnet range."); + } + if (gen_subnet_max($config['interfaces'][$if]['ipaddr'], $config['interfaces'][$if]['subnet']) == $pconfig['range_to']) { + $input_errors[] = gettext("You cannot use the broadcast address in the ending subnet range."); + } + + if ($pconfig['omapi'] && (empty($pconfig['omapiport']) || !is_numeric($pconfig['omapiport']) || $pconfig['omapiport'] < 1 || $pconfig['omapiport'] > 65535)) { + $input_errors[] = gettext("A valid port number must be specified for the OMAPI port."); + } + + // Disallow a range that includes the virtualip + if (isset($config['virtualip']['vip'])) { + foreach($config['virtualip']['vip'] as $vip) { + if ($vip['interface'] == $if) { + if ($vip['subnet'] && is_inrange_v4($vip['subnet'], $pconfig['range_from'], $pconfig['range_to'])) { + $input_errors[] = sprintf(gettext("The subnet range cannot overlap with virtual IP address %s."),$vip['subnet']); + } + } + } + } + + $a_maps = &config_read_array('dhcpd', $if, 'staticmap'); + $noip = false; + foreach ($a_maps as $map) { + if (empty($map['ipaddr'])) { + $noip = true; + } + } + if (!empty($pconfig['staticarp']) && $noip) { + $input_errors[] = gettext("Cannot enable static ARP when you have static map entries without IP addresses. Ensure all static maps have IP addresses and try again."); + } + if (!empty($pconfig['interface_mtu']) && ( + (string)((int)$pconfig['interface_mtu']) != $pconfig['interface_mtu'] || $pconfig['interface_mtu'] < 68 || $pconfig['interface_mtu'] > 65535) + ) { + $input_errors[] = gettext("A valid MTU value must be specified."); + } + if ($pconfig['failover_split'] != "" && ( + (string)((int)$pconfig['failover_split']) != $pconfig['failover_split'] || $pconfig['failover_split'] < 0 || $pconfig['failover_split'] > 256) + ) { + $input_errors[] = gettext("Failover split must be a number between 0 and 256."); + } + + if (is_array($pconfig['numberoptions']['item'])) { + foreach ($pconfig['numberoptions']['item'] as $numberoption) { + if ($numberoption['type'] == 'text' && strstr($numberoption['value'], '"')) { + $input_errors[] = gettext("Text type cannot include quotation marks."); + } elseif ($numberoption['type'] == 'string' && !preg_match('/^"[^"]*"$/', $numberoption['value']) && !preg_match('/^[0-9a-f]{2}(?:\:[0-9a-f]{2})*$/i', $numberoption['value'])) { + $input_errors[] = gettext("String type must be enclosed in quotes like \"this\" or must be a series of octets specified in hexadecimal, separated by colons, like 01:23:45:67:89:ab:cd:ef"); + } elseif ($numberoption['type'] == 'boolean' && $numberoption['value'] != 'true' && $numberoption['value'] != 'false' && $numberoption['value'] != 'on' && $numberoption['value'] != 'off') { + $input_errors[] = gettext("Boolean type must be true, false, on, or off."); + } elseif ($numberoption['type'] == 'unsigned integer 8' && (!is_numeric($numberoption['value']) || $numberoption['value'] < 0 || $numberoption['value'] > 255)) { + $input_errors[] = gettext("Unsigned 8-bit integer type must be a number in the range 0 to 255."); + } elseif ($numberoption['type'] == 'unsigned integer 16' && (!is_numeric($numberoption['value']) || $numberoption['value'] < 0 || $numberoption['value'] > 65535)) { + $input_errors[] = gettext("Unsigned 16-bit integer type must be a number in the range 0 to 65535."); + } elseif ($numberoption['type'] == 'unsigned integer 32' && (!is_numeric($numberoption['value']) || $numberoption['value'] < 0 || $numberoption['value'] > 4294967295) ) { + $input_errors[] = gettext("Unsigned 32-bit integer type must be a number in the range 0 to 4294967295."); + } elseif ($numberoption['type'] == 'signed integer 8' && (!is_numeric($numberoption['value']) || $numberoption['value'] < -128 || $numberoption['value'] > 127)) { + $input_errors[] = gettext("Signed 8-bit integer type must be a number in the range -128 to 127."); + } elseif ($numberoption['type'] == 'signed integer 16' && (!is_numeric($numberoption['value']) || $numberoption['value'] < -32768 || $numberoption['value'] > 32767)) { + $input_errors[] = gettext("Signed 16-bit integer type must be a number in the range -32768 to 32767."); + } elseif ($numberoption['type'] == 'signed integer 32' && (!is_numeric($numberoption['value']) || $numberoption['value'] < -2147483648 || $numberoption['value'] > 2147483647)) { + $input_errors[] = gettext("Signed 32-bit integer type must be a number in the range -2147483648 to 2147483647."); + } elseif ($numberoption['type'] == 'ip-address' && !is_ipaddrv4($numberoption['value']) && !is_hostname($numberoption['value'])) { + $input_errors[] = gettext("IP address or host type must be an IP address or host name."); + } + } + } + + if (count($input_errors) == 0 && isset($pconfig['enable'])) { + /* make sure the range lies within the current subnet */ + $subnet_start = ip2ulong(long2ip32(ip2long($config['interfaces'][$if]['ipaddr']) & gen_subnet_mask_long($config['interfaces'][$if]['subnet']))); + $subnet_end = ip2ulong(long2ip32(ip2long($config['interfaces'][$if]['ipaddr']) | (~gen_subnet_mask_long($config['interfaces'][$if]['subnet'])))); + + if ((ip2ulong($pconfig['range_from']) < $subnet_start) || (ip2ulong($pconfig['range_from']) > $subnet_end) || + (ip2ulong($pconfig['range_to']) < $subnet_start) || (ip2ulong($pconfig['range_to']) > $subnet_end)) { + $input_errors[] = gettext("The specified range lies outside of the current subnet."); + } + + if (ip2ulong($pconfig['range_from']) > ip2ulong($pconfig['range_to'])) { + $input_errors[] = gettext("The range is invalid (first element higher than second element)."); + } + + if (isset($pool) || ($act == "newpool")) { + $rfrom = $config['dhcpd'][$if]['range']['from']; + $rto = $config['dhcpd'][$if]['range']['to']; + if (is_inrange_v4($pconfig['range_from'], $rfrom, $rto) || is_inrange_v4($pconfig['range_to'], $rfrom, $rto)) { + $input_errors[] = gettext("The specified range must not be within the DHCP range for this interface."); + } + } + + foreach ($a_pools as $id => $p) { + if (isset($pool) && ($id == $pool)) { + continue; + } + if (is_inrange_v4($pconfig['range_from'], $p['range']['from'], $p['range']['to']) || + is_inrange_v4($pconfig['range_to'], $p['range']['from'], $p['range']['to'])) { + $input_errors[] = gettext("The specified range must not be within the range configured on a DHCP pool for this interface."); + break; + } + } + } + + // save data + if (count($input_errors) == 0) { + $dhcpdconf = array(); + // simple 1-on-1 copy + foreach ($config_copy_fieldsnames as $fieldname) { + if (!empty($pconfig[$fieldname]) || $pconfig[$fieldname] === "0") { + $dhcpdconf[$fieldname] = $pconfig[$fieldname]; + } + } + // handle booleans + $dhcpdconf['enable'] = !empty($dhcpdconf['enable']); + $dhcpdconf['staticarp'] = !empty($dhcpdconf['staticarp']); + $dhcpdconf['denyunknown'] = !empty($dhcpdconf['denyunknown']); + $dhcpdconf['ignoreuids'] = !empty($dhcpdconf['ignoreuids']); + $dhcpdconf['ddnsupdate'] = !empty($dhcpdconf['ddnsupdate']); + $dhcpdconf['netboot'] = !empty($dhcpdconf['netboot']); + + // supply range + $dhcpdconf['range'] = array(); + $dhcpdconf['range']['from'] = $pconfig['range_from']; + $dhcpdconf['range']['to'] = $pconfig['range_to']; + + // array types + $dhcpdconf['winsserver'] = []; + if (!empty($pconfig['wins1'])) { + $dhcpdconf['winsserver'][] = $pconfig['wins1']; + } + if (!empty($pconfig['wins2'])) { + $dhcpdconf['winsserver'][] = $pconfig['wins2']; + } + $dhcpdconf['dnsserver'] = []; + if (!empty($pconfig['dns1'])) { + $dhcpdconf['dnsserver'][] = $pconfig['dns1']; + } + if (!empty($pconfig['dns2'])) { + $dhcpdconf['dnsserver'][] = $pconfig['dns2']; + } + $dhcpdconf['ntpserver'] = []; + if (!empty($pconfig['ntp1'])) { + $dhcpdconf['ntpserver'][] = $pconfig['ntp1']; + } + if (!empty($pconfig['ntp2'])) { + $dhcpdconf['ntpserver'][] = $pconfig['ntp2']; + } + + // handle changes + if (!isset($pool) && $act != "newpool") { + if (isset($config['dhcpd'][$if]['enable']) != !empty($pconfig['enable'])) { + // DHCP has been enabled or disabled. + // The pf ruleset will need to be rebuilt to allow or disallow DHCP. + $exec_filter_configure = true; + } + $previous = !empty($config['dhcpd'][$if]['failover_peerip']) ? $config['dhcpd'][$if]['failover_peerip'] : ""; + if ($previous != $pconfig['failover_peerip']) { + mwexecf('/bin/rm -rf /var/dhcpd/var/db/*'); + } + } + // save changes to config + if (isset($pool)) { + $a_pools[$pool] = $dhcpdconf; + } elseif ($act == "newpool") { + $a_pools[] = $dhcpdconf; + } else { + // copy structures back in + foreach (array('pool', 'staticmap') as $fieldname) { + if (!empty($config['dhcpd'][$if][$fieldname])) { + $dhcpdconf[$fieldname] = $config['dhcpd'][$if][$fieldname]; + } + } + $config['dhcpd'][$if] = $dhcpdconf; + } + write_config(); + if (isset($exec_filter_configure)) { + filter_configure(); + } + reconfigure_dhcpd(); + header(url_safe('Location: /services_dhcp.php?if=%s', array($if))); + exit; + } + } elseif (isset($_POST['apply'])) { + // apply changes + reconfigure_dhcpd(); + header(url_safe('Location: /services_dhcp.php?if=%s', array($if))); + exit; + } elseif ($act == "del") { + if (!empty($config['dhcpd'][$if]['staticmap'][$_POST['id']])) { + if (isset($config['dhcpd'][$if]['staticmap'][$_POST['id']]['ipaddr'])) { + configdp_run('interface remove arp', [ + $config['dhcpd'][$if]['staticmap'][$_POST['id']]['ipaddr'] + ]); + } + unset($config['dhcpd'][$if]['staticmap'][$_POST['id']]); + write_config(); + if (isset($config['dhcpd'][$if]['enable'])) { + mark_subsystem_dirty('staticmaps'); + } + } + header(url_safe('Location: /services_dhcp.php?if=%s', array($if))); + exit; + } elseif ($act == "delpool") { + if (!empty($a_pools[$_POST['id']])) { + unset($a_pools[$_POST['id']]); + write_config(); + } + header(url_safe('Location: /services_dhcp.php?if=%s', array($if))); + } elseif ($act == "export") { + $static = dhcpd_staticmap(4, true, null, true, true); + + header("Content-Type: text/csv"); + header("Content-Disposition: attachment; filename=\"isc_export_{$if}.csv\""); + header("Content-Transfer-Encoding: binary"); + header("Pragma: no-cache"); + header("Expires: 0"); + + $stream = fopen('php://output', 'w'); + + fputcsv($stream, ['ip_address', 'hw_address', 'hostname', 'description']); + foreach ($static as $record) { + if ($record['interface'] !== $if) { + continue; + } + + fputcsv($stream, array_map(fn($k) => $record[$k], ['ipaddr', 'mac', 'hostname', 'descr'])); + } + + @fclose($stream); + + exit; + } +} + +$range_from = ip2long(long2ip32(ip2long($config['interfaces'][$if]['ipaddr']) & gen_subnet_mask_long($config['interfaces'][$if]['subnet']))) + 1; +$range_to = ip2long(long2ip32(ip2long($config['interfaces'][$if]['ipaddr']) | (~gen_subnet_mask_long($config['interfaces'][$if]['subnet'])))) - 1; + +$service_hook = 'dhcpd'; +legacy_html_escape_form_data($pconfig); +include("head.inc"); +?> + + + + + + + + +
    +
    +
    + 0) print_input_errors($input_errors); ?> + +
    + " . gettext("You must apply the changes in order for them to take effect."));?>
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + /> + +
    + +
    + /> + +
    + /> + +
    + +
    + +
    + + + + - + + +
    In-use DHCP Pool Ranges: +
    - + +
    - + + +
    + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + +
    + +
    + + +
    + +
    +
    + +
    +
    + + +
    + + +
    + + +
    + + +
    + + +
    () + + +
    () + + +
    + + +
    + + +
    + + +
    + />  + + +
    +
    + - +
    + +
    +
    + - +
    + +
    +
    + - +
    + +
    +
    + - +
    + +
    +
    + - +
    + +
    +
    + - +
    + +
    +
    + - +
    + +
    +
    + " /> - +
    + +
    +
    + - +
    + +
      + + + + + + + + +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + +
    +
    + +
    + + + + + + + + + + + + + + +
    +
    +
    +
    + +
    +
    +
    + diff --git a/net/isc-dhcp/src/www/services_dhcp_edit.php b/net/isc-dhcp/src/www/services_dhcp_edit.php new file mode 100644 index 0000000000..ceb180a706 --- /dev/null +++ b/net/isc-dhcp/src/www/services_dhcp_edit.php @@ -0,0 +1,509 @@ + + * 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. + */ + +require_once("guiconfig.inc"); +require_once("interfaces.inc"); + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // handle identifiers and action + if (!empty($_GET['if']) && !empty($config['interfaces'][$_GET['if']])) { + $if = $_GET['if']; + } else { + header(url_safe('Location: /services_dhcp.php')); + exit; + } + if (isset($if) && isset($_GET['id']) && !empty($config['dhcpd'][$if]['staticmap'][$_GET['id']])) { + $id = $_GET['id']; + } + + // read form data + $pconfig = array(); + $config_copy_fieldnames = array('mac', 'cid', 'hostname', 'filename', 'rootpath', 'descr', 'arp_table_static_entry', + 'defaultleasetime', 'maxleasetime', 'gateway', 'domain', 'domainsearchlist', 'winsserver', 'dnsserver', 'ddnsdomain', + 'ntpserver', 'tftp', 'bootfilename', 'ipaddr', 'winsserver', 'dnsserver'); + foreach ($config_copy_fieldnames as $fieldname) { + if (isset($if) && isset($id) && isset($config['dhcpd'][$if]['staticmap'][$id][$fieldname])) { + $pconfig[$fieldname] = $config['dhcpd'][$if]['staticmap'][$id][$fieldname]; + } elseif (isset($_GET[$fieldname])) { + $pconfig[$fieldname] = $_GET[$fieldname]; + } else { + $pconfig[$fieldname] = null; + } + } + + // handle array types + if (isset($pconfig['winsserver'][0])) { + $pconfig['wins1'] = $pconfig['winsserver'][0]; + } + if (isset($pconfig['winsserver'][1])) { + $pconfig['wins2'] = $pconfig['winsserver'][1]; + } + if (isset($pconfig['dnsserver'][0])) { + $pconfig['dns1'] = $pconfig['dnsserver'][0]; + } + if (isset($pconfig['dnsserver'][1])) { + $pconfig['dns2'] = $pconfig['dnsserver'][1]; + } + if (isset($pconfig['ntpserver'][0])) { + $pconfig['ntp1'] = $pconfig['ntpserver'][0]; + } + if (isset($pconfig['ntpserver'][1])) { + $pconfig['ntp2'] = $pconfig['ntpserver'][1]; + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + $pconfig = $_POST; + + // handle identifiers and actions + if (!empty($pconfig['if']) && !empty($config['interfaces'][$pconfig['if']])) { + $if = $pconfig['if']; + } + if (!empty($config['dhcpd'][$if]['staticmap'][$pconfig['id']])) { + $id = $pconfig['id']; + } + + $a_maps = &config_read_array('dhcpd', $if, 'staticmap'); + $input_errors = array(); + + /* input validation */ + $reqdfields = array(); + $reqdfieldsn = array(); + do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); + + /* either MAC or Client-ID must be specified */ + if (empty($pconfig['mac']) && empty($pconfig['cid'])) { + $input_errors[] = gettext("Either MAC address or Client identifier must be specified"); + } + + /* normalize MAC addresses - lowercase and convert Windows-ized hyphenated MACs to colon delimited */ + $pconfig['mac'] = strtolower(str_replace("-", ":", $pconfig['mac'])); + + if (!empty($pconfig['hostname'])) { + preg_match("/\-\$/", $pconfig['hostname'], $matches); + if ($matches) { + $input_errors[] = gettext("The hostname cannot end with a hyphen according to RFC952"); + } + if (!is_hostname($pconfig['hostname'])) { + $input_errors[] = gettext("The hostname can only contain the characters A-Z, 0-9 and '-'."); + } elseif (strpos($pconfig['hostname'],'.')) { + $input_errors[] = gettext("A valid hostname is specified, but the domain name part should be omitted"); + } + } + + if (!empty($pconfig['domain'])) { + if (strpos($pconfig['domain'], '.') === 0) { + $input_errors[] = gettext("The domain name cannot start with a dot."); + } elseif (!is_domain($pconfig['domain'])) { + $input_errors[] = gettext("A valid domain name must be specified."); + } + } + + if (!empty($pconfig['ipaddr']) && !is_ipaddr($_POST['ipaddr'])) { + $input_errors[] = gettext("A valid IP address must be specified."); + } + if (!empty($pconfig['mac']) && !is_macaddr($pconfig['mac'])) { + $input_errors[] = gettext("A valid MAC address must be specified."); + } + if (isset($config['dhcpd'][$if]['staticarp']) && empty($pconfig['ipaddr'])) { + $input_errors[] = gettext("Static ARP is enabled. You must specify an IP address."); + } + + /* check for overlaps */ + foreach ($a_maps as $mapent) { + if (isset($id) && ($a_maps[$id] === $mapent)) { + continue; + } + if (!empty($mapent['mac']) && $pconfig['mac'] == $mapent['mac']) { + $input_errors[] = gettext('This MAC address already exists.'); + break; + } + if (!empty($mapent['cid']) && $pconfig['cid'] == $mapent['cid']) { + $input_errors[] = gettext('This client identifier already exists.'); + break; + } + } + + list (, $parent_net) = interfaces_primary_address($if); + + if (!empty($pconfig['ipaddr'])) { + if (!ip_in_subnet($pconfig['ipaddr'], $parent_net)) { + $ifcfgdescr = convert_friendly_interface_to_friendly_descr($if); + $input_errors[] = sprintf(gettext('The IP address must lie in the %s subnet.'), $ifcfgdescr); + } + } + + if (!empty($pconfig['gateway']) && $pconfig['gateway'] != "none" && !is_ipaddrv4($pconfig['gateway'])) { + $input_errors[] = gettext("A valid IP address must be specified for the gateway."); + } + + if ((!empty($pconfig['wins1']) && !is_ipaddrv4($pconfig['wins1'])) || + (!empty($pconfig['wins2']) && !is_ipaddrv4($pconfig['wins2']))) { + $input_errors[] = gettext("A valid IP address must be specified for the primary/secondary WINS servers."); + } + + if (is_subnetv4($parent_net) && $pconfig['gateway'] != "none" && !empty($pconfig['gateway'])) { + if (!ip_in_subnet($pconfig['gateway'], $parent_net) && !ip_in_interface_alias_subnet($if, $pconfig['gateway'])) { + $input_errors[] = sprintf(gettext("The gateway address %s does not lie within the chosen interface's subnet."), $_POST['gateway']); + } + } + + if ((!empty($pconfig['dns1']) && !is_ipaddrv4($pconfig['dns1'])) || (!empty($pconfig['dns2']) && !is_ipaddrv4($pconfig['dns2']))) { + $input_errors[] = gettext("A valid IP address must be specified for the primary/secondary DNS servers."); + } + + if (!empty($pconfig['defaultleasetime']) && (!is_numeric($pconfig['defaultleasetime']) || ($pconfig['defaultleasetime'] < 60))) { + $input_errors[] = gettext("The default lease time must be at least 60 seconds."); + } + if (!empty($pconfig['maxleasetime']) && (!is_numeric($pconfig['maxleasetime']) || ($pconfig['maxleasetime'] < 60) || ($pconfig['maxleasetime'] <= $pconfig['defaultleasetime']))) { + $input_errors[] = gettext("The maximum lease time must be at least 60 seconds and higher than the default lease time."); + } + if (!empty($pconfig['ddnsdomain']) && !is_domain($pconfig['ddnsdomain'])) { + $input_errors[] = gettext("A valid domain name must be specified for the dynamic DNS registration."); + } + if (!empty($pconfig['domainsearchlist'])) { + $domain_array=preg_split("/[ ;]+/", $pconfig['domainsearchlist']); + foreach ($domain_array as $curdomain) { + if (!is_domain($curdomain, true)) { + $input_errors[] = gettext("A valid domain search list must be specified."); + break; + } + } + } + + if ((!empty($pconfig['ntp1']) && !is_ipaddrv4($pconfig['ntp1'])) || (!empty($pconfig['ntp2']) && !is_ipaddrv4($pconfig['ntp2']))) { + $input_errors[] = gettext("A valid IP address must be specified for the primary/secondary NTP servers."); + } + if (!empty($pconfig['tftp']) && !is_ipaddrv4($pconfig['tftp']) && !is_domain($pconfig['tftp']) && !is_URL($pconfig['tftp'])) { + $input_errors[] = gettext("A valid IP address or hostname must be specified for the TFTP server."); + } + if ((!empty($pconfig['nextserver']) && !is_ipaddrv4($pconfig['nextserver']))) { + $input_errors[] = gettext("A valid IP address must be specified for the network boot server."); + } + + if (count($input_errors) == 0){ + $mapent = array(); + $config_copy_fieldnames = array('mac', 'cid', 'ipaddr', 'hostname', 'descr', 'filename', 'rootpath', + 'arp_table_static_entry', 'defaultleasetime', 'maxleasetime', 'gateway', 'domain', 'domainsearchlist', + 'ddnsdomain', 'tftp', 'bootfilename', 'winsserver', 'dnsserver'); + + foreach ($config_copy_fieldnames as $fieldname) { + if (!empty($pconfig[$fieldname])) { + $mapent[$fieldname] = $pconfig[$fieldname]; + } + } + + // boolean + $mapent['arp_table_static_entry'] = !empty($pconfig['arp_table_static_entry']); + + // arrays + $mapent['winsserver'] = array(); + if (!empty($pconfig['wins1'])) { + $mapent['winsserver'][] = $pconfig['wins1']; + } + if (!empty($pconfig['wins2'])) { + $mapent['winsserver'][] = $pconfig['wins2']; + } + + $mapent['dnsserver'] = array(); + if (!empty($pconfig['dns1'])) { + $mapent['dnsserver'][] = $_POST['dns1']; + } + if (!empty($pconfig['dns2'])) { + $mapent['dnsserver'][] = $_POST['dns2']; + } + + $mapent['ntpserver'] = array(); + if (!empty($pconfig['ntp1'])) { + $mapent['ntpserver'][] = $pconfig['ntp1']; + } + if (!empty($pconfig['ntp2'])) { + $mapent['ntpserver'][] = $pconfig['ntp2']; + } + + if (isset($id)) { + $a_maps[$id] = $mapent; + } else { + $a_maps[] = $mapent; + } + + usort($config['dhcpd'][$if]['staticmap'], function ($a, $b) { + return ipcmp($a['ipaddr'], $b['ipaddr']); + }); + + write_config(); + + if (isset($config['dhcpd'][$if]['enable'])) { + mark_subsystem_dirty('staticmaps'); + } + + header(url_safe('Location: /services_dhcp.php?if=%s', array($if))); + exit; + } +} + +$service_hook = 'dhcpd'; +legacy_html_escape_form_data($pconfig); + +include("head.inc"); + +?> + + + +
    +
    +
    + 0) print_input_errors($input_errors); ?> +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + + + +
    + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + /> + +
    +
    + +
    +
    + + +
    + + +
    + + +
    + + +
    () + + +
    () + + +
    + + +
    +
    + - +
    + +
    +
    + - +
    + +
    + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + diff --git a/net/isc-dhcp/src/www/services_dhcpv6.php b/net/isc-dhcp/src/www/services_dhcpv6.php new file mode 100644 index 0000000000..8f1f17e6f6 --- /dev/null +++ b/net/isc-dhcp/src/www/services_dhcpv6.php @@ -0,0 +1,937 @@ + + * Copyright (C) 2010 Seth Mos + * 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. + */ + +require_once("guiconfig.inc"); +require_once("filter.inc"); +require_once("system.inc"); +require_once("interfaces.inc"); +require_once("plugins.inc.d/dhcpd.inc"); + +function show_track6_form($if) +{ + global $config; + $service_hook = 'dhcpd6'; + include("head.inc"); + include("fbegin.inc"); + $enable_label = gettext("Enable"); + $range_label = gettext("Range"); + $enable_descr = sprintf(gettext("Enable DHCPv6 server on " . "%s " ."interface"),!empty($config['interfaces'][$if]['descr']) ? htmlspecialchars($config['interfaces'][$if]['descr']) : strtoupper($if)); + $enable_input = 'checked="checked"'; + $save_btn_text = html_safe(gettext('Save')); + /* calculated "fixed" range */ + list ($ifcfgipv6) = interfaces_primary_address6($if, legacy_interfaces_details()); + $range = ['from' => '?', 'to' => '?']; + if (is_ipaddrv6($ifcfgipv6)) { + $ifcfgipv6 = Net_IPv6::getNetmask($ifcfgipv6, 64); + $ifcfgipv6arr = explode(':', $ifcfgipv6); + $ifcfgipv6arr[7] = '1000'; + $range['from'] = Net_IPv6::compress(implode(':', $ifcfgipv6arr)); + $ifcfgipv6arr[7] = '2000'; + $range['to'] = Net_IPv6::compress(implode(':', $ifcfgipv6arr)); + } + + if (!empty($config['dhcpdv6']) && !empty($config['dhcpdv6'][$if]) && isset($config['dhcpdv6'][$if]['enable']) && $config['dhcpdv6'][$if]['enable'] == '-1') { + /* disabled */ + $enable_input = ''; + } + + $range_tr = << + $range_label + {$range['from']} - {$range['to']} + + EOD; + + + echo << +
    +
    +
    +
    +
    +
    + + + + + + + + + $range_tr + + + + + +
    +
    +
    $enable_label + + $enable_descr +
      + + +
    +
    +
    +
    +
    +
    +
    + + EOD; + include("foot.inc"); +} + +function process_track6_form($if) +{ + $dhcpdv6cfg = &config_read_array('dhcpdv6'); + $this_server = []; + if (empty($_POST['enable'])) { + $this_server['enable'] = '-1'; + } + $dhcpdv6cfg[$if] = $this_server; + write_config(); + reconfigure_dhcpd(); + filter_configure(); + header(url_safe('Location: /services_dhcpv6.php?if=%s', array($if))); +} + +function reconfigure_dhcpd() +{ + system_resolver_configure(); + dhcpd_dhcp6_configure(); + clear_subsystem_dirty('staticmapsv6'); +} + +$if = null; +$act = null; + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // handle identifiers and action + if (!empty($_GET['if']) && !empty($config['interfaces'][$_GET['if']]) && + isset($config['interfaces'][$_GET['if']]['enable']) && + (is_ipaddr($config['interfaces'][$_GET['if']]['ipaddrv6']) || + in_array($config['interfaces'][$_GET['if']]['ipaddrv6'], ['idassoc6', 'track6']))) { + $if = $_GET['if']; + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + // handle identifiers and actions + if (!empty($_POST['if']) && !empty($config['interfaces'][$_POST['if']])) { + $if = $_POST['if']; + } + if (!empty($_POST['act'])) { + $act = $_POST['act']; + } +} + +if ($if === null) { + /* if no interface is provided this invoke is invalid */ + header(url_safe('Location: /index.php')); + exit; +} elseif (($config['interfaces'][$if]['ipaddrv6'] ?? 'none') == 'track6' && !isset($config['interfaces'][$if]['dhcpd6track6allowoverride'])) { + /* + * In case of automatic tracking, show different form and only handle on/off options. + * This code injection is intended to keep changes as minimal as possible and avoid + * regressions on existing isc-dhcp installs, while showing current state for tracking + * interfaces. + */ + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + show_track6_form($if); + } elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + process_track6_form($if); + } else { + header(url_safe('Location: /index.php')); + } + exit; +} + +/* default form processing (for manual track6 and idassoc6 but handled via 'track6-interface') */ + +$ifcfgip = $config['interfaces'][$if]['ipaddrv6']; +$ifcfgsn = $config['interfaces'][$if]['subnetv6']; + +if (isset($config['interfaces'][$if]['track6-interface'])) { + list ($ifcfgip,, $ifcfgsn) = interfaces_primary_address6($if); + $pdlen = calculate_ipv6_delegation_length($config['interfaces'][$if]['track6-interface']) - 1; +} + +$subnet_start = gen_subnetv6($ifcfgip, $ifcfgsn); +$subnet_end = gen_subnetv6_max($ifcfgip, $ifcfgsn); + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + $pconfig = array(); + + if (!empty($config['dhcpdv6'][$if]['range'])) { + $pconfig['range_from'] = $config['dhcpdv6'][$if]['range']['from']; + $pconfig['range_to'] = $config['dhcpdv6'][$if]['range']['to']; + } + if (!empty($config['dhcpdv6'][$if]['prefixrange'])) { + $pconfig['prefixrange_from'] = $config['dhcpdv6'][$if]['prefixrange']['from']; + $pconfig['prefixrange_to'] = $config['dhcpdv6'][$if]['prefixrange']['to']; + $pconfig['prefixrange_length'] = $config['dhcpdv6'][$if]['prefixrange']['prefixlength']; + } + $config_copy_fieldsnames = array('defaultleasetime', 'maxleasetime', 'domain', 'domainsearchlist', 'ddnsdomain', + 'ddnsdomainprimary', 'ddnsdomainkeyname', 'ddnsdomainkey', 'ddnsdomainalgorithm', 'bootfile_url', 'netmask', + 'numberoptions', 'dhcpv6leaseinlocaltime', 'staticmap', 'minsecs'); + foreach ($config_copy_fieldsnames as $fieldname) { + if (isset($config['dhcpdv6'][$if][$fieldname])) { + $pconfig[$fieldname] = $config['dhcpdv6'][$if][$fieldname]; + } else { + $pconfig[$fieldname] = null; + } + } + // handle booleans + $pconfig['enable'] = isset($config['dhcpdv6'][$if]['enable']); + $pconfig['ddnsupdate'] = isset($config['dhcpdv6'][$if]['ddnsupdate']); + $pconfig['netboot'] = isset($config['dhcpdv6'][$if]['netboot']); + + // handle arrays + $pconfig['staticmap'] = empty($pconfig['staticmap']) ? array() : $pconfig['staticmap']; + $pconfig['numberoptions'] = empty($pconfig['numberoptions']) ? array() : $pconfig['numberoptions']; + $pconfig['dns1'] = !empty($config['dhcpdv6'][$if]['dnsserver'][0]) ? $config['dhcpdv6'][$if]['dnsserver'][0] : ""; + $pconfig['dns2'] = !empty($config['dhcpdv6'][$if]['dnsserver'][1]) ? $config['dhcpdv6'][$if]['dnsserver'][1] : ""; + $pconfig['ntp1'] = !empty($config['dhcpdv6'][$if]['ntpserver'][0]) ? $config['dhcpdv6'][$if]['ntpserver'][0] : ""; + $pconfig['ntp2'] = !empty($config['dhcpdv6'][$if]['ntpserver'][1]) ? $config['dhcpdv6'][$if]['ntpserver'][1] : ""; + + // backward compatibility: migrate 'domain' to 'domainsearchlist' + if (empty($pconfig['domainsearchlist'])) { + $pconfig['domainsearchlist'] = $pconfig['domain']; + } + +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + $pconfig = $_POST; + $input_errors = array(); + + if (isset($pconfig['submit'])) { + // transform Additional BOOTP/DHCP Options + $pconfig['numberoptions'] = array(); + if (isset($pconfig['numberoptions_number'])) { + $pconfig['numberoptions']['item'] = array(); + foreach ($pconfig['numberoptions_number'] as $opt_seq => $opt_number) { + if (!empty($opt_number)) { + $pconfig['numberoptions']['item'][] = array('number' => $opt_number, + 'type' => $pconfig['numberoptions_type'][$opt_seq], + 'value' => $pconfig['numberoptions_value'][$opt_seq] + ); + } + } + } + + if (!empty($pconfig['prefixrange_from']) && !is_ipaddrv6($pconfig['prefixrange_from']) || + !empty($pconfig['prefixrange_to']) && !is_ipaddrv6($pconfig['prefixrange_to'])) { + $input_errors[] = gettext("A valid prefix range must be specified."); + } + if (!empty($pconfig['range_from']) && empty($pconfig['range_to']) || + empty($pconfig['range_from']) && !empty($pconfig['range_to'])) { + $input_errors[] = gettext("A valid range must be specified."); + } elseif (!empty($pconfig['range_from']) && !is_ipaddrv6($pconfig['range_from']) || + !empty($pconfig['range_to']) && !is_ipaddrv6($pconfig['range_to'])) { + $input_errors[] = gettext("A valid range must be specified."); + } else { + /* Disallow a range that includes the virtualip */ + if (!empty($config['virtualip']['vip'])) { + foreach($config['virtualip']['vip'] as $vip) { + if ($vip['interface'] == $if) { + if (!empty($vip['subnetv6']) && is_inrange_v6($vip['subnetv6'], $pconfig['range_from'], $pconfig['range_to'])) { + $input_errors[] = sprintf(gettext("The subnet range cannot overlap with virtual IPv6 address %s."),$vip['subnetv6']); + } + } + } + } + } + + if (!empty($pconfig['gateway']) && !is_ipaddrv6($pconfig['gateway'])) { + $input_errors[] = gettext("A valid IPv6 address must be specified for the gateway."); + } + if ((!empty($pconfig['dns1']) && !is_ipaddrv6($pconfig['dns1'])) || (!empty($pconfig['dns2']) && !is_ipaddrv6($pconfig['dns2']))) { + $input_errors[] = gettext("A valid IPv6 address must be specified for the primary/secondary DNS servers."); + } + + if (!empty($pconfig['defaultleasetime']) && (!is_numeric($pconfig['defaultleasetime']) || ($pconfig['defaultleasetime'] < 60))) { + $input_errors[] = gettext("The default lease time must be at least 60 seconds."); + } + if (!empty($pconfig['maxleasetime']) && (!is_numeric($pconfig['maxleasetime']) || ($pconfig['maxleasetime'] < 60) || ($pconfig['maxleasetime'] <= $_POST['defaultleasetime']))) { + $input_errors[] = gettext("The maximum lease time must be at least 60 seconds and higher than the default lease time."); + } + if (!empty($pconfig['minsecs']) && (!is_numeric($pconfig['minsecs']) || ($pconfig['minsecs'] < 0) || ($pconfig['minsecs'] > 255))) { + $input_errors[] = gettext("The response delay must be at least 0 and no more than 255 seconds."); + } + if (!empty($pconfig['ddnsdomain']) && !is_domain($pconfig['ddnsdomain'])) { + $input_errors[] = gettext("A valid domain name must be specified for the dynamic DNS registration."); + } + if (!empty($pconfig['ddnsdomainprimary']) && !is_ipaddrv6($pconfig['ddnsdomainprimary'])) { + $input_errors[] = gettext("A valid primary domain name server IPv6 address must be specified for the dynamic domain name."); + } + if (!empty($pconfig['ddnsdomainkey']) && base64_encode(base64_decode($pconfig['ddnsdomainkey'], true)) !== $pconfig['ddnsdomainkey']) { + $input_errors[] = gettext('You must specify a Base64-encoded domain key.'); + } + if ((!empty($pconfig['ddnsdomainkey']) && empty($pconfig['ddnsdomainkeyname'])) || + (!empty($pconfig['ddnsdomainkeyname']) && empty($pconfig['ddnsdomainkey']))) { + $input_errors[] = gettext("You must specify both a valid domain key and key name."); + } + if (!empty($pconfig['domainsearchlist'])) { + $domain_array=preg_split("/[ ;]+/",$pconfig['domainsearchlist']); + foreach ($domain_array as $curdomain) { + if (!is_domain($curdomain, true)) { + $input_errors[] = gettext("A valid domain search list must be specified."); + break; + } + } + } + + if ((!empty($pconfig['ntp1']) && !is_ipaddrv6($pconfig['ntp1'])) || (!empty($pconfig['ntp2']) && !is_ipaddrv6($pconfig['ntp2']))) { + $input_errors[] = gettext("A valid IPv6 address must be specified for the primary/secondary NTP servers."); + } + if (!empty($pconfig['bootfile_url']) && !is_URL($pconfig['bootfile_url'])) { + $input_errors[] = gettext("A valid URL must be specified for the network bootfile."); + } + + if (count($input_errors) == 0 && !empty($pconfig['enable'])) { + $range_from = $pconfig['range_from']; + $range_to = $pconfig['range_to']; + + if (isset($config['interfaces'][$if]['track6-interface'])) { + $range_from = merge_ipv6_address($ifcfgip, $pconfig['range_from']); + $range_to = merge_ipv6_address($ifcfgip, $pconfig['range_to']); + } + + if (!empty($pconfig['range_from']) && !empty($pconfig['range_to'])) { + /* make sure the range lies within the current subnet */ + if ((!is_inrange_v6($range_from, $subnet_start, $subnet_end)) || + (!is_inrange_v6($range_to, $subnet_start, $subnet_end))) { + $input_errors[] = gettext('The specified range lies outside of the current subnet.'); + } + + /* single IP subnet does not have enough addresses */ + if ($subnet_start == $subnet_end) { + $input_errors[] = gettext('The range is unavailable (single host network mask /128 used).'); + /* "from" cannot be higher than "to" */ + } elseif (inet_pton($pconfig['range_from']) > inet_pton($pconfig['range_to'])) { + $input_errors[] = gettext("The range is invalid (first element higher than second element)."); + } + + /* Verify static mappings do not overlap: + - available DHCP range + - prefix delegation range (FIXME: still need to be completed) */ + $dynsubnet_start = inet_pton($pconfig['range_from']); + $dynsubnet_end = inet_pton($pconfig['range_to']); + if (!empty($config['dhcpdv6'][$if]['staticmap'])) { + foreach ($config['dhcpdv6'][$if]['staticmap'] as $map) { + if (!empty($map['ipaddrv6']) && inet_pton($map['ipaddrv6']) > $dynsubnet_start && inet_pton($map['ipaddrv6']) < $dynsubnet_end) { + $input_errors[] = sprintf(gettext("The DHCP range cannot overlap any static DHCP mappings.")); + break; + } + } + } + } + } + + if (count($input_errors) == 0) { + config_read_array('dhcpdv6', $if); + $dhcpdconf = array(); + + // simple 1-on-1 copy + $config_copy_fieldsnames = array('defaultleasetime', 'maxleasetime', 'netmask', 'domainsearchlist', + 'ddnsdomain', 'ddnsdomainprimary', 'ddnsdomainkeyname', 'ddnsdomainkey', 'ddnsdomainalgorithm', 'bootfile_url', + 'dhcpv6leaseinlocaltime', 'minsecs'); + foreach ($config_copy_fieldsnames as $fieldname) { + if (!empty($pconfig[$fieldname])) { + $dhcpdconf[$fieldname] = $pconfig[$fieldname]; + } + } + + $dhcpdv6_enable_changed = !empty($pconfig['enable']) != !empty($config['dhcpdv6'][$if]['enable']); + + // boolean types + $dhcpdconf['netboot'] = !empty($pconfig['netboot']); + $dhcpdconf['enable'] = !empty($pconfig['enable']); + $dhcpdconf['ddnsupdate'] = !empty($pconfig['ddnsupdate']); + + // array types + $dhcpdconf['range'] = array(); + $dhcpdconf['range']['from'] = $pconfig['range_from']; + $dhcpdconf['range']['to'] = $pconfig['range_to']; + $dhcpdconf['prefixrange'] = array(); + $dhcpdconf['prefixrange']['from'] = $pconfig['prefixrange_from']; + $dhcpdconf['prefixrange']['to'] = $pconfig['prefixrange_to']; + $dhcpdconf['prefixrange']['prefixlength'] = $pconfig['prefixrange_length']; + $dhcpdconf['dnsserver'] = array(); + if (!empty($pconfig['dns1'])) { + $dhcpdconf['dnsserver'][] = $pconfig['dns1']; + } + if (!empty($pconfig['dns2'])) { + $dhcpdconf['dnsserver'][] = $pconfig['dns2']; + } + $dhcpdconf['ntpserver'] = []; + if (!empty($pconfig['ntp1'])) { + $dhcpdconf['ntpserver'][] = $pconfig['ntp1']; + } + if (!empty($pconfig['ntp2'])) { + $dhcpdconf['ntpserver'][] = $pconfig['ntp2']; + } + $dhcpdconf['numberoptions'] = $pconfig['numberoptions']; + + // copy structures back in + foreach (array('staticmap') as $fieldname) { + if (!empty($config['dhcpdv6'][$if][$fieldname])) { + $dhcpdconf[$fieldname] = $config['dhcpdv6'][$if][$fieldname]; + } + } + // router advertisement data lives in the same spot, copy + foreach (array_keys($config['dhcpdv6'][$if]) as $fieldname) { + if ((substr($fieldname, 0, 2) == 'ra' || substr($fieldname, 0, 3) == 'Adv') && !in_array($fieldname, array_keys($dhcpdconf))) { + $dhcpdconf[$fieldname] = $config['dhcpdv6'][$if][$fieldname]; + } + } + $config['dhcpdv6'][$if] = $dhcpdconf; + + write_config(); + + reconfigure_dhcpd(); + if ($dhcpdv6_enable_changed) { + filter_configure(); + } + + header(url_safe('Location: /services_dhcpv6.php?if=%s', array($if))); + exit; + } + } elseif (isset($pconfig['apply'])) { + reconfigure_dhcpd(); + header(url_safe('Location: /services_dhcpv6.php?if=%s', array($if))); + exit; + } elseif ($act == "del") { + if (!empty($config['dhcpdv6'][$if]['staticmap'][$_POST['id']])) { + unset($config['dhcpdv6'][$if]['staticmap'][$_POST['id']]); + write_config(); + if (isset($config['dhcpdv6'][$if]['enable'])) { + mark_subsystem_dirty('staticmapsv6'); + } + } + exit; + } elseif ($act == "export") { + $static = dhcpd_staticmap(6, true, null, true, true); + + header("Content-Type: text/csv"); + header("Content-Disposition: attachment; filename=\"isc_dhcpdv6_export_{$if}.csv\""); + header("Content-Transfer-Encoding: binary"); + header("Pragma: no-cache"); + header("Expires: 0"); + + $stream = fopen('php://output', 'w'); + + fputcsv($stream, ['ip_address', 'duid', 'hostname', 'domain_search', 'description']); + foreach ($static as $record) { + if ($record['interface'] !== $if) { + continue; + } + + fputcsv($stream, array_map(fn($k) => $record[$k], ['ipaddrv6', 'duid', 'hostname', 'domain', 'descr'])); + } + + @fclose($stream); + + exit; + } +} + +$service_hook = 'dhcpd6'; + +legacy_html_escape_form_data($pconfig); + +include("head.inc"); + +?> + + + + + + + +
    +
    +
    + 0) print_input_errors($input_errors); ?> + +

    + " . gettext("You must apply the changes in order for them to take effect."));?>
    + +

    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + += 0): ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + /> + +
    + + + + - + +
    + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + +
    + : + +
    + +
    +
    + + +
    + + +
    () + + +
    () + + +
    () + + +
    + /> + + + + +
    +
    + - +
    + +
    +
    + - +
    + +
    +
    + - +
    + +
    +
    + - +
    + +
      + + +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + +
    +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + diff --git a/net/isc-dhcp/src/www/services_dhcpv6_edit.php b/net/isc-dhcp/src/www/services_dhcpv6_edit.php new file mode 100644 index 0000000000..b7fe8cb0b4 --- /dev/null +++ b/net/isc-dhcp/src/www/services_dhcpv6_edit.php @@ -0,0 +1,273 @@ + + * Copyright (C) 2011 Seth Mos + * 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. + */ + +require_once("guiconfig.inc"); +require_once("interfaces.inc"); + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // handle identifiers and action + if (!empty($_GET['if']) && !empty($config['interfaces'][$_GET['if']])) { + $if = $_GET['if']; + } else { + header(url_safe('Location: /services_dhcpv6.php')); + exit; + } + if (isset($if) && isset($_GET['id']) && !empty($config['dhcpdv6'][$if]['staticmap'][$_GET['id']])) { + $id = $_GET['id']; + } + + // read form data + $pconfig = array(); + $config_copy_fieldnames = array('duid', 'hostname', 'ipaddrv6', 'filename' ,'rootpath' ,'descr', 'domain', 'domainsearchlist'); + foreach ($config_copy_fieldnames as $fieldname) { + if (isset($if) && isset($id) && isset($config['dhcpdv6'][$if]['staticmap'][$id][$fieldname])) { + $pconfig[$fieldname] = $config['dhcpdv6'][$if]['staticmap'][$id][$fieldname]; + } elseif (isset($_GET[$fieldname])) { + $pconfig[$fieldname] = $_GET[$fieldname]; + } else { + $pconfig[$fieldname] = null; + } + } + + // backward compatibility: migrate 'domain' to 'domainsearchlist' + if (empty($pconfig['domainsearchlist'])) { + $pconfig['domainsearchlist'] = $pconfig['domain']; + } + +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + $input_errors = array(); + $pconfig = $_POST; + + // handle identifiers and actions + if (!empty($pconfig['if']) && !empty($config['interfaces'][$pconfig['if']])) { + $if = $pconfig['if']; + } + if (!empty($config['dhcpdv6'][$if]['staticmap'][$pconfig['id']])) { + $id = $pconfig['id']; + } + + config_read_array('dhcpdv6', $if, 'staticmap'); + + /* input validation */ + if (!empty($pconfig['hostname'])) { + preg_match("/\-\$/", $pconfig['hostname'], $matches); + if ($matches) { + $input_errors[] = gettext("The hostname cannot end with a hyphen according to RFC952"); + } + if (!is_hostname($pconfig['hostname'])) { + $input_errors[] = gettext("The hostname can only contain the characters A-Z, 0-9 and '-'."); + } elseif (strpos($pconfig['hostname'],'.')) { + $input_errors[] = gettext("A valid hostname is specified, but the domain name part should be omitted"); + } + } + if (!empty($pconfig['ipaddrv6']) && !is_ipaddrv6($pconfig['ipaddrv6'])) { + $input_errors[] = gettext("A valid IPv6 address must be specified."); + } + + if (!empty($pconfig['duid'])) { + $pconfig['duid'] = str_replace("-",":",$pconfig['duid']); + if( preg_match('/^([a-fA-F0-9]{2}[:])*([a-fA-F0-9]{2}){1}$/', $pconfig['duid']) !== 1) { + $input_errors[] = gettext("A valid DUID Identifier must be specified."); + } + } + + if (!empty($pconfig['domainsearchlist'])) { + $domain_array=preg_split("/[ ;]+/",$pconfig['domainsearchlist']); + foreach ($domain_array as $curdomain) { + if (!is_domain($curdomain, true)) { + $input_errors[] = gettext("A valid domain search list must be specified."); + break; + } + } + } + + /* check for overlaps */ + $a_maps = &config_read_array('dhcpdv6', $if, 'staticmap'); + foreach ($a_maps as $mapent) { + if (isset($id) && ($a_maps[$id] === $mapent)) { + continue; + } + if ((($mapent['hostname'] == $pconfig['hostname']) && $mapent['hostname']) || ($mapent['duid'] == $pconfig['duid'])) { + $input_errors[] = gettext("This Hostname, IP or DUID Identifier already exists."); + break; + } + } + if (count($input_errors) == 0) { + $mapent = array(); + $config_copy_fieldnames = array('duid', 'ipaddrv6', 'hostname', 'descr', 'filename', 'rootpath', 'domainsearchlist'); + foreach ($config_copy_fieldnames as $fieldname) { + if (!empty($pconfig[$fieldname])) { + $mapent[$fieldname] = $pconfig[$fieldname]; + } + } + + if (isset($id)) { + $config['dhcpdv6'][$if]['staticmap'][$id] = $mapent; + } else { + $config['dhcpdv6'][$if]['staticmap'][] = $mapent; + } + + usort($config['dhcpdv6'][$if]['staticmap'], function ($a, $b) { + return ipcmp($a['ipaddrv6'], $b['ipaddrv6']); + }); + + write_config(); + + if (isset($config['dhcpdv6'][$if]['enable'])) { + mark_subsystem_dirty('staticmapsv6'); + } + + header(url_safe('Location: /services_dhcpv6.php?if=%s', array($if))); + exit; + } +} + +$service_hook = 'dhcpd6'; + +legacy_html_escape_form_data($pconfig); + +include("head.inc"); + +?> + + + +
    +
    +
    + 0) print_input_errors($input_errors); ?> +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + diff --git a/net/mdns-repeater/Makefile b/net/mdns-repeater/Makefile index 2c17394c8d..35f45dfb85 100644 --- a/net/mdns-repeater/Makefile +++ b/net/mdns-repeater/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= mdns-repeater -PLUGIN_VERSION= 1.0 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.2 PLUGIN_COMMENT= Proxy multicast DNS between networks PLUGIN_MAINTAINER= franz.fabian.94@gmail.com PLUGIN_DEPENDS= mdns-repeater diff --git a/net/mdns-repeater/pkg-descr b/net/mdns-repeater/pkg-descr index 3bcc2b3b9e..c67d64e00a 100644 --- a/net/mdns-repeater/pkg-descr +++ b/net/mdns-repeater/pkg-descr @@ -2,6 +2,20 @@ mdns-repeater is a Multicast DNS repeater. Multicast DNS uses the 224.0.0.251 address, which is "administratively scoped" and does not leave the subnet. This program re-broadcast mDNS packets from one interface to other interfaces. +It can be used to bridge zeroconf devices to work properly across the two subnets. -It can be used to bridge zeroconf devices to work properly across the two -subnets. +Plugin Changelog +================ + +1.2 + +* modernize plugin code +* blocklist support (contributed by Kodehyrden) + +1.1 + +* CARP support (contributed by Markus Reiter) + +1.0 + +* Initial release diff --git a/net/mdns-repeater/src/etc/inc/plugins.inc.d/mdnsrepeater.inc b/net/mdns-repeater/src/etc/inc/plugins.inc.d/mdnsrepeater.inc index 7cf9b86814..1ab4f9fe63 100644 --- a/net/mdns-repeater/src/etc/inc/plugins.inc.d/mdnsrepeater.inc +++ b/net/mdns-repeater/src/etc/inc/plugins.inc.d/mdnsrepeater.inc @@ -1,67 +1,67 @@ enabled == '1') { - return true; - } + global $config; - return false; -} + $services = []; -function mdnsrepeater_firewall($fw) -{ - if (!mdnsrepeater_enabled()) { - return; + if ( + isset($config['OPNsense']['MDNSRepeater']['enabled']) && + $config['OPNsense']['MDNSRepeater']['enabled'] == '1' + ) { + $services[] = [ + 'description' => gettext('mDNS Repeater'), + 'configd' => [ + 'start' => ['mdnsrepeater start'], + 'restart' => ['mdnsrepeater restart'], + 'stop' => ['mdnsrepeater stop'], + ], + 'name' => 'mdns-repeater', + 'pidfile' => '/var/run/mdns-repeater.pid', + ]; } + + return $services; } -function mdnsrepeater_services() +function mdnsrepeater_xmlrpc_sync() { - $services = array(); + $result = []; - // don't load the service if it is not enabled - // maybe there are no settings yet - if (!mdnsrepeater_enabled()) { - return $services; - } - - $services[] = array( - 'description' => gettext('MDNS Repeater'), - 'configd' => array( - 'restart' => array('mdnsrepeater restart'), - 'start' => array('mdnsrepeater start'), - 'stop' => array('mdnsrepeater stop'), - ), - 'name' => 'mdns-repeater', - ); + $result[] = [ + 'description' => gettext('mDNS Repeater'), + 'section' => 'OPNsense.MDNSRepeater', + 'id' => 'mdns-repeater', + 'services' => ['mdns-repeater'], + ]; - return $services; + return $result; } diff --git a/net/mdns-repeater/src/etc/rc.syshook.d/carp/50-mdns b/net/mdns-repeater/src/etc/rc.syshook.d/carp/50-mdns new file mode 100755 index 0000000000..25af77e521 --- /dev/null +++ b/net/mdns-repeater/src/etc/rc.syshook.d/carp/50-mdns @@ -0,0 +1,74 @@ +#!/usr/local/bin/php + + * 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. + */ + +require_once("config.inc"); +require_once("util.inc"); +require_once('interfaces.inc'); +require_once("plugins.inc.d/mdnsrepeater.inc"); + +$mdns_repeater = new \OPNsense\MDNSRepeater\MDNSRepeater(); +$mdns_repeater_carp_enabled = + (string)$mdns_repeater->enabled == '1' && + (string)$mdns_repeater->enablecarp == '1'; + +if ($mdns_repeater_carp_enabled) { + $subsystem = !empty($argv[1]) ? $argv[1] : ''; + $type = !empty($argv[2]) ? $argv[2] : ''; + + if ($type != 'MASTER' && $type != 'BACKUP') { + log_msg("Carp '$type' event unknown from source '{$subsystem}'"); + exit(1); + } + + if (!strstr($subsystem, '@')) { + log_msg("Carp '$type' event triggered from wrong source '{$subsystem}'"); + exit(1); + } + + list ($vhid, $iface) = explode('@', $subsystem); + + $friendly_interface = convert_real_interface_to_friendly_interface_name($iface); + $mdns_repeater_interfaces = explode(',', $mdns_repeater->interfaces); + if (!in_array($friendly_interface, $mdns_repeater_interfaces)) { + exit(0); + } + + $backend = new \OPNsense\Core\Backend(); + + switch ($type) { + case 'MASTER': + touch('/var/run/mdns-repeater.CARP_MASTER'); + $backend->configdRun('mdnsrepeater start'); + break; + case 'BACKUP': + @unlink('/var/run/mdns-repeater.CARP_MASTER'); + $backend->configdRun('mdnsrepeater stop'); + break; + } +} diff --git a/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/Api/ServiceController.php b/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/Api/ServiceController.php index 1d5637292f..f109dd8a32 100644 --- a/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/Api/ServiceController.php +++ b/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/Api/ServiceController.php @@ -2,6 +2,7 @@ /** * Copyright (C) 2017 Fabian Franz + * Copyright (C) 2024 Cedrik Pischem * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,52 +29,12 @@ namespace OPNsense\MDNSRepeater\Api; -use OPNsense\Base\ApiControllerBase; -use OPNsense\Core\Backend; -use OPNsense\MDNSRepeater\MDNSRepeater; +use OPNsense\Base\ApiMutableServiceControllerBase; -class ServiceController extends ApiControllerBase +class ServiceController extends ApiMutableServiceControllerBase { - public function statusAction() - { - $backend = new Backend(); - $result = array('result' => 'failed'); - $res = $backend->configdRun('mdnsrepeater status'); - if (stripos($res, 'is running')) { - $result['result'] = 'running'; - } elseif (stripos($res, 'not running')) { - $general = new MDNSRepeater(); - if ((string)$general->enabled == '1') { - $result['result'] = 'stopped'; - } else { - $result['result'] = 'disabled'; - } - } else { - $result['message'] = $res; - } - return $result; - } - - public function startAction() - { - $backend = new Backend(); - $result = array('result' => 'failed'); - $backend->configdRun('template reload OPNsense/MDNSRepeater'); - $result['result'] = $backend->configdRun('mdnsrepeater start'); - return $result; - } - - public function stopAction() - { - $backend = new Backend(); - $result = array("result" => "failed"); - $result['result'] = $backend->configdRun('mdnsrepeater stop'); - return $result; - } - - public function restartAction() - { - $this->stopAction(); - return $this->startAction(); - } + protected static $internalServiceClass = '\OPNsense\MDNSRepeater\MDNSRepeater'; + protected static $internalServiceTemplate = 'OPNsense/MDNSRepeater'; + protected static $internalServiceEnabled = 'enabled'; + protected static $internalServiceName = 'mdnsrepeater'; } diff --git a/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/IndexController.php b/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/IndexController.php index 10fdf5234c..1830697d45 100644 --- a/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/IndexController.php +++ b/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/IndexController.php @@ -37,7 +37,7 @@ class IndexController extends \OPNsense\Base\IndexController { /** - * MDNS Repeater index page + * mDNS Repeater index page * @throws \Exception */ public function indexAction() diff --git a/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/forms/general.xml b/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/forms/general.xml index e6a87140ea..4f6ba145ba 100644 --- a/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/forms/general.xml +++ b/net/mdns-repeater/src/opnsense/mvc/app/controllers/OPNsense/MDNSRepeater/forms/general.xml @@ -1,14 +1,32 @@
    - - mdnsrepeater.enabled - - checkbox - Enable the repeater. - - - mdnsrepeater.interfaces - - select_multiple - At least two interfaces must be selected. - + + header + + + + mdnsrepeater.enabled + + checkbox + Enable the repeater. + + + mdnsrepeater.enablecarp + + checkbox + This will activate the repeater service only on the master device. + + + mdnsrepeater.interfaces + + select_multiple + At least 2 interfaces must be selected. The maximum number of supported interfaces by the daemon is 5. + + + mdnsrepeater.blocklist + + select_multiple + + true + Enter up to 16 client IPv4 addresses including subnet or networks to be exluded. +
    diff --git a/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/ACL/ACL.xml b/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/ACL/ACL.xml index d7bef3537c..8fe0a0f3aa 100644 --- a/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/ACL/ACL.xml +++ b/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/ACL/ACL.xml @@ -1,6 +1,6 @@ - Services: MDNS Repeatery + Services: mDNS Repeater ui/mdnsrepeater/* api/mdnsrepeater/* diff --git a/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/MDNSRepeater.xml b/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/MDNSRepeater.xml index 245cc6ebe8..e5e868d550 100644 --- a/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/MDNSRepeater.xml +++ b/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/MDNSRepeater.xml @@ -1,16 +1,28 @@ - //OPNsense/MDNSRepeater - 1.0.0 - mdns-repeater settings - - - 0 - Y - - - lan - Y - Y - - + //OPNsense/MDNSRepeater + 1.0.1 + mdns-repeater settings + + + 0 + Y + + + 0 + Y + + + lan + Y + Y + + + Y + ipv4 + , + N + Y + Only valid IPv4 networks or individual addresses including subnet are allowed. + + diff --git a/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/Menu/Menu.xml b/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/Menu/Menu.xml index c3cea26cbd..6b42fa7bc7 100644 --- a/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/Menu/Menu.xml +++ b/net/mdns-repeater/src/opnsense/mvc/app/models/OPNsense/MDNSRepeater/Menu/Menu.xml @@ -1,5 +1,5 @@ - + diff --git a/net/mdns-repeater/src/opnsense/mvc/app/views/OPNsense/MDNSRepeater/index.volt b/net/mdns-repeater/src/opnsense/mvc/app/views/OPNsense/MDNSRepeater/index.volt index 014decb92c..639ff6f3b9 100644 --- a/net/mdns-repeater/src/opnsense/mvc/app/views/OPNsense/MDNSRepeater/index.volt +++ b/net/mdns-repeater/src/opnsense/mvc/app/views/OPNsense/MDNSRepeater/index.volt @@ -1,63 +1,75 @@ {# - -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. - -#} + # Copyright (c) 2017 Fabian Franz + # Copyright (c) 2024 Cedrik Pischem + # 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': general,'id':'general'])}} -
    -
    - +
    +
    + {{ partial("layout_partials/base_form", ['fields': general, 'id': 'frm_GeneralSettings']) }} +
    +
    +
    +
    +
    + +

    +
    -
    + diff --git a/net/mdns-repeater/src/opnsense/service/conf/actions.d/actions_mdnsrepeater.conf b/net/mdns-repeater/src/opnsense/service/conf/actions.d/actions_mdnsrepeater.conf index 16ef2c5c5f..6372ffde18 100644 --- a/net/mdns-repeater/src/opnsense/service/conf/actions.d/actions_mdnsrepeater.conf +++ b/net/mdns-repeater/src/opnsense/service/conf/actions.d/actions_mdnsrepeater.conf @@ -17,8 +17,3 @@ message:mdns-repeater status command:service mdns-repeater restart type:script message:restarting mdns-repeater - -[reload] -command:service mdns-repeater reload -type:script -message:reload mdns-repeater diff --git a/net/mdns-repeater/src/opnsense/service/templates/OPNsense/MDNSRepeater/mdnsrepeater b/net/mdns-repeater/src/opnsense/service/templates/OPNsense/MDNSRepeater/mdnsrepeater index feb71084a6..7d397003e1 100644 --- a/net/mdns-repeater/src/opnsense/service/templates/OPNsense/MDNSRepeater/mdnsrepeater +++ b/net/mdns-repeater/src/opnsense/service/templates/OPNsense/MDNSRepeater/mdnsrepeater @@ -1,12 +1,21 @@ {% if helpers.exists('OPNsense.MDNSRepeater.enabled') and OPNsense.MDNSRepeater.enabled == '1' %} {% from 'OPNsense/Macros/interface.macro' import physical_interface %} mdns_repeater_enable="YES" +{% if helpers.exists('OPNsense.MDNSRepeater.enablecarp') and OPNsense.MDNSRepeater.enablecarp == '1' %} +required_files="/var/run/mdns-repeater.CARP_MASTER" +{% endif %} {% set osifnames = OPNsense.MDNSRepeater.interfaces.split(',') %} {% set interface_list=[] %} {% for i in osifnames %} {% do interface_list.append(physical_interface(i)) %} {% endfor %} -mdns_repeater_interfaces="{{ interface_list | join(' ') }}" +{% set repeater_interfaces = interface_list | join(' ') %} +{% if helpers.exists('OPNsense.MDNSRepeater.blocklist') and OPNsense.MDNSRepeater.blocklist != '' %} +{% set blocklist_and_repeater_interfaces = "-b " + OPNsense.MDNSRepeater.blocklist.split(',') | join(' -b ') + " " + repeater_interfaces %} +mdns_repeater_interfaces="{{ blocklist_and_repeater_interfaces }}" +{% else %} +mdns_repeater_interfaces="{{ repeater_interfaces }}" +{% endif %} {% else %} mdns_repeater_enable="NO" {% endif %} diff --git a/net/ndp-proxy-go/Makefile b/net/ndp-proxy-go/Makefile new file mode 100644 index 0000000000..886c424755 --- /dev/null +++ b/net/ndp-proxy-go/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= ndp-proxy-go +PLUGIN_VERSION= 1.3 +PLUGIN_REVISION= 1 +PLUGIN_COMMENT= IPv6 Neighbor Discovery Protocol (NDP) Proxy +PLUGIN_MAINTAINER= cedrik@pischem.com +PLUGIN_DEPENDS= ndp-proxy-go + +.include "../../Mk/plugins.mk" diff --git a/net/ndp-proxy-go/pkg-descr b/net/ndp-proxy-go/pkg-descr new file mode 100644 index 0000000000..161f8684eb --- /dev/null +++ b/net/ndp-proxy-go/pkg-descr @@ -0,0 +1,25 @@ +IPv6 Neighbor Discovery Protocol (NDP) Proxy + +WWW: https://github.com/monviech/ndp-proxy-go +DOC: https://docs.opnsense.org/manual/ndp-proxy-go.html + +Plugin Changelog +================ + +1.3 + +* Add ratelimit for pfctl operations +* Add CARP failover + +1.2 + +* Add firewall alias support +* Add cache file support for faster recovery after system reboots + +1.1 + +* Add experimental point-to-point device upstream support (e.g. PPPoE) + +1.0 + +* Initial Release diff --git a/net/ndp-proxy-go/src/etc/inc/plugins.inc.d/ndpproxy.inc b/net/ndp-proxy-go/src/etc/inc/plugins.inc.d/ndpproxy.inc new file mode 100644 index 0000000000..52b0f5b251 --- /dev/null +++ b/net/ndp-proxy-go/src/etc/inc/plugins.inc.d/ndpproxy.inc @@ -0,0 +1,66 @@ + gettext('NDP Proxy'), + 'configd' => [ + 'start' => ['ndpproxy start'], + 'restart' => ['ndpproxy restart'], + 'stop' => ['ndpproxy stop'], + ], + 'name' => 'ndpproxy', + 'pidfile' => '/var/run/ndp_proxy_go.pid' + ]; + } + + return $services; +} + +function ndpproxy_xmlrpc_sync() +{ + $result = []; + + $result[] = array( + 'description' => gettext('NDP Proxy'), + 'section' => 'OPNsense.ndpproxy', + 'id' => 'ndpproxy', + 'services' => ["ndpproxy"], + ); + + return $result; +} diff --git a/net/ndp-proxy-go/src/etc/rc.syshook.d/carp/20-ndpproxy b/net/ndp-proxy-go/src/etc/rc.syshook.d/carp/20-ndpproxy new file mode 100755 index 0000000000..8ec0ccb33b --- /dev/null +++ b/net/ndp-proxy-go/src/etc/rc.syshook.d/carp/20-ndpproxy @@ -0,0 +1,53 @@ +#!/usr/local/bin/php +general->enabled->isEmpty() || + $model->general->carp_depend_on->isEmpty() +) { + exit(0); +} + +$actions = [ + 'MASTER' => 'start', + 'BACKUP' => 'stop', +]; + +mwexecfm('/usr/local/etc/rc.d/ndp-proxy-go ' . $actions[$type]); diff --git a/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/Api/GeneralController.php b/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/Api/GeneralController.php new file mode 100644 index 0000000000..7662fcbd8e --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/Api/GeneralController.php @@ -0,0 +1,65 @@ +searchBase('aliases.alias'); + } + + public function setAliasAction($uuid) + { + return $this->setBase('alias', 'aliases.alias', $uuid); + } + + public function addAliasAction() + { + return $this->addBase('alias', 'aliases.alias'); + } + + public function getAliasAction($uuid = null) + { + return $this->getBase('alias', 'aliases.alias', $uuid); + } + + public function delAliasAction($uuid) + { + return $this->delBase('aliases.alias', $uuid); + } +} diff --git a/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/Api/ServiceController.php b/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/Api/ServiceController.php new file mode 100644 index 0000000000..1430b910ac --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/Api/ServiceController.php @@ -0,0 +1,41 @@ +view->pick('OPNsense/NdpProxy/general'); + $this->view->generalForm = $this->getForm('general'); + + $this->view->formDialogAlias = $this->getForm('dialogAlias'); + $this->view->formGridAlias = $this->getFormGrid('dialogAlias'); + } +} diff --git a/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/forms/dialogAlias.xml b/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/forms/dialogAlias.xml new file mode 100644 index 0000000000..31ac751182 --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/forms/dialogAlias.xml @@ -0,0 +1,22 @@ +
    + + alias.interface + + dropdown + Add IPv6 addresses to the firewall alias that belongs to this proxied interface. When choosing any, all IPv6 addresses will be added. + + any + + + + alias.alias + + dropdown + Choose an "external (advanced)" type alias from "Firewall - Aliases". Whenever a client is discovered, the IPv6 address will be automatically added to the chosen alias. When the neighbor cache lifetime expires, the IPv6 address will be removed from the alias. + + + alias.description + + text + +
    diff --git a/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/forms/general.xml b/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/forms/general.xml new file mode 100644 index 0000000000..596c7b1af4 --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/mvc/app/controllers/OPNsense/NdpProxy/forms/general.xml @@ -0,0 +1,109 @@ +
    + + header + + + + ndpproxy.general.enabled + + checkbox + Enable or disable this service. + + + ndpproxy.general.carp_depend_on + + checkbox + true + If any CARP VHID on this node is in MASTER state the service will be started, otherwise stopped. As NDP is stateless, a short interruption of IPv6 connectivity must be expected during CARP transitions. + + + header + + + + ndpproxy.general.upstream + + dropdown + Choose the upstream interface which receives the external IPv6 prefix from the ISP. Usually, this is the WAN interface. Ethernet interfaces are fully supported, point-to-point (PPPoE) devices are experimental. + + + ndpproxy.general.downstream + + select_multiple + Choose one or multiple downstream interfaces which should proxy the upstream IPv6 prefix. Only ethernet interfaces are supported. + + + ndpproxy.general.ra + + checkbox + Proxy upstream RAs to downstream interfaces. Disable this if you use your own RA daemon. + + + ndpproxy.general.routes + + checkbox + Automatically create host routes for discovered clients. Disabling this means you must manually handle all routing decisions. + + + header + + true + + + ndpproxy.general.cache_ttl + + text + 10 + Neighbor cache lifetime in minutes. This controls when stale clients, host routes and firewall aliases are cleaned up. When using a point-to-point interface as upstream, increasing this lifetime is necessary to not prematurely clean up routes. + + + ndpproxy.general.cache_max + + text + 4096 + Maximum learned neighbors, increase for large networks. + + + ndpproxy.general.cache_file + + checkbox + Persist cache to file on service stop and load it on service start. Only neighbors with a valid cache lifetime are loaded. This helps on system reboots to minimize downtime of individual clients. + + + header + + true + + + ndpproxy.general.route_qps + + text + 50 + Maximum route operations per second. Limits how fast routes are applied; excess operations are queued, not dropped. + + + ndpproxy.general.pf_qps + + text + 50 + Maximum firewall alias operations per second. Limits how fast aliases are populated; excess operations are queued, not dropped. + + + ndpproxy.general.pcap_timeout + + text + 50 + Controls CPU usage vs. NDP responsiveness. Lower values (e.g., 25 ms) minimize latency during cache refresh at the cost of more CPU. Higher values (100–250 ms) reduce CPU use but may introduce small latency spikes. + + + header + + true + + + ndpproxy.general.debug + + checkbox + Enable debug logging. + +
    diff --git a/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/ACL/ACL.xml b/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/ACL/ACL.xml new file mode 100644 index 0000000000..b70514587e --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/ACL/ACL.xml @@ -0,0 +1,17 @@ + + + Services: NDP Proxy: General Settings + Allow access to NDP Proxy General Settings + + ui/ndpproxy/general/* + api/ndpproxy/general/* + + + + Services: NDP Proxy: Log File + + ui/diagnostics/log/core/ndpproxy/* + api/diagnostics/log/core/ndpproxy/* + + + diff --git a/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/Menu/Menu.xml b/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/Menu/Menu.xml new file mode 100644 index 0000000000..7efc570587 --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/Menu/Menu.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/NdpProxy.php b/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/NdpProxy.php new file mode 100644 index 0000000000..5831f6887a --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/NdpProxy.php @@ -0,0 +1,68 @@ +general->enabled->isEqual('1')) { + foreach (['upstream', 'downstream'] as $field) { + if ($this->general->$field->isEmpty()) { + $messages->appendMessage(new Message( + gettext('Interface is required.'), + "general.$field" + )); + } + } + + $upstream = $this->general->upstream->getValue(); + $downstreamList = array_filter(explode(',', $this->general->downstream->getValue())); + + if (!empty($upstream) && in_array($upstream, $downstreamList, true)) { + $messages->appendMessage(new Message( + gettext('Downstream interfaces cannot contain upstream interface.'), + 'general.downstream' + )); + } + } + } + + public function performValidation($validateFullModel = false) + { + $messages = parent::performValidation($validateFullModel); + $this->checkConfiguration($messages); + return $messages; + } +} diff --git a/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/NdpProxy.xml b/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/NdpProxy.xml new file mode 100644 index 0000000000..9f02e11462 --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/mvc/app/models/OPNsense/NdpProxy/NdpProxy.xml @@ -0,0 +1,74 @@ + + //OPNsense/ndpproxy + NDP Proxy model + 1.1 + + + + 0 + Y + + + + Y + + + 1 + Y + + + 1 + Y + + + 1 + + + 1 + + + 0 + Y + + + 1 + + + 1 + + + 1 + + + 0 + Y + + + 0 + Y + + + + + + any + + + + + OPNsense.Firewall.Alias + aliases.alias + name + + /^[Ee]xternal.*/ + /^(?!bogons$|bogonsv6$|virusprot$|sshlockout$|__.*).*/ + + + + Y + + + + + + diff --git a/net/ndp-proxy-go/src/opnsense/mvc/app/views/OPNsense/NdpProxy/general.volt b/net/ndp-proxy-go/src/opnsense/mvc/app/views/OPNsense/NdpProxy/general.volt new file mode 100644 index 0000000000..4c36c5e879 --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/mvc/app/views/OPNsense/NdpProxy/general.volt @@ -0,0 +1,79 @@ +{# + # Copyright (c) 2025 Cedrik Pischem + # 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': generalForm, 'id': 'frm_GeneralSettings']) }} +
    +
    + {{ partial('layout_partials/base_bootgrid_table', formGridAlias)}} +
    +
    + +{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/ndpproxy/service/reconfigure', 'data_service_widget': 'ndpproxy'}) }} +{{ partial('layout_partials/base_dialog',['fields':formDialogAlias,'id':formGridAlias['edit_dialog_id'],'label':lang._('Edit Alias')])}} diff --git a/net/ndp-proxy-go/src/opnsense/service/conf/actions.d/actions_ndpproxy.conf b/net/ndp-proxy-go/src/opnsense/service/conf/actions.d/actions_ndpproxy.conf new file mode 100644 index 0000000000..f5fcf8731d --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/service/conf/actions.d/actions_ndpproxy.conf @@ -0,0 +1,24 @@ +[start] +command:service ndp-proxy-go start +parameters: +type:script +message:Starting NDP Proxy service + +[stop] +command:service ndp-proxy-go stop +parameters: +type:script +message:Stopping NDP Proxy service + +[restart] +command:service ndp-proxy-go restart +parameters: +type:script +message:Restarting NDP Proxy service +description:Restart NDP Proxy service + +[status] +command:service ndp-proxy-go status +parameters: +type:script_output +message:Requesting NDP Proxy status diff --git a/net/ndp-proxy-go/src/opnsense/service/templates/OPNsense/NdpProxy/+TARGETS b/net/ndp-proxy-go/src/opnsense/service/templates/OPNsense/NdpProxy/+TARGETS new file mode 100644 index 0000000000..a75c8d5120 --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/service/templates/OPNsense/NdpProxy/+TARGETS @@ -0,0 +1 @@ +ndp_proxy_go:/etc/rc.conf.d/ndp_proxy_go diff --git a/net/ndp-proxy-go/src/opnsense/service/templates/OPNsense/NdpProxy/ndp_proxy_go b/net/ndp-proxy-go/src/opnsense/service/templates/OPNsense/NdpProxy/ndp_proxy_go new file mode 100644 index 0000000000..83d2d4002a --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/service/templates/OPNsense/NdpProxy/ndp_proxy_go @@ -0,0 +1,55 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +{% set general = helpers.getNodeByTag('OPNsense.ndpproxy.general') %} +{% if general.enabled|default("0") == "1" and general.upstream and general.downstream %} +ndp_proxy_go_enable="YES" +{% if general.carp_depend_on|default("0") == "1" %} +ndp_proxy_go_check_carp="YES" +{% endif %} +ndp_proxy_go_upstream="{{ helpers.physical_interface(general.upstream) }}" +{% set downstream_interfaces = [] %} +{% for interface in general.downstream.split(',') %} +{% do downstream_interfaces.append(helpers.physical_interface(interface)) %} +{% endfor %} +ndp_proxy_go_downstream="{{ downstream_interfaces|join(' ') }}" +{% if general.cache_file == "1" %} +ndp_proxy_go_cache_file="/var/db/ndpproxy/cache.json" +{% endif %} +{% set flags = [] %} +{% if general.debug == "1" %} +{% do flags.append('--debug') %} +{% endif %} +{% if general.ra == "0" %} +{% do flags.append('--no-ra') %} +{% endif %} +{% if general.routes == "0" %} +{% do flags.append('--no-routes') %} +{% endif %} +{% if general.cache_ttl %} +{% do flags.append('--cache-ttl ' ~ general.cache_ttl ~ 'm') %} +{% endif %} +{% if general.cache_max %} +{% do flags.append('--cache-max ' ~ general.cache_max) %} +{% endif %} +{% if general.route_qps %} +{% do flags.append('--route-qps ' ~ general.route_qps) %} +{% endif %} +{% if general.pf_qps %} +{% do flags.append('--pf-qps ' ~ general.pf_qps) %} +{% endif %} +{% if general.pcap_timeout %} +{% do flags.append('--pcap-timeout ' ~ general.pcap_timeout ~ 'ms') %} +{% endif %} +{% for alias in helpers.toList('OPNsense.ndpproxy.aliases.alias') %} +{% set iface = alias.interface|default('') %} +{% if iface == '' %} +{% do flags.append('--pf=:' ~ helpers.getUUID(alias.alias).name) %} +{% else %} +{% do flags.append('--pf=' ~ helpers.physical_interface(iface) ~ ':' ~ helpers.getUUID(alias.alias).name) %} +{% endif %} +{% endfor %} +{% if flags|length > 0 %} +ndp_proxy_go_flags="{{ flags|join(' ') }}" +{% endif %} +{% else %} +ndp_proxy_go_enable="NO" +{% endif %} diff --git a/net/ndp-proxy-go/src/opnsense/service/templates/OPNsense/Syslog/local/ndpproxy.conf b/net/ndp-proxy-go/src/opnsense/service/templates/OPNsense/Syslog/local/ndpproxy.conf new file mode 100644 index 0000000000..302335e1c9 --- /dev/null +++ b/net/ndp-proxy-go/src/opnsense/service/templates/OPNsense/Syslog/local/ndpproxy.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration [ndpproxy]. +################################################################### +filter f_local_ndpproxy { + program("ndpproxy"); +}; diff --git a/net/ndproxy/Makefile b/net/ndproxy/Makefile new file mode 100644 index 0000000000..9c7a2855ba --- /dev/null +++ b/net/ndproxy/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= ndproxy +PLUGIN_VERSION= 1.1 +PLUGIN_DEPENDS= ndproxy +PLUGIN_COMMENT= Neighbor Discovery Proxy +PLUGIN_MAINTAINER= cedrik@pischem.com + +.include "../../Mk/plugins.mk" diff --git a/net/ndproxy/pkg-descr b/net/ndproxy/pkg-descr new file mode 100644 index 0000000000..a0c380fb0d --- /dev/null +++ b/net/ndproxy/pkg-descr @@ -0,0 +1,12 @@ +Ndproxy is a kernel module that implements IPv6 Neighbor Discovery proxying over Ethernet-like access networks. + +Plugin Changelog +================ + +1.1 + +* Promiscuous mode for the Uplink Interface is no longer enabled + +1.0 + +* Initial Release diff --git a/net/ndproxy/src/etc/inc/plugins.inc.d/ndproxy.inc b/net/ndproxy/src/etc/inc/plugins.inc.d/ndproxy.inc new file mode 100644 index 0000000000..9ea3e573a7 --- /dev/null +++ b/net/ndproxy/src/etc/inc/plugins.inc.d/ndproxy.inc @@ -0,0 +1,66 @@ + gettext('Ndproxy'), + 'configd' => [ + 'start' => ['ndproxy start'], + 'restart' => ['ndproxy restart'], + 'stop' => ['ndproxy stop'], + ], + 'name' => 'ndproxy', + 'nocheck' => true, + ]; + } + + return $services; +} + +function ndproxy_xmlrpc_sync() +{ + $result = []; + + $result[] = array( + 'description' => gettext('Ndproxy'), + 'section' => 'OPNsense.ndproxy', + 'id' => 'ndproxy', + 'services' => ["ndproxy"], + ); + + return $result; +} diff --git a/net/ndproxy/src/opnsense/mvc/app/controllers/OPNsense/Ndproxy/Api/GeneralController.php b/net/ndproxy/src/opnsense/mvc/app/controllers/OPNsense/Ndproxy/Api/GeneralController.php new file mode 100644 index 0000000000..3fe747dc6c --- /dev/null +++ b/net/ndproxy/src/opnsense/mvc/app/controllers/OPNsense/Ndproxy/Api/GeneralController.php @@ -0,0 +1,40 @@ +view->pick('OPNsense/Ndproxy/general'); + $this->view->generalForm = $this->getForm("general"); + } +} diff --git a/net/ndproxy/src/opnsense/mvc/app/controllers/OPNsense/Ndproxy/forms/general.xml b/net/ndproxy/src/opnsense/mvc/app/controllers/OPNsense/Ndproxy/forms/general.xml new file mode 100644 index 0000000000..2075466263 --- /dev/null +++ b/net/ndproxy/src/opnsense/mvc/app/controllers/OPNsense/Ndproxy/forms/general.xml @@ -0,0 +1,40 @@ +
    + + header + + + + ndproxy.general.enabled + + checkbox + + + + ndproxy.general.ndproxy_uplink_interface + + dropdown + + + + ndproxy.general.ndproxy_downlink_mac_address + + text + + + + ndproxy.general.ndproxy_uplink_ipv6_addresses + + select_multiple + + true + + + + ndproxy.general.ndproxy_exception_ipv6_addresses + + select_multiple + + true + + +
    diff --git a/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/ACL/ACL.xml b/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/ACL/ACL.xml new file mode 100644 index 0000000000..a2aa1cba51 --- /dev/null +++ b/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/ACL/ACL.xml @@ -0,0 +1,10 @@ + + + Services: Ndproxy: General Settings + Allow access to Ndproxy General Settings + + ui/ndproxy/general/* + api/ndproxy/general/* + + + diff --git a/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/Menu/Menu.xml b/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/Menu/Menu.xml new file mode 100644 index 0000000000..32ba653a7e --- /dev/null +++ b/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/Menu/Menu.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/Ndproxy.php b/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/Ndproxy.php new file mode 100644 index 0000000000..1db32ccfc3 --- /dev/null +++ b/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/Ndproxy.php @@ -0,0 +1,66 @@ +general->enabled === '1') { + $requiredFields = [ + 'ndproxy_uplink_interface', + 'ndproxy_downlink_mac_address', + 'ndproxy_uplink_ipv6_addresses' + ]; + + foreach ($requiredFields as $field) { + if (empty((string)$this->general->$field)) { + $messages->appendMessage(new Message( + gettext('Field is required to enable Ndproxy.'), + "general." . $field + )); + } + } + } + } + + public function performValidation($validateFullModel = false) + { + $messages = parent::performValidation($validateFullModel); + + $this->checkConfiguration($messages); + + return $messages; + } +} diff --git a/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/Ndproxy.xml b/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/Ndproxy.xml new file mode 100644 index 0000000000..1076dd8111 --- /dev/null +++ b/net/ndproxy/src/opnsense/mvc/app/models/OPNsense/Ndproxy/Ndproxy.xml @@ -0,0 +1,27 @@ + + //OPNsense/ndproxy + ndproxy configuration model + 1.0 + + + + 0 + Y + + + + + ipv6 + , + Y + Please enter one or multiple valid IPv6 addresses. + + + ipv6 + , + Y + Please enter one or multiple valid IPv6 addresses. + + + + diff --git a/net/ndproxy/src/opnsense/mvc/app/views/OPNsense/Ndproxy/general.volt b/net/ndproxy/src/opnsense/mvc/app/views/OPNsense/Ndproxy/general.volt new file mode 100644 index 0000000000..5e212445bd --- /dev/null +++ b/net/ndproxy/src/opnsense/mvc/app/views/OPNsense/Ndproxy/general.volt @@ -0,0 +1,73 @@ +{# + # Copyright (c) 2024 Cedrik Pischem + # 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': generalForm, 'id': 'frm_GeneralSettings']) }} +
    +
    +
    +
    +
    + +

    +
    +
    +
    diff --git a/net/ndproxy/src/opnsense/service/conf/actions.d/actions_ndproxy.conf b/net/ndproxy/src/opnsense/service/conf/actions.d/actions_ndproxy.conf new file mode 100644 index 0000000000..eeb5174bbf --- /dev/null +++ b/net/ndproxy/src/opnsense/service/conf/actions.d/actions_ndproxy.conf @@ -0,0 +1,24 @@ +[start] +command:service ndproxy start +parameters: +type:script +message:Starting ndproxy service + +[stop] +command:service ndproxy stop +parameters: +type:script +message:Stopping ndproxy service + +[restart] +command:service ndproxy restart +parameters: +type:script +message:Restarting ndproxy service +description:Restart ndproxy service + +[status] +command:/usr/local/sbin/pluginctl -s ndproxy status +parameters: +type:script_output +message:Request ndproxy status diff --git a/net/ndproxy/src/opnsense/service/templates/OPNsense/Ndproxy/+TARGETS b/net/ndproxy/src/opnsense/service/templates/OPNsense/Ndproxy/+TARGETS new file mode 100644 index 0000000000..181513c0d1 --- /dev/null +++ b/net/ndproxy/src/opnsense/service/templates/OPNsense/Ndproxy/+TARGETS @@ -0,0 +1 @@ +rc.conf.d/ndproxy:/etc/rc.conf.d/ndproxy diff --git a/net/ndproxy/src/opnsense/service/templates/OPNsense/Ndproxy/rc.conf.d/ndproxy b/net/ndproxy/src/opnsense/service/templates/OPNsense/Ndproxy/rc.conf.d/ndproxy new file mode 100644 index 0000000000..aa5066c63d --- /dev/null +++ b/net/ndproxy/src/opnsense/service/templates/OPNsense/Ndproxy/rc.conf.d/ndproxy @@ -0,0 +1,21 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +{% set generalSettings = helpers.getNodeByTag('OPNsense.ndproxy.general') %} +{% if generalSettings.enabled|default("0") == "1" %} +ndproxy_enable="YES" +{# Always disable promiscuous mode forced by port since it should be controlled via interface settings if needed. #} +ndproxy_promisc_enable="NO" +{% if generalSettings.ndproxy_uplink_interface %} +ndproxy_uplink_interface="{{ helpers.physical_interface(generalSettings.ndproxy_uplink_interface) }}" +{% endif %} +{% if generalSettings.ndproxy_downlink_mac_address %} +ndproxy_downlink_mac_address="{{ generalSettings.ndproxy_downlink_mac_address }}" +{% endif %} +{% if generalSettings.ndproxy_exception_ipv6_addresses %} +ndproxy_exception_ipv6_addresses="{{ generalSettings.ndproxy_exception_ipv6_addresses | replace(',', ';') }}" +{% endif %} +{% if generalSettings.ndproxy_uplink_ipv6_addresses %} +ndproxy_uplink_ipv6_addresses="{{ generalSettings.ndproxy_uplink_ipv6_addresses | replace(',', ';') }}" +{% endif %} +{% else %} +ndproxy_enable="NO" +{% endif %} diff --git a/net/ntopng/Makefile b/net/ntopng/Makefile index fdced159bb..5da05c18f5 100644 --- a/net/ntopng/Makefile +++ b/net/ntopng/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= ntopng -PLUGIN_VERSION= 1.2 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.3 PLUGIN_COMMENT= Traffic Analysis and Flow Collection PLUGIN_DEPENDS= ntopng PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/net/ntopng/pkg-descr b/net/ntopng/pkg-descr index 5ea68cabd8..968b0f6607 100644 --- a/net/ntopng/pkg-descr +++ b/net/ntopng/pkg-descr @@ -8,6 +8,10 @@ and on Windows as well. Plugin Changelog ================ +1.3 + +* Change interface selection to multiselect + 1.2 * Changed data directory to avoid warning diff --git a/net/ntopng/src/opnsense/mvc/app/controllers/OPNsense/Ntopng/forms/general.xml b/net/ntopng/src/opnsense/mvc/app/controllers/OPNsense/Ntopng/forms/general.xml index e417cd9f63..b334d3ad09 100644 --- a/net/ntopng/src/opnsense/mvc/app/controllers/OPNsense/Ntopng/forms/general.xml +++ b/net/ntopng/src/opnsense/mvc/app/controllers/OPNsense/Ntopng/forms/general.xml @@ -8,7 +8,7 @@ general.interface - dropdown + select_multiple true Select the interface to listen to. Set to none if you want to choose the interface via ntopng UI. diff --git a/net/ntopng/src/opnsense/mvc/app/models/OPNsense/Ntopng/General.xml b/net/ntopng/src/opnsense/mvc/app/models/OPNsense/Ntopng/General.xml index 822fc38dc6..580e20651f 100644 --- a/net/ntopng/src/opnsense/mvc/app/models/OPNsense/Ntopng/General.xml +++ b/net/ntopng/src/opnsense/mvc/app/models/OPNsense/Ntopng/General.xml @@ -1,19 +1,20 @@ //OPNsense/ntopng/general ntopng configuration - 0.0.1 + 0.0.2 - 0 + 0 Y N - N + Y + Y Y - 3000 + 3000 N diff --git a/net/ntopng/src/opnsense/service/conf/actions.d/actions_ntopng.conf b/net/ntopng/src/opnsense/service/conf/actions.d/actions_ntopng.conf index 172e64bb7b..2f14f5bc23 100644 --- a/net/ntopng/src/opnsense/service/conf/actions.d/actions_ntopng.conf +++ b/net/ntopng/src/opnsense/service/conf/actions.d/actions_ntopng.conf @@ -1,5 +1,5 @@ [start] -command:/usr/local/opnsense/scripts/OPNsense/Ntopng/setup.sh;/usr/local/etc/rc.d/ntopng start +command:/usr/local/etc/rc.d/ntopng start parameters: type:script message:starting ntopng @@ -11,7 +11,7 @@ type:script message:stopping ntopng [restart] -command:/usr/local/opnsense/scripts/OPNsense/Ntopng/setup.sh;/usr/local/etc/rc.d/ntopng restart +command:/usr/local/etc/rc.d/ntopng restart parameters: type:script message:restarting ntopng diff --git a/net/ntopng/src/opnsense/service/templates/OPNsense/Ntopng/ntopng b/net/ntopng/src/opnsense/service/templates/OPNsense/Ntopng/ntopng index d1077ce1cf..e665022956 100644 --- a/net/ntopng/src/opnsense/service/templates/OPNsense/Ntopng/ntopng +++ b/net/ntopng/src/opnsense/service/templates/OPNsense/Ntopng/ntopng @@ -1,8 +1,7 @@ {% if helpers.exists('OPNsense.ntopng.general.enabled') and OPNsense.ntopng.general.enabled == '1' %} -ntopng_var_script="/usr/local/opnsense/scripts/OPNsense/Ntopng/setup.sh" +ntopng_setup="/usr/local/opnsense/scripts/OPNsense/Ntopng/setup.sh" ntopng_enable="YES" ntopng_flags="/usr/local/etc/ntopng.conf" {% else %} ntopng_enable="NO" {% endif %} -ntopng_var_mfs="/var/db/ntopng" diff --git a/net/ntopng/src/opnsense/service/templates/OPNsense/Ntopng/ntopng.conf b/net/ntopng/src/opnsense/service/templates/OPNsense/Ntopng/ntopng.conf index 47749eeeb4..48def3ea32 100644 --- a/net/ntopng/src/opnsense/service/templates/OPNsense/Ntopng/ntopng.conf +++ b/net/ntopng/src/opnsense/service/templates/OPNsense/Ntopng/ntopng.conf @@ -1,7 +1,9 @@ {% if helpers.exists('OPNsense.ntopng.general.enabled') and OPNsense.ntopng.general.enabled == '1' %} {% from 'OPNsense/Macros/interface.macro' import physical_interface %} {% if helpers.exists('OPNsense.ntopng.general.interface') and OPNsense.ntopng.general.interface != '' %} --i={{ physical_interface(OPNsense.ntopng.general.interface) }} +{% for iface in OPNsense.ntopng.general.interface.split(',') %} +-i={{ physical_interface(iface) }} +{% endfor %} {% endif %} {% if helpers.exists('OPNsense.ntopng.general.httpport') and OPNsense.ntopng.general.httpport != '' %} -w={{ OPNsense.ntopng.general.httpport }} diff --git a/net/radsecproxy/Makefile b/net/radsecproxy/Makefile index 7ee4427db0..bd0443bdc6 100644 --- a/net/radsecproxy/Makefile +++ b/net/radsecproxy/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= radsecproxy -PLUGIN_VERSION= 1.0 +PLUGIN_VERSION= 1.1 PLUGIN_COMMENT= RADIUS proxy provides both RADIUS UDP and TCP/TLS (RadSec) transport PLUGIN_DEPENDS= radsecproxy PLUGIN_MAINTAINER= tobias@boehnert.dev diff --git a/net/radsecproxy/pkg-descr b/net/radsecproxy/pkg-descr index ef872b8a71..b7d0cefc83 100644 --- a/net/radsecproxy/pkg-descr +++ b/net/radsecproxy/pkg-descr @@ -3,3 +3,14 @@ transport, also supports TLS (RadSec), as well as RADIUS over TCP and DTLS. The aim is for the proxy to have sufficient features to be flexible, while at the same time to be small, efficient and easy to configure. + +Plugin Changelog +================ + +1.1 + +* Added AccountingServer options (contributed by Marcel Ritter) + +1.0 + +* Initial release (contributed by Tobias Boehnert) diff --git a/net/radsecproxy/src/etc/inc/plugins.inc.d/radsecproxy.inc b/net/radsecproxy/src/etc/inc/plugins.inc.d/radsecproxy.inc index a614e0bffc..3b4d464b5b 100644 --- a/net/radsecproxy/src/etc/inc/plugins.inc.d/radsecproxy.inc +++ b/net/radsecproxy/src/etc/inc/plugins.inc.d/radsecproxy.inc @@ -1,6 +1,7 @@ general->enabled == '1') { - return true; - } - - return false; + return (string)(new \OPNsense\RadSecProxy\RadSecProxy())->general->enabled == '1'; } function radsecproxy_syslog() { - // $syslogconf = array(); - - // $syslogconf['radsecproxy'] = array( - // 'facility' => array('radsecproxy'), - // ); - - // return $syslogconf; - - $logfacilities = array(); - $logfacilities['radsecproxy'] = array( - 'facility' => array('LOG_DAEMON'), - ); - return $logfacilities; + return [ + 'radsecproxy' => [ + 'facility' => ['radsecproxy'] + ] + ]; } function radsecproxy_services() { - $services = array(); + $services = []; if (radsecproxy_enabled()) { - $services[] = array( + $services[] = [ 'description' => gettext('Radius Secure Proxy'), - 'configd' => array( - 'restart' => array('radsecproxy restart'), - 'start' => array('radsecproxy start'), - 'stop' => array('radsecproxy stop'), - ), + 'configd' => [ + 'restart' => ['radsecproxy restart'], + 'start' => ['radsecproxy start'], + 'stop' => ['radsecproxy stop'], + ], 'name' => 'radsecproxy', 'pidfile' => '/var/run/radsecproxy/radsecproxy.pid' - ); + ]; } return $services; } diff --git a/net/radsecproxy/src/etc/rc.d/os-radsecproxy b/net/radsecproxy/src/etc/rc.d/os-radsecproxy deleted file mode 100755 index cb79588f7e..0000000000 --- a/net/radsecproxy/src/etc/rc.d/os-radsecproxy +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/sh - -# PROVIDE: radsecproxy -# REQUIRE: LOGIN -# KEYWORD: shutdown - -# Add the following line to /etc/rc.conf.local or /etc/rc.conf -# to enable this service: -# -# radsecproxy_enable (bool): Set to NO by default. -# Set it to YES to enable radsecproxy. - -. /etc/rc.subr - -name="radsecproxy" -rcvar=radsecproxy_enable - -: ${radsecproxy_enable:="NO"} -: ${radsecproxy_user:="root"} -: ${radsecproxy_group:="wheel"} -: ${radsecproxy_pidfile:="/var/run/radsecproxy.pid"} - -user=${radsecproxy_user} -group=${radsecproxy_group} -pidfile=${radsecproxy_pidfile} -required_files=/usr/local/etc/radsecproxy.conf - -command="/usr/local/sbin/${name}" -command_args="-c /usr/local/etc/radsecproxy.conf -i ${pidfile}" - -start_precmd="radsecproxy_prestart" -stop_postcmd="radsecproxy_poststop" - -radsecproxy_prestart() -{ - mkdir -p $(dirname $pidfile) - chown ${user}:${group} $(dirname $pidfile) -} - -radsecproxy_poststop() -{ - rm -f ${pidfile} -} - -load_rc_config $name -run_rc_command "$1" diff --git a/net/radsecproxy/src/opnsense/mvc/app/models/OPNsense/RadSecProxy/Menu/Menu.xml b/net/radsecproxy/src/opnsense/mvc/app/models/OPNsense/RadSecProxy/Menu/Menu.xml index 38211fc76c..274409397b 100644 --- a/net/radsecproxy/src/opnsense/mvc/app/models/OPNsense/RadSecProxy/Menu/Menu.xml +++ b/net/radsecproxy/src/opnsense/mvc/app/models/OPNsense/RadSecProxy/Menu/Menu.xml @@ -1,12 +1,13 @@ - - - - - - + + + + + + + diff --git a/net/radsecproxy/src/opnsense/mvc/app/models/OPNsense/RadSecProxy/RadSecProxy.xml b/net/radsecproxy/src/opnsense/mvc/app/models/OPNsense/RadSecProxy/RadSecProxy.xml index 231a2e9ba7..41d446fe56 100644 --- a/net/radsecproxy/src/opnsense/mvc/app/models/OPNsense/RadSecProxy/RadSecProxy.xml +++ b/net/radsecproxy/src/opnsense/mvc/app/models/OPNsense/RadSecProxy/RadSecProxy.xml @@ -6,15 +6,13 @@ 0.0.1 - - 0 + 0 Y - Y - 2 + 2 1 (only serious errors) 2 (default) @@ -23,19 +21,17 @@ 5 (log everything) - Y - off + off On Off - Y - Original + Original Static Original @@ -45,61 +41,48 @@ FullyKeyHashed - Y - on + on On Off - N - N - N - N - N - N - N - N - - - - 1 + 1 Y - Y - /^([0-9a-zA-Z_\-]){1,25}$/u + /^([0-9a-zA-Z_\-]){1,25}$/u Should be a string between 1 and 25 characters whithout special characters. @@ -108,11 +91,9 @@ - N - Y Y @@ -122,10 +103,9 @@ - Y - udp + udp UDP TCP @@ -133,7 +113,6 @@ DTLS - N @@ -151,7 +130,6 @@ - N @@ -162,20 +140,17 @@ - Y - off + off On Off - N - N @@ -186,7 +161,6 @@ - N @@ -197,16 +171,13 @@ - - - Y - /^([0-9a-zA-Z_\-]){1,25}$/u + /^([0-9a-zA-Z_\-]){1,25}$/u Should be a string between 1 and 25 characters whithout special characters. @@ -215,23 +186,19 @@ - N - Y Y - N - Y - off + off On Off @@ -239,10 +206,9 @@ Auto - Y - udp + udp UDP TCP @@ -250,7 +216,6 @@ DTLS - N @@ -268,7 +233,6 @@ - N @@ -279,20 +243,17 @@ - Y - off + off On Off - N - N @@ -303,7 +264,6 @@ - N @@ -314,18 +274,15 @@ - - - Y - /^([0-9a-zA-Z_\-]){1,25}$/u + /^([0-9a-zA-Z_\-]){1,25}$/u Should be a string between 1 and 25 characters whithout special characters. - default + default UniqueConstraint @@ -333,56 +290,45 @@ - N - Y Field is required ca - Y Field is required cert - N - Y + Y - Y - off + off On Off - N - - - - 1 + 1 Y - N - Y Must not be empty @@ -393,7 +339,6 @@ - Y N @@ -407,7 +352,6 @@ Related server not found - Y N @@ -421,36 +365,30 @@ Related server not found - Y - off + off On Off - N - - - - 1 + 1 Y - Y - /^([0-9a-zA-Z_\-]){1,25}$/u + /^([0-9a-zA-Z_\-]){1,25}$/u Should be a string between 1 and 25 characters whithout special characters. - default + default UniqueConstraint @@ -458,56 +396,44 @@ - N - N - N - N - N - N - N - N - Y - off + off On Off - N - N - diff --git a/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/clients.volt b/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/clients.volt index c03c5d45b1..5909db4c47 100644 --- a/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/clients.volt +++ b/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/clients.volt @@ -1,12 +1,12 @@ - - - - - - - - - - - - - - - - - - - - -
    {{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Type') }}{{ lang._('Host') }}{{ lang._('Identifier') }}{{ lang._('Description') }}{{ lang._('Commands') }}
    - - -
    +
    + + + + + + + + + + + + + + + + + + + + +
    {{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Type') }}{{ lang._('Host') }}{{ lang._('Identifier') }}{{ lang._('Description') }}{{ lang._('Commands') }}
    + + +
    -
    - +
    + +
    {{ partial("layout_partials/base_dialog",['fields':formDialogClient,'id':'DialogClient','label':lang._('Edit client')])}} diff --git a/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/realms.volt b/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/realms.volt index 85453ec79d..ddc99d63d7 100644 --- a/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/realms.volt +++ b/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/realms.volt @@ -1,12 +1,12 @@ +
    + + + + + + + + + + + + + + + + + + +
    {{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Realm') }}{{ lang._('Description') }}{{ lang._('Commands') }}
    + + +
    - - - - - - - - - - - - - - - - - - -
    {{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Realm') }}{{ lang._('Description') }}{{ lang._('Commands') }}
    - - -
    - -
    - +
    + +
    - {{ partial("layout_partials/base_dialog",['fields':formDialogRealm,'id':'DialogRealm','label':lang._('Edit realm')])}} diff --git a/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/rewrites.volt b/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/rewrites.volt index 0da6b6612c..ddcd19cd42 100644 --- a/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/rewrites.volt +++ b/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/rewrites.volt @@ -1,12 +1,12 @@ - - - - - - - - - - - - - - - - - - -
    {{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Type') }}{{ lang._('Description') }}{{ lang._('Commands') }}
    - - -
    +
    + + + + + + + + + + + + + + + + + + +
    {{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Type') }}{{ lang._('Description') }}{{ lang._('Commands') }}
    + + +
    -
    - +
    + +
    {{ partial("layout_partials/base_dialog",['fields':formDialogRewrite,'id':'DialogRewrite','label':lang._('Edit rewrite-rule')])}} diff --git a/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/servers.volt b/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/servers.volt index b394b1e5ad..ac13379150 100644 --- a/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/servers.volt +++ b/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/servers.volt @@ -1,12 +1,12 @@ - - - - - - - - - - - - - - - - - - - - -
    {{ lang._('ID') }}{{ lang._('Host') }}{{ lang._('Identifier') }}{{ lang._('Description') }}{{ lang._('Type') }}{{ lang._('TLS-Config') }}{{ lang._('Commands') }}
    - - -
    +
    + + + + + + + + + + + + + + + + + + + + +
    {{ lang._('ID') }}{{ lang._('Host') }}{{ lang._('Identifier') }}{{ lang._('Description') }}{{ lang._('Type') }}{{ lang._('TLS-Config') }}{{ lang._('Commands') }}
    + + +
    -
    - +
    + +
    - {{ partial("layout_partials/base_dialog",['fields':formDialogServer,'id':'DialogServer','label':lang._('Edit server')])}} diff --git a/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/tls.volt b/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/tls.volt index cc63e0c747..4d29c9dddc 100644 --- a/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/tls.volt +++ b/net/radsecproxy/src/opnsense/mvc/app/views/OPNsense/RadSecProxy/tls.volt @@ -1,12 +1,12 @@ - - - - - - - - - - - - - - - - - - - -
    {{ lang._('ID') }}{{ lang._('Name') }}{{ lang._('Description') }}{{ lang._('CA-certificate') }}{{ lang._('Proxy-certificate') }}{{ lang._('Commands') }}
    - - -
    +
    + + + + + + + + + + + + + + + + + + + +
    {{ lang._('ID') }}{{ lang._('Name') }}{{ lang._('Description') }}{{ lang._('CA-certificate') }}{{ lang._('Proxy-certificate') }}{{ lang._('Commands') }}
    + + +
    -
    - +
    + +
    - {{ partial("layout_partials/base_dialog",['fields':formDialogTls,'id':'DialogTls','label':lang._('Edit TLS-config')])}} diff --git a/net/radsecproxy/src/opnsense/scripts/OPNsense/RadSecProxy/setup.sh b/net/radsecproxy/src/opnsense/scripts/OPNsense/RadSecProxy/setup.sh index cd09c51f98..420c0e62f1 100755 --- a/net/radsecproxy/src/opnsense/scripts/OPNsense/RadSecProxy/setup.sh +++ b/net/radsecproxy/src/opnsense/scripts/OPNsense/RadSecProxy/setup.sh @@ -1,10 +1,10 @@ #!/bin/sh -RADSECPROXY_DIRS="/usr/local/etc/radsecproxy.d /usr/local/etc/radsecproxy.d/certs" +RADSECPROXY_DIRS="/usr/local/etc/radsecproxy.d/certs" for directory in ${RADSECPROXY_DIRS}; do mkdir -p ${directory} - chown -R www:www ${directory} + chown -R root:wheel ${directory} chmod -R 750 ${directory} done @@ -12,7 +12,4 @@ done # export required certs to filesystem /usr/local/opnsense/scripts/OPNsense/RadSecProxy/generate_certs.php > /dev/null 2>&1 -# remove logfile - sometimes it will stop radsecproxy from starting -#rm /var/log/radsecproxy.log - exit 0 diff --git a/net/radsecproxy/src/opnsense/service/conf/actions.d/actions_radsecproxy.conf b/net/radsecproxy/src/opnsense/service/conf/actions.d/actions_radsecproxy.conf index 79ca190462..0d60ddcf77 100644 --- a/net/radsecproxy/src/opnsense/service/conf/actions.d/actions_radsecproxy.conf +++ b/net/radsecproxy/src/opnsense/service/conf/actions.d/actions_radsecproxy.conf @@ -1,35 +1,29 @@ -[setup] -command:/usr/local/opnsense/scripts/OPNsense/RadSecProxy/setup.sh; -parameters: -type:script -message:setup radsecproxy service requirements - [start] -command:/usr/local/opnsense/scripts/OPNsense/RadSecProxy/setup.sh;/usr/local/etc/rc.d/radsecproxy start; +command:/usr/local/etc/rc.d/radsecproxy start parameters: type:script message:starting radsecproxy [stop] -command:/usr/local/etc/rc.d/radsecproxy stop; +command:/usr/local/etc/rc.d/radsecproxy stop parameters: type:script message:stopping radsecproxy [restart] -command:/usr/local/opnsense/scripts/OPNsense/RadSecProxy/setup.sh;/usr/local/etc/rc.d/radsecproxy restart; +command:/usr/local/etc/rc.d/radsecproxy restart parameters: type:script message:restarting radsecproxy [reload] -command:/usr/local/opnsense/scripts/OPNsense/RadSecProxy/setup.sh;/usr/local/etc/rc.d/radsecproxy restart; +command:/usr/local/etc/rc.d/radsecproxy restart parameters: type:script message:reloading radsecproxy [status] -command:/usr/local/etc/rc.d/radsecproxy status;exit 0; +command:/usr/local/etc/rc.d/radsecproxy status; exit 0 parameters: type:script_output message:radsecproxy status diff --git a/net/radsecproxy/src/opnsense/service/templates/OPNsense/RadSecProxy/radsecproxy.conf b/net/radsecproxy/src/opnsense/service/templates/OPNsense/RadSecProxy/radsecproxy.conf index bdb62ce381..57ccea9fe5 100644 --- a/net/radsecproxy/src/opnsense/service/templates/OPNsense/RadSecProxy/radsecproxy.conf +++ b/net/radsecproxy/src/opnsense/service/templates/OPNsense/RadSecProxy/radsecproxy.conf @@ -5,8 +5,6 @@ # GENERAL ########################################### -#PidFile /var/run/radsecproxy.pid -#LogDestination file:///var/log/radsecproxy.log LogDestination x-syslog:///LOG_DAEMON {% if OPNsense.radsecproxy.general.logLevel is defined and OPNsense.radsecproxy.general.logLevel != "" %} @@ -223,6 +221,12 @@ realm {{ realm.realm }} { Server {{ server.identifier }} {% endfor %} {% endif %} +{% if realm.accountingServer is defined and realm.accountingServer != "" %} +{% for serverUuid in realm.accountingServer.split(',') %} +{% set accountingServer = helpers.getUUID(serverUuid) %} + AccountingServer {{ accountingServer.identifier }} +{% endfor %} +{% endif %} {% if realm.replyMessage is defined and realm.replyMessage != "" %} ReplyMessage "{{ realm.replyMessage }}" {% endif %} diff --git a/net/radsecproxy/src/opnsense/service/templates/OPNsense/RadSecProxy/rc.conf.d b/net/radsecproxy/src/opnsense/service/templates/OPNsense/RadSecProxy/rc.conf.d index 35042a335c..7f32bf52ab 100644 --- a/net/radsecproxy/src/opnsense/service/templates/OPNsense/RadSecProxy/rc.conf.d +++ b/net/radsecproxy/src/opnsense/service/templates/OPNsense/RadSecProxy/rc.conf.d @@ -1,7 +1,8 @@ -{% if helpers.exists('OPNsense.radsecproxy.general.enabled') and OPNsense.radsecproxy.general.enabled == '1' %} +{% if not helpers.empty('OPNsense.radsecproxy.general.enabled') %} radsecproxy_enable="YES" +radsecproxy_user="root" +radsecproxy_group="wheel" +radsecproxy_setup="/usr/local/opnsense/scripts/OPNsense/RadSecProxy/setup.sh" {% else %} radsecproxy_enable="NO" {% endif %} -radsecproxy_user="root" -radsecproxy_group="wheel" diff --git a/net/radsecproxy/src/opnsense/service/templates/OPNsense/Syslog/local/radsecproxy.conf b/net/radsecproxy/src/opnsense/service/templates/OPNsense/Syslog/local/radsecproxy.conf new file mode 100644 index 0000000000..2133f76f55 --- /dev/null +++ b/net/radsecproxy/src/opnsense/service/templates/OPNsense/Syslog/local/radsecproxy.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [radsecproxy]. +################################################################### +filter f_local_radsecproxy { + program("radsecproxy"); +}; diff --git a/net/relayd/Makefile b/net/relayd/Makefile index e995794309..eac86962db 100644 --- a/net/relayd/Makefile +++ b/net/relayd/Makefile @@ -1,8 +1,9 @@ PLUGIN_NAME= relayd -PLUGIN_VERSION= 2.6 -PLIGIN_REVISION= 1 +PLUGIN_VERSION= 2.9 +PLUGIN_REVISION= 3 PLUGIN_DEPENDS= relayd PLUGIN_COMMENT= Relayd Load Balancer PLUGIN_MAINTAINER= frank.brendel@eurolog.com +PLUGIN_TIER= 2 .include "../../Mk/plugins.mk" diff --git a/net/relayd/src/etc/rc.d/os-relayd b/net/relayd/src/etc/rc.d/os-relayd index 04dade24f7..2bab72bcb9 100755 --- a/net/relayd/src/etc/rc.d/os-relayd +++ b/net/relayd/src/etc/rc.d/os-relayd @@ -1,8 +1,5 @@ #!/bin/sh # -# $FreeBSD$ -# - # PROVIDE: os-relayd # REQUIRE: NETWORKING syslogd # BEFORE: DAEMON diff --git a/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/ServiceController.php b/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/ServiceController.php index 83eda01320..0bffec3e78 100644 --- a/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/ServiceController.php +++ b/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/ServiceController.php @@ -71,7 +71,6 @@ public function configtestAction() { if ($this->request->isPost()) { $result['status'] = 'ok'; - $this->sessionClose(); $backend = new Backend(); @@ -96,7 +95,6 @@ public function reconfigureAction() { if ($this->request->isPost()) { if ($this->lock()) { - $this->sessionClose(); $result['function'] = "reconfigure"; $result['status'] = 'failed'; $backend = new Backend(); diff --git a/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/SettingsController.php b/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/SettingsController.php index 0f71cf62c6..4be8107273 100644 --- a/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/SettingsController.php +++ b/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/SettingsController.php @@ -1,31 +1,30 @@ 'failed', 'validations' => array()); + $result = array('result' => 'failed', 'validations' => []); if ($this->request->isPost() && $this->request->hasPost('relayd') && $nodeType != null) { $this->validateNodeType($nodeType); if ($nodeType == 'general') { @@ -112,93 +110,7 @@ public function setAction($nodeType = null, $uuid = null) } } if ($node != null) { - $relaydInfo = $this->request->getPost('relayd'); - - // perform plugin specific validations - if ($nodeType == 'virtualserver') { - // preset defaults for validations - if (empty($relaydInfo[$nodeType]['type'])) { - $relaydInfo[$nodeType]['type'] = $node->type->__toString(); - } - if (empty($relaydInfo[$nodeType]['transport_tablemode'])) { - $relaydInfo[$nodeType]['transport_tablemode'] = $node->transport_tablemode->__toString(); - } - if (empty($relaydInfo[$nodeType]['backuptransport_tablemode'])) { - $relaydInfo[$nodeType]['backuptransport_tablemode'] = - $node->backuptransport_tablemode->__toString(); - } - - if ($relaydInfo[$nodeType]['type'] == 'redirect') { - if ( - $relaydInfo[$nodeType]['transport_tablemode'] != 'least-states' && - $relaydInfo[$nodeType]['transport_tablemode'] != 'roundrobin' - ) { - $result['validations']['relayd.virtualserver.transport_tablemode'] = sprintf( - gettext('Scheduler "%s" not supported for redirects.'), - $relaydInfo[$nodeType]['transport_tablemode'] - ); - } - if ( - $relaydInfo[$nodeType]['backuptransport_tablemode'] != 'least-states' && - $relaydInfo[$nodeType]['backuptransport_tablemode'] != 'roundrobin' - ) { - $result['validations']['relayd.virtualserver.backuptransport_tablemode'] = sprintf( - gettext('Scheduler "%s" not supported for redirects.'), - $relaydInfo[$nodeType]['backuptransport_tablemode'] - ); - } - if ( - $relaydInfo[$nodeType]['transport_type'] == 'route' && - empty($relaydInfo[$nodeType]['routing_interface']) - ) { - $result['validations']['relayd.virtualserver.routing_interface'] = - gettext('Routing interface cannot be empty'); - } - } - if ($relaydInfo[$nodeType]['type'] == 'relay') { - if ($relaydInfo[$nodeType]['transport_tablemode'] == 'least-states') { - $result['validations']['relayd.virtualserver.transport_tablemode'] = sprintf( - gettext('Scheduler "%s" not supported for relays.'), - $relaydInfo[$nodeType]['transport_tablemode'] - ); - } - if ($relaydInfo[$nodeType]['backuptransport_tablemode'] == 'least-states') { - $result['validations']['relayd.virtualserver.backuptransport_tablemode'] = sprintf( - gettext('Scheduler "%s" not supported for relays.'), - $relaydInfo[$nodeType]['backuptransport_tablemode'] - ); - } - } - } elseif ($nodeType == 'tablecheck') { - switch ($relaydInfo[$nodeType]['type']) { - case 'send': - if (empty($relaydInfo[$nodeType]['expect'])) { - $result['validations']['relayd.tablecheck.expect'] = - gettext('Expect Pattern cannot be empty.'); - } - break; - case 'script': - if (empty($relaydInfo[$nodeType]['path'])) { - $result['validations']['relayd.tablecheck.path'] = - gettext('Script path cannot be empty.'); - } - break; - case 'http': - if (empty($relaydInfo[$nodeType]['path'])) { - $result['validations']['relayd.tablecheck.path'] = - gettext('Path cannot be empty.'); - } - if (empty($relaydInfo[$nodeType]['code']) && empty($relaydInfo[$nodeType]['digest'])) { - $result['validations']['relayd.tablecheck.code'] = - gettext('Provide one of Response Code or Message Digest.'); - $result['validations']['relayd.tablecheck.digest'] = - gettext('Provide one of Response Code or Message Digest.'); - } - break; - } - } - - $node->setNodes($relaydInfo[$nodeType]); + $node->setNodes($this->request->getPost('relayd')[$nodeType]); $valMsgs = $this->getModel()->performValidation(); foreach ($valMsgs as $field => $msg) { $fieldnm = str_replace($node->__reference, "relayd." . $nodeType, $msg->getField()); @@ -232,7 +144,7 @@ public function delAction($nodeType = null, $uuid = null) if ($uuid != null) { $node = $this->getModel()->getNodeByReference($nodeType . '.' . $uuid); if ($node != null) { - $nodeName = $this->getModel()->getNodeByReference($nodeType . '.' . $uuid . '.name')->__toString(); + $nodeName = $this->getModel()->getNodeByReference($nodeType . '.' . $uuid . '.name')->getValue(); if ($this->getModel()->$nodeType->del($uuid) == true) { // delete relations switch ($nodeType) { @@ -311,7 +223,7 @@ public function delAction($nodeType = null, $uuid = null) * @param string $uuid id to toggled * @param string|null $enabled set enabled by default * @return array status - * @throws \Phalcon\Validation\Exception when field validations fail + * @throws \OPNsense\Base\ValidationException when field validations fail * @throws \ReflectionException when not bound to model */ public function toggleAction($nodeType, $uuid, $enabled = null) @@ -327,26 +239,25 @@ public function toggleAction($nodeType, $uuid, $enabled = null) */ public function searchAction($nodeType = null) { - $this->sessionClose(); if ($this->request->isPost() && $nodeType != null) { $this->validateNodeType($nodeType); $grid = new UIModelGrid($this->getModel()->$nodeType); - $fields = array(); + $fields = []; switch ($nodeType) { case 'host': - $fields = array('enabled', 'name', 'address'); + $fields = ['enabled', 'name', 'address']; break; case 'tablecheck': - $fields = array('name', 'type'); + $fields = ['name', 'type']; break; case 'table': - $fields = array('enabled', 'name'); + $fields = ['enabled', 'name']; break; case 'protocol': - $fields = array('name', 'type'); + $fields = ['name', 'type']; break; case 'virtualserver': - $fields = array('enabled', 'name', 'type'); + $fields = ['enabled', 'name', 'type', 'listen_address', 'listen_startport', 'listen_endport']; break; } $result = $grid->fetchBindRequest($this->request, $fields); @@ -391,13 +302,13 @@ private function deleteRelations( if ($fieldUuid == $relUuid) { $refField = $nodeType . '.' . $nodeUuid . '.' . $nodeField; $relNode = $this->getModel()->getNodeByReference($refField); - $nodeRels = str_replace($relUuid, '', $relNode->__toString()); + $nodeRels = str_replace($relUuid, '', $relNode->getValue()); $nodeRels = str_replace(',,', ',', $nodeRels); $nodeRels = rtrim($nodeRels, ','); $nodeRels = ltrim($nodeRels, ','); $this->getModel()->setNodeByReference($refField, $nodeRels); - if ($relNode->isEmptyAndRequired()) { - $nodeName = $this->getModel()->getNodeByReference("{$nodeType}.{$nodeUuid}.name")->__toString(); + if ($relNode->isRequired() && !$relNode->isSet()) { + $nodeName = $this->getModel()->getNodeByReference("{$nodeType}.{$nodeUuid}.name")->getValue(); throw new \Exception("Cannot delete $relNodeType '$relNodeName' from $nodeType '$nodeName'"); } } diff --git a/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/StatusController.php b/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/StatusController.php index 8f10376439..d2a8b0d5de 100644 --- a/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/StatusController.php +++ b/net/relayd/src/opnsense/mvc/app/controllers/OPNsense/Relayd/Api/StatusController.php @@ -47,13 +47,13 @@ class StatusController extends ApiControllerBase */ public function sumAction($wait = 0) { - $result = array("result" => "failed"); + $result = ["result" => "failed"]; $backend = new Backend(); $relaydMdl = new Relayd(); // when $wait is set, try for max 10 seconds to receive a sensible status (wait for unknowns to resolve) $max_tries = !empty($wait) ? 10 : 1; - $output = array(); + $output = []; for ($i = 0; $i < $max_tries; $i++) { $output = explode("\n", trim($backend->configdRun('relayd summary'))); $unknowns = 0; @@ -75,8 +75,8 @@ public function sumAction($wait = 0) $virtualServerId = 0; $virtualServerType = ''; $tableId = 0; - $virtualserver = array(); - $rows = array(); + $virtualserver = []; + $rows = []; foreach ($output as $line) { $words = array_map('trim', explode("\t", $line)); $id = $words[0]; @@ -138,7 +138,7 @@ public function sumAction($wait = 0) $virtualserver['id'] = $id; $virtualserver['type'] = $type; $virtualserver['name'] = $words[2]; - $virtualserver['status'] = $words[4]; + $virtualserver['status'] = $words[4] ?? null; $objs = $relaydMdl->getObjectsByAttribute("virtualserver", "name", $virtualserver['name']); if (count($objs) > 0) { $obj = $objs[0]; @@ -200,9 +200,8 @@ public function sumAction($wait = 0) */ public function toggleAction($nodeType = null, $id = null, $action = null) { - $result = array("result" => "failed", "function" => "toggle"); + $result = ["result" => "failed", "function" => "toggle"]; if ($this->request->isPost()) { - $this->sessionClose(); $backend = new Backend(); if (in_array($nodeType, ['redirect', 'table', 'host']) && in_array($action, ['enable', 'disable'])) { if ($id != null && $id > 0) { @@ -225,7 +224,9 @@ public function toggleAction($nodeType = null, $id = null, $action = null) $relaydMdl->serializeToConfig(); Config::getInstance()->save(); // invoke service controller - return (new ServiceController())->reconfigureAction(); + $srv = new ServiceController(); + $srv->request = $this->request; + return $srv->reconfigureAction(); } } return $result; diff --git a/net/relayd/src/opnsense/mvc/app/models/OPNsense/Relayd/Migrations/M1_0_5.php b/net/relayd/src/opnsense/mvc/app/models/OPNsense/Relayd/Migrations/M1_0_5.php new file mode 100644 index 0000000000..7c4664ae9e --- /dev/null +++ b/net/relayd/src/opnsense/mvc/app/models/OPNsense/Relayd/Migrations/M1_0_5.php @@ -0,0 +1,43 @@ +tablecheck->iterateItems() as $tablecheck) { + if ($tablecheck->type == 'ssl') { + $tablecheck->type = 'tls'; + } + } + } +} diff --git a/net/relayd/src/opnsense/mvc/app/models/OPNsense/Relayd/Relayd.php b/net/relayd/src/opnsense/mvc/app/models/OPNsense/Relayd/Relayd.php index 0669849ed4..f9d33c6254 100644 --- a/net/relayd/src/opnsense/mvc/app/models/OPNsense/Relayd/Relayd.php +++ b/net/relayd/src/opnsense/mvc/app/models/OPNsense/Relayd/Relayd.php @@ -31,6 +31,7 @@ namespace OPNsense\Relayd; use OPNsense\Base\BaseModel; +use OPNsense\Base\Messages\Message; /** * Class Relayd @@ -65,6 +66,107 @@ public function configClean() return @unlink("/tmp/relayd.dirty"); } + /** + * {@inheritdoc} + */ + public function performValidation($validateFullModel = false) + { + $messages = parent::performValidation($validateFullModel); + foreach ($this->virtualserver->iterateItems() as $node) { + if (!$validateFullModel && !$node->isFieldChanged()) { + continue; + } + $key = $node->__reference; + if ($node->type == 'redirect') { + if (!in_array((string)$node->transport_tablemode, ['least-states', 'roundrobin'])) { + $messages->appendMessage( + new Message( + sprintf(gettext('Scheduler "%s" not supported for redirects.'), $node->transport_tablemode), + $key . ".transport_tablemode" + ) + ); + } + if (!in_array((string)$node->backuptransport_tablemode, ['least-states', 'roundrobin'])) { + $messages->appendMessage( + new Message( + sprintf(gettext('Scheduler "%s" not supported for redirects.'), $node->transport_tablemode), + $key . ".backuptransport_tablemode" + ) + ); + } + if ($node->transport_type == 'route' && empty((string)$node->routing_interface)) { + $messages->appendMessage( + new Message(gettext('Routing interface cannot be empty'), $key . ".routing_interface") + ); + } + } elseif ($node->type == 'relay') { + if ($node->transport_tablemode == 'least-states') { + $messages->appendMessage( + new Message( + sprintf(gettext('Scheduler "%s" not supported for relays.'), $node->transport_tablemode), + $key . ".transport_tablemode" + ) + ); + } + if ($node->backuptransport_tablemode == 'least-states') { + $messages->appendMessage( + new Message( + sprintf( + gettext('Scheduler "%s" not supported for relays.'), + $node->backuptransport_tablemode + ), + $key . ".backuptransport_tablemode" + ) + ); + } + } + foreach ($this->tablecheck->iterateItems() as $node) { + if (!$validateFullModel && !$node->isFieldChanged()) { + continue; + } + $key = $node->__reference; + switch ((string)$node->type) { + case 'send': + if (empty((string)$node->expect)) { + $messages->appendMessage( + new Message(gettext('Expect Pattern cannot be empty.'), $key . ".expect") + ); + } + break; + case 'script': + if (empty((string)$node->path)) { + $messages->appendMessage( + new Message(gettext('Script path cannot be empty.'), $key . ".path") + ); + } + break; + case 'http': + if (empty((string)$node->path)) { + $messages->appendMessage( + new Message(gettext('Path cannot be empty.'), $key . ".path") + ); + } + if (empty((string)$node->code) && empty((string)$node->digest)) { + $messages->appendMessage( + new Message( + gettext('Provide one of Response Code or Message Digest.'), + $key . ".code" + ) + ); + $messages->appendMessage( + new Message( + gettext('Provide one of Response Code or Message Digest.'), + $key . ".digest" + ) + ); + } + break; + } + } + } + return $messages; + } + /** * @param string $type type of object (host, table, virtualserver) * @param string $name name of the attribute diff --git a/net/relayd/src/opnsense/mvc/app/models/OPNsense/Relayd/Relayd.xml b/net/relayd/src/opnsense/mvc/app/models/OPNsense/Relayd/Relayd.xml index 985f0e23de..5029e52726 100644 --- a/net/relayd/src/opnsense/mvc/app/models/OPNsense/Relayd/Relayd.xml +++ b/net/relayd/src/opnsense/mvc/app/models/OPNsense/Relayd/Relayd.xml @@ -1,373 +1,359 @@ - //OPNsense/relayd - 1.0.4 - Relayd settings - - - - 0 - Y - - - 10 - N - 1 - Check interval must be greater than 0 - - - N - - new states - all states - - - - 3 - N - 1 - Number of processes must be greater than 0 - - - 200 - N - 1 - The timeout must be greater than 0 - - - - - 1 - Y - - - Y - /^([0-9a-zA-Z\._\- ]){1,255}$/u - Should be a string between 1 and 255 characters. Allowed characters are letters and numbers as well as underscore, minus, dot and space. - - - Host names should be unique. - UniqueConstraint - - - -
    - Y - /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u - lower - Please specify a valid servername or IP address. -
    - - N - 1 - The IP TTL must be greater than 0 - - - N - 1 - The route priority must be greater than 0 - - - N - 1 - The number of retries must be greater than 0 - -
    - - - Y - /^([0-9a-zA-Z\._\- ]){1,255}$/u - Should be a string between 1 and 255 characters. Allowed characters are letters and numbers as well as underscore, minus, dot and space. - - - Table names should be unique. - UniqueConstraint - - - - - 0 - Y - - - - - - Host not found - Y - Y - -
    - - - Y - /^([0-9a-zA-Z\._\- ]){1,255}$/u - Should be a string between 1 and 255 characters. Allowed characters are letters and numbers as well as underscore, minus, dot and space. - - - icmp - Y - - ICMP - TCP - SSL - SEND - - HTTP - - - - N - - - /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u - lower - Please specify a valid servername or IP address. - N - - - N - Expected return code must be a number. - - - N - - - N - - - N - - - N - - - - - Y - /^([0-9a-zA-Z\._\- ]){1,255}$/u - Should be a string between 1 and 255 characters. Allowed characters are letters and numbers as well as underscore, minus, dot and space. - - - Virtual server names should be unique. - UniqueConstraint - - - - - 0 - Y - - - relay - Y - - Relay - Redirection - - - - Y - /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u - lower - Please specify a valid servername or IP address. - - - Y - tcp - - TCP - UDP - - - - Y - 1 - 65535 - A valid Port number must be specified. - - - N - 1 - 65535 - A valid Port number must be specified. - - - N - N - - /^(?!0).*$/ - /^((?!dhcp).)*$/ - - - - forward - Y - - Forward - Route - - - - N - N - - /^(?!0).*$/ - /^((?!dhcp).)*$/ - - - - - - - Table not found - N - Y - - - N - 1 - 65535 - A valid Port number must be specified. - - - N - 2 - Check interval must be a multiple of the global interval. - - - N - 1 - The timeout must be greater than 0 - - - roundrobin - N - - Hash - Least States - Load Balance - Random - Round Robin - Source Hash - - - - - - - Table check not found - N - Y - - - - - - Table not found - N - N - - - N - 2 - Check interval must be a multiple of the global interval. - - - N - 1 - The timeout must be greater than 0 - - - - - - Table check not found - N - N - - - Table check must be set. - DependConstraint - - backuptransport_table - - - - - - roundrobin - N - - Hash - Least States - Load Balance - Random - Round Robin - Source Hash - - - - 600 - N - 1 - 2147483647 - The timeout must be a number between 1 and 2147483647. - - - 0 - N - - - - - - Protocol not found - N - N - --> - - - - Y - /^([0-9a-zA-Z\._\- ]){1,255}$/u - Should be a string between 1 and 255 characters. Allowed characters are letters and numbers as well as underscore, minus, dot and space. - - - tcp - Y - - TCP - DNS - HTTP - - - - N - - -
    + //OPNsense/relayd + 1.0.6 + Relayd settings + + + + 0 + Y + + + N + 1 + Check interval must be greater than 0 + + + N + + new states + all states + + + + N + 1 + Number of processes must be greater than 0 + + + N + 1 + The timeout must be greater than 0 + + + + + 1 + Y + + + Y + /^([0-9a-zA-Z\._\- ]){1,255}$/u + Should be a string between 1 and 255 characters. Allowed characters are letters and numbers as well as underscore, minus, dot and space. + + + Host names should be unique. + UniqueConstraint + + + +
    + Y + /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u + lower + Please specify a valid servername or IP address. +
    + + N + 1 + The IP TTL must be greater than 0 + + + N + 1 + The route priority must be greater than 0 + + + N + 1 + The number of retries must be greater than 0 + +
    + + + Y + /^([0-9a-zA-Z\._\- ]){1,255}$/u + Should be a string between 1 and 255 characters. Allowed characters are letters and numbers as well as underscore, minus, dot and space. + + + Table names should be unique. + UniqueConstraint + + + + + 1 + Y + + + + + + Host not found + Y + Y + +
    + + + Y + /^([0-9a-zA-Z\._\- ]){1,255}$/u + Should be a string between 1 and 255 characters. Allowed characters are letters and numbers as well as underscore, minus, dot and space. + + + icmp + Y + + ICMP + TCP + TLS + SEND + + HTTP + + + + N + + + /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u + lower + Please specify a valid servername or IP address. + N + + + N + Expected return code must be a number. + + + N + + + N + + + N + + + N + + + + + Y + /^([0-9a-zA-Z\._\- ]){1,31}$/u + Should be a string between 1 and 31 characters. Allowed characters are letters and numbers as well as underscore, minus, dot and space. + + + Virtual server names should be unique. + UniqueConstraint + + + + + 1 + Y + + + relay + Y + + Relay + Redirection + + + + Y + /^([0-9a-zA-Z\.,_\-:]){0,1024}$/u + lower + Please specify a valid servername or IP address. + + + Y + tcp + + TCP + UDP + + + + Y + 1 + 65535 + A valid Port number must be specified. + + + N + 1 + 65535 + A valid Port number must be specified. + + + N + + /^(?!0).*$/ + /^((?!dhcp).)*$/ + + + + forward + Y + + Forward + Route + + + + N + + /^(?!0).*$/ + /^((?!dhcp).)*$/ + + + + + + + Table not found + Y + + + N + 1 + 65535 + A valid Port number must be specified. + + + N + 2 + Check interval must be a multiple of the global interval. + + + N + 1 + The timeout must be greater than 0 + + + Y + roundrobin + + Hash + Least States + Load Balance + Random + Round Robin + Source Hash + + + + + + + Table check not found + Y + + + + + + Table not found + N + + + N + 2 + Check interval must be a multiple of the global interval. + + + N + 1 + The timeout must be greater than 0 + + + + + + Table check not found + N + + + Table check must be set. + DependConstraint + + backuptransport_table + + + + + + Y + roundrobin + + Hash + Least States + Load Balance + Random + Round Robin + Source Hash + + + + N + 1 + 2147483647 + The timeout must be a number between 1 and 2147483647. + + + + + + + Protocol not found + N + + + + + Y + /^([0-9a-zA-Z\._\- ]){1,255}$/u + Should be a string between 1 and 255 characters. Allowed characters are letters and numbers as well as underscore, minus, dot and space. + + + tcp + Y + + TCP + DNS + HTTP + + + + N + + +
    diff --git a/net/relayd/src/opnsense/mvc/app/views/OPNsense/Relayd/index.volt b/net/relayd/src/opnsense/mvc/app/views/OPNsense/Relayd/index.volt index 71cb811011..98e6cff5b5 100644 --- a/net/relayd/src/opnsense/mvc/app/views/OPNsense/Relayd/index.volt +++ b/net/relayd/src/opnsense/mvc/app/views/OPNsense/Relayd/index.volt @@ -1,31 +1,29 @@ {# - -Copyright © 2018 by EURO-LOG AG -Copyright (c) 2021 Deciso B.V. -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. - -#} + # Copyright (c) 2018 EURO-LOG AG + # Copyright (c) 2021 Deciso B.V. + # 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. + #} diff --git a/net/sslh/src/opnsense/service/conf/actions.d/actions_sslh.conf b/net/sslh/src/opnsense/service/conf/actions.d/actions_sslh.conf new file mode 100644 index 0000000000..611a1fa408 --- /dev/null +++ b/net/sslh/src/opnsense/service/conf/actions.d/actions_sslh.conf @@ -0,0 +1,30 @@ +################################################################################ +# Service Actions # +################################################################################ + +[status] +command:/usr/local/etc/rc.d/sslh status; exit 0 +parameters: +type:script_output +message: sslh: requesting status + +[start] +command:/usr/local/etc/rc.d/sslh start +parameters: +type:script +message: sslh: starting +description: sslh: Start service + +[stop] +command:/usr/local/etc/rc.d/sslh stop +parameters: +type:script +message: sslh: stopping +description: sslh: Stop service + +[restart] +command:/usr/local/etc/rc.d/sslh restart +parameters: +type:script +message: sslh: restarting +description: sslh: Restart service diff --git a/net/sslh/src/opnsense/service/templates/OPNsense/Sslh/+TARGETS b/net/sslh/src/opnsense/service/templates/OPNsense/Sslh/+TARGETS new file mode 100644 index 0000000000..14554864c4 --- /dev/null +++ b/net/sslh/src/opnsense/service/templates/OPNsense/Sslh/+TARGETS @@ -0,0 +1,2 @@ +sslh.jinja:/etc/rc.conf.d/sslh +sslh.conf.jinja:/usr/local/etc/sslh.conf diff --git a/net/sslh/src/opnsense/service/templates/OPNsense/Sslh/sslh.conf.jinja b/net/sslh/src/opnsense/service/templates/OPNsense/Sslh/sslh.conf.jinja new file mode 100644 index 0000000000..eb0c7378a9 --- /dev/null +++ b/net/sslh/src/opnsense/service/templates/OPNsense/Sslh/sslh.conf.jinja @@ -0,0 +1,120 @@ +{# + # Copyright (c) 2014-2018 Deciso B.V. + # Copyright (c) 2022 agh1467 + # 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. + #} + +{#- /usr/local/etc/sslh.conf -#} +{% set plugin_name = 'sslh' %} +{# Prevent configd reload from erroring out if node doesn't exist #} +{% if OPNsense[plugin_name] is defined %} +{% set cfg = OPNsense[plugin_name] %} +################################################################################ +# # +# sslh configuration file for v1.21c # +# # +################################################################################ + +{# ------------ Static options, should remain unchanged by the user. -------- #} +{# standard location for pid files #} +pidfile:"/var/run/sslh.pid"; +{# Run as nobody for security reasons. #} +user: "nobody"; + +{# ------------ Dynamic options, user configurable. -------------------------- #} +{% if cfg['verbose'] is defined %} +verbose:{{ '1' if (cfg['verbose'] == '1') else '0' }}; +{% endif -%} + +{% if cfg['numeric'] is defined %} +numeric:{{ 'true' if (cfg['numeric'] == '1') else 'false' }}; +{% endif -%} + +{% if cfg['timeout'] is defined %} +timeout:{{ cfg['timeout'] }}; +{% endif -%} + +{% if cfg['on_timeout'] is defined %} +on_timeout:"{{ cfg['on_timeout'] }}"; +{% endif -%} + +{% if cfg['listen_addresses'] is defined %} +{% set listen_list = [] %} +{# # listen_addresses is comma delimited string, split() to iterate through. #} +{% for for_listen in cfg['listen_addresses'].split(',') %} +{# # Need to further split the listen address by hostname:port #} +{% set listen_hostname, separator, listen_port = for_listen.rpartition(':') %} +{% if listen_hostname != '' and listen_port != '' %} +{% do listen_list.append(' { host: "'~listen_hostname~'"; port: "'~listen_port~'" }') %} +{% endif %} +{% endfor %} +{% if listen_list != [] -%}{# Don't put this setting unless we have listen addreses to put. #} +listen: +( +{{ listen_list|join(",\n") }} +); +{% endif %} +{% endif %} + +{# All of the protocols #} +protocols: +( +{% if cfg['ssh_target'] is defined %} +{% set hostname, separator, port = cfg['ssh_target'].rpartition(':') %} +{% if hostname != '' and port != '' %} + { name: "ssh"; service: "ssh"; host: "{{ hostname }}"; port: "{{ port }}"; }, +{% endif %} +{% endif %} +{% if cfg['openvpn_target'] is defined %} +{% set hostname, separator, port = cfg['openvpn_target'].rpartition(':') %} +{% if hostname != '' and port != '' %} + { name: "openvpn"; host: "{{ hostname }}"; port: "{{ port }}"; }, +{% endif %} +{% endif %} +{% if cfg['xmpp_target'] is defined %} +{% set hostname, separator, port = cfg['xmpp_target'].rpartition(':') %} +{% if hostname != '' and port != '' %} + { name: "xmpp"; host: "{{ hostname }}"; port: "{{ port }}"; }, +{% endif %} +{% endif %} +{% if cfg['http_target'] is defined %} +{% set hostname, separator, port = cfg['http_target'].rpartition(':') %} +{% if hostname != '' and port != '' %} + { name: "http"; host: "{{ hostname }}"; port: "{{ port }}"; }, +{% endif %} +{% endif %} +{% if cfg['tls_target'] is defined %} +{% set hostname, separator, port = cfg['tls_target'].rpartition(':') %} +{% if hostname != '' and port != '' %} + { name: "tls"; host: "{{ hostname }}"; port: "{{ port }}"; }, +{% endif %} +{% endif %} +{% if cfg['anyprot_target'] is defined %} +{% set hostname, separator, port = cfg['anyprot_target'].rpartition(':') %} +{% if hostname != '' and port != '' %} + { name: "anyprot"; host: "{{ hostname }}"; port: "{{ port }}"; }, +{% endif %} +{% endif %} +); +{% endif %} diff --git a/net/sslh/src/opnsense/service/templates/OPNsense/Sslh/sslh.jinja b/net/sslh/src/opnsense/service/templates/OPNsense/Sslh/sslh.jinja new file mode 100644 index 0000000000..cc858aab1a --- /dev/null +++ b/net/sslh/src/opnsense/service/templates/OPNsense/Sslh/sslh.jinja @@ -0,0 +1,11 @@ +{# /usr/local/etc/rc.conf.d/sslh #} +{% set plugin_name = 'sslh' %} +{% if OPNsense[plugin_name] is defined %}{# Prevent configd reload from erroring out if node doesn't exist #} +{% set cfg = OPNsense[plugin_name] %} +{% if cfg['enabled'] is defined %} +sslh_enable={{ '"YES"' if (cfg['enabled'] == '1') else '"NO"' }} +{% endif %} +{% if cfg['mode'] is defined %} +sslh_mode={{ '"fork"' if (cfg['mode'] == 'fork') else '"select"' }} +{% endif %} +{% endif %} diff --git a/net/tayga/Makefile b/net/tayga/Makefile index e350d4580b..e6f3f582b4 100644 --- a/net/tayga/Makefile +++ b/net/tayga/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= tayga -PLUGIN_VERSION= 1.1 -PLUGIN_REVISION= 2 +PLUGIN_VERSION= 1.4 PLUGIN_COMMENT= Tayga NAT64 PLUGIN_DEPENDS= tayga PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/net/tayga/pkg-descr b/net/tayga/pkg-descr index 0ba5d319c5..1e354df0b0 100644 --- a/net/tayga/pkg-descr +++ b/net/tayga/pkg-descr @@ -7,6 +7,18 @@ networks where dedicated NAT64 hardware would be overkill. Plugin Changelog ================ +1.4 + +* Enable forwarding of UDP packets with zero checksum (contributed by Maurice Walker) + +1.3 + +* Static mapping support (contributed by Matthias Valvekens) + +1.2 + +* Custom IPv6 routing option + 1.1 * Register Tayga virtual interface diff --git a/net/tayga/src/etc/inc/plugins.inc.d/tayga.inc b/net/tayga/src/etc/inc/plugins.inc.d/tayga.inc index e273043341..8efe00f868 100644 --- a/net/tayga/src/etc/inc/plugins.inc.d/tayga.inc +++ b/net/tayga/src/etc/inc/plugins.inc.d/tayga.inc @@ -70,9 +70,9 @@ function tayga_interfaces() return $interfaces; } $oic = array('enable' => true); - $oic['if'] = 'nat64'; + $oic['if'] = 'tayga'; $oic['descr'] = 'Tayga'; - $oic['type'] = 'none'; + $oic['type'] = 'group'; $oic['virtual'] = true; $oic['networks'] = array(); $interfaces['tayga'] = $oic; diff --git a/net/tayga/src/etc/rc.d/opnsense-tayga b/net/tayga/src/etc/rc.d/opnsense-tayga index d86a7dca3e..1b4be79c94 100755 --- a/net/tayga/src/etc/rc.d/opnsense-tayga +++ b/net/tayga/src/etc/rc.d/opnsense-tayga @@ -1,11 +1,8 @@ #!/bin/sh # -# $FreeBSD$ -# # PROVIDE: opnsense-tayga # REQUIRE: SERVERS # KEYWORD: shutdown -# . /etc/rc.subr @@ -29,8 +26,11 @@ tayga_start() sleep 1 ifconfig nat64 inet ${tayga_v4destination}/32 ${tayga_v4address} ifconfig nat64 inet6 ${tayga_v6destination}/128 - route -6 add ${tayga_v6prefix} -interface nat64 + ifconfig nat64 group tayga route -4 add ${tayga_v4pool} -interface nat64 + if [ "$tayga_v6routedisabled" != "YES" ]; then + route -6 add ${tayga_v6prefix} -interface nat64 + fi } tayga_stop() diff --git a/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/Api/MappingController.php b/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/Api/MappingController.php new file mode 100644 index 0000000000..05b88d7270 --- /dev/null +++ b/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/Api/MappingController.php @@ -0,0 +1,67 @@ + + 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. +*/ + +namespace OPNsense\Tayga\Api; + +use OPNsense\Base\ApiMutableModelControllerBase; + +class MappingController extends ApiMutableModelControllerBase +{ + protected static $internalModelName = 'staticmapping'; + protected static $internalModelClass = '\OPNsense\Tayga\StaticMapping'; + + public function searchStaticmappingAction() + { + return $this->searchBase('staticmappings.staticmapping', ['enabled', 'v4', 'v6']); + } + + public function getStaticmappingAction($uuid = null) + { + return $this->getBase('staticmapping', 'staticmappings.staticmapping', $uuid); + } + + public function addStaticmappingAction() + { + return $this->addBase('staticmapping', 'staticmappings.staticmapping'); + } + + public function delStaticmappingAction($uuid) + { + return $this->delBase('staticmappings.staticmapping', $uuid); + } + + public function setStaticmappingAction($uuid) + { + return $this->setBase('staticmapping', 'staticmappings.staticmapping', $uuid); + } + + public function toggleStaticmappingAction($uuid) + { + return $this->toggleBase('staticmappings.staticmapping', $uuid); + } +} diff --git a/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/Api/ServiceController.php b/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/Api/ServiceController.php index 4c82981cef..54288658cf 100644 --- a/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/Api/ServiceController.php +++ b/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/Api/ServiceController.php @@ -36,4 +36,13 @@ class ServiceController extends ApiMutableServiceControllerBase protected static $internalServiceTemplate = 'OPNsense/Tayga'; protected static $internalServiceEnabled = 'enabled'; protected static $internalServiceName = 'tayga'; + + /** + * hook group interface registration on reconfigure + * @return bool + */ + protected function invokeInterfaceRegistration() + { + return true; + } } diff --git a/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/GeneralController.php b/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/GeneralController.php index 6cf94562b2..667f30cc9d 100644 --- a/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/GeneralController.php +++ b/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/GeneralController.php @@ -34,5 +34,7 @@ public function indexAction() { $this->view->generalForm = $this->getForm("general"); $this->view->pick('OPNsense/Tayga/general'); + $this->view->formDialogEditStaticMapping = $this->getForm("dialogEditStaticMapping"); + $this->view->formGridStaticMapping = $this->getFormGrid("dialogEditStaticMapping"); } } diff --git a/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/forms/dialogEditStaticMapping.xml b/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/forms/dialogEditStaticMapping.xml new file mode 100644 index 0000000000..e1402cbf33 --- /dev/null +++ b/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/forms/dialogEditStaticMapping.xml @@ -0,0 +1,34 @@ +
    + + staticmapping.enabled + + checkbox + This will enable or disable the static mapping + + 6em + boolean + rowtoggle + + + + staticmapping.v4 + + text + IPv4 network to map. Can overlap with dynamic pool. + + + staticmapping.v6 + + text + IPv6 network to map. Must not overlap with NAT64 prefix. + + + staticmapping.description + + text + Optionally describe the purpose of the mapping. + + false + + +
    diff --git a/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/forms/general.xml b/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/forms/general.xml index 14a6c52724..db60e2fc7e 100644 --- a/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/forms/general.xml +++ b/net/tayga/src/opnsense/mvc/app/controllers/OPNsense/Tayga/forms/general.xml @@ -41,4 +41,10 @@ text IPv6 hosts which send traffic through Tayga will be dynamically assigned an IPv4 address from this pool. Can be any size, but each IPv6 host requires one address. + + general.v6routedisabled + + checkbox + This is an advanced setting for selective routing scenarios. It will prevent installing the route which routes the IPv6 Prefix to Tayga. This requires assigning and locking the nat64 interface, enabling dynamic gateway policy, configuring a dynamic IPv6 gateway and adding custom routes. + diff --git a/net/tayga/src/opnsense/mvc/app/models/OPNsense/Tayga/General.xml b/net/tayga/src/opnsense/mvc/app/models/OPNsense/Tayga/General.xml index 63680a3dbb..393717aedf 100644 --- a/net/tayga/src/opnsense/mvc/app/models/OPNsense/Tayga/General.xml +++ b/net/tayga/src/opnsense/mvc/app/models/OPNsense/Tayga/General.xml @@ -1,34 +1,43 @@ //OPNsense/tayga/general Tayga configuration - 0.0.4 + 1.3.0 - 0 + 0 Y - 192.168.255.1 + 192.168.255.1 Y + ipv4 - 192.168.254.1 + 192.168.254.1 Y + ipv4 - N + ipv6 - 2001:db8:1:ffff::1 + 2001:db8:1:ffff::1 + ipv6 Y - 64:ff9b::/96 + 64:ff9b::/96 + ipv6 Y - 192.168.255.0/24 + 192.168.255.0/24 + ipv4 Y + + 0 + Y + diff --git a/net/tayga/src/opnsense/mvc/app/models/OPNsense/Tayga/StaticMapping.php b/net/tayga/src/opnsense/mvc/app/models/OPNsense/Tayga/StaticMapping.php new file mode 100644 index 0000000000..be26d28b0e --- /dev/null +++ b/net/tayga/src/opnsense/mvc/app/models/OPNsense/Tayga/StaticMapping.php @@ -0,0 +1,35 @@ + + 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. +*/ + +namespace OPNsense\Tayga; + +use OPNsense\Base\BaseModel; + +class StaticMapping extends BaseModel +{ +} diff --git a/net/tayga/src/opnsense/mvc/app/models/OPNsense/Tayga/StaticMapping.xml b/net/tayga/src/opnsense/mvc/app/models/OPNsense/Tayga/StaticMapping.xml new file mode 100644 index 0000000000..ad7429652e --- /dev/null +++ b/net/tayga/src/opnsense/mvc/app/models/OPNsense/Tayga/StaticMapping.xml @@ -0,0 +1,24 @@ + + //OPNsense/tayga/staticmapping + Static NAT64 mappings + 1.3.0 + + + + + Y + 1 + + + Y + ipv4 + + + Y + ipv6 + + + + + + diff --git a/net/tayga/src/opnsense/mvc/app/views/OPNsense/Tayga/general.volt b/net/tayga/src/opnsense/mvc/app/views/OPNsense/Tayga/general.volt index bded11ae61..0732903f5b 100644 --- a/net/tayga/src/opnsense/mvc/app/views/OPNsense/Tayga/general.volt +++ b/net/tayga/src/opnsense/mvc/app/views/OPNsense/Tayga/general.volt @@ -26,13 +26,23 @@ 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':generalForm,'id':'frm_general_settings'])}} -
    -
    - + + + + +
    +
    + {{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_general_settings'])}} +
    +
    + {{ partial('layout_partials/base_bootgrid_table', formGridStaticMapping) }}
    +{{ partial("layout_partials/base_dialog",['fields':formDialogEditStaticMapping,'id': formGridStaticMapping['edit_dialog_id'], 'label':lang._('Edit mapping')])}} +{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/tayga/service/reconfigure', 'data_service_widget': 'tayga'}) }} diff --git a/net/tayga/src/opnsense/service/conf/actions.d/actions_tayga.conf b/net/tayga/src/opnsense/service/conf/actions.d/actions_tayga.conf index 5d4cf502ac..527e918577 100644 --- a/net/tayga/src/opnsense/service/conf/actions.d/actions_tayga.conf +++ b/net/tayga/src/opnsense/service/conf/actions.d/actions_tayga.conf @@ -1,17 +1,20 @@ -[stop] -command:/usr/local/etc/rc.d/opnsense-tayga stop +[start] +command:/usr/local/etc/rc.d/opnsense-tayga start; /usr/local/etc/rc.routing_configure parameters: type:script_output -message:stopping tayga +message:starting tayga -[start] -command:/usr/local/opnsense/scripts/OPNsense/Tayga/setup.sh; /usr/local/etc/rc.d/opnsense-tayga start +[stop] +command:/usr/local/etc/rc.d/opnsense-tayga stop parameters: type:script_output -message:starting tayga +message:stopping tayga [restart] -command:/usr/local/etc/rc.d/opnsense-tayga stop; /usr/local/opnsense/scripts/OPNsense/Tayga/setup.sh; /usr/local/etc/rc.d/opnsense-tayga start +command: + /usr/local/etc/rc.d/opnsense-tayga stop; + /usr/local/etc/rc.d/opnsense-tayga start; + /usr/local/etc/rc.routing_configure parameters: type:script_output message:restarting tayga diff --git a/net/tayga/src/opnsense/service/templates/OPNsense/Tayga/tayga b/net/tayga/src/opnsense/service/templates/OPNsense/Tayga/tayga index c84c7881d7..0d740d98c5 100644 --- a/net/tayga/src/opnsense/service/templates/OPNsense/Tayga/tayga +++ b/net/tayga/src/opnsense/service/templates/OPNsense/Tayga/tayga @@ -1,5 +1,5 @@ {% if helpers.exists('OPNsense.tayga.general.enabled') and OPNsense.tayga.general.enabled == '1' %} -tayga_var_script="/usr/local/opnsense/scripts/OPNsense/Tayga/setup.sh" +tayga_script="/usr/local/opnsense/scripts/OPNsense/Tayga/setup.sh" tayga_enable="YES" tayga_v4address={{ OPNsense.tayga.general.v4address }} tayga_v4destination={{ OPNsense.tayga.general.v4destination }} @@ -7,6 +7,11 @@ tayga_v4pool={{ OPNsense.tayga.general.v4pool }} tayga_v6prefix={{ OPNsense.tayga.general.v6prefix }} tayga_v6address={{ OPNsense.tayga.general.v6address }} tayga_v6destination={{ OPNsense.tayga.general.v6destination }} +{% if helpers.exists('OPNsense.tayga.general.v6routedisabled') and OPNsense.tayga.general.v6routedisabled == '1' %} +tayga_v6routedisabled="YES" +{% else %} +tayga_v6routedisabled="NO" +{% endif %} {% else %} tayga_enable="NO" {% endif %} diff --git a/net/tayga/src/opnsense/service/templates/OPNsense/Tayga/tayga.conf b/net/tayga/src/opnsense/service/templates/OPNsense/Tayga/tayga.conf index 2c8a3bd580..6441f1f9c7 100644 --- a/net/tayga/src/opnsense/service/templates/OPNsense/Tayga/tayga.conf +++ b/net/tayga/src/opnsense/service/templates/OPNsense/Tayga/tayga.conf @@ -2,6 +2,7 @@ tun-device nat64 data-dir /var/db/tayga +udp-cksum-mode fwd ipv4-addr {{ OPNsense.tayga.general.v4address }} {% if helpers.exists('OPNsense.tayga.general.v6address') and OPNsense.tayga.general.v6address != '' %} @@ -10,4 +11,12 @@ ipv6-addr {{ OPNsense.tayga.general.v6address }} prefix {{ OPNsense.tayga.general.v6prefix }} dynamic-pool {{ OPNsense.tayga.general.v4pool }} +{% if helpers.exists('OPNsense.tayga.staticmapping.staticmappings.staticmapping') %} +{% for mapping in helpers.toList('OPNsense.tayga.staticmapping.staticmappings.staticmapping') %} +{% if mapping.enabled == '1' %} +map {{ mapping.v4 }} {{ mapping.v6 }} +{% endif %} +{% endfor %} +{% endif %} + {% endif %} diff --git a/net/turnserver/Makefile b/net/turnserver/Makefile new file mode 100644 index 0000000000..9fbad5318f --- /dev/null +++ b/net/turnserver/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= turnserver +PLUGIN_VERSION= 1.1 +PLUGIN_COMMENT= The coturn STUN/TURN Server +PLUGIN_DEPENDS= turnserver +PLUGIN_MAINTAINER= opnsense@moov.de + +.include "../../Mk/plugins.mk" diff --git a/net/turnserver/pkg-descr b/net/turnserver/pkg-descr new file mode 100644 index 0000000000..3b816781b2 --- /dev/null +++ b/net/turnserver/pkg-descr @@ -0,0 +1,23 @@ +Coturn is a free open source implementation of TURN and STUN Server. +The TURN Server is a VoIP media traffic NAT traversal server and gateway. + +WWW: https://github.com/coturn/coturn + +Plugin Changelog +================ + +1.1 + +Added: +* add log page + +Changed: +* hide protocol violating options +* switch to local syslog logging + +Removed: +* remove old log files + +1.0 + +* Initial release diff --git a/net/turnserver/src/etc/inc/plugins.inc.d/turnserver.inc b/net/turnserver/src/etc/inc/plugins.inc.d/turnserver.inc new file mode 100644 index 0000000000..26757713ba --- /dev/null +++ b/net/turnserver/src/etc/inc/plugins.inc.d/turnserver.inc @@ -0,0 +1,71 @@ + gettext('coturn STUN/TURN Server'), + 'pidfile' => '/var/run/turnserver.pid', + 'configd' => array( + 'restart' => array('turnserver restart'), + 'start' => array('turnserver start'), + 'stop' => array('turnserver stop'), + ), + 'name' => 'turnserver', + ); + + return $services; +} + +function turnserver_xmlrpc_sync() +{ + $result = array(); + $result['id'] = 'turnserver'; + $result['section'] = 'OPNsense.turnserver'; + $result['description'] = gettext('coturn STUN/TURN Server'); + $result['services'] = ['turnserver']; + return array($result); +} diff --git a/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/Api/ServiceController.php b/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/Api/ServiceController.php new file mode 100644 index 0000000000..3973ff2e60 --- /dev/null +++ b/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/Api/ServiceController.php @@ -0,0 +1,42 @@ +view->pick('OPNsense/Turnserver/index'); + // fetch form data + $this->view->settingsForm = $this->getForm("settings"); + } +} diff --git a/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/forms/settings.xml b/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/forms/settings.xml new file mode 100644 index 0000000000..f16808f18c --- /dev/null +++ b/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/forms/settings.xml @@ -0,0 +1,129 @@ +
    + + + header + + + turnserver.settings.Enabled + + checkbox + Enable the Turnserver service + + + turnserver.settings.ListenIP + + + select_multiple + true + + + + turnserver.settings.ListenPort + + text + TURN listener port for UDP and TCP (Default: 3478). NOTE: Do NOT set this to 80 or 443 when listening on all IPs, this may block access to the OPNsense WebUI. + + + turnserver.settings.MinPort + + text + Lower bound of the UDP relay endpoints (Default: 49152). + + + turnserver.settings.MaxPort + + text + Upper bound of the UDP relay endpoints (Default: 65535). + + + + header + + + turnserver.settings.TlsEnabled + + checkbox + Enable TLS/DTLS support. This requires a valid TLS certificate. + + + turnserver.settings.TlsCertificate + + dropdown + + Select a valid TLS certificate. + + + turnserver.settings.TlsPort + + text + TURN listener port for TLS (Default: 5349). NOTE: Do NOT set this to 80 or 443 when listening on all IPs, this may block access to the OPNsense WebUI. + + + + header + + + turnserver.settings.UseAuthSecret + + checkbox + This sets a special authorization option that is based upon authentication secret. Enables TURN REST API. + + + turnserver.settings.StaticAuthSecret + + password + The authentication secret value for TURN REST API. It is recommended to use a long random string, at least 32 characters long. + + + + header + + + turnserver.settings.Realm + + text + The default realm to be used for the users. Must be used with TURN REST API. A good choice may be the domain name of the company. + + + turnserver.settings.FingerprintsEnabled + + checkbox + Use fingerprints in the TURN messages. + + + + header + + + turnserver.settings.UserQuota + + text + Per-user allocation quota. Default value is 0 (no quota, unlimited number of sessions per user). + + + turnserver.settings.TotalQuota + + text + Total allocation quota. Default value is 0 (no quota). + + + turnserver.settings.StaleNonce + + text + Limit the nonce lifetime (in seconds) for extra security. Default value is 600 secs (10 minutes). + + + turnserver.settings.ChannelLifetime + + text + The lifetime for the channel in seconds. Default value is 600 seconds (10 minutes). Changing this value violates RFC 5766. Use with care. + true + + + turnserver.settings.PermissionLifetime + + text + The permission lifetime in seconds. Default value is 300 seconds (5 minutes). Changing this value violates RFC 5766. Use with care. + true + +
    diff --git a/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/ACL/ACL.xml b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/ACL/ACL.xml new file mode 100644 index 0000000000..4e4cb549ab --- /dev/null +++ b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/ACL/ACL.xml @@ -0,0 +1,11 @@ + + + Services: Turnserver + + ui/turnserver + api/turnserver/* + ui/diagnostics/log/core/turnserver/* + api/diagnostics/log/core/turnserver/* + + + diff --git a/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Menu/Menu.xml b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Menu/Menu.xml new file mode 100644 index 0000000000..a52806dd54 --- /dev/null +++ b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Menu/Menu.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.php b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.php new file mode 100644 index 0000000000..3d004cb77d --- /dev/null +++ b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.php @@ -0,0 +1,53 @@ +settings->enabled === "1") { + return true; + } + return false; + } +} diff --git a/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.xml b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.xml new file mode 100644 index 0000000000..47aae5f065 --- /dev/null +++ b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.xml @@ -0,0 +1,90 @@ + + //OPNsense/turnserver + 1.0.0 + The coturn STUN/TURN Server + + + + 0 + Y + + + 127.0.0.1 + Y + Y + + + 3478 + Y + + + 49152 + Y + + + 65535 + Y + + + 0 + Y + + + + 5349 + Y + + + 1 + Y + + + /^.{16,128}$/u + Should be a string between 16 and 128 characters. + + + /^.{1,128}$/u + Should be a string between 1 and 128 characters. + + + 1 + Y + + + 0 + 0 + 1000000000 + Please specify a value between 0 and 1000000000. + Y + + + 0 + 0 + 1000000000 + Please specify a value between 0 and 1000000000. + Y + + + 600 + 1 + 1000000000 + Please specify a value between 1 and 1000000000. + Y + + + 600 + 1 + 1000000000 + Please specify a value between 1 and 1000000000. + Y + + + 300 + 1 + 1000000000 + Please specify a value between 1 and 1000000000. + Y + + + + diff --git a/net/turnserver/src/opnsense/mvc/app/views/OPNsense/Turnserver/index.volt b/net/turnserver/src/opnsense/mvc/app/views/OPNsense/Turnserver/index.volt new file mode 100644 index 0000000000..950b9d7685 --- /dev/null +++ b/net/turnserver/src/opnsense/mvc/app/views/OPNsense/Turnserver/index.volt @@ -0,0 +1,57 @@ +{# + +Copyright (C) 2025 Frank Wall +OPNsense® is Copyright © 2014 – 2015 by Deciso B.V. +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':settingsForm,'id':'frm_Settings'])}} +
    + +
    + +
    diff --git a/net/turnserver/src/opnsense/scripts/OPNsense/Turnserver/export_certs.php b/net/turnserver/src/opnsense/scripts/OPNsense/Turnserver/export_certs.php new file mode 100755 index 0000000000..cbacca2b13 --- /dev/null +++ b/net/turnserver/src/opnsense/scripts/OPNsense/Turnserver/export_certs.php @@ -0,0 +1,70 @@ +#!/usr/local/bin/php +object(); +if (isset($configObj->OPNsense->turnserver->settings->TlsCertificate) and !empty((string)$configObj->OPNsense->turnserver->settings->TlsCertificate)) { + $cert_refid = (string)$configObj->OPNsense->turnserver->settings->TlsCertificate; + foreach ((new Cert())->cert->iterateItems() as $cert) { + $refid = (string)$cert->refid; + + if ($cert_refid == $refid) { + $cert_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->crt))); + $pkey_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->prv))); + + if (!empty((string)$cert->caref)) { + $ca = CertStore::getCaChain((string)$cert->caref); + if ($ca) { + $cert_content .= "\n" . $ca; + } + } + + file_put_contents($cert_filename, $cert_content); + file_put_contents($pkey_filename, $pkey_content); + chmod($pkey_filename, 0600); + } + } +} + +# Purge obsolete log files. +# TODO: Should be removed in plugin version 2.0. +$log_files = glob('/var/log/turn_*.log'); +foreach ($log_files as $file) { + if (is_file($file)) { + unlink($file); + } +} diff --git a/net/turnserver/src/opnsense/service/conf/actions.d/actions_turnserver.conf b/net/turnserver/src/opnsense/service/conf/actions.d/actions_turnserver.conf new file mode 100644 index 0000000000..358637c93c --- /dev/null +++ b/net/turnserver/src/opnsense/service/conf/actions.d/actions_turnserver.conf @@ -0,0 +1,26 @@ +[start] +command:/usr/local/etc/rc.d/turnserver start +parameters: +type:script +description:Start Turnserver +message:starting turnserver + +[stop] +command:/usr/local/etc/rc.d/turnserver stop +parameters: +type:script +description:Stop Turnserver +message:stopping turnserver + +[restart] +command:/usr/local/etc/rc.d/turnserver restart +parameters: +type:script +description:Restart Turnserver +message:restarting turnserver + +[status] +command:/usr/local/etc/rc.d/turnserver status; exit 0 +parameters: +type:script_output +message:requesting turnserver status diff --git a/net/turnserver/src/opnsense/service/templates/OPNsense/Syslog/local/turnserver.conf b/net/turnserver/src/opnsense/service/templates/OPNsense/Syslog/local/turnserver.conf new file mode 100644 index 0000000000..a1623cd385 --- /dev/null +++ b/net/turnserver/src/opnsense/service/templates/OPNsense/Syslog/local/turnserver.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [turnserver]. +################################################################### +filter f_local_turnserver { + program("turnserver"); +}; diff --git a/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/+TARGETS b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/+TARGETS new file mode 100644 index 0000000000..52ca689507 --- /dev/null +++ b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/+TARGETS @@ -0,0 +1,2 @@ +turnserver.conf:/usr/local/etc/turnserver.conf +rc.conf.d:/etc/rc.conf.d/turnserver diff --git a/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/rc.conf.d b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/rc.conf.d new file mode 100644 index 0000000000..fdf9b7be43 --- /dev/null +++ b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/rc.conf.d @@ -0,0 +1,6 @@ +{% if helpers.exists('OPNsense.turnserver.settings.Enabled') and OPNsense.turnserver.settings.Enabled|default("0") == "1" %} +turnserver_enable="YES" +turnserver_setup="/usr/local/opnsense/scripts/OPNsense/Turnserver/export_certs.php" +{% else %} +turnserver_enable="NO" +{% endif %} diff --git a/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/turnserver.conf b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/turnserver.conf new file mode 100644 index 0000000000..f5442f1d9d --- /dev/null +++ b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/turnserver.conf @@ -0,0 +1,61 @@ +# General +{% if helpers.exists('OPNsense.turnserver.settings.ListenIP') and OPNsense.turnserver.settings.ListenIP|default("") != "" %} +{% for listenip in OPNsense.turnserver.settings.ListenIP.split(",") %} +listening-ip={{ listenip }} +{% endfor %} +{% endif %} +listening-port={{ OPNsense.turnserver.settings.ListenPort }} +min-port={{ OPNsense.turnserver.settings.MinPort }} +max-port={{ OPNsense.turnserver.settings.MaxPort }} + +# TLS +{% if helpers.exists('OPNsense.turnserver.settings.TlsEnabled') and OPNsense.turnserver.settings.TlsEnabled|default("") == "1" %} +{% if OPNsense.turnserver.settings.TlsCertificate|default("") != "" %} +tls-listening-port={{ OPNsense.turnserver.settings.TlsPort }} +cert=/usr/local/etc/turnserver_cert.pem +pkey=/usr/local/etc/turnserver_pkey.pem +{% else %} +# ERROR: Required TLS certificate was not specified. TLS support will be disabled. +no-tls +no-dtls +{% endif %} +{% else %} +no-tls +no-dtls +{% endif %} + +# Security +{% if helpers.exists('OPNsense.turnserver.settings.UseAuthSecret') and OPNsense.turnserver.settings.UseAuthSecret|default("") == "1" %} +{% if OPNsense.turnserver.settings.StaticAuthSecret|default("") != "" %} +use-auth-secret +static-auth-secret={{ OPNsense.turnserver.settings.StaticAuthSecret }} +{% else %} +# ERROR: Required Auth Secret was not specified; this feature will be disabled. +{% endif %} +{% endif %} + +# Features +{% if OPNsense.turnserver.settings.Realm|default("") != "" %} +realm={{ OPNsense.turnserver.settings.Realm }} +{% endif %} +{% if OPNsense.turnserver.settings.FingerprintsEnabled|default("") == "1" %} +fingerprint +{% endif %} + +# Tuning +user-quota={{ OPNsense.turnserver.settings.UserQuota }} +total-quota={{ OPNsense.turnserver.settings.TotalQuota }} +stale-nonce={{ OPNsense.turnserver.settings.StaleNonce }} +channel-lifetime={{ OPNsense.turnserver.settings.ChannelLifetime }} +permission-lifetime={{ OPNsense.turnserver.settings.PermissionLifetime }} + +# Defaults +log-file=syslog +no-cli +no-software-attribute +no-multicast-peers +no-tlsv1 +no-tlsv1_1 +no-rfc5780 +no-stun-backward-compatibility +response-origin-only-with-rfc5780 diff --git a/net/udpbroadcastrelay/Makefile b/net/udpbroadcastrelay/Makefile index 9d11bafa50..e0170423b9 100644 --- a/net/udpbroadcastrelay/Makefile +++ b/net/udpbroadcastrelay/Makefile @@ -1,7 +1,7 @@ PLUGIN_NAME= udpbroadcastrelay PLUGIN_VERSION= 1.0 -PLUGIN_REVISION= 2 -PLUGIN_COMMENT= Control ubpbroadcastrelay processes +PLUGIN_REVISION= 6 +PLUGIN_COMMENT= Control udpbroadcastrelay processes PLUGIN_DEPENDS= udpbroadcastrelay PLUGIN_MAINTAINER= mjwasley@gmail.com diff --git a/net/udpbroadcastrelay/pkg-descr b/net/udpbroadcastrelay/pkg-descr index bd8f32978d..b2aad18334 100644 --- a/net/udpbroadcastrelay/pkg-descr +++ b/net/udpbroadcastrelay/pkg-descr @@ -1,4 +1,4 @@ -udbproadcastrelay is a UDP multicast relayer. Its intended use is to +udpbroadcastrelay is a UDP multicast relayer. Its intended use is to rebroadbcast udp packets on a specific port across interfaces, be those interfaces physical or VLAN. diff --git a/net/udpbroadcastrelay/src/etc/inc/plugins.inc.d/udpbroadcastrelay.inc b/net/udpbroadcastrelay/src/etc/inc/plugins.inc.d/udpbroadcastrelay.inc index bb8a14f9f0..aeddc27c4a 100644 --- a/net/udpbroadcastrelay/src/etc/inc/plugins.inc.d/udpbroadcastrelay.inc +++ b/net/udpbroadcastrelay/src/etc/inc/plugins.inc.d/udpbroadcastrelay.inc @@ -1,29 +1,29 @@ udpbroadcastrelay->iterateItems() as $server) { - if ($server->enabled == '1') { + if ((string)$server->enabled == '1') { return true; } } @@ -48,7 +48,7 @@ function udpbroadcastrelay_firewall($fw) function udpbroadcastrelay_services() { - $services = array(); + $services = []; if (!udpbroadcastrelay_enabled()) { return $services; @@ -57,22 +57,21 @@ function udpbroadcastrelay_services() $model = new \OPNsense\UDPBroadcastRelay\UDPBroadcastRelay(); foreach ($model->udpbroadcastrelay->iterateItems() as $server) { - if ($server->enabled == '0') { + if ((string)$server->enabled == '0') { continue; } - $services[] = array( - 'description' => $server->description, - 'id' => $server->InstanceID, + $services[] = [ + 'description' => (string)$server->description, + 'id' => (string)$server->InstanceID, 'pidfile' => "/var/run/udpbroadcastrelay_{$server->InstanceID}.pid", - 'configd' => array( - 'restart' => array('udpbroadcastrelay restart ' . $server->InstanceID), - 'start' => array('udpbroadcastrelay start ' . $server->InstanceID), - 'stop' => array('udpbroadcastrelay stop ' . $server->InstanceID), - - ), + 'configd' => [ + 'restart' => ['udpbroadcastrelay restart ' . $server->InstanceID], + 'start' => ['udpbroadcastrelay start ' . $server->InstanceID], + 'stop' => ['udpbroadcastrelay stop ' . $server->InstanceID], + ], 'name' => 'udpbroadcastrelay', - ); + ]; } return $services; diff --git a/net/udpbroadcastrelay/src/etc/rc.d/os-udpbroadcastrelay b/net/udpbroadcastrelay/src/etc/rc.d/os-udpbroadcastrelay index e16cab0c38..431191c467 100755 --- a/net/udpbroadcastrelay/src/etc/rc.d/os-udpbroadcastrelay +++ b/net/udpbroadcastrelay/src/etc/rc.d/os-udpbroadcastrelay @@ -1,8 +1,5 @@ #!/bin/sh # -# $FreeBSD$ -# - # PROVIDE: osudpbroadcastrelay # KEYWORD: shutdown diff --git a/net/udpbroadcastrelay/src/opnsense/mvc/app/controllers/OPNsense/UDPBroadcastRelay/Api/ServiceController.php b/net/udpbroadcastrelay/src/opnsense/mvc/app/controllers/OPNsense/UDPBroadcastRelay/Api/ServiceController.php index b64d378d5f..09e6feb639 100644 --- a/net/udpbroadcastrelay/src/opnsense/mvc/app/controllers/OPNsense/UDPBroadcastRelay/Api/ServiceController.php +++ b/net/udpbroadcastrelay/src/opnsense/mvc/app/controllers/OPNsense/UDPBroadcastRelay/Api/ServiceController.php @@ -45,9 +45,6 @@ class ServiceController extends ApiMutableModelControllerBase public function statusAction($uuid) { $result = array("result" => "failed", "function" => "status"); - if ($this->request->isPost()) { - $this->sessionClose(); - } if ($uuid != null) { $mdlUDPBroadcastRelay = new UDPBroadcastRelay(); $node = $mdlUDPBroadcastRelay->getNodeByReference('udpbroadcastrelay.' . $uuid); @@ -66,9 +63,6 @@ public function statusAction($uuid) public function startAction($uuid) { $result = array("result" => "failed", "function" => "start"); - if ($this->request->isPost()) { - $this->sessionClose(); - } if ($uuid != null) { $mdlUDPBroadcastRelay = new UDPBroadcastRelay(); $node = $mdlUDPBroadcastRelay->getNodeByReference('udpbroadcastrelay.' . $uuid); @@ -87,9 +81,6 @@ public function startAction($uuid) public function stopAction($uuid) { $result = array("result" => "failed", "function" => "stop"); - if ($this->request->isPost()) { - $this->sessionClose(); - } if ($uuid != null) { $mdlUDPBroadcastRelay = new UDPBroadcastRelay(); $node = $mdlUDPBroadcastRelay->getNodeByReference('udpbroadcastrelay.' . $uuid); @@ -107,9 +98,6 @@ public function stopAction($uuid) */ public function restartAction($uuid) { - if ($this->request->isPost()) { - $this->sessionClose(); - } if ($uuid != null) { $mdlUDPBroadcastRelay = new UDPBroadcastRelay(); $node = $mdlUDPBroadcastRelay->getNodeByReference('udpbroadcastrelay.' . $uuid); @@ -127,9 +115,6 @@ public function restartAction($uuid) public function configAction() { $result = array("result" => "failed", "function" => "config"); - if ($this->request->isPost()) { - $this->sessionClose(); - } $result['result'] = $this->callBackend('template'); return $result; } @@ -140,9 +125,6 @@ public function configAction() */ public function reloadAction() { - if ($this->request->isPost()) { - $this->sessionClose(); - } $result = $this->configAction(); if ($result['result'] == 'OK') { $result['function'] = "reload"; diff --git a/net/udpbroadcastrelay/src/opnsense/mvc/app/controllers/OPNsense/UDPBroadcastRelay/Api/SettingsController.php b/net/udpbroadcastrelay/src/opnsense/mvc/app/controllers/OPNsense/UDPBroadcastRelay/Api/SettingsController.php index 4f63375ccb..6a27ad28ef 100644 --- a/net/udpbroadcastrelay/src/opnsense/mvc/app/controllers/OPNsense/UDPBroadcastRelay/Api/SettingsController.php +++ b/net/udpbroadcastrelay/src/opnsense/mvc/app/controllers/OPNsense/UDPBroadcastRelay/Api/SettingsController.php @@ -1,29 +1,29 @@ sessionClose(); $fields = array( "enabled", "interfaces", diff --git a/net/udpbroadcastrelay/src/opnsense/mvc/app/models/OPNsense/UDPBroadcastRelay/UDPBroadcastRelay.xml b/net/udpbroadcastrelay/src/opnsense/mvc/app/models/OPNsense/UDPBroadcastRelay/UDPBroadcastRelay.xml index 20638487e7..aa3ca1b4bb 100644 --- a/net/udpbroadcastrelay/src/opnsense/mvc/app/models/OPNsense/UDPBroadcastRelay/UDPBroadcastRelay.xml +++ b/net/udpbroadcastrelay/src/opnsense/mvc/app/models/OPNsense/UDPBroadcastRelay/UDPBroadcastRelay.xml @@ -1,60 +1,52 @@ - //OPNsense/udpbroadcastrelays - 1.0.0 - UDP Broadcast Relay settings - - - - 1 - Y - + //OPNsense/udpbroadcastrelays + 1.0.0 + UDP Broadcast Relay settings + + + + 1 + Y + - lan - Y - Y + lan + Y + Y - - N - - Y - /^([\/0-9.,])*/u - Broadcast address must be a valid IPv4 address - - - N - - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-4]|2[0-5][0-9]|[01]?[0-9][0-9]?)$/ - Source address must be a valid IPv4 address - - - - Y - 1 - 65535 - Listen port needs to be an integer value between 1 and 65535 - - - N - - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-4]|2[0-5][0-9]|[01]?[0-9][0-9]?)$/ - Source address must be a valid IPv4 address - - - 0 - Y - 1 - 63 - InstanceID needs to be an integer value between 1 and 63 - - - 0 - Y - - - N - /^([\t\n\v\f\r 0-9a-zA-Z.,_\x{00A0}-\x{FFFF}]){1,255}$/u - Enter a description. - - - + + Y + /^([\/0-9.,])*/u + Broadcast address must be a valid IPv4 address. + + + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-4]|2[0-5][0-9]|[01]?[0-9][0-9]?)$/ + Source address must be a valid IPv4 address. + + + Y + 1 + 65535 + Listen port needs to be an integer value between 1 and 65535. + + + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-4]|2[0-5][0-9]|[01]?[0-9][0-9]?)$/ + Source address must be a valid IPv4 address. + + + 0 + Y + 1 + 63 + InstanceID needs to be an integer value between 1 and 63. + + + 0 + Y + + + /^([\t\n\v\f\r 0-9a-zA-Z.,_\x{00A0}-\x{FFFF}]){1,255}$/u + Enter a description. + + + diff --git a/net/udpbroadcastrelay/src/opnsense/mvc/app/views/OPNsense/UDPBroadcastRelay/index.volt b/net/udpbroadcastrelay/src/opnsense/mvc/app/views/OPNsense/UDPBroadcastRelay/index.volt index cd84f0720a..f521ae1899 100644 --- a/net/udpbroadcastrelay/src/opnsense/mvc/app/views/OPNsense/UDPBroadcastRelay/index.volt +++ b/net/udpbroadcastrelay/src/opnsense/mvc/app/views/OPNsense/UDPBroadcastRelay/index.volt @@ -33,8 +33,8 @@ POSSIBILITY OF SUCH DAMAGE. */ function openDialog(uuid) { var editDlg = "DialogEdit"; - var setUrl = "/api/udpbroadcastrelay/settings/setRelay/"; - var getUrl = "/api/udpbroadcastrelay/settings/getRelay/"; + var setUrl = "/api/udpbroadcastrelay/settings/set_relay/"; + var getUrl = "/api/udpbroadcastrelay/settings/get_relay/"; var urlMap = {}; urlMap['frm_' + editDlg] = getUrl + uuid; mapDataToFormUI(urlMap).done(function () { @@ -55,12 +55,12 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-proxies").UIBootgrid( - { 'search':'/api/udpbroadcastrelay/settings/searchRelay', - 'get':'/api/udpbroadcastrelay/settings/getRelay/', - 'set':'/api/udpbroadcastrelay/settings/setRelay/', - 'add':'/api/udpbroadcastrelay/settings/addRelay/', - 'del':'/api/udpbroadcastrelay/settings/delRelay/', - 'toggle':'/api/udpbroadcastrelay/settings/toggleRelay/', + { 'search':'/api/udpbroadcastrelay/settings/search_relay', + 'get':'/api/udpbroadcastrelay/settings/get_relay/', + 'set':'/api/udpbroadcastrelay/settings/set_relay/', + 'add':'/api/udpbroadcastrelay/settings/add_relay/', + 'del':'/api/udpbroadcastrelay/settings/del_relay/', + 'toggle':'/api/udpbroadcastrelay/settings/toggle_relay/', 'options':{selection:false, multiSelect:false} } ); @@ -87,16 +87,16 @@ POSSIBILITY OF SUCH DAMAGE. - - - - - - - - - - + + + + + + + + + + diff --git a/net/upnp/Makefile b/net/upnp/Makefile index 145438e569..4c5c4109b4 100644 --- a/net/upnp/Makefile +++ b/net/upnp/Makefile @@ -1,8 +1,7 @@ PLUGIN_NAME= upnp -PLUGIN_VERSION= 1.4 -PLUGIN_REVISION= 2 +PLUGIN_VERSION= 1.8 PLUGIN_DEPENDS= miniupnpd -PLUGIN_COMMENT= Universal Plug and Play Service +PLUGIN_COMMENT= UPnP IGD & PCP/NAT-PMP Service PLUGIN_MAINTAINER= franco@opnsense.org .include "../../Mk/plugins.mk" diff --git a/net/upnp/pkg-descr b/net/upnp/pkg-descr index 6b4094da82..e970623794 100644 --- a/net/upnp/pkg-descr +++ b/net/upnp/pkg-descr @@ -1,5 +1,23 @@ -Mini UPnPd is a lightweight implementation of a UPnP IGD daemon. This is -supposed to be run on your gateway machine to allow client systems to redirect -ports and punch holes in the firewall. +MiniUPnPd is a lightweight implementation of a UPnP IGD & PCP/NAT-PMP daemon. +This is supposed to be run on your gateway machine to allow client systems to +map ports and punch holes in the firewall. -WWW: http://miniupnp.free.fr/ +WWW: https://miniupnp.tuxfamily.org/ + +Plugin Changelog +================ + +1.7 + +* Add option to allow arbitrary number of UPnP/NAT-PMP rules (contributed by Kreeblah) + +1.6 + +* Fix port maps not listed without description (contributed by Self-Hosting-Group) + +1.5 + +* Enable STUN and allow LAN subnet override (contributed by Tawmu) +* Add missing firewall anchors (contributed by Tawmu) +* Allow subnet mask 0 in rules (contributed by Reiko Asakura) +* Switch to miniupnpd 2.3.1 diff --git a/net/upnp/src/etc/inc/plugins.inc.d/miniupnpd.inc b/net/upnp/src/etc/inc/plugins.inc.d/miniupnpd.inc index 9547836fe8..069680a361 100644 --- a/net/upnp/src/etc/inc/plugins.inc.d/miniupnpd.inc +++ b/net/upnp/src/etc/inc/plugins.inc.d/miniupnpd.inc @@ -41,22 +41,24 @@ function miniupnpd_firewall($fw) $fw->registerAnchor('miniupnpd', 'rdr'); $fw->registerAnchor('miniupnpd', 'fw'); + $fw->registerAnchor('miniupnpd', 'nat', 0, 'head'); + $fw->registerAnchor('miniupnpd', 'binat'); } function miniupnpd_services() { - $services = array(); + $services = []; if (!miniupnpd_enabled()) { return $services; } - $pconfig = array(); + $pconfig = []; $pconfig['name'] = 'miniupnpd'; - $pconfig['description'] = gettext('Univeral Plug and Play'); - $pconfig['php']['restart'] = array('miniupnpd_stop', 'miniupnpd_start'); - $pconfig['php']['start'] = array('miniupnpd_start'); - $pconfig['php']['stop'] = array('miniupnpd_stop'); + $pconfig['description'] = gettext('UPnP IGD & PCP/NAT-PMP'); + $pconfig['php']['restart'] = ['miniupnpd_stop', 'miniupnpd_start']; + $pconfig['php']['start'] = ['miniupnpd_start']; + $pconfig['php']['stop'] = ['miniupnpd_stop']; $pconfig['pidfile'] = '/var/run/miniupnpd.pid'; $services[] = $pconfig; @@ -73,19 +75,23 @@ function miniupnpd_start() return; } - mwexec_bg('/usr/local/sbin/miniupnpd -f /var/etc/miniupnpd.conf -P /var/run/miniupnpd.pid'); + mwexecfb('/usr/local/sbin/miniupnpd -f %s -P %s', [ '/var/etc/miniupnpd.conf', '/var/run/miniupnpd.pid']); } function miniupnpd_stop() { - killbypid('/var/run/miniupnpd.pid', 'TERM', true); - mwexec('/sbin/pfctl -aminiupnpd -Fr 2>&1 >/dev/null'); - mwexec('/sbin/pfctl -aminiupnpd -Fn 2>&1 >/dev/null'); + killbypid('/var/run/miniupnpd.pid'); + + mwexecf('/sbin/pfctl -a miniupnpd -Fr'); + mwexecf('/sbin/pfctl -a miniupnpd -Fn'); } function miniupnpd_configure() { - return array('bootup' => array('miniupnpd_configure_do')); + return [ + 'bootup' => ['miniupnpd_configure_do'], + 'newwanip' => ['miniupnpd_configure_do'], + ]; } function miniupnpd_uuid() @@ -98,10 +104,17 @@ function miniupnpd_uuid() function miniupnpd_permuser_list() { - $ret = array(); - $count = 8; + global $config; + + $num_permuser = 8; + + if (!empty($config['installedpackages']['miniupnpd']['config'][0]['num_permuser'])) { + $num_permuser = $config['installedpackages']['miniupnpd']['config'][0]['num_permuser']; + } + + $ret = []; - for ($i = 1; $i <= $count; $i++) { + for ($i = 1; $i <= $num_permuser; $i++) { $ret[$i] = "permuser{$i}"; } @@ -118,23 +131,18 @@ function miniupnpd_configure_do($verbose = false) return; } - if ($verbose) { - echo 'Starting UPnP service...'; - flush(); - } + service_log('Starting UPnP IGD & PCP/NAT-PMP service...', $verbose); $upnp_config = $config['installedpackages']['miniupnpd']['config'][0]; $ext_ifname = get_real_interface($upnp_config['ext_iface']); if ($ext_ifname == $upnp_config['ext_iface']) { - if ($verbose) { - echo "failed.\n"; - } + service_log("failed.\n", $verbose); return; } $config_text = "ext_ifname={$ext_ifname}\n"; - $config_text .= "port=2189\n"; + $config_text .= "http_port=2189\n"; $ifaces_active = ''; @@ -148,9 +156,13 @@ function miniupnpd_configure_do($verbose = false) $if = get_real_interface($iface); /* above function returns iface if fail */ if ($if != $iface) { - $addr = get_interface_ip($iface); + list ($addr) = interfaces_primary_address($iface); if (!empty($addr)) { - $config_text .= "listening_ip={$if}\n"; + $config_text .= "listening_ip={$if}"; + if (!empty($upnp_config['overridesubnet'])) { + $config_text .= "/{$upnp_config['overridesubnet']}"; + } + $config_text .= "\n"; if (!$ifaces_active) { $webgui_ip = $addr; $ifaces_active = $iface; @@ -158,10 +170,10 @@ function miniupnpd_configure_do($verbose = false) $ifaces_active .= ", {$iface}"; } } else { - log_error("miniupnpd: Interface {$iface} has no ip address, ignoring"); + log_msg("miniupnpd: Interface {$iface} has no ip address, ignoring", LOG_WARNING); } } else { - log_error("miniupnpd: Could not resolve real interface for {$iface}"); + log_msg("miniupnpd: Could not resolve real interface for {$iface}", LOG_ERR); } } @@ -170,6 +182,14 @@ function miniupnpd_configure_do($verbose = false) if (!empty($upnp_config['overridewanip'])) { $config_text .= "ext_ip={$upnp_config['overridewanip']}\n"; } + + /* configure STUN server if needed */ + if (!empty($upnp_config['stun_host'])) { + $config_text .= "ext_perform_stun=allow-filtered\n"; + $config_text .= "ext_stun_host=" . ($upnp_config['stun_host']) . "\n"; + $config_text .= "ext_stun_port=" . ($upnp_config['stun_port'] ?? "3478") . "\n"; + } + /* set upload and download bitrates */ if (!empty($upnp_config['download']) && !empty($upnp_config['upload'])) { $download = $upnp_config['download'] * 1000; @@ -178,7 +198,17 @@ function miniupnpd_configure_do($verbose = false) $config_text .= "bitrate_up={$upload}\n"; } - $config_text .= "secure_mode=yes\n"; + if (!empty($upnp_config['allow_third_party_mapping'])) { + $config_text .= "secure_mode=no\n"; + $config_text .= "pcp_allow_thirdparty=yes\n"; + } else { + $config_text .= "secure_mode=yes\n"; + $config_text .= "pcp_allow_thirdparty=no\n"; + } + + if (!empty($upnp_config['ipv6_disable'])) { + $config_text .= "ipv6_disable=yes\n"; + } /* enable logging of packets handled by miniupnpd rules */ if (!empty($upnp_config['logpackets'])) { @@ -199,12 +229,19 @@ function miniupnpd_configure_do($verbose = false) $config_text .= "/\n"; } + if (!empty($upnp_config['friendly_name'])) { + // Encode required XML entities of text UPnP IGD config options until the daemon does so + $config_text .= "friendly_name=" . htmlspecialchars($upnp_config['friendly_name'], ENT_NOQUOTES | ENT_XML1) . "\n"; + } else { + $config_text .= "friendly_name=OPNsense UPnP IGD & PCP\n"; + } + /* set uuid and serial */ $config_text .= "uuid=" . miniupnpd_uuid() . "\n"; $config_text .= "serial=" . strtoupper(substr(miniupnpd_uuid(), 0, 8)) . "\n"; /* set model number */ - $config_text .= "model_number=" . trim(shell_exec('opnsense-version -v')) . "\n"; + $config_text .= "model_number=" . shell_safe('opnsense-version -v') . "\n"; /* upnp access restrictions */ foreach (miniupnpd_permuser_list() as $permuser) { @@ -214,27 +251,30 @@ function miniupnpd_configure_do($verbose = false) } if (!empty($upnp_config['permdefault'])) { - $config_text .= "deny 0-65535 0.0.0.0/0 0-65535\n"; + $config_text .= "deny 1-65535 0.0.0.0/0 1-65535\n"; } - /* Allow UPnP or NAT-PMP as requested */ + /* Allow UPnP IGD or PCP/NAT-PMP as requested */ $config_text .= "enable_upnp=" . ( $upnp_config['enable_upnp'] ? "yes\n" : "no\n" ); - $config_text .= "enable_natpmp=" . ( $upnp_config['enable_natpmp'] ? "yes\n" : "no\n" ); + $config_text .= "enable_pcp_pmp=" . ( $upnp_config['enable_natpmp'] ? "yes\n" : "no\n" ); + + // When building the daemon with UPnP IGDv2, infinite (IGDv1 only) lease duration port maps are reduced + // to 7d, following the IGDv2 standard. Disabling it at runtime allows IGDv2-incompatible clients + if (($upnp_config['upnp_igd_compat'] ?? 'igdv1') == 'igdv1') { + $config_text .= "force_igd_desc_v1=yes\n"; + } - /* configure lifetimes to force periodic expire */ - $config_text .= "clean_ruleset_interval=600\n"; - $config_text .= "min_lifetime=120\n"; - $config_text .= "max_lifetime=86400\n"; + $config_text .= "lease_file=/var/run/miniupnpd.leases\n"; + $config_text .= "lease_file6=/var/run/miniupnpd.leases-ipv6\n"; /* write out the configuration */ file_put_contents('/var/etc/miniupnpd.conf', $config_text); - log_error("miniupnpd: Starting service on interface: {$ifaces_active}"); + log_msg("miniupnpd: Starting service on interface: {$ifaces_active}"); + miniupnpd_start(); } } - if ($verbose) { - echo "done.\n"; - } + service_log("done.\n", $verbose); } diff --git a/net/upnp/src/opnsense/mvc/app/models/OPNsense/UPnP/ACL/ACL.xml b/net/upnp/src/opnsense/mvc/app/models/OPNsense/UPnP/ACL/ACL.xml index cc93dde4ad..23a2fa96e2 100644 --- a/net/upnp/src/opnsense/mvc/app/models/OPNsense/UPnP/ACL/ACL.xml +++ b/net/upnp/src/opnsense/mvc/app/models/OPNsense/UPnP/ACL/ACL.xml @@ -1,12 +1,12 @@ - Service: Universal Plug and Play + Services: UPnP IGD & PCP: Settings services_upnp.php* - Status: Universal Plug and Play + Services: UPnP IGD & PCP: Active Maps status_upnp.php* diff --git a/net/upnp/src/opnsense/mvc/app/models/OPNsense/UPnP/Menu/Menu.xml b/net/upnp/src/opnsense/mvc/app/models/OPNsense/UPnP/Menu/Menu.xml index 0dfae6b0f3..c040d5ea16 100644 --- a/net/upnp/src/opnsense/mvc/app/models/OPNsense/UPnP/Menu/Menu.xml +++ b/net/upnp/src/opnsense/mvc/app/models/OPNsense/UPnP/Menu/Menu.xml @@ -1,10 +1,10 @@ - - + + - + diff --git a/net/upnp/src/www/services_upnp.php b/net/upnp/src/www/services_upnp.php index e3512ae830..ec54020425 100644 --- a/net/upnp/src/www/services_upnp.php +++ b/net/upnp/src/www/services_upnp.php @@ -36,10 +36,10 @@ function miniupnpd_validate_ip($ip) { /* validate cidr */ - $ip_array = array(); + $ip_array = []; $ip_array = explode('/', $ip); if (count($ip_array) == 2) { - if ($ip_array[1] < 1 || $ip_array[1] > 32) { + if ($ip_array[1] < 0 || $ip_array[1] > 32) { return false; } } elseif (count($ip_array) != 1) { @@ -66,15 +66,36 @@ function miniupnpd_validate_port($port) } if ($_SERVER['REQUEST_METHOD'] === 'GET') { - $pconfig = array(); + $pconfig = []; - $copy_fields = array('enable', 'enable_upnp', 'enable_natpmp', 'ext_iface', 'iface_array', 'download', - 'upload', 'overridewanip', 'logpackets', 'sysuptime', 'permdefault'); + $copy_fields = [ + 'allow_third_party_mapping', + 'download', + 'enable', + 'enable_natpmp', + 'enable_upnp', + 'ext_iface', + 'friendly_name', + 'iface_array', + 'ipv6_disable', + 'logpackets', + 'overridesubnet', + 'overridewanip', + 'permdefault', + 'stun_host', + 'stun_port', + 'num_permuser', + 'sysuptime', + 'upload', + 'upnp_igd_compat', + ]; foreach (miniupnpd_permuser_list() as $permuser) { $copy_fields[] = $permuser; } + $pconfig['num_permuser'] = null; + foreach ($copy_fields as $fieldname) { if (isset($config['installedpackages']['miniupnpd']['config'][0][$fieldname])) { $pconfig[$fieldname] = $config['installedpackages']['miniupnpd']['config'][0][$fieldname]; @@ -83,12 +104,12 @@ function miniupnpd_validate_port($port) // parse array $pconfig['iface_array'] = explode(',', $pconfig['iface_array']); } elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { - $input_errors = array(); + $input_errors = []; $pconfig = $_POST; // validate form data if (!empty($pconfig['enable']) && (empty($pconfig['enable_upnp']) && empty($pconfig['enable_natpmp']))) { - $input_errors[] = gettext('At least one of \'UPnP\' or \'NAT-PMP\' must be allowed'); + $input_errors[] = gettext('At least one of \'UPnP IGD\' or \'PCP/NAT-PMP\' must be allowed'); } if (!empty($pconfig['iface_array'])) { foreach($pconfig['iface_array'] as $iface) { @@ -104,15 +125,30 @@ function miniupnpd_validate_port($port) if (!empty($pconfig['overridewanip']) && !is_ipaddr($pconfig['overridewanip'])) { $input_errors[] = gettext('You must specify a valid ip address in the \'Override WAN address\' field'); } + if (!empty($pconfig['overridewanip']) && !empty($pconfig['stun_host'])) { + $input_errors[] = gettext('You cannot override the WAN IP if you have a STUN host set.'); + } + if (!empty($pconfig['stun_host']) && !is_ipaddr($pconfig['stun_host']) && !is_hostname($pconfig['stun_host'])) { + $input_errors[] = gettext('The STUN host must be a valid IP address or hostname.'); + } + if (!empty($pconfig['stun_port']) && !is_port($pconfig['stun_port'])) { + $input_errors[] = gettext('STUN port must contain a valid port number.'); + } + if (!empty($pconfig['overridesubnet']) && count($pconfig['iface_array']) > 1) { + $input_errors[] = gettext('You can only override the interface subnet when one LAN interface is selected'); + } if ((!empty($pconfig['download']) && empty($pconfig['upload'])) || (!empty($pconfig['upload']) && empty($pconfig['download']))) { $input_errors[] = gettext('You must fill in both \'Maximum Download Speed\' and \'Maximum Upload Speed\' fields'); } - if (!empty($pconfig['download']) && ($pconfig['download'] <= 0 || !is_numeric($pconfig['download']))) { + if (!empty($pconfig['download']) && (!is_numeric($pconfig['download']) || $pconfig['download'] <= 0)) { $input_errors[] = gettext('You must specify a value greater than 0 in the \'Maximum Download Speed\' field'); } - if (!empty($pconfig['upload']) && ($pconfig['upload'] <= 0 || !is_numeric($pconfig['upload']))) { + if (!empty($pconfig['upload']) && (!is_numeric($pconfig['upload']) || $pconfig['upload'] <= 0)) { $input_errors[] = gettext('You must specify a value greater than 0 in the \'Maximum Upload Speed\' field'); } + if (!empty($pconfig['num_permuser'] && (!is_numeric($pconfig['num_permuser']) || $pconfig['num_permuser'] < 1))) { + $input_errors[] = gettext('Number of permissions must be an integer greater than 0'); + } /* user permissions validation */ foreach (miniupnpd_permuser_list() as $i => $permuser) { @@ -140,13 +176,17 @@ function miniupnpd_validate_port($port) if (count($input_errors) == 0) { // save form data - $upnp = array(); + $upnp = []; // boolean types - foreach (array('enable', 'enable_upnp', 'enable_natpmp', 'logpackets', 'sysuptime', 'permdefault') as $fieldname) { + foreach (['enable', 'enable_upnp', 'enable_natpmp', 'logpackets', 'sysuptime', 'permdefault', 'allow_third_party_mapping', 'ipv6_disable'] as $fieldname) { $upnp[$fieldname] = !empty($pconfig[$fieldname]); } + // numeric types + if (!empty($pconfig['num_permuser'])) { + $upnp['num_permuser'] = $pconfig['num_permuser']; + } // text field types - foreach (array('ext_iface', 'download', 'upload', 'overridewanip') as $fieldname) { + foreach (['ext_iface', 'download', 'upload', 'overridewanip', 'overridesubnet', 'stun_host', 'stun_port', 'friendly_name', 'upnp_igd_compat'] as $fieldname) { $upnp[$fieldname] = $pconfig[$fieldname]; } foreach (miniupnpd_permuser_list() as $fieldname) { @@ -157,7 +197,7 @@ function miniupnpd_validate_port($port) // sync to config $config['installedpackages']['miniupnpd']['config'] = $upnp; - write_config('Modified Universal Plug and Play settings'); + write_config('Modified UPnP IGD & PCP settings'); miniupnpd_configure_do(); filter_configure(); header(url_safe('Location: /services_upnp.php')); @@ -183,9 +223,7 @@ function miniupnpd_validate_port($port)
    {{ lang._('Enabled') }}{{ lang._('Interfaces') }}{{ lang._('Multicast Addresses') }}{{ lang._('Source Address') }}{{ lang._('Listen Port') }}{{ lang._('ID') }}{{ lang._('Description') }}{{ lang._('ID') }}{{ lang._('Use ID as TTL') }}{{ lang._('Commands') }}{{ lang._('Enabled') }}{{ lang._('Interfaces') }}{{ lang._('Multicast Addresses') }}{{ lang._('Source Address') }}{{ lang._('Listen Port') }}{{ lang._('ID') }}{{ lang._('Description') }}{{ lang._('ID') }}{{ lang._('Use ID as TTL') }}{{ lang._('Commands') }}
    - + - + - + - + - + - + + +
    - - @@ -195,13 +233,16 @@ function miniupnpd_validate_port($port)
    /> +
    />
    />
    -
    +
    +
    + +
    +
    +
    + + + + + + + + - + - + - + - + + + + + - + + + + - + @@ -317,28 +387,96 @@ function miniupnpd_validate_port($port)
    - -
    - - +
    - /> - + + +
    + /> +
    + /> +
    - /> -
    - + + + + + + + + + + + + + + + + + + + +
    + +
    + + +
    + + +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + $permuser): ?> - + - + +
    + /> + +
    + + +
    - +
    @@ -352,7 +490,7 @@ function miniupnpd_validate_port($port)   - " /> + diff --git a/net/upnp/src/www/status_upnp.php b/net/upnp/src/www/status_upnp.php index d0c5d4fcda..394d421706 100644 --- a/net/upnp/src/www/status_upnp.php +++ b/net/upnp/src/www/status_upnp.php @@ -29,19 +29,21 @@ require_once("guiconfig.inc"); require_once("interfaces.inc"); +require_once("util.inc"); require_once("plugins.inc.d/miniupnpd.inc"); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!empty($_POST['clear'])) { miniupnpd_stop(); + unlink('/var/run/miniupnpd.leases'); + unlink('/var/run/miniupnpd.leases-ipv6'); miniupnpd_start(); header(url_safe('Location: /status_upnp.php')); exit; } } -$rdr_entries = array(); -exec("/sbin/pfctl -aminiupnpd -sn", $rdr_entries, $pf_ret); +$rdr_entries = shell_safe('/sbin/pfctl -P -a miniupnpd -s nat; /sbin/pfctl -P -a miniupnpd -s rules', [], true); $service_hook = 'miniupnpd'; include("head.inc"); @@ -57,49 +59,62 @@
    -

    +

    - +
    - - - - - + + + + + + + (.*) port (.*)/", $rdr_entry, $matches)) { + if (!preg_match('/on (?P.+) inet proto (?P.+) from (?P[^ ]+) (port (?P.+) )?to (?P.+) port = (?P.+) keep state (label "(?P.+)" )?rtable [0-9] -> (?P.+) port (?P.+)/', $rdr_entry, $matches) && + !preg_match('/on (?P.+) inet6 proto (?P.+) from (?P[^ ]+) (port = (?P.+) )?to (?P.+) port = (?P\d+) (flags [^ ]+ )?keep state (label "(?P.+)" )?rtable [0-9]/', $rdr_entry, $matches)) { continue; } - $rdr_proto = $matches[2]; - $rdr_port = $matches[5]; - $rdr_label =$matches[6]; - $rdr_ip = $matches[7]; - $rdr_iport = $matches[8]; + if (preg_match('/PCP ([A-Z]+) ([0-9a-f]{24})$/', $matches['descr'], $descrmatch) === 1) { + $descr = "PCP ({$descrmatch[1]} nonce {$descrmatch[2]})"; + } elseif (preg_match('/^NAT-PMP \d+ \w+$/', $matches['descr'], $descrmatch) === 1) { + $descr = 'NAT-PMP'; + } elseif (preg_match('/^pinhole-(\d+).*IGD2 pinhole$/', $matches['descr'], $descrmatch) === 1) { + $descr = "UPnP IGDv2 IPv6 (UID {$descrmatch[1]})"; + } elseif (preg_match('/^UPnP IGD/', $matches['descr'], $descrmatch) === 1) { + $descr = $matches['descr']; + } else { + $descr = "UPnP IGD / {$matches['descr']}"; + } ?> - - - - - + + + + + + + - diff --git a/net/vnstat/Makefile b/net/vnstat/Makefile index 445106e949..8bbf7518fb 100644 --- a/net/vnstat/Makefile +++ b/net/vnstat/Makefile @@ -1,6 +1,7 @@ PLUGIN_NAME= vnstat PLUGIN_VERSION= 1.3 -PLUGIN_COMMENT= vnStat is a console-based network traffic monitor +PLUGIN_REVISION= 1 +PLUGIN_COMMENT= Network traffic monitor PLUGIN_DEPENDS= vnstat PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/net/vnstat/src/opnsense/mvc/app/models/OPNsense/Vnstat/General.xml b/net/vnstat/src/opnsense/mvc/app/models/OPNsense/Vnstat/General.xml index fa94d7dc05..568eccc481 100644 --- a/net/vnstat/src/opnsense/mvc/app/models/OPNsense/Vnstat/General.xml +++ b/net/vnstat/src/opnsense/mvc/app/models/OPNsense/Vnstat/General.xml @@ -4,12 +4,12 @@ 0.0.1 - 0 + 0 Y N - Y + Y diff --git a/net/vnstat/src/opnsense/service/conf/actions.d/actions_vnstat.conf b/net/vnstat/src/opnsense/service/conf/actions.d/actions_vnstat.conf index 0e13bb1b3f..4a4fcbd64e 100644 --- a/net/vnstat/src/opnsense/service/conf/actions.d/actions_vnstat.conf +++ b/net/vnstat/src/opnsense/service/conf/actions.d/actions_vnstat.conf @@ -1,23 +1,23 @@ [start] -command:/usr/local/opnsense/scripts/OPNsense/Vnstat/setup.sh;/usr/local/etc/rc.d/vnstat start && sleep 1 +command:/usr/local/etc/rc.d/vnstat start && sleep 1 parameters: type:script message:starting Vnstat [stop] -command:/usr/local/etc/rc.d/vnstat stop; exit 0 +command:/usr/local/etc/rc.d/vnstat stop parameters: type:script message:stopping Vnstat [restart] -command:/usr/local/opnsense/scripts/OPNsense/Vnstat/setup.sh;/usr/local/etc/rc.d/vnstat restart && sleep 1 +command:/usr/local/etc/rc.d/vnstat restart && sleep 1 parameters: type:script message:restarting Vnstat [status] -command:/usr/local/etc/rc.d/vnstat status;exit 0 +command:/usr/local/etc/rc.d/vnstat status; exit 0 parameters: type:script_output message:request Vnstat status diff --git a/net/vnstat/src/opnsense/service/templates/OPNsense/Vnstat/vnstat b/net/vnstat/src/opnsense/service/templates/OPNsense/Vnstat/vnstat index 923be6d5eb..345177c8a0 100644 --- a/net/vnstat/src/opnsense/service/templates/OPNsense/Vnstat/vnstat +++ b/net/vnstat/src/opnsense/service/templates/OPNsense/Vnstat/vnstat @@ -1,6 +1,6 @@ {% if helpers.exists('OPNsense.vnstat.general.enabled') and OPNsense.vnstat.general.enabled == '1' %} {% from 'OPNsense/Macros/interface.macro' import physical_interface %} -vnstat_var_script="/usr/local/opnsense/scripts/OPNsense/Vnstat/setup.sh" +vnstat_setup="/usr/local/opnsense/scripts/OPNsense/Vnstat/setup.sh" vnstat_enable="YES" {% if helpers.exists('OPNsense.vnstat.general.interface') and OPNsense.vnstat.general.interface != '' %} {% set interfaces = [] %} @@ -12,4 +12,3 @@ vnstat_additional_ifaces="{{ interfaces|join(' ') }}" {% else %} vnstat_enable="NO" {% endif %} -vnstat_var_mfs="/var/lib/vnstat" diff --git a/net/wireguard/Makefile b/net/wireguard/Makefile deleted file mode 100644 index 8cd2d5c43a..0000000000 --- a/net/wireguard/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -PLUGIN_NAME= wireguard -PLUGIN_VERSION= 1.9 -PLUGIN_COMMENT= WireGuard VPN service -PLUGIN_DEPENDS= wireguard-go wireguard-tools -PLUGIN_MAINTAINER= m.muenz@gmail.com - -.include "../../Mk/plugins.mk" diff --git a/net/wireguard/pkg-descr b/net/wireguard/pkg-descr deleted file mode 100644 index e6d9ce270f..0000000000 --- a/net/wireguard/pkg-descr +++ /dev/null @@ -1,60 +0,0 @@ -WireGuard® is an extremely simple yet fast and modern VPN -that utilizes state-of-the-art cryptography. It aims to be -faster, simpler, leaner, and more useful than IPSec, while -avoiding the massive headache. It intends to be considerably -more performant than OpenVPN. WireGuard is designed as a -general purpose VPN for running on embedded interfaces and -super computers alike, fit for many different circumstances. -Initially released for the Linux kernel, it is now -cross-platform and widely deployable. It is currently under -heavy development, but already it might be regarded as the -most secure, easiest to use, and simplest VPN solution in -the industry. - -WWW: https://www.wireguard.com/ - -Changelog ---------- - -1.9 - -* Rename interface label in filter rules (#2577) - -1.8 - -* Empty port in Endpoint is allowed - -1.7 - -* Make tunnel address (wg interface address) optional - -1.6 - -* Move DNS setting to advanced -* Make listen port optional - -1.5 - -* Allow synchronization of config - -1.4 - -* Add IPv6 gateway support (contributed by Alexander Korinek) - -1.3 - -* Client/peer name validation to use HostnameField - -1.2 - -* Dashboard widget (contributed by D. Domig) - -1.1 - -* Allow adding interface route for PBR - -1.0 - -* Support for most features like S2S, Roadwarrior -* DNS, MTU, PSK -* Allow to disable setting routes for PBR diff --git a/net/wireguard/src/etc/inc/plugins.inc.d/wireguard.inc b/net/wireguard/src/etc/inc/plugins.inc.d/wireguard.inc deleted file mode 100644 index 2efb42b500..0000000000 --- a/net/wireguard/src/etc/inc/plugins.inc.d/wireguard.inc +++ /dev/null @@ -1,80 +0,0 @@ - - * 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. - */ - -function wireguard_enabled() -{ - $model = new \OPNsense\Wireguard\General(); - return (string)$model->enabled == '1'; -} - -function wireguard_services() -{ - $services = array(); - - if (!wireguard_enabled()) { - return $services; - } - - $services[] = array( - 'description' => gettext('WireGuard VPN'), - 'configd' => array( - 'restart' => array('wireguard restart'), - 'start' => array('wireguard start'), - 'stop' => array('wireguard stop'), - ), - 'name' => 'wireguard-go' - ); - - return $services; -} - -function wireguard_interfaces() -{ - $interfaces = array(); - if (!wireguard_enabled()) { - return $interfaces; - } - $oic = array('enable' => true); - $oic['if'] = 'wireguard'; - $oic['descr'] = gettext('WireGuard (Group)'); - $oic['type'] = 'group'; - $oic['virtual'] = true; - $oic['networks'] = array(); - $interfaces['wireguard'] = $oic; - return $interfaces; -} - -function wireguard_xmlrpc_sync() -{ - $result = array(); - $result['id'] = 'wireguard'; - $result['section'] = 'OPNsense.wireguard'; - $result['description'] = gettext('WireGuard'); - $result['services'] = ['wireguard-go']; - return array($result); -} diff --git a/net/wireguard/src/etc/rc.syshook.d/start/50-wireguard b/net/wireguard/src/etc/rc.syshook.d/start/50-wireguard deleted file mode 100755 index d322638f21..0000000000 --- a/net/wireguard/src/etc/rc.syshook.d/start/50-wireguard +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -# reconfigure routing for wireguard -/usr/local/etc/rc.routing_configure diff --git a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/ClientController.php b/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/ClientController.php deleted file mode 100644 index 3ebd24dfd6..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/ClientController.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * 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. - * - */ - -namespace OPNsense\Wireguard\Api; - -use OPNsense\Base\ApiMutableModelControllerBase; - -class ClientController extends ApiMutableModelControllerBase -{ - protected static $internalModelName = 'client'; - protected static $internalModelClass = '\OPNsense\Wireguard\Client'; - - public function searchClientAction() - { - return $this->searchBase('clients.client', array("enabled", "name", "pubkey", "tunneladdress", "serveraddress", "serverport")); - } - public function getClientAction($uuid = null) - { - $this->sessionClose(); - return $this->getBase('client', 'clients.client', $uuid); - } - public function addClientAction() - { - return $this->addBase('client', 'clients.client'); - } - public function delClientAction($uuid) - { - return $this->delBase('clients.client', $uuid); - } - public function setClientAction($uuid) - { - return $this->setBase('client', 'clients.client', $uuid); - } - public function toggleClientAction($uuid) - { - return $this->toggleBase('clients.client', $uuid); - } -} diff --git a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/GeneralController.php b/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/GeneralController.php deleted file mode 100644 index da33e0f57f..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/GeneralController.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * 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. - * - */ - -namespace OPNsense\Wireguard\Api; - -use OPNsense\Base\ApiMutableModelControllerBase; - -class GeneralController extends ApiMutableModelControllerBase -{ - protected static $internalModelClass = '\OPNsense\Wireguard\General'; - protected static $internalModelName = 'general'; -} diff --git a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/ServerController.php b/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/ServerController.php deleted file mode 100644 index a492e93802..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/ServerController.php +++ /dev/null @@ -1,98 +0,0 @@ - - * 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. - */ - -namespace OPNsense\Wireguard\Api; - -use OPNsense\Base\ApiMutableModelControllerBase; -use OPNsense\Core\Backend; - -class ServerController extends ApiMutableModelControllerBase -{ - protected static $internalModelName = 'server'; - protected static $internalModelClass = '\OPNsense\Wireguard\Server'; - - public function searchServerAction() - { - return $this->searchBase('servers.server', array("enabled", "name", "networks", "pubkey", "port", "tunneladdress")); - } - public function getServerAction($uuid = null) - { - $this->sessionClose(); - return $this->getBase('server', 'servers.server', $uuid); - } - public function addServerAction($uuid = null) - { - if ($this->request->isPost() && $this->request->hasPost("server")) { - if ($uuid != null) { - $node = $this->getModel()->getNodeByReference('servers.server.' . $uuid); - } else { - $node = $this->getModel()->servers->server->Add(); - } - $node->setNodes($this->request->getPost("server")); - if (empty((string)$node->pubkey) && empty((string)$node->privkey)) { - // generate new keypair - $backend = new Backend(); - $keyspriv = $backend->configdpRun("wireguard genkey", 'private'); - $keyspub = $backend->configdpRun("wireguard genkey", 'public'); - $node->privkey = $keyspriv; - $node->pubkey = $keyspub; - } - return $this->validateAndSave($node, 'server'); - } - return array("result" => "failed"); - } - public function delServerAction($uuid) - { - return $this->delBase('servers.server', $uuid); - } - public function setServerAction($uuid = null) - { - if ($this->request->isPost() && $this->request->hasPost("server")) { - if ($uuid != null) { - $node = $this->getModel()->getNodeByReference('servers.server.' . $uuid); - } else { - $node = $this->getModel()->servers->server->Add(); - } - $node->setNodes($this->request->getPost("server")); - if (empty((string)$node->pubkey) && empty((string)$node->privkey)) { - // generate new keypair - $backend = new Backend(); - $keyspriv = $backend->configdpRun("wireguard genkey", 'private'); - $keyspub = $backend->configdpRun("wireguard genkey", 'public'); - $node->privkey = $keyspriv; - $node->pubkey = $keyspub; - } - return $this->validateAndSave($node, 'server'); - } - return array("result" => "failed"); - } - public function toggleServerAction($uuid) - { - return $this->toggleBase('servers.server', $uuid); - } -} diff --git a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/ServiceController.php b/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/ServiceController.php deleted file mode 100644 index 94fe65ecd6..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/Api/ServiceController.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * 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. - * - */ - -namespace OPNsense\Wireguard\Api; - -use OPNsense\Base\ApiMutableServiceControllerBase; -use OPNsense\Core\Backend; -use OPNsense\Wireguard\General; - -/** - * Class ServiceController - * @package OPNsense\Wireguard - */ -class ServiceController extends ApiMutableServiceControllerBase -{ - protected static $internalServiceClass = '\OPNsense\Wireguard\General'; - protected static $internalServiceTemplate = 'OPNsense/Wireguard'; - protected static $internalServiceEnabled = 'enabled'; - protected static $internalServiceName = 'wireguard'; - - /** - * show wireguard config - * @return array - */ - public function showconfAction() - { - $backend = new Backend(); - $response = $backend->configdRun("wireguard showconf"); - return array("response" => $response); - } - - /** - * show wireguard handshakes - * @return array - */ - public function showhandshakeAction() - { - $backend = new Backend(); - $response = $backend->configdRun("wireguard showhandshake"); - return array("response" => $response); - } -} diff --git a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/GeneralController.php b/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/GeneralController.php deleted file mode 100644 index 404fce6823..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/GeneralController.php +++ /dev/null @@ -1,40 +0,0 @@ - - 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. -*/ - -namespace OPNsense\Wireguard; - -class GeneralController extends \OPNsense\Base\IndexController -{ - public function indexAction() - { - $this->view->generalForm = $this->getForm("general"); - $this->view->formDialogEditWireguardClient = $this->getForm("dialogEditWireguardClient"); - $this->view->formDialogEditWireguardServer = $this->getForm("dialogEditWireguardServer"); - $this->view->pick('OPNsense/Wireguard/general'); - } -} diff --git a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/forms/dialogEditWireguardClient.xml b/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/forms/dialogEditWireguardClient.xml deleted file mode 100644 index 7cfca02ce0..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/forms/dialogEditWireguardClient.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - client.enabled - - checkbox - This will enable or disable the client config. - - - client.name - - text - Set the name for this instance. - - - client.pubkey - - text - Public key of this instance. - - - client.psk - - text - Shared secret (PSK) for this peer. - - - client.tunneladdress - - - select_multiple - true - List of addresses allowed to pass trough the tunnel adapter. Please use CIDR notation like 10.0.0.1/24. - - - client.serveraddress - - text - Set public IP address the endpoint listens to. - - - client.serverport - - text - Set port the endpoint listens to. - - - client.keepalive - - text - Set persistent keepalive interval. - - diff --git a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/forms/dialogEditWireguardServer.xml b/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/forms/dialogEditWireguardServer.xml deleted file mode 100644 index 3667908a88..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/forms/dialogEditWireguardServer.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - server.enabled - - checkbox - This will enable or disable the server config. - - - server.name - - text - Set the name for this instance. - - - server.instance - - info - Set the instance number needed for interface calculation. It has to be unique for each instance. - - - server.pubkey - - text - Public key of this instance. After saving you will see here your public key. - - - server.privkey - - text - Private key of this instance. After saving you will see here your private key, please keep it safe. - - - server.port - - text - Optionally set a fixed port for this instance to listen on. The standard port range starts at 51820. - - - server.mtu - - text - true - Set the interface MTU for this interface. Leaving empty uses the MTU from main interface which is fine for most setups. - - - server.dns - - select_multiple - - true - true - Set the interface specific DNS server. - - - server.tunneladdress - - - select_multiple - true - List of addresses to configure on the tunnel adapter. Please use CIDR notation like 10.0.0.1/24. - - - server.peers - - select_multiple - true - List of peers for this server. - - - server.disableroutes - - checkbox - This will prevent installing routes. Usually you only enable this to do own routing decisions via a local gateway and gateway rules. - - - server.gateway - - text - true - Set the gateway IP here when using Disable Routes feature. You also have to add this as a gateway in OPNsense. - - diff --git a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/forms/general.xml b/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/forms/general.xml deleted file mode 100644 index 7a74ebf81b..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/controllers/OPNsense/Wireguard/forms/general.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - general.enabled - - checkbox - This will activate WireGuard and start all enabled instances. - - diff --git a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/ACL/ACL.xml b/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/ACL/ACL.xml deleted file mode 100644 index 21012db805..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/ACL/ACL.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - VPN: Wireguard - - ui/wireguard/* - api/wireguard/* - - - diff --git a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Client.php b/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Client.php deleted file mode 100644 index b069a766d4..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Client.php +++ /dev/null @@ -1,31 +0,0 @@ - - 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. -*/ - -namespace OPNsense\Wireguard; - -use OPNsense\Base\BaseModel; - -class Client extends BaseModel -{ -} diff --git a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Client.xml b/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Client.xml deleted file mode 100644 index e547d80bd5..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Client.xml +++ /dev/null @@ -1,45 +0,0 @@ - - //OPNsense/wireguard/client - Wireguard Client configuration - 0.0.6 - - - - - 1 - Y - - - - Y - - - Y - Should be a base64-encoded 32 byte string. - - - N - Should be a base64-encoded 32 byte string. - - - - , - Y - Y - - - N - - - N - - - 1 - 86400 - Please specify a value between 1 and 86400. - N - - - - - diff --git a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/General.php b/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/General.php deleted file mode 100644 index 6caf9eabaf..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/General.php +++ /dev/null @@ -1,35 +0,0 @@ - - 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. -*/ - -namespace OPNsense\Wireguard; - -use OPNsense\Base\BaseModel; - -class General extends BaseModel -{ -} diff --git a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/General.xml b/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/General.xml deleted file mode 100644 index 432fd654c2..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/General.xml +++ /dev/null @@ -1,11 +0,0 @@ - - //OPNsense/wireguard/general - WireGuard configuration - 0.0.1 - - - 0 - Y - - - diff --git a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Menu/Menu.xml b/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Menu/Menu.xml deleted file mode 100644 index a2934e1b07..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Menu/Menu.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Server.php b/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Server.php deleted file mode 100644 index 8fccdd577e..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Server.php +++ /dev/null @@ -1,31 +0,0 @@ - - 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. -*/ - -namespace OPNsense\Wireguard; - -use OPNsense\Base\BaseModel; - -class Server extends BaseModel -{ -} diff --git a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Server.xml b/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Server.xml deleted file mode 100644 index 7b99312233..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/models/OPNsense/Wireguard/Server.xml +++ /dev/null @@ -1,80 +0,0 @@ - - //OPNsense/wireguard/server - Wireguard Server configuration - 0.0.2 - - - - - 1 - Y - - - - Y - /^([0-9a-zA-Z]){1,32}$/u - Should be a string between 1 and 32 characters. Allowed characters are 0-9, a-z, and A-Z - - - 0 - 19 - Maximum number of instances reached - Y - - - N - - - N - - - N - - - 1 - 9300 - N - - - N - /^([a-fA-F0-9\.:\[\]]*?,)*([a-fA-F0-9\.:\[\]]*)$/ - Please use valid IPv4 or IPv6 addresses. - - - - , - N - Y - - - 0 - Y - - - You have to enable Disable Routes option. - DependConstraint - - gateway - - - - - - N - - - - - - Y - N - Choose an Peer. - - - - - diff --git a/net/wireguard/src/opnsense/mvc/app/views/OPNsense/Wireguard/general.volt b/net/wireguard/src/opnsense/mvc/app/views/OPNsense/Wireguard/general.volt deleted file mode 100644 index 60c1c6c390..0000000000 --- a/net/wireguard/src/opnsense/mvc/app/views/OPNsense/Wireguard/general.volt +++ /dev/null @@ -1,190 +0,0 @@ -{# - # OPNsense (c) 2014-2018 by Deciso B.V. - # OPNsense (c) 2018 Michael Muenz - # 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':generalForm,'id':'frm_general_settings'])}} -
    -
    - -
    -
    -
    -
    -
    +
    - - . +
    - - - - - - - - - - - - - - - - - - -
    {{ lang._('Enabled') }}{{ lang._('Name') }}{{ lang._('Endpoint Address') }}{{ lang._('Allowed IPs') }}{{ lang._('ID') }}{{ lang._('Commands') }}
    - -
    -
    -
    - -

    -
    -
    -
    - - - - - - - - - - - - - - - - - - - -
    {{ lang._('Enabled') }}{{ lang._('Name') }}{{ lang._('Port') }}{{ lang._('Tunnel Address') }}{{ lang._('ID') }}{{ lang._('Commands') }}
    - -
    -
    -
    - -

    -
    -
    -
    -
    
    -    
    -
    -
    
    -    
    -
    - -{{ partial("layout_partials/base_dialog",['fields':formDialogEditWireguardClient,'id':'dialogEditWireguardClient','label':lang._('Edit Endpoint')])}} -{{ partial("layout_partials/base_dialog",['fields':formDialogEditWireguardServer,'id':'dialogEditWireguardServer','label':lang._('Edit Local Configuration')])}} - - diff --git a/net/wireguard/src/opnsense/scripts/OPNsense/Wireguard/genkey.sh b/net/wireguard/src/opnsense/scripts/OPNsense/Wireguard/genkey.sh deleted file mode 100755 index b580bf49dc..0000000000 --- a/net/wireguard/src/opnsense/scripts/OPNsense/Wireguard/genkey.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/sh - -# Copyright (c) 2018 Michael Muenz -# -# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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. - -TMPDIR="/tmp" -GENPRIV="/usr/local/bin/wg genkey" -GENPUB="/usr/local/bin/wg pubkey" - -cleanup() { - # Delete old files - rm -f $TMPDIR/wireguard.* -} - -private() { - # Generate a private key and put it to /tmp - umask 077 && ${GENPRIV} | tee ${TMPDIR}/wireguard.priv -} - -public() { - # Generate a public key and put it to /tmp - ${GENPUB} < ${TMPDIR}/wireguard.priv | tee ${TMPDIR}/wireguard.pub -} - -case "$1" in -private) - cleanup - private - ;; -public) - public - ;; -esac diff --git a/net/wireguard/src/opnsense/scripts/OPNsense/Wireguard/setup.sh b/net/wireguard/src/opnsense/scripts/OPNsense/Wireguard/setup.sh deleted file mode 100755 index 75ba580c99..0000000000 --- a/net/wireguard/src/opnsense/scripts/OPNsense/Wireguard/setup.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -mkdir -p /var/run/wireguard -chmod 755 /var/run/wireguard diff --git a/net/wireguard/src/opnsense/service/conf/actions.d/actions_wireguard.conf b/net/wireguard/src/opnsense/service/conf/actions.d/actions_wireguard.conf deleted file mode 100644 index 3257da26d0..0000000000 --- a/net/wireguard/src/opnsense/service/conf/actions.d/actions_wireguard.conf +++ /dev/null @@ -1,42 +0,0 @@ -[start] -command: - /usr/local/opnsense/scripts/OPNsense/Wireguard/setup.sh; - /usr/local/etc/rc.d/wireguard start; - /usr/local/etc/rc.routing_configure -parameters: -type:script -message:Starting WireGuard - -[stop] -command:/usr/local/etc/rc.d/wireguard stop -parameters: -type:script -message:Stopping WireGuard - -[restart] -command: - /usr/local/opnsense/scripts/OPNsense/Wireguard/setup.sh; - /usr/local/etc/rc.d/wireguard restart; - /usr/local/etc/rc.routing_configure -parameters: -type:script -message:Restarting WireGuard -description: Restart WireGuard - -[genkey] -command:/usr/local/opnsense/scripts/OPNsense/Wireguard/genkey.sh -parameters: %s -type:script_output -message:Generating WireGuard keys - -[showconf] -command:/usr/local/bin/wg show all -parameters: -type:script_output -message:Show WireGuard config - -[showhandshake] -command:/usr/local/bin/wg show all latest-handshakes -parameters: -type:script_output -message:Show WireGuard handshakes diff --git a/net/wireguard/src/opnsense/service/templates/OPNsense/Wireguard/+TARGETS b/net/wireguard/src/opnsense/service/templates/OPNsense/Wireguard/+TARGETS deleted file mode 100644 index 655def7d10..0000000000 --- a/net/wireguard/src/opnsense/service/templates/OPNsense/Wireguard/+TARGETS +++ /dev/null @@ -1,2 +0,0 @@ -wireguard:/etc/rc.conf.d/wireguard -wireguard-server.conf:/usr/local/etc/wireguard/wg[OPNsense.wireguard.server.servers.server.%.instance].conf diff --git a/net/wireguard/src/opnsense/service/templates/OPNsense/Wireguard/wireguard b/net/wireguard/src/opnsense/service/templates/OPNsense/Wireguard/wireguard deleted file mode 100644 index 5cbde5ecf5..0000000000 --- a/net/wireguard/src/opnsense/service/templates/OPNsense/Wireguard/wireguard +++ /dev/null @@ -1,22 +0,0 @@ -{% if helpers.exists('OPNsense.wireguard.general.enabled') and OPNsense.wireguard.general.enabled == '1' %} -wireguard_var_script="/usr/local/opnsense/scripts/OPNsense/Wireguard/setup.sh" -wireguard_enable="YES" -{% if helpers.exists('OPNsense.wireguard.server.servers.server') %} -{% set activeservers=[] %} -{% for servers in helpers.toList('OPNsense.wireguard.server.servers.server') %} -{% if servers.enabled == '1' %} -{% do activeservers.append("wg" + servers.instance) %} -{% endif %} -{% endfor %} -{% endif %} -wireguard_interfaces="{{ activeservers | join(' ') }}" -start_postcmd=opnsense_postcmd -opnsense_postcmd() -{ - for interface in ${wireguard_interfaces}; do - ifconfig ${interface} group wireguard - done -} -{% else %} -wireguard_enable="NO" -{% endif %} diff --git a/net/wireguard/src/opnsense/service/templates/OPNsense/Wireguard/wireguard-server.conf b/net/wireguard/src/opnsense/service/templates/OPNsense/Wireguard/wireguard-server.conf deleted file mode 100644 index e76aaf1fac..0000000000 --- a/net/wireguard/src/opnsense/service/templates/OPNsense/Wireguard/wireguard-server.conf +++ /dev/null @@ -1,52 +0,0 @@ -{% if helpers.exists('OPNsense.wireguard.general.enabled') and OPNsense.wireguard.general.enabled == '1' %} -{% if helpers.exists('OPNsense.wireguard.server.servers.server') %} -{% for server_list in helpers.toList('OPNsense.wireguard.server.servers.server') %} -{% if TARGET_FILTERS['OPNsense.wireguard.server.servers.server.' ~ loop.index0] or TARGET_FILTERS['OPNsense.wireguard.server.servers.server'] %} -{% if server_list.enabled == '1' %} -[Interface] -PrivateKey = {{ server_list.privkey }} -{% if server_list.tunneladdress|default('') != '' %} -Address = {{ server_list.tunneladdress }} -{% endif %} -{% if server_list.port|default('') != '' %} -ListenPort = {{ server_list.port }} -{% endif %} -{% if server_list.dns|default('') != '' %} -DNS = {{ server_list.dns }} -{% endif %} -{% if server_list.mtu|default('') != '' %} -MTU = {{ server_list.mtu }} -{% endif %} -{% if server_list.disableroutes == '1' %} -Table = off -{% endif %} -{% if server_list.disableroutes == '1' and server_list.gateway|default('') != '' %} -PostUp = route {{- ' -6' if ':' in server_list.gateway }} add {{ server_list.gateway }} -iface %i -PostDown = route {{- ' -6' if ':' in server_list.gateway }} del {{ server_list.gateway }} -iface %i -{% endif %} -{% if server_list.peers|default('') != '' %} -{% for peerlist in server_list.peers.split(",") %} -{% set peerlist2_data = helpers.getUUID(peerlist) %} -{% if peerlist2_data != {} and peerlist2_data.enabled == '1' %} - -[Peer] -PublicKey = {{ peerlist2_data.pubkey }} -{% if peerlist2_data.psk|default('') != '' %} -PresharedKey = {{ peerlist2_data.psk }} -{% endif %} -{% if peerlist2_data.serveraddress|default('') != '' %} -Endpoint = {{ peerlist2_data.serveraddress }}{% if peerlist2_data.serverport|default('') != '' %}:{{ peerlist2_data.serverport }}{% else %}:51820{% endif %} -{% endif %} - -AllowedIPs = {{ peerlist2_data.tunneladdress }} -{% if peerlist2_data.keepalive|default('') != '' %} -PersistentKeepalive = {{ peerlist2_data.keepalive }} -{% endif %} -{% endif %} -{% endfor %} -{% endif %} -{% endif %} -{% endif %} -{% endfor %} -{% endif %} -{% endif %} diff --git a/net/wireguard/src/www/widgets/include/wireguard.inc b/net/wireguard/src/www/widgets/include/wireguard.inc deleted file mode 100644 index b95fc1fa6f..0000000000 --- a/net/wireguard/src/www/widgets/include/wireguard.inc +++ /dev/null @@ -1,4 +0,0 @@ - - - - - - - - - - - - - 0): - $dt = new DateTime("@$epoch"); - $dt->setTimezone(new DateTimeZone(date_default_timezone_get())); - $latest = $dt->format(gettext("Y-m-d H:i:sP")); - endif; ?> - - - - - - - - - - - - - - - - - - -
    ...
    diff --git a/net/wol/Makefile b/net/wol/Makefile index 818fde3268..90f4508aac 100644 --- a/net/wol/Makefile +++ b/net/wol/Makefile @@ -1,7 +1,7 @@ PLUGIN_NAME= wol -PLUGIN_VERSION= 2.4 +PLUGIN_VERSION= 2.5 +PLUGIN_REVISION= 3 PLUGIN_DEPENDS= wol PLUGIN_COMMENT= Wake on LAN Service -PLUGIN_MAINTAINER= franco@opnsense.org .include "../../Mk/plugins.mk" diff --git a/net/wol/src/opnsense/mvc/app/controllers/OPNsense/Wol/Api/WolController.php b/net/wol/src/opnsense/mvc/app/controllers/OPNsense/Wol/Api/WolController.php index e56554a27b..7b7841b5e7 100644 --- a/net/wol/src/opnsense/mvc/app/controllers/OPNsense/Wol/Api/WolController.php +++ b/net/wol/src/opnsense/mvc/app/controllers/OPNsense/Wol/Api/WolController.php @@ -66,27 +66,32 @@ public function delHostAction($uuid) { $this->delBase('wolentry', $uuid); } + public function searchHostAction() { - return $this->searchBase('wolentry', array("interface", "mac", "descr")); + return $this->searchBase('wolentry', ['interface', 'mac', 'descr']); } + public function getHostAction($uuid = null) { - $this->sessionClose(); return $this->getBase('host', 'wolentry', $uuid); } + public function getwakeAction() { return $this->getBase('wake', 'wolentry', null); } + public function addHostAction() { return $this->addBase('host', 'wolentry'); } + public function setHostAction($uuid) { return $this->setBase('host', 'wolentry', $uuid); } + public function wakeallAction() { if (!$this->request->isPost()) { @@ -100,6 +105,7 @@ public function wakeallAction() } return $results; } + private function wakeHostByNode($wolent, &$result) { $backend = new Backend(); @@ -114,6 +120,7 @@ private function wakeHostByNode($wolent, &$result) $broadcast_ip = escapeshellarg($this->calculateSubnetBroadcast($ipaddr, $cidr)); $result['status'] = trim($backend->configdRun("wol wake {$broadcast_ip} " . escapeshellarg((string)$wolent->mac))); } + private function getInterfaceIP($if) { $cfg = Config::getInstance()->object(); @@ -128,6 +135,7 @@ private function getInterfaceIP($if) return null; } } + private function getInterfaceSubnet($if) { $cfg = Config::getInstance()->object(); @@ -137,6 +145,7 @@ private function getInterfaceSubnet($if) return null; } } + private function calculateSubnetBroadcast($ip_addr, $cidr) { // TODO undefined offset diff --git a/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/ACL/ACL.xml b/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/ACL/ACL.xml index 43a21eb742..5103b5c239 100644 --- a/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/ACL/ACL.xml +++ b/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/ACL/ACL.xml @@ -3,7 +3,7 @@ Services: Wake on LAN ui/wol/* - api/wol/* + api/wol/wol/* diff --git a/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/Wol.xml b/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/Wol.xml index d4fb195305..9fddd8987f 100644 --- a/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/Wol.xml +++ b/net/wol/src/opnsense/mvc/app/models/OPNsense/Wol/Wol.xml @@ -3,27 +3,20 @@ Wake On Lan configuration 1.0.0 - - - Y - N - - - /^(?!0).*$/ - - - - Y - N - /^((?:[a-fA-F0-9]{2}:){5}(?:[a-fA-F0-9]{2}))$/ - 00:00:00:00:00:00 - Should be 6 groups of 2 hex characters (a-fA-F0-9) separated by ':' - - - N - N - - - + + + Y + + /^(?!0).*$/ + + + + Y + /^((?:[a-fA-F0-9]{2}:){5}(?:[a-fA-F0-9]{2}))$/ + 00:00:00:00:00:00 + Should be 6 groups of 2 hex characters (a-fA-F0-9) separated by ':'. + + + diff --git a/net/wol/src/opnsense/mvc/app/views/OPNsense/Wol/index.volt b/net/wol/src/opnsense/mvc/app/views/OPNsense/Wol/index.volt index 0e4454452c..61dd88fd0c 100644 --- a/net/wol/src/opnsense/mvc/app/views/OPNsense/Wol/index.volt +++ b/net/wol/src/opnsense/mvc/app/views/OPNsense/Wol/index.volt @@ -41,16 +41,16 @@ $( document ).ready(function() { var grid = $("#grid-wol-settings").UIBootgrid( - { 'search':'/api/wol/wol/searchHost', - 'get':'/api/wol/wol/getHost/', - 'set':'/api/wol/wol/setHost/', - 'add':'/api/wol/wol/addHost/', - 'del':'/api/wol/wol/delHost/', + { 'search':'/api/wol/wol/search_host', + 'get':'/api/wol/wol/get_host/', + 'set':'/api/wol/wol/set_host/', + 'add':'/api/wol/wol/add_host/', + 'del':'/api/wol/wol/del_host/', 'options':{ selection:false, multiSelect:false, formatters: { - "commandswithwake": function (column, row) { + "commands": function (column, row) { return " " + " " + "" + @@ -111,7 +111,7 @@ $( document ).ready(function() { {{ lang._('Interface') }} {{ lang._('MAC') }} {{ lang._('Description') }} - {{ lang._('Commands') }} + {{ lang._('Commands') }} diff --git a/net/wol/src/opnsense/service/conf/actions.d/actions_wol.conf b/net/wol/src/opnsense/service/conf/actions.d/actions_wol.conf index bc90627a32..696fc145d1 100644 --- a/net/wol/src/opnsense/service/conf/actions.d/actions_wol.conf +++ b/net/wol/src/opnsense/service/conf/actions.d/actions_wol.conf @@ -2,3 +2,5 @@ command:/usr/local/bin/wol -i parameters: %s %s type:script +description:Wake-On-LAN for host with broadcast IP and MAC +message:Waking up host %s %s diff --git a/net/wol/src/opnsense/www/js/widgets/Metadata/WakeOnLan.xml b/net/wol/src/opnsense/www/js/widgets/Metadata/WakeOnLan.xml new file mode 100644 index 0000000000..aa46ac5dfd --- /dev/null +++ b/net/wol/src/opnsense/www/js/widgets/Metadata/WakeOnLan.xml @@ -0,0 +1,17 @@ + + + WakeOnLan.js + + /api/diagnostics/interface/get_arp* + /api/wol/wol/search_host + /api/wol/wol/set + + + WakeOnLan + No saved WOL addresses + Device + Interface + Status + + + diff --git a/net/wol/src/opnsense/www/js/widgets/WakeOnLan.js b/net/wol/src/opnsense/www/js/widgets/WakeOnLan.js new file mode 100644 index 0000000000..be5f12b575 --- /dev/null +++ b/net/wol/src/opnsense/www/js/widgets/WakeOnLan.js @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 Michał Brzeziński + * 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. + */ + +export default class WakeOnLan extends BaseTableWidget { + constructor() { + super(); + } + + getGridOptions() { + return { + // trigger overflow-y:scroll after 650px height + sizeToContent: 650 + } + } + + getMarkup() { + let $container = $('
    '); + let $wol_table = this.createTable('wol-table', { + headerPosition: 'none' + }); + + $container.append($wol_table); + return $container; + } + + async onWidgetTick() { + const data = await this.ajaxCall('/api/wol/wol/search_host'); + + let rows = []; + if (data.total == 0) { + const empty_list = [`${this.translations.msg_empty_wol}`]; + rows.push(empty_list); + } else { + const header = [`${this.translations.h_device}`, + `${this.translations.h_interface}`, + `${this.translations.h_status}`, + '']; + rows.push(header); + + //NOTE: this ARP list call is the most expensive one and can grow substantialy in big networks + //With previous widget it had been done on the backend side with direct exec of 'arp -an | grep ...' + const arp = await this.ajaxCall(`/api/diagnostics/interface/get_arp${''}`); + + for(let it = 0; it < data.rows.length; it++){ + const item = data.rows[it]; + let is_active = this.checkActive(arp, item.mac, item["%interface"]); + let row = [ + `${item.descr.length !== 0 ? item.descr + '
    ': ''} ${item.mac}`, + `${item.interface}`, + ` + ${ is_active == 1 ? "Online" : "Offline"}`, + `` + ]; + rows.push(row); + } + } + super.updateTable('wol-table', rows); + + $('.wakeupbtn').on('click', async (event) => { + event.preventDefault(); + let btn = $(event.currentTarget).find('i'); + /* the call is quick, omit fa-spinner fa-pulse use */ + const data = {uuid: $(event.currentTarget).data('uuid')}; + const result = await this.ajaxCall('/api/wol/wol/set', JSON.stringify(data), 'POST').then(() => { + btn.removeClass('fa-bolt').addClass('fa-check'); + }); + event.currentTarget.blur(); + }); + } + + checkActive(arp_list, mac, intf) { + const arp = arp_list.find((obj) => obj.mac === mac.toLowerCase()); + if (arp === undefined) { + return 0; + } else { + return (arp.expired === false && arp.intf_description === intf); + } + } +} diff --git a/net/wol/src/www/widgets/include/wake_on_lan.inc b/net/wol/src/www/widgets/include/wake_on_lan.inc deleted file mode 100644 index db09f7f1d4..0000000000 --- a/net/wol/src/www/widgets/include/wake_on_lan.inc +++ /dev/null @@ -1,4 +0,0 @@ - - - - - - - - - - - -wolentry->iterateItems() as $wolent): - $is_active = exec("/usr/sbin/arp -an |/usr/bin/grep -i {$wolent->mac}| /usr/bin/wc -l|/usr/bin/awk '{print $1;}'");?> - - - - - - -wolentry->iterateItems())) == 0):?> - - - - - - - - - - -
    descr) ? $wolent->descr : gettext('Unnamed entry') ?>
    mac ?>
    interface));?> - fa-fw text-" > - - - -
    - diff --git a/net/zerotier/Makefile b/net/zerotier/Makefile index bc69d92c72..ae7f99b1ee 100644 --- a/net/zerotier/Makefile +++ b/net/zerotier/Makefile @@ -1,8 +1,7 @@ PLUGIN_NAME= zerotier PLUGIN_VERSION= 1.3.2 -PLUGIN_REVISION= 3 +PLUGIN_REVISION= 6 PLUGIN_COMMENT= Virtual Networks That Just Work PLUGIN_DEPENDS= zerotier -PLUGIN_MAINTAINER= dharrigan@gmail.com .include "../../Mk/plugins.mk" diff --git a/net/zerotier/src/etc/inc/plugins.inc.d/zerotier.inc b/net/zerotier/src/etc/inc/plugins.inc.d/zerotier.inc index 3f2cd08d1a..68d740538b 100644 --- a/net/zerotier/src/etc/inc/plugins.inc.d/zerotier.inc +++ b/net/zerotier/src/etc/inc/plugins.inc.d/zerotier.inc @@ -59,3 +59,8 @@ function zerotier_services() return $services; } + +function zerotier_devices() +{ + return [['pattern' => '^zt', 'spoofmac' => false, 'volatile' => true]]; +} diff --git a/net/zerotier/src/opnsense/mvc/app/controllers/OPNsense/Zerotier/Api/NetworkController.php b/net/zerotier/src/opnsense/mvc/app/controllers/OPNsense/Zerotier/Api/NetworkController.php index 03dde0179a..72c0550496 100644 --- a/net/zerotier/src/opnsense/mvc/app/controllers/OPNsense/Zerotier/Api/NetworkController.php +++ b/net/zerotier/src/opnsense/mvc/app/controllers/OPNsense/Zerotier/Api/NetworkController.php @@ -39,13 +39,11 @@ class NetworkController extends ApiMutableModelControllerBase { - protected static $internalModelName = 'Zerotier'; protected static $internalModelClass = '\OPNsense\Zerotier\Zerotier'; public function searchAction() { - $this->sessionClose(); $mdlZerotier = $this->getModel(); $grid = new UIModelGrid($mdlZerotier->networks->network); return $grid->fetchBindRequest( diff --git a/net/zerotier/src/opnsense/mvc/app/controllers/OPNsense/Zerotier/Api/SettingsController.php b/net/zerotier/src/opnsense/mvc/app/controllers/OPNsense/Zerotier/Api/SettingsController.php index ac37bcf878..dd1dcc4bbb 100644 --- a/net/zerotier/src/opnsense/mvc/app/controllers/OPNsense/Zerotier/Api/SettingsController.php +++ b/net/zerotier/src/opnsense/mvc/app/controllers/OPNsense/Zerotier/Api/SettingsController.php @@ -39,7 +39,6 @@ class SettingsController extends ApiMutableModelControllerBase { - protected static $internalModelName = 'Zerotier'; protected static $internalModelClass = '\OPNsense\Zerotier\Zerotier'; diff --git a/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.php b/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.php index a16fa8f8aa..c4067fc9de 100644 --- a/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.php +++ b/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.php @@ -35,5 +35,4 @@ class Zerotier extends BaseModel { - } diff --git a/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.xml b/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.xml index d081852fbb..bbd7750f6a 100644 --- a/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.xml +++ b/net/zerotier/src/opnsense/mvc/app/models/OPNsense/Zerotier/Zerotier.xml @@ -1,35 +1,24 @@ //OPNsense/zerotier - - Zerotier - Virtual Networks That Just Work. - + Zerotier configuration 1.3.0 - 0 + 0 Y - - - N - - - N - + + - 0 + 0 Y - Y - - - N - + diff --git a/net/zerotier/src/opnsense/mvc/app/views/OPNsense/Zerotier/index.volt b/net/zerotier/src/opnsense/mvc/app/views/OPNsense/Zerotier/index.volt index 55eb6159e9..f71aedc927 100644 --- a/net/zerotier/src/opnsense/mvc/app/views/OPNsense/Zerotier/index.volt +++ b/net/zerotier/src/opnsense/mvc/app/views/OPNsense/Zerotier/index.volt @@ -98,10 +98,10 @@ POSSIBILITY OF SUCH DAMAGE. - - - - + + + + diff --git a/net/zerotier/src/opnsense/service/templates/OPNsense/zerotier/zerotier b/net/zerotier/src/opnsense/service/templates/OPNsense/zerotier/zerotier index 78740ebaaf..b7faa41054 100644 --- a/net/zerotier/src/opnsense/service/templates/OPNsense/zerotier/zerotier +++ b/net/zerotier/src/opnsense/service/templates/OPNsense/zerotier/zerotier @@ -6,4 +6,3 @@ zerotier_enable="YES" {% else %} zerotier_enable="NO" {% endif %} -zerotier_var_mfs="/var/db/zerotier-one" diff --git a/ruleset.xml b/ruleset.xml deleted file mode 100644 index ff88d20ce9..0000000000 --- a/ruleset.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - Slightly enhanced version of the PSR12 standard - - - - - *.css - *.js - diff --git a/security/acme-client/Makefile b/security/acme-client/Makefile index eab7942448..9f8906b31e 100644 --- a/security/acme-client/Makefile +++ b/security/acme-client/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= acme-client -PLUGIN_VERSION= 3.5 +PLUGIN_VERSION= 4.13 PLUGIN_COMMENT= ACME Client PLUGIN_MAINTAINER= opnsense@moov.de PLUGIN_DEPENDS= acme.sh py${PLUGIN_PYTHON}-dns-lexicon diff --git a/security/acme-client/pkg-descr b/security/acme-client/pkg-descr index 07084fd483..9fbe37ce3b 100644 --- a/security/acme-client/pkg-descr +++ b/security/acme-client/pkg-descr @@ -8,6 +8,379 @@ WWW: https://github.com/acmesh-official/acme.sh Plugin Changelog ================ +4.13 + +Added: +* add support for ACME profiles (#5154) +* add support for deploy hook "Ruckus" (#5157) +* add support for Spaceship.com DNS API (#5158) +* add global access key option for TransIP DNS API (#5166) + +Changed: +* allow setting renewal interval to 0 (#5168) + +Fixed: +* remove duplicate slashes in nsupdate, TransIP, OPNsense, TLS ALPN challenge types (#5166) + +4.12 + +Added: +* new automation to upload to Zyxel GS1900 series switches (#5080) +* add support for Technitium DNS API (#5111) +* add support for Hurricane Electric DDNS API (#5138) +* add support for Timeweb Cloud DNS API (#5149) + +Fixed: +* fix mijn.host and scaleway DNS API settings (#5146) + +4.11 + +Added: +* add support for Hetzner Cloud DNS API (#5020) +* add support for Selectel.ru V2 API (#4824) +* add support for mijn.host DNS API (#4446) +* add support for AzureDNS System Assigned Managed Identity #4830 (4830) +* add support for ZoneEdit DNS API (#4671) + +Changed: +* use mwexec/file_safe for HTTP-01/TLSALPN challenge types (core #9325) + +Fixed: +* fix deprecated PHP syntax in cron settings (#4824) + +Deprecated: +* deprecate support for HTTP-01 challenge types (no removal date yet) + +4.10 + +Added: +* new automation to reload www/caddy (#4692) +* new automation to upload to Proxmox Backup Server (#4785) +* add support for Websupport.sk DNS API (#4540) +* add SFTP option: Preserve Modification Time (#3862) + +Changed: +* automatically fix account config if CERT_HOME is set (#4622) +* automatically resolve cron job mismatch (#4627) +* change default SFTP options to NOT preserve modification time (#3862) +* migrate SFTP/SSH operations to AcmeClient logging +* add detailed SFTP/SSH logging when debug logging is enabled +* add new log messages and improve existing ones + +Fixed: +* deploy hooks may use the old CERT_HOME (#4622) +* acme.sh is always called with "--days 1" (#4711) +* avoid startup error: "rmdir... Not a directory" (#4743) +* fails to create/update cron job on UUID mismatch (#4627) +* accounts/certificates not visible on 25.7 alpha (#4790) + +Removed: +* remove stdout (CLI) logging from SFTP library + +4.9 + +Added: +* Add support for Scaleway DNS API (#4492) + +4.8 + +BREAKING CHANGE: Let's Encrypt ends support for the OCSP Must Staple +extension on 30.01.2025. Issuance requests will fail if this option is +still enabled past this date. + +Changed: +* Add note regarding the support of OCSP + +Fixed: +* SFTP automation unable to transfer certs (#4477) + +4.7 + +Added: +* Add support for MyDNS.JP DNS API (#4328) +* Add support for fornex DNS API (#4389) +* Add support for OTP Code to Synology deploy hook (#4045) +* Add support for Shared Secret to INWX DNS API (#3942) + +Changed: +* Convert Synology deploy hook variables to uppercase (#4286) +* Migrate to MVC Trust storage + +Fixed: +* SFTP/SSH automation results in fatal PHP error (#4363) +* Typo in INWX password field name +* Certs not fully functional after import into Trust storage (#4401) + +4.6 + +Added: +* add Lima-City DNS API (#3566) + +Fixed: +* fix Hashicorp Vault support, added Vault token (#4270) +* fix acme.sh error: "Cannot find dns api hook for..." + +4.5 + +Fixed: +* fix empty System Log +* avoid unnecessary error log messages (#3860) +* don't log errors for successful commands (#3955) +* fix more PHP deprecation messages (#4008) + +4.4 + +Fixed: +* fix EasyDNS variable assignment (#4068) + +4.3 + +Fixed: +* fix acme.sh debug log levels (#3933) + +4.2 + +Added: +* add ArtFiles DNS API (#3866) +* add dnsHome DNS API (#3882) +* add Oracle Cloud Infrastructure DNS API (#3901) + +Fixed: +* fix PHP deprecation messages (#3892) + +4.1 + +Fixed: +* automation does not start (#3790) +* bugfixes for HTTP-01 and TLS-ALPN-01 challenge types (#3813) + +4.0 + +NOTE: This is a new major release with backwards-incompatible changes. +Downgrade to older releases is not supported. Be sure to create a +full backup and include /var/etc/acme-client. + +NOTE: Lexicon is deprecated and most configurations are converted +to native acme.sh DNS APIs. However, in some case manual reconfiguration +may be necessary. + +Added: +* add nic.ru DNS API (#3684) +* add Aurora DNS API +* add ConoHa DNS API +* add Constellix DNS API +* add Exoscale DNS API +* add internetbs.net DNS API +* add PointHQ DNS API +* add Rackspace DNS API +* add rage4 DNS API + +Changed: +* use a dedicated acme.sh runtime directory for every cert (#3127) +* migrate lexicon provider aliyun to acme.sh DNS API (#2920) +* migrate lexicon provider aurora to acme.sh DNS API (#2920) +* migrate lexicon provider azure to acme.sh DNS API (#2920) +* migrate lexicon provider cloudflare to acme.sh DNS API (#2920) +* migrate lexicon provider cloudns to acme.sh DNS API (#2920) +* migrate lexicon provider cloudxns to acme.sh DNS API (#2920) +* migrate lexicon provider conoha to acme.sh DNS API (#2920) +* migrate lexicon provider constellix to acme.sh DNS API (#2920) +* migrate lexicon provider digitalocean to acme.sh DNS API (#2920) +* migrate lexicon provider directadmin to acme.sh DNS API (#2920) +* migrate lexicon provider dnsimple to acme.sh DNS API (#2920) +* migrate lexicon provider dnsmadeeasy to acme.sh DNS API (#2920) +* migrate lexicon provider dnspod to acme.sh DNS API (#2920) +* migrate lexicon provider dreamhost to acme.sh DNS API (#2920) +* migrate lexicon provider easydns to acme.sh DNS API (#2920) +* migrate lexicon provider exoscale to acme.sh DNS API (#2920) +* migrate lexicon provider gandi to acme.sh DNS API (#2920) +* migrate lexicon provider godaddy to acme.sh DNS API (#2920) +* migrate lexicon provider googleclouddns to acme.sh DNS API (#2920) +* migrate lexicon provider gratisdns to acme.sh DNS API (#2920) +* migrate lexicon provider henet to acme.sh DNS API (#2920) +* migrate lexicon provider hetzner to acme.sh DNS API (#2920) +* migrate lexicon provider infoblox to acme.sh DNS API (#2920) +* migrate lexicon provider internetbs to acme.sh DNS API (#2920) +* migrate lexicon provider inwx to acme.sh DNS API (#2920) +* migrate lexicon provider linode to acme.sh DNS API (#2920) +* migrate lexicon provider linode4 to acme.sh DNS API (#2920) +* migrate lexicon provider luadns to acme.sh DNS API (#2920) +* migrate lexicon provider namecheap to acme.sh DNS API (#2920) +* migrate lexicon provider namesilo to acme.sh DNS API (#2920) +* migrate lexicon provider netcup to acme.sh DNS API (#2920) +* migrate lexicon provider nfsn to acme.sh DNS API (#2920) +* migrate lexicon provider nsone to acme.sh DNS API (#2920) +* migrate lexicon provider online to acme.sh DNS API (#2920) +* migrate lexicon provider ovh to acme.sh DNS API (#2920) +* migrate lexicon provider plesk to acme.sh DNS API (#2920) +* migrate lexicon provider pointhq to acme.sh DNS API (#2920) +* migrate lexicon provider powerdns to acme.sh DNS API (#2920) +* migrate lexicon provider rackspace to acme.sh DNS API (#2920) +* migrate lexicon provider rage4 to acme.sh DNS API (#2920) +* migrate lexicon provider route53 to acme.sh DNS API (#2920) +* migrate lexicon provider transip to acme.sh DNS API (#2920) +* migrate lexicon provider vultr to acme.sh DNS API (#2920) +* migrate lexicon provider yandex to acme.sh DNS API (#2920) +* migrate lexicon provider zeit to acme.sh DNS API (#2920) +* migrate lexicon provider zilore to acme.sh DNS API (#2920) +* migrate lexicon provider zonomi to acme.sh DNS API (#2920) + +Fixed: +* fix sporadic command failure with gcloud DNS API (#3745) +* fix errors when the same Common Name is used multiple times (#3127) +* fix crash on invalid automations (#3752) + +Deprecated: +* deprecate support for lexicon DNS APIs (no removal date yet) + +3.20 + +Added: +* add Bunny DNS API (#3715) +* add DNSExit DNS API (#3724) +* add World4You DNS API (#3722) + +Changed: +* support token authentication in Gandi LiveDNS (#3526) + +Fixed: +* fix 2FA support in Synology automation (#3627) + +Removed: +* remove automation: Highwinds CDN (#3626) + +3.19 + +Added: +* add IPv64.net DNS API (#3504) + +Fixed: +* fix Proxmox VE deployhook (#3517) + +3.18 + +Added: +* add support for TrueNAS deployhook (#3421) +* add support for Proxmox VE deployhook (#3422) +* add server,port and node name values for Proxmox VE deployhook (#3516) +* add Google Domains DNS API + +3.17 + +Added: +* add DNS.services DNS API (#3399) +* add RegRu DNS API (#3359) + +3.16 + +Added: +* add RegRu DNS API +* add JD Cloud DNS API (#3315) +* new automation: deploy certificates on Palo Alto Networks Firewall (#3289) + +3.15 + +Added: +* add online.net DNS API (#3213) + +Changed: +* increase max value for certificate renewal interval (#3219) + +3.14 + +NOTE: Users of Selfhost need to manually fix their configuration, see +https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#151-use-selfhost-dns-api + +Added: +* add support for Google CA (#3029) +* add support for querying public DNS services (#3079) + +Fixed: +* fix Selfhost DNS API (#3122) +* fix invalid cert state due to deploy error (#3120) + +Changed: +* change default DNS sleep time to 0 (#3079) +* remove saved deploy hook from acme.sh config files (#3120) + +3.13 + +Added: +* add Mythic Beasts DNS API (#2998) + +3.12 + +Added: +* add Simply.com DNS API (#2888) +* add Active24 challenge type (#3049) +* add united-domains Reselling challenge type (#3066) +* add support for Zone ID in Cloudflare challenge type (#2973) +* new automation: upload certificate to Vault (#2796) + +Fixed: +* re-order function parameters due to PHP8 deprecation notice (#3043) + +Changed: +* simplyfi DNS service names +* relax port number restriction in SSH/SFTP automations (#3005) + +3.11 + +Fixed: +* add missing - - - action.highwinds_account_hash - - text - Account hash for Highwinds API. - - - action.highwinds_access_token - - text - Access token for Highwinds API. - header @@ -85,6 +68,12 @@ The path can be absolute or relative to home and must exist. Leave blank to not change path after login. + + action.sftp_modtime + + checkbox + Preserves modification times from the source file. Note that this is not supported by all SFTP servers and may cause the upload to fail, e.g. on VMware + action.sftp_chmod @@ -142,6 +131,49 @@ Leave blank to use default "{{name}}/fullchain.pem". true + + + header + + + + action.remote_ssh_host + + text + IP address or hostname of the SSH server. + + + action.remote_ssh_port + + text + SSH server port. Leave blank to use default "22". + true + + + action.remote_ssh_key + + text + SSH server host key, formatted as in 'known_hosts'. + Leave blank to auto accept host key on first connect (not as secure as specifying it). + + + action.remote_ssh_user + + text + The username to login to the SSH server. + + + action.remote_ssh_identity_type + + dropdown + The type of identify to present to the SSH server for authorization. Select 'none' to use default "ECDSA". + + + action.remote_ssh_command + + text + The command to execute on the SSH server. + header @@ -162,7 +194,7 @@ action.acme_synology_dsm_hostname text - Hostname of IP adress of the Synology DSM, i.e. synology.example.com or 192.168.0.1. + Hostname of IP address of the Synology DSM, i.e. synology.example.com or 192.168.0.1. action.acme_synology_dsm_port @@ -193,6 +225,18 @@ text If Synology DSM has OTP enabled, then the device ID has to be provided so that no OTP is required when running the automation. + + action.acme_synology_dsm_devicename + + text + If Synology DSM has OTP enabled, then the device name has to be provided so that no OTP is required when running the automation. + + + action.acme_synology_dsm_otpcode + + text + If Synology DSM has OTP enabled, then a OTP may be required. + action.acme_synology_dsm_create @@ -220,4 +264,232 @@ password + + + header + + + + action.acme_panos_username + + text + + + action.acme_panos_password + + password + + + action.acme_panos_host + + text + + + + header + + + + action.acme_proxmoxve_user + + text + The user who owns the API key. Defaults to root. + + + action.acme_proxmoxve_server + + text + The hostname of the proxmox ve node. + + + action.acme_proxmoxve_port + + text + The port number the management interface is on. Defaults to 8006. + + + action.acme_proxmoxve_nodename + + text + The name of the node we will be connecting to. + + + action.acme_proxmoxve_realm + + text + The authentication realm the user authenticates with. Defaults to pam. + + + action.acme_proxmoxve_tokenid + + text + The name of the API token created for the user account. Defaults to acme. + + + action.acme_proxmoxve_tokenkey + + text + The API token. Required. + + + + header + + + + action.acme_proxmoxbs_user + + text + The user who owns the API key. Defaults to root. + + + action.acme_proxmoxbs_server + + text + The hostname of the proxmox BS node. + + + action.acme_proxmoxbs_port + + text + The port number the management interface is on. Defaults to 8007. + + + action.acme_proxmoxbs_realm + + text + The authentication realm the user authenticates with. Defaults to pam. + + + action.acme_proxmoxbs_tokenid + + text + The name of the API token created for the user account. Defaults to acme. + + + action.acme_proxmoxbs_tokenkey + + text + The API token. Required. + + + + header + + + + + header + + + + action.acme_ruckus_host + + text + + + + action.acme_ruckus_user + + text + + + + action.acme_ruckus_pass + + password + + + action.acme_truenas_apikey + + text + API key generated in the TrueNAS web UI. + + + action.acme_truenas_hostname + + text + Hostname or IP address of TrueNAS Core Server. + + + action.acme_truenas_scheme + + dropdown + Connection scheme that will be used when uploading certificates to TrueNAS Core Server. + + + + header + + + + action.acme_unifi_keystore + + text + Path to the Unifi keystore file in the local filesystem, i.e. /usr/local/share/java/unifi/data/keystore. + + + + header + + + + action.acme_vault_url + + text + URL of the Vault, i.e. http://vault.example.com:8200. + + + action.acme_vault_prefix + + text + This specifies the prefix path in Vault. If you select KV v2 you need to add .../data/... between the secret-mount-path and the path. Example: v1 prefix path: secret/acme, v2 prefix path: secret/data/acme. + + + action.acme_vault_token + + password + This specifies the Vault token to authenticate with. + + + action.acme_vault_kvv2 + + checkbox + If checked version 2 of the kv store will be used, otherwise version 1. If you use v2 please consider the comment in the field "Vault Prefix". + + + + header + + + + action.acme_zyxel_gs1900_host + + text + Hostname or IP adress of a Zyxel GS1900 series switch. + + + action.acme_zyxel_gs1900_user + + text + A user configured as an adminstrator. Defaults to admin. + + + action.acme_zyxel_gs1900_password + + + password + + + action.acme_zyxel_gs1900_insecure + + checkbox + If checked the deployment will be run in insecure mode and will skip certificate validation when connecting to the switch. + + + action.acme_zyxel_gs1900_reboot + + checkbox + If checked the switch will be rebooted once the certificate has been successfully installed. + diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml index ead828f216..4c05f7706c 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml @@ -56,7 +56,7 @@ certificate.renewIntervaltext - + @@ -68,11 +68,21 @@ dropdown + + + info + certificate.ocsp checkbox - Generate and add OCSP Must Staple extension to the certificate. + Generate and add OCSP Must Staple extension to the certificate. When this option is enabled and issueance/renewal requests fail, then this extension is probably not supported by the CA. + + + certificate.profile + + text + @@ -84,6 +94,7 @@ select_multiple true + true Choose the automations that should be run after certificate creation and renewal. Basically every application requires a quick restart to reload the updated certificate. If you don't configure an automation, the in-memory certificate may expire and cause security warnings and other issues. diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml index 36a2853568..d8c86d6e64 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml @@ -78,6 +78,42 @@ true Choose the local HAProxy frontends. They will automatically be configured to redirect acme challenges to the internal acme client. The HAProxy service will automatically be restarted if a certificate was renewed. + + + header + + + + validation.tlsalpn_service + + dropdown + + + + header + + + + validation.tlsalpn_acme_autodiscovery + + checkbox +
    NOTE:This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.
    ]]>
    +
    + + validation.tlsalpn_acme_interface + + dropdown +
    NOTE:This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.
    ]]>
    +
    + + validation.tlsalpn_acme_ipaddresses + + select_multiple + + true +
    NOTE:This will ONLY work if the official IP addresses are LOCALLY configured on your OPNsense firewall.
    ]]>
    + Enter IP addresses here. Finish each with TAB. +
    header @@ -90,9 +126,20 @@ validation.dns_sleep - + + text + The time in seconds to wait for all the TXT records to take effect after adding them to the DNS API. Defaults to 0 seconds, which causes Acme Client to check public DNS services every 10 seconds for up to 20 minutes. If set to a non-zero value, a fixed DNS sleep time will be used and the local DNS servers will be queried instead. A DNS sleep time of 120 seconds or more is recommended for some DNS APIs. + 0 for public DNS check or 1-84600s + + + + header + + + + validation.dns_active24_token + text - The time in seconds to wait for all the TXT records to take effect after adding them to the DNS API. Defaults to 120 seconds. @@ -179,6 +226,22 @@ text + + validation.dns_azuredns_managedidentity + + checkbox + documentation.]]> + + + + header + + + + validation.dns_bunny_api_key + + text + header @@ -217,6 +280,12 @@ password The token needs "Read" access to Zone.Zone and "Edit" access to Zone.DNS across all zones from an account. + + validation.dns_cf_zone_id + + text + Note that specifying a Zone ID will limit this configuration to a single domain. + header @@ -267,7 +336,7 @@ password - + header @@ -303,6 +372,41 @@ text + + + header + + + + validation.dns_dnsexit_auth_user + + text + + + validation.dns_dnsexit_auth_pass + + password + + + validation.dns_dnsexit_api + + text + + + + header + + + + validation.dns_dnshome_password + + password + + + validation.dns_dnshome_subdomain + + text + header @@ -314,6 +418,21 @@ text Note that this is the account token not the user token. + + + header + + + + validation.dns_dnsservices_user + + text + + + validation.dns_dnsservices_password + + password + header @@ -439,15 +558,46 @@ password + + + header + + + + validation.dns_fornex_api_key + + password + + + + header + + + + validation.dns_zoneedit_id + + text + + + validation.dns_zoneedit_token + + text + header + + validation.dns_gandi_livedns_token + + text + validation.dns_gandi_livedns_key - + text + The API key is the previous mechanism that was replaced with Personal Access Tokens. API keys should no longer be used. @@ -488,6 +638,21 @@ text + + + header + + + + validation.dns_googledomains_access_token + + text + + + validation.dns_googledomains_zone + + text + header @@ -518,6 +683,16 @@ password + + + header + + + + validation.dns_he_ddns_key + + text + header @@ -546,10 +721,16 @@ text - validation.dns_inws_password + validation.dns_inwx_password password + + validation.dns_inwx_shared_secret + + password + When 2FA is enabled, the Shared Secret must be provided. Note that this feature requires the package oath-toolkit, which must be installed manually. + header @@ -565,6 +746,16 @@ password + + + header + + + + validation.dns_ipv64_token + + password + header @@ -591,6 +782,26 @@ checkbox Uncheck this box if you have a valid SSL certificate for your ISPConfig installation. + + + header + + + + validation.dns_jd_id + + text + + + validation.dns_jd_region + + text + + + validation.dns_jd_secret + + text + header @@ -639,12 +850,7 @@ Specify the location of the generated TSIG Key inside the TSIG file using grep and cut, example: grep \# /etc/knot/acme.key | cut -d' ' -f2 - - header - - - - + header @@ -664,7 +870,17 @@ text - + + header + + + + validation.dns_limacity_apikey + + text + + + header @@ -766,6 +982,36 @@ textMailinaBox Server FQDN + + + header + + + + validation.dns_mydnsjp_masterid + + text + + + validation.dns_mydnsjp_password + + password + + + + header + + + + validation.dns_mythic_beasts_key + + text + + + validation.dns_mythic_beasts_secret + + text + header @@ -884,6 +1130,16 @@ text Optionally set the name of the hosted zone (e.g. example.com) as some DNS Providers require, like dyn.com's 'Standard DNS'. + + + header + + + + validation.dns_online_key + + text + header @@ -924,6 +1180,32 @@ info + + + header + + + + validation.dns_oci_cli_user + + text + + + validation.dns_oci_cli_tenancy + + text + + + validation.dns_oci_cli_region + + text + + + validation.dns_oci_cli_key + + textbox + acme.sh documentation for further information.]]> + header @@ -973,15 +1255,69 @@ text - + header validation.dns_sl_key - + + text + + + validation.dns_sl_apiver + + dropdown + + + validation.dns_sl_token_lifetime + + text + Token lifetime in minutes (0-1440) + + + validation.dns_sl_account_id + + text + The account number can be found on the Selectel control panel + + + validation.dns_sl_project_name + + text + + + validation.dns_sl_login_name + + text + The service username can be found in the Selectel control panel + + + validation.dns_sl_password + + password + + + + header + + + + validation.dns_selfhost_user + text + + validation.dns_selfhost_password + + password + + + validation.dns_selfhost_map + + textbox + Please create the TXT record with subdomain _acme-challenge first, than you can get the ID from the edit page. Up to two RIDs per fulldomain are supported but at least one must be set, e.g. _acme-challenge.domain.net:RID:RID2 + header @@ -997,6 +1333,79 @@ password + + + header + + + + validation.dns_spaceship_api_key + + text + + + validation.dns_spaceship_api_secret + + text + + + validation.dns_spaceship_root_domain + + text + + + + header + + + + validation.dns_timeweb_token + + text + + + + header + + + + + header + + + + validation.dns_transip_username + + text + Your TransIP username. + + + validation.dns_transip_key + + textbox + Requires the whole key file in a format that is compatible with TransIP. + + + validation.dns_transip_token_global_key + + checkbox + When using a global key, IP whitelisting is disabled. + + + + header + + + + validation.dns_udr_user + + text + + + validation.dns_udr_password + + password + header @@ -1012,6 +1421,21 @@ text + + + header + + + + validation.dns_simply_api_key + + text + + + validation.dns_simply_account_name + + text + header @@ -1124,10 +1548,10 @@ text - validation.dns_acmedns_updateurl - + validation.dns_acmedns_baseurl + text - Specify the custom ACME DNS Update URL, i.e. https://auth.acme-dns.io/update (optional) + Specify the custom ACME DNS URL, i.e. https://auth.acme-dns.io:443 (optional) @@ -1175,6 +1599,21 @@ validation.dns_schlundtech_password password + + + + header + + + + validation.dns_easydns_apitoken + + text + + + validation.dns_easydns_apikey + + password @@ -1226,6 +1665,21 @@ password + + + header + + + + validation.dns_artfiles_username + + text + + + validation.dns_artfiles_password + + password + header @@ -1236,6 +1690,16 @@ password + + + header + + + + validation.dns_hetznercloud_token + + password + header @@ -1264,7 +1728,7 @@ validation.dns_kas_authdata - text + password validation.dns_kas_authtype @@ -1353,4 +1817,274 @@ password + + + header + + + + validation.dns_dynv6_token + + password + + + + header + + + + validation.dns_cpanel_user + + text + + + validation.dns_cpanel_token + + password + cPanel documentation for further information.]]> + + + validation.dns_cpanel_hostname + + text + EX: https://hostname:2083 + + + + header + + + + validation.dns_regru_username + + text + + + validation.dns_regru_password + + password + + + + header + + + + validation.dns_nic_username + + text + + + validation.dns_nic_password + + password + + + validation.dns_nic_client + + text + + + validation.dns_nic_secret + + password + + + + header + + + + validation.dns_websupport_api_key + + text + + + validation.dns_websupport_api_secret + + password + + + + header + + + + validation.dns_world4you_username + + text + + + validation.dns_world4you_password + + password + + + + header + + + + validation.dns_aurora_key + + text + + + validation.dns_aurora_secret + + password + + + + header + + + + validation.dns_conoha_user + + text + + + validation.dns_conoha_password + + password + + + validation.dns_conoha_tenantid + + text + + + validation.dns_conoha_idapi + + text + + + + header + + + + validation.dns_constellix_key + + text + + + validation.dns_constellix_secret + + password + + + + header + + + + validation.dns_exoscale_key + + text + + + validation.dns_exoscale_secret + + password + + + + header + + + + validation.dns_internetbs_key + + text + + + validation.dns_internetbs_password + + password + + + + header + + + + validation.dns_pointhq_key + + password + + + validation.dns_pointhq_email + + text + + + + header + + + + validation.dns_rackspace_user + + text + + + validation.dns_rackspace_key + + password + + + + header + + + + validation.dns_rage4_user + + text + + + validation.dns_rage4_token + + password + + + + header + + + + validation.dns_mijnhost_api_key + + text + API documentation for further information.]]> + + + + header + + + + validation.dns_scaleway_token + + text + + + + header + + + + validation.dns_technitium_token + + password + + + + validation.dns_technitium_hostname + + text +
    NOTE: Remove the trailing / if it is present.
    ]]>
    +
    diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml index 4089801a00..9c4601096e 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml @@ -15,7 +15,7 @@ acmeclient.settings.haproxyIntegration checkbox - Requires that the OPNsense HAProxy plugin is installed. This will automatically add the required backend, server, action and ACL for you. You just need to select your HAProxy frontend when configuration the certificate or challenge type.
    NOTE:This will only work for HTTP-01 validation and HAProxy frontends running in http mode; TCP frontends are not supported.
    ]]>
    + Requires that the OPNsense HAProxy plugin is installed. This will automatically add the required backend, server, action and ACL for you. You just need to select your HAProxy frontend when configuring the certificate or challenge type.
    NOTE:This will only work for HTTP-01 validation and HAProxy frontends running in http mode; TCP frontends are not supported.
    ]]>
    acmeclient.settings.logLevel @@ -36,6 +36,13 @@ true + + acmeclient.settings.TLSchallengePort + + text + + true + acmeclient.settings.restartTimeout diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php index 94a91008d7..4eef9200a6 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAccount.php @@ -1,7 +1,7 @@ config->key)) { - LeUtils::log_debug('creating account key for ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('creating account key for ' . (string)$this->config->name); // Check if we have an account key in our configuration if (!empty((string)$this->config->key)) { - LeUtils::log_debug('exporting existing account key to filesystem for ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('exporting existing account key to filesystem for ' . (string)$this->config->name); // Write key to disk file_put_contents($account_key_file, (string)base64_decode((string)$this->config->key)); chmod($account_key_file, 0600); return true; } else { - LeUtils::log_debug('generating a new account key for ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('generating a new account key for ' . (string)$this->config->name); + // Preparation to run acme client - $proc_env = $this->acme_env; // env variables for proc_open() + $proc_env = $this->acme_env; // add env variables $proc_env['PATH'] = $this::ACME_ENV_PATH; - $proc_desc = array( // descriptor array for proc_open() - 0 => array("pipe", "r"), // stdin - 1 => array("pipe", "w"), // stdout - 2 => array("pipe", "w") // stderr - ); - $proc_pipes = array(); - - // Run acme client to generate a account key + + // Prepare acme.sh command to generate a account key $acmecmd = '/usr/local/sbin/acme.sh ' . '--createAccountKey ' . implode(' ', $this->acme_args) . ' ' . LeUtils::execSafe('--accountkeylength %s', self::ACME_ACCOUNT_KEY_LENGTH) . ' ' . LeUtils::execSafe('--accountconf %s', $account_conf_file); - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); - $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); - - // Make sure the resource could be setup properly - if (is_resource($proc)) { - // Close all pipes - fclose($proc_pipes[0]); - fclose($proc_pipes[1]); - fclose($proc_pipes[2]); - // Get exit code - $result = proc_close($proc); - } else { - LeUtils::log_error('unable to start acme client process'); - $this->setStatus(500); - return false; - } + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); + + // Run acme.sh command + $result = LeUtils::run_shell_command($acmecmd, $proc_env); - // Check exit code + // Check acme.sh result if ($result) { LeUtils::log_error('failed to create a new account key for ' . (string)$this->config->name); $this->setStatus(300); @@ -147,7 +131,7 @@ public function generateKey() // Read account key file $account_key_content = @file_get_contents($account_key_file); if (empty($account_key_content) || ($account_key_content == false)) { - LeUtils::log_error("unable to read account key from file ${account_key_file}"); + LeUtils::log_error("unable to read account key from file {$account_key_file}"); $this->setStatus(500); return false; } @@ -172,7 +156,7 @@ public function generateKey() LeUtils::log_error('failed to save account key for ' . (string)$this->config->name); return false; } - LeUtils::log_debug('successfully created account key for ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('successfully created account key for ' . (string)$this->config->name); return true; } } @@ -210,48 +194,30 @@ public function register() // Check if account is already registered if (!($this->isRegistered())) { - LeUtils::log_debug('starting account registration for ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('starting account registration for ' . (string)$this->config->name); // Check if ACME External Account Binding (EAB) is enabled if (!empty((string)$this->config->eab_kid) && !empty((string)$this->config->eab_hmac)) { - LeUtils::log_debug('enabling ACME EAB for this account', $this->debug); + LeUtils::log_debug('enabling ACME EAB for this account'); $this->acme_args[] = LeUtils::execSafe('--eab-kid %s', $this->config->eab_kid); $this->acme_args[] = LeUtils::execSafe('--eab-hmac-key %s', $this->config->eab_hmac); } // Preparation to run acme client - $proc_env = $this->acme_env; // env variables for proc_open() + $proc_env = $this->acme_env; // add env variables $proc_env['PATH'] = $this::ACME_ENV_PATH; - $proc_desc = array( // descriptor array for proc_open() - 0 => array("pipe", "r"), // stdin - 1 => array("pipe", "w"), // stdout - 2 => array("pipe", "w") // stderr - ); - $proc_pipes = array(); - - // Run acme client + + // Prepare acme.sh command to register an account $acmecmd = '/usr/local/sbin/acme.sh ' . '--registeraccount ' . implode(' ', $this->acme_args) . ' ' . LeUtils::execSafe('--accountconf %s', $this->account_conf_file); - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); - $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); - - // Make sure the resource could be setup properly - if (is_resource($proc)) { - // Close all pipes - fclose($proc_pipes[0]); - fclose($proc_pipes[1]); - fclose($proc_pipes[2]); - // Get exit code - $result = proc_close($proc); - } else { - LeUtils::log_error('unable to start acme client process'); - $this->setStatus(500); - return false; - } + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); + + // Run acme.sh command + $result = LeUtils::run_shell_command($acmecmd, $proc_env); - // Check validation result + // Check acme.sh result if ($result) { LeUtils::log_error('account registration failed for ' . $this->config->name); $this->setStatus(400); @@ -259,12 +225,48 @@ public function register() } // Update account status. - LeUtils::log_error('account registration successful for ' . $this->config->name); + LeUtils::log('account registration successful for ' . $this->config->name); $this->setStatus(200); } else { - LeUtils::log_debug('account already registered: ' . (string)$this->config->name, $this->debug); + LeUtils::log_debug('account already registered: ' . (string)$this->config->name); } + // Always check (and fix) account config + $this->fixConfig(); + return true; } + + /** + * Remove CERT_HOME property from account config, + * otherwise --cert-home will be ignored by acme.sh. + */ + public function fixConfig() + { + $account_conf_dir = self::ACME_BASE_ACCOUNT_DIR . '/' . (string)$this->config->id . '_' . $this->ca_compat; + $account_conf_file = $account_conf_dir . '/account.conf'; + + if (is_dir($account_conf_dir)) { + if (is_file($account_conf_file)) { + // Parse config file and remove property + $account_conf = parse_ini_file($account_conf_file); + if (isset($account_conf['CERT_HOME'])) { + LeUtils::log('fixing invalid account config (CERT_HOME): ' . $this->config->name); + unset($account_conf['CERT_HOME']); + + // Convert array back to ini file format + $new_account_conf = array(); + foreach ($account_conf as $key => $value) { + $new_account_conf[] = "{$key}='{$value}'"; + } + + // Write changes back to file + file_put_contents($account_conf_file, implode("\n", $new_account_conf) . "\n"); + chmod($account_conf_file, 0600); + } else { + LeUtils::log('account config is valid (CERT_HOME): ' . $this->config->name); + } + } + } + } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmePanos.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmePanos.php new file mode 100644 index 0000000000..82ce66d12c --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmePanos.php @@ -0,0 +1,47 @@ +acme_env['PANOS_USER'] = (string)$this->config->acme_panos_username; + $this->acme_env['PANOS_PASS'] = (string)$this->config->acme_panos_password; + $this->acme_env['PANOS_HOST'] = (string)$this->config->acme_panos_host; + $this->acme_args[] = '--deploy-hook panos --insecure'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeProxmoxbs.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeProxmoxbs.php new file mode 100644 index 0000000000..5a2c6b7013 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeProxmoxbs.php @@ -0,0 +1,50 @@ +acme_env['DEPLOY_PROXMOXBS_USER'] = (string)$this->config->acme_proxmoxbs_user; + $this->acme_env['DEPLOY_PROXMOXBS_SERVER'] = (string)$this->config->acme_proxmoxbs_server; + $this->acme_env['DEPLOY_PROXMOXBS_SERVER_PORT'] = (string)$this->config->acme_proxmoxbs_port; + $this->acme_env['DEPLOY_PROXMOXBS_USER_REALM'] = (string)$this->config->acme_proxmoxbs_realm; + $this->acme_env['DEPLOY_PROXMOXBS_API_TOKEN_NAME'] = (string)$this->config->acme_proxmoxbs_tokenid; + $this->acme_env['DEPLOY_PROXMOXBS_API_TOKEN_KEY'] = (string)$this->config->acme_proxmoxbs_tokenkey; + $this->acme_args[] = '--deploy-hook proxmoxbs'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeProxmoxve.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeProxmoxve.php new file mode 100644 index 0000000000..4ac4ba46e9 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeProxmoxve.php @@ -0,0 +1,51 @@ +acme_env['DEPLOY_PROXMOXVE_USER'] = (string)$this->config->acme_proxmoxve_user; + $this->acme_env['DEPLOY_PROXMOXVE_SERVER'] = (string)$this->config->acme_proxmoxve_server; + $this->acme_env['DEPLOY_PROXMOXVE_SERVER_PORT'] = (string)$this->config->acme_proxmoxve_port; + $this->acme_env['DEPLOY_PROXMOXVE_NODE_NAME'] = (string)$this->config->acme_proxmoxve_nodename; + $this->acme_env['DEPLOY_PROXMOXVE_USER_REALM'] = (string)$this->config->acme_proxmoxve_realm; + $this->acme_env['DEPLOY_PROXMOXVE_API_TOKEN_NAME'] = (string)$this->config->acme_proxmoxve_tokenid; + $this->acme_env['DEPLOY_PROXMOXVE_API_TOKEN_KEY'] = (string)$this->config->acme_proxmoxve_tokenkey; + $this->acme_args[] = '--deploy-hook proxmoxve'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeRuckus.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeRuckus.php new file mode 100644 index 0000000000..63fb88e340 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeRuckus.php @@ -0,0 +1,47 @@ +acme_env['RUCKUS_HOST'] = (string)$this->config->acme_ruckus_host; + $this->acme_env['RUCKUS_USER'] = (string)$this->config->acme_ruckus_user; + $this->acme_env['RUCKUS_PASS'] = (string)$this->config->acme_ruckus_pass; + $this->acme_args[] = '--deploy-hook ruckus'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeSynologyDsm.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeSynologyDsm.php index c1fb58a0a1..48c520d9c6 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeSynologyDsm.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeSynologyDsm.php @@ -1,7 +1,7 @@ acme_env['SYNO_Certificate'] = 'OPNsense ACME cert ' . $this->cert_id; - $this->acme_env['SYNO_Hostname'] = (string)$this->config->acme_synology_dsm_hostname; - $this->acme_env['SYNO_Port'] = (string)$this->config->acme_synology_dsm_port; - $this->acme_env['SYNO_Scheme'] = (string)$this->config->acme_synology_dsm_scheme; - $this->acme_env['SYNO_Username'] = (string)$this->config->acme_synology_dsm_username; - $this->acme_env['SYNO_Password'] = (string)$this->config->acme_synology_dsm_password; + $this->acme_env['SYNO_CERTIFICATE'] = 'OPNsense ACME cert ' . $this->cert_id; + $this->acme_env['SYNO_HOSTNAME'] = (string)$this->config->acme_synology_dsm_hostname; + $this->acme_env['SYNO_PORT'] = (string)$this->config->acme_synology_dsm_port; + $this->acme_env['SYNO_SCHEME'] = (string)$this->config->acme_synology_dsm_scheme; + $this->acme_env['SYNO_USERNAME'] = (string)$this->config->acme_synology_dsm_username; + $this->acme_env['SYNO_PASSWORD'] = (string)$this->config->acme_synology_dsm_password; if (!empty((string)$this->config->acme_synology_dsm_create)) { - $this->acme_env['SYNO_Create'] = (string)$this->config->acme_synology_dsm_create; + $this->acme_env['SYNO_CREATE'] = (string)$this->config->acme_synology_dsm_create; } if (!empty((string)$this->config->acme_synology_dsm_deviceid)) { - $this->acme_env['SYNO_DID'] = (string)$this->config->acme_synology_dsm_deviceid; + $this->acme_env['SYNO_DEVICE_ID'] = (string)$this->config->acme_synology_dsm_deviceid; + } + if (!empty((string)$this->config->acme_synology_dsm_devicename)) { + $this->acme_env['SYNO_DEVICE_NAME'] = (string)$this->config->acme_synology_dsm_devicename; + } + if (!empty((string)$this->config->acme_synology_dsm_otpcode)) { + $this->acme_env['SYNO_OTP_CODE'] = (string)$this->config->acme_synology_dsm_otpcode; } $this->acme_args[] = '--deploy-hook synology_dsm'; return true; diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeTruenas.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeTruenas.php new file mode 100644 index 0000000000..22cd3180b7 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeTruenas.php @@ -0,0 +1,47 @@ +acme_env['DEPLOY_TRUENAS_APIKEY'] = (string)$this->config->acme_truenas_apikey; + $this->acme_env['DEPLOY_TRUENAS_HOSTNAME'] = (string)$this->config->acme_truenas_hostname; + $this->acme_env['DEPLOY_TRUENAS_SCHEME'] = (string)$this->config->acme_truenas_scheme; + $this->acme_args[] = '--deploy-hook truenas --insecure'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeUnifi.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeUnifi.php new file mode 100644 index 0000000000..e8af643447 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeUnifi.php @@ -0,0 +1,46 @@ +acme_env['DEPLOY_UNIFI_KEYSTORE'] = (string)$this->config->acme_unifi_keystore; + $this->acme_env['DEPLOY_UNIFI_RELOAD'] = 'service unifi restart'; + $this->acme_args[] = '--deploy-hook unifi'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeVault.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeVault.php new file mode 100644 index 0000000000..86f59f910f --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeVault.php @@ -0,0 +1,54 @@ +acme_env['VAULT_ADDR'] = (string)$this->config->acme_vault_url; + if (!empty((string)$this->config->acme_vault_prefix)) { + $this->acme_env['VAULT_PREFIX'] = (string)$this->config->acme_vault_prefix; + } + if ((string)$this->config->acme_vault_kvv2 == 1) { + $this->acme_env['VAULT_KV_V2'] = 1; + } + if (!empty((string)$this->config->acme_vault_token)) { + $this->acme_env['VAULT_TOKEN'] = (string)$this->config->acme_vault_token; + } + $this->acme_args[] = '--deploy-hook vault'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeZyxelGs1900.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeZyxelGs1900.php new file mode 100644 index 0000000000..ccd7de67ab --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeZyxelGs1900.php @@ -0,0 +1,66 @@ +acme_env['DEPLOY_ZYXEL_SWITCH_PASSWORD'] = (string)$this->config->acme_zyxel_gs1900_password; + + // Optional Parameters + if (!empty((string)$this->config->acme_zyxel_gs1900_host)) { + $this->acme_env['DEPLOY_ZYXEL_SWITCH'] = (string)$this->config->acme_zyxel_gs1900_host; + } + if (!empty((string)$this->config->acme_zyxel_gs1900_user)) { + $this->acme_env['DEPLOY_ZYXEL_SWITCH_USER'] = (string)$this->config->acme_zyxel_gs1900_user; + } + if (!empty((string)$this->config->acme_zyxel_gs1900_validate)) { + $this->acme_env['DEPLOY_ZYXEL_SWITCH_VALIDATE'] = (string)$this->config->acme_zyxel_gs1900_validate; + } + if (!empty((string)$this->config->acme_zyxel_gs1900_reboot)) { + $this->acme_env['DEPLOY_ZYXEL_SWITCH_REBOOT'] = (string)$this->config->acme_zyxel_gs1900_reboot; + } + + $this->acme_args[] = '--deploy-hook zyxel_gs1900'; + + if ((string)$this->config->acme_zyxel_gs1900_insecure == 1) { + array_push($this->acme_args, '--insecure'); + } + + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php index e15ab16f42..0571a8e085 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php @@ -1,7 +1,7 @@ * All rights reserved. @@ -47,7 +47,7 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon * Initialize LeAutomation object by adding the required configuration. * @return boolean */ - public function init(string $certid, string $certname, string $accountuuid) + public function init(string $certid, string $certname, string $accountuuid, bool $certecc = false) { // Get config object $this->loadConfig(self::CONFIG_PATH, $this->uuid); @@ -71,6 +71,7 @@ public function init(string $certid, string $certname, string $accountuuid) // Store acme filenames $this->acme_args[] = LeUtils::execSafe('--home %s', self::ACME_HOME_DIR); + $this->acme_args[] = LeUtils::execSafe('--cert-home %s', sprintf(self::ACME_CERT_HOME_DIR, $this->cert_id)); $this->acme_args[] = LeUtils::execSafe('--certpath %s', sprintf(self::ACME_CERT_FILE, $this->cert_id)); $this->acme_args[] = LeUtils::execSafe('--keypath %s', sprintf(self::ACME_KEY_FILE, $this->cert_id)); $this->acme_args[] = LeUtils::execSafe('--capath %s', sprintf(self::ACME_CHAIN_FILE, $this->cert_id)); @@ -79,6 +80,13 @@ public function init(string $certid, string $certname, string $accountuuid) // Main domain for acme $this->acme_args[] = LeUtils::execSafe('--domain %s', $certname); + // ECC cert + $this->cert_ecc = $certecc; + if ($this->cert_ecc) { + // Pass --ecc to acme client to locate the correct cert directory + $this->acme_args[] = '--ecc'; + } + return true; } @@ -113,39 +121,49 @@ public function runAcme() LeUtils::log('running automation (acme.sh): ' . $this->config->name); // Preparation to run acme client - $proc_env = $this->acme_env; // env variables for proc_open() + $proc_env = $this->acme_env; // add env variables $proc_env['PATH'] = $this::ACME_ENV_PATH; - $proc_desc = array( // descriptor array for proc_open() - 0 => array("pipe", "r"), // stdin - 1 => array("pipe", "w"), // stdout - 2 => array("pipe", "w") // stderr - ); - $proc_pipes = array(); - - // Run acme client + + // Prepare acme.sh command to run a deploy hook $acmecmd = self::ACME_CMD . ' ' . '--deploy ' . implode(' ', $this->acme_args); - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); - $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); - - // Make sure the resource could be setup properly - if (is_resource($proc)) { - // Close all pipes - fclose($proc_pipes[0]); - fclose($proc_pipes[1]); - fclose($proc_pipes[2]); - // Get exit code - $result = proc_close($proc); - } else { - LeUtils::log_error('unable to start acme client process'); - return false; + + // Run acme.sh command + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); + $result = LeUtils::run_shell_command($acmecmd, $proc_env); + + // acme.sh records the last used deploy hook and would automatically + // use it on the next run. This information must be removed from the + // configuration file. Otherwise it would be impossible to disable + // or remove a deploy hook from the GUI. + foreach (glob(self::ACME_HOME_DIR . '/*/*.conf') as $filename) { + // Skip openssl config files. + if (preg_match('/.*.csr.conf/i', $filename)) { + continue; + } + + // Read contents from file. + $contents = file_get_contents($filename); + + // Check if deploy hook string can be found. + if (strpos($contents, self::ACME_DEPLOY_HOOK_STRING) !== false) { + // Replace the whole line with an empty string. + $contents = preg_replace('(' . self::ACME_DEPLOY_HOOK_STRING . '.*)', '', $contents); + + // Write changes to the file. + if (!file_put_contents($filename, $contents)) { + LeUtils::log_error('clearing recorded deploy hook from acme.sh failed (' . $filename . ')'); + } else { + LeUtils::log_debug('cleared recorded deploy deploy hook from acme.sh (' . $filename . ')'); + } + } } - // Check validation result + // Check result if ($result) { - LeUtils::log_error('running acme.sh deploy hook failed (' . $this->getMethod() . ')'); + LeUtils::log_error('running acme.sh deploy hook failed (' . $this->getType() . ')'); return false; } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdReloadCaddy.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdReloadCaddy.php new file mode 100644 index 0000000000..3acd8c0085 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdReloadCaddy.php @@ -0,0 +1,45 @@ +command = 'caddy reload'; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdRemoteSsh.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdRemoteSsh.php new file mode 100644 index 0000000000..e924e2cc28 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdRemoteSsh.php @@ -0,0 +1,45 @@ +config->id; + $this->command = $command; + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdUploadHighwinds.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdUploadHighwinds.php deleted file mode 100644 index a17197815f..0000000000 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdUploadHighwinds.php +++ /dev/null @@ -1,45 +0,0 @@ -cert_id . ' ' . $this->config->id; - $this->command = $command; - return true; - } -} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationFactory.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationFactory.php index 044b5d8400..ec068df483 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationFactory.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomationFactory.php @@ -1,7 +1,7 @@ getNodeByReference(self::CONFIG_PATH . '.' . $uuid); if ($obj == null) { - LeUtils::log_error("automation not found: ${uuid}"); + LeUtils::log_error("automation not found: {$uuid}"); return null; } @@ -75,7 +75,7 @@ public function getAutomation(string $uuid) } } - LeUtils::log_error("automation not supported: " . (string)$obj->type . " (${uuid})"); + LeUtils::log_error("automation not supported: " . (string)$obj->type . " ({$uuid})"); return null; } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCertificate.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCertificate.php index 72a8f1eced..342ce0d817 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCertificate.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCertificate.php @@ -1,7 +1,7 @@ config->keyLength == 'key_ec256' || $this->config->keyLength == 'key_ec384') { // Pass --ecc to acme client to locate the correct cert directory $this->acme_args[] = '--ecc'; + $this->cert_ecc = true; + } else { + $this->cert_ecc = false; } // Store cert filenames @@ -91,6 +97,7 @@ public function __construct(string $uuid, bool $force = false, bool $cron = fals // Store acme filenames $this->acme_args[] = LeUtils::execSafe('--home %s', self::ACME_HOME_DIR); + $this->acme_args[] = LeUtils::execSafe('--cert-home %s', sprintf(self::ACME_CERT_HOME_DIR, $this->config->id)); $this->acme_args[] = LeUtils::execSafe('--certpath %s', $this->cert_file); $this->acme_args[] = LeUtils::execSafe('--keypath %s', $this->cert_key_file); $this->acme_args[] = LeUtils::execSafe('--capath %s', $this->cert_chain_file); @@ -125,7 +132,7 @@ public function import(bool $skip_validation = false) clearstatcache(); // don't let the cache fool us foreach (array($this->cert_file, $this->cert_key_file, $this->cert_chain_file, $this->cert_fullchain_file) as $file) { if (!is_file($file)) { - LeUtils::log_error("unable to import certificate " . $this->config->name . ", file not found: ${file}"); + LeUtils::log_error("unable to import certificate " . $this->config->name . ", file not found: {$file}"); Config::getInstance()->unlock(); return false; } @@ -138,61 +145,54 @@ public function import(bool $skip_validation = false) // Read contents from CA file $ca_content = @file_get_contents($this->cert_chain_file); if ($ca_content != false) { - $ca_subject = cert_get_subject($ca_content, false); - $ca_serial = cert_get_serial($ca_content, false); - $ca_cn = LeUtils::local_cert_get_cn($ca_content, false); - $ca_issuer = cert_get_issuer($ca_content, false); - $ca_purpose = cert_get_purpose($ca_content, false); + $ca_details = CertStore::parseX509($ca_content); + $ca_subject = $ca_details['name']; + $ca_serial = $ca_details['serialNumber']; + $ca_cn = $ca_details['commonname']; + $ca_issuer = implode(",", $ca_details['issuer']); } else { LeUtils::log_error('unable to read CA certificate content from file'); Config::getInstance()->unlock(); return false; } - // Prepare CA for import in Cert Manager + // Prepare CA + $caModel = new Ca(); $ca = array(); - $ca['crt'] = base64_encode($ca_content); $ca['refid'] = uniqid(); + $ca['descr'] = (string)$ca_cn . ' (ACME Client)'; $ca_found = false; // Check if CA was previously imported - foreach (Config::getInstance()->object()->ca as $cacrt) { - $cacrt_subject = cert_get_subject($cacrt->crt, true); - $cacrt_issuer = cert_get_issuer($cacrt->crt, true); + foreach ($caModel->ca->iterateItems() as $cacrt) { + $cacrt_content = base64_decode((string)$cacrt->crt); + $cacrt_details = CertStore::parseX509($cacrt_content); + $cacrt_subject = $cacrt_details['name']; + $cacrt_issuer = implode(",", $cacrt_details['issuer']); if (($ca_subject === $cacrt_subject) and ($ca_issuer === $cacrt_issuer)) { // Use old refid instead of generating a new one $ca['refid'] = (string)$cacrt->refid; + // Update existing CA + $cacrt->descr = $ca['descr']; $ca_found = true; break; } } - // Collect required CA information - $ca_cn = LeUtils::local_cert_get_cn($ca_content, false); - $ca['descr'] = (string)$ca_cn . ' (ACME Client)'; - - // Prepare CA for import - LeUtils::local_ca_import($ca, $ca_content); - - // Check if CA was found in config - if ($ca_found == true) { - // Update existing CA - foreach (Config::getInstance()->object()->ca as $cacrt) { - if ((string)$cacrt->refid == $ca['refid']) { - $cacrt->crt = $ca['crt']; - $cacrt->descr = $ca['descr']; - break; - } - } - } else { - // Create new CA - LeUtils::log("importing ACME CA: ${ca_cn}"); - $newca = Config::getInstance()->object()->addChild('ca'); + // Create new CA + if ($ca_found == false) { + LeUtils::log("imported ACME CA: {$ca_cn} ({$ca['refid']})"); + $newca = $caModel->ca->Add(); foreach (array_keys($ca) as $cacfg) { - $newca->addChild($cacfg, (string)$ca[$cacfg]); + $newca->$cacfg = (string)$ca[$cacfg]; } + $newca->crt = base64_encode($ca_content); } + // Serialize to config and save + $caModel->serializeToConfig(); + Config::getInstance()->save(); + /** * Step 2: import certificate */ @@ -200,11 +200,11 @@ public function import(bool $skip_validation = false) // Read contents from certificate file $cert_content = @file_get_contents($this->cert_file); if ($cert_content != false) { - $cert_subject = cert_get_subject($cert_content, false); - $cert_serial = cert_get_serial($cert_content, false); - $cert_cn = LeUtils::local_cert_get_cn($cert_content, false); - $cert_issuer = cert_get_issuer($cert_content, false); - $cert_purpose = cert_get_purpose($cert_content, false); + $cert_details = CertStore::parseX509($cert_content); + $cert_subject = $cert_details['name']; + $cert_serial = $cert_details['serialNumber']; + $cert_cn = $cert_details['commonname']; + $cert_issuer = implode(",", $cert_details['issuer']); } else { LeUtils::log_error('unable to read certificate content from file'); Config::getInstance()->unlock(); @@ -212,65 +212,53 @@ public function import(bool $skip_validation = false) return false; } - // Prepare certificate for import in Cert Manager + // Read private key + $key_content = @file_get_contents($this->cert_key_file); + if ($key_content == false) { + LeUtils::log_error('unable to read private key from file: ' . $this->cert_key_file); + Config::getInstance()->unlock(); + $this->setStatus(500); + return false; + } + + // Prepare certificate + $certModel = new Cert(); $cert = array(); - $cert_refid = uniqid(); - $cert['refid'] = $cert_refid; + $cert['refid'] = uniqid(); $cert['caref'] = (string)$ca['refid']; + if (empty($cert_cn)) { + // Fallback to configured name if Common Name is empty (e.g. for IP certificates) + $cert['descr'] = (string)$this->config->name . ' (ACME Client)'; + } else { + $cert['descr'] = (string)$cert_cn . ' (ACME Client)'; + } $import_log_message = 'imported'; $cert_found = false; - // Check if cert was previously imported + // Check if cert was previously imported. + // Otherwise just import as new cert. if (!empty((string)$this->config->certRefId)) { // Check if the previously imported certificate can still be found - foreach (Config::getInstance()->object()->cert as $cfgCert) { + foreach ($certModel->cert->iterateItems() as $cfgCert) { // Check if IDs match if ((string)$this->config->certRefId == (string)$cfgCert->refid) { + // Use old refid instead of generating a new one + $cert['refid'] = (string)$cfgCert->refid; + $import_log_message = 'updated'; $cert_found = true; break; } } - // Existing cert? - if ($cert_found) { - // Use old refid instead of generating a new one - $cert_refid = (string)$this->config->certRefId; - $import_log_message = 'updated'; - } - } else { - // Not found. Just import as new cert. - } - - // Read private key - $key_content = @file_get_contents($this->cert_key_file); - if ($key_content == false) { - LeUtils::log_error('unable to read private key from file: ' . $this->cert_key_file); - Config::getInstance()->unlock(); - $this->setStatus(500); - return false; } - // Collect required cert information - $cert_cn = LeUtils::local_cert_get_cn($cert_content, false); - $cert['descr'] = (string)$cert_cn . ' (ACME Client)'; - $cert['refid'] = $cert_refid; - - // Prepare certificate for import - cert_import($cert, $cert_content, $key_content); - - // Overwrite caref in order to use the correct CA (GH #2550). - // This is required because cert_import() uses lookup_ca_by_subject() - // to find a matching CA. If multiple CAs are using the same name, the - // first CA wins, but it may still be the wrong CA. - $cert['caref'] = (string)$ca['refid']; - // Check if cert was found in config if ($cert_found == true) { // Update existing cert - foreach (Config::getInstance()->object()->cert as $cfgCert) { + foreach ($certModel->cert->iterateItems() as $cfgCert) { if ((string)$cfgCert->refid == $cert['refid']) { - $cfgCert->crt = $cert['crt']; - $cfgCert->prv = $cert['prv']; $cfgCert->descr = $cert['descr']; + $cfgCert->crt = base64_encode($cert_content); + $cfgCert->prv = base64_encode($key_content); // Update CA ref, because it may be signed by a different CA. $cfgCert->caref = $cert['caref']; break; @@ -278,27 +266,32 @@ public function import(bool $skip_validation = false) } } else { // Create new cert - $newcert = Config::getInstance()->object()->addChild('cert'); + $newcert = $certModel->cert->Add(); foreach (array_keys($cert) as $certcfg) { - $newcert->addChild($certcfg, (string)$cert[$certcfg]); + $newcert->$certcfg = (string)$cert[$certcfg]; } + $newcert->crt = base64_encode($cert_content); + $newcert->prv = base64_encode($key_content); } - LeUtils::log("${import_log_message} ACME X.509 certificate: ${cert_cn}"); + LeUtils::log("{$import_log_message} ACME X.509 certificate: {$cert_cn} ({$cert['refid']})"); + + // Serialize to config and save + // Skip validation because the current in-memory model may not + // know about the CA item that was just created. + $certModel->serializeToConfig(false, true); + Config::getInstance()->save(); /** * Step 3: update configuration */ - // Add refid to certObj - $this->config->certRefId = $cert_refid; - // Set update/create time + // Update Acme cert config + $this->config->certRefId = $cert['refid']; $this->config->lastUpdate = time(); // Serialize to config and save $this->model->serializeToConfig(); Config::getInstance()->save(); - - // Reload to get most recent config Config::getInstance()->forceReload(); $this->loadConfig(self::CONFIG_PATH, $this->uuid); @@ -356,7 +349,7 @@ public function issue() LeUtils::log('auto renewal is disabled for certificate: ' . (string)$this->config->name); return false; } - LeUtils::log("${acme_action} certificate: " . (string)$this->config->name); + LeUtils::log("{$acme_action} certificate: " . (string)$this->config->name); LeUtils::log('using CA: ' . $this->ca); // Ensure that account is registered. @@ -370,7 +363,7 @@ public function issue() $configdir = (string)sprintf(self::ACME_CONFIG_DIR, (string)$this->config->id); foreach (array($certdir, $keydir, $configdir) as $dir) { if (!is_dir($dir)) { - LeUtils::log_debug("creating directory: ${dir}", $this->debug); + LeUtils::log_debug("creating directory: {$dir}"); mkdir($dir, 0700, true); } } @@ -397,12 +390,12 @@ public function issue() return false; } - // Run referenced automations. - $this->runAutomations(); - // Update cert status. $this->setStatus(200); + // Run referenced automations. + $this->runAutomations(); + return true; } @@ -414,8 +407,31 @@ public function needsRenewal() { $return = false; + // Try to get issue date from certificate + if (is_file($this->cert_file)) { + // Read contents from certificate file + $cert_content = @file_get_contents($this->cert_file); + if ($cert_content != false) { + $cert_info = @openssl_x509_parse($cert_content); + if (!empty($cert_info['validFrom_time_t'])) { + $last_update = $cert_info['validFrom_time_t']; + } else { + LeUtils::log_error('unable to get expiration time from certificate for ' . (string)$this->config->name); + $last_update = 0; // Just assume the cert requires renewal. + } + } else { + LeUtils::log_error('unable to read certificate content from file for ' . (string)$this->config->name); + $last_update = 0; // Just assume the cert requires renewal. + } + } elseif (!empty((string)$this->config->lastUpdate)) { + // Fallback to lastUpdate() state, although it may not be correct + // if the cert was imported manually after issue/renewal. + $last_update = (string)$this->config->lastUpdate; + } else { + $last_update = 0; // Just assume the cert requires renewal. + } + // Collect required information - $last_update = !empty((string)$this->config->lastUpdate) ? (string)$this->config->lastUpdate : 0; $current_time = new \DateTime(); $last_update_time = new \DateTime(); $last_update_time->setTimestamp($last_update); @@ -448,37 +464,20 @@ public function remove() LeUtils::log('wiping certificate config: ' . (string)$this->config->name); // Preparation to run acme client - $proc_env = $this->acme_env; // env variables for proc_open() + $proc_env = $this->acme_env; // add env variables $proc_env['PATH'] = $this::ACME_ENV_PATH; - $proc_desc = array( // descriptor array for proc_open() - 0 => array("pipe", "r"), // stdin - 1 => array("pipe", "w"), // stdout - 2 => array("pipe", "w") // stderr - ); - $proc_pipes = array(); - - // Run acme client to remove certificate and related config + + // Prepare acme.sh command to remove certificate and related config $acmecmd = '/usr/local/sbin/acme.sh ' . '--remove ' . implode(' ', $this->acme_args) . ' ' . LeUtils::execSafe('--domain %s', (string)$this->config->name); - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); - $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); - - // Make sure the resource could be setup properly - if (is_resource($proc)) { - // Close all pipes - fclose($proc_pipes[0]); - fclose($proc_pipes[1]); - fclose($proc_pipes[2]); - // Get exit code - $result = proc_close($proc); - } else { - LeUtils::log_error('unable to start acme client process'); - return false; - } + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); - // Check exit code + // Run acme.sh command + $result = LeUtils::run_shell_command($acmecmd, $proc_env); + + // Check acme.sh result if ($result) { LeUtils::log_error('error removing certificate ' . (string)$this->config->name); return false; @@ -539,36 +538,19 @@ public function revoke() $account_conf_file = $account_conf_dir . '/account.conf'; // Preparation to run acme client - $proc_env = $this->acme_env; // env variables for proc_open() + $proc_env = $this->acme_env; // add env variables $proc_env['PATH'] = $this::ACME_ENV_PATH; - $proc_desc = array( // descriptor array for proc_open() - 0 => array("pipe", "r"), // stdin - 1 => array("pipe", "w"), // stdout - 2 => array("pipe", "w") // stderr - ); - $proc_pipes = array(); - - // Run acme client to revoke certificate + + // Prepare acme.sh command to revoke certificate $acmecmd = '/usr/local/sbin/acme.sh ' . '--revoke ' . implode(' ', $this->acme_args) . ' ' . LeUtils::execSafe('--domain %s', (string)$this->config->name) . ' ' . LeUtils::execSafe('--accountconf %s', $account_conf_file); - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); - $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); - - // Make sure the resource could be setup properly - if (is_resource($proc)) { - // Close all pipes - fclose($proc_pipes[0]); - fclose($proc_pipes[1]); - fclose($proc_pipes[2]); - // Get exit code - $result = proc_close($proc); - } else { - LeUtils::log_error('unable to start acme client process'); - return false; - } + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); + + // Run acme.sh command + $result = LeUtils::run_shell_command($acmecmd, $proc_env); // Check exit code if ($result) { @@ -605,10 +587,14 @@ public function runAutomations() foreach ($automations as $auto_uuid) { $autoFactory = new LeAutomationFactory(); $automation = $autoFactory->getAutomation($auto_uuid); - $automation->init($this->getId(), (string)$this->config->name, (string)$this->config->account); - // Ignore invalid automations. - if ($automation->prepare()) { - $automation->run(); + // Skip invalid automations. + if (!is_null($automation)) { + $automation->init($this->getId(), (string)$this->config->name, (string)$this->config->account, $this->cert_ecc); + if ($automation->prepare()) { + $automation->run(); + } + } else { + LeUtils::log_error("ignoring invalid automation: {$auto_uuid}"); } } @@ -637,6 +623,8 @@ public function setAccount() $this->loadConfig(self::CONFIG_PATH, $this->uuid); } LeUtils::log('account is registered: ' . (string)$account->config->name); + // Always check (and fix) account config + $account->fixConfig(); return true; } @@ -654,16 +642,20 @@ public function setValidation() LeUtils::log_error('invalid challenge type for certificate: ' . (string)$this->config->name); return false; } - if (!$val->init((string)$this->config->id, (string)$this->config->account)) { + if (!$val->init((string)$this->config->id, (string)$this->config->account, $this->cert_ecc)) { LeUtils::log_error('failed to initialize validation for certificate: ' . (string)$this->config->name); return false; } // Configure validation object $val->setNames($this->config->name, $this->config->altNames, $this->config->aliasmode, $this->config->domainalias, $this->config->challengealias); - $val->setRenewal((int)$this->config->renewInterval); + $renewInterval = (string)$this->config->renewInterval; + $val->setRenewal((int)$renewInterval); $val->setForce($this->force); $val->setOcsp((string)$this->config->ocsp == 1 ? true : false); + if (!empty((string)$this->config->profile)) { + $val->setProfile((string)$this->config->profile); + } // strip prefix from key value $val->setKey(substr($this->config->keyLength, 4)); $val->prepare(); diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php index ce1ed6e715..7f60fe0666 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php @@ -1,7 +1,7 @@ getNodeByReference("${path}.${uuid}"); + $obj = $model->getNodeByReference("{$path}.{$uuid}"); if ($obj == null) { - LeUtils::log_error("config of type ${path} not found: ${uuid}"); + LeUtils::log_error("config of type {$path} not found: {$uuid}"); return false; } // Store config objects @@ -148,9 +153,9 @@ public function setCa(string $uuid) { // Get account config object $model = new \OPNsense\AcmeClient\AcmeClient(); - $obj = $model->getNodeByReference("accounts.account.${uuid}"); + $obj = $model->getNodeByReference("accounts.account.{$uuid}"); if (empty($obj) || $obj == null) { - LeUtils::log_error("unable to set CA, account not found: ${uuid}"); + LeUtils::log_error("unable to set CA, account not found: {$uuid}"); return false; } @@ -214,13 +219,13 @@ public function setLoglevel() $this->debug = true; break; case 'debug2': - $this->acme_args[] = '--syslog 7'; + $this->acme_args[] = '--syslog 8'; $this->acme_args[] = '--debug 2'; $this->acme_syslog = 7; $this->debug = true; break; case 'debug3': - $this->acme_args[] = '--syslog 7'; + $this->acme_args[] = '--syslog 9'; $this->acme_args[] = '--debug 3'; $this->acme_syslog = 7; $this->debug = true; diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php index ab4d46c85b..50ca996d85 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeUtils.php @@ -1,7 +1,7 @@ * Copyright (C) 2008 Shrew Soft Inc. @@ -32,6 +32,7 @@ namespace OPNsense\AcmeClient; use OPNsense\Core\Config; +use OPNsense\AcmeClient\AcmeClient; /** * Helper functions for LeAcme @@ -70,110 +71,40 @@ public static function execSafe($format, $args = array()) return vsprintf($format, $args); } - // Copied from system_camanager.php. - public static function local_ca_import(&$ca, $str, $key = "", $serial = 0) - { - // Get config object. - $config = Config::getInstance()->object(); - - $ca['crt'] = base64_encode($str); - if (!empty($key)) { - $ca['prv'] = base64_encode($key); - } - if (!empty($serial)) { - $ca['serial'] = $serial; - } - $subject = cert_get_subject($str, false); - $issuer = cert_get_issuer($str, false); - - // Find my issuer unless self-signed - if ($issuer != $subject) { - $issuer_crt =& lookup_ca_by_subject($issuer); - if ($issuer_crt) { - $ca['caref'] = $issuer_crt['refid']; - } - } - - /* Correct if child certificate was loaded first */ - if (is_array($config['ca'])) { - foreach ($config['ca'] as & $oca) { - $issuer = cert_get_issuer($oca['crt']); - if ($ca['refid'] != $oca['refid'] && $issuer == $subject) { - $oca['caref'] = $ca['refid']; - } - } - } - if (is_array($config['cert'])) { - foreach ($config['cert'] as & $cert) { - $issuer = cert_get_issuer($cert['crt']); - if ($issuer == $subject) { - $cert['caref'] = $ca['refid']; - } - } - } - return true; - } - - // copied from certs.inc - public static function local_cert_get_cn($crt, $decode = true) - { - $sub = self::local_cert_get_subject_array($crt, $decode); - if (is_array($sub)) { - foreach ($sub as $s) { - if (strtoupper($s['a']) == "CN") { - return $s['v']; - } - } - } - return ""; - } - - // copied from certs.inc - public static function local_cert_get_subject_array($str_crt, $decode = true) - { - if ($decode) { - $str_crt = base64_decode($str_crt); - } - $inf_crt = openssl_x509_parse($str_crt); - $components = $inf_crt['subject']; - - if (!is_array($components)) { - return; - } - - $subject_array = array(); - - foreach ($components as $a => $v) { - $subject_array[] = array('a' => $a, 'v' => $v); - } - - return $subject_array; - } - /** * log runtime information */ public static function log($msg) { - syslog(LOG_NOTICE, "AcmeClient: ${msg}"); + syslog(LOG_NOTICE, "AcmeClient: {$msg}"); } /** * log additional debug output */ - public static function log_debug($msg, bool $debug = false) + public static function log_debug($msg) { + $log_config = (new AcmeClient())->getNodeByReference('settings.logLevel'); + if (strpos($log_config, "debug") !== false) { + $debug = true; + } + if ($debug) { - syslog(LOG_NOTICE, "AcmeClient: ${msg}"); + syslog(LOG_NOTICE, "AcmeClient: {$msg}"); } } /** * log error messages */ - public static function log_error($msg) + public static function log_error($msg, $error = null) { - syslog(LOG_ERR, "AcmeClient: ${msg}"); + syslog( + LOG_ERR, + $error + ? ("AcmeClient: $msg; Trace: " . json_encode($error, JSON_UNESCAPED_SLASHES)) + : "AcmeClient: $msg" + ); } /** @@ -194,17 +125,26 @@ public static function run_shell_command($proc_cmd, $proc_env = array()) // Make sure the resource could be setup properly if (is_resource($proc)) { - // Close all pipes + // This workaround ensures that the accurate return code + // is reliably returned. fclose($proc_pipes[0]); + stream_set_blocking($proc_pipes[1], false); + stream_set_blocking($proc_pipes[2], false); + while (!feof($proc_pipes[1]) || !feof($proc_pipes[2])) { + $stdout = fread($proc_pipes[1], 1024); + $stderr = fread($proc_pipes[2], 1024); + usleep(50000); + } fclose($proc_pipes[1]); fclose($proc_pipes[2]); + // Get exit code $result = proc_close($proc); - log_error(sprintf("AcmeClient: The shell command '%s' returned exit code '%d'", $proc_cmd, $result)); + self::log(sprintf("AcmeClient: The shell command returned exit code '%d': '%s'", $result, $proc_cmd)); return($result); } else { - log_error(sprintf("AcmeClient: Unable to prepare shell command '%s'", $proc_cmd)); - return false; + self::log_error(sprintf("AcmeClient: Unable to prepare shell command '%s'", $proc_cmd)); + return(-999); } } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php index 433572cba9..db814ef191 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php @@ -1,7 +1,7 @@ * All rights reserved. @@ -50,7 +50,7 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon * @param $accountuuid string the UUID of the account object * @return bool */ - public function init(string $certid, string $accountuuid) + public function init(string $certid, string $accountuuid, bool $certecc = false) { // Get config object $this->loadConfig(self::CONFIG_PATH, $this->uuid); @@ -58,7 +58,7 @@ public function init(string $certid, string $accountuuid) // Get account object to query ID $account = new LeAccount($accountuuid); if (empty($account) || $account == null) { - LeUtils::log_error("unable to load account information: ${accountuuid}"); + LeUtils::log_error("unable to load account information: {$accountuuid}"); return false; } @@ -80,20 +80,29 @@ public function init(string $certid, string $accountuuid) switch ((string)$this->config->method) { case 'dns01': $this->acme_args[] = LeUtils::execSafe('--dns %s', (string)$this->config->dns_service); - $this->acme_args[] = LeUtils::execSafe('--dnssleep %s', (string)$this->config->dns_sleep); + if (! (string)$this->config->dns_sleep == '0') { + $this->acme_args[] = LeUtils::execSafe('--dnssleep %s', (string)$this->config->dns_sleep); + } break; case 'http01': $this->acme_args[] = '--webroot ' . self::ACME_WEBROOT; break; + case 'tlsalpn01': + $this->acme_args[] = '--alpn'; + break; } // Store acme filenames $this->acme_args[] = LeUtils::execSafe('--home %s', self::ACME_HOME_DIR); + $this->acme_args[] = LeUtils::execSafe('--cert-home %s', sprintf(self::ACME_CERT_HOME_DIR, $this->cert_id)); $this->acme_args[] = LeUtils::execSafe('--certpath %s', sprintf(self::ACME_CERT_FILE, $this->cert_id)); $this->acme_args[] = LeUtils::execSafe('--keypath %s', sprintf(self::ACME_KEY_FILE, $this->cert_id)); $this->acme_args[] = LeUtils::execSafe('--capath %s', sprintf(self::ACME_CHAIN_FILE, $this->cert_id)); $this->acme_args[] = LeUtils::execSafe('--fullchainpath %s', sprintf(self::ACME_FULLCHAIN_FILE, $this->cert_id)); + // ECC cert + $this->cert_ecc = $certecc; + return true; } @@ -133,8 +142,8 @@ public function run(bool $renew = false) // Issue or renew $acme_action = $renew == true ? 'renew' : 'issue'; - // Handle special key types - if ($this->cert_keylength == 'ec256' || $this->cert_keylength == 'ec384') { + // Handle ECC certs + if ($this->cert_ecc) { if ($renew == true) { // If it's a renew then pass --ecc to acme client to locate the correct cert directory $this->acme_args[] = '--ecc'; @@ -146,44 +155,27 @@ public function run(bool $renew = false) $account_conf_file = $account_conf_dir . '/account.conf'; // Preparation to run acme client - $proc_env = $this->acme_env; // env variables for proc_open() + $proc_env = $this->acme_env; // add env variables $proc_env['PATH'] = $this::ACME_ENV_PATH; - $proc_desc = array( // descriptor array for proc_open() - 0 => array("pipe", "r"), // stdin - 1 => array("pipe", "w"), // stdout - 2 => array("pipe", "w") // stderr - ); - $proc_pipes = array(); - - // Run acme client + + // Prepare acme.sh command // NOTE: We "export" certificates to our own directory, so we don't have to deal // with domain names in filesystem, but instead can use the ID of our certObj, which // will never change. $acmecmd = self::ACME_CMD . ' ' - . "--${acme_action} " + . "--{$acme_action} " . implode(' ', $this->acme_args) . ' ' . LeUtils::execSafe('--accountconf %s', $account_conf_file); - LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug); - $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env); - - // Make sure the resource could be setup properly - if (is_resource($proc)) { - // Close all pipes - fclose($proc_pipes[0]); - fclose($proc_pipes[1]); - fclose($proc_pipes[2]); - // Get exit code - $result = proc_close($proc); - } else { - LeUtils::log_error('unable to start acme client process'); - return false; - } + LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd); + + // Run acme.sh command + $result = LeUtils::run_shell_command($acmecmd, $proc_env); // Run optional cleanup tasks. $this->cleanup(); - // Check validation result + // Check acme.sh result if ($result) { LeUtils::log_error('domain validation failed (' . $this->getMethod() . ')'); return false; @@ -288,12 +280,21 @@ public function setOcsp(bool $ocsp = false) $this->acme_args[] = $ocsp == true ? '--ocsp' : null; } + /** + * set certificate profile + * @param $profile string profile name + */ + public function setProfile(string $profile) + { + $this->acme_args[] = LeUtils::execSafe('--cert-profile %s', $profile); + } + /** * set renewal interval * @param $interval int specifies the renewal interval in days */ public function setRenewal(int $interval = 60) { - $this->acme_args[] = LeUtils::execSafe('--days %s', (string)$interval); + $this->acme_args[] = LeUtils::execSafe('--days %s', $interval); } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmedns.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmedns.php index 4ccefe4e3e..029b24e8c8 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmedns.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAcmedns.php @@ -42,6 +42,6 @@ public function prepare() $this->acme_env['ACMEDNS_USERNAME'] = (string)$this->config->dns_acmedns_user; $this->acme_env['ACMEDNS_PASSWORD'] = (string)$this->config->dns_acmedns_password; $this->acme_env['ACMEDNS_SUBDOMAIN'] = (string)$this->config->dns_acmedns_subdomain; - $this->acme_env['ACMEDNS_UPDATE_URL'] = (string)$this->config->dns_acmedns_updateurl; + $this->acme_env['ACMEDNS_BASE_URL'] = (string)$this->config->dns_acmedns_baseurl; } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsActive24.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsActive24.php new file mode 100644 index 0000000000..4b26d1bcf6 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsActive24.php @@ -0,0 +1,44 @@ +acme_env['ACTIVE24_Token'] = (string)$this->config->dns_active24_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsArtfiles.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsArtfiles.php new file mode 100644 index 0000000000..2c6e5eca80 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsArtfiles.php @@ -0,0 +1,45 @@ +acme_env['AF_API_USERNAME'] = (string)$this->config->dns_artfiles_username; + $this->acme_env['AF_API_PASSWORD'] = (string)$this->config->dns_artfiles_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAurora.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAurora.php new file mode 100644 index 0000000000..64dd256228 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAurora.php @@ -0,0 +1,44 @@ +acme_env['AURORA_Key'] = (string)$this->config->dns_aurora_key; + $this->acme_env['AURORA_Secret'] = (string)$this->config->dns_aurora_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAzure.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAzure.php index 1b2acc9dcc..88e86551fc 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAzure.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsAzure.php @@ -43,5 +43,9 @@ public function prepare() $this->acme_env['AZUREDNS_TENANTID'] = (string)$this->config->dns_azuredns_tenantid; $this->acme_env['AZUREDNS_APPID'] = (string)$this->config->dns_azuredns_appid; $this->acme_env['AZUREDNS_CLIENTSECRET'] = (string)$this->config->dns_azuredns_clientsecret; + + if ($this->config->dns_azuredns_managedidentity == '1') { + $this->acme_env['AZUREDNS_MANAGEDIDENTITY'] = 'true'; + } } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsBunny.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsBunny.php new file mode 100644 index 0000000000..092d621609 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsBunny.php @@ -0,0 +1,43 @@ + + * + * 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. + */ + +namespace OPNsense\AcmeClient\LeValidation; + +use OPNsense\AcmeClient\LeValidationInterface; +use OPNsense\Core\Config; + +/** + * Bunny DNS API + * @package OPNsense\AcmeClient + */ +class DnsBunny extends Base implements LeValidationInterface +{ + public function prepare() + { + $this->acme_env['BUNNY_API_KEY'] = (string)$this->config->dns_bunny_api_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCf.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCf.php index 3f6f735e01..107a09f6eb 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCf.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCf.php @@ -45,5 +45,9 @@ public function prepare() // Restricted API token (recommended) $this->acme_env['CF_Token'] = (string)$this->config->dns_cf_token; $this->acme_env['CF_Account_ID'] = (string)$this->config->dns_cf_account_id; + // Optional Zone ID + if (!empty((string)$this->config->dns_cf_zone_id)) { + $this->acme_env['CF_Zone_ID'] = (string)$this->config->dns_cf_zone_id; + } } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsConoha.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsConoha.php new file mode 100644 index 0000000000..f1c46fc514 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsConoha.php @@ -0,0 +1,46 @@ +acme_env['CONOHA_Username'] = (string)$this->config->dns_conoha_user; + $this->acme_env['CONOHA_Password'] = (string)$this->config->dns_conoha_password; + $this->acme_env['CONOHA_TenantId'] = (string)$this->config->dns_conoha_tenantid; + $this->acme_env['CONOHA_IdentityServiceApi'] = (string)$this->config->dns_conoha_idapi; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsConstellix.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsConstellix.php new file mode 100644 index 0000000000..6d6e536997 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsConstellix.php @@ -0,0 +1,44 @@ +acme_env['CONSTELLIX_Key'] = (string)$this->config->dns_constellix_key; + $this->acme_env['CONSTELLIX_Secret'] = (string)$this->config->dns_constellix_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCpanel.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCpanel.php new file mode 100644 index 0000000000..7425b01d92 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsCpanel.php @@ -0,0 +1,47 @@ +acme_env['cPanel_Username'] = (string)$this->config->dns_cpanel_user; + $this->acme_env['cPanel_Apitoken'] = (string)$this->config->dns_cpanel_token; + $this->acme_env['cPanel_Hostname'] = (string)$this->config->dns_cpanel_hostname; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnsexit.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnsexit.php new file mode 100644 index 0000000000..239e8ef89e --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnsexit.php @@ -0,0 +1,46 @@ +acme_env['DNSEXIT_AUTH_USER'] = (string)$this->config->dns_dnsexit_auth_user; + $this->acme_env['DNSEXIT_AUTH_PASS'] = (string)$this->config->dns_dnsexit_auth_pass; + $this->acme_env['DNSEXIT_API_KEY'] = (string)$this->config->dns_dnsexit_api; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnshome.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnshome.php new file mode 100644 index 0000000000..2e7facd9ca --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnshome.php @@ -0,0 +1,45 @@ +acme_env['DNSHOME_SubdomainPassword'] = (string)$this->config->dns_dnshome_password; + $this->acme_env['DNSHOME_Subdomain'] = (string)$this->config->dns_dnshome_subdomain; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnsservices.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnsservices.php new file mode 100644 index 0000000000..b0ffed2038 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDnsservices.php @@ -0,0 +1,45 @@ +acme_env['DnsServices_Username'] = (string)$this->config->dns_dnsservices_user; + $this->acme_env['DnsServices_Password'] = (string)$this->config->dns_dnsservices_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDynv6.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDynv6.php new file mode 100644 index 0000000000..c1d9115f05 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsDynv6.php @@ -0,0 +1,44 @@ +acme_env['DYNV6_TOKEN'] = (string)$this->config->dns_dynv6_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsEasydns.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsEasydns.php new file mode 100644 index 0000000000..47f8420ee3 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsEasydns.php @@ -0,0 +1,46 @@ +acme_env['EASYDNS_Key'] = (string)$this->config->dns_easydns_apikey; + $this->acme_env['EASYDNS_Token'] = (string)$this->config->dns_easydns_apitoken; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsExoscale.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsExoscale.php new file mode 100644 index 0000000000..f3204e2164 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsExoscale.php @@ -0,0 +1,44 @@ +acme_env['EXOSCALE_API_KEY'] = (string)$this->config->dns_exoscale_key; + $this->acme_env['EXOSCALE_SECRET_KEY'] = (string)$this->config->dns_exoscale_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsFornex.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsFornex.php new file mode 100644 index 0000000000..d25c60f20a --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsFornex.php @@ -0,0 +1,44 @@ +acme_env['FORNEX_API_KEY'] = (string)$this->config->dns_fornex_api_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGandiLivedns.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGandiLivedns.php index afebc94825..928cf65742 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGandiLivedns.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGandiLivedns.php @@ -39,6 +39,10 @@ class DnsGandiLivedns extends Base implements LeValidationInterface { public function prepare() { - $this->acme_env['GANDI_LIVEDNS_KEY'] = (string)$this->config->dns_gandi_livedns_key; + if (!empty((string)$this->config->dns_gandi_livedns_token)) { + $this->acme_env['GANDI_LIVEDNS_TOKEN'] = (string)$this->config->dns_gandi_livedns_token; + } else { + $this->acme_env['GANDI_LIVEDNS_KEY'] = (string)$this->config->dns_gandi_livedns_key; + } } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGcloud.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGcloud.php index 045363b7c3..e22558eed8 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGcloud.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGcloud.php @@ -1,7 +1,7 @@ config->id; - $gcloud_config = "acme-${val_id}"; - $gcloud_key_file = '/tmp/acme_' . (string)$this->config->dns_service . "_${val_id}.json"; + // NOTE: Never versions of gcloud SDK no longer allow dots in config names. + $val_id = str_replace('.', '-', (string)$this->config->id); + $gcloud_config = "acme-{$val_id}"; + $gcloud_key_file = '/tmp/acme_' . (string)$this->config->dns_service . "_{$val_id}.json"; file_put_contents($gcloud_key_file, (string)$this->config->dns_gcloud_key); chmod($gcloud_key_file, 0600); $proc_env['CLOUDSDK_PYTHON'] = '/usr/local/bin/python3'; @@ -74,11 +75,11 @@ public function prepare() $proc_env['CLOUDSDK_CORE_PROJECT'] = $gcloud_project; // Ensure that a working gcloud config exists. - LeUtils::run_shell_command("/usr/local/bin/gcloud config configurations create ${gcloud_config}", $proc_env); - LeUtils::run_shell_command("/usr/local/bin/gcloud config configurations activate ${gcloud_config}", $proc_env); - LeUtils::run_shell_command("/usr/local/bin/gcloud auth activate-service-account --key-file=${gcloud_key_file}", $proc_env); - LeUtils::run_shell_command("/usr/local/bin/gcloud config set account ${gcloud_account}", $proc_env); - LeUtils::run_shell_command("/usr/local/bin/gcloud config set project ${gcloud_project}", $proc_env); + LeUtils::run_shell_command("/usr/local/bin/gcloud --quiet config configurations create {$gcloud_config}", $proc_env); + LeUtils::run_shell_command("/usr/local/bin/gcloud --quiet config configurations activate {$gcloud_config}", $proc_env); + LeUtils::run_shell_command("/usr/local/bin/gcloud --quiet auth activate-service-account --key-file={$gcloud_key_file}", $proc_env); + LeUtils::run_shell_command("/usr/local/bin/gcloud --quiet config set account {$gcloud_account}", $proc_env); + LeUtils::run_shell_command("/usr/local/bin/gcloud --quiet config set project {$gcloud_project}", $proc_env); // Save config for acme client. $this->acme_env['CLOUDSDK_PYTHON'] = '/usr/local/bin/python3'; diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGoogledomains.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGoogledomains.php new file mode 100644 index 0000000000..d6119cad62 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsGoogledomains.php @@ -0,0 +1,49 @@ + + * + * 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. + */ + +namespace OPNsense\AcmeClient\LeValidation; + +use OPNsense\AcmeClient\LeValidationInterface; +use OPNsense\Core\Config; + +/** + * Google Domains DNS API + * @package OPNsense\AcmeClient + */ +class DnsGoogleDomains extends Base implements LeValidationInterface +{ + public function prepare() + { + // It is possible to override $GOOGLEDOMAINS_API env variable to + // control the endpoint acme.sh talks to. However there is only one + // option (https://acmedns.googleapis.com/v1/acmeChallengeSets) that is + // currently the default, so exposing this only adds to confusion and + // noise in the UI. + $this->acme_env['GOOGLEDOMAINS_ACCESS_TOKEN'] = (string)$this->config->dns_googledomains_access_token; + $this->acme_env['GOOGLEDOMAINS_ZONE'] = (string)$this->config->dns_googledomains_zone; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHeDdns.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHeDdns.php new file mode 100644 index 0000000000..7144227e7b --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHeDdns.php @@ -0,0 +1,44 @@ +acme_env['HE_DDNS_KEY'] = (string)$this->config->dns_he_ddns_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHetznercloud.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHetznercloud.php new file mode 100644 index 0000000000..a45a3007fb --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsHetznercloud.php @@ -0,0 +1,44 @@ +acme_env['HETZNER_TOKEN'] = (string)$this->config->dns_hetznercloud_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInternetbs.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInternetbs.php new file mode 100644 index 0000000000..3d9f40e80b --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInternetbs.php @@ -0,0 +1,44 @@ +acme_env['INTERNETBS_API_KEY'] = (string)$this->config->dns_internetbs_key; + $this->acme_env['INTERNETBS_API_PASSWORD'] = (string)$this->config->dns_internetbs_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInwx.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInwx.php index f4e0533fb6..736dd0c0e3 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInwx.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsInwx.php @@ -1,7 +1,7 @@ acme_env['INWX_User'] = (string)$this->config->dns_inwx_user; - $this->acme_env['INWX_Password'] = (string)$this->config->dns_inws_password; + $this->acme_env['INWX_Password'] = (string)$this->config->dns_inwx_password; + if (!empty((string)$this->config->dns_inwx_shared_secret)) { + if ((string)$this->model->isPackageInstalled('oath-toolkit') != '1') { + LeUtils::log_error('Required package oath-toolkit is NOT installed. Please install the package or remove the INWX Shared Secret.'); + return false; + } + $this->acme_env['INWX_Shared_Secret'] = (string)$this->config->dns_inwx_shared_secret; + } } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsIpv64.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsIpv64.php new file mode 100644 index 0000000000..0320f3402c --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsIpv64.php @@ -0,0 +1,44 @@ +acme_env['IPv64_Token'] = (string)$this->config->dns_ipv64_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsJd.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsJd.php new file mode 100644 index 0000000000..8d38888159 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsJd.php @@ -0,0 +1,46 @@ +acme_env['JD_ACCESS_KEY_ID'] = (string)$this->config->dns_jd_id; + $this->acme_env['JD_REGION'] = (string)$this->config->dns_jd_region; + $this->acme_env['JD_ACCESS_KEY_SECRET'] = (string)$this->config->dns_jd_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLimacity.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLimacity.php new file mode 100644 index 0000000000..ee2dd9ab2e --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsLimacity.php @@ -0,0 +1,44 @@ +acme_env['LIMACITY_APIKEY'] = (string)$this->config->dns_limacity_apikey; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMijnhost.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMijnhost.php new file mode 100644 index 0000000000..f077995c25 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMijnhost.php @@ -0,0 +1,44 @@ +acme_env['MIJNHOST_API_KEY'] = (string)$this->config->dns_mijnhost_api_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMydnsjp.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMydnsjp.php new file mode 100644 index 0000000000..41a86c9647 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMydnsjp.php @@ -0,0 +1,40 @@ +acme_env['MYDNSJP_MasterID'] = (string)$this->config->dns_mydnsjp_masterid; + $this->acme_env['MYDNSJP_Password'] = (string)$this->config->dns_mydnsjp_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMythicBeasts.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMythicBeasts.php new file mode 100644 index 0000000000..f61b4fa687 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsMythicBeasts.php @@ -0,0 +1,45 @@ +acme_env['MB_AK'] = (string)$this->config->dns_mythic_beasts_key; + $this->acme_env['MB_AS'] = (string)$this->config->dns_mythic_beasts_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNic.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNic.php new file mode 100644 index 0000000000..bd16440483 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNic.php @@ -0,0 +1,47 @@ +acme_env['NIC_Username'] = (string)$this->config->dns_nic_username; + $this->acme_env['NIC_Password'] = (string)$this->config->dns_nic_password; + $this->acme_env['NIC_ClientID'] = (string)$this->config->dns_nic_client; + $this->acme_env['NIC_ClientSecret'] = (string)$this->config->dns_nic_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsupdate.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsupdate.php index 4d6f349d58..fba6d3662f 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsupdate.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsNsupdate.php @@ -1,7 +1,7 @@ cert_id); - $secret_key_filename = "${configdir}/secret.key"; + $secret_key_filename = "{$configdir}secret.key"; $secret_key_data = (string)$this->config->dns_nsupdate_key . "\n"; file_put_contents($secret_key_filename, $secret_key_data); diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOci.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOci.php new file mode 100644 index 0000000000..aada37b84c --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOci.php @@ -0,0 +1,47 @@ +acme_env['OCI_CLI_USER'] = (string)$this->config->dns_oci_cli_user; + $this->acme_env['OCI_CLI_TENANCY'] = (string)$this->config->dns_oci_cli_tenancy; + $this->acme_env['OCI_CLI_REGION'] = (string)$this->config->dns_oci_cli_region; + $this->acme_env['OCI_CLI_KEY'] = (string)$this->config->dns_oci_cli_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOnline.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOnline.php new file mode 100644 index 0000000000..c6a07a29a0 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsOnline.php @@ -0,0 +1,44 @@ +acme_env['ONLINE_API_KEY'] = (string)$this->config->dns_online_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsPointhq.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsPointhq.php new file mode 100644 index 0000000000..6abc9deb98 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsPointhq.php @@ -0,0 +1,44 @@ +acme_env['PointHQ_Key'] = (string)$this->config->dns_pointhq_key; + $this->acme_env['exportPointHQ_Email'] = (string)$this->config->dns_pointhq_email; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsRackspace.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsRackspace.php new file mode 100644 index 0000000000..1b4c1e6a1b --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsRackspace.php @@ -0,0 +1,44 @@ +acme_env['RACKSPACE_Username'] = (string)$this->config->dns_rackspace_user; + $this->acme_env['RACKSPACE_Apikey'] = (string)$this->config->dns_rackspace_key; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsRage4.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsRage4.php new file mode 100644 index 0000000000..86e598a72f --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsRage4.php @@ -0,0 +1,44 @@ +acme_env['RAGE4_TOKEN'] = (string)$this->config->dns_rage4_user; + $this->acme_env['RAGE4_USERNAME'] = (string)$this->config->dns_rage4_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsRegru.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsRegru.php new file mode 100644 index 0000000000..9312c8c1df --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsRegru.php @@ -0,0 +1,45 @@ +acme_env['REGRU_API_Username'] = (string)$this->config->dns_regru_username; + $this->acme_env['REGRU_API_Password'] = (string)$this->config->dns_regru_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsScaleway.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsScaleway.php new file mode 100644 index 0000000000..22f237ac99 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsScaleway.php @@ -0,0 +1,44 @@ +acme_env['SCALEWAY_API_TOKEN'] = (string)$this->config->dns_scaleway_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSelectel.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSelectel.php index 56e3112666..54a115cd39 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSelectel.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSelectel.php @@ -2,6 +2,7 @@ /* * Copyright (C) 2020 Frank Wall + * Copyright (C) 2025 Renat Gorbushin * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -40,5 +41,11 @@ class DnsSelectel extends Base implements LeValidationInterface public function prepare() { $this->acme_env['SL_Key'] = (string)$this->config->dns_sl_key; + $this->acme_env['SL_Ver'] = (string)$this->config->dns_sl_apiver; + $this->acme_env['SL_Expire'] = (string)$this->config->dns_sl_token_lifetime; + $this->acme_env['SL_Login_ID'] = (string)$this->config->dns_sl_account_id; + $this->acme_env['SL_Project_Name'] = (string)$this->config->dns_sl_project_name; + $this->acme_env['SL_Login_Name'] = (string)$this->config->dns_sl_login_name; + $this->acme_env['SL_Pswd'] = (string)$this->config->dns_sl_password; } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSelfhost.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSelfhost.php new file mode 100644 index 0000000000..881ebf792d --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSelfhost.php @@ -0,0 +1,46 @@ +acme_env['SELFHOSTDNS_USERNAME'] = (string)$this->config->dns_selfhost_user; + $this->acme_env['SELFHOSTDNS_PASSWORD'] = (string)$this->config->dns_selfhost_password; + $this->acme_env['SELFHOSTDNS_MAP'] = (string)$this->config->dns_selfhost_map; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSimply.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSimply.php new file mode 100644 index 0000000000..f6b5476a9b --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSimply.php @@ -0,0 +1,45 @@ +acme_env['SIMPLY_ApiKey'] = (string)$this->config->dns_simply_api_key; + $this->acme_env['SIMPLY_AccountName'] = (string)$this->config->dns_simply_account_name; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSpaceship.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSpaceship.php new file mode 100644 index 0000000000..b8b8697581 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsSpaceship.php @@ -0,0 +1,51 @@ +acme_env['SPACESHIP_API_KEY'] = (string)$this->config->dns_spaceship_api_key; + $this->acme_env['SPACESHIP_API_SECRET'] = (string)$this->config->dns_spaceship_api_secret; + + // optional root domain + if (!empty((string)$this->config->dns_spaceship_root_domain)) { + $this->acme_env['SPACESHIP_ROOT_DOMAIN'] = (string)$this->config->dns_spaceship_root_domain; + } + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsTechnitium.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsTechnitium.php new file mode 100644 index 0000000000..bff5a5f094 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsTechnitium.php @@ -0,0 +1,45 @@ +acme_env['Technitium_Token'] = (string)$this->config->dns_technitium_token; + $this->acme_env['Technitium_Server'] = (string)$this->config->dns_technitium_hostname; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsTimeweb.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsTimeweb.php new file mode 100644 index 0000000000..e4bba4264f --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsTimeweb.php @@ -0,0 +1,45 @@ +acme_env['TW_Token'] = (string)$this->config->dns_timeweb_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsTransip.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsTransip.php new file mode 100644 index 0000000000..b62043acfa --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsTransip.php @@ -0,0 +1,52 @@ +cert_id); + $secret_key_filename = "{$configdir}secret.key"; + $secret_key_data = (string)$this->config->dns_transip_key . "\n"; + file_put_contents($secret_key_filename, $secret_key_data); + + // Add env variables + $this->acme_env['TRANSIP_Username'] = (string)$this->config->dns_transip_username; + $this->acme_env['TRANSIP_Key_File'] = $secret_key_filename; + $this->acme_env['TRANSIP_Token_Global_Key'] = ((string)$this->config->dns_transip_token_global_key === '1') ? 'true' : 'false'; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsUdr.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsUdr.php new file mode 100644 index 0000000000..0fb39e8717 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsUdr.php @@ -0,0 +1,45 @@ +acme_env['UDR_USER'] = (string)$this->config->dns_udr_user; + $this->acme_env['UDR_PASS'] = (string)$this->config->dns_udr_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsWebsupport.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsWebsupport.php new file mode 100644 index 0000000000..83393e3eb7 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsWebsupport.php @@ -0,0 +1,44 @@ +acme_env['WS_ApiKey'] = (string)$this->config->dns_websupport_api_key; + $this->acme_env['WS_ApiSecret'] = (string)$this->config->dns_websupport_api_secret; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsWorld4you.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsWorld4you.php new file mode 100644 index 0000000000..192c8d63e1 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsWorld4you.php @@ -0,0 +1,44 @@ +acme_env['WORLD4YOU_USERNAME'] = (string)$this->config->dns_world4you_username; + $this->acme_env['WORLD4YOU_PASSWORD'] = (string)$this->config->dns_world4you_password; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsZoneedit.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsZoneedit.php new file mode 100644 index 0000000000..5252b44104 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/DnsZoneedit.php @@ -0,0 +1,45 @@ +acme_env['ZONEEDIT_ID'] = (string)$this->config->dns_zoneedit_id; + $this->acme_env['ZONEEDIT_Token'] = (string)$this->config->dns_zoneedit_token; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php index 32f6d31385..bbb2cb5941 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/HttpOpnsense.php @@ -1,7 +1,7 @@ cert_id); + // Get configured HTTP port for local lighttpd server. $configObj = Config::getInstance()->object(); $local_http_port = $configObj->OPNsense->AcmeClient->settings->challengePort; @@ -50,12 +52,12 @@ public function prepare() $iplist = array(); // Add IP addresses from auto-discovery feature - if ($this->config->http_opn_autodiscovery == 1) { + if ($this->config->http_opn_autodiscovery == '1') { $dnslist = explode(',', $this->cert_altnames); $dnslist[] = $this->cert_name; foreach ($dnslist as $fqdn) { // NOTE: This may take some time. - $ip_found = gethostbyname("${fqdn}."); + $ip_found = gethostbyname("{$fqdn}."); if (!empty($ip_found)) { $iplist[] = (string)$ip_found; } @@ -72,9 +74,16 @@ public function prepare() // Add IP address from chosen interface if (!empty((string)$this->config->http_opn_interface)) { - $interface_ip = get_interface_ip((string)$this->config->http_opn_interface); - if (!empty($interface_ip)) { - $iplist[] = $interface_ip; + $backend = new \OPNsense\Core\Backend(); + $interface = (string)$this->config->http_opn_interface; + $response = json_decode($backend->configdpRun('interface address', [$interface])); + // XXX Returns both IPv4 and IPv6 now. While "[0]" and + // "[1]" should remain in this order it would make sense + // to ensure "family" matches "inet" or "inet6" and/or + // pull both addresses for missing IPv6 support depending + // on how this should work. + if (!empty($response->$interface[0]->address)) { + $iplist[] = $response->$interface[0]->address; } } @@ -95,16 +104,16 @@ public function prepare() // IPv4 $_dst = '127.0.0.1'; $_family = 'inet'; - LeUtils::log("using IPv4 address: ${ip}"); + LeUtils::log("using IPv4 address: {$ip}"); } elseif (($_ipv6_enabled == true) && (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) { // IPv6 $_dst = '::1'; $_family = 'inet6'; - LeUtils::log("using IPv6 address: ${ip}"); + LeUtils::log("using IPv6 address: {$ip}"); } else { continue; // skip broken entries } - $anchor_rules .= "rdr pass ${_family} proto tcp from any to ${ip} port 80 -> ${_dst} port ${local_http_port}\n"; + $anchor_rules .= "rdr pass {$_family} proto tcp from any to {$ip} port 80 -> {$_dst} port {$local_http_port}\n"; } } else { LeUtils::log_error("no IP addresses found to setup port forward"); @@ -118,19 +127,16 @@ public function prepare() } // Create temporary port forward to allow acme challenges to get through - $anchor_setup = "rdr-anchor \"acme-client\"\n"; - file_put_contents("${configdir}/acme_anchor_setup", $anchor_setup); - chmod("${configdir}/acme_anchor_setup", 0600); - mwexec("/sbin/pfctl -f ${configdir}/acme_anchor_setup"); - file_put_contents("${configdir}/acme_anchor_rules", $anchor_rules); - chmod("${configdir}/acme_anchor_rules", 0600); - mwexec("/sbin/pfctl -a acme-client -f ${configdir}/acme_anchor_rules"); + File::file_put_contents("{$configdir}acme_anchor_setup", "rdr-anchor \"acme-client\"\n", 0600); + Shell::run_safe('/sbin/pfctl -f %s', ["{$configdir}/acme_anchor_setup"]); + File::file_put_contents("{$configdir}acme_anchor_rules", $anchor_rules, 0600); + Shell::run_safe('/sbin/pfctl -a %s -f %s', ['acme-client', "{$configdir}acme_anchor_rules"]); } public function cleanup() { // Flush OPNsense port forward rules. - mwexec('/sbin/pfctl -a acme-client -F all'); + Shell::run_safe('/sbin/pfctl -a %s -F %s', ['acme-client', 'all']); // Workaround to solve disconnection issues reported by some users. $backend = new \OPNsense\Core\Backend(); diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/TlsalpnAcme.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/TlsalpnAcme.php new file mode 100644 index 0000000000..f662e90a2f --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/TlsalpnAcme.php @@ -0,0 +1,147 @@ +cert_id); + + // Get configured TLS port for acme.sh web server. + $configObj = Config::getInstance()->object(); + $local_tls_port = $configObj->OPNsense->AcmeClient->settings->TLSchallengePort; + $this->acme_args[] = LeUtils::execSafe('--tlsport %s', (string)$local_tls_port); + + // Collect all IP addresses here, automatic port forward will be applied for each IP + $iplist = array(); + + // Add IP addresses from auto-discovery feature + if ($this->config->tlsalpn_acme_autodiscovery == '1') { + $dnslist = explode(',', $this->cert_altnames); + $dnslist[] = $this->cert_name; + foreach ($dnslist as $fqdn) { + // NOTE: This may take some time. + $ip_found = gethostbyname("{$fqdn}."); + if (!empty($ip_found)) { + $iplist[] = (string)$ip_found; + } + } + } + + // Add IP addresses from user input + $additional_ip = (string)$this->config->tlsalpn_acme_ipaddresses; + if (!empty($additional_ip)) { + foreach (explode(',', $additional_ip) as $ip) { + $iplist[] = $ip; + } + } + + // Add IP address from chosen interface + if (!empty((string)$this->config->tlsalpn_acme_interface)) { + $backend = new \OPNsense\Core\Backend(); + $interface = (string)$this->config->tlsalpn_acme_interface; + $response = json_decode($backend->configdpRun('interface address', [$interface])); + // XXX Returns both IPv4 and IPv6 now. While "[0]" and + // "[1]" should remain in this order it would make sense + // to ensure "family" matches "inet" or "inet6" and/or + // pull both addresses for missing IPv6 support depending + // on how this should work. + if (!empty($response->$interface[0]->address)) { + $iplist[] = $response->$interface[0]->address; + } + } + + // Check if IPv6 support is enabled + if (isset($configObj->system->ipv6allow) && ($configObj->system->ipv6allow == '1')) { + $_ipv6_enabled = true; + } else { + $_ipv6_enabled = false; + } + + // Generate rules for all IP addresses + $anchor_rules = ""; + if (!empty($iplist)) { + $dedup_iplist = array_unique($iplist); + // Add one rule for every IP + foreach ($dedup_iplist as $ip) { + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + // IPv4 + $_dst = '127.0.0.1'; + $_family = 'inet'; + LeUtils::log("using IPv4 address: {$ip}"); + } elseif (($_ipv6_enabled == true) && (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) { + // IPv6 + $_dst = '::1'; + $_family = 'inet6'; + LeUtils::log("using IPv6 address: {$ip}"); + } else { + continue; // skip broken entries + } + $anchor_rules .= "rdr pass {$_family} proto tcp from any to {$ip} port 443 -> {$_dst} port {$local_tls_port}\n"; + } + } else { + LeUtils::log_error("no IP addresses found to setup port forward"); + return false; + } + + // Abort if no rules were generated + if (empty($anchor_rules)) { + LeUtils::log_error("unable to setup a port forward (empty ruleset)"); + return false; + } + + // Create temporary port forward to allow acme challenges to get through + File::file_put_contents("{$configdir}acme_anchor_setup", "rdr-anchor \"acme-client\"\n", 0600); + Shell::run_safe('/sbin/pfctl -f %s', ["{$configdir}/acme_anchor_setup"]); + File::file_put_contents("{$configdir}acme_anchor_rules", $anchor_rules, 0600); + Shell::run_safe("/sbin/pfctl -a %s -f %s", ['acme-client', "{$configdir}acme_anchor_rules"]); + } + + public function cleanup() + { + // Flush OPNsense port forward rules. + Shell::run_safe('/sbin/pfctl -a %s -F %s', ['acme-client', 'all']); + + // Workaround to solve disconnection issues reported by some users. + $backend = new \OPNsense\Core\Backend(); + $response = $backend->configdRun('filter reload'); + return true; + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationFactory.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationFactory.php index 1a2611d219..ea12db6fa6 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationFactory.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidationFactory.php @@ -1,7 +1,7 @@ getNodeByReference(self::CONFIG_PATH . '.' . $uuid); if ($obj == null) { - LeUtils::log_error("challenge type not found: ${uuid}"); + LeUtils::log_error("challenge type not found: {$uuid}"); return null; } @@ -62,6 +62,9 @@ public function getValidation(string $uuid) case 'http01': $search_name = "http_" . $obj->http_service; break; + case 'tlsalpn01': + $search_name = "tlsalpn_" . $obj->tlsalpn_service; + break; } // Convert to PascalCase @@ -84,7 +87,7 @@ public function getValidation(string $uuid) } } } - LeUtils::log_error("challenge type not supported: " . (string)$search_name . " (${uuid})"); + LeUtils::log_error("challenge type not supported: " . (string)$search_name . " ({$uuid})"); return null; } } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Process.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Process.php index 24c9667078..0a0b150fe3 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Process.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Process.php @@ -1,6 +1,7 @@ error("Terminating process: " . json_encode(proc_get_status($handle))); + LeUtils::log_error("Terminating process: " . json_encode(proc_get_status($handle))); @proc_terminate($handle); } } @@ -102,7 +105,7 @@ public function __construct($cmd, $cwd = null, $env = null) self::manageOpenedProcess($this->handle); } else { - Utils::log()->error("Failed opening '$cmd' in '$cwd'"); + LeUtils::log_error("Failed opening '$cmd' in '$cwd'"); } } @@ -115,21 +118,49 @@ public function __destruct() } } - public function get($timeout = 5, $max_length = 8192, $ending = PHP_EOL) + private $linesBuffer = []; + + private function nextBufferedLine() { - $readables = array_filter($this->outputs, function ($stream) { - return is_resource($stream) && !feof($stream); - }); - - $micros = intval(($timeout - floor($timeout)) * 1000000); - $can_read = !empty($readables) && stream_select($readables, $w = [], $e = [], $timeout, $micros); - $stream = array_reduce(($can_read ? $readables : []), function ($a, $b) { - return is_resource($a) && !feof($a) ? $a : $b; - }, null); - - return is_resource($stream) - ? stream_get_line($stream, $max_length, $ending) - : false; + return empty($this->linesBuffer) + ? false + : array_shift($this->linesBuffer); + } + + /** + * Returns one line from stdout or stdin as it gets available. May return 'false' when no line became available + * within the specified $timeout or when another stream events occurred that returned no new content. + * @param $timeout float timeout in seconds + * @param $max_length int max length of a single line + * @return false|string One line of stdout/err (merged) or false when no new line exists. + */ + public function get($timeout = 5, $max_length = 64 * 1024) + { + if (($line = $this->nextBufferedLine()) !== false) { + return $line; + } + + $readables = array_filter($this->outputs, fn($stream) => is_resource($stream) && !feof($stream)); + $micros = intval(($timeout - floor($timeout)) * 1000000) + 100; + $timeout = floor($timeout); + $__ = null; + + $can_read = !empty($readables) + && stream_select($readables, $__, $__, $timeout, $micros) !== false; + + if ($can_read) { + foreach ($readables as $stream) { + $content = fread($stream, $max_length); + if ($content !== false) { + array_push($this->linesBuffer, ...preg_split('/\r\n|\n|\r/', $content)); + if (empty($this->linesBuffer[-1])) { + array_pop($this->linesBuffer); // remove trailing empty newline + } + } + } + } + + return $this->nextBufferedLine(); } public function put($data, $append = PHP_EOL) @@ -153,7 +184,7 @@ public function close($force = false) { // Read up-to 10k remaining lines from STDOUT/ERR to release locks before closing. for ($i = 0; ($line = $this->get(0)) && $i < 10000; $i++) { - Utils::log()->error("WARN: process: $line"); + LeUtils::log_error("WARN: process: $line"); } if ($this->isRunning()) { diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SSHKeys.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SSHKeys.php index 39c905bdda..ac70602d26 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SSHKeys.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SSHKeys.php @@ -1,6 +1,7 @@ info("No host key specified, using existing known_hosts entry for '$host'"); + LeUtils::log_debug("No host key specified, using existing known_hosts entry for '$host'"); $host_key = $known_by_host["key_info"]; } else { - Utils::log()->info("No host key specified and existing entry for '$host' cannot be used as isn't matching port $port."); + LeUtils::log_debug("No host key specified and existing entry for '$host' cannot be used as isn't matching port $port."); } } @@ -165,7 +168,7 @@ public function trustHost(string $host, $host_key = "", $port = self::DEFAULT_PO $is_key_known = true; } elseif ($known_by_key) { if (strcasecmp(trim($host), trim($known_by_key["host"])) != 0) { - Utils::log()->info("Host key is in known_hosts but hostname differs. Changing '$host' to '{$known_by_key["host"]}'."); + LeUtils::log_debug("Host key is in known_hosts but hostname differs. Changing '$host' to '{$known_by_key["host"]}'."); $host = $known_by_key["host"]; } $is_key_known = true; @@ -182,7 +185,7 @@ public function trustHost(string $host, $host_key = "", $port = self::DEFAULT_PO if ( empty($remote_host_keys) && $query_error - && $query_error["connection_refused"] + && ($query_error["connection_refused"] ?? false) && !$host_key && self::ALTERNATE_DEFAULT_KEY_TYPE != self::DEFAULT_KEY_TYPE ) { @@ -196,15 +199,15 @@ public function trustHost(string $host, $host_key = "", $port = self::DEFAULT_PO if (!empty($matching_remote_host_keys)) { if ($known_by_host && $known_by_host_matches_port) { - Utils::log()->info("Removing known_hosts entry with differing key for '{$known_by_host["host_query"]}' as it is in the way."); + LeUtils::log_debug("Removing known_hosts entry with differing key for '{$known_by_host["host_query"]}' as it is in the way."); $this->removeKnownHost($known_by_host["host_query"]); } foreach ($matching_remote_host_keys as $key) { - Utils::log()->info("Adding known_hosts entry: " . json_encode($key["key_info"], JSON_UNESCAPED_SLASHES)); + LeUtils::log_debug("Adding known_hosts entry: " . json_encode($key["key_info"], JSON_UNESCAPED_SLASHES)); $ok = file_put_contents($this->knownHostsFile(), $key["host_key"] . PHP_EOL, FILE_APPEND); if (!$ok) { - Utils::log()->error("Failed adding known_hosts entry {$key["host_key"]}"); + LeUtils::log_error("Failed adding SSH known_hosts entry {$key["host_key"]}"); } } @@ -331,12 +334,12 @@ public static function queryHostKey(string $host, $key_type = self::DEFAULT_KEY_ ? "" : PHP_EOL . "ssh-keyscan: " . join(PHP_EOL . "ssh-keyscan: ", $lines); - Utils::log()->error("Failed querying host keys ($key_type) for [$names] port $port. Exit code: {$p->exitCode} (error-marker: $marker) $output"); + LeUtils::log_error("Failed querying SSH host keys ($key_type) for [$names] port $port. Exit code: {$p->exitCode} (error-marker: $marker) $output"); } } if (empty($keys)) { - Utils::log()->info("Couldn't fetch public host key ($key_type) from {$host}:{$port}"); + LeUtils::log_debug("Couldn't fetch public host key ($key_type) from {$host}:{$port}"); if (!is_array($error) || empty($error)) { $error = ["connection_refused" => true]; @@ -384,13 +387,13 @@ public function getKnownHostKey(string $host, int $port = self::DEFAULT_PORT) ? "" : PHP_EOL . join(PHP_EOL, $lines); - Utils::log()->error("Failed querying known hosts for $name_or_ip ($host). Exit code: {$p->exitCode} $output"); + LeUtils::log_error("Failed querying SSH known hosts for $name_or_ip ($host). Exit code: {$p->exitCode} $output"); } } } if (empty($keys)) { - Utils::log()->info("Didn't find $host in known_hosts"); + LeUtils::log_debug("Could not find $host in known_hosts"); } return $keys; @@ -408,7 +411,7 @@ public function removeKnownHost(string $host) if ($p = Process::open(["ssh-keygen", "-R", $host, "-f", $this->knownHostsFile()])) { $ok = $p->close() === 0; if (!$ok) { - Utils::log()->error("Failed removing known hosts for $host. Return code was: {$p->exitCode}"); + LeUtils::log_error("Failed removing SSH known hosts for $host. Return code was: {$p->exitCode}"); } } @@ -433,11 +436,11 @@ public static function getHostKeyInfo(string $host_key) "key_length" => $matches[1] ]; } else { - Utils::log()->error("Unsupported hash type: $hash"); + LeUtils::log_error("Unsupported hash type: $hash"); } } - Utils::log()->error("Failed getting hash for host_key"); + LeUtils::log_error("Failed getting hash for host_key"); return false; } @@ -451,7 +454,7 @@ public function getIdentity(string $identity_type = self::DEFAULT_IDENTITY_TYPE, { Utils::requireThat(in_array($identity_type, self::IDENTITY_TYPES), "Identity type '$identity_type' unknown."); - list($key_type, $key_size) = explode('_', $identity_type, 2); + list($key_type, $key_size) = explode('_', "{$identity_type}_", 2); if (!$key_size && self::DEFAULT_IDENTITY_KEY_BITS[$key_type] > 0) { $key_size = self::DEFAULT_IDENTITY_KEY_BITS[$key_type]; } @@ -472,7 +475,7 @@ public function getIdentity(string $identity_type = self::DEFAULT_IDENTITY_TYPE, if ($p = Process::open($generate_key)) { while (($line = $p->get(10)) !== false) { - Utils::log()->info("SSH keygen: $line"); + LeUtils::log_debug("SSH keygen: $line"); } Utils::requireThat( diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpClient.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpClient.php index f9d2a00021..ff3982dc6c 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpClient.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpClient.php @@ -1,6 +1,7 @@ failed_status = ["invalid_parameters" => true]; - Utils::log()->error("Failed connecting to '$host'. Hostname or username is missing."); + LeUtils::log_error("Failed connecting to '$host'. Hostname or username is missing."); return false; } $trust = $this->ssh_keys->trustHost($host, $host_key, $port); if ($trust["ok"] !== true) { - Utils::log()->error("Failed establishing trust in '$host'; Cause: {$trust["error"]}"); + LeUtils::log_error("Failed establishing trust in '$host'; Cause: {$trust["error"]}"); unset($trust["ok"]); $this->failed_status = array_merge($trust, ["host_not_trusted" => true]); return false; @@ -103,7 +106,7 @@ public function connect($host, $username, $host_key = "", $port = SSHKeys::DEFAU "-oPreferredAuthentications=publickey" ); } else { - Utils::log()->error("Failed adding client identity ($identity). Connect will likely fail."); + LeUtils::log_error("Failed adding client identity ($identity). Connect will likely fail."); } // Adding the host @@ -113,7 +116,7 @@ public function connect($host, $username, $host_key = "", $port = SSHKeys::DEFAU if ($this->process = Process::open($cmd)) { $this->processAvailableInput(self::CONNECT_REPLY_TIMEOUT, 1, null, 0.75); if (($error = $this->lastError()) || !$this->process->isRunning()) { - Utils::log()->error("Failed connecting to '$host' (user: '$username')", $error); + LeUtils::log_error("Failed connecting to '$host' (user: '$username')", $error); return false; } $this->connection_info = ["host" => $host, "port" => $port, "user" => $username]; @@ -151,7 +154,7 @@ private function processAvailableInput(float $timeout = 0, $expected_lines = 0, $consumed = ($lines_consumer && $lines_consumer($line) === true); if (!$consumed) { - Utils::log()->info("SFTP: " . rtrim($line)); + LeUtils::log_debug("SFTP: " . rtrim($line)); } if (!$lines_consumer || $consumed) { @@ -173,7 +176,7 @@ public function close() $this->process = null; - if ($this->failed_status && $this->failed_status["connection_closed"]) { + if ($this->failed_status && ($this->failed_status["connection_closed"] ?? false)) { $this->clearError(); } } @@ -199,17 +202,28 @@ public function ls() $this->processAvailableInput(); $this->process->put("ls -la"); - $regex = '/^([bcdlsp\-][rwx\-]{9}[+@]?)\s+[0-9]+\s+([^\s]+)\s+([^\s]+)\s+([0-9]+)\s+(\w+\s+[0-9]+\s+[0-9:]+)\s+(.+)$/'; + $regex = '/^' + . '(?P[?bcCdDlMnpPs\-])' + . '(?P([rw\-]{2}[sStTx\-]){3})' + . '(?P[^\s])?' . '\s+' + . '(?P[^\s]+)' . '\s+' + . '(?P[^\s]+)' . '\s+' + . '(?P[^\s]+)' . '\s+' + . '(?P[^\s]+)' . '\s+' + . '(?P\w+\s+[0-9]+\s+[0-9:]+)' . '\s+' + . '(?P.+)' . '\s*' + . '$/'; + $this->processAvailableInput(self::COMMAND_REPLY_TIMEOUT, 2, function ($line) use (&$files, $regex) { if (preg_match($regex, $line, $matches)) { - $filename = trim(stripcslashes($matches[6])); // decodes octal UTF-8 sequences + $filename = trim(stripcslashes($matches["filename"])); // decodes octal UTF-8 sequences $files[$filename] = [ - "type" => $matches[1][0], - "permissions" => $matches[1], - "owner" => $matches[2], - "group" => $matches[3], - "size" => intval($matches[4]), - "mtime" => strtotime($matches[5]) + "type" => $matches["type"], + "permissions" => $matches["permissions"], + "owner" => stripcslashes($matches["owner"]), + "group" => stripcslashes($matches["group"]), + "size" => intval($matches["size"]), + "mtime" => strtotime($matches["mtime"]) ]; return true; } @@ -275,7 +289,7 @@ public function put($local_file, $remote_file = "", $preserve = true) . (empty($remote_file) ? "" : " " . escapeshellarg($remote_file))); $this->processAvailableInput(self::COMMAND_REPLY_TIMEOUT, 2); } else { - Utils::log()->info("put: File $local_file doesn't exist."); + LeUtils::log_debug("put: File $local_file doesn't exist."); $this->failed_status = ["file_not_found" => true, "error" => $local_file]; } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpUploader.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpUploader.php index bf6a466e32..bfd4b1885e 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpUploader.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/SftpUploader.php @@ -1,6 +1,7 @@ $local_file, "target" => $remote_file, "mode" => $chmod, - "group" => $chgrp + "group" => $chgrp, + "modtime" => $modtime ]; return $local_file; @@ -94,9 +99,10 @@ public function addFile(string $local_file, $remote_file = "", $chmod = false, $ * @param int $content_last_modified the unix timestamp when the content was last modified (is preserved when chmod is also specified). * @param bool $chmod the 4 digit unix permission to apply or false to leave it unchanged. * @param bool $chgrp the numeric group on the remote server to apply or false to leave it unchanged. + * @param bool $modtime a boolean to indicate that the modification times should be preserved. * @return string the name of the remote file. */ - public function addContent(string $content, string $remote_file = "", $content_last_modified = 0, $chmod = false, $chgrp = false): string + public function addContent(string $content, string $remote_file = "", $content_last_modified = 0, $chmod = false, $chgrp = false, $modtime = false): string { $local_file = $this->temporaryFile(); Utils::requireThat($local_file, "Failed creating temporary file for '$remote_file'"); @@ -113,7 +119,7 @@ public function addContent(string $content, string $remote_file = "", $content_l $remote_file = basename($local_file); } - $local_file = $this->addFile($local_file, $remote_file, $chmod, $chgrp); + $local_file = $this->addFile($local_file, $remote_file, $chmod, $chgrp, $modtime); $this->pending_files[$local_file]["delete_source"] = true; return $remote_file; @@ -153,7 +159,7 @@ public function upload(): int ->lastError(); if ($error) { - Utils::log()->error("Cannot continue since changing to initial remote path '{$this->pending_base_path}' failed", $error); + LeUtils::log_error("Cannot continue with SFTP upload since changing to initial remote path '{$this->pending_base_path}' failed", $error); return self::UPLOAD_ERROR; } } @@ -183,7 +189,7 @@ public function upload(): int try { $connection = $this->sftp->connected(); if (!$connection) { - Utils::log()->error("The sftp client is not connected, upload stopped."); + LeUtils::log_error("The SFTP client is not connected, upload stopped."); return self::UPLOAD_ERROR; } @@ -205,7 +211,7 @@ public function upload(): int foreach ($dir_names as $dir) { if ($error = $this->sftp->cd($dir)->lastError()) { if ($error["file_not_found"]) { - Utils::log()->info("Creating remote directory: $dir"); + LeUtils::log_debug("SFTP creating remote directory: $dir"); $this->sftp->clearError() ->mkdir($dir) ->cd($dir); @@ -216,7 +222,7 @@ public function upload(): int } if ($error = $this->sftp->lastError()) { - Utils::log()->error("Failed to cd into '$target_dir'.", $error); + LeUtils::log_error("SFTP failed to cd into '$target_dir'", $error); return self::UPLOAD_ERROR; } @@ -228,7 +234,7 @@ public function upload(): int if (empty($remote_files)) { $remote_files = $this->sftp->clearError()->ls(); if ($error = $this->sftp->lastError()) { - Utils::log()->error("Failed listing remote files.", $error); + LeUtils::log_error("SFTP failed listing remote files", $error); return self::UPLOAD_ERROR; } } @@ -236,32 +242,42 @@ public function upload(): int // Preparing upload $username = $connection["user"]; $remote_filename = basename((empty($file["target"]) ? $local_file : $file["target"])); - $remote_file = $remote_files[$remote_filename] ?: ["type" => "-", "owner" => $username]; + $remote_file = $remote_files[$remote_filename] ?? ["type" => "-", "owner" => $username]; $remote_is_file = $remote_file["type"] === "-"; - $remote_is_readonly = preg_match('/^[^wW]+$/', $remote_file["permissions"] ?: ""); + $remote_is_readonly = preg_match('/^[^wW]+$/', $remote_file["permissions"] ?? ""); + LeUtils::log_debug("SFTP current remote file permissions: '{$remote_file["permissions"]}'"); // Check if a folder/socket/symlink, etc is in the way if (!$remote_is_file) { - Utils::log()->error("Failed uploading file '{$local_file}' as there is a non-file in the way at '{$file["target"]}'"); + LeUtils::log_error("SFTP failed uploading file '{$local_file}' as there is a non-file in the way at '{$file["target"]}'"); return self::UPLOAD_ERROR_NO_OVERWRITE; } - $chgrp = $file["group"] ?: ""; + $chgrp = $file["group"] ?? ""; $chgrp = preg_match('/^\d+$/', $chgrp) ? (string)$chgrp : false; - $chmod = $file["mode"] ?: ""; + $chmod = $file["mode"] ?? ""; $chmod = preg_match('/^0\d{3}$/', $chmod) ? (string)$chmod : false; + // Preserving the modification time is not supported by all SFTP servers. + $preserve = $file["modtime"]; + if ($preserve !== false) { + LeUtils::log("SFTP upload will try to preserve file modification time for '{$file["target"]}'"); + } else { + LeUtils::log("SFTP upload will not preserve file modification time for '{$file["target"]}'"); + } // Initial upload when permissions are properly set. $should_upload_with_permission_change = $chmod !== false && isset($remote_files[$remote_filename]); + // Upload file. if (!$remote_is_readonly) { + LeUtils::log("Uploading file '{$local_file}' to '{$file["target"]}'"); $preserve_times_and_mod = $chmod !== false; - if ($error = $this->sftp->put($local_file, $remote_filename, $preserve_times_and_mod)->lastError()) { + if ($error = $this->sftp->put($local_file, $remote_filename, $preserve)->lastError()) { if ($error["permission_denied"] !== true) { $should_upload_with_permission_change = false; } @@ -269,7 +285,7 @@ public function upload(): int if ($should_upload_with_permission_change) { $this->sftp->clearError(); } else { - Utils::log()->error("Failed uploading file '{$local_file}' to '{$file["target"]}'", $error); + LeUtils::log_error("SFTP failed uploading file '{$local_file}' to '{$file["target"]}'", $error); return self::UPLOAD_ERROR_NO_PERMISSION; } } else { @@ -279,19 +295,21 @@ public function upload(): int // Second attempt when initial failed or was skipped due to write protection (only possible if we have chmod defined to reset permissions later) if ($should_upload_with_permission_change && $this->isFileOwnedByConnection($remote_file, $connection)) { - Utils::log()->info("Trying to upload file '{$local_file}' to '{$file["target"]}' with adjusted permissions"); + LeUtils::log("Trying to upload file '{$local_file}' to '{$file["target"]}' with adjusted permissions"); + // Change file permission to make it writable. if ($error = $this->sftp->chmod($remote_filename, '0600')->lastError()) { - Utils::log()->error("Failed changing permission to '0600' for '{$file["target"]}'. ", $error); + LeUtils::log_error("SFTP failed changing permission to '0600' for '{$file["target"]}'", $error); $this->sftp->clearError(); } - if ($error = $this->sftp->put($local_file, $remote_filename)->lastError()) { - Utils::log()->error("Failed uploading file (with adjusted permissions) '{$local_file}' to '{$file["target"]}'", $error); + // Try again to upload file. + if ($error = $this->sftp->put($local_file, $remote_filename, $preserve)->lastError()) { + LeUtils::log_error("SFTP failed uploading file (with adjusted permissions) '{$local_file}' to '{$file["target"]}'", $error); return self::UPLOAD_ERROR_NO_PERMISSION; } } elseif ($remote_is_readonly) { - Utils::log()->error("Failed uploading file '{$local_file}' to '{$file["target"]}'. Existing file is write protected."); + LeUtils::log_error("SFTP failed uploading file '{$local_file}' to '{$file["target"]}'. Existing file is write protected."); return self::UPLOAD_ERROR_NO_PERMISSION; } @@ -299,14 +317,14 @@ public function upload(): int // Applying chmod / chgrp if requested. if ($chmod) { if ($error = $this->sftp->chmod($remote_filename, $chmod)->lastError()) { - Utils::log()->error("Failed chmod ($chmod) for '{$file["target"]}'", $error); + LeUtils::log_error("SFTP failed chmod ($chmod) for '{$file["target"]}'", $error); return self::UPLOAD_ERROR_CHMOD_FAILED; } } if ($chgrp) { if ($error = $this->sftp->chgrp($remote_filename, $chgrp)->lastError()) { - Utils::log()->error("Failed chgrp ($chgrp) for '{$file["target"]}'", $error); + LeUtils::log_error("SFTP failed chgrp ($chgrp) for '{$file["target"]}'", $error); return self::UPLOAD_ERROR_CHGRP_FAILED; } } @@ -343,11 +361,12 @@ private function isFileOwnedByConnection(array $remote_file, array $connection): $this->sftp->clearError(); - if ($error = $this->sftp->put($local_test_file, $remote_test_file)->lastError()) { - Utils::log()->error("Failed uploading test file to detect ownership. Next uploads may fail as well.", $error); + // Perform test upload without preserving file modification time (extra safekeeping). + if ($error = $this->sftp->put($local_test_file, $remote_test_file, false)->lastError()) { + LeUtils::log_error("Failed uploading SFTP test file to detect ownership. Next uploads may fail as well", $error); } else { // Get owner of the test file - $file_info = $this->sftp->ls()[$remote_test_file] ?: ["owner" => -1]; + $file_info = $this->sftp->ls()[$remote_test_file] ?? ["owner" => -1]; // Cleanup $this->sftp->rm($remote_test_file); $this->sftp->clearError(); @@ -370,7 +389,7 @@ private function deleteSourceIfRequested($file) if ( isset($this->pending_files[$file]) && is_array($existing = $this->pending_files[$file]) - && $existing["delete_source"] === true + && ($existing["delete_source"] ?? false) === true ) { unlink($existing["source"]); } @@ -400,7 +419,7 @@ private function temporaryFile($delete_all = false) } if ($count > 0) { - Utils::log()->info("Removed $count files in shutdown hook instead of object destruction."); + LeUtils::log_debug("Removed $count files in shutdown hook instead of object destruction."); } $shared_temporary_files = []; @@ -408,7 +427,7 @@ private function temporaryFile($delete_all = false) } $index = $this->temporary_files_index; - if ($index <= 0 || !is_array($shared_temporary_files[$index])) { + if ($index <= 0 || !is_array($shared_temporary_files[$index] ?? null)) { $index = $this->temporary_files_index = ++$shared_temporary_files_index_sequence; $shared_temporary_files[$index] = []; } diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Utils.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Utils.php index f16d4082f1..a06e7b8069 100644 --- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Utils.php +++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/Utils.php @@ -1,6 +1,7 @@ error("FATAL: $message"); + LeUtils::log_error("FATAL: $message"); throw new \AssertionError($message); } return $expression; @@ -142,4 +82,169 @@ public static function resolvePath(string $file, string $base = ".") return DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, $path); } + + /** + * @return string the path where acme.sh config is stored or null if not available. + */ + public static function configPath(): string + { + static $paths = [ + '/var/etc/acme-client', + __DIR__ + ]; + foreach ($paths as $path) { + if (is_dir($path)) { + return $path; + } + } + return self::requireThat(false, "No config path"); + } + + /** + * @param string $automation_id the automation numeric id or UUID. + * @return mixed|null the automation action when found. + */ + public static function getAutomationActionById($automation_id) + { + $config = \OPNsense\Core\Config::getInstance()->object(); + $client = $config->OPNsense->AcmeClient; + + foreach ($client->actions->children() as $action) { + if ( + $automation_id === (string)$action->attributes()["uuid"] + || $automation_id === (string)$action->id + ) { + return $action; + } + } + + return null; + } + + /** + * Print CLI help. + * @see Utils::runCLIMain + * @param string $about about text + * @param string $examples examples text + * @param array $commands the commands + * @return void + */ + public static function printCLIHelp($about, $examples, $commands = []) + { + static $options = [ + "-h, --help Print commandline help", + "--automation-id Read options from the action specified by id or uuid", + "--no-error Always exit with 0 (original exit codes are still logged)", + ]; + + echo $about . PHP_EOL + . "Usage: " . basename($GLOBALS["argv"][0]) . " [options] [--command=]COMMAND" . PHP_EOL + . PHP_EOL . join(PHP_EOL, $options) . PHP_EOL; + + foreach ($commands as $name => $cmd) { + echo PHP_EOL . "COMMAND \"$name\" {$cmd["description"]}" . PHP_EOL . "Options:" . PHP_EOL; + foreach ($cmd["options"] as $option) { + $option = preg_replace(['/^([^:]+)$/', '/(.+)::$/', '/(.+):$/'], ['[$1]', '[$1=value]', '$1=value'], "--$option"); + echo " $option" . PHP_EOL; + } + } + + echo PHP_EOL . "Examples:" . PHP_EOL + . preg_replace('/\r\n|\n|\r/', PHP_EOL, $examples) + . PHP_EOL . PHP_EOL; + } + + /** + * Helper that implements `main();` for a CLI application following the command design. + * + * `$commands` follows the format: + * ```php + * [ + * "command-name" => [ + * "description" => "...", + * "options" => ["arg1::", "arg2::", "arg3::"], + * "implementation" => "commandImplementationFunction", + * "default" => true | false, + * ], + * ] + * ``` + * + * @param callable $help method that display's CLI help. + * @param callable $optionsByActionId method that returns CLI args (assoc array) from an automation action id. + * @param array $commands the list of commands that the CLI application can execute. + * @param int $exit_success exit code for success + * @param int $exit_unknown_command exit code for no matching command + * @return void + */ + public static function runCLIMain(callable $help, callable $optionsByActionId, $commands = [], $exit_success = 0, $exit_unknown_command = 255) + { + global $argv; + $command = self::getSelectedCLICommand($commands); + $options = ["help", "no-error"]; + + $has_automation_id = preg_match('/--automation-id=\S+/', join(" ", $argv)); + if ($has_automation_id) { + $options = array_merge($options, ["automation-id:", "certificates::"]); + } else { + $options = array_merge($options, $command["options"]); + } + + $index = 0; + if ($options = getopt("h", $options, $index)) { + if (isset($options["h"]) || isset($options["help"])) { + $help(); + } else { + $options = array_filter($options, function ($value) { + return !is_string($value) + || (!empty($value = trim($value)) && $value !== "__default_value"); + }); + + if (isset($options["automation-id"])) { + if (is_array($config = $optionsByActionId($options["automation-id"]))) { + $options = array_merge($config, $options); + } else { + LeUtils::log_error("No usable config found for automation-id {$options["automation-id"]}"); + exit(1); + } + } + + if (is_callable($runner = $command["implementation"])) { + $code = $runner($options); + + if ($code != $exit_success) { + LeUtils::log_error("Command execution failed, exit code $code. Last input was: " . json_encode($options, JSON_UNESCAPED_SLASHES)); + } + + exit(isset($options["no-error"]) ? $exit_success : $code); + } else { + exit($exit_unknown_command); + } + } + } else { + if (count($argv) < 2) { + $help(); + } else { + $cmd = join(" ", $argv); + LeUtils::log_error("Parsing of '$cmd' failed at argument '{$argv[$index]}'"); + } + exit(1); + } + } + + private static function getSelectedCLICommand($commands = []) + { + $default = null; + $command = null; + $parsed_args = getopt("", ["command::"]); + foreach ($commands as $name => $cmd) { + if (in_array($name, $GLOBALS["argv"]) || ($parsed_args["command"] ?? "") === $name) { + $command = $cmd; + } + if (($cmd["default"] ?? false) === true) { + $default = $cmd; + } + } + + return $command ?? $default; + } } diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.php b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.php index 347cd471b0..0bfef54092 100644 --- a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.php +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.php @@ -1,7 +1,7 @@ configdRun('firmware plugin ' . escapeshellarg($name))); } + + /** + * check if the specfied package is installed + * @param $name package name + * @return bool is the package installed + */ + public function isPackageInstalled($name) + { + $backend = new Backend(); + $_package_list = $backend->configdRun('firmware local'); + if (preg_match("/^$name\|\|.*/m", $_package_list)) { + return 1; + } + return 0; + } } diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml index 362bb20177..5569efaabb 100644 --- a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml @@ -1,15 +1,15 @@ //OPNsense/AcmeClient - 3.1.0 + 4.3.0 A secure ACME Client plugin - 0 + 0 Y - 1 + 1 Y @@ -28,26 +28,32 @@ N - prod + prod Production Environment [default] Staging Environment - - 43580 + + 43580 1024 65535 Y + + 43581 + 1024 + 65535 + Y + - 600 + 600 10 86400 Y - 0 + 0 N @@ -59,7 +65,7 @@ Related HAProxy ACL not found. - N + N N @@ -71,7 +77,7 @@ Related HAProxy action not found. - N + N N @@ -83,7 +89,7 @@ Related HAProxy server not found. - N + N N @@ -95,12 +101,12 @@ Related HAProxy backend not found. - N + N N Y - normal + normal normal extended @@ -111,7 +117,7 @@ Y - 1 + 1 @@ -120,17 +126,17 @@ N - 1 + 1 Y Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. @@ -138,10 +144,12 @@ Y - letsencrypt + letsencrypt Buypass Buypass Test CA + Google + Google Test CA Let's Encrypt [default] Let's Encrypt Test CA SSL.com @@ -151,17 +159,17 @@ N - /^https?:\/\/.*[^\/]$/ + /^https?:\/\/.*[^\/]$/ The URL must be a valid ACME endpoint without a trailing slash. N - /^.{1,8192}$/u + /^.{1,8192}$/u Should be a string between 1 and 8192 characters. N - /^.{1,8192}$/u + /^.{1,8192}$/u Should be a string between 1 and 8192 characters. @@ -171,7 +179,7 @@ N - 100 + 100 100 1000 @@ -187,23 +195,23 @@ N - 1 + 1 Y Y - /^[^\s^\t^,^;^\\^\/^(^)^\[^\]]{1,255}$/u + /^[^\s^\t^,^;^\\^\/^(^)^\[^\]]{1,255}$/u Please provide a valid FQDN, i.e. www.example.com or mail.example.com (max 255 characters). N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - Y - /^[^\s^\t^;^\\^\/^(^)^\[^\]]{1,65535}$/u + Y + /^[^\s^\t^;^\\^\/^(^)^\[^\]]{1,65535}$/u lower Please provide one or more valid FQDNs, i.e. www.example.com or mail.example.com. Field length is limited to 65535 characters. @@ -218,8 +226,8 @@ - Related item not found - N + Related item not found. + N Y @@ -233,13 +241,13 @@ - Related item not found - N + Related item not found. + N Y Y - key_4096 + key_4096 2048 bit 3072 bit @@ -249,9 +257,14 @@ - 0 + 0 N + + N + /^.{1,255}$/u + Should be a string between 1 and 255 characters. + @@ -263,23 +276,24 @@ - Related automation not found - Y + Related automation not found. + Y + Y N - 1 + 1 Y Y - 1 - 60 - 60 + 0 + 5000 + 60 Y - none + none Not using DNS alias mode Automatic Mode (uses DNS lookups) @@ -304,7 +318,7 @@ N - 100 + 100 100 1000 @@ -320,52 +334,52 @@ N - 1 + 1 Y Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. Y - dns01 + dns01 HTTP-01 DNS-01 + TLS-ALPN-01 Y - opnsense + opnsense OPNsense Web Service (automatic port forward) HAProxy HTTP Frontend Integration (OPNsense plugin) - 1 + 1 N N - wan /^(?!0).*$/ N - Y + Y - 1 + 1 N @@ -380,101 +394,167 @@ - Related HAProxy frontend not found - Y + Related HAProxy frontend not found. + Y N + + Y + acme + + acme.sh TLS Web Server (automatic port forward) + + + + 1 + N + + + N + + /^(?!0).*$/ + + + + N + Y + Y - dns_freedns + dns_freedns - 1984Hosting API - ACME DNS API - Acmeproxy API - Alwaysdata.com API - aliyun.com API - All-Inkl.com domain API - ArvanCloud API - AutoDNS (InterNetX) API + 1984Hosting + ACME DNS + Acmeproxy + Active24 + Alwaysdata.com + aliyun.com + All-Inkl.com + ArvanCloud + ArtFiles + Aurora (PCextreme/Versio) + AutoDNS (InterNetX) AWS Route 53 - Azure DNS API - ClouDNS API - CloudFlare.com API - CloudXNS.com API - Core-Networks API - cyon.ch API - DDNSS API - deSEC.io API - DigitalOcean API - DirectAdmin API - DNSimple API - Domeneshop API - DNSMadeEasy.com API - DNSPod.cn API - Domain-Offensive LetsEncrypt API - Domain-Offensive Resellerinterface/Domainrobot API - DreamHost DNS API - DuckDNS API - Dyn Managed DNS API - Dynu API + Azure DNS + Bunny + ClouDNS + CloudFlare.com + CloudXNS.com + Core-Networks + ConoHa + Constellix + cPanel + cyon.ch + DDNSS + deSEC.io + DigitalOcean + DirectAdmin + DNSExit + dnsHome + DNSimple + DNS.Services + Domeneshop + DNSMadeEasy.com + DNSPod.cn + Domain-Offensive LetsEncrypt + Domain-Offensive Resellerinterface/Domainrobot + DreamHost + DuckDNS + Dyn Managed + Dynu + dynv6 + EasyDNS EUserv - FreeDNS API - Gandi LiveDNS API - GoDaddy.com API - Google Cloud DNS API + Exoscale + Fornex + FreeDNS + Gandi LiveDNS + GoDaddy.com + Google Cloud DNS + Google Domains GratisDNS.dk - Hetzner DNS API - hexonet.com DNS API - hosting.de API + Hetzner + Hetzner Cloud + hexonet.com + hosting.de Hurricane Electric - Infoblox API - Infomaniak API - INWX XMLRPC API - IONOS domain API - ISPConfig 3.1+ API - Joker API - KingHost DNS API - Knot (knsupdate) DNS API - LeaseWeb API - lexicon DNS API - Linode API (v3 / Deprecated) - Linode API (v4) - Loopia API - LuaDNS.com API - MailinaBox API - Name.com API - Namecheap API - Namesilo.com API - Nederhost API - netcup DNS API - Njalla API - NS1.com API + Hurricane Electric DDNS + Infoblox + Infomaniak + internetbs.net + INWX XMLRPC + IONOS domain + IPv64.net + ISPConfig 3.1+ + JD Cloud + Joker + KingHost + Knot (knsupdate) + LeaseWeb + Lexicon (deprecated) + Lima-City (TrafficPlex) + Linode (v3/deprecated) + Linode (v4) + Loopia + LuaDNS.com + MailinaBox + mijn.host + MyDNS.JP + Mythic Beasts + Name.com + Namecheap + Namesilo.com + Nederhost + netcup + nic.ru + Njalla + NS1.com nsupdate (RFC 2136) + online.net OPNsense BIND Plugin - OVH, kimsufi, soyoustart and runabove API - PowerDNS.com API - Plesk XML API - Porkbun API + Oracle Cloud Infrastructure (OCI) + OVH, kimsufi, soyoustart and runabove + PowerDNS.com + Plesk + PointHQ + Porkbun + Rackspace + rage4 + RegRu + Scaleway SchlundTech - selectel.com / selectel.ru domain API - Servercow API v1 - UnoEuro API - Variomedia.de API - Vscale API - Vultr API - Yandex PDD API - Zilore DNS API - Zone.eu API - zonomi.com domain API + selectel.com / selectel.ru + Selfhost + Servercow + Simply.com + Spaceship + Technitium + Timeweb Cloud + Transip + united-domains Reselling + UnoEuro + Variomedia.de + Vscale + Vultr + Websupport.sk + World4You + Yandex PDD + Zilore + Zone.eu + ZoneEdit + zonomi.com - 1 + 0 84600 - 120 - Please specify a value between 1 and 84600. + 0 + Please specify a value between 0 and 84600 seconds. Y + + N + N @@ -511,6 +591,13 @@ N + + 0 + N + + + N + N @@ -523,6 +610,9 @@ N + + N + N @@ -549,7 +639,7 @@ N - 1 + 1 N @@ -557,9 +647,30 @@ N + + N + + + N + + + N + + + N + + + N + N + + N + + + N + N @@ -608,12 +719,24 @@ N + + N + N + + N + N + + N + + + N + N @@ -632,6 +755,9 @@ N + + N + N @@ -641,15 +767,25 @@ N + N + + N + + + N + N N + + N + N @@ -661,8 +797,17 @@ N - 1 + 1 + + N + + + N + + + N + N @@ -683,69 +828,69 @@ N - cloudflare + cloudflare - Aliyun.com API - AuroraDNS API + Aliyun.com (UNSUPPORTED) + AuroraDNS (UNSUPPORTED) Auto API - Azure DNS API - CloudFlare API - ClouDNS API - CloudXNS API - ConoHa API - Constellix API - DigitalOcean API - Dinahosting API - DirectAdmin API - DNSimple API - DnsMadeEasy API - DNSPark API - DNSPod API - Dreamhost API - EasyDNS API - Easyname API - ExoScale API - Gandi API - Gehirn API - Glesys API - GoDaddy API - Google Cloud DNS API - GratisDNS API - Hurricane Electric DNS API - Hetzner API - Hover API - Infoblox API - Internet.bs API - INWX API - Linode API - Linode v4 API - Localzone API - LuaDNS API - Memset API - Namecheap API - Namesilo API - Netcup API - NFSN API - NS1 API - OnApp API - Online API - OVH API - Plesk API - PointHQ API - PowerDNS API - Rackspace API - Rage4 API - Route 53 API - SafeDNS API - SakuraCloud API - Softlayer API - Subreg API - Transip API - Vultr API - Yandex API - Zeit API - Zilore API - Zonomi API + Azure (UNSUPPORTED) + CloudFlare (UNSUPPORTED) + ClouDNS (UNSUPPORTED) + CloudXNS (UNSUPPORTED) + ConoHa (UNSUPPORTED) + Constellix (UNSUPPORTED) + DigitalOcean (UNSUPPORTED) + Dinahosting (UNSUPPORTED) + DirectAdmin (UNSUPPORTED) + DNSimple (UNSUPPORTED) + DnsMadeEasy (UNSUPPORTED) + DNSPark + DNSPod (UNSUPPORTED) + Dreamhost (UNSUPPORTED) + EasyDNS (UNSUPPORTED) + Easyname + ExoScale (UNSUPPORTED) + Gandi (UNSUPPORTED) + Gehirn + Glesys + GoDaddy (UNSUPPORTED) + Google Cloud (UNSUPPORTED) + GratisDNS (UNSUPPORTED) + Hurricane Electric (UNSUPPORTED) + Hetzner (UNSUPPORTED) + Hover + Infoblox (UNSUPPORTED) + Internet.bs (UNSUPPORTED) + INWX (UNSUPPORTED) + Linode v3 (UNSUPPORTED) + Linode v4 (UNSUPPORTED) + Localzone + LuaDNS (UNSUPPORTED) + Memset + Namecheap (UNSUPPORTED) + Namesilo (UNSUPPORTED) + Netcup (UNSUPPORTED) + NFSN (UNSUPPORTED) + NS1 (UNSUPPORTED) + OnApp + Online (UNSUPPORTED) + OVH (UNSUPPORTED) + Plesk (UNSUPPORTED) + PointHQ (UNSUPPORTED) + PowerDNS (UNSUPPORTED) + Rackspace (UNSUPPORTED) + Rage4 (UNSUPPORTED) + Route 53 (UNSUPPORTED) + SafeDNS + SakuraCloud + Softlayer + Subreg + Transip (UNSUPPORTED) + Vultr (UNSUPPORTED) + Yandex (UNSUPPORTED) + Zeit (UNSUPPORTED) + Zilore (UNSUPPORTED) + Zonomi (UNSUPPORTED) @@ -754,6 +899,9 @@ N + + N + N @@ -762,7 +910,7 @@ N - https://api.loopia.se/RPCSERV + https://api.loopia.se/RPCSERV N @@ -791,6 +939,18 @@ N + + N + + + N + + + N + + + N + N @@ -837,13 +997,28 @@ N + + N + + + N + + + N + + + N + + + N + N - localhost + localhost N - 443 + 443 N @@ -853,7 +1028,7 @@ N - 0 + 0 N @@ -894,12 +1069,83 @@ N + + N + + API version 1 (deprecated) + API version 2 + + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + N N + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + 0 + + + N + + + N + + + N + N @@ -936,9 +1182,13 @@ N + N + + N + N @@ -957,6 +1207,12 @@ N + + N + + + N + N @@ -975,9 +1231,18 @@ N + + N + + + N + N + + N + N @@ -998,9 +1263,10 @@ N - sha1 + plain - SHA1 + plain + SHA1 (deprecated in December 2022) @@ -1018,6 +1284,115 @@ N + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + https://identity.xxxx.conoha.io/v2.0 + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + + + N + @@ -1026,17 +1401,17 @@ N - 1 + 1 Y Y - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. @@ -1045,45 +1420,44 @@ Restart OPNsense Web UI Restart HAProxy (OPNsense plugin) Restart Nginx (OPNsense plugin) - Upload certificate to Highwinds CDN + Reload Caddy (OPNsense plugin) Upload certificate via SFTP + Remote Command via SSH Upload certificate to FRITZ!Box router + Upload certificate to Palo Alto Networks Firewall + Upload certificate to Proxmox Backup Server + Upload certificate to Proxmox VE + Upload certificate to Ruckus controller + Upload certificate to HashiCorp Vault Upload certificate to Synology DSM + Upload certificate to TrueNAS Core Server + Upload certificate to Zyxel GS1900 series switches + Update local Unifi keystore System or Plugin Command - - N - /^.{1,1024}$/u - Should be a string between 1 and 1024 characters. - - - N - /^.{1,1024}$/u - Should be a string between 1 and 1024 characters. - N - /^.{1,255}$/u + /^.{1,255}$/u Should be a string between 1 and 255 characters. N - /^.+?\s(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?(?:\s.+?)?$/i + /^.+?\s(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?(?:\s.+?)?$/i Should be a valid public SSH host key (see "known_hosts"). N 1 - 49151 - 22 - Should be a valid port number between 1 and 49151. + 65535 + 22 + Should be a valid port number between 1 and 65535. N - /^.{1,128}$/u + /^.{1,128}$/u Should be a string between 1 and 128 characters. @@ -1096,48 +1470,89 @@ N - /^.{1,512}$/u + /^.{1,512}$/u Should be a string between 1 and 512 characters. N - /^[0-9]+$/u + /^[0-9]+$/u Should be a numeric value. N - /^0[0-9]{3}$/u + /^0[0-9]{3}$/u A unix permission, 4 digits (e.g. 0440). N - /^0[0-9]{3}$/u + /^0[0-9]{3}$/u A unix permission, 4 digits (e.g. 0400). + + N + 0 + N - /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui + /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui Should be a string between 1 and 255 characters. Characters are limited to [a-z], [0-9] and [{}@./-_%] and the string must neither begin nor end with '/'. N - /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui + /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui Should be a string between 1 and 255 characters. Characters are limited to [a-z], [0-9] and [{}@./-_%] and the string must neither begin nor end with '/'. N - /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui + /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui Should be a string between 1 and 255 characters. Characters are limited to [a-z], [0-9] and [{}@./-_%] and the string must neither begin nor end with '/'. N - /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui + /^(?![\/\\])[\w\d_\-@.\/{}%]{1,255}(?<![\/\\])$/ui Should be a string between 1 and 255 characters. Characters are limited to [a-z], [0-9] and [{}@./-_%] and the string must neither begin nor end with '/'. + + N + /^.{1,255}$/u + Should be a string between 1 and 255 characters. + + + N + + /^.+?\s(?:[a-z0-9+\/]{4})*(?:[a-z0-9+\/]{2}==|[a-z0-9+\/]{3}=)?(?:\s.+?)?$/i + Should be a valid public SSH host key (see "known_hosts"). + + + N + 1 + 65535 + 22 + Should be a valid port number between 1 and 65535. + + + N + /^.{1,128}$/u + Should be a string between 1 and 128 characters. + + + N + + ECDSA + RSA + ed25519 + + + + N + /^.{1,1024}$/u + Should be a shell command between 1 and 1024 characters. + @@ -1153,16 +1568,13 @@ Select a command from the list. N - - - N - + - 5000 + 5000 N - http + http N HTTP [default] @@ -1171,37 +1583,210 @@ N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. - 1 + 1 N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. N - /^.{1,1024}$/u + /^.{1,1024}$/u Should be a string between 1 and 1024 characters. + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + root + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + 8006 + N + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + pam + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + acme + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + root + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + 8007 + N + + + N + localhost + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + pam + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + acme + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + N + + + N + + + N + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + localhost + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + http + N + + HTTP [default] + HTTPS + + + + /usr/local/share/java/unifi/data/keystore + N + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + acme + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + 1 + + + N + /^.{1,1024}$/u + Should be a string between 1 and 1024 characters. + + + admin + N + + + N + + + 0 + N + + + 0 + N + diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M1_6_0.php b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M1_6_0.php index 22bb910018..f2718c91f2 100644 --- a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M1_6_0.php +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M1_6_0.php @@ -44,26 +44,26 @@ public function run($model) foreach ($model->getNodeByReference('accounts.account')->iterateItems() as $account) { $account_id = (string)$account->id; $account_dir = $dir . $account_id; - $new_account_dir = "${dir}${account_id}_${env}"; + $new_account_dir = "{$dir}{$account_id}_{$env}"; // Check if account directory exists // Accounts that haven't been used yet don't need to be migrated. if (is_dir($account_dir)) { // Check if account configuration can be found. - $account_file = "${account_dir}/account.conf"; + $account_file = "{$account_dir}/account.conf"; if (is_file($account_file)) { // Parse config file and modify path information $account_conf = parse_ini_file($account_file); foreach ($account_conf as $key => $value) { switch ($key) { case 'ACCOUNT_KEY_PATH': - $account_conf[$key] = "${new_account_dir}/account.key"; + $account_conf[$key] = "{$new_account_dir}/account.key"; break; case 'ACCOUNT_JSON_PATH': - $account_conf[$key] = "${new_account_dir}/account.json"; + $account_conf[$key] = "{$new_account_dir}/account.json"; break; case 'CA_CONF': - $account_conf[$key] = "${new_account_dir}/ca.conf"; + $account_conf[$key] = "{$new_account_dir}/ca.conf"; break; } } @@ -71,11 +71,11 @@ public function run($model) // Convert array back to ini file format $new_account_conf = array(); foreach ($account_conf as $key => $value) { - $new_account_conf[] = "${key}='${value}'"; + $new_account_conf[] = "{$key}='{$value}'"; } // Write changes back to file - file_put_contents($account_file, implode("\r\n", $new_account_conf) . "\n"); + file_put_contents($account_file, implode("\n", $new_account_conf) . "\n"); chmod($account_file, 0600); // Finally, rename account directory diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M3_3_0.php b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M3_3_0.php new file mode 100644 index 0000000000..16c2164bb6 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M3_3_0.php @@ -0,0 +1,52 @@ +getNodeByReference('validations.validation')->iterateItems() as $validation) { + $updateUrl = trim(strval($validation->dns_acmedns_updateurl ?? '')); + $baseUrl = trim(strval($validation->dns_acmedns_baseurl ?? '')); + + if (!empty($updateUrl) && empty($baseUrl)) { + // Translate "https://auth.acme-dns.io/update" to "https://auth.acme-dns.io" + $baseUrl = preg_replace('/\/update$/', '', $updateUrl); + $validation->dns_acmedns_baseurl = $baseUrl; + $validation->dns_acmedns_updateurl = null; + } + } + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M4_0_0.php b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M4_0_0.php new file mode 100644 index 0000000000..51b3817707 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M4_0_0.php @@ -0,0 +1,107 @@ + $value) { + $new_account_conf[] = "{$key}='{$value}'"; + } + + // Write changes back to file + file_put_contents($account_file, implode("\n", $new_account_conf) . "\n"); + chmod($account_file, 0600); + } + } + } + + // Create new acme home directory + if (!is_dir($new_acme_home)) { + mkdir($new_acme_home, 0750); + } + + // Migrate all certificates to new directory + // OLD: /var/etc/acme-client/home/opnsense.example.com + // NEW: /var/etc/acme-client/cert-home/659971be677b69.19708532/opnsense.example.com + foreach ($model->getNodeByReference('certificates.certificate')->iterateItems() as $cert) { + $cert_id = (string)$cert->id; + $cert_name = (string)$cert->name; + + $old_cert_home = $old_acme_home . $cert_name; + $new_cert_home = $new_acme_home . $cert_id . '/' . $cert_name; + $old_cert_home_ecc = $old_acme_home . $cert_name . '_ecc'; + $new_cert_home_ecc = $new_acme_home . $cert_id . '/' . $cert_name . '_ecc'; + $_parent_dir = $new_acme_home . $cert_id; + + // Check if cert home directory exists + // Certs that haven't been issued yet don't need to be migrated. + if (is_dir($old_cert_home)) { + // Create parent directory + if (!is_dir($_parent_dir)) { + mkdir($_parent_dir, 0750); + } + // Rename cert home directory + rename($old_cert_home, $new_cert_home); + } + + // Migrate ECC certs + if (is_dir($old_cert_home_ecc)) { + // Create parent directory + if (!is_dir($_parent_dir)) { + mkdir($_parent_dir, 0750); + } + // Rename cert home directory + rename($old_cert_home_ecc, $new_cert_home_ecc); + } + } + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M4_1_0.php b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M4_1_0.php new file mode 100644 index 0000000000..a28ae71f44 --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M4_1_0.php @@ -0,0 +1,266 @@ +getNodeByReference('validations.validation')->iterateItems() as $validation) { + $dns_service = (string)$validation->dns_service; + $dns_lexicon_provider = (string)$validation->dns_lexicon_provider; + + if (!empty($dns_lexicon_provider) && ($dns_service === 'dns_lexicon')) { + // Migrate old lexicon values to DNS API values. + switch ($dns_lexicon_provider) { + case 'aliyun': + $validation->dns_service = 'dns_ali'; + $validation->dns_ali_key = (string)$validation->dns_lexicon_user; + $validation->dns_ali_secret = (string)$validation->dns_lexicon_token; + break; + case 'aurora': + $validation->dns_service = 'dns_aurora'; + $validation->dns_aurora_key = (string)$validation->dns_lexicon_user; + $validation->dns_aurora_secret = (string)$validation->dns_lexicon_token; + break; + case 'cloudflare': + $validation->dns_service = 'dns_cf'; + $validation->dns_cf_key = (string)$validation->dns_lexicon_user; + $validation->dns_cf_token = (string)$validation->dns_lexicon_token; + break; + case 'cloudns': + $validation->dns_service = 'dns_cloudns'; + $validation->dns_cloudns_auth_id = (string)$validation->dns_lexicon_user; + $validation->dns_cloudns_auth_password = (string)$validation->dns_lexicon_token; + break; + case 'cloudxns': + $validation->dns_service = 'dns_cx'; + $validation->dns_cx_key = (string)$validation->dns_lexicon_user; + $validation->dns_cx_secret = (string)$validation->dns_lexicon_token; + break; + case 'conoha': + $validation->dns_service = 'dns_conoha'; + $validation->dns_conoha_user = (string)$validation->dns_lexicon_user; + $validation->dns_conoha_password = (string)$validation->dns_lexicon_token; + break; + case 'constellix': + $validation->dns_service = 'dns_constellix'; + $validation->dns_constellix_key = (string)$validation->dns_lexicon_user; + $validation->dns_constellix_secret = (string)$validation->dns_lexicon_token; + break; + case 'digitalocean': + $validation->dns_service = 'dns_dgon'; + $validation->dns_dgon_key = (string)$validation->dns_lexicon_token; + break; + case 'directadmin': + $validation->dns_service = 'dns_da'; + $validation->dns_da_key = (string)$validation->dns_lexicon_token; + break; + case 'dnsimple': + $validation->dns_service = 'dns_dnsimple'; + $validation->dns_dnsimple_token = (string)$validation->dns_lexicon_token; + break; + case 'dnsmadeeasy': + $validation->dns_service = 'dns_me'; + $validation->dns_me_key = (string)$validation->dns_lexicon_user; + $validation->dns_me_secret = (string)$validation->dns_lexicon_token; + break; + case 'dnspod': + $validation->dns_service = 'dns_dp'; + $validation->dns_dp_id = (string)$validation->dns_lexicon_user; + $validation->dns_dp_key = (string)$validation->dns_lexicon_token; + break; + case 'dreamhost': + $validation->dns_service = 'dns_dreamhost'; + // FIXME: parameter prefix should match $dns_service + $validation->dns_dh_key = (string)$validation->dns_lexicon_token; + break; + case 'easydns': + $validation->dns_service = 'dns_easydns'; + $validation->dns_easydns_apikey = (string)$validation->dns_lexicon_user; + $validation->dns_easydns_apitoken = (string)$validation->dns_lexicon_token; + break; + case 'exoscale': + $validation->dns_service = 'dns_exoscale'; + $validation->dns_exoscale_key = (string)$validation->dns_lexicon_user; + $validation->dns_exoscale_secret = (string)$validation->dns_lexicon_token; + break; + case 'gandi': + $validation->dns_service = 'dns_gandi_livedns'; + $validation->dns_gandi_livedns_key = (string)$validation->dns_lexicon_user; + $validation->dns_gandi_livedns_token = (string)$validation->dns_lexicon_token; + break; + case 'godaddy': + $validation->dns_service = 'dns_gd'; + $validation->dns_gd_key = (string)$validation->dns_lexicon_user; + $validation->dns_gd_secret = (string)$validation->dns_lexicon_token; + break; + case 'googleclouddns': + $validation->dns_service = 'dns_gcloud'; + $validation->dns_gcloud_key = (string)$validation->dns_lexicon_token; + break; + case 'gratisdns': + $validation->dns_service = 'dns_gdnsdk'; + $validation->dns_gdnsdk_user = (string)$validation->dns_lexicon_user; + $validation->dns_gdnsdk_password = (string)$validation->dns_lexicon_token; + break; + case 'henet': + $validation->dns_service = 'dns_he'; + $validation->dns_he_user = (string)$validation->dns_lexicon_user; + $validation->dns_he_password = (string)$validation->dns_lexicon_token; + break; + case 'hetzner': + $validation->dns_service = 'dns_hetzner'; + $validation->dns_hetzner_token = (string)$validation->dns_lexicon_token; + break; + case 'infoblox': + $validation->dns_service = 'dns_infoblox'; + $validation->dns_infoblox_credentials = (string)$validation->dns_lexicon_token; + break; + case 'internetbs': + $validation->dns_service = 'dns_internetbs'; + $validation->dns_internetbs_key = (string)$validation->dns_lexicon_user; + $validation->dns_internetbs_password = (string)$validation->dns_lexicon_token; + break; + case 'inwx': + $validation->dns_service = 'dns_inwx'; + $validation->dns_inwx_user = (string)$validation->dns_lexicon_user; + $validation->dns_inwx_password = (string)$validation->dns_lexicon_token; + break; + case 'linode': + $validation->dns_service = 'dns_linode'; + $validation->dns_linode_key = (string)$validation->dns_lexicon_token; + break; + case 'linode4': + $validation->dns_service = 'dns_linode_v4'; + $validation->dns_linode_v4_key = (string)$validation->dns_lexicon_token; + break; + case 'luadns': + $validation->dns_service = 'dns_lua'; + $validation->dns_lua_email = (string)$validation->dns_lexicon_user; + $validation->dns_lua_key = (string)$validation->dns_lexicon_token; + break; + case 'namecheap': + $validation->dns_service = 'dns_namecheap'; + $validation->dns_namecheap_user = (string)$validation->dns_lexicon_user; + $validation->dns_namecheap_api = (string)$validation->dns_lexicon_token; + break; + case 'namesilo': + $validation->dns_service = 'dns_namesilo'; + $validation->dns_namesilo_key = (string)$validation->dns_lexicon_token; + break; + case 'netcup': + $validation->dns_service = 'dns_netcup'; + $validation->dns_netcup_key = (string)$validation->dns_lexicon_user; + $validation->dns_netcup_pw = (string)$validation->dns_lexicon_token; + break; + case 'nfsn': + $validation->dns_service = 'dns_njalla'; + $validation->dns_njalla_token = (string)$validation->dns_lexicon_token; + break; + case 'nsone': + $validation->dns_service = 'dns_nsone'; + $validation->dns_nsone_key = (string)$validation->dns_lexicon_token; + break; + case 'online': + $validation->dns_service = 'dns_online'; + $validation->dns_online_key = (string)$validation->dns_lexicon_token; + break; + case 'ovh': + $validation->dns_service = 'dns_ovh'; + $validation->dns_ovh_app_key = (string)$validation->dns_lexicon_user; + $validation->dns_ovh_app_secret = (string)$validation->dns_lexicon_token; + break; + case 'plesk': + $validation->dns_service = 'dns_pleskxml'; + $validation->dns_pleskxml_user = (string)$validation->dns_lexicon_user; + $validation->dns_pleskxml_pass = (string)$validation->dns_lexicon_token; + break; + case 'pointhq': + $validation->dns_service = 'dns_pointhq'; + $validation->dns_pointhq_email = (string)$validation->dns_lexicon_user; + $validation->dns_pointhq_key = (string)$validation->dns_lexicon_token; + break; + case 'powerdns': + $validation->dns_service = 'dns_pdns'; + $validation->dns_pdns_serverid = (string)$validation->dns_lexicon_user; + $validation->dns_pdns_token = (string)$validation->dns_lexicon_token; + break; + case 'rackspace': + $validation->dns_service = 'dns_rackspace'; + $validation->dns_rackspace_user = (string)$validation->dns_lexicon_user; + $validation->dns_rackspace_key = (string)$validation->dns_lexicon_token; + break; + case 'rage4': + $validation->dns_service = 'dns_rage4'; + $validation->dns_rage4_user = (string)$validation->dns_lexicon_user; + $validation->dns_rage4_token = (string)$validation->dns_lexicon_token; + break; + case 'route53': + $validation->dns_service = 'dns_aws'; + $validation->dns_aws_id = (string)$validation->dns_lexicon_user; + $validation->dns_aws_secret = (string)$validation->dns_lexicon_token; + break; + case 'transip': + $validation->dns_service = 'dns_transip'; + $validation->dns_transip_username = (string)$validation->dns_lexicon_user; + $validation->dns_transip_key = (string)$validation->dns_lexicon_token; + break; + case 'vultr': + $validation->dns_service = 'dns_vultr'; + $validation->dns_vultr_key = (string)$validation->dns_lexicon_token; + break; + case 'yandex': + $validation->dns_service = 'dns_yandex'; + $validation->dns_yandex_token = (string)$validation->dns_lexicon_token; + break; + case 'zeit': + // URL points to zilore, so we'll use this DNS API as a replacement. + $validation->dns_service = 'dns_zilore'; + $validation->dns_zilore_key = (string)$validation->dns_lexicon_token; + break; + case 'zilore': + $validation->dns_service = 'dns_zilore'; + $validation->dns_zilore_key = (string)$validation->dns_lexicon_token; + break; + case 'zonomi': + $validation->dns_service = 'dns_zonomi'; + // FIXME: parameter prefix should match $dns_service + $validation->dns_zm_key = (string)$validation->dns_lexicon_token; + break; + } + } + } + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M4_2_0.php b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M4_2_0.php new file mode 100644 index 0000000000..f896a8e65b --- /dev/null +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M4_2_0.php @@ -0,0 +1,47 @@ +getNodeByReference('validations.validation')->iterateItems() as $validation) { + $dns_service = (string)$validation->dns_service; + if ($dns_service === 'dns_inwx') { + // Migrate data from misspelled item to new one + $validation->dns_inwx_password = (string)$validation->dns_inws_password; + } + } + } +} diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt index d9e417ebde..3836955d8f 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/accounts.volt @@ -49,7 +49,6 @@ POSSIBILITY OF SUCH DAMAGE. ajax: true, selection: true, multiSelect: true, - rowCount:[10,25,50,100,500,1000], url: '/api/acmeclient/accounts/search', formatters: { "commands": function (column, row) { @@ -115,7 +114,9 @@ POSSIBILITY OF SUCH DAMAGE. /** * copy actions for selected items from opnsense_bootgrid_plugin.js */ - var grid_accounts = $("#grid-accounts").bootgrid(gridopt).on("loaded.rs.jquery.bootgrid", function (e) + const grid_accounts = $("#grid-accounts").UIBootgrid($.extend(gridParams, { options: gridopt })); + + $("#grid-accounts").on("loaded.rs.jquery.bootgrid", function (e) { // toggle all rendered tooltips (once for all) $('.bootgrid-tooltip').tooltip(); @@ -359,6 +360,7 @@ POSSIBILITY OF SUCH DAMAGE. + diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt index 5cbf1e502d..2b57076f1b 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt @@ -43,7 +43,6 @@ POSSIBILITY OF SUCH DAMAGE. del:'/api/acmeclient/actions/del/', toggle:'/api/acmeclient/actions/toggle/', options: { - rowCount:[10,25,50,100,500,1000] } } ); @@ -92,17 +91,21 @@ POSSIBILITY OF SUCH DAMAGE. .hide(); } - // SFTP - Identity show button - (function ($identityType) { + // SFTP/SSH - Identity show button + [ + {selector: '#action\\.sftp_identity_type', group: "configd_upload_sftp", action: "sftpGetIdentity"}, + {selector: '#action\\.remote_ssh_identity_type', group: "configd_remote_ssh", action: "sshGetIdentity"}, + ].forEach(function(config) { + var $identityType = $(config.selector); var identityDiv = makeStatusDiv($identityType); - makeButton("{{ lang._('Show Identity') }}", "upload_sftp", "btn-info") + makeButton("{{ lang._('Show Identity') }}", config.group, "btn-info") .click(function () { identityDiv.hide(); var button = $(this); button.prop('disabled', true).find(".fa-spinner").show(); - ajaxCall("/api/acmeclient/actions/sftpGetIdentity", getFormData("DialogAction").action, function (data, status) { + ajaxCall("/api/acmeclient/actions/" + config.action, getFormData("DialogAction").action, function (data, status) { button.prop('disabled', false).find(".fa-spinner").hide(); if (status === "success" && data.status === "ok") { @@ -117,10 +120,15 @@ POSSIBILITY OF SUCH DAMAGE. $identityType.change(function() { identityDiv.hide(); }); - })($('#action\\.sftp_identity_type')); + }); + + // SFTP/SSH - Connection test button + [ + {selector: '#action\\.sftp_user', group: "configd_upload_sftp", action: "sftpTestConnection", success: "{{ lang._('Connection and upload test succeeded.') }}"}, + {selector: '#action\\.remote_ssh_user', group: "configd_remote_ssh", action: "sshTestConnection", success: "{{ lang._('Connection test succeeded.') }}"}, + ].forEach(function(config) { + var $user = $(config.selector); - // SFTP - Connection test button - (function ($user) { var statusDiv = makeStatusDiv($user, 'alert-success').html( '
    ' + '
    ' @@ -145,13 +153,13 @@ POSSIBILITY OF SUCH DAMAGE. {msg: "{{ lang._('Test failed, see details.') }}"}, ]; - makeButton("{{ lang._('Test Connection') }}", "upload_sftp") + makeButton("{{ lang._('Test Connection') }}", config.group) .click(function () { statusDiv.hide(); var button = $(this); button.prop('disabled', true).find(".fa-spinner").show(); - ajaxCall("/api/acmeclient/actions/sftpTestConnection", getFormData("DialogAction").action, function (data, status) { + ajaxCall("/api/acmeclient/actions/" + config.action, getFormData("DialogAction").action, function (data, status) { button.prop('disabled', false).find(".fa-spinner").hide(); var message = "", @@ -161,7 +169,7 @@ POSSIBILITY OF SUCH DAMAGE. if (status === "success") { if (data.success === true) { statusClass = "alert-success"; - message = "{{ lang._('Connection and upload test succeeded.') }}" + message = config.success } else { detail = JSON.stringify(data, null, ' ').replace(/\\"/g, "'"); @@ -188,7 +196,7 @@ POSSIBILITY OF SUCH DAMAGE. statusDiv.removeClass("alert-success alert-warning").addClass(statusClass).show(); }); }); - })($('#action\\.sftp_user')); + }); // Eagerly hiding method tables to avoid contents popping up when opening the dialog for the first time. $(".method_table").hide(); diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt index c0483969bf..e824edd850 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/certificates.volt @@ -56,7 +56,6 @@ POSSIBILITY OF SUCH DAMAGE. ajax: true, selection: true, multiSelect: true, - rowCount:[10,25,50,100,500,1000], url: '/api/acmeclient/certificates/search', formatters: { "commands": function (column, row) { @@ -140,7 +139,9 @@ POSSIBILITY OF SUCH DAMAGE. /** * copy actions for selected items from opnsense_bootgrid_plugin.js */ - var grid_certificates = $("#grid-certificates").bootgrid(gridopt).on("loaded.rs.jquery.bootgrid", function (e) + const grid_certificates = $("#grid-certificates").UIBootgrid($.extend(gridParams, { options: gridopt })); + + $("#grid_certificates").on("loaded.rs.jquery.bootgrid", function (e) { // toggle all rendered tooltips (once for all) $('.bootgrid-tooltip').tooltip(); @@ -330,7 +331,7 @@ POSSIBILITY OF SUCH DAMAGE. '{{ lang._('Forcefully issue or renew the selected certificate?') }}', '{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() { // Handle HAProxy integration (no-op if not applicable) - ajaxCall(url="/api/acmeclient/settings/fetchHAProxyIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_ha_proxy_integration", sendData={}, callback=function(data,status) { ajaxCall(url=gridParams['sign'] + uuid,sendData={},callback=function(data,status){ // reload grid after sign $("#"+gridId).bootgrid("reload"); @@ -440,7 +441,7 @@ POSSIBILITY OF SUCH DAMAGE. $("#signallcertsAct").click(function(){ //$("#signallcertsAct_progress").addClass("fa fa-spinner fa-pulse"); // Handle HAProxy integration (no-op if not applicable) - ajaxCall(url="/api/acmeclient/settings/fetchHAProxyIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_ha_proxy_integration", sendData={}, callback=function(data,status) { ajaxCall(url="/api/acmeclient/service/signallcerts", sendData={}, callback=function(data,status) { // when done, disable progress animation. //$("#signallcertsAct_progress").removeClass("fa fa-spinner fa-pulse"); @@ -466,8 +467,8 @@ POSSIBILITY OF SUCH DAMAGE.

    {{ lang._('The following principles apply when managing certificates with this plugin:') }}

    • {{ lang._('Certificates must be %svalidated%s by the CA before they can be used. This process runs in the background and may take several minutes to complete. The progress can be monitored by using the %slog files%s.') | format('', '', '', '') }}
    • -
    • {{ lang._('Certificates are stored in the %sOPNsense certificate storage%s. When a CA has completed the validation of a certificate request, the resulting certificate is then automatically imported into the OPNsense certificate storage. The same applies when renewing certificates, the existing entry in the OPNsense certificate storage will automatically be updated.') | format('', '') }}
    • -
    • {{ lang._('When removing a certificate from the plugin, the certificate in the %sOPNsense certificate storage%s is %sNOT removed%s, because it may still be used by a core application or another plugin. Obsolete certificates should be manually removed from the OPNsense certificate storage. Note that when creating a new certificate with the same name, a new certificated will be imported into the OPNsense certificate storage (instead of updating the existing entry).') | format('', '', '', '') }}
    • +
    • {{ lang._('Certificates are stored in the %sOPNsense certificate storage%s. When a CA has completed the validation of a certificate request, the resulting certificate is then automatically imported into the OPNsense certificate storage. The same applies when renewing certificates, the existing entry in the OPNsense certificate storage will automatically be updated.') | format('', '') }}
    • +
    • {{ lang._('When removing a certificate from the plugin, the certificate in the %sOPNsense certificate storage%s is %sNOT removed%s, because it may still be used by a core application or another plugin. Obsolete certificates should be manually removed from the OPNsense certificate storage. Note that when creating a new certificate with the same name, a new certificated will be imported into the OPNsense certificate storage (instead of updating the existing entry).') | format('', '', '', '') }}

    {{ lang._('When experiencing issues, try setting the log level to "debug" on the %ssettings%s page.') | format('', '') }}

    diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/logs.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/logs.volt index 78f1d5e6f1..dbfb34efb8 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/logs.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/logs.volt @@ -1,4 +1,5 @@ {# + # Copyright (c) 2024 Frank Wall # Copyright (c) 2019 Deciso B.V. # All rights reserved. # @@ -34,10 +35,9 @@ sorting:false, rowSelect: false, selection: false, - rowCount:[20,50,100,200,500,1000,-1], requestHandler: function(request){ // Show only log entries that match 'AcmeClient' - request['searchPhrase'] = 'AcmeClient'; + request['searchPhrase'] = 'acmeclient'; return request; }, }, @@ -50,7 +50,6 @@ sorting:false, rowSelect: false, selection: false, - rowCount:[20,50,100,200,500,1000,-1], }, search:'/api/diagnostics/log/core/acmeclient' }); diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt index fb111608b1..38e05c5e84 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/settings.volt @@ -87,11 +87,11 @@ POSSIBILITY OF SUCH DAMAGE. }); // Handle cron integration - ajaxCall(url="/api/acmeclient/settings/fetchCronIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_cron_integration", sendData={}, callback=function(data,status) { }); // Handle HAProxy integration - ajaxCall(url="/api/acmeclient/settings/fetchHAProxyIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_ha_proxy_integration", sendData={}, callback=function(data,status) { }); // when done, disable progress animation @@ -128,9 +128,9 @@ POSSIBILITY OF SUCH DAMAGE. } // Handle cron integration - ajaxCall(url="/api/acmeclient/settings/fetchCronIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_cron_integration", sendData={}, callback=function(data,status) { // Handle HAProxy integration - ajaxCall(url="/api/acmeclient/settings/fetchHAProxyIntegration", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/fetch_ha_proxy_integration", sendData={}, callback=function(data,status) { // when done, disable progress animation $('[id*="reconfigureAct_progress"]').each(function(){ $(this).removeClass("fa fa-spinner fa-pulse"); @@ -254,6 +254,7 @@ POSSIBILITY OF SUCH DAMAGE.
    • {{ lang._("%sLet's Encrypt:%s A free, automated, and open certificate authority, run for the public's benefit. It is a service provided by the Internet Security Research Group (ISRG). Read more about the ACME protocol in %stheir documentation%s.") | format('', '', '', '') }}
    • {{ lang._('%sBuypass:%s A commercial, european certificate authority, based in Norway. Check out %stheir documentation%s for details about rate-limits and the usage policy.') | format('', '', '', '') }}
    • +
    • {{ lang._('%sGoogle:%s A commercial certificate authority. More information is available from %stheir documentation%s.') | format('', '', '', '') }}
    • {{ lang._('%sSSL.com:%s A commercial, globally trusted certificate authority. They provide an %sextensive guide%s for using their paid services with the ACME protocol.') | format('', '', '', '') }}
    • {{ lang._("%sZeroSSL:%s A commercial, european certificate authority, based in Austria. They provide a feature overview on %stheir website%s for users of Let's Encrypt.") | format('', '', '', '') }}
    diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt index bd1e4c42fa..f5643178b0 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt @@ -43,7 +43,6 @@ POSSIBILITY OF SUCH DAMAGE. del:'/api/acmeclient/validations/del/', toggle:'/api/acmeclient/validations/toggle/', options: { - rowCount:[10,25,50,100,500,1000] } } ); @@ -57,13 +56,13 @@ POSSIBILITY OF SUCH DAMAGE. $("."+service_id).show(); } // Show a warning if the Google Cloud SDK plugin is missing. - ajaxCall(url="/api/acmeclient/settings/getGcloudPluginStatus", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/get_gcloud_plugin_status", sendData={}, callback=function(data,status) { if (data['result'] != 0) { $(".gcloud_plugin_warning").hide(); } }); // Show a warning if the BIND plugin is missing. - ajaxCall(url="/api/acmeclient/settings/getBindPluginStatus", sendData={}, callback=function(data,status) { + ajaxCall(url="/api/acmeclient/settings/get_bind_plugin_status", sendData={}, callback=function(data,status) { if (data['result'] != 0) { $(".bind_plugin_warning").hide(); } @@ -77,11 +76,20 @@ POSSIBILITY OF SUCH DAMAGE. } else { } }); + $("#validation\\.tlsalpn_service").change(function(){ + var service_id = 'table_tlsalpn_' + $(this).val(); + $(".table_tlsalpn").hide(); + if ($("#validation\\.method").val() == 'tlsalpn01') { + $("."+service_id).show(); + } else { + } + }); $("#validation\\.method").change(function(){ $(".method_table").hide(); $(".method_table_"+$(this).val()).show(); $("#validation\\.dns_service").change(); $("#validation\\.http_service").change(); + $("#validation\\.tlsalpn_service").change(); }); $("#validation\\.method").change(); @@ -103,7 +111,8 @@ POSSIBILITY OF SUCH DAMAGE.

    {{ lang._('As defined by the ACME standard, Certificate Authorities (CAs) must validate that you control a domain name. This is done by using "challenges". The following challenge types are supported:') }}

    • {{ lang._('%sDNS-01:%s This is the most reliable challenge type and thus highly recommended when using this plugin. It requires that you control the DNS for your domain name and that your DNS provider is supported both %sby acme.sh%s and this plugin.') | format('', '', '', '') }}
    • -
    • {{ lang._("%sHTTP-01:%s This challenge type usually requires manual configuration and is not recommended. The DNS name used in the certificate must point to the OPNsense host where the ACME Client plugin is running on. The integrated web service will try to guess the correct settings for your setup, but this may not always work out-of-the-box. Furthermore this challenge type cannot be used to create %swildcard certificates with Let's Encrypt%s.") | format('', '', '', '') }}
    • +
    • {{ lang._("%sHTTP-01:%s This challenge type usually requires manual configuration and is not recommended. The DNS name used in the certificate must point to the OPNsense host where the ACME Client plugin is running on. The integrated web service will try to guess the correct settings for your setup, but this may not always work out-of-the-box. Furthermore this challenge type cannot be used to validate %swildcard certificates with Let's Encrypt%s.") | format('', '', '', '') }}
    • +
    • {{ lang._("%sTLS-ALPN-01:%s This works similar to the HTTP-01 challenge type and has the same requirements. It works if port 80 is unavailable. Other challenge types should be preferred. This challenge type cannot be used to validate %swildcard certificates with Let's Encrypt%s.") | format('', '', '', '') }}

    {{ lang._('When experiencing issues with a challenge type, try setting the log level to "debug". Please provide full logs when %sreporting issues%s for a challenge type. You should also consider to ask the Certificate Authority for support, if you choose to use a commercial CA.') | format('', '') }}

    diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/lecert.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/lecert.php index 73bb87ee28..0079a3a8f7 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/lecert.php +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/lecert.php @@ -2,7 +2,7 @@ [ + "description" => "runs the a command on the specified target host", + "options" => [ + "host::", "port::", "host-key::", "user::", "identity-type::", "run::"], + "implementation" => "commandRunRemote", + "default" => true, + ], + + "test-connection" => [ + "description" => "connects to the host and returns results as JSON", + "options" => ["host:", "port::", "host-key::", "user:", "identity-type::"], + "implementation" => "commandTestConnection", + ], + + "show-identity" => [ + "description" => "prints the ssh client identity (publickey)", + "options" => ["identity-type::", "source-ip::", "host::", "unrestricted"], + "implementation" => "commandShowIdentity", + ], +]; + +const EXAMPLES = <<getIdentity($identity_type)) && is_readable($id_file)) { + if ( + !isset($options["unrestricted"]) + && ($restrictions = SSHKeys::getIdentityRestrictions($host, $source_ip, "")) + ) { + echo "$restrictions "; + } + + echo file_get_contents($id_file); + return EXITCODE_SUCCESS; + } else { + LeUtils::log_error("SSH failed getting identity. See log output for details."); + } + return EXITCODE_ERROR; +} + +function commandTestConnection(array &$options): int +{ + $result = ["actions" => ["connecting"], "success" => false]; + + $options["run"] = CONNECTION_TEST_COMMAND; + $lines = runRemoteCommand($options, $error); + + if (!$error) { + $result["actions"][] = "connected"; + if (($result["success"] = in_array(CONNECTION_TEST_RESULT, $lines))) { + $result["actions"][] = "echo-tested"; + } + } else { + $result = array_merge($result, ($error ?: [])); + } + + echo json_encode($result, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL; + + return $result["success"] ? EXITCODE_SUCCESS : EXITCODE_ERROR; +} + +function commandRunRemote(array &$options): int +{ + if (empty($options["run"])) { + LeUtils::log_error("SSH: Command is empty, nothing to do."); + return EXITCODE_ERROR; + } + + $lines = runRemoteCommand($options, $error); + if (!$error) { + $host = $options["host"] . (($port = ($options["port"] ?? false)) ? ":$port" : ""); + LeUtils::log_debug("SSH [$host]> {$options["run"]}:" . PHP_EOL . join(PHP_EOL, $lines)); + return EXITCODE_SUCCESS; + } + + return EXITCODE_ERROR; +} + +function runRemoteCommand(array $options, &$error): ?array +{ + static $expected_errors = [ + ["host_not_resolved", /* -> */ '/.*not resolve.*/i'], + ["host_not_trusted", /* -> */ '/.*IDENTIFICATION HAS CHANGED.*/i'], + ["connection_refused", /* -> */ '/.*connection refused.*/i'], + ["connection_closed", /* -> */ '/.*connection closed.*/i'], + ["network_timeout", /* -> */ '/.*timed out.*/i'], + ["network_unreachable", /* -> */ '/.*network.+unreachable.*/i'], + ["permission_denied", /* -> */ '/.*permission denied.*/i'], + ["failure", /* -> */ '/.*(error|failure|you must supply).*/i'], + ]; + + $ssh_keys = new SSHKeys(configPath()); + + $identity_type = trim(($options["identity-type"] ?? "")); + $host = trim(($options["host"] ?? "")); + $host_key = ($options["host-key"] ?? ""); + $port = !empty($options["port"]) ? $options["port"] : SSHKeys::DEFAULT_PORT; + $username = $options["user"] ?? false; + $command = $options["run"] ?? ""; + + list($ok, $cmd) = buildSSHArguments($ssh_keys, $host, $username, $identity_type, $host_key, $port); + if ($ok) { + if (empty($command)) { + $error = ["no_command" => true]; + } else { + $cmd[] = $command; + } + } else { + $error = $cmd; + $error["connect_failed"] = true; + return null; + } + + $result = []; + $exit_code = null; + $expected_error = null; + + if ($process = Process::open($cmd)) { + $process->closeInput(); + + $lines = 0; + $start = time(); + $mustClose = fn($lines) => (time() - $start) > CONNECTION_EXECUTE_TIMEOUT || $lines > 10000; + + while ($process->isRunning() && !$mustClose($lines)) { + for (; ($line = $process->get()) !== false && !$mustClose($lines); $lines++) { + if (!$expected_error) { + foreach ($expected_errors as $ee) { + if (preg_match($ee[1], $line)) { + if ($ee[0] !== "connection_closed") { + $expected_error = [$ee[0] => true, "error" => trim($line)]; + } + break; + } + } + } + $result[] = $line; + } + } + $exit_code = $process->close(); + $ok = $exit_code === 0; + } else { + $ok = false; + } + + if (!$ok) { + $cl = join(" ", array_map(fn($v) => escapeshellarg($v), $cmd)); + $error = array_merge(($expected_error ?? []), [ + "result" => $result, + "exit_code" => $exit_code + ]); + $error["connect_failed"] = $exit_code == 255; + LeUtils::log_error("SSH failed with '$exit_code': $cl", $error); + } + + return $result; +} + +function buildSSHArguments(SSHKeys $ssh_keys, $host, $username, $identity_type = "", $host_key = "", $port = SSHKeys::DEFAULT_PORT): array +{ + if (empty(trim($host)) || empty(trim($username))) { + LeUtils::log_error("Failed connecting to '$host'. Hostname or username is missing."); + return [false, ["invalid_parameters" => true]]; + } + + if (empty($identity_type)) { + $identity_type = SSHKeys::DEFAULT_IDENTITY_TYPE; + } + + $trust = $ssh_keys->trustHost($host, $host_key, $port); + if ($trust["ok"] !== true) { + LeUtils::log_error("Failed establishing trust in '$host'; Cause: {$trust["error"]}"); + unset($trust["ok"]); + return [false, array_merge($trust, ["host_not_trusted" => true])]; + } else { + $host = $trust["host"]; + } + + // Building ssh command. + $cmd = [ + "ssh", + "-p", $port, + "-oUser=$username", + "-oUserKnownHostsFile={$ssh_keys->knownHostsFile()}", + ]; + + // Handle client side identity + $identity = $ssh_keys->getIdentity($identity_type, true); + if (is_file($identity) && is_readable($identity)) { + array_push( + $cmd, + "-i", + $identity, + "-oPreferredAuthentications=publickey" + ); + } else { + LeUtils::log_error("Failed adding SSH client identity ($identity). Connect will likely fail."); + } + + // Adding the host + $cmd[] = "$host"; + + return [true, $cmd]; +} + +function help() +{ + Utils::printCLIHelp(ABOUT, EXAMPLES, COMMANDS); +} + +function getOptionsById($automation_id) +{ + LeUtils::log_debug("Reading options from automation: $automation_id"); + + if (is_object($action = Utils::getAutomationActionById($automation_id))) { + if ($action->enabled && "configd_remote_ssh" === (string)$action->type) { + return [ + "host" => trim((string)$action->remote_ssh_host), + "host-key" => trim((string)$action->remote_ssh_host_key), + "port" => trim((string)$action->remote_ssh_port), + "identity-type" => trim((string)$action->remote_ssh_identity_type), + "user" => trim((string)$action->remote_ssh_user), + "run" => trim((string)$action->remote_ssh_command), + ]; + } else { + LeUtils::log_error("Ignoring disabled or invalid automation '$automation_id'"); + } + } else { + LeUtils::log_error("No upload automation found with uuid = '$automation_id'"); + } + + return false; +} + +function configPath(): string +{ + if (($path = Utils::configPath())) { + return $path . DIRECTORY_SEPARATOR . "sftp-config"; // shared with sftp to have the same identities + } + die("Failed detecting config path"); +} + +function requireThat($expression, $message) +{ + try { + Utils::requireThat($message, $message); + } catch (\AssertionError $e) { + exit(EXITCODE_ERROR); + } + return $expression; +} + +// Running the main script +Utils::runCLIMain( + "help", + "getOptionsById", + COMMANDS, + EXITCODE_SUCCESS, + EXITCODE_ERROR_UNKNOWN_COMMAND +); diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh index 43140eea72..567364b401 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh @@ -1,23 +1,36 @@ #!/bin/sh ACME_BASE="/var/etc/acme-client" -ACME_DIRS="/var/etc/acme-client/certs /var/etc/acme-client/keys /var/etc/acme-client/configs /var/etc/acme-client/challenges /var/etc/acme-client/home" +ACME_DIRS="/var/etc/acme-client/certs /var/etc/acme-client/keys /var/etc/acme-client/configs /var/etc/acme-client/challenges /var/etc/acme-client/home /var/etc/acme-client/cert-home" +ACME_LINKS="deploy dnsapi notify" +ACME_LINK_TARGET="/usr/local/share/examples/acme.sh" -# Generating dirs if missing and setting owner and mode (recursively) +# Create required directories and set owner/mode recursively. for directory in ${ACME_DIRS}; do mkdir -p ${directory} chown -R root:wheel ${directory} chmod -R 750 ${directory} done -# Remove symlink in order to use upstream version -# see https://github.com/opnsense/plugins/pull/1888 -if [ -L /var/etc/acme-client/home/dns_opnsense.sh ]; then - unlink /var/etc/acme-client/home/dns_opnsense.sh -fi - -# Setting owner and mode for base and immediate children (non recursive) +# Set owner/mode for base and immediate children (non recursive). chown root:wheel ${ACME_BASE} ${ACME_BASE}/* chmod 750 ${ACME_BASE} ${ACME_BASE}/* +# Create symlinks for acme.sh script directories. +# This should guard against manual misconfiguration. +for link in ${ACME_LINKS}; do + # First remove any existing file/directory. + if [ -L "${ACME_BASE}/home/${link}" ]; then + # Already a symlink, skip this enty. + continue + elif [ -f "${ACME_BASE}/home/${link}" ]; then + rm ${ACME_BASE}/home/${link} + elif [ -d "${ACME_BASE}/home/${link}" ]; then + rmdir ${ACME_BASE}/home/${link} + elif [ ! -e "${ACME_BASE}/home/${link}" ]; then + # Create the symlink. + ln -s ${ACME_LINK_TARGET}/${link} ${ACME_BASE}/home/${link} + fi +done + exit 0 diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php deleted file mode 100755 index 2819626a45..0000000000 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/local/bin/php -object(); - if (isset($configObj->OPNsense->AcmeClient->certificates) && $configObj->OPNsense->AcmeClient->certificates->count() > 0) { - foreach ($configObj->OPNsense->AcmeClient->certificates->children() as $certObj) { - $cert_id = (string)$certObj->id; - $cert_name = (string)$certObj->name; - if ($cert_id == $acme_cert_id) { - if ($certObj->enabled == 0) { - log_error("AcmeClient: certificate ${cert_name} is disabled, ignoring upload request"); - return 'None'; - } - if (isset($certObj->certRefId)) { - $data = array(); - $data['name'] = $cert_name; - $data['refid'] = (string)$certObj->certRefId; - return $data; - } else { - log_error("AcmeClient: certificate ${cert_name} could not be found in trust storage, ignoring upload request"); - break; - } - } - } - return 'None'; - } -} - -function export_certificate($cert_refid) -{ - $configObj = Config::getInstance()->object(); - foreach ($configObj->cert as $cert) { - if ($cert_refid == (string)$cert->refid) { - $cert_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->crt))); - $key_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->prv))); - // check if a CA is linked - if (!empty((string)$cert->caref)) { - $cert = (array)$cert; - $ca = ca_chain($cert); - $ca_content = $ca; - } - $result = array(); - $result['cert'] = $cert_content; - $result['key'] = $key_content; - $result['ca'] = $ca_content; - return $result; - } - } - log_error("AcmeClient: cert with refid ${cert_refid} not found in trust storage"); - return 'None'; -} - -function upload_certificate($cert_name, $cert_refid, $acme_cert_id, $acme_automation_id) -{ - $modelObj = new OPNsense\AcmeClient\AcmeClient(); - $configObj = Config::getInstance()->object(); - if (isset($configObj->OPNsense->AcmeClient->actions) && $configObj->OPNsense->AcmeClient->actions->count() > 0) { - foreach ($configObj->OPNsense->AcmeClient->actions->children() as $automObj) { - $autom_id = (string)$automObj->id; - if ($autom_id == $acme_automation_id) { - if ($automObj->enabled == 0) { - log_error("AcmeClient: ignoring disabled upload job for cert ${cert_name}"); - return 'None'; - } - if (isset($automObj->highwinds_account_hash) && isset($automObj->highwinds_access_token)) { - $hw_account_hash = (string)$automObj->highwinds_account_hash; - $hw_access_token = (string)$automObj->highwinds_access_token; - $cert_data = export_certificate($cert_refid); - if ($cert_data !== 'None') { - $hw_result = hw_upload_certificate($hw_account_hash, $hw_access_token, $cert_name, $cert_data); - if ($hw_result !== 'None') { - return true; - } - } - } else { - log_error("AcmeClient: upload job for cert ${cert_name} is incomplete, missing Highwinds configuration"); - return 'None'; - } - } - } - return 'None'; - } -} - -function hw_list_certificates($account_hash, $access_token) -{ - global $HIGHWINDS_API_URL; - $curl = curl_init(); - curl_setopt_array($curl, array( - CURLOPT_URL => "${HIGHWINDS_API_URL}/${account_hash}/certificates", - CURLOPT_CUSTOMREQUEST => 'GET', - CURLOPT_RETURNTRANSFER => true, - CURLOPT_MAXREDIRS => 1, - CURLOPT_TIMEOUT => 10, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_HTTPHEADER => array( - "Authorization: Bearer ${access_token}", - "Content-Type: application/json", - "User-Agent: OPNsense Firewall", - "X-Application-Id: OPNsense Firewall" - ) - )); - $response = curl_exec($curl); - $err = curl_error($curl); - $info = curl_getinfo($curl); - curl_close($curl); - $http_code = $info['http_code']; - if ($http_code != 200 || $err) { - log_error("AcmeClient: failed to access Highwinds API, HTTP Code: ${http_code}, error ${err}"); - return 'None'; - } - return json_decode($response); -} - -function hw_get_certificate($account_hash, $access_token, $cert_name) -{ - $certificates = hw_list_certificates($account_hash, $access_token); - if ($certificates !== 'None') { - foreach ($certificates->list as $cert) { - if ($cert->commonName == $cert_name) { - return $cert; - } - } - } - return 'None'; -} - -function hw_upload_certificate($account_hash, $access_token, $cert_name, $cert_data) -{ - global $HIGHWINDS_API_URL; - // Check current status of certificate at Highwinds - $hw_cert = hw_get_certificate($account_hash, $access_token, $cert_name); - $hw_url = 'certificates'; - $hw_method = 'POST'; - if ($hw_cert == 'None') { - log_error("AcmeClient: cert for ${cert_name} not found in Highwinds API, starting upload..."); - } else { - log_error("AcmeClient: cert for ${cert_name} found in Highwinds API"); - $hw_method = 'PUT'; - - // Extract certificate details - $cert = openssl_x509_parse($cert_data['cert']); - $cert_sn = (string)$cert['serialNumber']; - $hw_cert_sn = (string)$hw_cert->certificateInformation->serialNumber; - $hw_cert_id = $hw_cert->id; - - // Compare local and remote certificates - if ($cert_sn == $hw_cert_sn) { - log_error("AcmeClient: cert ${cert_name} has same serial in Highwinds API, not updating (${cert_sn})"); - return 'None'; - } - log_error("AcmeClient: cert serial is different in Highwinds API, updating..."); - $hw_url = "${hw_url}/${hw_cert_id}"; - } - - // adjust data format for Highwinds API - $cert_post = json_encode(array('certificate' => $cert_data['cert'], 'key' => $cert_data['key'], 'caBundle' => $cert_data['ca'])); - - $curl = curl_init(); - curl_setopt_array($curl, array( - CURLOPT_URL => "${HIGHWINDS_API_URL}/${account_hash}/${hw_url}", - CURLOPT_CUSTOMREQUEST => $hw_method, - CURLOPT_POSTFIELDS => (string)$cert_post, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_MAXREDIRS => 1, - CURLOPT_TIMEOUT => 10, - CURLOPT_SAFE_UPLOAD => true, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_HTTPHEADER => array( - "Authorization: Bearer ${access_token}", - "Content-Type: application/json", - "User-Agent: OPNsense Firewall", - "X-Application-Id: OPNsense Firewall", - "Expect:" - ) - )); - $response = curl_exec($curl); - $err = curl_error($curl); - $info = curl_getinfo($curl); - curl_close($curl); - $http_code = $info['http_code']; - if ($http_code != 200 || $err) { - log_error("AcmeClient: Failed to upload cert ${cert_name} to Highwinds API, HTTP Code: ${http_code}, error ${err}"); - return 'None'; - } - return json_decode($response); -} - -// Evaluate CLI arguments -$options = getopt("a:c:"); -if (!isset($options["a"]) or !isset($options["c"])) { - print "ERROR: not enough arguments\n"; - exit(1); -} -$acme_cert_id = $options["c"]; -$acme_automation_id = $options["a"]; - -// Search certificate in configuration -$cert_data = find_certificate($acme_cert_id); -if ($cert_data == 'None') { - log_error("AcmeClient: ignoring cert ID ${acme_cert_id}"); - exit(1); -} else { - // Upload certificate (if required) - $upload_result = upload_certificate($cert_data['name'], $cert_data['refid'], $acme_cert_id, $acme_automation_id); - if ($upload_result === 'None') { - log_error("AcmeClient: cert ID ${acme_cert_id} was neither uploaded nor updated"); - } else { - log_error("AcmeClient: cert ID ${acme_cert_id} was uploaded or updated"); - } -} -exit(0); diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php index e7899a4364..b3510b9097 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php @@ -2,6 +2,7 @@ getIdentity($identity_type)) && is_readable($id_file)) { @@ -166,7 +163,7 @@ function commandShowIdentity(array &$options): int echo file_get_contents($id_file); return EXITCODE_SUCCESS; } else { - Utils::log()->error("Failed getting identity. See log output for details."); + LeUtils::log_error("SFTP failed getting identity. See log output for details."); } return EXITCODE_ERROR; } @@ -190,7 +187,7 @@ function commandTestConnection(array &$options): int $uploader = new SftpUploader($sftp); - $chgrp = $options["chgrp"] ?: false; + $chgrp = ($options["chgrp"] ?? "") ?: false; $chmod = isset($options["chmod"]) ? ($options["chmod"] ?: DEFAULT_CERT_MODE) : false; $filename = $uploader->addContent("upload-test", "", 0, $chmod, $chgrp); @@ -218,7 +215,7 @@ function commandTestConnection(array &$options): int if ($remove_file) { if ($error = $sftp->clearError()->rm($filename)->lastError(3)) { - Utils::log()->error("Failed removing upload test file '$filename'", $error); + LeUtils::log_error("SFTP failed removing upload test file '$filename'", $error); } } @@ -265,7 +262,7 @@ function commandUpload(array &$options): int } elseif (isset($options["host"])) { return uploadCertificatesToHost($options); } else { - Utils::log()->error("No work to do, neither --host nor --certificates is present."); + LeUtils::log_error("No work to do, neither --host nor --certificates is present."); return EXITCODE_ERROR_NOTHING_TO_UPLOAD; } } @@ -274,8 +271,8 @@ function uploadCertificatesToHost(array $options): int { $sftp = connectWithServer($options, $error); if ($sftp === null) { - Utils::log()->error("Aborting after connect failure."); - return $error["connect_failed"] + LeUtils::log_error("SFTP aborting after connect failure."); + return ($error["connect_failed"] ?? false) ? EXITCODE_ERROR : EXITCODE_ERROR_NO_PERMISSION; } @@ -293,7 +290,7 @@ function uploadCertificatesToHost(array $options): int $result = $uploader->upload(); if ($result != SftpUploader::UPLOAD_SUCCESS) { - Utils::log()->error("Failed on " . json_encode($uploader->current(), JSON_UNESCAPED_SLASHES)); + LeUtils::log_error("SFTP failed on " . json_encode($uploader->current(), JSON_UNESCAPED_SLASHES)); switch ($result) { case SftpUploader::UPLOAD_ERROR_NO_PERMISSION: @@ -321,10 +318,10 @@ function uploadCertificatesToHost(array $options): int function connectWithServer(array $options, &$error): ?SftpClient { - $identity_type = trim(($options["identity-type"] ?: SSHKeys::DEFAULT_IDENTITY_TYPE)); - $host = trim(($options["host"] ?: "")); - $host_key = ($options["host-key"] ?: ""); - $port = $options["port"] ?: 22; + $identity_type = trim(($options["identity-type"] ?? "")) ?: SSHKeys::DEFAULT_IDENTITY_TYPE; + $host = trim(($options["host"] ?? "")); + $host_key = ($options["host-key"] ?? ""); + $port = !empty($options["port"]) ? $options["port"] : SSHKeys::DEFAULT_PORT; $username = $options["user"]; $sftp = new SftpClient(configPath(), $identity_type); @@ -336,11 +333,11 @@ function connectWithServer(array $options, &$error): ?SftpClient } // Apply start path (if one was specified, defaults to home dir) - if (($remote_path = $options["remote-path"])) { + if (!empty($remote_path = ($options["remote-path"] ?? ""))) { if ($err = $sftp->cd($remote_path)->lastError()) { $error = $err; $error["change_home_dir_failed"] = true; - Utils::log()->error("Failed cd into '{$remote_path}'", $err); + LeUtils::log_error("SFTP failed cd into '{$remote_path}'", $err); return null; } } @@ -350,65 +347,17 @@ function connectWithServer(array $options, &$error): ?SftpClient function help() { - echo ABOUT . PHP_EOL - . "Usage: " . basename($GLOBALS["argv"][0]) . " [options] [--command=]COMMAND" . PHP_EOL - . PHP_EOL . STATIC_OPTIONS . PHP_EOL; - - foreach (COMMANDS as $name => $cmd) { - echo PHP_EOL . "COMMAND \"$name\" {$cmd["description"]}" . PHP_EOL . "Options:" . PHP_EOL; - foreach ($cmd["options"] as $option) { - $option = preg_replace(['/^([^:]+)$/', '/(.+)::$/', '/(.+):$/'], ['[$1]', '[$1=value]', '$1=value'], "--$option"); - echo " $option" . PHP_EOL; - } - } - - echo PHP_EOL . "Examples:" . PHP_EOL - . str_replace('/\r\n|\n|\r/g', PHP_EOL, EXAMPLES) - . PHP_EOL . PHP_EOL; -} - -function getCommand() -{ - $default = null; - $command = null; - $parsed_args = getopt("", ["command::"]); - foreach (COMMANDS as $name => $cmd) { - if (in_array($name, $GLOBALS["argv"]) || $parsed_args["command"] === $name) { - $command = $cmd; - } - if ($cmd["default"] === true) { - $default = $cmd; - } - } - - return $command ?: $default; -} - -function getActionById($automation_id) -{ - $config = OPNsense\Core\Config::getInstance()->object(); - $client = $config->OPNsense->AcmeClient; - - foreach ($client->actions->children() as $action) { - if ( - $automation_id === (string)$action->attributes()["uuid"] - || $automation_id === (string)$action->id - ) { - return $action; - } - } - - return null; + Utils::printCLIHelp(ABOUT, EXAMPLES, COMMANDS); } function getOptionsById($automation_id, $silent = false) { if (!$silent) { - Utils::log()->info("Reading options from automation: $automation_id"); + LeUtils::log_debug("Reading options from automation: $automation_id"); } - if (is_object($action = getActionById($automation_id))) { - if ($action->enabled && "upload_sftp" === (string)$action->type) { + if (is_object($action = Utils::getAutomationActionById($automation_id))) { + if ($action->enabled && "configd_upload_sftp" === (string)$action->type) { return [ "host" => trim((string)$action->sftp_host), "host-key" => trim((string)$action->sftp_host_key), @@ -419,6 +368,7 @@ function getOptionsById($automation_id, $silent = false) "chgrp" => trim((string)$action->sftp_chgrp), "chmod" => trim((string)$action->sftp_chmod), "chmod-key" => trim((string)$action->sftp_chmod_key), + "modtime" => trim((string)$action->sftp_modtime), "cert-name" => trim((string)$action->sftp_filename_cert), "key-name" => trim((string)$action->sftp_filename_key), "ca-name" => trim((string)$action->sftp_filename_ca), @@ -426,10 +376,10 @@ function getOptionsById($automation_id, $silent = false) "certificates" => "", // defaults to all (= empty), may be overridden via CLI ]; } elseif (!$silent) { - Utils::log()->error("Ignoring disabled or invalid automation '$automation_id'"); + LeUtils::log_error("SFTP ignoring disabled or invalid automation '$automation_id'"); } } else { - Utils::log()->error("No upload automation found with uuid = '$automation_id'"); + LeUtils::log_error("No SFTP upload automation found with uuid = '$automation_id'"); } return false; @@ -439,20 +389,21 @@ function addFilesToUpload(array $options, SftpUploader &$uploader) { $chmod = isset($options["chmod"]) ? ($options["chmod"] ?: DEFAULT_CERT_MODE) : false; $chmod_key = isset($options["chmod-key"]) ? ($options["chmod-key"] ?: DEFAULT_KEY_MODE) : false; - $chgrp = $options["chgrp"] ?: false; + $chgrp = ($options["chgrp"] ?? "") ?: false; + $modtime = ($options["modtime"] ?? "") ?: false; if (isset($options["certificates"])) { $cert_ids = preg_split('/[,;\s]+/', $options["certificates"] ?: "", 0, PREG_SPLIT_NO_EMPTY); foreach (findCertificates($cert_ids) as $cert) { if (!isset($cert["content"])) { - Utils::log()->error("Ignoring upload for cert '{$cert["name"]}', since it is not available in trust storage."); + LeUtils::log_error("Ignoring SFTP upload for cert '{$cert["name"]}', since it is not available in trust storage."); continue; } foreach ($cert["content"] as $name => $content) { if (empty($content)) { - Utils::log()->error("Content for '{$name}.pem' in cert '{$cert["name"]}' is empty, skipping it."); + LeUtils::log_error("Content for '{$name}.pem' in cert '{$cert["name"]}' is empty, skipping SFTP upload."); continue; } @@ -493,27 +444,27 @@ function ($m) use (&$cert) { ? $chmod_key : $chmod; - $uploader->addContent($content, $target_path, $cert["updated"], $mod, $chgrp); + $uploader->addContent($content, $target_path, $cert["updated"], $mod, $chgrp, $modtime); } else { - Utils::log()->error("Cannot add '{$name}.pem' since the upload path '$target_path' is invalid."); + LeUtils::log_error("Cannot add '{$name}.pem' to SFTP upload since the upload path '$target_path' is invalid."); } } } if (empty($uploader->pending())) { - Utils::log()->error("Didn't find any certificates to upload (cert-ids: " . (empty($cert_ids) ? "*all*" : join(", ", $cert_ids)) . ")."); + LeUtils::log_error("Could not find any certificates for SFTP upload (cert-ids: " . (empty($cert_ids) ? "*all*" : join(", ", $cert_ids)) . ")."); } } elseif (isset($options["files"])) { $files = preg_split('/[,;\s]+/', $options["files"] ?: "", 0, PREG_SPLIT_NO_EMPTY); foreach ($files as $file) { - $uploader->addFile($file, "", $chmod, $chgrp); + $uploader->addFile($file, "", $chmod, $chgrp, $modtime); } if (empty($uploader->pending())) { - Utils::log()->error("Didn't files to upload (files: " . join(", ", $files) . ")."); + LeUtils::log_error("Could not find files for SFTP upload (files: " . join(", ", $files) . ")."); } } else { - Utils::log()->error("Neither '--certificates' nor '--files' was specified. Have nothing to upload."); + LeUtils::log_error("Neither '--certificates' nor '--files' was specified. Have nothing to upload."); } } @@ -541,7 +492,7 @@ function findCertificates(array $certificate_ids_or_names, $load_content = true) ) { if ($cert->enabled == 0) { if (!empty($certificate_ids_or_names)) { - Utils::log()->error("Certificate '{$name}' (id: $id) is disabled, skipping it."); + LeUtils::log_error("Certificate '{$name}' (id: $id) is disabled, skipping SFTP upload."); } continue; @@ -571,20 +522,21 @@ function findCertificates(array $certificate_ids_or_names, $load_content = true) return $result; } -function exportCertificates(array $cert_refids) +function exportCertificates(array $cert_refids): array { $result = []; - $config = OPNsense\Core\Config::getInstance()->object(); - foreach ($config->cert as $cert) { + $certModel = new Cert(); + foreach ($certModel->cert->iterateItems() as $cert) { $refid = (string)$cert->refid; $item = []; if (in_array($refid, $cert_refids)) { - $item["cert"] = str_replace(["\n\n", "\r"], ["\n", ""], base64_decode($cert->crt)); - $item["key"] = str_replace(["\n\n", "\r"], ["\n", ""], base64_decode($cert->prv)); + $_tmp = CertStore::getCertificate($refid); + $item["cert"] = $_tmp["crt"]; + $item["key"] = $_tmp["prv"]; // check if a CA is linked if (!empty((string)$cert->caref)) { - $cert = (array)$cert; - $item["ca"] = ca_chain($cert); + $item['ca'] = $_tmp['ca']['crt']; + // combine files to export a fullchain.pem $item["fullchain"] = $item["cert"] . $item["ca"]; } @@ -597,72 +549,12 @@ function exportCertificates(array $cert_refids) function configPath(): string { - static $paths = [ - '/var/etc/acme-client', - __DIR__ - ]; - foreach ($paths as $path) { - if (is_dir($path)) { - return $path . DIRECTORY_SEPARATOR . 'sftp-config'; - } + if (($path = Utils::configPath())) { + return $path . DIRECTORY_SEPARATOR . "sftp-config"; } die("Failed detecting config path"); } -function main() -{ - global $argv; - $command = getCommand(); - $options = ["help", "log", "no-error"]; - - $has_automation_id = preg_match('/--automation-id=\S+/', join(" ", $argv)); - if ($has_automation_id) { - $options = array_merge($options, ["automation-id:", "certificates::"]); - } else { - $options = array_merge($options, $command["options"]); - } - - $index = 0; - if ($options = getopt("h", $options, $index)) { - if (isset($options["h"]) || isset($options["help"])) { - help(); - } else { - if (isset($options["log"])) { - Utils::log(true)->info("Logging to stdout enabled"); - } - - $options = array_filter($options, function ($value) { - return !is_string($value) - || (!empty($value = trim($value)) && $value !== "__default_value"); - }); - - if (isset($options["automation-id"])) { - $options = array_merge(getOptionsById($options["automation-id"]), $options); - } - - if (is_callable($runner = $command["implementation"])) { - $code = $runner($options); - - if ($code != EXITCODE_SUCCESS) { - Utils::log()->error("Command execution failed, exit code $code. Last input was: " . json_encode($options, JSON_UNESCAPED_SLASHES)); - } - - exit(isset($options["no-error"]) ? EXITCODE_SUCCESS : $code); - } else { - exit(EXITCODE_ERROR_UNKNOWN_COMMAND); - } - } - } else { - if (count($argv) < 2) { - help(); - } else { - $cmd = join(" ", $argv); - Utils::log()->error("Parsing of '$cmd' failed at argument '{$argv[$index]}'"); - } - exit(1); - } -} - function requireThat($expression, $message) { try { @@ -674,4 +566,10 @@ function requireThat($expression, $message) } // Running the main script -main(); +Utils::runCLIMain( + "help", + "getOptionsById", + COMMANDS, + EXITCODE_SUCCESS, + EXITCODE_ERROR_UNKNOWN_COMMAND +); diff --git a/security/acme-client/src/opnsense/scripts/systemhealth/logformats/acmeclient.py b/security/acme-client/src/opnsense/scripts/syslog/logformats/acmeclient.py similarity index 100% rename from security/acme-client/src/opnsense/scripts/systemhealth/logformats/acmeclient.py rename to security/acme-client/src/opnsense/scripts/syslog/logformats/acmeclient.py diff --git a/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf b/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf index 7af68eaa45..c58df0bef8 100644 --- a/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf +++ b/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf @@ -1,8 +1,3 @@ -[setup] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh -parameters: -type:script_output - ########################################## ## lighttpd actions ########################################## @@ -42,7 +37,7 @@ message:testing acme_http_challenge configuration ########################################## [sign-cert] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode issue --force --cert +command:/usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode issue --force --cert parameters:%s type:script message:signing or renewing a certificate @@ -66,25 +61,25 @@ type:script message:removing a certificate private key [sign-all-certs] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode issue --all +command:/usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode issue --all parameters: type:script message:signing or renewing all certificates [run-automation] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode automation --cert +command:/usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode automation --cert parameters:%s type:script message:running automations for a certificate [import] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode import --cert +command:/usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode import --cert parameters:%s type:script message:running import for a certificate [cron-auto-renew] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh; /usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode issue --all --cron +command:/usr/sbin/daemon -f /usr/local/opnsense/scripts/OPNsense/AcmeClient/lecert.php --mode issue --all --cron parameters: type:script message:cronjob running to sign or renew certificates @@ -96,12 +91,6 @@ parameters:%s type:script message:registering an account -[upload_highwinds] -command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php -parameters:-c %s -a %s -type:script -message:uploading a certificate to highwinds - [upload-sftp] command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/upload_sftp.php parameters:--certificates=%s --automation-id=%s @@ -120,6 +109,24 @@ parameters:--identity-type=%s --host=%s show-identity type:script_output message:prints the public key used to connect to sftp server +[run-remote-ssh-command] +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/run_remote_ssh.php +parameters:--automation-id=%s +type:script +message:running a command on the ssh server + +[test-remote-ssh-connection] +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/run_remote_ssh.php +parameters:--host=%s --host-key=%s --port=%s --user=%s --identity-type=%s --no-error test-connection +type:script_output +message:testing connection to ssh server + +[show-remote-ssh-identity] +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/run_remote_ssh.php +parameters:--identity-type=%s --host=%s show-identity +type:script_output +message:prints the public key used to connect to ssh server + [reset-acme-client] command:/usr/bin/find /var/etc/acme-client/home /var/etc/acme-client/configs /var/etc/acme-client/certs /var/etc/acme-client/keys /var/etc/acme-client/accounts -type f -delete parameters: diff --git a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf index 5f3cf31d29..b59553e269 100644 --- a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf +++ b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/lighttpd-acme-challenge.conf @@ -72,18 +72,16 @@ $SERVER["socket"] == "[::1]:{{OPNsense.AcmeClient.settings.challengePort}}" { } # to help the rc.scripts server.pid-file = "/var/run/lighttpd-acme-challenge.pid" -# virtual directory listings -server.dir-listing = "disable" - # enable debugging debug.log-request-header = "disable" debug.log-response-header = "disable" debug.log-request-handling = "disable" debug.log-file-not-found = "disable" -# gzip compression -compress.cache-dir = "/tmp/acmelighttpdcompress/" -compress.filetype = ("text/plain","text/css", "text/xml", "text/javascript" ) +# enable compression +deflate.cache-dir = "/tmp/acmelighttpdcompress/" +deflate.mimetypes = ("text/plain", "text/css", "text/xml", "text/javascript") +deflate.allowed-encodings = ("br", "gzip", "deflate") server.max-request-size = 4096 server.tag = "lighttpd/ACME" diff --git a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/rc.conf.d b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/rc.conf.d index 41ca3359a6..96d20f1bc0 100644 --- a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/rc.conf.d +++ b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/rc.conf.d @@ -2,8 +2,7 @@ acme_http_challenge_enable=YES acme_http_challenge_conf="/var/etc/lighttpd-acme-challenge.conf" acme_http_challenge_pidfile="/var/run/lighttpd-acme-challenge.pid" -acme_http_challenge_var_script="/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh" +acme_http_challenge_setup="/usr/local/opnsense/scripts/OPNsense/AcmeClient/setup.sh" {% else %} acme_http_challenge_enable=NO {% endif %} -acme_http_challenge_var_mfs="/var/etc/acme-client" diff --git a/security/clamav/Makefile b/security/clamav/Makefile index 668ca3fd0c..92c7a19be6 100644 --- a/security/clamav/Makefile +++ b/security/clamav/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= clamav -PLUGIN_VERSION= 1.7 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.8.1 PLUGIN_COMMENT= Antivirus engine for detecting malicious threats PLUGIN_DEPENDS= clamav PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/security/clamav/pkg-descr b/security/clamav/pkg-descr index e5066efc45..d54ada5824 100644 --- a/security/clamav/pkg-descr +++ b/security/clamav/pkg-descr @@ -9,6 +9,15 @@ database updates. Plugin Changelog ================ +1.8.1 + +* Fixed detect broken executables option (contributed by sopex) + +1.8 + +* Use syslog for logging +* ClamAV verbose logging option added + 1.7 * Allow addition of external signatures diff --git a/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/Api/UrlController.php b/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/Api/UrlController.php index 768b95b432..447ab1f854 100644 --- a/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/Api/UrlController.php +++ b/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/Api/UrlController.php @@ -1,31 +1,29 @@ +/* + * Copyright (C) 2019 Michael Muenz + * All rights reserved. * - * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: * - * 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. * - * 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. + * 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. */ namespace OPNsense\ClamAV\Api; @@ -39,25 +37,29 @@ class UrlController extends ApiMutableModelControllerBase public function searchUrlAction() { - return $this->searchBase('lists.list', array("enabled", "name", "link")); + return $this->searchBase('lists.list', ['enabled', 'name', 'link']); } + public function getUrlAction($uuid = null) { - $this->sessionClose(); return $this->getBase('list', 'lists.list', $uuid); } + public function addUrlAction() { return $this->addBase('list', 'lists.list'); } + public function delUrlAction($uuid) { return $this->delBase('lists.list', $uuid); } + public function setUrlAction($uuid) { return $this->setBase('list', 'lists.list', $uuid); } + public function toggleUrlAction($uuid) { return $this->toggleBase('lists.list', $uuid); diff --git a/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/forms/general.xml b/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/forms/general.xml index 8f40db9bd9..d1cba308d0 100644 --- a/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/forms/general.xml +++ b/security/clamav/src/opnsense/mvc/app/controllers/OPNsense/ClamAV/forms/general.xml @@ -73,7 +73,7 @@ general.detectbroken checkbox - With this option clamav will try to detect broken executables (both PE and ELF) and mark them as Broken. + With this option ClamAV will alert on broken executables (both PE and ELF) and mark them as Broken. general.scanole2 @@ -121,7 +121,7 @@ general.scanhtml checkbox - Perform HTML normalisation and decryption of MS Script Encoder code. + Perform HTML normalization and decryption of MS Script Encoder code. general.scanarchive @@ -159,11 +159,17 @@ text Number of files to be scanned within an archive, a document, or any other container file. + + general.logverbose + + checkbox + Enable ClamAV verbose logging. + general.fc_logverbose checkbox - Enable verbose logging. + Enable Freshclam verbose logging. general.fc_databasemirror diff --git a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/ACL/ACL.xml b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/ACL/ACL.xml index 3ef9a83584..3299876ae1 100644 --- a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/ACL/ACL.xml +++ b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/ACL/ACL.xml @@ -4,10 +4,8 @@ ui/clamav/* api/clamav/* - ui/diagnostics/log/clamav/clamd/* - api/diagnostics/log/clamav/clamd/* - ui/diagnostics/log/clamav/freshclam/* - api/diagnostics/log/clamav/freshclam/* + ui/diagnostics/log/core/clamd/* + api/diagnostics/log/core/clamd/* diff --git a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/General.xml b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/General.xml index c3594eb8e0..ff0be29cc9 100644 --- a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/General.xml +++ b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/General.xml @@ -4,139 +4,143 @@ 1.0.0 - 0 + 0 Y - 0 + 0 Y - 1 + 1 Y - 10 + 10 N - 100 + 100 N - 30 + 30 N - 20 + 20 N - 0 + 0 N - 0 - N - + 0 + N + - 0 - N - + 0 + N + - 1 - N - + 1 + N + - 1 + 1 N - 0 + 0 N - 1 + 1 N - 0 + 0 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 1 + 1 N - 0 + 0 N - 100M + 100M N - - 25M + + 25M N - - 16 + + 16 N - - 10000 + + 10000 + N + + + 0 N - + - 0 + 0 N - database.clamav.net + database.clamav.net Y - 60 + 60 Y - 0 + 0 N - 0 + 0 N - 0 + 0 N - 0 + 0 N diff --git a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Menu/Menu.xml b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Menu/Menu.xml index e10858f138..273926ee6b 100644 --- a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Menu/Menu.xml +++ b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Menu/Menu.xml @@ -2,8 +2,7 @@ - - + diff --git a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Url.xml b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Url.xml index 84447c3b98..0300219bf0 100644 --- a/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Url.xml +++ b/security/clamav/src/opnsense/mvc/app/models/OPNsense/ClamAV/Url.xml @@ -6,7 +6,7 @@ - 1 + 1 Y @@ -14,7 +14,7 @@ Y - /^https?:\/\/.*$/i + /^https?:\/\/.*$/i URL has to start with http:// or https:// diff --git a/security/clamav/src/opnsense/mvc/app/views/OPNsense/ClamAV/general.volt b/security/clamav/src/opnsense/mvc/app/views/OPNsense/ClamAV/general.volt index c0765170ef..a3cafb7383 100644 --- a/security/clamav/src/opnsense/mvc/app/views/OPNsense/ClamAV/general.volt +++ b/security/clamav/src/opnsense/mvc/app/views/OPNsense/ClamAV/general.volt @@ -112,12 +112,12 @@ $( document ).ready(function() { }); $("#grid-lists").UIBootgrid( - { 'search':'/api/clamav/url/searchUrl', - 'get':'/api/clamav/url/getUrl/', - 'set':'/api/clamav/url/setUrl/', - 'add':'/api/clamav/url/addUrl/', - 'del':'/api/clamav/url/delUrl/', - 'toggle':'/api/clamav/url/toggleUrl/' + { 'search':'/api/clamav/url/search_url', + 'get':'/api/clamav/url/get_url/', + 'set':'/api/clamav/url/set_url/', + 'add':'/api/clamav/url/add_url/', + 'del':'/api/clamav/url/del_url/', + 'toggle':'/api/clamav/url/toggle_url/' } ); diff --git a/security/clamav/src/opnsense/scripts/OPNsense/ClamAV/freshclam.sh b/security/clamav/src/opnsense/scripts/OPNsense/ClamAV/freshclam.sh index 5ce3c3d66d..715b1ab14e 100755 --- a/security/clamav/src/opnsense/scripts/OPNsense/ClamAV/freshclam.sh +++ b/security/clamav/src/opnsense/scripts/OPNsense/ClamAV/freshclam.sh @@ -37,7 +37,7 @@ elif pgrep -qF ${PIDFILE} 2> /dev/null; then elif [ -z "${COMMAND}" ]; then echo "missing" else - daemon -f -p ${PIDFILE} freshclam --quiet + daemon -f -p ${PIDFILE} /usr/local/etc/rc.d/clamav_freshclam restart echo "starting" fi diff --git a/security/clamav/src/opnsense/service/conf/actions.d/actions_clamav.conf b/security/clamav/src/opnsense/service/conf/actions.d/actions_clamav.conf index f1d5d96a19..2e20af3ab3 100644 --- a/security/clamav/src/opnsense/service/conf/actions.d/actions_clamav.conf +++ b/security/clamav/src/opnsense/service/conf/actions.d/actions_clamav.conf @@ -1,39 +1,36 @@ [start] command: - /usr/local/opnsense/scripts/OPNsense/ClamAV/setup.sh; - /usr/local/etc/rc.d/clamav-freshclam start; - /usr/local/etc/rc.d/clamav-clamd start + /usr/local/etc/rc.d/clamav_freshclam start; + /usr/local/etc/rc.d/clamav_clamd start parameters: type:script message:starting ClamAV [stop] command: - /usr/local/etc/rc.d/clamav-freshclam stop; - /usr/local/etc/rc.d/clamav-clamd stop + /usr/local/etc/rc.d/clamav_freshclam stop; + /usr/local/etc/rc.d/clamav_clamd stop parameters: type:script message:stopping ClamAV [restart] command: - /usr/local/opnsense/scripts/OPNsense/ClamAV/setup.sh; - /usr/local/etc/rc.d/clamav-freshclam restart; - /usr/local/etc/rc.d/clamav-clamd restart + /usr/local/etc/rc.d/clamav_freshclam restart; + /usr/local/etc/rc.d/clamav_clamd restart parameters: type:script +description:Restart ClamAV message:restarting ClamAV [status] -command: /usr/local/etc/rc.d/clamav-clamd status; exit 0 +command: /usr/local/etc/rc.d/clamav_clamd status; exit 0 parameters: type:script_output message:request ClamAV status [freshclam] -command: - /usr/local/opnsense/scripts/OPNsense/ClamAV/setup.sh; - /usr/local/opnsense/scripts/OPNsense/ClamAV/freshclam.sh +command:/usr/local/opnsense/scripts/OPNsense/ClamAV/freshclam.sh parameters:%s type:script_output message:Check or install signatures diff --git a/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamav_clamd b/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamav_clamd index 271642d42e..a36465aaa7 100644 --- a/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamav_clamd +++ b/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamav_clamd @@ -1,7 +1,6 @@ {% if helpers.exists('OPNsense.clamav.general.enabled') and OPNsense.clamav.general.enabled == '1' %} -clamav_clamd_var_script="/usr/local/opnsense/scripts/OPNsense/ClamAV/setup.sh" +clamav_clamd_setup="/usr/local/opnsense/scripts/OPNsense/ClamAV/setup.sh" clamav_clamd_enable="YES" {% else %} clamav_clamd_enable="NO" {% endif %} -clamav_clamd_var_mfs="/var/db/clamav" diff --git a/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamav_freshclam b/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamav_freshclam index 7315a1ae4a..5a2fc06ca4 100644 --- a/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamav_freshclam +++ b/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamav_freshclam @@ -1,5 +1,5 @@ {% if helpers.exists('OPNsense.clamav.general.fc_enabled') and OPNsense.clamav.general.fc_enabled == '1' %} -clamav_freshclam_var_script="/usr/local/opnsense/scripts/OPNsense/ClamAV/setup.sh" +clamav_freshclam_setup="/usr/local/opnsense/scripts/OPNsense/ClamAV/setup.sh" clamav_freshclam_enable="YES" {% else %} clamav_freshclam_enable="NO" diff --git a/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamd.conf b/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamd.conf index 8d97d2bc71..7116c54286 100644 --- a/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamd.conf +++ b/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/clamd.conf @@ -1,6 +1,9 @@ {% if helpers.exists('OPNsense.clamav.general.enabled') and OPNsense.clamav.general.enabled == '1' %} -LogFile /var/log/clamav/clamd.log LogTime yes +LogSyslog yes +{% if helpers.exists('OPNsense.clamav.general.logverbose') and OPNsense.clamav.general.logverbose == '1' %} +LogVerbose yes +{% endif %} PidFile /var/run/clamav/clamd.pid DatabaseDirectory /var/db/clamav LocalSocket /var/run/clamav/clamd.sock @@ -35,7 +38,7 @@ ScanPE yes ScanELF yes {% endif %} {% if helpers.exists('OPNsense.clamav.general.detectbroken') and OPNsense.clamav.general.detectbroken == '1' %} -DetectBrokenExecutables yes +AlertBrokenExecutables yes {% endif %} {% if helpers.exists('OPNsense.clamav.general.scanole2') and OPNsense.clamav.general.scanole2 == '1' %} ScanOLE2 yes diff --git a/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/freshclam.conf b/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/freshclam.conf index b2720128da..3bfd536c35 100644 --- a/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/freshclam.conf +++ b/security/clamav/src/opnsense/service/templates/OPNsense/ClamAV/freshclam.conf @@ -2,7 +2,7 @@ DatabaseDirectory /var/db/clamav -UpdateLogFile /var/log/clamav/freshclam.log +LogSyslog yes LogTime yes {% if helpers.exists('OPNsense.clamav.general.fc_logverbose') and OPNsense.clamav.general.fc_logverbose == '1' %} diff --git a/security/clamav/src/opnsense/service/templates/OPNsense/Syslog/local/clamd.conf b/security/clamav/src/opnsense/service/templates/OPNsense/Syslog/local/clamd.conf new file mode 100644 index 0000000000..048381b2c5 --- /dev/null +++ b/security/clamav/src/opnsense/service/templates/OPNsense/Syslog/local/clamd.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [clamd]. +################################################################### +filter f_local_clamd { + program("clamd") or program("freshclam"); +}; diff --git a/security/crowdsec/+POST_DEINSTALL.post b/security/crowdsec/+POST_DEINSTALL.post new file mode 100755 index 0000000000..5934e52d49 --- /dev/null +++ b/security/crowdsec/+POST_DEINSTALL.post @@ -0,0 +1,50 @@ +#!/bin/sh + +# Removing the plugin from the web interface will autoremove the dependencies +# too, and here we have to delete the files in rc.conf.d (because they are +# generated from templates when the configuration is saved, and the package +# system did not keep track of them). + +# But.. If the plugin is removed from the command line (which does not happen +# outside of testing conditions), the crowdsec and bouncer services will not be +# removed. However, since we deleted the files that enabled these services, +# they will be disabled at the next reboot. + +rm -f /etc/rc.conf.d/crowdsec \ + /etc/rc.conf.d/crowdsec_firewall \ + /etc/rc.conf.d/oscrowdsec + + +# Remove aliases and with them, the rules. We don't have plugin files +# anymore so we do that on the fly. + +/usr/local/bin/php <<'EOT' +aliases->alias->iterateItems() as $index => $alias) { + if (strval($alias->name) == $name) { + if ($model->aliases->alias->del($index)) { + $model->serializeToConfig(); + Config::getInstance()->save(); + } + } + } +} + +removeAlias('crowdsec_blocklists'); +removeAlias('crowdsec6_blocklists'); +EOT + + +# apply the configuration changes to the packet filter +configctl filter reload diff --git a/security/crowdsec/+POST_INSTALL.post b/security/crowdsec/+POST_INSTALL.post new file mode 100755 index 0000000000..fd090d0dcf --- /dev/null +++ b/security/crowdsec/+POST_INSTALL.post @@ -0,0 +1,6 @@ +#!/bin/sh + +configctl crowdsec reconfigure + +# apply new configuration immediately, don't wait for hub updates +service crowdsec reload >/dev/null 2>&1 || : diff --git a/security/crowdsec/+POST_INSTALL.pre b/security/crowdsec/+POST_INSTALL.pre new file mode 100755 index 0000000000..e5733ae4a3 --- /dev/null +++ b/security/crowdsec/+POST_INSTALL.pre @@ -0,0 +1,9 @@ +#!/bin/sh + +# The configuration file used in reconfigure (i.e. settings.json) may eventually +# have credentials, so we need to restrict its permissions. We do so by pre-creating +# the directory, and the template package will use its permissions while creating the file. +# If we do that in setup.sh, the file already exists with bad permissions. + +# shellcheck disable=SC2174 +mkdir -p -m 0700 /usr/local/etc/crowdsec/opnsense diff --git a/security/crowdsec/+PRE_DEINSTALL.pre b/security/crowdsec/+PRE_DEINSTALL.pre new file mode 100755 index 0000000000..954af708ed --- /dev/null +++ b/security/crowdsec/+PRE_DEINSTALL.pre @@ -0,0 +1,7 @@ +#!/bin/sh + +# need to temporarily stop the bouncer to remove all the rules +service crowdsec_firewall stop >/dev/null 2>&1 | : + +# the rest of the cleanup is done in the post-deinstall script, otherwise +# the plugin recreates the objects during "filter reload". diff --git a/security/crowdsec/LICENSE b/security/crowdsec/LICENSE new file mode 100644 index 0000000000..92d86fc27c --- /dev/null +++ b/security/crowdsec/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2021 Crowdsec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/security/crowdsec/Makefile b/security/crowdsec/Makefile new file mode 100644 index 0000000000..1d6d54f333 --- /dev/null +++ b/security/crowdsec/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= crowdsec +PLUGIN_VERSION= 1.0.12 +PLUGIN_DEPENDS= crowdsec +PLUGIN_COMMENT= Lightweight and collaborative security engine +PLUGIN_MAINTAINER= marco@crowdsec.net +PLUGIN_WWW= https://crowdsec.net/ + +.include "../../Mk/plugins.mk" diff --git a/security/crowdsec/pkg-descr b/security/crowdsec/pkg-descr new file mode 100644 index 0000000000..6547b151f5 --- /dev/null +++ b/security/crowdsec/pkg-descr @@ -0,0 +1,119 @@ +Crowdsec is an open-source, lightweight software, detecting peers with +aggressive behaviors to prevent them from accessing your systems. Its user +friendly design and assistance offers a low technical barrier of entry and +nevertheless a high security gain. + +WWW: https://crowdsec.net/ + +Plugin Changelog +================ + +1.0.12 + + * Fix and update service management (start/stop/reload/configure) + +1.0.11 + +* convert tables to UIBootGrid (required for opnsense 25.7) +* separate page for each table +* IPv6 validation for LAPI listen address broken (contributed by BPplays) +* Fix alert time not showing in grid + +1.0.10 + +* changed alias names crowdsec*blacklists -> crowdsec*blocklists +* added rules for outgoing connections too +* added enroll_key to settings for automatic enrollment +* option to disable rule generation (bring your own rules!) +* code cleanup, reformat, typing + +1.0.9 + +* Update rule reference ($ -> <>) for opnsense 25.1 + +1.0.8 + +* Enable use_wal, remove warning +* Randomize cron execution over 5 minutes +* Refactor javascript +* Fix initial service start with no pending hub updates (1.6.1) +* Add input validation for `rules_tag` to prevent invalid `pf` syntax. +* Fix service start when lapi is disabled (revision 1) + +1.0.7 + +* Add option `retry_initial_connect` to bouncer configuration for more robust startup. The option was introduced in Crowdsec 1.5.4. + +1.0.6 + +* default acquis.d/opnsense.yaml to "poll_without_inotify=true" which is now required + to acquire content from symlinks. + +1.0.5 + +* fix ban example + +1.0.4 + +* Add force_inotify option to aquire logs when /var/log is in RAM, otherwise + a restart of the service is required after a reboot. + +1.0.3 + +* acquire filter logs for the firewallservices/pf collection (port scans). + If you already added it manually, you can remove it now to avoid counting + the events twice. + +1.0.2 + +* updated cron job (reload only when there are updates), added small random delay +* limit log collection to latest.log (requires 22.7+) +* some javascript lint + +1.0.1 + +* fixed live logs + +1.0 + +* first non-devel release +* changed service restart to reload on hub update; fixed "service oscrowdsec status" + +0.2 + +* first published release +* added options `lapi_enabled`, `crowdsec_firewall_verbose` +* removed options `crowdsec_flags`, `crowdsec_firewall_flags` +* changed default for `agent_enabled`, `firewall_bouncer_enabled` to 1 + +0.1 + +* fixed packet tags with ipv6 +* custom `crowdsec_flags`, `crosdsec_firewall_flags` + +0.0.9 + +* fixed the javascript, 0.0.8 had a syntax error +* new option: rules_tag +* new option: lapi_manual_configuration +* ipv4/ipv6 validation with regexp + +0.0.8 + +* crowdsec update 1.3.2 +* configurable `rules_log` and LAPI address/port + +0.0.7 + +* automated removal of Alias objects when the plugin is uninstalled + +0.0.6 + +* crowdsec update 1.3.1.r1 +* bouncer update to 0.0.23.r1 +* automated creation of Alias and Rule objects + +0.0.5 + +* fixed an issue that prevented the bouncer from banning IPs on opnsense +* fixed support for notification plugins diff --git a/security/crowdsec/src/etc/cron.d/oscrowdsec.cron b/security/crowdsec/src/etc/cron.d/oscrowdsec.cron new file mode 100644 index 0000000000..bff08edd3e --- /dev/null +++ b/security/crowdsec/src/etc/cron.d/oscrowdsec.cron @@ -0,0 +1,9 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +# +# User-defined crontab files can be loaded via /etc/cron.d +# or /usr/local/etc/cron.d and follow the same format as +# /etc/crontab, see the crontab(5) manual page. +SHELL=/bin/sh +PATH=/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin +#minute hour mday month wday who command +0 1 * * * root (sleep $(jot -r 1 1 300); /usr/local/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh) diff --git a/security/crowdsec/src/etc/crowdsec/acquis.d/opnsense.yaml b/security/crowdsec/src/etc/crowdsec/acquis.d/opnsense.yaml new file mode 100644 index 0000000000..98ec286d5f --- /dev/null +++ b/security/crowdsec/src/etc/crowdsec/acquis.d/opnsense.yaml @@ -0,0 +1,32 @@ +# +# Before 22.1, OPNsense used circular logs under /var/log/*.log that +# can still be around. They are old, in binary format and are not needed by crowdsec. +# +# For this reason we don't scan /var/log/*.log, but some plugins can write +# their (plaintext) logs in that location, in such case add their pathnames too. +# + +filenames: + # DO NOT EDIT - to add new datasources (log locations), + # create new files in /usr/local/etc/crowdsec/acquis.d/ + + # collection: crowdsecurity/sshd + - /var/log/audit/latest.log + # collection: crowdsecurity/opnsense-gui (web admin) + - /var/log/lighttpd/latest.log + # collection: firewallservices/pf + - /var/log/filter/latest.log + +# When OPNsense is configured with /var/log in a RAM disk, +# the log directories are created after crowdsec is run. +# We force crowdsec to watch over directory creation as well +# as file creation. FreeBSD has kqueue instead of inotify +# but the option works with both. +force_inotify: true + +# this option is required from crowdsec v1.5.0 to follow +# changes in symlinks +poll_without_inotify: true + +labels: + type: syslog diff --git a/security/crowdsec/src/etc/inc/plugins.inc.d/crowdsec.inc b/security/crowdsec/src/etc/inc/plugins.inc.d/crowdsec.inc new file mode 100644 index 0000000000..b226f424a6 --- /dev/null +++ b/security/crowdsec/src/etc/inc/plugins.inc.d/crowdsec.inc @@ -0,0 +1,124 @@ + + +use OPNsense\Core\Config; +use OPNsense\Firewall\Alias; +use OPNsense\Firewall\Plugin; + +function add_alias_if_not_exist($name, $description, $proto) +{ + $model = new Alias(); + + if ($model->getByName($name) != null) { + return; + } + + $new_alias = $model->aliases->alias->Add(); + $new_alias->name = $name; + $new_alias->description = $description; + $new_alias->proto = $proto; + $new_alias->type = 'external'; + $model->serializeToConfig(); + Config::getInstance()->save(); +} + +function crowdsec_firewall(Plugin $fw) +{ + global $config; + + $general = $config['OPNsense']['crowdsec']['general']; + + $bouncer_enabled = isset($general['firewall_bouncer_enabled']) && $general['firewall_bouncer_enabled']; + + if (!$bouncer_enabled) { + return; + } + + $rules_log_enabled = isset($general['rules_log']) && $general['rules_log']; + + $rules_tag = ""; + if (isset($general['rules_tag'])) { + $rules_tag = $general['rules_tag']; + } + + add_alias_if_not_exist('crowdsec_blocklists', 'CrowdSec (IPv4)', 'IPv4'); + add_alias_if_not_exist('crowdsec6_blocklists', 'CrowdSec (IPv6)', 'IPv6'); + + // https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/library/OPNsense/Firewall/FilterRule.php + + // if missing, default to true + if (!isset($general['rules_enabled']) || $general['rules_enabled'] != 0) { + $fw->registerFilterRule( + 1, /* priority */ + array( + 'ipprotocol' => 'inet', + 'descr' => 'CrowdSec (IPv4) in', + 'from' => '', + 'direction' => 'in', + 'type' => 'block', + 'log' => $rules_log_enabled, + 'tag' => $rules_tag, + 'quick' => true + ) + ); + + $fw->registerFilterRule( + 1, /* priority */ + array( + 'ipprotocol' => 'inet', + 'descr' => 'CrowdSec (IPv4) out', + 'to' => '', + 'direction' => 'out', + 'type' => 'block', + 'log' => $rules_log_enabled, + 'tag' => $rules_tag, + 'quick' => true + ) + ); + + $fw->registerFilterRule( + 1, /* priority */ + array( + 'ipprotocol' => 'inet6', + 'descr' => 'CrowdSec (IPv6) in', + 'from' => '', + 'direction' => 'in', + 'type' => 'block', + 'log' => $rules_log_enabled, + 'tag' => $rules_tag, + 'quick' => true + ) + ); + + $fw->registerFilterRule( + 1, /* priority */ + array( + 'ipprotocol' => 'inet6', + 'descr' => 'CrowdSec (IPv6) out', + 'to' => '', + 'direction' => 'out', + 'type' => 'block', + 'log' => $rules_log_enabled, + 'tag' => $rules_tag, + 'quick' => true + ) + ); + } +} + +function crowdsec_services() +{ + $services[] = array( + 'description' => 'CrowdSec', + 'configd' => array( + 'restart' => array('crowdsec restart'), + 'start' => array('crowdsec start'), + 'stop' => array('crowdsec stop'), + ), + 'name' => 'crowdsec' + ); + + return $services; +} diff --git a/security/crowdsec/src/etc/rc.d/oscrowdsec b/security/crowdsec/src/etc/rc.d/oscrowdsec new file mode 100755 index 0000000000..87a34c703b --- /dev/null +++ b/security/crowdsec/src/etc/rc.d/oscrowdsec @@ -0,0 +1,102 @@ +#!/bin/sh +# +# PROVIDE: oscrowdsec +# REQUIRE: NETWORKING syslogd +# BEFORE: DAEMON +# KEYWORD: shutdown + +# shellcheck disable=SC1091 +. /etc/rc.subr + +name="oscrowdsec" +rcvar="oscrowdsec_enable" + +load_rc_config $name + +: "${oscrowdsec_enable="NO"}" + + +oscrowdsec_start () { + # + # Start, or stop the services according to the plugin's configuration. + # When starting -> error if the services are already running + # When stopping -> no error + # + + if service crowdsec enabled; then + service crowdsec start + else + service crowdsec stop || : + fi + + if service crowdsec_firewall enabled; then + service crowdsec_firewall start + else + service crowdsec_firewall stop || : + fi +} + +oscrowdsec_stop () { + # Always stop the services, enabled or not, running or not. No errors. + + service crowdsec stop || : + service crowdsec_firewall stop || : +} + +oscrowdsec_restart () { + oscrowdsec_stop || : + oscrowdsec_start +} + +oscrowdsec_status () { + # return error if at least one program is not running + service crowdsec status + ret=$? + + if ! service crowdsec_firewall enabled; then + return $ret + fi + + if ! service crowdsec_firewall status; then + ret=1 + fi + return $ret +} + +oscrowdsec_reload () { + if service crowdsec enabled; then + if service crowdsec status >/dev/null 2>&1; then + service crowdsec reload + else + service crowdsec restart + fi + fi + + if service crowdsec_firewall enabled; then + # the bouncer does not support reload + service crowdsec_firewall restart + fi +} + +case $1 in + start) + oscrowdsec_start + exit $? + ;; + stop) + oscrowdsec_stop + exit $? + ;; + restart) + oscrowdsec_restart + exit $? + ;; + status) + oscrowdsec_status + exit $? + ;; + reload) + oscrowdsec_reload + exit $? + ;; +esac diff --git a/security/crowdsec/src/etc/rc.syshook.d/start/50-crowdsec b/security/crowdsec/src/etc/rc.syshook.d/start/50-crowdsec new file mode 100755 index 0000000000..35922bb443 --- /dev/null +++ b/security/crowdsec/src/etc/rc.syshook.d/start/50-crowdsec @@ -0,0 +1,5 @@ +#!/bin/sh + +# https://docs.opnsense.org/development/backend/autorun.html + +/usr/local/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AlertsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AlertsController.php new file mode 100644 index 0000000000..b37b606604 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AlertsController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class AlertsController + * @package OPNsense\CrowdSec + */ +class AlertsController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/alerts'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AlertsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AlertsController.php new file mode 100644 index 0000000000..ae648a7933 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AlertsController.php @@ -0,0 +1,88 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class AlertsController extends ApiControllerBase +{ + /** + * Format scope and value as "scope:value" + * + * @param array $source Array with 'scope' and 'value' keys (can be a decision) + * @return string Formatted string + */ + private function formatScopeValue(array $source): string + { + $scope = $source['scope'] ?? ''; + if ($source['value'] !== '') { + $scope = $scope . ':' . $source['value']; + } + return $scope; + } + + /** + * Summarize decision types as "type1:count1 type2:count2 ..." + * + * @param array $decisions List of decision arrays + * @return string Summary string + */ + private function formatDecisions(array $decisions): string + { + $counts = []; + + foreach ($decisions as $decision) { + if (!isset($decision['type'])) { + continue; + } + + $type = $decision['type']; + $counts[$type] = ($counts[$type] ?? 0) + 1; + } + + $parts = []; + foreach ($counts as $type => $count) { + $parts[] = "{$type}:{$count}"; + } + + return implode(' ', $parts); + } + + /** + * Retrieve list of alerts + * + * @return array of alerts + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec alerts-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $rows = []; + foreach ($result as $alert) { + $source = $alert['source'] ?? []; + $rows[] = [ + 'id' => $alert['id'], + 'value' => $this->formatScopeValue($source ?? []), + 'reason' => $alert['scenario'] ?? '', + 'country' => $source['cn'] ?? '', + 'as' => $source['as_name'] ?? '', + 'decisions' => $this->formatDecisions($alert['decisions'] ?? []), + 'created' => $alert['created_at'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecconfigsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecconfigsController.php new file mode 100644 index 0000000000..320339f867 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecconfigsController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class AppsecconfigsController extends ApiControllerBase +{ + /** + * Retrieve the installed appsec-configs + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec appsec-configs-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["appsec-configs"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecrulesController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecrulesController.php new file mode 100644 index 0000000000..583b58bafb --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/AppsecrulesController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class AppsecrulesController extends ApiControllerBase +{ + /** + * Retrieve the installed appsec-rules + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec appsec-rules-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["appsec-rules"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/BouncersController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/BouncersController.php new file mode 100644 index 0000000000..ecaadbea4c --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/BouncersController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class BouncersController extends ApiControllerBase +{ + /** + * Retrieve list of bouncers + * + * @return array of bouncers + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec bouncers-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $rows = []; + foreach ($result as $bouncer) { + $rows[] = [ + 'name' => $bouncer['name'], + 'type' => $bouncer['type'] ?? '', + 'version' => $bouncer['version'] ?? '', + 'created' => $bouncer['created_at'] ?? '', + 'valid' => ($bouncer['revoked'] ?? false) !== true, + 'ip_address' => $bouncer['ip_address'] ?? '', + 'last_seen' => $bouncer['last_pull'] ?? '', + 'os' => $bouncer['os'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/CollectionsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/CollectionsController.php new file mode 100644 index 0000000000..40449fdeea --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/CollectionsController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class CollectionsController extends ApiControllerBase +{ + /** + * Retrieve the installed collections + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec collections-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["collections"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/DecisionsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/DecisionsController.php new file mode 100644 index 0000000000..99c3fbb533 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/DecisionsController.php @@ -0,0 +1,120 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\Core\Backend; + +function unrollDecisions(array $alerts): array +{ + $result = []; + + foreach ($alerts as $alert) { + if (!isset($alert['decisions']) || !is_array($alert['decisions'])) { + continue; + } + + foreach ($alert['decisions'] as $decision) { + // ignore deleted decisions + if (isset($decision['duration']) && str_starts_with($decision['duration'], '-')) { + continue; + } + + $row = $decision; + + // Add parent alert fields with prefix + foreach ($alert as $key => $value) { + if ($key === 'decisions') { + continue; // skip nested array + } + $row["alert_" . $key] = $value; + } + + $result[] = $row; + } + } + + return $result; +} + + +/** + * @package OPNsense\CrowdSec + */ +class DecisionsController extends ApiControllerBase +{ + /** + * Format scope and value as "scope:value" + * + * @param array $source Array with 'scope' and 'value' keys + * @return string Formatted string + */ + private function formatScopeValue(array $source): string + { + $scope = $source['scope'] ?? ''; + if ($source['value'] !== '') { + $scope = $scope . ':' . $source['value']; + } + return $scope; + } + + /** + * Retrieve list of decisions + * + * @return array of decisions + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec decisions-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $decisions = unrollDecisions($result); + + $rows = []; + foreach ($decisions as $dec) { + $alert_source = $dec['alert_source'] ?? []; + + $rows[] = [ + 'id' => $dec['id'], + 'source' => $dec['origin'] ?? '', + 'scope_value' => $this->formatScopeValue($dec), + 'reason' => $dec['scenario'] ?? '', + 'action' => $dec['type'] ?? '', + 'country' => $alert_source['cn'] ?? '', + 'as' => $alert_source['as_name'] ?? '', + 'events_count' => $dec['alert_events_count'] ?? '', + 'expiration' => $dec['duration'] ?? '', + 'alert_id' => $dec['alert_id'], + ]; + } + + return $this->searchRecordsetBase($rows); + } + + public function delAction($decision_id): array + { + if ($this->request->isPost()) { + $result = (new Backend())->configdRun("crowdsec decisions-delete {$decision_id}"); + if ($result === null) { + return ["result" => "deleted"]; + } + + // why does the action return \n\n for empty output? + if (trim($result) === '') { + return ["result" => "deleted"]; + } + // TODO assume not found, should handle other errors + return ["result" => "not found"]; + } else { + $this->response->setStatusCode(405, "Method Not Allowed"); + $this->response->setHeader("Allow", "DELETE"); + } + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/GeneralController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/GeneralController.php new file mode 100644 index 0000000000..038e491818 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/GeneralController.php @@ -0,0 +1,17 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiMutableModelControllerBase; + +/** + * @package OPNsense\CrowdSec + */ +class GeneralController extends ApiMutableModelControllerBase +{ + protected static $internalModelName = 'general'; + protected static $internalModelClass = '\OPNsense\CrowdSec\General'; +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/MachinesController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/MachinesController.php new file mode 100644 index 0000000000..714a5c2527 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/MachinesController.php @@ -0,0 +1,45 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class MachinesController extends ApiControllerBase +{ + /** + * Retrieve list of machines + * + * @return array of machines + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec machines-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $rows = []; + foreach ($result as $machine) { + $rows[] = [ + 'name' => $machine['machineId'], + 'ip_address' => $machine['ipAddress'] ?? '', + 'version' => $machine['version'] ?? '', + 'validated' => $machine['isValidated'] ?? false, + 'created' => $machine['created_at'] ?? '', + 'last_seen' => $machine['last_heartbeat'] ?? '', + 'os' => $machine['os'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ParsersController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ParsersController.php new file mode 100644 index 0000000000..7a904e66a1 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ParsersController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class ParsersController extends ApiControllerBase +{ + /** + * Retrieve the installed parsers + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec parsers-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["parsers"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/PostoverflowsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/PostoverflowsController.php new file mode 100644 index 0000000000..4a9ea031f2 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/PostoverflowsController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class PostoverflowsController extends ApiControllerBase +{ + /** + * Retrieve the installed postoverflows + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec postoverflows-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["postoverflows"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ScenariosController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ScenariosController.php new file mode 100644 index 0000000000..4c1244576b --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ScenariosController.php @@ -0,0 +1,46 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\CrowdSec\Util; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class ScenariosController extends ApiControllerBase +{ + /** + * Retrieve the installed scenarios + * + * @return dictionary of items, by type + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function searchAction(): array + { + $result = json_decode(trim((new Backend())->configdRun("crowdsec scenarios-list")), true); + if ($result === null) { + return ["message" => "unable to retrieve data"]; + } + + $items = $result["scenarios"]; + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'name' => $item['name'], + 'status' => $item['status'] ?? '', + 'local_version' => $item['local_version'] ?? '', + 'local_path' => Util::trimLocalPath($item['local_path'] ?? ''), + 'description' => $item['description'] ?? '', + ]; + } + + return $this->searchRecordsetBase($rows); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ServiceController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ServiceController.php new file mode 100644 index 0000000000..c18ebb0295 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/ServiceController.php @@ -0,0 +1,45 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiMutableServiceControllerBase; +use OPNsense\Core\Backend; + +/** + * Class ServiceController + * @package OPNsense\CrowdSec + */ +class ServiceController extends ApiMutableServiceControllerBase +{ + protected static $internalServiceClass = '\OPNsense\CrowdSec\General'; + protected static $internalServiceTemplate = 'OPNsense/CrowdSec'; + protected static $internalServiceName = 'crowdsec'; + + protected function ServiceEnabled() + { + $mdl = $this->getModel(); + + return ( + $mdl->agent_enabled->__toString() === "1" || + $mdl->lapi_enabled->__toString() === "1" || + $mdl->firewall_bouncer_enabled->__toString() === "1" + ); + } + + public function reconfigureAction() + { + // Run the default reconfigure logic + $result = parent::reconfigureAction(); + + // Now we generate the config.yaml and config-firewall-bouncer.yaml files + if (isset($result['status']) && $result['status'] === 'ok') { + $backend = new Backend(); + $backend->configdRun('crowdsec reconfigure'); + } + + return $result; + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/VersionController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/VersionController.php new file mode 100644 index 0000000000..cea95d0d7c --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/Api/VersionController.php @@ -0,0 +1,27 @@ + + +namespace OPNsense\CrowdSec\Api; + +use OPNsense\Base\ApiControllerBase; +use OPNsense\Core\Backend; + +/** + * @package OPNsense\CrowdSec + */ +class VersionController extends ApiControllerBase +{ + /** + * Retrieve version description + * + * @return version description + * @throws \OPNsense\Base\ModelException + * @throws \ReflectionException + */ + public function getAction(): string + { + return (new Backend())->configdRun("crowdsec version"); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecconfigsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecconfigsController.php new file mode 100644 index 0000000000..d63e2f6a78 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecconfigsController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class AppsecconfigsController + * @package OPNsense\CrowdSec + */ +class AppsecconfigsController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/appsecconfigs'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecrulesController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecrulesController.php new file mode 100644 index 0000000000..ad06ba30ac --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/AppsecrulesController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class AppsecrulesController + * @package OPNsense\CrowdSec + */ +class AppsecrulesController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/appsecrules'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/BouncersController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/BouncersController.php new file mode 100644 index 0000000000..beb79086de --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/BouncersController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class BouncersController + * @package OPNsense\CrowdSec + */ +class BouncersController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/bouncers'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/CollectionsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/CollectionsController.php new file mode 100644 index 0000000000..814c1ebe6d --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/CollectionsController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class CollectionsController + * @package OPNsense\CrowdSec + */ +class CollectionsController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/collections'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/DecisionsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/DecisionsController.php new file mode 100644 index 0000000000..6cd08cc039 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/DecisionsController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class DecisionsController + * @package OPNsense\CrowdSec + */ +class DecisionsController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/decisions'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/GeneralController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/GeneralController.php new file mode 100644 index 0000000000..fc3ea58a30 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/GeneralController.php @@ -0,0 +1,19 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class GeneralController + * @package OPNsense\CrowdSec + */ +class GeneralController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/general'); + $this->view->generalForm = $this->getForm("general"); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/MachinesController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/MachinesController.php new file mode 100644 index 0000000000..efa867450e --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/MachinesController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class MachinesController + * @package OPNsense\CrowdSec + */ +class MachinesController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/machines'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/OverviewController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/OverviewController.php new file mode 100644 index 0000000000..f15ca5218b --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/OverviewController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class OverviewController + * @package OPNsense\CrowdSec + */ +class OverviewController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/overview'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ParsersController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ParsersController.php new file mode 100644 index 0000000000..adb1c6c142 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ParsersController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class ParsersController + * @package OPNsense\CrowdSec + */ +class ParsersController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/parsers'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/PostoverflowsController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/PostoverflowsController.php new file mode 100644 index 0000000000..d20de2e82a --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/PostoverflowsController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class PostoverflowsController + * @package OPNsense\CrowdSec + */ +class PostoverflowsController extends \OPNsense\Base\IndexController +{ + public function indexAction() + { + $this->view->pick('OPNsense/CrowdSec/postoverflows'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ScenariosController.php b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ScenariosController.php new file mode 100644 index 0000000000..d1bdbd4a0d --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/ScenariosController.php @@ -0,0 +1,18 @@ + + +namespace OPNsense\CrowdSec; + +/** + * Class ScenariosController + * @package OPNsense\CrowdSec + */ +class ScenariosController extends \OPNsense\Base\IndexController +{ + public function indexAction(): void + { + $this->view->pick('OPNsense/CrowdSec/scenarios'); + } +} diff --git a/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/forms/general.xml b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/forms/general.xml new file mode 100644 index 0000000000..c4480f9f78 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/controllers/OPNsense/CrowdSec/forms/general.xml @@ -0,0 +1,113 @@ +
    + + + + general.agent_enabled + + checkbox + Enable/disable the CrowdSec agent. Keep this enabled to detect + attacks and receive alerts from the CrowSec central service. + + + + + general.lapi_enabled + + checkbox + Enable/disable the CrowdSec Local API. Keep this enabled unless you + connect to a LAPI on another machine. + + + + + general.firewall_bouncer_enabled + + checkbox + Enable/disable the remediation component. Keep this enabled to block + packets from the attacking IP addresses. + + + + + general.enroll_key + + text + Click "Enroll command" on the the website and copy the key here. + + + + + general.lapi_manual_configuration + + checkbox + Avoid overwriting LAPI settings for config.yaml, + local_api_credentials.yaml, crowdsec-firewall-bouncer.yaml. The next + two configuration options (lapi_listen_address, lapi_listen_port) will + be ignored. Allows unsupported configurations like linking together + multiple opnsense instances or connecting to an existing crowdsec + multi-server setup. + + + + + general.lapi_listen_address + + text + Where to listen for LAPI connections: IP address. The default value + is 127.0.0.1. You can change it to a LAN address to connect from other + agents/machines and bouncers. + + This is written in /usr/local/etc/crowdsec/config.yaml, + local_api_credentials.yaml and bouncers/crowdsec-firewall-bouncer.yaml. + To enable TLS, add the certificate information to config.yaml and change + http to https in the other two files. Comments in YAML will not be + preserved. + + + + + general.lapi_listen_port + + text + Where to listen for LAPI connections: port. The default value is + 8080, but you can change it to avoid conflicts with existing + services. + + + + + general.rules_enabled + + checkbox + Generate block rules from the Crowdsec blocklists. + They are applied to all interfaces, ipv4/v6, ingress and egress. + If you disable this, you'll have to write your own rules to block anything. + + + + + general.rules_log + + checkbox + Enable log collection for CrowdSec's block rules. + + + + + general.rules_tag + + text + Add a tag to packets that are dropped by CrowdSec rules for + diagnostic purposes. + + + + + general.crowdsec_firewall_verbose + + checkbox + Verbose /var/log/crowdsec/crowdsec-firewall-bouncer.log. Enable this + for debugging. + + + diff --git a/security/crowdsec/src/opnsense/mvc/app/library/OPNsense/CrowdSec/Util.php b/security/crowdsec/src/opnsense/mvc/app/library/OPNsense/CrowdSec/Util.php new file mode 100644 index 0000000000..e0eb5a0dbc --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/library/OPNsense/CrowdSec/Util.php @@ -0,0 +1,15 @@ + + + CrowdSec + + ui/crowdsec/* + api/crowdsec/* + + + diff --git a/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.php b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.php new file mode 100644 index 0000000000..3307e8ba60 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.php @@ -0,0 +1,12 @@ + + +namespace OPNsense\CrowdSec; + +use OPNsense\Base\BaseModel; + +class General extends BaseModel +{ +} diff --git a/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.xml b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.xml new file mode 100644 index 0000000000..8494d3d2f9 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/General.xml @@ -0,0 +1,54 @@ + + //OPNsense/crowdsec/general + CrowdSec general configuration + 1.0.12 + + + 1 + Y + + + 1 + Y + + + 1 + Y + + + 0 + Y + + + 127.0.0.1 + Y + N + + + 8080 + Y + N + N + + + 1 + Y + + + 0 + Y + + + /^([0-9a-zA-Z]{1,63})$/u + A tag must only contain numbers and letters and must be between 1 and 63 characters. + + + /^([0-9a-zA-Z]{1,63})$/u + The enrollment key can only contain numbers and letters and must be between 1 and 63 characters. + + + 0 + Y + + + diff --git a/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/Menu/Menu.xml b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/Menu/Menu.xml new file mode 100644 index 0000000000..99e4d4e336 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/models/OPNsense/CrowdSec/Menu/Menu.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/alerts.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/alerts.volt new file mode 100644 index 0000000000..9ad6bc6e21 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/alerts.volt @@ -0,0 +1,62 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + +
    {{ lang._('Enabled') }}{{ lang._('Network Id') }}{{ lang._('Local Description') }}{{ lang._('Commands') }}{{ lang._('Enabled') }}{{ lang._('Network Id') }}{{ lang._('Local Description') }}{{ lang._('Commands') }} {{ lang._('ID') }}
    {{ lang._('Enabled') }} {{ lang._('Name') }} {{ lang._('E-Mail') }}{{ lang._('CA') }} {{ lang._('Status') }} {{ lang._('Registration Date') }} {{ lang._('Commands') }}
    + + + + + + + + + + + + + + + +
    IDValueReasonCountryASDecisionsCreated
    diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecconfigs.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecconfigs.volt new file mode 100644 index 0000000000..53dcfd0ae1 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecconfigs.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
    NameStatusVersionPathDescription
    diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecrules.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecrules.volt new file mode 100644 index 0000000000..8e6ce811be --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/appsecrules.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
    NameStatusVersionPathDescription
    diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/bouncers.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/bouncers.volt new file mode 100644 index 0000000000..43d4e0e7f1 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/bouncers.volt @@ -0,0 +1,44 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeVersionCreatedValidIP AddressLast SeenOS
    diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/collections.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/collections.volt new file mode 100644 index 0000000000..ca85d2e7ba --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/collections.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
    NameStatusVersionPathDescription
    diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/decisions.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/decisions.volt new file mode 100644 index 0000000000..0eb4bc1971 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/decisions.volt @@ -0,0 +1,51 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + +Note: the decisions coming from the CAPI (signals collected by the CrowdSec users) do not appear here. +To show them, use cscli decisions list -a in a shell. + + + + + + + + + + + + + + + + + + + + + + + + +
    IDSourceScope:ValueReasonActionCountryASEventsExpirationAlert IDCommands
    + + +
    diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/general.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/general.volt new file mode 100644 index 0000000000..95b2577272 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/general.volt @@ -0,0 +1,189 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + +
    +
    +

    Introduction

    + +

    This plugin installs a CrowdSec agent/LAPI + node, and a Firewall Bouncer.

    + +

    Out of the box, by enabling them in the "Settings" tab, they can protect the OPNsense server + by receiving thousands of IP addresses of active attackers, which are immediately banned at the + firewall level. In addition, the logs of the ssh service and OPNsense administration interface are + analyzed for possible brute-force attacks; any such scenario triggers a ban and is reported to the + CrowdSec Central API + (meaning timestamp, scenario, attacking IP).

    + +

    Other attack behaviors can be recognized on the OPNsense server and its plugins, or + any other agent + connected to the same LAPI node. Other types of remediation are possible (ex. captcha test for scraping attempts).

    + + We recommend you to register to the Console. This helps you manage your instances, + and us to have better overall metrics. + +

    Please refer to the tutorials to explore + the possibilities.

    + +

    For the latest plugin documentation, including how to use it with an external LAPI, see Install + CrowdSec (OPNsense)

    + +

    A few remarks:

    + +
      +
    • + New acquisition files go under /usr/local/etc/crowdsec/acquis.d. See opnsense.yaml for details. + The option poll_without_inotify: true is required if the log sources are symlinks (which + is the case for most opnsense logs). +
    • +
    • + If your OPNsense is <22.1, you must check "Disable circular logs" in the Settings menu for the + ssh and web-auth parsers to work. If you upgrade to 22.1, it will be done automatically. + See acquis.d/opnsense.yaml +
    • +
    • + At the moment, the CrowdSec package for OPNsense is fully functional on the + command line but its web interface is limited; you can only list the installed objects and revoke + decisions. For anything else + you need the shell or the CrowdSec Console. +
    • +
    • + Do not enable/start the agent and bouncer services with sysrc or /etc/rc.conf + like you would on vanilla freebsd, the plugin takes care of that. +
    • +
    • + The parsers, scenarios and all plugins from the Hub are periodically upgraded. The + crowdsecurity/freebsd and + crowdsecurity/opnsense + collections are installed by default. +
    • +
    + + + +

    Installation

    + +

    + On the Settings tab, you can expose CrowdSec to the LAN for other servers by changing `LAPI listen address`. + Otherwise, leave the default value. +

    + +

    + Select the first three checkboxes: IDS, LAPI and IPS. Click Apply. If you need to restart, you can do so + from the System > Diagnostics > Services page. +

    + +

    Test the plugin

    + +

    + A quick way to test that everything is working correctly is to + execute the following command. +

    + +

    + Your ssh session should freeze and you should be kicked out from + the firewall. You will not be able to connect to it (from the same + IP address) for two minutes. +

    + +

    + It might be a good idea to have a secondary IP from which you can + connect, should anything go wrong. +

    + +
    [root@OPNsense ~]# cscli decisions add -t ban -d 2m -i <your_ip_address>
    + +

    + This is a more secure way to test than attempting to brute-force + yourself: the default ban period is 4 hours, and Crowdsec reads the + logs from the beginning, so it could ban you even if you failed ssh + login 10 times in 30 seconds two hours before installing it. +

    + + +
    + +
    + +
    + {{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_GeneralSettings'])}} +
    + +
    + +
    +
    +
    diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/machines.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/machines.volt new file mode 100644 index 0000000000..2b4b81d1dc --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/machines.volt @@ -0,0 +1,43 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + + + +
    NameVersionValidated?IP AddressCreatedLast SeenOS
    diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/parsers.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/parsers.volt new file mode 100644 index 0000000000..0ae9c2576c --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/parsers.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
    NameStatusVersionPathDescription
    diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/postoverflows.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/postoverflows.volt new file mode 100644 index 0000000000..9008ff2248 --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/postoverflows.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
    NameStatusVersionPathDescription
    diff --git a/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/scenarios.volt b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/scenarios.volt new file mode 100644 index 0000000000..a42cd7a62d --- /dev/null +++ b/security/crowdsec/src/opnsense/mvc/app/views/OPNsense/CrowdSec/scenarios.volt @@ -0,0 +1,36 @@ +{# SPDX-License-Identifier: MIT #} +{# SPDX-FileCopyrightText: © 2021 CrowdSec #} + + + + + + + + + + + + + + + + + + + +
    NameStatusVersionPathDescription
    diff --git a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh new file mode 100755 index 0000000000..2b6d7731b4 --- /dev/null +++ b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +test -x /usr/local/bin/cscli || exit 0 + +/usr/local/bin/cscli --error -o human hub update >/dev/null + +upgraded=$(/usr/local/bin/cscli --error -o human hub upgrade) + +if [ ! -e "/usr/local/etc/crowdsec/collections/opnsense.yaml" ]; then + /usr/local/bin/cscli --error collections install crowdsecurity/opnsense +fi + +if service crowdsec enabled; then + if ! service crowdsec status >/dev/null 2>&1; then + service crowdsec start >/dev/null 2>&1 || : + else + if [ -n "$upgraded" ]; then + service crowdsec reload >/dev/null 2>&1 || : + fi + fi +fi diff --git a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py new file mode 100755 index 0000000000..6d4869c5b9 --- /dev/null +++ b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +import logging +import json +import subprocess +import urllib.parse +from typing import cast, Any +import yaml + +logging.basicConfig(level=logging.INFO) + + +def is_ipv6(ip: str) -> bool: + return ":" in ip + + +def load_config(filename: str) -> dict[str, Any]: + with open(filename) as fin: + return yaml.safe_load(fin) + + +# only save if some value has changed +def save_config(filename: str, new_config: dict[str, Any]): + old_config = load_config(filename) + if old_config != new_config: + with open(filename, 'w') as fout: + yaml.dump(new_config, fout) + + +def get_netloc(settings: dict[str, str]): + # defaults if config has not been saved yet + listen_address = settings.get('lapi_listen_address', '127.0.0.1') + listen_port = settings.get('lapi_listen_port', '8080') + if is_ipv6(listen_address): + listen_address = '[{}]'.format(listen_address) + return '{}:{}'.format(listen_address, listen_port) + + +def get_new_url(old_url: str, settings: dict[str, str]): + old_tuple = urllib.parse.urlsplit(old_url) + new_tuple = old_tuple._replace(netloc=get_netloc(settings)) + new_url = urllib.parse.urlunsplit(new_tuple) + # client lapi requires a trailing slash for the path part + # and no, query and fragment don't make much sense + if not new_tuple.query and not new_tuple.fragment and not new_url.endswith('/'): + new_url += '/' + return new_url + + +def configure_agent(settings: dict[str, str]): + config_path = '/usr/local/etc/crowdsec/config.yaml' + config = load_config(config_path) + + config['common']['log_dir'] = '/var/log/crowdsec' + config['crowdsec_service']['acquisition_dir'] = '/usr/local/etc/crowdsec/acquis.d/' + config['db_config']['use_wal'] = True + + enable = int(settings.get('agent_enabled', '0')) + config['crowdsec_service']['enable'] = bool(enable) + + if not int(settings.get('lapi_manual_configuration', '0')): + config['api']['server']['listen_uri'] = get_netloc(settings) + + save_config(config_path, config) + + +def configure_lapi(settings: dict[str, str]): + config_path = '/usr/local/etc/crowdsec/config.yaml' + config = load_config(config_path) + + enable = int(settings.get('lapi_enabled', '0')) + config['api']['server']['enable'] = bool(enable) + + save_config(config_path, config) + + +def configure_lapi_credentials(settings: dict[str, str]): + config_path = '/usr/local/etc/crowdsec/local_api_credentials.yaml' + config = load_config(config_path) + + if not int(settings.get('lapi_manual_configuration', '0')): + config['url'] = get_new_url(config['url'], settings) + + save_config(config_path, config) + + +def configure_bouncer(settings: dict[str, str]): + config_path = '/usr/local/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml' + config = load_config(config_path) + + config['log_dir'] = '/var/log/crowdsec' + config['blacklists_ipv4'] = 'crowdsec_blocklists' + config['blacklists_ipv6'] = 'crowdsec6_blocklists' + config['retry_initial_connect'] = True + config['pf'] = {'anchor_name': ''} + + if not int(settings.get('lapi_manual_configuration', '0')): + config['api_url'] = get_new_url(config['api_url'], settings) + + save_config(config_path, config) + + +def enroll(settings: dict[str, str]): + enroll_key = settings.get('enroll_key') + if enroll_key: + try: + p = subprocess.run(['cscli', 'capi', 'status'], check=True, text=True, stdout=subprocess.PIPE) + if "instance is enrolled" in p.stdout: + logging.info("crowdsec instance is already enrolled") + return + except subprocess.CalledProcessError: + return + except Exception as e: + logging.error("could not run command 'cscli' to perform enrollment: %s", e) + + try: + logging.info("enrolling crowdsec instance, please accept the enrollment on https://app.crowdsec.net") + _ = subprocess.run( + ['cscli', 'console', 'enroll', '-e', 'context', enroll_key], + check=True, text=True) + except subprocess.CalledProcessError as e: + logging.error("enrollment failed: %s", e) + return + except Exception as e: + logging.error("could not run command 'cscli' to perform enrollment: %s", e) + + +def main(): + try: + with open('/usr/local/etc/crowdsec/opnsense/settings.json') as f: + settings = cast(dict[str, str], json.load(f)) + except FileNotFoundError: + logging.info("settings.json not found, won't change crowdsec config") + return + + configure_agent(settings) + configure_lapi(settings) + configure_lapi_credentials(settings) + enroll(settings) + configure_bouncer(settings) + + +if __name__ == '__main__': + main() diff --git a/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh new file mode 100755 index 0000000000..9ecdd3f8e8 --- /dev/null +++ b/security/crowdsec/src/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +# This script is run +# - when the plugin is installed (by +POST_INSTALL.post) +# - when saving the "settings" form (which calls /api/crowdsec/service/reconfigure) +# - by hand, running "configctl crowdsec reconfigure" + +set -e + +# apply configuration options specific to opnsense +/usr/local/opnsense/scripts/OPNsense/CrowdSec/reconfigure.py + +/usr/local/sbin/configctl filter reload >/dev/null + +# the hub is upgraded by cron too +/usr/local/opnsense/scripts/OPNsense/CrowdSec/hub-upgrade.sh + +# crowdsec was already restarted by hub-upgrade.sh +if service crowdsec_firewall enabled; then + # have to check status explicitly because "restart" can set $? = 0 even when failing + if ! service crowdsec_firewall status >/dev/null 2>&1; then + service crowdsec_firewall start >/dev/null 2>&1 || : + else + service crowdsec_firewall restart >/dev/null 2>&1 || : + fi +fi + +# left from v0.0.8 +rm -f /usr/local/etc/crowdsec/opnsense-settings.json + +echo "OK" diff --git a/security/crowdsec/src/opnsense/service/conf/actions.d/actions_crowdsec.conf b/security/crowdsec/src/opnsense/service/conf/actions.d/actions_crowdsec.conf new file mode 100644 index 0000000000..020fc4669d --- /dev/null +++ b/security/crowdsec/src/opnsense/service/conf/actions.d/actions_crowdsec.conf @@ -0,0 +1,103 @@ + +# https://docs.opnsense.org/development/backend/configd.html + +[start] +command:/usr/local/etc/rc.d/oscrowdsec start +type: script +message: starting crowdsec services + +[stop] +command:/usr/local/etc/rc.d/oscrowdsec stop +type: script +message: stopping crowdsec services + +[status] +command:/usr/local/etc/rc.d/oscrowdsec status; exit 0 +type: script_output +message: oscrowdsec status + +[restart] +command:/usr/local/etc/rc.d/oscrowdsec restart +type: script +message: restarting crowdsec services + +[reload] +command:/usr/local/etc/rc.d/oscrowdsec reload +type: script +message: reload crowdsec configuration + +[crowdsec-status] +command:/usr/local/etc/rc.d/crowdsec status;exit 0 +type:script_output +message: request crowdsec status + +[crowdsec-firewall-status] +command:/usr/local/etc/rc.d/crowdsec_firewall status;exit 0 +type:script_output +message: request crowdsec_firewall status + +[alerts-list] +command:/usr/local/bin/cscli alerts list -l 0 -o json +type:script_output +message:crowdsec alerts list + +[bouncers-list] +command:/usr/local/bin/cscli bouncers list -o json +type:script_output +message:crowdsec bouncers list + +[decisions-list] +command:/usr/local/bin/cscli decisions list -l 0 -o json +type:script_output +message:crowdsec decisions list + +[decisions-delete] +command:/usr/local/bin/cscli --error decisions delete 2>&1 +parameters:--id %s +type:script_output +message:crowdsec decisions delete + +[collections-list] +command:/usr/local/bin/cscli collections list -o json +type:script_output +message:crowdsec collections list + +[scenarios-list] +command:/usr/local/bin/cscli scenarios list -o json +type:script_output +message:crowdsec scenarios list + +[parsers-list] +command:/usr/local/bin/cscli parsers list -o json +type:script_output +message:crowdsec parsers list + +[postoverflows-list] +command:/usr/local/bin/cscli postoverflows list -o json +type:script_output +message:crowdsec postoverflows list + +[appsec-rules-list] +command:/usr/local/bin/cscli appsec-rules list -o json +type:script_output +message:crowdsec appsec-rules list + +[appsec-configs-list] +command:/usr/local/bin/cscli appsec-configs list -o json +type:script_output +message:crowdsec appsec-configs list + +[machines-list] +command:/usr/local/bin/cscli machines list -o json +type:script_output +message:crowdsec machines list + +[version] +command:/usr/local/bin/cscli version 2>&1 +type:script_output +message:crowdsec version + +[reconfigure] +command:/usr/local/opnsense/scripts/OPNsense/CrowdSec/reconfigure.sh +type:script_output +message:crowdsec reconfigure diff --git a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/+TARGETS b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/+TARGETS new file mode 100644 index 0000000000..c2ebc92240 --- /dev/null +++ b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/+TARGETS @@ -0,0 +1,4 @@ +oscrowdsec.rc.conf.d:/etc/rc.conf.d/oscrowdsec +crowdsec.rc.conf.d:/etc/rc.conf.d/crowdsec +crowdsec_firewall.rc.conf.d:/etc/rc.conf.d/crowdsec_firewall +settings.json:/usr/local/etc/crowdsec/opnsense/settings.json diff --git a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec.rc.conf.d b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec.rc.conf.d new file mode 100644 index 0000000000..de9a90fad4 --- /dev/null +++ b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec.rc.conf.d @@ -0,0 +1,10 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +{% if + (helpers.exists('OPNsense.crowdsec.general.agent_enabled') and OPNsense.crowdsec.general.agent_enabled|default("1") == "1") + or + (helpers.exists('OPNsense.crowdsec.general.lapi_enabled') and OPNsense.crowdsec.general.lapi_enabled|default("1") == "1") +%} +crowdsec_enable="YES" +{% else %} +crowdsec_enable="NO" +{% endif %} diff --git a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec_firewall.rc.conf.d b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec_firewall.rc.conf.d new file mode 100644 index 0000000000..61bae583b4 --- /dev/null +++ b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/crowdsec_firewall.rc.conf.d @@ -0,0 +1,11 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +{% if helpers.exists('OPNsense.crowdsec.general.firewall_bouncer_enabled') and OPNsense.crowdsec.general.firewall_bouncer_enabled|default("1") == "1" %} +crowdsec_firewall_enable="YES" +{% else %} +crowdsec_firewall_enable="NO" +{% endif %} +{% if helpers.exists('OPNsense.crowdsec.general.crowdsec_firewall_verbose') and OPNsense.crowdsec.general.crowdsec_firewall_verbose|default("0") == "1" %} +crowdsec_firewall_flags="-v" +{% else %} +crowdsec_firewall_flags="" +{% endif %} diff --git a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/oscrowdsec.rc.conf.d b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/oscrowdsec.rc.conf.d new file mode 100644 index 0000000000..dd3cec08ea --- /dev/null +++ b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/oscrowdsec.rc.conf.d @@ -0,0 +1 @@ +oscrowdsec_enable="YES" diff --git a/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/settings.json b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/settings.json new file mode 100644 index 0000000000..5b3e0c90e8 --- /dev/null +++ b/security/crowdsec/src/opnsense/service/templates/OPNsense/CrowdSec/settings.json @@ -0,0 +1,3 @@ +{% if helpers.exists('OPNsense.crowdsec.general') -%} + {{ OPNsense.crowdsec.general | tojson }} +{%- endif %} diff --git a/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec-misc.js b/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec-misc.js new file mode 100644 index 0000000000..23468edbd8 --- /dev/null +++ b/security/crowdsec/src/opnsense/www/js/CrowdSec/crowdsec-misc.js @@ -0,0 +1,47 @@ +/* global moment, $ */ +/* exported CrowdSec */ +/* eslint no-undef: "error" */ +/* eslint semi: "error" */ + +const CrowdSec = (function () { + 'use strict'; + + function _humanizeDate(text) { + return moment(text).fromNow(); + } + + const formatters = { + yesno: function(column, row) { + const val = row[column.id]; + if (val) { + return ''; + } else { + return ''; + } + }, + + datetime: function (column, row) { + const val = row[column.id]; + const parsed = moment(val); + if (!val) { + return ''; + } + if (!parsed.isValid()) { + console.error('Cannot parse timestamp: %s', val); + return '???'; + } + return $('
    ') + .attr({ + 'data-toggle': 'tooltip', + 'data-placement': 'left', + title: parsed.format(), + }) + .text(_humanizeDate(val)) + .prop('outerHTML'); + }, + }; + + return { + formatters: formatters, + }; +})(); diff --git a/security/etpro-telemetry/Makefile b/security/etpro-telemetry/Makefile index c40b30bac2..d206a5d1c5 100644 --- a/security/etpro-telemetry/Makefile +++ b/security/etpro-telemetry/Makefile @@ -1,7 +1,9 @@ PLUGIN_NAME= etpro-telemetry -PLUGIN_VERSION= 1.6 +PLUGIN_VERSION= 1.8 PLUGIN_COMMENT= ET Pro Telemetry Edition PLUGIN_MAINTAINER= ad@opnsense.org +PLUGIN_DEPENDS= py${PLUGIN_PYTHON}-netaddr PLUGIN_WWW= https://docs.opnsense.org/manual/etpro_telemetry.html +PLUGIN_TIER= 2 .include "../../Mk/plugins.mk" diff --git a/security/etpro-telemetry/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/ProofpointEtController.php b/security/etpro-telemetry/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/ProofpointEtController.php index 5ac5edf8dd..a0f306ad7e 100644 --- a/security/etpro-telemetry/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/ProofpointEtController.php +++ b/security/etpro-telemetry/src/opnsense/mvc/app/controllers/OPNsense/Diagnostics/Api/ProofpointEtController.php @@ -43,7 +43,6 @@ class ProofpointEtController extends ApiControllerBase */ public function statusAction() { - $this->sessionClose(); $backend = new Backend(); $response = $backend->configdRun('proofpoint et status'); $activity = json_decode($response, true); diff --git a/security/etpro-telemetry/src/opnsense/mvc/app/models/OPNsense/ProofpointET/ACL/ACL.xml b/security/etpro-telemetry/src/opnsense/mvc/app/models/OPNsense/ProofpointET/ACL/ACL.xml new file mode 100644 index 0000000000..7233fdbd04 --- /dev/null +++ b/security/etpro-telemetry/src/opnsense/mvc/app/models/OPNsense/ProofpointET/ACL/ACL.xml @@ -0,0 +1,8 @@ + + + Dashboard: ET Pro Telemetry widget + + api/diagnostics/proofpoint_et/status + + + diff --git a/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/send_telemetry.py b/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/send_telemetry.py index dea1240b7d..1604274b02 100755 --- a/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/send_telemetry.py +++ b/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/send_telemetry.py @@ -78,7 +78,7 @@ # data collected, log and push if row_count > 0 and max_timestamp is not None: syslog.syslog( - syslog.LOG_NOTICE, + syslog.LOG_DEBUG, 'telemetry data collected %d records in %.2f seconds @%s' % ( row_count, time.time() - send_start_time, max_timestamp ) diff --git a/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/sensor_info.py b/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/sensor_info.py index 9cec122d0c..762fa70d90 100755 --- a/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/sensor_info.py +++ b/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/sensor_info.py @@ -45,7 +45,7 @@ cnf = telemetry.get_config(args.config) if cnf.token is not None: try: - req = requests.get(args.endpoint, headers={'Authorization': 'Bearer %s' % cnf.token}) + req = requests.get(args.endpoint, headers={'Authorization': 'Bearer %s' % cnf.token}, verify=not args.insecure) if req.status_code == 200: response = ujson.loads(req.text) response['status'] = 'ok' diff --git a/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/telemetry/log.py b/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/telemetry/log.py index 2afb91cf68..50a88c73cf 100755 --- a/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/telemetry/log.py +++ b/security/etpro-telemetry/src/opnsense/scripts/etpro_telemetry/telemetry/log.py @@ -36,7 +36,7 @@ def reverse_log_reader(filename): :return: generator """ block_size = 81920 - input_stream = open(filename, 'rU') + input_stream = open(filename, 'r') input_stream.seek(0, os.SEEK_END) file_byte_start = input_stream.tell() diff --git a/security/etpro-telemetry/src/opnsense/scripts/suricata/metadata/rules/et-telemetry.xml b/security/etpro-telemetry/src/opnsense/scripts/suricata/metadata/rules/et-telemetry.xml index d5e0324c25..f221080542 100644 --- a/security/etpro-telemetry/src/opnsense/scripts/suricata/metadata/rules/et-telemetry.xml +++ b/security/etpro-telemetry/src/opnsense/scripts/suricata/metadata/rules/et-telemetry.xml @@ -7,8 +7,7 @@ telemetry_sids.txt - telemetry_version.json - botcc.portgrouped.rules + telemetry_version.json botcc.rules ciarmy.rules compromised.rules @@ -17,6 +16,7 @@ emerging-activex.rules emerging-adware_pup.rules emerging-attack_response.rules + emerging-botcc_portgrouped.rules emerging-chat.rules emerging-coinminer.rules emerging-current_events.rules diff --git a/security/etpro-telemetry/src/opnsense/www/js/widgets/ETProTelemetry.js b/security/etpro-telemetry/src/opnsense/www/js/widgets/ETProTelemetry.js new file mode 100644 index 0000000000..046736bc75 --- /dev/null +++ b/security/etpro-telemetry/src/opnsense/www/js/widgets/ETProTelemetry.js @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 Deciso B.V. + * 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. + */ + +export default class ETProTelemetry extends BaseTableWidget { + constructor() { + super(); + this.tickTimeout = 3600; + } + + getMarkup() { + let $container = $('
    '); + let $ETProTelemetrytable = this.createTable('ETProTelemetry-table', { + headerPosition: 'left', + }); + $container.append($ETProTelemetrytable); + return $container; + } + + async onWidgetTick() { + const data = await this.ajaxCall('/api/diagnostics/proofpoint_et/status'); + if (data['sensor_status'].length) { + $('#etpro_sensor_status').text(data['sensor_status']); + $('#etpro_event_received').text(data['event_received']); + $('#etpro_last_rule_download').text(data['last_rule_download']); + $('#etpro_last_heartbeat').text(data['last_heartbeat']); + } else { + $('#etpro_sensor_status').text('-'); + $('#etpro_event_received').text('-'); + $('#etpro_last_rule_download').text('-'); + $('#etpro_last_heartbeat').text('-'); + } + } + + async onMarkupRendered() { + let rows = []; + rows.push([[''], '']); + rows.push([[this.translations['sensor_status']], $('').prop('outerHTML')]); + rows.push([[this.translations['event_received']], $('').prop('outerHTML')]); + rows.push([[this.translations['last_rule_download']], $('').prop('outerHTML')]); + rows.push([[this.translations['last_heartbeat']], $('').prop('outerHTML')]); + + super.updateTable('ETProTelemetry-table', rows); + } +} diff --git a/security/etpro-telemetry/src/opnsense/www/js/widgets/Metadata/etpro-telemetry.xml b/security/etpro-telemetry/src/opnsense/www/js/widgets/Metadata/etpro-telemetry.xml new file mode 100644 index 0000000000..5e8949977f --- /dev/null +++ b/security/etpro-telemetry/src/opnsense/www/js/widgets/Metadata/etpro-telemetry.xml @@ -0,0 +1,15 @@ + + + ETProTelemetry.js + + /api/diagnostics/proofpoint_et/status + + + Telemetry status + Status + Last event + Last rule download + Last heartbeat + + + diff --git a/security/etpro-telemetry/src/www/widgets/include/proofpoint_et.inc b/security/etpro-telemetry/src/www/widgets/include/proofpoint_et.inc deleted file mode 100644 index abf77c3bc4..0000000000 --- a/security/etpro-telemetry/src/www/widgets/include/proofpoint_et.inc +++ /dev/null @@ -1,2 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    -
    - - docs.opnsense.org -
    diff --git a/security/intrusion-detection-content-et-open/Makefile b/security/intrusion-detection-content-et-open/Makefile index cb93113e2b..e71a634738 100644 --- a/security/intrusion-detection-content-et-open/Makefile +++ b/security/intrusion-detection-content-et-open/Makefile @@ -1,7 +1,7 @@ PLUGIN_NAME= intrusion-detection-content-et-open -PLUGIN_VERSION= 1.0.1 -#PLUGIN_REVISION= 1 -PLUGIN_COMMENT= IDS Proofpoint ET open ruleset complementary subset for ET Pro Telemetry edition +PLUGIN_VERSION= 1.0.2 +PLUGIN_REVISION= 2 +PLUGIN_COMMENT= IDS Proofpoint full ET open ruleset complementary subset for ET Pro Telemetry edition PLUGIN_MAINTAINER= ad@opnsense.org PLUGIN_WWW= https://rules.emergingthreats.net/ diff --git a/security/intrusion-detection-content-et-open/pkg-descr b/security/intrusion-detection-content-et-open/pkg-descr index 7065fc3ed4..5b7ad62ece 100644 --- a/security/intrusion-detection-content-et-open/pkg-descr +++ b/security/intrusion-detection-content-et-open/pkg-descr @@ -1,5 +1,6 @@ -IDS Proofpoint ET open ruleset duplicates rule files which are being -delivered empty in ET Pro Telemetry edition so both can be installed. +IDS Proofpoint ET open full ruleset to complement ET Pro Telemetry edition. +This plugin will trigger duplicate rules warnings in Suricata logs when +selecting the same categories for both ET open and ET Telemetry. LICENSE: https://www.proofpoint.com/us/license WWW: https://www.proofpoint.com/us/blog/threat-insight diff --git a/security/intrusion-detection-content-et-open/src/opnsense/scripts/suricata/metadata/rules/et-open-extra.xml b/security/intrusion-detection-content-et-open/src/opnsense/scripts/suricata/metadata/rules/et-open-extra.xml index ca6f397f64..10c7a0f9ab 100644 --- a/security/intrusion-detection-content-et-open/src/opnsense/scripts/suricata/metadata/rules/et-open-extra.xml +++ b/security/intrusion-detection-content-et-open/src/opnsense/scripts/suricata/metadata/rules/et-open-extra.xml @@ -1,15 +1,59 @@ - - - + + + - et_open-botcc.portgrouped.rules + 3coresec.rules + botcc.portgrouped.rules et_open.botcc.rules et_open.ciarmy.rules et_open.compromised.rules et_open.drop.rules et_open.dshield.rules - et_open.tor.rules + et_open.emerging-activex.rules + et_open.emerging-adware_pup.rules + et_open.emerging-attack_response.rules + et_open.emerging-chat.rules + et_open.emerging-coinminer.rules + et_open.emerging-current_events.rules + et_open.emerging-deleted.rules + et_open.emerging-dns.rules + et_open.emerging-dos.rules + et_open.emerging-exploit.rules + et_open.emerging-exploit_kit.rules + et_open.emerging-ftp.rules + et_open.emerging-games.rules + et_open.emerging-hunting.rules + et_open.emerging-icmp.rules + et_open.emerging-icmp_info.rules + et_open.emerging-imap.rules et_open.emerging-inappropriate.rules + et_open.emerging-info.rules + et_open.emerging-ja3.rules + et_open.emerging-malware.rules + et_open.emerging-misc.rules + et_open.emerging-mobile_malware.rules + et_open.emerging-netbios.rules + et_open.emerging-p2p.rules + et_open.emerging-phishing.rules + et_open.emerging-policy.rules + et_open.emerging-pop3.rules + et_open.emerging-rpc.rules + et_open.emerging-scada.rules + et_open.emerging-scan.rules + et_open.emerging-shellcode.rules + et_open.emerging-smtp.rules + et_open.emerging-snmp.rules + et_open.emerging-sql.rules + et_open.emerging-telnet.rules + et_open.emerging-tftp.rules + et_open.emerging-user_agents.rules + et_open.emerging-voip.rules + et_open.emerging-web_client.rules + et_open.emerging-web_server.rules + et_open.emerging-web_specific_apps.rules + et_open.emerging-worm.rules + et_open.tor.rules + threatview_CS_c2.rules diff --git a/security/intrusion-detection-content-et-pro/src/opnsense/scripts/suricata/metadata/rules/et-pro.xml b/security/intrusion-detection-content-et-pro/src/opnsense/scripts/suricata/metadata/rules/et-pro.xml index 4accb4f376..715b749bb3 100644 --- a/security/intrusion-detection-content-et-pro/src/opnsense/scripts/suricata/metadata/rules/et-pro.xml +++ b/security/intrusion-detection-content-et-pro/src/opnsense/scripts/suricata/metadata/rules/et-pro.xml @@ -1,5 +1,5 @@ - + diff --git a/security/intrusion-detection-content-pt-open/LICENSE b/security/intrusion-detection-content-pt-open/LICENSE new file mode 100644 index 0000000000..2274378454 --- /dev/null +++ b/security/intrusion-detection-content-pt-open/LICENSE @@ -0,0 +1,24 @@ +(C) 2024 JSC Positive Technologies. All rights reserved. + +Definitions + +“Program” refers to any copyrightable work (including rule sets for open source network threat detection engine Suricata) and associated documentation files licensed under this License, accessible at: https://rules.ptsecurity.com “License” means the terms of this license agreement which apply to the Program. +“Licensee” refers to individuals or legal entities accessing and/or using the Program. +“Modify” a work (part of the work) means to make any change, including translation of the Program from one language into another, except for adaptation. +“Copyright holder” means JSС Positive Technologies as the holder of the exclusive right to the Program. + +Legal Usage + +The Licensee is hereby granted free of charge the rights to use, copy, publish, distribute, sublicense, and/or sell copies of the Program for non-commercial and commercial use subject to the following conditions: +· The above copyright notice shall be included in all copies or substantial portions of the Program. +· Neither the name of the Copyright holder nor the names of its contributors may be used to endorse or promote programs in which the Program was integrated without specific prior written permission. +· Redistributions of the Program must retain the above copyright notice and the full text of the License. +No permission is hereby granted to the Licensee to modify the Program and distribute the modified Program. However, for the avoidance of doubt, the Licensee is granted the right to integrate the original Program into other programs and distribute such programs. + +Applicable law + +This License is governed by the laws of the Russian Federation. The rules of the article 1286.1 of the Civil Code of the Russian Federation are applicable to this License. + +Disclaimer + +THIS PROGRAM IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS”. UNDER NO CIRCUMSTANCES THE COPYRIGHT HOLDER IS LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES RESULTING FROM (I) THE LICENSEE'S USE OF THE PROGRAM; (II) THE LICENSEE'S INTERPRETATION AND APPLICATION OF ANY FILES, METHODS, OR ANY OTHER INFORMATION PROVIDED ON OR THROUGH THE PROGRAM; (III) THE FAILURE OF THE PROGRAM TO MEET THE LICENSEE'S EXPECTATIONS. IF, NOTWITHSTANDING THE OTHER PROVISIONS OF THIS LISENCE, THE COPYRIGHT HOLDER IS FORCED TO BEAR RESPONSIBILITY TO THE LICENSEE FOR ANY LOSSES RELATED TO THE LICENSEE'S USE OF THE PROGRAM, THE COPYRIGHT HOLDER’S LIABILITY SHALL IN NO CASE EXCEED THE EQUIVALENT OF 10 (TEN) U.S. DOLLARS. diff --git a/security/intrusion-detection-content-pt-open/Makefile b/security/intrusion-detection-content-pt-open/Makefile index 255baf5fe9..5190a1fd90 100644 --- a/security/intrusion-detection-content-pt-open/Makefile +++ b/security/intrusion-detection-content-pt-open/Makefile @@ -1,8 +1,7 @@ -PLUGIN_NAME= intrusion-detection-content-pt-open +PLUGIN_NAME= intrusion-detection-content-ptopen PLUGIN_VERSION= 1.0 -PLUGIN_REVISION= 1 -PLUGIN_COMMENT= IDS PT Research ruleset (only for non-commercial use) -PLUGIN_MAINTAINER= ad@opnsense.org -PLUGIN_WWW= https://www.ptsecurity.com/ww-en/ +PLUGIN_COMMENT= IDS Positive Technologies ESC ruleset +PLUGIN_MAINTAINER= kulikov.a@gmail.com +PLUGIN_WWW= https://rules.ptsecurity.com .include "../../Mk/plugins.mk" diff --git a/security/intrusion-detection-content-pt-open/pkg-descr b/security/intrusion-detection-content-pt-open/pkg-descr index 27eb4adcf5..ff3dc89206 100644 --- a/security/intrusion-detection-content-pt-open/pkg-descr +++ b/security/intrusion-detection-content-pt-open/pkg-descr @@ -1,3 +1,13 @@ -The Attack Detection Team searches for new vulnerabilities and 0-days, -reproduces it and creates PoC exploits to understand how these security -flaws work and how related attacks can be detected on the network layer. +IDS PT ESC open ruleset designed to detect a variety of network threats, +including those communicated under TLS. + +PT Rules is an open-source project focused on enhancing network security +through proactive threat detection. As the PT Expert Security Center attack +detection team, we are a dedicated group of cybersecurity experts committed +to improve network security through open-source initiatives. + +Don't forget to define the $DC_SERVERS rule-variable if you want to use the +protection rules against DCShadow/DCSync attacks. + +LICENSE: https://rules.ptsecurity.com/view/LICENSE.txt +WWW: https://rules.ptsecurity.com/ diff --git a/security/intrusion-detection-content-pt-open/pkg-plist b/security/intrusion-detection-content-pt-open/pkg-plist deleted file mode 100644 index 3091d0d167..0000000000 --- a/security/intrusion-detection-content-pt-open/pkg-plist +++ /dev/null @@ -1,9 +0,0 @@ -The Attack Detection Team searches for new vulnerabilities and 0-days, -reproduces it and creates PoC exploits to understand how these security -flaws work and how related attacks can be detected on the network layer. -Additionally, we are interested in malware and hackers’ TTPs, so we -develop Suricata rules for detecting all sorts of such activities. - -LICENSE: https://github.com/ptresearch/AttackDetection/blob/master/LICENSE - -WWW: https://github.com/ptresearch/AttackDetection/ diff --git a/security/intrusion-detection-content-pt-open/src/opnsense/scripts/suricata/metadata/rules/pt-open.xml b/security/intrusion-detection-content-pt-open/src/opnsense/scripts/suricata/metadata/rules/pt-open.xml new file mode 100644 index 0000000000..632c0bdf18 --- /dev/null +++ b/security/intrusion-detection-content-pt-open/src/opnsense/scripts/suricata/metadata/rules/pt-open.xml @@ -0,0 +1,11 @@ + + + + + ptopen-attacks.rules + ptopen-info.rules + ptopen-malware.rules + ptopen-tools.rules + ptopen-windows.rules + + diff --git a/security/intrusion-detection-content-pt-open/src/opnsense/scripts/suricata/metadata/rules/pt-research.xml b/security/intrusion-detection-content-pt-open/src/opnsense/scripts/suricata/metadata/rules/pt-research.xml deleted file mode 100644 index 1e1b21b65f..0000000000 --- a/security/intrusion-detection-content-pt-open/src/opnsense/scripts/suricata/metadata/rules/pt-research.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - pt.research.rules - - diff --git a/security/intrusion-detection-content-snort-vrt/Makefile b/security/intrusion-detection-content-snort-vrt/Makefile index a2f900e6b7..d69fc583f8 100644 --- a/security/intrusion-detection-content-snort-vrt/Makefile +++ b/security/intrusion-detection-content-snort-vrt/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= intrusion-detection-content-snort-vrt -PLUGIN_VERSION= 1.1 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.2 PLUGIN_COMMENT= IDS Snort VRT ruleset (needs registration or subscription) PLUGIN_MAINTAINER= ad@opnsense.org PLUGIN_WWW= https://www.snort.org/downloads#rules diff --git a/security/intrusion-detection-content-snort-vrt/src/opnsense/scripts/suricata/metadata/rules/snort-vrt.xml b/security/intrusion-detection-content-snort-vrt/src/opnsense/scripts/suricata/metadata/rules/snort-vrt.xml index 699ef07494..caca730d05 100644 --- a/security/intrusion-detection-content-snort-vrt/src/opnsense/scripts/suricata/metadata/rules/snort-vrt.xml +++ b/security/intrusion-detection-content-snort-vrt/src/opnsense/scripts/suricata/metadata/rules/snort-vrt.xml @@ -123,6 +123,6 @@ - + diff --git a/security/maltrail/Makefile b/security/maltrail/Makefile index 04126919b3..5fe41603d0 100644 --- a/security/maltrail/Makefile +++ b/security/maltrail/Makefile @@ -1,5 +1,6 @@ PLUGIN_NAME= maltrail -PLUGIN_VERSION= 1.8 +PLUGIN_VERSION= 1.10 +PLUGIN_REVISION= 1 PLUGIN_COMMENT= Malicious traffic detection system PLUGIN_DEPENDS= maltrail PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/security/maltrail/pkg-descr b/security/maltrail/pkg-descr index 0bb1e62876..cbc444bf12 100644 --- a/security/maltrail/pkg-descr +++ b/security/maltrail/pkg-descr @@ -11,6 +11,15 @@ WWW: https://github.com/stamparm/maltrail Changelog --------- +1.10 + +* Add CHECK_HOST_DOMAINS option +* Fix CUSTOM_TRAILS_DIR (contributed by codiflow) + +1.9 + +* Remove MFS support for /var/log/ + 1.8 * Add firewall alias "BlocklistMaltrail" that points to the built-in ip block list @@ -26,7 +35,7 @@ Changelog 1.5 -* Change whitelisting format (by @jkellerer) +* Change whitelisting format (contributed by jkellerer) * Add alienvault to disabled feeds 1.4 diff --git a/security/maltrail/src/etc/rc.d/opnsense-maltrailsensor b/security/maltrail/src/etc/rc.d/opnsense-maltrailsensor index b064c35b4d..70788ea780 100755 --- a/security/maltrail/src/etc/rc.d/opnsense-maltrailsensor +++ b/security/maltrail/src/etc/rc.d/opnsense-maltrailsensor @@ -1,11 +1,8 @@ #!/bin/sh # -# $FreeBSD$ -# # PROVIDE: opnsense-maltrailsensor # REQUIRE: SERVERS # KEYWORD: shutdown -# . /etc/rc.subr diff --git a/security/maltrail/src/etc/rc.d/opnsense-maltrailserver b/security/maltrail/src/etc/rc.d/opnsense-maltrailserver index aa3de2883a..c32d698108 100755 --- a/security/maltrail/src/etc/rc.d/opnsense-maltrailserver +++ b/security/maltrail/src/etc/rc.d/opnsense-maltrailserver @@ -1,11 +1,8 @@ #!/bin/sh # -# $FreeBSD$ -# # PROVIDE: opnsense-maltrailserver # REQUIRE: SERVERS # KEYWORD: shutdown -# . /etc/rc.subr diff --git a/security/maltrail/src/opnsense/mvc/app/controllers/OPNsense/Maltrail/forms/general.xml b/security/maltrail/src/opnsense/mvc/app/controllers/OPNsense/Maltrail/forms/general.xml index 93f644561e..317840e712 100644 --- a/security/maltrail/src/opnsense/mvc/app/controllers/OPNsense/Maltrail/forms/general.xml +++ b/security/maltrail/src/opnsense/mvc/app/controllers/OPNsense/Maltrail/forms/general.xml @@ -5,6 +5,12 @@ checkbox Whether to enable or disable the usage or heuristic detection. + + general.checkhostheader + + checkbox + Check values in Host header (along with standard non-HTTP checks) for malicious DNS trails. + general.updateperiod diff --git a/security/maltrail/src/opnsense/mvc/app/controllers/OPNsense/Maltrail/forms/sensor.xml b/security/maltrail/src/opnsense/mvc/app/controllers/OPNsense/Maltrail/forms/sensor.xml index 754a3b3922..62cc353c4e 100644 --- a/security/maltrail/src/opnsense/mvc/app/controllers/OPNsense/Maltrail/forms/sensor.xml +++ b/security/maltrail/src/opnsense/mvc/app/controllers/OPNsense/Maltrail/forms/sensor.xml @@ -21,13 +21,13 @@ sensor.remoteserver text - IP address of the remote logging server. + IP address of the remote logging server. This is necessary if you are using a centralized Maltrail instance, otherwise leave it blank to use this instance as the logging server. sensor.remoteport text - Port of the logging server. Leave empty when sensor and server run on the same system. + Port of the remote logging server. sensor.syslogserver @@ -39,6 +39,6 @@ sensor.syslogport text - Port of the syslog server. + Port of the remote syslog server. diff --git a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/General.xml b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/General.xml index 4fbcc3cca4..d789be99b8 100644 --- a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/General.xml +++ b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/General.xml @@ -1,22 +1,26 @@ //OPNsense/maltrail/general Maltrail general configuration - 0.0.1 + 0.0.2 - 1 + 1 Y + + 0 + Y + - 86400 + 86400 Y - 9ab3cd9d67bf49d01f6a2e33d0bd9bc804ddbe6ce1ff5d219c42624851db5dbc + 9ab3cd9d67bf49d01f6a2e33d0bd9bc804ddbe6ce1ff5d219c42624851db5dbc Y - Y + Y N diff --git a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Sensor.xml b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Sensor.xml index 4a97c5757d..54512111fa 100644 --- a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Sensor.xml +++ b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Sensor.xml @@ -4,11 +4,11 @@ 0.0.3 - 0 + 0 Y - 0 + 0 Y @@ -21,14 +21,14 @@ N - 8337 + 8337 Y N - 514 + 514 Y diff --git a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Server.xml b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Server.xml index 94df269c2c..a9db942855 100644 --- a/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Server.xml +++ b/security/maltrail/src/opnsense/mvc/app/models/OPNsense/Maltrail/Server.xml @@ -4,20 +4,20 @@ 0.0.2 - 0 + 0 Y - 0 + 0 Y - 0.0.0.0 + 0.0.0.0 Y Please provide a valid hostname or IP address. - 8338 + 8338 Y diff --git a/security/maltrail/src/opnsense/service/conf/actions.d/actions_maltrailsensor.conf b/security/maltrail/src/opnsense/service/conf/actions.d/actions_maltrailsensor.conf index 51b2c66536..530780446c 100644 --- a/security/maltrail/src/opnsense/service/conf/actions.d/actions_maltrailsensor.conf +++ b/security/maltrail/src/opnsense/service/conf/actions.d/actions_maltrailsensor.conf @@ -1,5 +1,5 @@ [start] -command:/usr/local/opnsense/scripts/OPNsense/Maltrail/setup.sh;/usr/local/etc/rc.d/opnsense-maltrailsensor start +command:/usr/local/etc/rc.d/opnsense-maltrailsensor start parameters: type:script message:starting Maltrail sensor @@ -11,14 +11,14 @@ type:script message:stopping Maltrail sensor [restart] -command:/usr/local/opnsense/scripts/OPNsense/Maltrail/setup.sh;/usr/local/etc/rc.d/opnsense-maltrailsensor restart +command:/usr/local/etc/rc.d/opnsense-maltrailsensor restart parameters: type:script message:restarting Maltrail sensor description:Restart Maltrail sensor [status] -command:/usr/local/etc/rc.d/opnsense-maltrailsensor status;exit 0 +command:/usr/local/etc/rc.d/opnsense-maltrailsensor status; exit 0 parameters: type:script_output message:request Maltrail sensor status diff --git a/security/maltrail/src/opnsense/service/conf/actions.d/actions_maltrailserver.conf b/security/maltrail/src/opnsense/service/conf/actions.d/actions_maltrailserver.conf index ad8313f6f3..1cd65664b2 100644 --- a/security/maltrail/src/opnsense/service/conf/actions.d/actions_maltrailserver.conf +++ b/security/maltrail/src/opnsense/service/conf/actions.d/actions_maltrailserver.conf @@ -1,5 +1,5 @@ [start] -command:/usr/local/opnsense/scripts/OPNsense/Maltrail/setup.sh;/usr/local/etc/rc.d/opnsense-maltrailserver start +command:/usr/local/etc/rc.d/opnsense-maltrailserver start parameters: type:script message:starting Maltrail Server @@ -11,13 +11,13 @@ type:script message:stopping Maltrail Server [restart] -command:/usr/local/opnsense/scripts/OPNsense/Maltrail/setup.sh;/usr/local/etc/rc.d/opnsense-maltrailserver restart +command:/usr/local/etc/rc.d/opnsense-maltrailserver restart parameters: type:script message:restarting Maltrail Server [status] -command:/usr/local/etc/rc.d/opnsense-maltrailserver status;exit 0 +command:/usr/local/etc/rc.d/opnsense-maltrailserver status; exit 0 parameters: type:script_output message:request Maltrail Server status diff --git a/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrail.conf b/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrail.conf index 5e107c94ca..33a2170c48 100644 --- a/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrail.conf +++ b/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrail.conf @@ -36,7 +36,7 @@ SYSLOG_SERVER {{ OPNsense.maltrail.sensor.syslogserver }}:{{ OPNsense.maltrail.s {% endif %} SENSOR_NAME $HOSTNAME -CUSTOM_TRAILS_DIR /usr/local/maltrail/trails/custom/ +CUSTOM_TRAILS_DIR /usr/local/share/maltrail/trails/custom/ PROCESS_COUNT $CPU_CORES DISABLE_CPU_AFFINITY false USE_FEED_UPDATES true @@ -48,6 +48,11 @@ USE_HEURISTICS true {% else %} USE_HEURISTICS false {% endif %} +{% if helpers.exists('OPNsense.maltrail.general.checkhostheader') and OPNsense.maltrail.general.checkhostheader == '1' %} +CHECK_HOST_DOMAINS true +{% else %} +CHECK_HOST_DOMAINS false +{% endif %} CHECK_MISSING_HOST false {% if helpers.exists('OPNsense.maltrail.general.whitelist') and OPNsense.maltrail.general.whitelist != '' %} USER_WHITELIST /usr/local/share/maltrail/misc/user_whitelist.txt diff --git a/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrailsensor b/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrailsensor index 0f1dc4832f..f8af7a52e4 100644 --- a/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrailsensor +++ b/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrailsensor @@ -1,7 +1,6 @@ {% if helpers.exists('OPNsense.maltrail.sensor.enabled') and OPNsense.maltrail.sensor.enabled == '1' %} -maltrailsensor_var_script="/usr/local/opnsense/scripts/OPNsense/Maltrail/setup.sh" +maltrailsensor_setup="/usr/local/opnsense/scripts/OPNsense/Maltrail/setup.sh" maltrailsensor_enable="YES" {% else %} maltrailsensor_enable="NO" {% endif %} -maltrailsensor_var_mfs="/var/log/maltrail" diff --git a/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrailserver b/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrailserver index 12095bdd28..9ddea01c62 100644 --- a/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrailserver +++ b/security/maltrail/src/opnsense/service/templates/OPNsense/Maltrail/maltrailserver @@ -1,7 +1,6 @@ {% if helpers.exists('OPNsense.maltrail.server.enabled') and OPNsense.maltrail.server.enabled == '1' %} -maltrailserver_var_script="/usr/local/opnsense/scripts/OPNsense/Maltrail/setup.sh" +maltrailserver_setup="/usr/local/opnsense/scripts/OPNsense/Maltrail/setup.sh" maltrailserver_enable="YES" {% else %} maltrailserver_enable="NO" {% endif %} -maltrailserver_var_mfs="/var/log/maltrail" diff --git a/security/netbird/Makefile b/security/netbird/Makefile new file mode 100644 index 0000000000..94edc0d4f2 --- /dev/null +++ b/security/netbird/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= netbird +PLUGIN_VERSION= 1.1 +PLUGIN_DEPENDS= netbird +PLUGIN_COMMENT= Peer-to-peer VPN that seamlessly connects your devices +PLUGIN_MAINTAINER= dev@netbird.io +PLUGIN_WWW= https://netbird.io + +.include "../../Mk/plugins.mk" diff --git a/security/netbird/pkg-descr b/security/netbird/pkg-descr new file mode 100644 index 0000000000..e3c155b98d --- /dev/null +++ b/security/netbird/pkg-descr @@ -0,0 +1,18 @@ +NetBird is an open-source WireGuard-based overlay network combined with +Zero Trust Network Access, providing secure and reliable connectivity +to internal resources. + +Key features: +- Zero-config VPN: Easily create secure connections between devices without +manual network setup. +- Built on WireGuard: Leverages WireGuard's high-performance encryption for +fast and secure communication. +- Self-hosted or Cloud-managed: Users can deploy their own NetBird management +server or use NetBird Cloud for centralized control. +- Access Control & Routing: Fine-grained access control policies and automatic +network routing simplify connectivity. +- This FreeBSD port provides the NetBird client daemon and CLI tools, allowing +FreeBSD systems to join a NetBird mesh network and securely communicate with +other peers. + +For more details, visit: https://netbird.io diff --git a/security/netbird/src/etc/inc/plugins.inc.d/netbird.inc b/security/netbird/src/etc/inc/plugins.inc.d/netbird.inc new file mode 100644 index 0000000000..1d6b7fce09 --- /dev/null +++ b/security/netbird/src/etc/inc/plugins.inc.d/netbird.inc @@ -0,0 +1,70 @@ +general->enable->isEmpty(); +} + +function netbird_services() +{ + $services = []; + if (!netbird_enabled()) { + return $services; + } + + $services[] = [ + 'description' => gettext('NetBird'), + 'configd' => [ + 'restart' => ['netbird restart'], + 'start' => ['netbird start'], + 'stop' => ['netbird stop'], + ], + 'name' => 'netbird', + 'pidfile' => '/var/run/netbird.pid', + ]; + + return $services; +} + +function netbird_configure() +{ + return [ + 'netbird_sync_config' => ['netbird_configure_do'] + ]; +} + +function netbird_configure_do($verbose = false) +{ + service_log('Sync NetBird config...', $verbose); + (new \OPNsense\Netbird\Settings())->syncConfig(); + service_log("done.\n", $verbose); +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/AuthenticationController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/AuthenticationController.php new file mode 100644 index 0000000000..4691c3668d --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/AuthenticationController.php @@ -0,0 +1,102 @@ +managementUrl->__toString(); + $setupKey = $mdl->setupKey->__toString(); + + $defaultKey = '00000000-0000-0000-0000-000000000000'; + if (!empty($setupKey) && $setupKey !== $defaultKey) { + $visiblePart = substr($setupKey, 0, 4); + $maskedKey = $visiblePart . str_repeat('*', max(4, strlen($setupKey) - 4)); + } else { + $maskedKey = $defaultKey; + } + + return [ + 'authentication' => [ + 'managementUrl' => $managementUrl, + 'setupKey' => $maskedKey + ] + ]; + } + + public function upAction() + { + $backend = new Backend(); + $mdl = new Authentication(); + + $status = json_decode($backend->configdRun("netbird status-json"), true); + $connected = $status['management']['connected'] ?? false; + + if (json_last_error() === JSON_ERROR_NONE && $connected === true) { + $backend->configdRun("netbird down"); + } + + $managementUrl = $mdl->managementUrl->__toString(); + $setupKey = $mdl->setupKey->__toString(); + + $result = $backend->configdpRun("netbird up-setup-key", array($managementUrl, $setupKey)); + return ['result' => trim($result)]; + } + + public function downAction(): array + { + $backend = new Backend(); + + $status = json_decode($backend->configdRun("netbird status-json"), true); + $connected = $status['management']['connected'] ?? false; + + if (json_last_error() === JSON_ERROR_NONE && $connected === true) { + $result = $backend->configdRun("netbird down"); + return ['result' => trim($result)]; + } + return ['result' => 'already disconnected or not running']; + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/ServiceController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/ServiceController.php new file mode 100644 index 0000000000..df7000d121 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/ServiceController.php @@ -0,0 +1,43 @@ +configdRun("netbird sync-config"); + if (stripos($result, 'done') === false) { + return [ + 'result' => 'failed to sync config: ' . $result, + ]; + } + + $status = json_decode($backend->configdRun("netbird status-json"), true); + $connected = $status['management']['connected'] ?? false; + if (json_last_error() === JSON_ERROR_NONE && $connected === true) { + $backend->configdRun("netbird down"); + $backend->configdRun("netbird up"); + } + + return ['result' => 'synced']; + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/StatusController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/StatusController.php new file mode 100644 index 0000000000..7a91116e8a --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/Api/StatusController.php @@ -0,0 +1,55 @@ +configdRun("netbird status-json"), true); + if (json_last_error() === JSON_ERROR_NONE && is_array($status)) { + return $status; + } + return []; + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/AuthenticationController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/AuthenticationController.php new file mode 100644 index 0000000000..bc8790f2ad --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/AuthenticationController.php @@ -0,0 +1,42 @@ +view->authenticationForm = $this->getForm("authentication"); + $this->view->pick('OPNsense/Netbird/authentication'); + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/SettingsController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/SettingsController.php new file mode 100644 index 0000000000..b3677b198c --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/SettingsController.php @@ -0,0 +1,42 @@ +view->settingsForm = $this->getForm("settings"); + $this->view->pick('OPNsense/Netbird/settings'); + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/StatusController.php b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/StatusController.php new file mode 100644 index 0000000000..55797b515e --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/StatusController.php @@ -0,0 +1,41 @@ +view->pick('OPNsense/Netbird/status'); + } +} diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/authentication.xml b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/authentication.xml new file mode 100644 index 0000000000..75111fdaf3 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/authentication.xml @@ -0,0 +1,14 @@ +
    + + authentication.managementUrl + + text + Base URL of the management service + + + authentication.setupKey + + text + Set the authentication setup key + +
    diff --git a/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/settings.xml b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/settings.xml new file mode 100644 index 0000000000..2f5f4224fc --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/controllers/OPNsense/Netbird/forms/settings.xml @@ -0,0 +1,103 @@ +
    + + header + + + + settings.general.enable + + checkbox + Enable NetBird + + + settings.general.wireguardPort + + text + Wireguard interface listening port + + + header + + + + settings.firewall.allowConfig + + checkbox + Allow the client to filter traffic with its built-in firewall. If disabled, you must assign the NetBird interface and manage firewall rules via OPNsense firewall management. + Additionally, NetBird routing and DNS may not function as intended. + + + settings.firewall.blockInboundConnection + + checkbox + Block all inbound connections to the local machine from the WireGuard interface and any routed networks + + + header + + + + settings.ssh.enable + + checkbox + Allows incoming SSH connections + + + header + + + + settings.dns.enable + + checkbox + Allows the client to resolve and configure DNS on the host + + + header + + + + settings.routing.accessLan + + checkbox + Allow access to local networks (LAN) when using this peer as a routing peer or exit-node + + + settings.routing.acceptClientRoutes + + checkbox + Accept and process client routes received from the management + + + settings.routing.acceptServerRoutes + + checkbox + Enable this peer to act as a router for server routes received from the management + + + header + + + + settings.postquantum.enableRosenpass + + checkbox + Enable the Rosenpass to provide post-quantum secure connections (Experimental) + + + settings.postquantum.rosenpassPermissive + + checkbox + Enable Rosenpass permissive mode + + + header + + + + settings.syslog.logLevel + + dropdown + Set the syslog logging level. Setting a certain log level will cause all messages of the specified and more severe log levels to be logged. + +
    diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/ACL/ACL.xml b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/ACL/ACL.xml new file mode 100644 index 0000000000..bb0b3272fe --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + VPN: NetBird + + ui/netbird/* + api/netbird/* + + + diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Authentication.php b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Authentication.php new file mode 100644 index 0000000000..3dc00c2a21 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Authentication.php @@ -0,0 +1,35 @@ + + //OPNsense/netbird/authentication + NetBird authentication + 1.0.0 + + + Y + https://api.netbird.io:443 + + + + /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i + Please specify a valid setup key. + + +
    diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Menu/Menu.xml b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Menu/Menu.xml new file mode 100644 index 0000000000..6423021539 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Menu/Menu.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.php b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.php new file mode 100644 index 0000000000..0de5371db4 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.php @@ -0,0 +1,64 @@ +general->wireguardPort->__toString(); + $config["ServerSSHAllowed"] = $this->ssh->enable->__toString() == 1; + $config["DisableFirewall"] = $this->firewall->allowConfig->__toString() != 1; + $config["BlockInbound"] = $this->firewall->blockInboundConnection->__toString() == 1; + $config["DisableDNS"] = $this->dns->enable->__toString() != 1; + $config["BlockLANAccess"] = $this->routing->accessLan->__toString() != 1; + $config["DisableClientRoutes"] = $this->routing->acceptClientRoutes->__toString() != 1; + $config["DisableServerRoutes"] = $this->routing->acceptServerRoutes->__toString() != 1; + $config["RosenpassEnabled"] = $this->postquantum->enableRosenpass->__toString() == 1; + $config["RosenpassPermissive"] = $this->postquantum->rosenpassPermissive->__toString() == 1; + + + $result = file_put_contents($target, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + if ($result === false) { + syslog(LOG_ERR, "netbird: failed to write updated configuration to $target"); + } + } +} diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.xml b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.xml new file mode 100644 index 0000000000..a99ce9985a --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Settings.xml @@ -0,0 +1,80 @@ + + //OPNsense/netbird/settings + NetBird settings + 1.1.0 + + + + 0 + Y + + + 51820 + Y + 1 + 65535 + Please specify a valid port. + + + + + 1 + Y + + + 0 + Y + + + + + 0 + Y + + + + + 1 + Y + + + + + 1 + Y + + + 1 + Y + + + 1 + Y + + + + + 0 + Y + + + 0 + Y + + + + + Y + info + + fatal + error + warn + info + debug + trace + + + + + diff --git a/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Status.php b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Status.php new file mode 100644 index 0000000000..da3298cf1f --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/models/OPNsense/Netbird/Status.php @@ -0,0 +1,35 @@ + + :memory: + NetBird status + + + diff --git a/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/authentication.volt b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/authentication.volt new file mode 100644 index 0000000000..7d2652c4de --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/authentication.volt @@ -0,0 +1,111 @@ +{# + # Copyright (C) 2025 Ralph Moser, PJ Monitoring GmbH + # Copyright (C) 2025 squared GmbH + # Copyright (C) 2025 Christopher Linn, BackendMedia IT-Services GmbH + # Copyright (C) 2025 NetBird GmbH + # 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':authenticationForm,'id':'frmAuthentication']) }} +
    + diff --git a/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/settings.volt b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/settings.volt new file mode 100644 index 0000000000..7c505a7a11 --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/settings.volt @@ -0,0 +1,56 @@ +{# + # Copyright (C) 2025 Ralph Moser, PJ Monitoring GmbH + # Copyright (C) 2025 squared GmbH + # Copyright (C) 2025 Christopher Linn, BackendMedia IT-Services GmbH + # Copyright (C) 2025 NetBird GmbH + # 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':settingsForm,'id':'frmSettings']) }} +
    +{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/netbird/service/reconfigure', 'data_service_widget': 'netbird'}) }} diff --git a/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/status.volt b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/status.volt new file mode 100644 index 0000000000..bea4167a4e --- /dev/null +++ b/security/netbird/src/opnsense/mvc/app/views/OPNsense/Netbird/status.volt @@ -0,0 +1,281 @@ +{# + # Copyright (C) 2025 Ralph Moser, PJ Monitoring GmbH + # Copyright (C) 2025 squared GmbH + # Copyright (C) 2025 Christopher Linn, BackendMedia IT-Services GmbH + # Copyright (C) 2025 NetBird GmbH + # 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. + #} + + +
    +
    +
    +

    {{ lang._('Connection Status') }}

    +
    +
    + +
    +
    +
    +
    +

    {{ lang._('Package Versions') }}

    +
    +
    +
    +
    diff --git a/security/netbird/src/opnsense/service/conf/actions.d/actions_netbird.conf b/security/netbird/src/opnsense/service/conf/actions.d/actions_netbird.conf new file mode 100644 index 0000000000..78ae619610 --- /dev/null +++ b/security/netbird/src/opnsense/service/conf/actions.d/actions_netbird.conf @@ -0,0 +1,50 @@ +[start] +command:/usr/local/etc/rc.d/netbird start +parameters: +type:script +message:starting netbird + +[stop] +command:/usr/local/etc/rc.d/netbird stop +parameters: +type:script +message:stopping netbird + +[restart] +command:/usr/local/etc/rc.d/netbird restart +parameters: +type:script +message:restarting netbird + +[status] +command:/usr/local/etc/rc.d/netbird status +errors:no +type:script_output +message:get netbird status + +[up] +command:/usr/local/bin/netbird up +type:script +message:set netbird up + +[down] +command:/usr/local/bin/netbird down +type:script +message:set netbird down + +[status-json] +command:/usr/local/bin/netbird status --json +errors:no +type:script_output +message:get netbird status in json format + +[up-setup-key] +command:/usr/local/bin/netbird up +parameters: -m %s -k %s +type:script_output +message:set netbird up with setup key + +[sync-config] +command:/usr/local/sbin/pluginctl -c netbird_sync_config +type:script_output +message:sync netbird configuration diff --git a/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/+TARGETS b/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/+TARGETS new file mode 100644 index 0000000000..ee852218f5 --- /dev/null +++ b/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/+TARGETS @@ -0,0 +1 @@ +netbird:/etc/rc.conf.d/netbird diff --git a/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/netbird b/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/netbird new file mode 100644 index 0000000000..e6191178e6 --- /dev/null +++ b/security/netbird/src/opnsense/service/templates/OPNsense/Netbird/netbird @@ -0,0 +1,8 @@ +{% if OPNsense.netbird.settings.general.enable == '1' %} +netbird_enable="YES" +netbird_setup="/usr/local/sbin/pluginctl -c netbird_sync_config" +netbird_logfile="syslog" +netbird_loglevel="{{ OPNsense.netbird.settings.syslog.logLevel }}" +{% else %} +netbird_enable="NO" +{% endif %} diff --git a/security/netbird/src/opnsense/service/templates/OPNsense/Syslog/local/netbird.conf b/security/netbird/src/opnsense/service/templates/OPNsense/Syslog/local/netbird.conf new file mode 100644 index 0000000000..c16a43df75 --- /dev/null +++ b/security/netbird/src/opnsense/service/templates/OPNsense/Syslog/local/netbird.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [netbird]. +################################################################### +filter f_local_netbird { + program("netbird"); +}; diff --git a/security/openconnect/Makefile b/security/openconnect/Makefile index c8efe45d2b..9c0c4721ae 100644 --- a/security/openconnect/Makefile +++ b/security/openconnect/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= openconnect -PLUGIN_VERSION= 1.4.1 +PLUGIN_VERSION= 1.4.6 PLUGIN_COMMENT= OpenConnect Client PLUGIN_DEPENDS= openconnect PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/security/openconnect/pkg-descr b/security/openconnect/pkg-descr index 43b692d192..974ae1c81e 100644 --- a/security/openconnect/pkg-descr +++ b/security/openconnect/pkg-descr @@ -6,6 +6,29 @@ the Juniper SSL VPN which is now known as Pulse Connect Secure. Plugin Changelog ================ +1.4.6 + +* Add otion to allow insecure ciphers + +1.4.5 + +* Allow ":" and "/" characters in user name + +1.4.4 + +* Improve compatibility via useragent="AnyConnect" +* Register interfaces on reconfigure +* Ignore server-side DNS servers + +1.4.3 + +* Add support for one-time password generation +* Permit additional characters in group name + +1.4.2 + +* Allow usernames up to 64 characters + 1.4.1 * Allow selection of different protocols diff --git a/security/openconnect/src/etc/inc/plugins.inc.d/openconnect.inc b/security/openconnect/src/etc/inc/plugins.inc.d/openconnect.inc index c14a9d26d1..f8bc4c684c 100644 --- a/security/openconnect/src/etc/inc/plugins.inc.d/openconnect.inc +++ b/security/openconnect/src/etc/inc/plugins.inc.d/openconnect.inc @@ -53,7 +53,6 @@ function openconnect_services() return $services; } - function openconnect_interfaces() { $interfaces = array(); @@ -82,3 +81,8 @@ function openconnect_xmlrpc_sync() $result['services'] = ['openconnect']; return array($result); } + +function openconnect_devices() +{ + return [['pattern' => '^ocvpn', 'volatile' => true]]; +} diff --git a/security/openconnect/src/etc/rc.d/opnsense-openconnect b/security/openconnect/src/etc/rc.d/opnsense-openconnect index 518a83c7d4..3318f76673 100755 --- a/security/openconnect/src/etc/rc.d/opnsense-openconnect +++ b/security/openconnect/src/etc/rc.d/opnsense-openconnect @@ -1,11 +1,8 @@ #!/bin/sh # -# $FreeBSD$ -# # PROVIDE: opnsense-openconnect # REQUIRE: SERVERS # KEYWORD: shutdown -# . /etc/rc.subr diff --git a/security/openconnect/src/opnsense/mvc/app/controllers/OPNsense/Openconnect/Api/ServiceController.php b/security/openconnect/src/opnsense/mvc/app/controllers/OPNsense/Openconnect/Api/ServiceController.php index e7fc2ce5fd..9445a96f82 100644 --- a/security/openconnect/src/opnsense/mvc/app/controllers/OPNsense/Openconnect/Api/ServiceController.php +++ b/security/openconnect/src/opnsense/mvc/app/controllers/OPNsense/Openconnect/Api/ServiceController.php @@ -36,4 +36,13 @@ class ServiceController extends ApiMutableServiceControllerBase protected static $internalServiceTemplate = 'OPNsense/Openconnect'; protected static $internalServiceEnabled = 'enabled'; protected static $internalServiceName = 'openconnect'; + + /** + * hook group interface registration on reconfigure + * @return bool + */ + protected function invokeInterfaceRegistration() + { + return true; + } } diff --git a/security/openconnect/src/opnsense/mvc/app/controllers/OPNsense/Openconnect/forms/general.xml b/security/openconnect/src/opnsense/mvc/app/controllers/OPNsense/Openconnect/forms/general.xml index 2631072a6d..a40a423e0a 100644 --- a/security/openconnect/src/opnsense/mvc/app/controllers/OPNsense/Openconnect/forms/general.xml +++ b/security/openconnect/src/opnsense/mvc/app/controllers/OPNsense/Openconnect/forms/general.xml @@ -47,6 +47,24 @@ dropdown Select the client certificate to use. + + general.tokenmode + + dropdown + Use a one-time password generation mode. + + + general.tokensecret + + text + Enter a secret to use with one-time password generation. + + + general.allowinsecure + + checkbox + This option allows the use of insecure ciphers. + general.protocol diff --git a/security/openconnect/src/opnsense/mvc/app/models/OPNsense/Openconnect/General.xml b/security/openconnect/src/opnsense/mvc/app/models/OPNsense/Openconnect/General.xml index 8c09e9d66c..32234fa409 100644 --- a/security/openconnect/src/opnsense/mvc/app/models/OPNsense/Openconnect/General.xml +++ b/security/openconnect/src/opnsense/mvc/app/models/OPNsense/Openconnect/General.xml @@ -1,65 +1,73 @@ //OPNsense/openconnect/general Openconnect configuration - 1.0.2 + 1.0.4 - 0 + 0 Y - server + server Y - /\S*/ + /\S*/ Please provide IP or hostname (no spaces allowed). - user + user Y - /^[a-zA-Z0-9.\@_-]{1,32}$/ - Please provide a valid username. Allowed characters are a-zA-Z0-9._-@ and it has to be 1-32 characters long. + /^[a-zA-Z0-9.\@_\-:\/]{1,64}$/ + Please provide a valid username. Allowed characters are a-zA-Z0-9.@_-:/ and it has to be 1-64 characters long. - password + password Y - N - /^([a-zA-Z0-9\/\+\=]){40,64}$/u + /^([a-zA-Z0-9\/\+\=]){40,64}$/u Please provide a valid hash. - sha256 - N + sha256 Y - - SHA256 - SHA1 - PIN-SHA256 - + + SHA256 + SHA1 + PIN-SHA256 + - N - /^[a-zA-Z0-9._-]{1,64}$/ - Please provide a valid group name. Allowed characters are a-zA-Z0-9._- and it has to be 1-64 characters long. + /^[() a-zA-Z0-9._-]{1,64}$/ + Please provide a valid group name. Allowed are at most 64 characters from a-zA-Z0-9._-() and space. cert - N + + + RSA SecurID + TOTP + HOTP + OpenIDConnect + + + + + 0 + Y + - anyconnect - N + anyconnect Y - - Cisco AnyConnect - Juniper - Pulse Connect Secure - Palo Alto Networks GlobalProtect - F5 Big-IP - Fortinet Fortigate - Array Networks AG - + + Cisco AnyConnect + Juniper + Pulse Connect Secure + Palo Alto Networks GlobalProtect + F5 Big-IP + Fortinet Fortigate + Array Networks AG + diff --git a/security/openconnect/src/opnsense/mvc/app/views/OPNsense/Openconnect/general.volt b/security/openconnect/src/opnsense/mvc/app/views/OPNsense/Openconnect/general.volt index 2f876fdf0f..5faec528a5 100644 --- a/security/openconnect/src/opnsense/mvc/app/views/OPNsense/Openconnect/general.volt +++ b/security/openconnect/src/opnsense/mvc/app/views/OPNsense/Openconnect/general.volt @@ -1,35 +1,33 @@ {# + # Copyright (c) 2014-2018 Deciso B.V. + # Copyright (c) 2018 Michael Muenz + # 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. + #} -OPNsense® is Copyright © 2014 – 2018 by Deciso B.V. -This file is Copyright © 2018 by Michael Muenz -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':generalForm,'id':'frm_general_settings'])}} -
    -
    +
    diff --git a/security/openconnect/src/opnsense/scripts/OPNsense/Openconnect/vpnc.sh b/security/openconnect/src/opnsense/scripts/OPNsense/Openconnect/vpnc.sh new file mode 100755 index 0000000000..1257be1029 --- /dev/null +++ b/security/openconnect/src/opnsense/scripts/OPNsense/Openconnect/vpnc.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# do not pass DNS information that clobbers /etc/resolv.conf +export INTERNAL_IP4_DNS= + +. /usr/local/sbin/vpnc-script + +# XXX we can register the proper DNS via ifctl(8) if required later diff --git a/security/openconnect/src/opnsense/service/templates/OPNsense/Openconnect/openconnect b/security/openconnect/src/opnsense/service/templates/OPNsense/Openconnect/openconnect index 5b8dd90376..25975f7f93 100644 --- a/security/openconnect/src/opnsense/service/templates/OPNsense/Openconnect/openconnect +++ b/security/openconnect/src/opnsense/service/templates/OPNsense/Openconnect/openconnect @@ -1,9 +1,10 @@ {% if helpers.exists('OPNsense.openconnect.general.enabled') and OPNsense.openconnect.general.enabled == '1' %} openconnect_enable="YES" +openconnect_flags="-s /usr/local/opnsense/scripts/OPNsense/Openconnect/vpnc.sh" {% if helpers.exists('OPNsense.openconnect.general.server') and OPNsense.openconnect.general.server != '' %} {% if helpers.exists('OPNsense.openconnect.general.user') and OPNsense.openconnect.general.user != '' %} {% if helpers.exists('OPNsense.openconnect.general.password') and OPNsense.openconnect.general.password != '' %} -openconnect_flags="--config=/usr/local/etc/openconnect.conf {{ OPNsense.openconnect.general.server }}" +openconnect_flags="${openconnect_flags} --config=/usr/local/etc/openconnect.conf {{ OPNsense.openconnect.general.server }}" {% endif %} {% endif %} {% endif %} diff --git a/security/openconnect/src/opnsense/service/templates/OPNsense/Openconnect/openconnect.conf b/security/openconnect/src/opnsense/service/templates/OPNsense/Openconnect/openconnect.conf index 626091fadb..69d7a51ea0 100644 --- a/security/openconnect/src/opnsense/service/templates/OPNsense/Openconnect/openconnect.conf +++ b/security/openconnect/src/opnsense/service/templates/OPNsense/Openconnect/openconnect.conf @@ -19,7 +19,19 @@ authgroup={{ OPNsense.openconnect.general.group }} certificate=/usr/local/etc/openconnect_cert.pem sslkey=/usr/local/etc/openconnect_key.pem {% endif %} +{% if helpers.exists('OPNsense.openconnect.general.tokenmode') and OPNsense.openconnect.general.tokenmode != '' %} +{% if helpers.exists('OPNsense.openconnect.general.tokensecret') and OPNsense.openconnect.general.tokensecret != '' %} +token-mode={{ OPNsense.openconnect.general.tokenmode }} +token-secret={{ OPNsense.openconnect.general.tokensecret }} +{% endif %} +{% endif %} +{% if not helpers.empty('OPNsense.openconnect.general.allowinsecure') %} +allow-insecure-crypto +{% endif %} {% if helpers.exists('OPNsense.openconnect.general.protocol') and OPNsense.openconnect.general.protocol != '' %} protocol={{ OPNsense.openconnect.general.protocol }} +{% if OPNsense.openconnect.general.protocol == 'anyconnect' %} +useragent=AnyConnect +{% endif %} {% endif %} {% endif %} diff --git a/security/openvpn-legacy/Makefile b/security/openvpn-legacy/Makefile new file mode 100644 index 0000000000..91dd22fada --- /dev/null +++ b/security/openvpn-legacy/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= openvpn-legacy +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= OpenVPN legacy support +PLUGIN_DEPENDS= # openvpn +PLUGIN_MAINTAINER= ad@opnsense.org + +.include "../../Mk/plugins.mk" diff --git a/security/openvpn-legacy/pkg-descr b/security/openvpn-legacy/pkg-descr new file mode 100644 index 0000000000..aba0dbd28e --- /dev/null +++ b/security/openvpn-legacy/pkg-descr @@ -0,0 +1 @@ +This plugin adds the legacy OpenVPN server/client configuration pages. diff --git a/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/ACL/ACL.xml b/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/ACL/ACL.xml new file mode 100644 index 0000000000..0060530434 --- /dev/null +++ b/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/ACL/ACL.xml @@ -0,0 +1,14 @@ + + + VPN: OpenVPN: Client + + vpn_openvpn_client.php* + + + + VPN: OpenVPN: Server + + vpn_openvpn_server.php* + + + diff --git a/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/Menu/Menu.xml b/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/Menu/Menu.xml new file mode 100644 index 0000000000..c611e9fc6a --- /dev/null +++ b/security/openvpn-legacy/src/opnsense/mvc/app/models/OPNsense/OpenVPN/Menu/Menu.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/security/openvpn-legacy/src/www/vpn_openvpn_client.php b/security/openvpn-legacy/src/www/vpn_openvpn_client.php new file mode 100644 index 0000000000..bded6628b1 --- /dev/null +++ b/security/openvpn-legacy/src/www/vpn_openvpn_client.php @@ -0,0 +1,1231 @@ + + * 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. + */ + +require_once("guiconfig.inc"); +require_once("interfaces.inc"); +require_once("plugins.inc.d/openvpn.inc"); + +$a_client = &config_read_array('openvpn', 'openvpn-client'); + +$vpnid = 0; +$act = null; +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + if (isset($_GET['dup']) && isset($a_client[$_GET['dup']])) { + $configId = $_GET['dup']; + } elseif (isset($_GET['id']) && isset($a_client[$_GET['id']])) { + $id = $_GET['id']; + $configId = $id; + } + + if (isset($_GET['act'])) { + $act = $_GET['act']; + } + + $pconfig = array(); + // set defaults + $pconfig['autokey_enable'] = "yes"; // just in case the modes switch + $pconfig['autotls_enable'] = "yes"; // just in case the modes switch + $pconfig['tlsmode'] = "auth"; + $pconfig['digest'] = "SHA1"; + $pconfig['verbosity_level'] = 1; // Default verbosity is 1 + + // edit existing. + if (isset($configId)) { + // 1 on 1 copy of config attributes + $copy_fields = "auth_user,auth_pass,disable,mode,protocol,interface + ,local_port,server_addr,server_port,resolve_retry,remote_random,reneg-sec + ,proxy_addr,proxy_port,proxy_user,proxy_passwd,proxy_authtype,description + ,custom_options,ns_cert_type,dev_mode,tlsmode,caref,certref,crypto,digest + ,tunnel_network,tunnel_networkv6,remote_network,remote_networkv6,use_shaper + ,compression,passtos,route_no_pull,route_no_exec,verbosity_level"; + + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (isset($a_client[$configId][$fieldname])) { + $pconfig[$fieldname] = $a_client[$configId][$fieldname]; + } elseif (!isset($pconfig[$fieldname])) { + // initialize element + $pconfig[$fieldname] = null; + } + } + + // load / convert + if (!empty($a_client[$configId]['ipaddr'])) { + $pconfig['interface'] = $pconfig['interface'] . '|' . $a_client[$configId]['ipaddr']; + } + + if (isset($a_client[$configId]['tls'])) { + $pconfig['tls'] = base64_decode($a_client[$configId]['tls']); + } else { + $pconfig['tls'] = null; + $pconfig['tlsmode'] = null; + } + + if (isset($a_client[$configId]['shared_key'])) { + $pconfig['shared_key'] = base64_decode($a_client[$configId]['shared_key']); + } else { + $pconfig['shared_key'] = null ; + } + + if (isset($id)) { + $vpnid = $a_client[$id]['vpnid']; + } + } elseif ($act=="new") { + // create new + $pconfig['interface'] = 'any'; + $init_fields = "auth_user,auth_pass,disable,mode,protocol,interface + ,local_port,server_addr,server_port,resolve_retry,remote_random,reneg-sec + ,proxy_addr,proxy_port,proxy_user,proxy_passwd,proxy_authtype,description + ,custom_options,ns_cert_type,dev_mode,caref,certref,crypto,digest,tlsmode + ,tunnel_network,tunnel_networkv6,remote_network,remote_networkv6,use_shaper + ,compression,passtos,route_no_pull,route_no_exec,verbosity_level"; + + foreach (explode(",", $init_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!isset($pconfig[$fieldname])) { + $pconfig[$fieldname] = null; + } + } + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + $pconfig = $_POST; + $input_errors = array(); + if (isset($_POST['id']) && isset($a_client[$_POST['id']])) { + $id = $_POST['id']; + } + if (isset($_POST['act'])) { + $act = $_POST['act']; + } + + if ($act == "del") { + $response = ["status" => "failed", "message" => gettext("not found")]; + if (isset($id) && !empty($a_client[$id])) { + openvpn_delete('client', $a_client[$id]); + unset($a_client[$id]); + write_config(); + $response = ["status" => "ok"]; + } + echo json_encode($response); + exit; + } elseif ($act == "del_x") { + if (!empty($pconfig['rule']) && is_array($pconfig['rule'])) { + foreach ($pconfig['rule'] as $rulei) { + $vpn_id = !empty($a_client[$rulei]) ? $a_client[$rulei]['vpnid'] : null; + if (!empty($a_client[$rulei])) { + openvpn_delete('client', $a_client[$rulei]); + unset($a_client[$rulei]); + } + } + write_config(); + } + header(url_safe('Location: /vpn_openvpn_client.php')); + exit; + } elseif ($act == "move"){ + // move selected items + if (!isset($id)) { + // if id not set/found, move to end + $id = count($a_client); + } + $a_client = legacy_move_config_list_items($a_client, $id, $pconfig['rule']); + write_config(); + header(url_safe('Location: /vpn_openvpn_client.php')); + exit; + } elseif ($act == "toggle") { + if (isset($id)) { + if (isset($a_client[$id]['disable'])) { + unset($a_client[$id]['disable']); + } else { + $a_client[$id]['disable'] = true; + } + write_config(); + openvpn_configure_single($a_client[$id]['vpnid']); + } + header(url_safe('Location: /vpn_openvpn_client.php')); + exit; + } else { + // update client (after validation) + if (isset($id)) { + $vpnid = $a_client[$id]['vpnid']; + } + if (isset($pconfig['mode']) && $pconfig['mode'] != "p2p_shared_key") { + $tls_mode = true; + } else { + $tls_mode = false; + } + + // generate new key + if (!empty($pconfig['autokey_enable'])) { + $pconfig['shared_key'] = openvpn_create_key(); + } + + /* input validation */ + if (strpos($pconfig['interface'], '|') !== false) { + list($iv_iface, $iv_ip) = explode("|", $pconfig['interface']); + } else { + $iv_iface = $pconfig['interface']; + $iv_ip = null; + } + + if (is_ipaddrv4($iv_ip) && (stristr($pconfig['protocol'], "6") !== false)) { + $input_errors[] = gettext("Protocol and IP address families do not match. You cannot select an IPv6 protocol and an IPv4 address."); + } elseif (is_ipaddrv6($iv_ip) && (stristr($pconfig['protocol'], "6") === false)) { + $input_errors[] = gettext("Protocol and IP address families do not match. You cannot select an IPv4 protocol and an IPv6 address."); + } elseif ((stristr($pconfig['protocol'], "6") === false) && !get_interface_ip($iv_iface) && ($pconfig['interface'] != "any")) { + $input_errors[] = gettext("An IPv4 protocol was selected, but the selected interface has no IPv4 address."); + } elseif ((stristr($pconfig['protocol'], "6") !== false) && !get_interface_ipv6($iv_iface) && ($pconfig['interface'] != "any")) { + $input_errors[] = gettext("An IPv6 protocol was selected, but the selected interface has no IPv6 address."); + } + if (!empty($pconfig['local_port'])) { + if (!is_numeric($pconfig['local_port']) || $pconfig['local_port'] < 0 || ($pconfig['local_port'] > 65535)) { + $input_errors[] = gettext("The field 'Local port' must contain a valid port, ranging from 0 to 65535."); + } + $portused = openvpn_port_used($pconfig['protocol'], $pconfig['interface'], $pconfig['local_port'], $vpnid); + if (($portused != $vpnid) && ($portused != 0)) { + $input_errors[] = gettext("The specified 'Local port' is in use. Please select another value"); + } + } + + $server_addr_a = array(); + $server_port_a = array(); + + foreach (array_keys($pconfig['server_addr']) as $i) { + if (empty($pconfig['server_addr'][$i]) && empty($pconfig['server_port'][$i])) { + continue; + } + if (empty($pconfig['server_addr'][$i]) || (!is_domain($pconfig['server_addr'][$i]) && !is_ipaddr($pconfig['server_addr'][$i]))) { + $input_errors[] = gettext("The field 'Server host or address' must contain a valid IP address or domain name.") ; + } + if (empty($pconfig['server_port'][$i]) || !is_numeric($pconfig['server_port'][$i]) || $pconfig['server_port'][$i] < 0 || $pconfig['server_port'][$i] > 65535) { + $input_errors[] = gettext("The field 'Server port' must contain a valid port, ranging from 0 to 65535."); + } + $server_addr_a[] = $pconfig['server_addr'][$i]; + $server_port_a[] = $pconfig['server_port'][$i]; + } + + $pconfig['server_addr'] = implode(',', $server_addr_a); + $pconfig['server_port'] = implode(',', $server_port_a); + + if (empty($pconfig['server_addr']) || empty($pconfig['server_port'])) { + $input_errors[] = gettext("At least one remote server must be specified."); + } + + if (isset($pconfig['reneg-sec']) && $pconfig['reneg-sec'] != "" && (string)((int)$pconfig['reneg-sec']) != $pconfig['reneg-sec']) { + $input_errors[] = gettext("Renegotiate time should contain a valid number of seconds."); + } + + if (!empty($pconfig['proxy_addr'])) { + if (empty($pconfig['proxy_addr']) || (!is_domain($pconfig['proxy_addr']) && !is_ipaddr($pconfig['proxy_addr']))) { + $input_errors[] = gettext("The field 'Proxy host or address' must contain a valid IP address or domain name."); + } + if (empty($pconfig['proxy_port']) || !is_numeric($pconfig['proxy_port']) || $pconfig['proxy_port'] < 0 || ($pconfig['proxy_port'] > 65535)) { + $input_errors[] = gettext("The field 'Proxy port' must contain a valid port, ranging from 0 to 65535."); + } + if (isset($pconfig['proxy_authtype']) && $pconfig['proxy_authtype'] != "none") { + if (empty($pconfig['proxy_user']) || empty($pconfig['proxy_passwd'])) { + $input_errors[] = gettext("User name and password are required for proxy with authentication."); + } + } + } + if ($result = openvpn_validate_cidr($pconfig['tunnel_network'], gettext('IPv4 Tunnel Network'), false, 'ipv4')) { + $input_errors[] = $result; + } + if ($result = openvpn_validate_cidr($pconfig['tunnel_networkv6'], gettext('IPv6 Tunnel Network'), false, 'ipv6')) { + $input_errors[] = $result; + } + if ($result = openvpn_validate_cidr($pconfig['remote_network'], gettext('IPv4 Remote Network'), true, 'ipv4')) { + $input_errors[] = $result; + } + if ($result = openvpn_validate_cidr($pconfig['remote_networkv6'], gettext('IPv6 Remote Network'), true, 'ipv6')) { + $input_errors[] = $result; + } + if (!empty($pconfig['use_shaper']) && (!is_numeric($pconfig['use_shaper']) || ($pconfig['use_shaper'] <= 0))) { + $input_errors[] = gettext("The bandwidth limit must be a positive numeric value."); + } + if (!$tls_mode && empty($pconfig['autokey_enable'])) { + if (!strstr($pconfig['shared_key'], "-----BEGIN OpenVPN Static key V1-----") || + !strstr($pconfig['shared_key'], "-----END OpenVPN Static key V1-----")) { + $input_errors[] = gettext("The field 'Shared Key' does not appear to be valid"); + } + } + if ($tls_mode && !empty($pconfig['tlsmode']) && empty($pconfig['autotls_enable'])) { + if (!strstr($pconfig['tls'], "-----BEGIN OpenVPN Static key V1-----") || + !strstr($pconfig['tls'], "-----END OpenVPN Static key V1-----")) { + $input_errors[] = gettext("The field 'TLS Shared Key' does not appear to be valid"); + } + } + + /* If we are not in shared key mode, then we need the CA/Cert. */ + if (isset($pconfig['mode']) && $pconfig['mode'] != "p2p_shared_key") { + $reqdfields = explode(" ", "caref"); + $reqdfieldsn = array(gettext("Certificate Authority")); + } elseif (empty($pconfig['autokey_enable'])) { + /* We only need the shared key filled in if we are in shared key mode and autokey is not selected. */ + $reqdfields = array('shared_key'); + $reqdfieldsn = array(gettext('Shared key')); + } + + do_input_validation($pconfig, $reqdfields, $reqdfieldsn, $input_errors); + + if (($pconfig['mode'] != "p2p_shared_key") && empty($pconfig['certref']) && empty($pconfig['auth_user']) && empty($pconfig['auth_pass'])) { + $input_errors[] = gettext("If no Client Certificate is selected, a username and password must be entered."); + } + $prev_opt = (isset($id) && !empty($a_client[$id])) ? $a_client[$id]['custom_options'] : ""; + if ($prev_opt != str_replace("\r\n", "\n", $pconfig['custom_options']) && !userIsAdmin($_SESSION['Username'])) { + $input_errors[] = gettext('Advanced options may only be edited by system administrators due to the increased possibility of privilege escalation.'); + } + + if (count($input_errors) == 0) { + // save data + $client = array(); + // 1 on 1 copy of config attributes + $copy_fields = "auth_user,auth_pass,protocol,dev_mode,local_port,reneg-sec + ,server_addr,server_port,resolve_retry,proxy_addr,proxy_port,remote_random + ,proxy_authtype,proxy_user,proxy_passwd,description,mode,crypto,digest + ,tunnel_network,tunnel_networkv6,remote_network,remote_networkv6 + ,use_shaper,compression,passtos,route_no_pull,route_no_exec,tlsmode + ,verbosity_level,interface"; + + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!empty($pconfig[$fieldname]) || $pconfig[$fieldname] == '0') { + $client[$fieldname] = $pconfig[$fieldname]; + } + } + + // attributes containing some kind of logic + if ($vpnid) { + $client['vpnid'] = $vpnid; + } else { + $client['vpnid'] = openvpn_vpnid_next(); + } + if (isset($pconfig['disable']) && $pconfig['disable'] == "yes") { + $client['disable'] = true; + } + + if (strpos($pconfig['interface'], "|") !== false) { + list($client['interface'], $client['ipaddr']) = explode("|", $pconfig['interface']); + } + $client['custom_options'] = str_replace("\r\n", "\n", $pconfig['custom_options']); + + if ($tls_mode) { + $client['caref'] = $pconfig['caref']; + $client['certref'] = $pconfig['certref']; + if (!empty($pconfig['tlsmode'])) { + if (!empty($pconfig['autotls_enable'])) { + $pconfig['tls'] = openvpn_create_key(); + } + $client['tls'] = base64_encode($pconfig['tls']); + } + } else { + $client['shared_key'] = base64_encode($pconfig['shared_key']); + } + + if (isset($id)) { + $a_client[$id] = $client; + } else { + $a_client[] = $client; + } + + write_config(); + + openvpn_configure_single($client['vpnid']); + + header(url_safe('Location: /vpn_openvpn_client.php')); + exit; + } + } +} + +// escape form output before processing +legacy_html_escape_form_data($pconfig); + +include("head.inc"); + +?> + + + +
    +
    +
    + 0) { + print_input_errors($input_errors); + } + if (isset($savemsg)) { + print_info_box($savemsg); + }?> + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + /> + +
    + + +
    + +
    + + +
    + +
    + +
    + + + + + + + + + + $item): ?> + + + + + + + +
    + + + + + + + + + +
    +
    + /> + +
    + /> + + +
    + +
    + +
    + + + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + +
    +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + > + . + +
    + +

    .

    +
    +
    + + + +
    + . + +
    + + +
    + + +
    + + /> + . + +
    + + . +
    +
    + + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + /> + +
    + /> + +
    + /> + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + +
    + + + +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + +
      + + + + + +
    +
    +
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    +
    + diff --git a/security/openvpn-legacy/src/www/vpn_openvpn_server.php b/security/openvpn-legacy/src/www/vpn_openvpn_server.php new file mode 100644 index 0000000000..4bf340fe4b --- /dev/null +++ b/security/openvpn-legacy/src/www/vpn_openvpn_server.php @@ -0,0 +1,1710 @@ + + * 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. + */ + +require_once("guiconfig.inc"); +require_once("interfaces.inc"); +require_once("plugins.inc.d/openvpn.inc"); + +$a_server = &config_read_array('openvpn', 'openvpn-server'); + +$act = null; +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // fetch id if provided + if (isset($_GET['dup']) && isset($a_server[$_GET['dup']])) { + $configId = $_GET['dup']; + } elseif (isset($_GET['id']) && is_numericint($_GET['id'])) { + $id = $_GET['id']; + $configId = $id; + } + if (isset($_GET['act'])) { + $act = $_GET['act']; + } + $pconfig = array(); + // defaults + $vpnid = 0; + $pconfig['verbosity_level'] = 1; + $pconfig['digest'] = "SHA1"; // OpenVPN Defaults to SHA1 if unset + $pconfig['crypto'] = ""; + $pconfig['tlsmode'] = "auth"; + $pconfig['autokey_enable'] = "yes"; + $pconfig['autotls_enable'] = "yes"; + if (isset($configId) && isset($a_server[$configId])) { + if ($a_server[$configId]['mode'] != "p2p_shared_key") { + $pconfig['cert_depth'] = 1; + } + + // 1 on 1 copy of config attributes + $copy_fields = "mode,protocol,authmode,dev_mode,interface,local_port + ,description,custom_options,crypto,tunnel_network + ,tunnel_networkv6,remote_network,remote_networkv6,gwredir,local_network + ,local_networkv6,maxclients,compression,passtos,client2client + ,dynamic_ip,topology_subnet,serverbridge_dhcp + ,serverbridge_interface,serverbridge_dhcp_start,serverbridge_dhcp_end + ,dns_server1,dns_server2,dns_server3,dns_server4,ntp_server1 + ,ntp_server2,netbios_enable,netbios_ntype,netbios_scope,wins_server1 + ,wins_server2,push_register_dns,push_block_outside_dns,dns_domain,dns_domain_search,local_group + ,client_mgmt_port,verbosity_level,tlsmode,caref,crlref,certref + ,cert_depth,strictusercn,digest,disable,duplicate_cn,vpnid,reneg-sec,use-common-name,cso_login_matching"; + + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (isset($a_server[$configId][$fieldname])) { + $pconfig[$fieldname] = $a_server[$configId][$fieldname]; + } elseif (!isset($pconfig[$fieldname])) { + // initialize element + $pconfig[$fieldname] = null; + } + } + + // load / convert + if (!empty($a_server[$configId]['ipaddr'])) { + $pconfig['interface'] = $pconfig['interface'] . '|' . $a_server[$configId]['ipaddr']; + } + if (!empty($a_server[$configId]['shared_key'])) { + $pconfig['shared_key'] = base64_decode($a_server[$configId]['shared_key']); + } else { + $pconfig['shared_key'] = null; + } + if (!empty($a_server[$configId]['tls'])) { + $pconfig['tls'] = base64_decode($a_server[$configId]['tls']); + } else { + $pconfig['tls'] = null; + $pconfig['tlsmode'] = null; + } + } elseif ($act == "new") { + $pconfig['dev_mode'] = "tun"; + $pconfig['interface'] = 'any'; + $pconfig['protocol'] = 'UDP'; + $pconfig['local_port'] = openvpn_port_next($pconfig['protocol']); + $pconfig['cert_depth'] = 1; + // init all fields used in the form + $init_fields = "mode,protocol,authmode,dev_mode,interface,local_port + ,description,custom_options,crypto,tunnel_network + ,tunnel_networkv6,remote_network,remote_networkv6,gwredir,local_network + ,local_networkv6,maxclients,compression,passtos,client2client + ,dynamic_ip,topology_subnet,serverbridge_dhcp + ,serverbridge_interface,serverbridge_dhcp_start,serverbridge_dhcp_end + ,dns_server1,dns_server2,dns_server3,dns_server4,ntp_server1 + ,ntp_server2,netbios_enable,netbios_ntype,netbios_scope,wins_server1 + ,wins_server2,push_register_dns,push_block_outside_dns,dns_domain,dns_domain_search + ,client_mgmt_port,verbosity_level,tlsmode,caref,crlref,certref + ,cert_depth,strictusercn,digest,disable,duplicate_cn,vpnid,shared_key,tls,reneg-sec,use-common-name + ,cso_login_matching"; + foreach (explode(",", $init_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!isset($pconfig[$fieldname])) { + $pconfig[$fieldname] = null; + } + } + + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isset($_POST['id']) && isset($a_server[$_POST['id']])) { + $id = $_POST['id']; + } + if (isset($_POST['act'])) { + $act = $_POST['act']; + } + + if ($act == "del") { + $response = ["status" => "failed", "message" => gettext("not found")]; + if (isset($id) && !empty($a_server[$id])) { + openvpn_delete('server', $a_server[$id]); + unset($a_server[$id]); + write_config(); + $response = ["status" => "ok"]; + } + echo json_encode($response); + exit; + } elseif ($act == "toggle") { + if (isset($id)) { + if (isset($a_server[$id]['disable'])) { + unset($a_server[$id]['disable']); + } else { + $a_server[$id]['disable'] = true; + } + write_config(); + openvpn_configure_single($a_server[$id]['vpnid']); + } + header(url_safe('Location: /vpn_openvpn_server.php')); + exit; + } else { + // action add/update + $input_errors = array(); + $pconfig = $_POST; + + $vpnid = (isset($id) && $a_server[$id]) ? $a_server[$id]['vpnid'] : 0; + $tls_mode = ($pconfig['mode'] != "p2p_shared_key"); + + if (!empty($pconfig['autokey_enable'])) { + $pconfig['shared_key'] = openvpn_create_key(); + } + + // all input validators + if (strpos($pconfig['interface'], '|') !== false) { + list($iv_iface, $iv_ip) = explode("|", $pconfig['interface']); + } else { + $iv_iface = $pconfig['interface']; + $iv_ip = null; + } + + if (is_ipaddrv4($iv_ip) && (stristr($pconfig['protocol'], "6") !== false)) { + $input_errors[] = gettext("Protocol and IP address families do not match. You cannot select an IPv6 protocol and an IPv4 IP address."); + } elseif (is_ipaddrv6($iv_ip) && (stristr($pconfig['protocol'], "6") === false)) { + $input_errors[] = gettext("Protocol and IP address families do not match. You cannot select an IPv4 protocol and an IPv6 IP address."); + } elseif ((stristr($pconfig['protocol'], "6") === false) && !get_interface_ip($iv_iface) && ($pconfig['interface'] != "any")) { + $input_errors[] = gettext("An IPv4 protocol was selected, but the selected interface has no IPv4 address."); + } elseif ((stristr($pconfig['protocol'], "6") !== false) && !get_interface_ipv6($iv_iface) && ($pconfig['interface'] != "any")) { + $input_errors[] = gettext("An IPv6 protocol was selected, but the selected interface has no IPv6 address."); + } + + if (empty($pconfig['authmode']) && (($pconfig['mode'] == "server_user") || ($pconfig['mode'] == "server_tls_user"))) { + $input_errors[] = gettext("You must select a Backend for Authentication if the server mode requires User Auth."); + } + + if ($result = openvpn_validate_port($pconfig['local_port'], gettext('Local port'))) { + $input_errors[] = $result; + } + + if ($result = openvpn_validate_cidr($pconfig['tunnel_network'], gettext('IPv4 Tunnel Network'), false, 'ipv4')) { + $input_errors[] = $result; + } elseif (!empty($pconfig['tunnel_network']) && (strpos($pconfig['mode'], "p2p_") === false)) { + // Check IPv4 tunnel_network pool size for Remote Access modes + list($ipv4tunnel_base, $ipv4tunnel_prefix) = explode('/',trim($pconfig['tunnel_network'])); + if ($pconfig['dev_mode'] == "tun") { + if ($ipv4tunnel_prefix > 28 && empty($pconfig['topology_subnet'])) { + $input_errors[] = gettext('A prefix longer than 28 cannot be used with a net30 topology.'); + } elseif ($ipv4tunnel_prefix > 29 && !empty($pconfig['topology_subnet'])) { + $input_errors[] = gettext('A prefix longer than 29 cannot be used for tunnel network.'); + } + } elseif ($pconfig['dev_mode'] == "tap" && $ipv4tunnel_prefix > 29) { + $input_errors[] = gettext('A prefix longer than 29 cannot be used for tunnel network.'); + } + } + + if ($result = openvpn_validate_cidr($pconfig['tunnel_networkv6'], gettext('IPv6 Tunnel Network'), false, 'ipv6')) { + $input_errors[] = $result; + } + + if ($result = openvpn_validate_cidr($pconfig['remote_network'], gettext('IPv4 Remote Network'), true, 'ipv4')) { + $input_errors[] = $result; + } + + if ($result = openvpn_validate_cidr($pconfig['remote_networkv6'], gettext('IPv6 Remote Network'), true, 'ipv6')) { + $input_errors[] = $result; + } + + if ($result = openvpn_validate_cidr($pconfig['local_network'], gettext('IPv4 Local Network'), true, 'ipv4')) { + $input_errors[] = $result; + } + + if ($result = openvpn_validate_cidr($pconfig['local_networkv6'], gettext('IPv6 Local Network'), true, 'ipv6')) { + $input_errors[] = $result; + } + + if (!empty($pconfig['local_port'])) { + $portused = openvpn_port_used($pconfig['protocol'], $pconfig['interface'], $pconfig['local_port'], $vpnid); + if ($portused) { + $input_errors[] = gettext("The specified 'Local port' is in use. Please select another value"); + } + } + + if (!$tls_mode && empty($pconfig['autokey_enable'])) { + if (!strstr($pconfig['shared_key'], "-----BEGIN OpenVPN Static key V1-----") || + !strstr($pconfig['shared_key'], "-----END OpenVPN Static key V1-----")) { + $input_errors[] = gettext("The field 'Shared Key' does not appear to be valid"); + } + } + + if ($tls_mode && !empty($pconfig['tlsmode']) && empty($pconfig['autotls_enable'])) { + if (!strstr($pconfig['tls'], "-----BEGIN OpenVPN Static key V1-----") || + !strstr($pconfig['tls'], "-----END OpenVPN Static key V1-----")) { + $input_errors[] = gettext("The field 'TLS Shared Key' does not appear to be valid"); + } + } + + if (!empty($pconfig['dns_domain_search'])) { + $tmp_ok_domain = 0; + $tmp_nok_domain = 0; + foreach (explode(",", $pconfig['dns_domain_search'] ?? "") as $domain) { + if (is_domain($domain)) { + $tmp_ok_domain++; + } else { + $tmp_nok_domain++; + } + } + if ($tmp_nok_domain > 0) { + $input_errors[] = gettext("The field 'DNS Domain search list' must contain valid domain names"); + } elseif ($tmp_ok_domain > 10) { + $input_errors[] = gettext("The field 'DNS Domain search list' may contain max 10 entries"); + } + } + + if (!empty($pconfig['dns_server1']) && !is_ipaddr(trim($pconfig['dns_server1']))) { + $input_errors[] = gettext("The field 'DNS Server #1' must contain a valid IP address"); + } + if (!empty($pconfig['dns_server2']) && !is_ipaddr(trim($pconfig['dns_server2']))) { + $input_errors[] = gettext("The field 'DNS Server #2' must contain a valid IP address"); + } + if (!empty($pconfig['dns_server3']) && !is_ipaddr(trim($pconfig['dns_server3']))) { + $input_errors[] = gettext("The field 'DNS Server #3' must contain a valid IP address"); + } + if (!empty($pconfig['dns_server4']) && !is_ipaddr(trim($pconfig['dns_server4']))) { + $input_errors[] = gettext("The field 'DNS Server #4' must contain a valid IP address"); + } + + if (!empty($pconfig['ntp_server1']) && !is_ipaddr(trim($pconfig['ntp_server1']))) { + $input_errors[] = gettext("The field 'NTP Server #1' must contain a valid IP address"); + } + if (!empty($pconfig['ntp_server2']) && !is_ipaddr(trim($pconfig['ntp_server2']))) { + $input_errors[] = gettext("The field 'NTP Server #2' must contain a valid IP address"); + } + + if (!empty($pconfig['wins_server_enable'])) { + if (!empty($pconfig['wins_server1']) && !is_ipaddr(trim($pconfig['wins_server1']))) { + $input_errors[] = gettext("The field 'WINS Server #1' must contain a valid IP address"); + } + if (!empty($pconfig['wins_server2']) && !is_ipaddr(trim($pconfig['wins_server2']))) { + $input_errors[] = gettext("The field 'WINS Server #2' must contain a valid IP address"); + } + } + + if (!empty($pconfig['client_mgmt_port_enable'])) { + if ($result = openvpn_validate_port($pconfig['client_mgmt_port'], gettext('Client management port'))) { + $input_errors[] = $result; + } + } + + if (!empty($pconfig['maxclients']) && !is_numeric($pconfig['maxclients'])) { + $input_errors[] = gettext("The field 'Concurrent connections' must be numeric."); + } + + /* If we are not in shared key mode, then we need the CA/Cert. */ + if (isset($pconfig['mode']) && $pconfig['mode'] != "p2p_shared_key") { + $reqdfields = explode(" ", "caref certref"); + $reqdfieldsn = array(gettext("Certificate Authority"),gettext("Certificate")); + } elseif (empty($pconfig['autokey_enable'])) { + /* We only need the shared key filled in if we are in shared key mode and autokey is not selected. */ + $reqdfields = array('shared_key'); + $reqdfieldsn = array(gettext('Shared key')); + } + + $reqdfields[] = 'local_port'; + $reqdfieldsn[] = gettext('Local port'); + + if ($pconfig['dev_mode'] != "tap") { + $reqdfields[] = 'tunnel_network,tunnel_networkv6'; + $reqdfieldsn[] = gettext('Tunnel Network'); + } else { + if ($pconfig['serverbridge_dhcp'] && ($pconfig['tunnel_network'] || $pconfig['tunnel_networkv6'])) { + $input_errors[] = gettext("Using a tunnel network and server bridge settings together is not allowed."); + } + if (($pconfig['serverbridge_dhcp_start'] && !$pconfig['serverbridge_dhcp_end']) + || (!$pconfig['serverbridge_dhcp_start'] && $pconfig['serverbridge_dhcp_end'])) { + $input_errors[] = gettext("Server Bridge DHCP Start and End must both be empty, or defined."); + } + if (($pconfig['serverbridge_dhcp_start'] && !is_ipaddrv4($pconfig['serverbridge_dhcp_start']))) { + $input_errors[] = gettext("Server Bridge DHCP Start must be an IPv4 address."); + } + if (($pconfig['serverbridge_dhcp_end'] && !is_ipaddrv4($pconfig['serverbridge_dhcp_end']))) { + $input_errors[] = gettext("Server Bridge DHCP End must be an IPv4 address."); + } + if (ip2ulong($pconfig['serverbridge_dhcp_start']) > ip2ulong($pconfig['serverbridge_dhcp_end'])) { + $input_errors[] = gettext("The Server Bridge DHCP range is invalid (start higher than end)."); + } + } + if (isset($pconfig['reneg-sec']) && $pconfig['reneg-sec'] != "" && (string)((int)$pconfig['reneg-sec']) != $pconfig['reneg-sec']) { + $input_errors[] = gettext("Renegotiate time should contain a valid number of seconds."); + } + + if (!empty($pconfig['certref'])) { + foreach ($config['cert'] as $cert) { + if ($cert['refid'] == $pconfig['certref']) { + if (cert_get_purpose($cert['crt'])['id-kp-serverAuth'] == 'No') { + $input_errors[] = gettext( + sprintf('Certificate %s is not intended for server use.', $cert['descr']) + ); + } + } + } + } + + $prev_opt = (isset($id) && !empty($a_server[$id])) ? $a_server[$id]['custom_options'] : ""; + if ($prev_opt != str_replace("\r\n", "\n", $pconfig['custom_options']) && !userIsAdmin($_SESSION['Username'])) { + $input_errors[] = gettext('Advanced options may only be edited by system administrators due to the increased possibility of privilege escalation.'); + } + + do_input_validation($pconfig, $reqdfields, $reqdfieldsn, $input_errors); + + if (count($input_errors) == 0) { + // validation correct, save data + $server = array(); + + // delete(rename) old interface so a new TUN or TAP interface can be created. + if (isset($id) && $pconfig['dev_mode'] != $a_server[$id]['dev_mode']) { + openvpn_delete('server', $a_server[$id]); + } + // 1 on 1 copy of config attributes + $copy_fields = "mode,protocol,dev_mode,local_port,description,crypto,digest + ,tunnel_network,tunnel_networkv6,remote_network,remote_networkv6 + ,gwredir,local_network,local_networkv6,maxclients,compression + ,passtos,client2client,dynamic_ip,topology_subnet,local_group + ,serverbridge_dhcp,serverbridge_interface,serverbridge_dhcp_start + ,serverbridge_dhcp_end,dns_domain,dns_domain_search,dns_server1,dns_server2,dns_server3 + ,dns_server4,push_register_dns,push_block_outside_dns,ntp_server1,ntp_server2,netbios_enable + ,netbios_ntype,netbios_scope,verbosity_level,wins_server1,tlsmode + ,wins_server2,client_mgmt_port,strictusercn,reneg-sec,use-common-name,cso_login_matching"; + + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!empty($pconfig[$fieldname]) || $pconfig[$fieldname] == '0') { + $server[$fieldname] = $pconfig[$fieldname]; + } + } + + // attributes containing some kind of logic + if ($vpnid != 0) { + $server['vpnid'] = $vpnid; + } else { + $server['vpnid'] = openvpn_vpnid_next(); + } + + if ($pconfig['disable'] == "yes") { + $server['disable'] = true; + } + if (!empty($pconfig['authmode'])) { + $server['authmode'] = implode(",", $pconfig['authmode']); + } + if (strpos($pconfig['interface'], "|") !== false) { + list($server['interface'], $server['ipaddr']) = explode("|", $pconfig['interface']); + } else { + $server['interface'] = $pconfig['interface']; + } + + $server['custom_options'] = str_replace("\r\n", "\n", $pconfig['custom_options']); + + if ($tls_mode) { + if ($pconfig['tlsmode']) { + if (!empty($pconfig['autotls_enable'])) { + $pconfig['tls'] = openvpn_create_key(); + } + $server['tls'] = base64_encode($pconfig['tls']); + } + foreach (['caref', 'crlref', 'certref', 'cert_depth'] as $cpKey) { + if (isset($pconfig[$cpKey])) { + $server[$cpKey] = $pconfig[$cpKey]; + } + } + if (isset($pconfig['mode']) && $pconfig['mode'] == "server_tls_user" && isset($server['strictusercn'])) { + $server['strictusercn'] = $pconfig['strictusercn']; + } + } else { + $server['shared_key'] = base64_encode($pconfig['shared_key']); + } + + if (isset($_POST['duplicate_cn']) && $_POST['duplicate_cn'] == "yes") { + $server['duplicate_cn'] = true; + } + + // update or add to config + if (isset($id) && $a_server[$id]) { + $a_server[$id] = $server; + } else { + $a_server[] = $server; + } + + write_config(); + + openvpn_configure_single($server['vpnid']); + + header(url_safe('Location: /vpn_openvpn_server.php')); + exit; + } elseif (!empty($pconfig['authmode'])) { + $pconfig['authmode'] = implode(",", $pconfig['authmode']); + } + } +} + +include("head.inc"); + +legacy_html_escape_form_data($pconfig); + +?> + + + + +
    +
    +
    + 0) { + print_input_errors($input_errors); + } + if (isset($savemsg)) { + print_info_box($savemsg); + }?> + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + +
    + /> +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + /> + . + +
    + +

    .

    +
    +
    + + + + +
    . + +
    + + + + +
    . + +
    + + + + +
    . + +
    + +
    + /> + . +
    + +
    + + . +
    +
    + + +
    + + +
    + + + + + +
    + +
    + +
    +
    + /> + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + +
    + /> + +
    + + +
    + + +
    + +
    + /> + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + /> + +
    + /> + +
    + /> + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + /> + +
    + /> + +
    + /> + +
    + +   + + + +   + + +
    +
    + /> +
    + +
    + +
    + /> + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + +
    + + +
    + /> + +
      + + + + + +
    +
    +
    +
    +
    + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + "> + "> + + + / + + + + + + + + + + + + +
    +
    +
    + +
    +
    +
    + + diff --git a/security/q-feeds-connector/+POST_INSTALL.post b/security/q-feeds-connector/+POST_INSTALL.post new file mode 100755 index 0000000000..47481ad322 --- /dev/null +++ b/security/q-feeds-connector/+POST_INSTALL.post @@ -0,0 +1,3 @@ +#!/bin/sh + +/usr/local/sbin/pluginctl -s cron restart diff --git a/security/q-feeds-connector/Makefile b/security/q-feeds-connector/Makefile new file mode 100644 index 0000000000..ae71aa8ba9 --- /dev/null +++ b/security/q-feeds-connector/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= q-feeds-connector +PLUGIN_VERSION= 1.4 +PLUGIN_COMMENT= Connector for Q-Feeds threat intel +PLUGIN_MAINTAINER= devel@qfeeds.com +PLUGIN_TIER= 2 + +.include "../../Mk/plugins.mk" diff --git a/security/q-feeds-connector/README.md b/security/q-feeds-connector/README.md new file mode 100644 index 0000000000..b18fb8cc59 --- /dev/null +++ b/security/q-feeds-connector/README.md @@ -0,0 +1,48 @@ +# plugin to connect to QFeeds threat platform + +Register for a token at : https://qfeeds.com/opnsense/ + +# command line control + +Settings are persisted in `/usr/local/etc/qfeeds.conf`, using the following format: + +``` +[api] +key=tip_xxxxxxxx +``` + +Our commandline tool contains all actions used by the UI, which is practical for debuggging. + +``` +usage: qfeedsctl.py [-h] [--target_dir TARGET_DIR] [-f] [-v] [{fetch_index,fetch,show_index,firewall_load,update,stats} ...] + +positional arguments: + {fetch_index,fetch,show_index,firewall_load,update,stats} + +options: + -h, --help show this help message and exit + --target_dir TARGET_DIR + -f forced (auto index) + -v verbose output +``` + +The index is the driver for most actions, which is a json encoded file in `/var/db/qfeeds-tables/index.json`. + +Actions supported: + +* fetch_index --> download the index file +* fetch --> download the lists +* firewall_load --> collect ip lists into pre-defined firewall tables +* update [sleep when almost time] --> run fetch_index --> fetch --> firewall_load (to be used by cron) +* show_index --> dumps the index +* stats --> dumps feed information +* logs --> dumps firewall log information for aliases offered by Q-Feeds + + +Example usage: + +``` +/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py update +``` + +**Fetch index and update lists when updated remotely** diff --git a/security/q-feeds-connector/pkg-descr b/security/q-feeds-connector/pkg-descr new file mode 100644 index 0000000000..789ff86635 --- /dev/null +++ b/security/q-feeds-connector/pkg-descr @@ -0,0 +1,34 @@ +Connector for Q-Feeds threat intel + +Plugin Changelog +================ + +1.4 + +* Feature: Added DNSCrypt-Proxy integration + +1.3 + +* Widget: Added license info +* Events: added source and destination port + +1.3 + +* Events: added source and destination port +* Events: added quick Threat Lookup button to dest-ip and source-ip fields +* Widget: Added license info + +1.2 + +* fix rule parsing +* bug fixes for unbound blocklist support + +1.1 + +* remove QfeedsStatus as the new table defaults are different +* add unbound blocklist support +* Events: fix empty interface names + +1.0 + +* Intial release version diff --git a/security/q-feeds-connector/src/etc/inc/plugins.inc.d/qfeeds.inc b/security/q-feeds-connector/src/etc/inc/plugins.inc.d/qfeeds.inc new file mode 100644 index 0000000000..0d70f598a1 --- /dev/null +++ b/security/q-feeds-connector/src/etc/inc/plugins.inc.d/qfeeds.inc @@ -0,0 +1,35 @@ +configdRun('template reload OPNsense/QFeeds')); + if (strtolower($res) != 'ok') { + throw new UserException(sprintf(gettext("Unable to update settings (%s)"), $res)); + } + $res = trim($backend->configdRun('qfeeds reconfigure')); + if (strpos($res, 'EXIT OK') === false) { + throw new UserException($res); + } + return ['status' => 'ok', 'output' => $res]; + } + + public function searchFeedsAction() + { + $records = []; + $data = json_decode((new Backend())->configdRun('qfeeds info') ?? '[]', true); + if (!empty($data) && !empty($data['feeds'])) { + $records = $data['feeds']; + foreach ($records as &$record) { + $record['licensed'] = $record['licensed'] ? '1' : '0'; + } + } + return $this->searchRecordsetBase($records); + } + + public function searchEventsAction() + { + $records = []; + $ifnames = []; + foreach (Config::getInstance()->object()->interfaces->children() as $key => $node) { + if (!empty((string)$node->if)) { + $ifnames[(string)$node->if] = !empty((string)($node->descr)) ? (string)($node->descr) : strtoupper($key); + } + } + $data = json_decode((new Backend())->configdRun('qfeeds logs') ?? '[]', true); + if (!empty($data) && !empty($data['rows'])) { + foreach ($data['rows'] as $row) { + $records[] = [ + 'timestamp' => $row[0], + 'interface' => $ifnames[$row[1]] ?? $row[1], + 'direction' => $row[2], + 'source' => $row[3], + 'destination' => $row[4], + 'source_port' => $row[5] ?? '', + 'destination_port' => $row[6] ?? '', + ]; + } + } + return $this->searchRecordsetBase($records); + } + + public function statsAction() + { + $stats = json_decode((new Backend())->configdRun('qfeeds stats'), true); + if (!empty($stats) && !empty($stats['feeds'])) { + $info = json_decode((new Backend())->configdRun('qfeeds info'), true); + if (!empty($info) && !empty($info['feeds'])) { + $feeds = []; + foreach ($info['feeds'] as $feed) { + $feeds[$feed['feed_type']] = $feed; + } + foreach ($stats['feeds'] as &$feed) { + if (isset($feeds[$feed['name']])) { + $tmp = $feeds[$feed['name']]; + $feed['updated_at'] = $tmp['updated_at']; + $feed['next_update'] = $tmp['next_update']; + $feed['licensed'] = $tmp['licensed']; + } + } + } + // Add license information from company_info if available + if (!empty($info['company_info'])) { + $stats['license'] = [ + 'name' => $info['company_info']['license_name'] ?? null, + 'expiry_date' => $info['company_info']['license_expiry_date'] ?? null + ]; + } + } + return $stats; + } +} diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/IndexController.php b/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/IndexController.php new file mode 100644 index 0000000000..d2017c9e69 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/IndexController.php @@ -0,0 +1,40 @@ +view->formSettings = $this->getForm("settings"); + $this->view->pick('OPNsense/QFeeds/index'); + } +} diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/forms/settings.xml b/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/forms/settings.xml new file mode 100644 index 0000000000..0469ed6ca4 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/controllers/OPNsense/QFeeds/forms/settings.xml @@ -0,0 +1,18 @@ +
    + + header + + + + connect.general.apikey + + text + click here]]> + + + connect.general.enable_unbound_bl + + checkbox + Use domain feeds in Unbound DNS blocklist, requires blocklists to be enabled in order to have effect + +
    diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/QfeedsAliases.php b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/QfeedsAliases.php new file mode 100644 index 0000000000..f6e33c770c --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/Firewall/DynamicAliases/QfeedsAliases.php @@ -0,0 +1,56 @@ +configdRun('qfeeds index') ?? '', true) ?? []; + if (is_array($payload) && !empty($payload['feeds'])) { + foreach ($payload['feeds'] as $feed) { + if ($feed['type'] == 'ip' && !empty($feed['licensed'])) { + $name = '__qfeeds_' . $feed['feed_type']; + $result[$name] = [ + 'enabled' => '1', + 'counters' => '1', + 'name' => $name, + 'type' => 'external', + 'description' => $feed['feed_type'], + 'content' => '' + ]; + } + } + } + return $result; + } +} diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/ACL/ACL.xml b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/ACL/ACL.xml new file mode 100644 index 0000000000..ca1468d637 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + Services: QFeeds + + ui/q_feeds/* + api/q_feeds/* + + + diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Connector.php b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Connector.php new file mode 100644 index 0000000000..f81bceb4b0 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Connector.php @@ -0,0 +1,39 @@ + + //OPNsense/QFeedsConnector + 1.0.0 + QFeeds connector + + + + + + + diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Menu/Menu.xml b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Menu/Menu.xml new file mode 100644 index 0000000000..dda28fa4dd --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/models/OPNsense/QFeeds/Menu/Menu.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/security/q-feeds-connector/src/opnsense/mvc/app/views/OPNsense/QFeeds/index.volt b/security/q-feeds-connector/src/opnsense/mvc/app/views/OPNsense/QFeeds/index.volt new file mode 100644 index 0000000000..21d3b54c7e --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/mvc/app/views/OPNsense/QFeeds/index.volt @@ -0,0 +1,173 @@ +{# + +OPNsense® is Copyright © 2025 by Deciso B.V. +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':formSettings,'id':'frm_settings'])}} +
    +
    + + + + + + + + + + + + +
    {{ lang._('Description') }}{{ lang._('Type') }}{{ lang._('Updated at') }}{{ lang._('Next update') }}{{ lang._('Licensed') }}
    +
    +
    + + + + + + + + + + + + + + + +
    {{ lang._('Timestamp') }}{{ lang._('Interface') }}{{ lang._('Direction') }}{{ lang._('Source') }}{{ lang._('Source Port') }}{{ lang._('Destination') }}{{ lang._('Destination Port') }}
    +
    {{ lang._('Collected events from the firewall log for QFeed aliases') }}
    +
    +
    + +
    +
    +
    +
    + +

    +
    +
    +
    diff --git a/security/q-feeds-connector/src/opnsense/scripts/dnscryptproxy/blocklists/qfeeds_bl.py b/security/q-feeds-connector/src/opnsense/scripts/dnscryptproxy/blocklists/qfeeds_bl.py new file mode 100755 index 0000000000..b5f77a328b --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/dnscryptproxy/blocklists/qfeeds_bl.py @@ -0,0 +1,90 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" + +import os +import glob +import re + +# Check if 'qf' is selected in DNSCrypt-proxy DNSBL configuration +def is_qf_selected(): + rc_conf_file = '/etc/rc.conf.d/dnscrypt_proxy' + if os.path.exists(rc_conf_file): + try: + with open(rc_conf_file, 'r') as f: + for line in f: + # Look for dnscrypt_proxy_dnsbl="..." line + match = re.search(r'dnscrypt_proxy_dnsbl="([^"]*)"', line) + if match: + dnsbl_list = match.group(1) + # Check if 'qf' is in the comma-separated list + return 'qf' in [x.strip() for x in dnsbl_list.split(',')] + except Exception: + pass + return False + +# Q-Feeds domain files directory +qfeeds_tables_dir = '/var/db/qfeeds-tables' + +# Automatically find all domain files (*_domains.txt) in qfeeds-tables directory +# This will include malware_domains.txt, phishing_domains.txt, etc. +qfeeds_filenames = [] +if os.path.isdir(qfeeds_tables_dir): + # Find all files ending with _domains.txt (e.g., malware_domains.txt, phishing_domains.txt) + pattern = os.path.join(qfeeds_tables_dir, '*_domains.txt') + qfeeds_filenames = sorted(glob.glob(pattern)) + +# Collect q-feeds domains +qfeeds_domains = set() +for filename in qfeeds_filenames: + if os.path.exists(filename): + with open(filename, 'r') as f_in: + for line in f_in: + domain = line.strip() + if domain: + qfeeds_domains.add(domain) + +# Write q-feeds domains to blacklist-qfeeds.txt +# dnscrypt-proxy's dnsbl.sh qfeeds() function will read this file when 'qf' is selected in DNSBL config +qfeeds_blacklist_file = '/usr/local/etc/dnscrypt-proxy/blacklist-qfeeds.txt' +dnscrypt_proxy_dir = '/usr/local/etc/dnscrypt-proxy' + +# Only proceed if DNSCrypt-proxy directory exists (plugin is installed) AND 'qf' is selected +if os.path.isdir(dnscrypt_proxy_dir) and is_qf_selected(): + if qfeeds_domains: + # Write q-feeds domains to separate file + with open(qfeeds_blacklist_file, 'w') as f_out: + for domain in sorted(qfeeds_domains): + f_out.write("%s\n" % domain) + else: + # Remove q-feeds blacklist file if no domains available + if os.path.exists(qfeeds_blacklist_file): + os.remove(qfeeds_blacklist_file) +elif os.path.isdir(dnscrypt_proxy_dir): + # DNSCrypt-proxy is installed but 'qf' is not selected - remove the file if it exists + if os.path.exists(qfeeds_blacklist_file): + os.remove(qfeeds_blacklist_file) diff --git a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/__init__.py b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/__init__.py new file mode 100755 index 0000000000..f661bbfd39 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/__init__.py @@ -0,0 +1,206 @@ +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" +import os +import subprocess +import time +import ujson +from datetime import datetime +from lib.api import Api +from lib.log import PFLogCrawler +from lib.file import LockedFile + + +class QFeedsActions: + def __init__(self, target_dir, forced=False): + self._target_dir = target_dir + self._forced = forced + + @classmethod + def list_actions(cls): + return [ + 'fetch_index', + 'fetch', + 'show_index', + 'firewall_load', + 'unbound_load', + 'dnscryptproxy_load', + 'update', + 'stats', + 'logs' + ] + + @property + def index_file(self): + return "%s/index.json" % self._target_dir + + @property + def index(self): + if not os.path.exists(self.index_file) and self._forced: + # require index file to get feeds + list(self.fetch_index()) + elif not os.path.exists(self.index_file): + return {} + data = ujson.load(open(self.index_file)) or {} + if type(data) is dict: + for feed in data.get('feeds', []): + feed['local_filename'] = "%s/%s.txt" % (self._target_dir, feed['feed_type']) + feed['updated_at_dt'] = datetime.fromisoformat(feed['updated_at']).timestamp() + feed['next_update_dt'] = datetime.fromisoformat(feed['next_update']).timestamp() + + return data + + def _file_stat(self, filename): + if not os.path.exists(filename): + return 0 + return os.stat(filename).st_mtime + + def fetch_index(self): + if not os.path.isdir(self._target_dir): + os.makedirs(self._target_dir) + with LockedFile(self.index_file) as f: + payload = Api().licenses() + f.truncate() + f.write(ujson.dumps(payload)) + yield 'downloaded index to %s' % f.filename + + def show_index(self): + yield ujson.dumps(self.index) + + def fetch(self): + for feed in self.index.get('feeds', []): + if feed['licensed'] and feed['updated_at_dt'] != self._file_stat(feed['local_filename']): + with LockedFile(feed['local_filename']) as f: + counter = 0 + for entry in Api().fetch(feed['feed_type']): + if counter == 0: + f.truncate() + f.write("%s\n" % entry) + counter += 1 + os.utime(feed['local_filename'], (feed['updated_at_dt'], feed['updated_at_dt'])) + yield "downloaded %d entries into %s [%s]" % (counter, feed['local_filename'], feed['updated_at']) + elif feed['licensed']: + yield "skipped %s [%s]" % (feed['local_filename'], feed['updated_at']) + + def firewall_load(self): + for feed in self.index.get('feeds', []): + if feed['licensed'] and os.path.exists(feed['local_filename']) and feed['type'] == 'ip': + table_name = '__qfeeds_%s' % feed['feed_type'] + sp = subprocess.run( + ['/sbin/pfctl', '-t', table_name, '-T', 'replace', '-f', feed['local_filename']], + capture_output=True, + text=True + ) + yield 'load feed %s [%s]' % (feed['feed_type'], sp.stderr.strip().replace("\n", " ")) + + def unbound_load(self): + bl_conf = '/usr/local/etc/unbound/qfeeds-blocklists.conf' + if os.path.exists(bl_conf) and os.path.getsize(bl_conf) > 20: + # when qfeeds-blocklists.conf is ~empty, skip updates + subprocess.run(['/usr/local/sbin/configctl', 'unbound', 'dnsbl']) + yield 'update unbound blocklist' + + def dnscryptproxy_load(self): + script_path = '/usr/local/opnsense/scripts/dnscryptproxy/blocklists/qfeeds_bl.py' + dnscrypt_proxy_dir = '/usr/local/etc/dnscrypt-proxy' + if os.path.exists(script_path) and os.path.isdir(dnscrypt_proxy_dir): + subprocess.run([script_path], capture_output=True, text=True) + # Trigger dnscrypt-proxy DNSBL update to merge blacklist-qfeeds.txt + # Only if DNSCrypt-proxy is installed (directory exists) + result = subprocess.run(['/usr/local/sbin/configctl', 'dnscryptproxy', 'dnsbl'], + capture_output=True, text=True) + if result.returncode == 0: + yield 'update dnscrypt-proxy blocklist' + else: + yield 'dnscrypt-proxy not available' + elif not os.path.isdir(dnscrypt_proxy_dir): + yield 'dnscrypt-proxy not installed' + else: + yield 'dnscrypt-proxy blocklist script not found' + + def update(self): + update_sleep = 99999 + try: + index_payload = self.index + except TypeError: + # when the index can't be parsed, assume we have none while updating + index_payload = {} + do_update = len(index_payload.get('feeds', [])) == 0 + for feed in index_payload.get('feeds', []): + update_sleep = min(feed['next_update_dt'] - time.time(), update_sleep) + if feed['licensed'] and update_sleep <= 300: # 5 minute cron interval + do_update = True + if do_update: + if 0 < update_sleep <= 300: + time.sleep(update_sleep) + for action in ['fetch_index', 'fetch', 'firewall_load', 'unbound_load', 'dnscryptproxy_load']: + yield from getattr(self, action)() + + def stats(self): + result = {'feeds': []} + for feed in self.index.get('feeds', []): + if feed['licensed'] and os.path.exists(feed['local_filename']) and feed['type'] == 'ip': + table_name = '__qfeeds_%s' % feed['feed_type'] + sp = subprocess.Popen( + ['/sbin/pfctl', '-t', table_name, '-vT', 'show'], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True + ) + record = { + 'name': feed['feed_type'], + 'total_entries': 0, + 'packets_blocked': 0, + 'bytes_blocked': 0, + 'addresses_blocked': 0 + } + while (line := sp.stdout.readline()): + if line.startswith(' '): + record['total_entries'] += 1 + elif 'Packets:' in line and 'Packets: 0 ' not in line: + parts = line.split() + if parts[3].isdigit() and parts[5].isdigit() and parts[0].lower().find('block') > 0: + record['packets_blocked'] += int(parts[3]) + record['bytes_blocked'] += int(parts[5]) + record['addresses_blocked'] += 1 + + result['feeds'].append(record) + result['totals'] = { + 'entries': sum(r['total_entries'] for r in result['feeds']), + # assumes no overlaps in datafeeds + 'addresses_blocked': sum(r['addresses_blocked'] for r in result['feeds']), + 'packets_blocked': sum(r['packets_blocked'] for r in result['feeds']), + 'bytes_blocked': sum(r['bytes_blocked'] for r in result['feeds']), + } + + yield ujson.dumps(result) + + def logs(self): + feeds = [] + for feed in self.index.get('feeds', []): + if feed['type'] == 'ip': + feeds.append('__qfeeds_%s' % feed['feed_type']) + + yield ujson.dumps({'rows': PFLogCrawler(feeds).find()}) diff --git a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/api.py b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/api.py new file mode 100755 index 0000000000..6f2bb6a3cb --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/api.py @@ -0,0 +1,70 @@ +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" +import os +import requests +from configparser import ConfigParser + + +class QFeedsConfig: + api_key = None + + def __init__(self): + config_filename = '/usr/local/etc/qfeeds.conf' + if os.path.isfile(config_filename): + cnf = ConfigParser() + cnf.read(config_filename) + if cnf.has_section('api') and cnf.has_option('api', 'key'): + self.api_key = cnf.get('api', 'key') + + +class Api: + def __init__(self): + self.api_key = QFeedsConfig().api_key + + def licenses(self): + r = requests.get( + url='https://api.qfeeds.com/licenses.php', + auth=('api_token', self.api_key), + timeout=60, + headers={'User-Agent': 'Q-Feeds_OPNsense'} + ) + r.raise_for_status() + return r.json() + + def fetch(self, feed): + r = requests.get( + url='https://api.qfeeds.com/api.php', + params={'feed_type': feed}, + auth=('api_token', self.api_key), + headers={'User-Agent': 'Q-Feeds_OPNsense'}, + stream=True, + timeout=60 + ) + r.raise_for_status() + for line in r.raw: + entry = line.decode().strip() + if entry: + yield entry diff --git a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/file.py b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/file.py new file mode 100755 index 0000000000..89fbf8ae8a --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/file.py @@ -0,0 +1,52 @@ +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" +import fcntl + + +class LockedFile: + def __init__(self, filename): + self._filename = filename + self._fh = None + + def __enter__(self): + self._fh = open(self._filename, 'a+') + fcntl.flock(self._fh, fcntl.LOCK_EX | fcntl.LOCK_NB) + return self + + def __exit__(self, ex_type, ex_value, traceback): + if self._fh: + self._fh.close() + + def truncate(self): + self._fh.seek(0) + self._fh.truncate() + + def write(self, data): + self._fh.write(data) + + @property + def filename(self): + return self._filename diff --git a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/log.py b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/log.py new file mode 100755 index 0000000000..d3aca4ee2e --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/log.py @@ -0,0 +1,86 @@ +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" + +import glob +import time +import subprocess +import ipaddress + +def is_ip_address(value): + try: + ipaddress.ip_address(value) + return True + except ValueError: + return False + + +class PFLogCrawler: + def __init__(self, table_names:list=[]): + self._table_names = table_names + self._rule_ids = set() + self._collect_rule_ids() + + def _collect_rule_ids(self): + self._rule_ids = set() + sp = subprocess.run(['/sbin/pfctl', '-sr'], capture_output=True, text=True) + for line in sp.stdout.split("\n"): + for table in self._table_names: + if line.find("<%s>" % table) > 0: + self._rule_ids.add(line.split()[-1].strip('"')) + if 'label "' in line: + quote_start = line.find('"', line.find('label "')) + quote_end = line.find('"', quote_start + 1) + if quote_end > quote_start: + self._rule_ids.add(line[quote_start + 1:quote_end]) + + @staticmethod + def _parse_log_line(line): + # quick scan for datetime, interface, direction, source, dest, source_port, dest_port + parts = line.split() + fw_line = parts[-1].split(',') # strip syslog + ip_addresses = [x for x in fw_line if is_ip_address(x)] + # Find destination IP position to get ports from next fields (only if numeric) + dest_idx = fw_line.index(ip_addresses[1]) if len(ip_addresses) > 1 else len(fw_line) + source_port = fw_line[dest_idx + 1] if dest_idx + 1 < len(fw_line) and fw_line[dest_idx + 1].isdigit() else '' + dest_port = fw_line[dest_idx + 2] if dest_idx + 2 < len(fw_line) and fw_line[dest_idx + 2].isdigit() else '' + return [parts[1], fw_line[4], fw_line[7]] + ip_addresses + [source_port, dest_port] + + def find(self, max_time=60, max_results=50000): + result = [] + start_time = time.time() + rows_processed = 0 + for filename in sorted(glob.glob("/var/log/filter/filter_*.log"), reverse=True): + with open(filename) as f_in: + for idx, line in enumerate(f_in): + for rule_id in self._rule_ids: + if rule_id in line: + result.append(self._parse_log_line(line)) + rows_processed +=1 + break # inner loop + if (idx % 100000 == 0 and time.time() - start_time > max_time) or rows_processed >= max_results: + return result + + return result diff --git a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/qfeedsctl.py b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/qfeedsctl.py new file mode 100755 index 0000000000..7973d59f4c --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/qfeedsctl.py @@ -0,0 +1,63 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" + +import argparse +import sys +import ujson +from requests.exceptions import HTTPError, Timeout +from lib import QFeedsActions + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--target_dir', default='/var/db/qfeeds-tables') + parser.add_argument('-f', help='forced (auto index)' , default=False, action='store_true') + parser.add_argument('-v', help='verbose output' , default=False, action='store_true') + parser.add_argument("action", choices=QFeedsActions.list_actions(), nargs='*') + args = parser.parse_args() + if args.v: + # verbose mode + import http.client as http_client + http_client.HTTPConnection.debuglevel = 1 + try: + actions = QFeedsActions(args.target_dir, args.f) + for action in args.action: + for msg in getattr(actions, action)(): + print(msg) + except HTTPError as exc: + print('exit with HTTPError %d (%s)' % (exc.response.status_code, exc.response.text)) + sys.exit(-1) + except Timeout as exc: + print('timeout reaching api endpoint') + sys.exit(-1) + except IOError as e: + print("output filename locked or missing") + sys.exit(-1) + except ujson.JSONDecodeError: + print("JSON decode error") + sys.exit(-1) diff --git a/security/q-feeds-connector/src/opnsense/scripts/unbound/blocklists/qfeeds_bl.py b/security/q-feeds-connector/src/opnsense/scripts/unbound/blocklists/qfeeds_bl.py new file mode 100755 index 0000000000..e10428db08 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/unbound/blocklists/qfeeds_bl.py @@ -0,0 +1,58 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2025 Deciso B.V. + 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. +""" + +import os +from . import BaseBlocklistHandler + +class DefaultBlocklistHandler(BaseBlocklistHandler): + def __init__(self): + super().__init__('/usr/local/etc/unbound/qfeeds-blocklists.conf') + self.priority = 50 + + def get_config(self): + # do not use, unbound worker settings + return {} + + def get_blocklist(self): + # Only return domains if integration is enabled (filenames are offered) + qfeeds_filenames = [] + if self.cnf and self.cnf.has_section('settings'): + if self.cnf.has_option('settings', 'filenames'): + qfeeds_filenames = self.cnf.get('settings', 'filenames').split(',') + + result = {} + for filename in qfeeds_filenames: + bl_shortcode = "qf_%s" % os.path.splitext(os.path.basename(filename).strip())[0] + if os.path.exists(filename): + with open(filename, 'r') as f_in: + for line in f_in: + result[line.strip()] = {'bl': bl_shortcode, 'wildcard': False} + return result + + def get_passlist_patterns(self): + return [] diff --git a/security/q-feeds-connector/src/opnsense/service/conf/actions.d/actions_qfeeds.conf b/security/q-feeds-connector/src/opnsense/service/conf/actions.d/actions_qfeeds.conf new file mode 100644 index 0000000000..9558b9c88a --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/service/conf/actions.d/actions_qfeeds.conf @@ -0,0 +1,40 @@ +[reconfigure] +command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py fetch_index fetch firewall_load unbound_load dnscryptproxy_load && echo 'EXIT OK' +parameters: +type:script_output +message:reconfigure QFeeds +errors:no + +[update] +command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py update +parameters: +type:script_output +message:update QFeeds +errors:no + +[info] +command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py show_index +parameters: +type:script_output +message:fetch QFeeds info + +[stats] +command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py stats +parameters: +type:script_output +cache_ttl: 3600 +message:return Qfeeds local stats + +[logs] +command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py logs +parameters: +type:script_output +cache_ttl: 300 +message:return Qfeeds log data + +[index] +command:cat /var/db/qfeeds-tables/index.json +parameters: +type:script_output +message:return raw QFeeds index file +cache_ttl: 60 diff --git a/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/+TARGETS b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/+TARGETS new file mode 100644 index 0000000000..219dc39f84 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/+TARGETS @@ -0,0 +1,2 @@ +qfeeds.conf:/usr/local/etc/qfeeds.conf +qfeeds-blocklists.conf:/usr/local/etc/unbound/qfeeds-blocklists.conf diff --git a/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds-blocklists.conf b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds-blocklists.conf new file mode 100644 index 0000000000..6113177758 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds-blocklists.conf @@ -0,0 +1,5 @@ +{% if not helpers.empty('OPNsense.QFeedsConnector.general.apikey') and + not helpers.empty('OPNsense.QFeedsConnector.general.enable_unbound_bl') %} +[settings] +filenames=/var/db/qfeeds-tables/malware_domains.txt +{% endif %} diff --git a/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds.conf b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds.conf new file mode 100644 index 0000000000..be8460cb8e --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/service/templates/OPNsense/QFeeds/qfeeds.conf @@ -0,0 +1,4 @@ +{% if not helpers.empty('OPNsense.QFeedsConnector.general.apikey') %} +[api] +key={{OPNsense.QFeedsConnector.general.apikey}} +{% endif %} diff --git a/security/q-feeds-connector/src/opnsense/www/img/QFeeds.png b/security/q-feeds-connector/src/opnsense/www/img/QFeeds.png new file mode 100644 index 0000000000..5dd5e38a5c Binary files /dev/null and b/security/q-feeds-connector/src/opnsense/www/img/QFeeds.png differ diff --git a/security/q-feeds-connector/src/opnsense/www/js/widgets/Metadata/QFeeds.xml b/security/q-feeds-connector/src/opnsense/www/js/widgets/Metadata/QFeeds.xml new file mode 100644 index 0000000000..faec76e2a2 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/www/js/widgets/Metadata/QFeeds.xml @@ -0,0 +1,20 @@ + + + QFeeds.js + + /api/q_feeds/settings/* + + + Q-Feeds Threat Protection + Unable to contact information feed. + Installed Feeds + Database + Size + Blocked + Updated + Next + Licensed + Unlicensed + + + diff --git a/security/q-feeds-connector/src/opnsense/www/js/widgets/QFeeds.js b/security/q-feeds-connector/src/opnsense/www/js/widgets/QFeeds.js new file mode 100644 index 0000000000..d69ed21cd4 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/www/js/widgets/QFeeds.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2025 Deciso B.V. + * 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. + */ + +export default class QFeeds extends BaseTableWidget { + constructor() { + super(); + } + + getMarkup() { + let $container = $('
    '); + let $sysinfotable = this.createTable('qfeeds-table', { + headerPosition: 'left', + }); + $container.append($sysinfotable); + return $container; + } + + async onWidgetTick() { + return; + } + + async onMarkupRendered() { + let header = $("div.widget.widget-qfeeds").find('.widget-header'); + let title = $('#qfeeds-title'); + let divider = $("div.widget.widget-qfeeds").find('.panel-divider'); + header.css({ + 'background-image': 'URL("/ui/img/QFeeds.png")', + 'background-size': 'auto 50px', + 'background-position': 'center left', + 'margin-top': '0px', + 'mix-blend-mode': 'difference', + 'background-repeat': 'no-repeat' + }); + title.empty(); + title.css({ + 'height': '70px' + }) + divider.hide(); + $("#qfeeds-table").css({ + 'margin-top': '0px', + 'margin-bottom': '5px', + }); + + const data = await this.ajaxCall(`/api/q_feeds/settings/${'stats'}`); + if (!data.feeds.length) { + $('#qfeeds-table').html(`${this.translations.no_feed}`); + return; + } + let rows = []; + let feeds = []; + let licenseInfoShown = false; + + for (let feed of data.feeds) { + feeds.push( + ` ${feed.name}`, + `
      ${this.translations.last_update}: ${feed.updated_at}
    `, + `
      ${this.translations.next_update}: ${feed.next_update}
    ` + ); + if (feed.licensed) { + let licenseText = this.translations.licensed; + if (data.license && data.license.name) { + licenseText += ` (${data.license.name})`; + } + feeds.push(`
      ${licenseText}
    `); + if (!licenseInfoShown && data.license && data.license.expiry_date) { + const expiryDate = new Date(data.license.expiry_date); + if (!isNaN(expiryDate.getTime())) { + const formattedDate = expiryDate.toLocaleDateString(); + feeds.push(`
      Expires: ${formattedDate}
    `); + } + licenseInfoShown = true; + } + } else { + feeds.push(`
      ${this.translations.unlicensed}
    `); + } + } + rows.push([[this.translations.installed_feeds], feeds]); + let db = [ + `
    ${this.translations.size}: ${data.totals.entries.toLocaleString()}
    `, + `
    ${this.translations.blocked}: ${data.totals.addresses_blocked.toLocaleString()}
    ` + + ]; + rows.push([[this.translations.database], db]); + + super.updateTable('qfeeds-table', rows); + } +} diff --git a/security/softether/Makefile b/security/softether/Makefile deleted file mode 100644 index c595499554..0000000000 --- a/security/softether/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -PLUGIN_NAME= softether -PLUGIN_VERSION= 0.2 -PLUGIN_COMMENT= Cross-platform Multi-protocol VPN Program -PLUGIN_DEPENDS= softether -PLUGIN_MAINTAINER= m.muenz@gmail.com -PLUGIN_DEVEL= yes - -.include "../../Mk/plugins.mk" diff --git a/security/softether/pkg-descr b/security/softether/pkg-descr deleted file mode 100644 index e49e2da52d..0000000000 --- a/security/softether/pkg-descr +++ /dev/null @@ -1,8 +0,0 @@ -SoftEther VPN ("SoftEther" means "Software Ethernet") is one of -the world's most powerful and easy-to-use multi-protocol VPN -software. It runs on Windows, Linux, Mac, FreeBSD and Solaris. - -SoftEther VPN is open source. You can use SoftEther for any -personal or commercial use for free charge. - -WWW: https://www.softether.org/ diff --git a/security/softether/src/etc/inc/plugins.inc.d/softether.inc b/security/softether/src/etc/inc/plugins.inc.d/softether.inc deleted file mode 100644 index e11d8b8392..0000000000 --- a/security/softether/src/etc/inc/plugins.inc.d/softether.inc +++ /dev/null @@ -1,67 +0,0 @@ - - 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. -*/ - -function softether_enabled() -{ - $model = new \OPNsense\Softether\General(); - return (string)$model->enabled == '1'; -} - -function softether_carp_enabled() -{ - $model = new \OPNsense\Softether\General(); - return (string)$model->enabled == '1' && - (string)$model->enablecarp == '1'; -} - -function softether_carp_interfaces() -{ - $model = new \OPNsense\Softether\General(); - return (string)$model->carpinterfaces; -} - -function softether_services() -{ - $services = array(); - - if (!softether_enabled()) { - return $services; - } - - $services[] = array( - 'description' => gettext('SoftEther VPN'), - 'configd' => array( - 'restart' => array('softether restart'), - 'start' => array('softether start'), - 'stop' => array('softether stop'), - ), - 'name' => 'vpnserver' - ); - - return $services; -} diff --git a/security/softether/src/etc/rc.syshook.d/carp/50-softether b/security/softether/src/etc/rc.syshook.d/carp/50-softether deleted file mode 100755 index ca50c43ca6..0000000000 --- a/security/softether/src/etc/rc.syshook.d/carp/50-softether +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/local/bin/php - - * Copyright (C) 2004 Scott Ullrich - * 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. - */ - -require_once('config.inc'); -require_once('util.inc'); -require_once('interfaces.inc'); -require_once('plugins.inc.d/softether.inc'); - -if (softether_carp_enabled()) { - // XXX: carp enable/disable mode - $subsystem = !empty($argv[1]) ? $argv[1] : ''; - $type = !empty($argv[2]) ? $argv[2] : ''; - - if ($type != 'MASTER' && $type != 'BACKUP') { - log_error("Carp '$type' event unknown from source '{$subsystem}'"); - exit(1); - } - - if (!strstr($subsystem, '@')) { - log_error("Carp '$type' event triggered from wrong source '{$subsystem}'"); - exit(1); - } - - list ($vhid, $iface) = explode('@', $subsystem); - $friendly = convert_real_interface_to_friendly_interface_name($iface); - - if (!(strpos(softether_carp_interfaces(),$friendly) !== false)) { - exit(0); - } - - switch ($type) { - case 'MASTER': - touch('/var/run/softether/CARP_MASTER'); - shell_exec('/usr/local/etc/rc.d/softether_server start'); - break; - case 'BACKUP': - if (file_exists('/var/run/softether/CARP_MASTER')) { - unlink('/var/run/softether/CARP_MASTER'); - } - shell_exec('/usr/local/etc/rc.d/softether_server stop'); - break; - } -} diff --git a/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/Api/GeneralController.php b/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/Api/GeneralController.php deleted file mode 100644 index 5844a20752..0000000000 --- a/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/Api/GeneralController.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * 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. - * - */ - -namespace OPNsense\Softether\Api; - -use OPNsense\Base\ApiMutableModelControllerBase; - -class GeneralController extends ApiMutableModelControllerBase -{ - protected static $internalModelClass = '\OPNsense\Softether\General'; - protected static $internalModelName = 'general'; -} diff --git a/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/Api/ServiceController.php b/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/Api/ServiceController.php deleted file mode 100644 index ac6b37084c..0000000000 --- a/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/Api/ServiceController.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * 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. - * - */ - -namespace OPNsense\Softether\Api; - -use OPNsense\Base\ApiMutableServiceControllerBase; -use OPNsense\Core\Backend; -use OPNsense\Softether\General; - -/** - * Class ServiceController - * @package OPNsense\Softether - */ -class ServiceController extends ApiMutableServiceControllerBase -{ - protected static $internalServiceClass = '\OPNsense\Softether\General'; - protected static $internalServiceTemplate = 'OPNsense/Softether'; - protected static $internalServiceEnabled = 'enabled'; - protected static $internalServiceName = 'softether'; -} diff --git a/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/GeneralController.php b/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/GeneralController.php deleted file mode 100644 index 7d83f2871a..0000000000 --- a/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/GeneralController.php +++ /dev/null @@ -1,38 +0,0 @@ - - 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. -*/ - -namespace OPNsense\Softether; - -class GeneralController extends \OPNsense\Base\IndexController -{ - public function indexAction() - { - $this->view->generalForm = $this->getForm("general"); - $this->view->pick('OPNsense/Softether/general'); - } -} diff --git a/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/forms/general.xml b/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/forms/general.xml deleted file mode 100644 index 8e738f5d41..0000000000 --- a/security/softether/src/opnsense/mvc/app/controllers/OPNsense/Softether/forms/general.xml +++ /dev/null @@ -1,21 +0,0 @@ -
    - - general.enabled - - checkbox - This will activate SoftEther vpnserver process. - - - general.enablecarp - - checkbox - This will activate the vpnserver service only on the master device. - - - general.carpinterfaces - - select_multiple - - Type or select interface. - -
    diff --git a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/ACL/ACL.xml b/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/ACL/ACL.xml deleted file mode 100644 index d7a964e55a..0000000000 --- a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/ACL/ACL.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - VPN: SoftEther - - ui/softether/* - api/softether/* - - - diff --git a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/General.php b/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/General.php deleted file mode 100644 index 20ffb40cf3..0000000000 --- a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/General.php +++ /dev/null @@ -1,35 +0,0 @@ - - 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. -*/ - -namespace OPNsense\Softether; - -use OPNsense\Base\BaseModel; - -class General extends BaseModel -{ -} diff --git a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/General.xml b/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/General.xml deleted file mode 100644 index 05968a34c2..0000000000 --- a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/General.xml +++ /dev/null @@ -1,24 +0,0 @@ - - //OPNsense/softether/general - Softether configuration - 0.0.1 - - - 0 - Y - - - 0 - Y - - - N - Y - - Y - - /^(?!0).*$/ - - - - diff --git a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/Menu/Menu.xml b/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/Menu/Menu.xml deleted file mode 100644 index 6bebfff377..0000000000 --- a/security/softether/src/opnsense/mvc/app/models/OPNsense/Softether/Menu/Menu.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/security/softether/src/opnsense/mvc/app/views/OPNsense/Softether/general.volt b/security/softether/src/opnsense/mvc/app/views/OPNsense/Softether/general.volt deleted file mode 100644 index 7ddc14d6bb..0000000000 --- a/security/softether/src/opnsense/mvc/app/views/OPNsense/Softether/general.volt +++ /dev/null @@ -1,68 +0,0 @@ -{# - -OPNsense® is Copyright © 2014 – 2018 by Deciso B.V. -This file is Copyright © 2018 by Michael Muenz -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':generalForm,'id':'frm_general_settings'])}} -
    -
    - -
    -
    -
    -
    - - diff --git a/security/softether/src/opnsense/scripts/OPNsense/Softether/setup.sh b/security/softether/src/opnsense/scripts/OPNsense/Softether/setup.sh deleted file mode 100755 index f83ff509c7..0000000000 --- a/security/softether/src/opnsense/scripts/OPNsense/Softether/setup.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -mkdir -p /var/db/softether -mkdir -p /var/log/softether diff --git a/security/softether/src/opnsense/service/conf/actions.d/actions_softether.conf b/security/softether/src/opnsense/service/conf/actions.d/actions_softether.conf deleted file mode 100644 index 2470ac33fa..0000000000 --- a/security/softether/src/opnsense/service/conf/actions.d/actions_softether.conf +++ /dev/null @@ -1,23 +0,0 @@ -[start] -command:/usr/local/opnsense/scripts/OPNsense/Softether/setup.sh;/usr/local/etc/rc.d/softether_server start -parameters: -type:script -message:starting softether - -[stop] -command:/usr/local/etc/rc.d/softether_server stop -parameters: -type:script -message:stopping softether - -[restart] -command:/usr/local/opnsense/scripts/OPNsense/Softether/setup.sh;/usr/local/etc/rc.d/softether_server restart -parameters: -type:script -message:restarting softether - -[status] -command:sh /usr/local/etc/rc.d/softether_server status;exit 0 -parameters: -type:script_output -message:softether status diff --git a/security/softether/src/opnsense/service/templates/OPNsense/Softether/+TARGETS b/security/softether/src/opnsense/service/templates/OPNsense/Softether/+TARGETS deleted file mode 100644 index 04e32a9777..0000000000 --- a/security/softether/src/opnsense/service/templates/OPNsense/Softether/+TARGETS +++ /dev/null @@ -1 +0,0 @@ -softether_server:/etc/rc.conf.d/softether_server diff --git a/security/softether/src/opnsense/service/templates/OPNsense/Softether/softether_server b/security/softether/src/opnsense/service/templates/OPNsense/Softether/softether_server deleted file mode 100644 index 29fcf71906..0000000000 --- a/security/softether/src/opnsense/service/templates/OPNsense/Softether/softether_server +++ /dev/null @@ -1,9 +0,0 @@ -{% if helpers.exists('OPNsense.softether.general.enabled') and OPNsense.softether.general.enabled == '1' %} -softether_server_var_script="/usr/local/opnsense/scripts/OPNsense/Softether/setup.sh" -softether_server_enable="YES" -{% if helpers.exists('OPNsense.softether.general.enablecarp') and OPNsense.softether.general.enablecarp == '1' %} -required_files="/var/run/softether/CARP_MASTER" -{% endif %} -{% else %} -softether_server_enable="NO" -{% endif %} diff --git a/security/strongswan-legacy/Makefile b/security/strongswan-legacy/Makefile new file mode 100644 index 0000000000..f6a1ff3e6c --- /dev/null +++ b/security/strongswan-legacy/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= strongswan-legacy +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= IPsec legacy support +PLUGIN_DEPENDS= # strongswan +PLUGIN_MAINTAINER= ad@opnsense.org + +.include "../../Mk/plugins.mk" diff --git a/security/strongswan-legacy/pkg-descr b/security/strongswan-legacy/pkg-descr new file mode 100644 index 0000000000..4d829ceb3c --- /dev/null +++ b/security/strongswan-legacy/pkg-descr @@ -0,0 +1 @@ +This plugin adds the legacy IPsec tunnel and mobile configuration pages. diff --git a/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/ACL/ACL.xml b/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/ACL/ACL.xml new file mode 100644 index 0000000000..7441c7c6c0 --- /dev/null +++ b/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/ACL/ACL.xml @@ -0,0 +1,28 @@ + + + VPN: IPsec: Tunnels [legacy] + + ui/ipsec/tunnels + api/ipsec/tunnel/* + api/ipsec/legacy_subsystem/* + + + + VPN: IPsec: Edit Phase 1 + + vpn_ipsec_phase1.php* + + + + VPN: IPsec: Edit Phase 2 + + vpn_ipsec_phase2.php* + + + + VPN: IPsec: Mobile [legacy] + + vpn_ipsec_mobile.php* + + + diff --git a/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/Menu/Menu.xml b/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/Menu/Menu.xml new file mode 100644 index 0000000000..6a33b6cf60 --- /dev/null +++ b/security/strongswan-legacy/src/opnsense/mvc/app/models/OPNsense/IPsecLegacy/Menu/Menu.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/security/strongswan-legacy/src/www/vpn_ipsec_mobile.php b/security/strongswan-legacy/src/www/vpn_ipsec_mobile.php new file mode 100644 index 0000000000..23d00589aa --- /dev/null +++ b/security/strongswan-legacy/src/www/vpn_ipsec_mobile.php @@ -0,0 +1,310 @@ + + * 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. + */ + +require_once("guiconfig.inc"); +require_once("interfaces.inc"); +require_once("filter.inc"); +require_once("system.inc"); +require_once("plugins.inc.d/ipsec.inc"); + +config_read_array('ipsec', 'client'); +config_read_array('ipsec', 'phase1'); + +// define formfields +$form_fields = "pool_address,pool_netbits,pool_address_v6,pool_netbits_v6"; + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // pass savemessage + if (isset($_GET['savemsg'])) { + $savemsg = htmlspecialchars($_GET['savemsg']); + } + $pconfig = array(); + // defaults + $pconfig['pool_netbits'] = 24; + $pconfig['pool_netbits_v6'] = 64; + + // copy / initialize $pconfig attributes + foreach (explode(",", $form_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (isset($config['ipsec']['client'][$fieldname])) { + $pconfig[$fieldname] = $config['ipsec']['client'][$fieldname]; + } elseif (!isset($pconfig[$fieldname])) { + // initialize element + $pconfig[$fieldname] = null; + } + } + if (isset($config['ipsec']['client']['enable'])) { + $pconfig['enable'] = true; + } + +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + $input_errors = array(); + $pconfig = $_POST; + if (isset($_POST['create'])) { + // create new phase1 entry + header(url_safe('Location: /vpn_ipsec_phase1.php?mobile=true')); + exit; + } elseif (isset($_POST['apply'])) { + // apply changes + ipsec_configure_do(); + $savemsg = get_std_save_message(true); + clear_subsystem_dirty('ipsec'); + header(url_safe('Location: /vpn_ipsec_mobile.php?savemsg=%s', array($savemsg))); + exit; + } elseif (isset($_POST['submit'])) { + // save form changes + if (!empty($pconfig['pool_address']) && !is_ipaddr($pconfig['pool_address'])) { + $input_errors[] = gettext("A valid IPv4 address for 'Virtual IPv4 Address Pool Network' must be specified."); + } + + if (!empty($pconfig['pool_address_v6']) && !is_ipaddr($pconfig['pool_address_v6'])) { + $input_errors[] = gettext("A valid IPv6 address for 'Virtual IPv6 Address Pool Network' must be specified."); + } + + + if (count($input_errors) == 0) { + $client = array(); + $copy_fields = "pool_address,pool_netbits,pool_address_v6,pool_netbits_v6"; + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!empty($pconfig[$fieldname])) { + $client[$fieldname] = $pconfig[$fieldname]; + } + } + if (!empty($pconfig['enable'])) { + $client['enable'] = true; + } + + $config['ipsec']['client'] = $client; + + write_config(); + mark_subsystem_dirty('ipsec'); + header(url_safe('Location: /vpn_ipsec_mobile.php')); + exit; + } + } + + // initialize missing post attributes + foreach (explode(",", $form_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!isset($pconfig[$fieldname])) { + $pconfig[$fieldname] = null; + } + } +} + +legacy_html_escape_form_data($pconfig); + +$service_hook = 'strongswan'; + +include("head.inc"); + +?> + + + + + + + +
    +
    +
    +" . gettext("You must apply the changes in order for them to take effect.")); +} +$ph1found = false; +$legacy_radius_configured = false; +foreach ($config['ipsec']['phase1'] as $ph1ent) { + if (!isset($ph1ent['disabled']) && isset($ph1ent['mobile'])) { + $ph1found = true; + if (($ph1ent['authentication_method'] ?? '') == 'eap-radius') { + $legacy_radius_configured = true; + } + } +} + +function print_legacy_box($msg, $name, $value) +{ + $savebutton = "
    "; + $savebutton .= ""; + if (!empty($_POST['if'])) { + $savebutton .= ""; + } + $savebutton .= '
    '; + + echo << + +
    + +EOFnp; +} + +if (!empty($pconfig['enable']) && !$ph1found && !(new OPNsense\IPsec\Swanctl())->isEnabled()) { + print_legacy_box(gettext("Support for IPsec Mobile clients is enabled but a Phase1 definition was not found") . ".
    " . gettext("When using (legacy) tunnels, please click Create to define one."), "create", gettext("Create Phase1")); +} +if (isset($input_errors) && count($input_errors) > 0) { + print_input_errors($input_errors); +} +?> +
    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    + + +
    + /> + + +
    + onclick="pool_change()" /> + +
    + + +
    +
    + onclick="pool_v6_change()" /> + +
    + + +
    +
    +
    +
    +
    +
    + + + + + +
      + +
    +
    +
    +
    + +
    +
    +
    + + diff --git a/security/strongswan-legacy/src/www/vpn_ipsec_phase1.php b/security/strongswan-legacy/src/www/vpn_ipsec_phase1.php new file mode 100644 index 0000000000..f2548491cb --- /dev/null +++ b/security/strongswan-legacy/src/www/vpn_ipsec_phase1.php @@ -0,0 +1,1351 @@ + + * Copyright (C) 2014-2015 Deciso B.V. + * Copyright (C) 2008 Shrew Soft Inc. + * Copyright (C) 2003-2005 Manuel Kasper + * Copyright (C) 2014 Ermal Luçi + * 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. + */ + +require_once("guiconfig.inc"); +require_once("system.inc"); +require_once("filter.inc"); +require_once("interfaces.inc"); +require_once("plugins.inc.d/ipsec.inc"); + +/* + * ikeid management functions + */ + +function ipsec_ikeid_used($ikeid) { + global $config; + + if (!empty($config['ipsec']['phase1'])) { + foreach ($config['ipsec']['phase1'] as $ph1ent) { + if( $ikeid == $ph1ent['ikeid'] ) { + return true; + } + } + } + return false; +} + +function ipsec_ikeid_next() { + $ikeid = 1; + while(ipsec_ikeid_used($ikeid)) { + $ikeid++; + } + + return $ikeid; +} + +function ipsec_keypairs() +{ + $mdl = new \OPNsense\IPsec\IPsec(); + $node = $mdl->getNodeByReference('keyPairs.keyPair'); + + return $node ? $node->getNodes() : []; +} + +config_read_array('ipsec', 'phase1'); +config_read_array('ipsec', 'phase2'); + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // fetch data + if (isset($_GET['dup']) && is_numericint($_GET['dup'])) { + $p1index = $_GET['dup']; + } elseif (isset($_GET['p1index']) && is_numericint($_GET['p1index'])) { + $p1index = $_GET['p1index']; + } + $pconfig = array(); + + // generice defaults + $pconfig['interface'] = "wan"; + $pconfig['iketype'] = "ikev2"; + $phase1_fields = "mode,protocol,myid_type,myid_data,peerid_type,peerid_data + ,encryption-algorithm,lifetime,authentication_method,descr,nat_traversal,rightallowany,inactivity_timeout + ,interface,iketype,dpd_delay,dpd_maxfail,dpd_action,remote-gateway,pre-shared-key,certref,margintime,rekeyfuzz + ,caref,local-kpref,peer-kpref,reauth_enable,rekey_enable,auto,tunnel_isolation,authservers,mobike,keyingtries + ,closeaction,unique"; + if (isset($p1index) && isset($config['ipsec']['phase1'][$p1index])) { + // 1-on-1 copy + foreach (explode(",", $phase1_fields) as $fieldname) { + $fieldname = trim($fieldname); + if(isset($config['ipsec']['phase1'][$p1index][$fieldname])) { + $pconfig[$fieldname] = $config['ipsec']['phase1'][$p1index][$fieldname]; + } elseif (!isset($pconfig[$fieldname])) { + // initialize element + $pconfig[$fieldname] = null; + } + } + + // attributes with some kind of logic behind them... + if (!isset($_GET['dup']) || !is_numericint($_GET['dup'])) { + // don't copy the ikeid on dup + $pconfig['ikeid'] = $config['ipsec']['phase1'][$p1index]['ikeid']; + } + $pconfig['disabled'] = isset($config['ipsec']['phase1'][$p1index]['disabled']); + $pconfig['sha256_96'] = !empty($config['ipsec']['phase1'][$p1index]['sha256_96']); + $pconfig['installpolicy'] = empty($config['ipsec']['phase1'][$p1index]['noinstallpolicy']); // XXX: reversed + + foreach (array('authservers', 'dhgroup', 'hash-algorithm') as $fieldname) { + if (!empty($config['ipsec']['phase1'][$p1index][$fieldname])) { + $pconfig[$fieldname] = explode(',', $config['ipsec']['phase1'][$p1index][$fieldname]); + } else { + $pconfig[$fieldname] = array(); + } + } + + $pconfig['remotebits'] = null; + $pconfig['remotenet'] = null ; + if (isset($a_phase1[$p1index]['remote-subnet']) && strpos($config['ipsec']['phase1'][$p1index]['remote-subnet'],'/') !== false) { + list($pconfig['remotenet'],$pconfig['remotebits']) = explode("/", $config['ipsec']['phase1'][$p1index]['remote-subnet']); + } elseif (isset($config['ipsec']['phase1'][$p1index]['remote-subnet'])) { + $pconfig['remotenet'] = $config['ipsec']['phase1'][$p1index]['remote-subnet']; + } + + if (isset($config['ipsec']['phase1'][$p1index]['mobile'])) { + $pconfig['mobile'] = true; + } + } else { + /* defaults new */ + if (isset($config['interfaces']['lan'])) { + $pconfig['localnet'] = "lan"; + } + $pconfig['mode'] = "main"; + $pconfig['protocol'] = "inet"; + $pconfig['myid_type'] = "myaddress"; + $pconfig['peerid_type'] = "peeraddress"; + $pconfig['authentication_method'] = "pre_shared_key"; + $pconfig['encryption-algorithm'] = ["name" => "aes256gcm16"]; + $pconfig['hash-algorithm'] = ['sha256']; + $pconfig['dhgroup'] = array('14'); + $pconfig['nat_traversal'] = "on"; + $pconfig['installpolicy'] = true; + $pconfig['authservers'] = array(); + + /* mobile client */ + if (isset($_GET['mobile'])) { + $pconfig['mobile'] = true; + } + // init empty + foreach (explode(",", $phase1_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!isset($pconfig[$fieldname])) { + $pconfig[$fieldname] = null; + } + } + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + $a_phase1 = &config_read_array('ipsec', 'phase1'); + if (isset($_POST['p1index']) && is_numericint($_POST['p1index'])) { + $p1index = $_POST['p1index']; + } + $input_errors = array(); + $pconfig = $_POST; + $old_ph1ent = $a_phase1[$p1index]; + + // unset dpd on post + if (!isset($pconfig['dpd_enable'])) { + unset($pconfig['dpd_delay']); + unset($pconfig['dpd_maxfail']); + unset($pconfig['dpd_action']); + } + + /* My identity */ + if ($pconfig['myid_type'] == "myaddress") { + $pconfig['myid_data'] = ""; + } + /* Peer identity */ + if ($pconfig['myid_type'] == "peeraddress") { + $pconfig['peerid_data'] = ""; + } + + /* input validation */ + $method = $pconfig['authentication_method']; + + // Only require PSK here for normal PSK tunnels (not mobile) or xauth. + // For RSA methods, require the CA/Cert. + switch ($method) { + case "eap-tls": + case "psk_eap-tls": + case "eap-mschapv2": + case "rsa_eap-mschapv2": + case "eap-radius": + if (!in_array($pconfig['iketype'], array('ikev2', 'ike'))) { + $input_errors[] = sprintf(gettext("%s can only be used with IKEv2 type VPNs."), strtoupper($method)); + } + if ($method == 'eap-radius' && empty($pconfig['authservers'])) { + $input_errors[] = gettext("Please select radius servers to use."); + } + break; + case "pre_shared_key": + // If this is a mobile PSK tunnel the user PSKs go on + // the PSK tab, not here, so skip the check. + if ($pconfig['mobile']) { + break; + } + case "xauth_psk_server": + $reqdfields = explode(" ", "pre-shared-key"); + $reqdfieldsn = array(gettext("Pre-Shared Key")); + break; + case "hybrid_rsa_server": + $reqdfields = explode(' ', 'certref'); + $reqdfieldsn = array(gettext("Certificate")); + break; + case "xauth_rsa_server": + case "rsasig": + $reqdfields = explode(" ", "caref certref"); + $reqdfieldsn = array(gettext("Certificate Authority"),gettext("Certificate")); + break; + case "pubkey": + $reqdfields = explode(" ", "local-kpref peer-kpref"); + $reqdfieldsn = array(gettext("Local Key Pair"),gettext("Peer Key Pair")); + break; + } + + if (empty($pconfig['mobile'])) { + $reqdfields[] = "remote-gateway"; + $reqdfieldsn[] = gettext("Remote gateway"); + } + + do_input_validation($pconfig, $reqdfields, $reqdfieldsn, $input_errors); + + if (!empty($pconfig['inactivity_timeout']) && !is_numericint($pconfig['inactivity_timeout'])) { + $input_errors[] = gettext("The inactivity timeout must be an integer."); + } + if (!empty($pconfig['keyingtries']) && !is_numericint($pconfig['keyingtries']) && $pconfig['keyingtries'] != "-1") { + $input_errors[] = gettext("The keyingtries must be an integer."); + } + + if ((!empty($pconfig['lifetime']) && !is_numeric($pconfig['lifetime']))) { + $input_errors[] = gettext("The P1 lifetime must be an integer."); + } + if (!empty($pconfig['margintime'])) { + if (!is_numericint($pconfig['margintime'])) { + $input_errors[] = gettext("The margintime must be an integer."); + } else { + $rekeyfuzz = empty($pconfig['rekeyfuzz']) || !is_numeric($pconfig['rekeyfuzz']) ? 100 : $pconfig['rekeyfuzz']; + if (((int)$pconfig['margintime'] * 2) * ($rekeyfuzz / 100.0) > (int)$pconfig['lifetime']) { + $input_errors[] = gettext("The value margin... + margin... * rekeyfuzz must not exceed the original lifetime limit."); + } + } + } + if (!empty($pconfig['rekeyfuzz']) && !is_numericint($pconfig['rekeyfuzz'])) { + $input_errors[] = gettext("Rekeyfuzz must be an integer."); + } + + if (!empty($pconfig['remote-gateway'])) { + if (!is_ipaddr($pconfig['remote-gateway']) && !is_domain($pconfig['remote-gateway'])) { + $input_errors[] = gettext("A valid remote gateway address or host name must be specified."); + } elseif (is_ipaddrv4($pconfig['remote-gateway']) && ($pconfig['protocol'] != "inet")) { + $input_errors[] = gettext("A valid remote gateway IPv4 address must be specified or you need to change protocol to IPv6"); + } elseif (is_ipaddrv6($pconfig['remote-gateway']) && ($pconfig['protocol'] != "inet6")) { + $input_errors[] = gettext("A valid remote gateway IPv6 address must be specified or you need to change protocol to IPv4"); + } + } + + if (!empty($pconfig['remote-gateway']) && is_ipaddr($pconfig['remote-gateway']) && !isset($pconfig['disabled']) && + (empty($pconfig['iketype']) || $pconfig['iketype'] == "ikev1")) { + $t = 0; + foreach ($a_phase1 as $ph1tmp) { + if ($p1index != $t) { + if (isset($ph1tmp['remote-gateway']) && $ph1tmp['remote-gateway'] == $pconfig['remote-gateway'] && !isset($ph1tmp['disabled'])) { + $input_errors[] = sprintf(gettext('The remote gateway "%s" is already used by phase1 "%s".'), $pconfig['remote-gateway'], $ph1tmp['descr']); + } + } + $t++; + } + } + + if ($pconfig['interface'] == 'any' && $pconfig['myid_type'] == "myaddress") { + $input_errors[] = gettext("Please select an identifier (My Identifier) other then 'any' when selecting 'Any' interface"); + } elseif ($pconfig['myid_type'] == "address" && $pconfig['myid_data'] == "") { + $input_errors[] = gettext("Please enter an address for 'My Identifier'"); + } elseif ($pconfig['myid_type'] == "keyid tag" && $pconfig['myid_data'] == "") { + $input_errors[] = gettext("Please enter a keyid tag for 'My Identifier'"); + } elseif ($pconfig['myid_type'] == "fqdn" && $pconfig['myid_data'] == "") { + $input_errors[] = gettext("Please enter a fully qualified domain name for 'My Identifier'"); + } elseif ($pconfig['myid_type'] == "user_fqdn" && $pconfig['myid_data'] == "") { + $input_errors[] = gettext("Please enter a user and fully qualified domain name for 'My Identifier'"); + } elseif ($pconfig['myid_type'] == "dyn_dns" && $pconfig['myid_data'] == "") { + $input_errors[] = gettext("Please enter a dynamic domain name for 'My Identifier'"); + } elseif ((($pconfig['myid_type'] == "address") && !is_ipaddr($pconfig['myid_data']))) { + $input_errors[] = gettext("A valid IP address for 'My identifier' must be specified."); + } elseif ((($pconfig['myid_type'] == "fqdn") && !is_domain($pconfig['myid_data']))) { + $input_errors[] = gettext("A valid domain name for 'My identifier' must be specified."); + } elseif ($pconfig['myid_type'] == "fqdn" && !is_domain($pconfig['myid_data'])) { + $input_errors[] = gettext("A valid FQDN for 'My identifier' must be specified."); + } elseif ($pconfig['myid_type'] == "user_fqdn") { + $user_fqdn = explode("@", $pconfig['myid_data']); + if (is_domain($user_fqdn[1]) == false) { + $input_errors[] = gettext("A valid User FQDN in the form of user@my.domain.com for 'My identifier' must be specified."); + } + } elseif ($pconfig['myid_type'] == "dyn_dns") { + if (is_domain($pconfig['myid_data']) == false) { + $input_errors[] = gettext("A valid Dynamic DNS address for 'My identifier' must be specified."); + } + } + + // Only enforce peer ID if we are not dealing with a pure-psk mobile config. + if (!(($pconfig['authentication_method'] == "pre_shared_key") && !empty($pconfig['mobile']))) { + if ($pconfig['peerid_type'] == "address" and $pconfig['peerid_data'] == "") { + $input_errors[] = gettext("Please enter an address for 'Peer Identifier'"); + } + if ($pconfig['peerid_type'] == "keyid tag" and $pconfig['peerid_data'] == "") { + $input_errors[] = gettext("Please enter a keyid tag for 'Peer Identifier'"); + } + if ($pconfig['peerid_type'] == "fqdn" and $pconfig['peerid_data'] == "") { + $input_errors[] = gettext("Please enter a fully qualified domain name for 'Peer Identifier'"); + } + if ($pconfig['peerid_type'] == "user_fqdn" and $pconfig['peerid_data'] == "") { + $input_errors[] = gettext("Please enter a user and fully qualified domain name for 'Peer Identifier'"); + } + if ((($pconfig['peerid_type'] == "address") && !is_ipaddr($pconfig['peerid_data']))) { + $input_errors[] = gettext("A valid IP address for 'Peer identifier' must be specified."); + } + if ((($pconfig['peerid_type'] == "fqdn") && !is_domain($pconfig['peerid_data']))) { + $input_errors[] = gettext("A valid domain name for 'Peer identifier' must be specified."); + } + if ($pconfig['peerid_type'] == "fqdn") { + if (is_domain($pconfig['peerid_data']) == false) { + $input_errors[] = gettext("A valid FQDN for 'Peer identifier' must be specified."); + } + } + if ($pconfig['peerid_type'] == "user_fqdn") { + $user_fqdn = explode("@", $pconfig['peerid_data']); + if (is_domain($user_fqdn[1]) == false) { + $input_errors[] = gettext("A valid User FQDN in the form of user@my.domain.com for 'Peer identifier' must be specified."); + } + } + } + + if (!empty($pconfig['closeaction']) && !in_array($pconfig['closeaction'], ['clear', 'hold', 'restart'])) { + $input_errors[] = gettext('Invalid argument for close action.'); + } + + if (!empty($pconfig['unique']) && !in_array($pconfig['unique'], ['no', 'replace', 'never', 'keep'])) { + $input_errors[] = gettext('Invalid argument for unique.'); + } + + if (!empty($pconfig['dpd_enable'])) { + if (!is_numeric($pconfig['dpd_delay'])) { + $input_errors[] = gettext("A numeric value must be specified for DPD delay."); + } + if (!is_numeric($pconfig['dpd_maxfail'])) { + $input_errors[] = gettext("A numeric value must be specified for DPD retries."); + } + if (!empty($pconfig['dpd_action']) && !in_array($pconfig['dpd_action'], array("restart", "clear"))) { + $input_errors[] = gettext('Invalid argument for DPD action.'); + } + } + + if (!empty($pconfig['iketype']) && !in_array($pconfig['iketype'], array("ike", "ikev1", "ikev2"))) { + $input_errors[] = gettext('Invalid argument for key exchange protocol version.'); + } + + /* build our encryption algorithms array */ + if (!isset($pconfig['encryption-algorithm']) || !is_array($pconfig['encryption-algorithm'])) { + $pconfig['encryption-algorithm'] = array(); + } + $pconfig['encryption-algorithm']['name'] = $pconfig['ealgo']; + if (!empty($pconfig['ealgo_keylen'])) { + $pconfig['encryption-algorithm']['keylen'] = $pconfig['ealgo_keylen']; + } + + if (empty($pconfig['hash-algorithm'])) { + $input_errors[] = gettext("At least one hashing algorithm needs to be selected."); + $pconfig['hash-algorithm'] = array(); + } + + if (empty($pconfig['dhgroup'])) { + $pconfig['dhgroup'] = array(); + } + + foreach (ipsec_p1_ealgos() as $algo => $algodata) { + if (!empty($pconfig['iketype']) && !empty($pconfig['encryption-algorithm']['name']) && !empty($algodata['iketype']) + && $pconfig['iketype'] != $algodata['iketype'] && $pconfig['encryption-algorithm']['name'] == $algo) { + $input_errors[] = sprintf(gettext("%s can only be used with IKEv2 type VPNs."), $algodata['name']); + } + } + + if (!empty($pconfig['ikeid']) && !empty($pconfig['installpolicy'])) { + foreach ($config['ipsec']['phase2'] as $phase2ent) { + if ($phase2ent['ikeid'] == $pconfig['ikeid'] && $phase2ent['mode'] == 'route-based') { + $input_errors[] = gettext( + "Install policy on phase1 is not a valid option when using Route-based phase 2 entries." + ); + break; + } + } + } + + if (count($input_errors) == 0) { + $copy_fields = "ikeid,iketype,interface,mode,protocol,myid_type,myid_data + ,peerid_type,peerid_data,encryption-algorithm,margintime,rekeyfuzz,inactivity_timeout,keyingtries + ,lifetime,pre-shared-key,certref,caref,authentication_method,descr,local-kpref,peer-kpref + ,nat_traversal,auto,mobike,closeaction,unique"; + + foreach (explode(",",$copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if(!empty($pconfig[$fieldname])) { + $ph1ent[$fieldname] = $pconfig[$fieldname]; + } + } + + foreach (array('authservers', 'dhgroup', 'hash-algorithm') as $fieldname) { + if (!empty($pconfig[$fieldname])) { + $ph1ent[$fieldname] = implode(',', $pconfig[$fieldname]); + } + } + + $ph1ent['disabled'] = !empty($pconfig['disabled']); + $ph1ent['sha256_96'] = !empty($pconfig['sha256_96']); + $ph1ent['noinstallpolicy'] = empty($pconfig['installpolicy']); // XXX: reversed + $ph1ent['private-key'] =isset($pconfig['privatekey']) ? base64_encode($pconfig['privatekey']) : null; + if (!empty($pconfig['mobile'])) { + $ph1ent['mobile'] = true; + } else { + $ph1ent['remote-gateway'] = $pconfig['remote-gateway']; + } + if (isset($pconfig['reauth_enable'])) { + $ph1ent['reauth_enable'] = true; + } + if (isset($pconfig['rekey_enable'])) { + $ph1ent['rekey_enable'] = true; + } + + if (isset($pconfig['tunnel_isolation'])) { + $ph1ent['tunnel_isolation'] = true; + } + + if (isset($pconfig['rightallowany'])) { + $ph1ent['rightallowany'] = true; + } + + if (isset($pconfig['dpd_enable'])) { + $ph1ent['dpd_delay'] = $pconfig['dpd_delay']; + $ph1ent['dpd_maxfail'] = $pconfig['dpd_maxfail']; + $ph1ent['dpd_action'] = $pconfig['dpd_action']; + } + + /* generate unique phase1 ikeid */ + if ($ph1ent['ikeid'] == 0) { + $ph1ent['ikeid'] = ipsec_ikeid_next(); + } + + if (isset($p1index) && isset($a_phase1[$p1index])) { + $a_phase1[$p1index] = $ph1ent; + } else { + if (!empty($pconfig['clone_phase2']) && !empty($a_phase1[$_GET['dup']]) + && !empty($config['ipsec']['phase2'])) { + // clone phase 2 entries in disabled state if requested. + $prev_ike_id = $a_phase1[$_GET['dup']]['ikeid']; + foreach ($config['ipsec']['phase2'] as $phase2ent) { + if ($phase2ent['ikeid'] == $prev_ike_id) { + $new_phase2 = $phase2ent; + $new_phase2['disabled'] = true; + $new_phase2['uniqid'] = uniqid(); + $new_phase2['ikeid'] = $ph1ent['ikeid']; + unset($new_phase2['reqid']); + $config['ipsec']['phase2'][] = $new_phase2; + } + } + } + $a_phase1[] = $ph1ent; + } + + write_config(); + mark_subsystem_dirty('ipsec'); + + header(url_safe('Location: /ui/ipsec/tunnels')); + exit; + } +} + +$service_hook = 'strongswan'; + +legacy_html_escape_form_data($pconfig); +include("head.inc"); +?> + + + + + +
    +
    +
    + 0) { + print_input_errors($input_errors); + } +?> + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + /> + +
    + /> + + +
    + + + +
    + + + +
    + + +
    + + +
    + + +
    + /> + + +
    + + +
     
    + + +
    + + +
    + +
    + +
    +
    + + + + + +
    + " /> + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + + + +
    + + +
    + + +
    + /> + +
    + /> + +
    + /> + +
    + /> + +
    + /> + +
    + + +
    + /> + +
    + + +
    + + +
    + /> + +
    +
    + + + +
    + + + +
    + + + +
    +
    + + +
    + + +
    + + +
    + + +
    + + +
      + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + + + * Copyright (C) 2003-2005 Manuel Kasper + * 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. + */ + +require_once("guiconfig.inc"); +require_once("interfaces.inc"); +require_once("plugins.inc.d/ipsec.inc"); + +/** + * combine ealgos and keylen_* tags + */ +function pconfig_to_ealgos($pconfig) +{ + $ealgos = []; + if (isset($pconfig['ealgos'])) { + foreach (ipsec_p2_ealgos() as $algo_name => $algo_data) { + if (in_array($algo_name, $pconfig['ealgos'])) { + $ealgos[] = ['name' => $algo_name]; + } + } + } + return $ealgos; +} + +function ealgos_to_pconfig(& $ealgos, & $pconfig) +{ + $p2_ealgos = ipsec_p2_ealgos(); + $pconfig['ealgos'] = []; + foreach ($ealgos as $cnf_algo_data) { + foreach ($p2_ealgos as $algo_name => $algo_data) { + if ($algo_name == $cnf_algo_data['name']) { + $pconfig['ealgos'][] = $algo_name; + } elseif ($algo_data['name'] == $cnf_algo_data['name']) { + // XXX: extract and convert legacy encryption-algorithm-option setting + if ($cnf_algo_data['keylen'] == $algo_data['keylen'] || $cnf_algo_data['keylen'] == "auto") { + $pconfig['ealgos'][] = $algo_name; + } + } + } + } + + return $ealgos; +} + +/** + * convert id_address, id_netbits, id_type + * to type/address/netbits structure + */ +function pconfig_to_idinfo($prefix, $pconfig) +{ + $type = isset($pconfig[$prefix."id_type"]) ? $pconfig[$prefix."id_type"] : null; + $address = isset($pconfig[$prefix."id_address"]) ? $pconfig[$prefix."id_address"] : null; + $netbits = isset($pconfig[$prefix."id_netbits"]) ? $pconfig[$prefix."id_netbits"] : null; + + switch ($type) { + case "address": + return array('type' => $type, 'address' => $address); + case "network": + return array('type' => $type, 'address' => $address, 'netbits' => $netbits); + default: + return array('type' => $type ); + } +} + +/** + * reverse pconfig_to_idinfo from $idinfo array to $pconfig + */ +function idinfo_to_pconfig($prefix, $idinfo, & $pconfig) +{ + switch ($idinfo['type']) { + case "address": + $pconfig[$prefix."id_type"] = $idinfo['type']; + $pconfig[$prefix."id_address"] = $idinfo['address']; + break; + case "network": + $pconfig[$prefix."id_type"] = $idinfo['type']; + $pconfig[$prefix."id_address"] = $idinfo['address']; + $pconfig[$prefix."id_netbits"] = $idinfo['netbits']; + break; + default: + $pconfig[$prefix."id_type"] = $idinfo['type']; + break; + } +} + +/** + * search phase 2 entries for record with uniqid + */ +function getIndexByUniqueId($uniqid) +{ + global $config; + $p2index = null; + if ($uniqid != null) { + foreach ($config['ipsec']['phase2'] as $idx => $ph2) { + if ($ph2['uniqid'] == $uniqid) { + $p2index = $idx; + break; + } + } + } + return $p2index; +} + +config_read_array('ipsec', 'client'); +config_read_array('ipsec', 'phase2'); + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // lookup p2index + if (!empty($_GET['dup'])) { + $p2index = getIndexByUniqueId($_GET['dup']); + } elseif (!empty($_GET['p2index'])) { + $p2index = getIndexByUniqueId($_GET['p2index']); + } else { + $p2index = null; + } + // initialize form data + $pconfig = array(); + + $phase2_fields = "ikeid,mode,descr,uniqid,proto,hash-algorithm-option,pfsgroup,lifetime,pinghost,protocol,spd,"; + $phase2_fields .= "tunnel_local,tunnel_remote"; + if ($p2index !== null) { + // 1-on-1 copy + foreach (explode(",", $phase2_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (isset($config['ipsec']['phase2'][$p2index][$fieldname])) { + $pconfig[$fieldname] = $config['ipsec']['phase2'][$p2index][$fieldname]; + } elseif (!isset($pconfig[$fieldname])) { + // initialize element + $pconfig[$fieldname] = null; + } + } + // fields with some kind of logic + $pconfig['disabled'] = isset($config['ipsec']['phase2'][$p2index]['disabled']); + + idinfo_to_pconfig("local", $config['ipsec']['phase2'][$p2index]['localid'], $pconfig); + idinfo_to_pconfig("remote", $config['ipsec']['phase2'][$p2index]['remoteid'], $pconfig); + if (!empty($config['ipsec']['phase2'][$p2index]['encryption-algorithm-option'])) { + ealgos_to_pconfig($config['ipsec']['phase2'][$p2index]['encryption-algorithm-option'], $pconfig); + } else { + $pconfig['ealgos'] = []; + } + + if (!empty($_GET['dup'])) { + $pconfig['uniqid'] = uniqid(); + } + } else { + if (isset($_GET['ikeid'])) { + $pconfig['ikeid'] = $_GET['ikeid']; + } + /* defaults */ + $pconfig['localid_type'] = "lan"; + $pconfig['remoteid_type'] = "network"; + $pconfig['protocol'] = "esp"; + $pconfig['ealgos'] = ['aes256gcm16']; + $pconfig['hash-algorithm-option'] = ['hmac_sha256']; + $pconfig['pfsgroup'] = "0"; + $pconfig['uniqid'] = uniqid(); + + // init empty + foreach (explode(",", $phase2_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!isset($pconfig[$fieldname])) { + $pconfig[$fieldname] = null; + } + } + } + /* mobile client */ + foreach ($config['ipsec']['phase1'] as $phase1ent) { + if ($phase1ent['ikeid'] == $pconfig['ikeid'] && isset($phase1ent['mobile'])) { + $pconfig['mobile'] = true; + break; + } + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!empty($_POST['uniqid'])) { + $p2index = getIndexByUniqueId($_POST['uniqid']); + } else { + $p2index = null; + } + $input_errors = array(); + $pconfig = $_POST; + + /* input validation */ + if (!isset($_POST['ikeid'])) { + $input_errors[] = gettext("A valid ikeid must be specified."); + } + $reqdfields = explode(" ", "localid_type uniqid"); + $reqdfieldsn = array(gettext("Local network type"), gettext("Unique Identifier")); + if (!isset($pconfig['mobile'])) { + $reqdfields[] = "remoteid_type"; + $reqdfieldsn[] = gettext("Remote network type"); + } + + do_input_validation($_POST, $reqdfields, $reqdfieldsn, $input_errors); + + if (($pconfig['mode'] == 'tunnel') || ($pconfig['mode'] == 'tunnel6')) { + switch ($pconfig['localid_type']) { + case 'network': + if (($pconfig['localid_netbits'] != 0 && !$pconfig['localid_netbits']) || !is_numeric($pconfig['localid_netbits'])) { + $input_errors[] = gettext('A valid local network bit count must be specified.'); + } + /* FALLTHROUGH */ + case 'address': + if (!$pconfig['localid_address'] || !is_ipaddr($pconfig['localid_address'])) { + $input_errors[] = gettext('A valid local network IP address must be specified.'); + } elseif (is_ipaddrv4($pconfig['localid_address']) && ($pconfig['mode'] != 'tunnel')) { + $input_errors[] = gettext('A valid local network IPv4 address must be specified or you need to change Mode to IPv6'); + } elseif (is_ipaddrv6($pconfig['localid_address']) && ($pconfig['mode'] != 'tunnel6')) { + $input_errors[] = gettext('A valid local network IPv6 address must be specified or you need to change Mode to IPv4'); + } + break; + default: + if ($pconfig['mode'] == 'tunnel') { + list (, $subnet) = interfaces_primary_address($pconfig['localid_type']); + if (!is_subnetv4($subnet)) { + $input_errors[] = sprintf( + gettext('Invalid local network: %s has no valid IPv4 network.'), + convert_friendly_interface_to_friendly_descr($pconfig['localid_type']) + ); + } + } elseif ($pconfig['mode'] == 'tunnel6') { + list (, $subnet) = interfaces_primary_address6($pconfig['localid_type']); + if (!is_subnetv6($subnet)) { + $input_errors[] = sprintf( + gettext('Invalid local network: %s has no valid IPv6 network.'), + convert_friendly_interface_to_friendly_descr($pconfig['localid_type']) + ); + } + } + break; + } + + switch ($pconfig['remoteid_type']) { + case "network": + if (($pconfig['remoteid_netbits'] != 0 && !$pconfig['remoteid_netbits']) || !is_numeric($pconfig['remoteid_netbits'])) { + $input_errors[] = gettext("A valid remote network bit count must be specified."); + } + // address rules also apply to network type (hence, no break) + case "address": + if (!$pconfig['remoteid_address'] || !is_ipaddr($pconfig['remoteid_address'])) { + $input_errors[] = gettext("A valid remote network IP address must be specified."); + } elseif (is_ipaddrv4($pconfig['remoteid_address']) && ($pconfig['mode'] != "tunnel")) { + $input_errors[] = gettext("A valid remote network IPv4 address must be specified or you need to change Mode to IPv6"); + } elseif (is_ipaddrv6($pconfig['remoteid_address']) && ($pconfig['mode'] != "tunnel6")) { + $input_errors[] = gettext("A valid remote network IPv6 address must be specified or you need to change Mode to IPv4"); + } + break; + } + } elseif ($pconfig['mode'] == 'route-based') { + // validate if both tunnel networks are using the correct address family + if (!is_ipaddr($pconfig['tunnel_local']) || !is_ipaddr($pconfig['tunnel_remote'])) { + if (!is_ipaddr($pconfig['tunnel_local'])) { + $input_errors[] = gettext('A valid local network IP address must be specified.'); + } + if (!is_ipaddr($pconfig['tunnel_remote'])) { + $input_errors[] = gettext("A valid remote network IP address must be specified."); + } + } elseif( + !(is_ipaddrv4($pconfig['tunnel_local']) && is_ipaddrv4($pconfig['tunnel_remote'])) && + !(is_ipaddrv6($pconfig['tunnel_local']) && is_ipaddrv6($pconfig['tunnel_remote'])) + ) { + $input_errors[] = gettext("A valid local network IP address must be specified."); + $input_errors[] = gettext("A valid remote network IP address must be specified."); + } + } + /* Validate enabled phase2's are not duplicates */ + if (isset($pconfig['mobile'])) { + /* User is adding phase 2 for mobile phase1 */ + foreach ($config['ipsec']['phase2'] as $key => $name) { + if (isset($name['mobile']) && $pconfig['ikeid'] == $name['ikeid'] && $name['uniqid'] != $pconfig['uniqid']) { + /* check duplicate localids only for mobile clients */ + $localid_data = ipsec_idinfo_to_cidr($name['localid'], false, $name['mode']); + $entered = array(); + $entered['type'] = $pconfig['localid_type']; + if (isset($pconfig['localid_address'])) { + $entered['address'] = $pconfig['localid_address']; + } + if (isset($pconfig['localid_netbits'])) { + $entered['netbits'] = $pconfig['localid_netbits']; + } + $entered_localid_data = ipsec_idinfo_to_cidr($entered, false, $pconfig['mode']); + if ($localid_data == $entered_localid_data) { + /* adding new p2 entry */ + $input_errors[] = gettext("Phase2 with this Local Network is already defined for mobile clients."); + break; + } + } + } + } else { + /* User is adding phase 2 for site-to-site phase1 */ + foreach ($config['ipsec']['phase2'] as $key => $name) { + if (!isset($name['mobile']) && $pconfig['mode'] != 'route-based' && + $pconfig['ikeid'] == $name['ikeid'] && $pconfig['uniqid'] != $name['uniqid']) { + /* check duplicate subnets only for given phase1 */ + $localid_data = ipsec_idinfo_to_cidr($name['localid'], false, $name['mode']); + $remoteid_data = ipsec_idinfo_to_cidr($name['remoteid'], false, $name['mode']); + $entered_local = array(); + $entered_local['type'] = $pconfig['localid_type']; + if (isset($pconfig['localid_address'])) { + $entered_local['address'] = $pconfig['localid_address']; + } + if (isset($pconfig['localid_netbits'])) { + $entered_local['netbits'] = $pconfig['localid_netbits']; + } + $entered_localid_data = ipsec_idinfo_to_cidr($entered_local, false, $pconfig['mode']); + $entered_remote = array(); + $entered_remote['type'] = $pconfig['remoteid_type']; + if (isset($pconfig['remoteid_address'])) { + $entered_remote['address'] = $pconfig['remoteid_address']; + } + if (isset($pconfig['remoteid_netbits'])) { + $entered_remote['netbits'] = $pconfig['remoteid_netbits']; + } + $entered_remoteid_data = ipsec_idinfo_to_cidr($entered_remote, false, $pconfig['mode']); + if ($localid_data == $entered_localid_data && $remoteid_data == $entered_remoteid_data) { + /* adding new p2 entry */ + $input_errors[] = gettext("Phase2 with this Local/Remote networks combination is already defined for this Phase1."); + break; + } + } + } + } + + if (!empty($pconfig['ikeid'])) { + foreach ($config['ipsec']['phase1'] as $phase1ent) { + if ($phase1ent['ikeid'] == $pconfig['ikeid'] && + $pconfig['mode'] == 'route-based' && + empty($phase1ent['noinstallpolicy']) + ) { + $input_errors[] = gettext( + "Install policy on phase1 is not a valid option when using Route-based phase 2 entries." + ); + break; + } + } + } + + /* For ESP protocol, handle encryption algorithms */ + if ($pconfig['protocol'] == "esp") { + $ealgos = pconfig_to_ealgos($pconfig); + + if (!count($ealgos)) { + $input_errors[] = gettext("At least one encryption algorithm must be selected."); + } else { + if (empty($pconfig['hash-algorithm-option'])) { + foreach ($ealgos as $ealgo) { + if (!strpos($ealgo['name'], "gcm")) { + $input_errors[] = gettext("At least one hashing algorithm needs to be selected."); + break; + } + } + $pconfig['hash-algorithm-option'] = array(); + } + } + } + if ((!empty($_POST['lifetime']) && !is_numeric($_POST['lifetime']))) { + $input_errors[] = gettext("The P2 lifetime must be an integer."); + } + + if (!empty($pconfig['spd'])) { + foreach (explode(',', $pconfig['spd']) as $spd_entry) { + if (($pconfig['mode'] == "tunnel" && !is_subnetv4(trim($spd_entry))) || + ($pconfig['mode'] == "tunnel6" && !is_subnetv6(trim($spd_entry)))) { + $input_errors[] = sprintf(gettext('SPD "%s" is not a valid network, it should match the tunnel type (IPv4/IPv6).'), $spd_entry) ; + } + } + } + + if (count($input_errors) == 0) { + $ph2ent = array(); + $copy_fields = "ikeid,uniqid,mode,pfsgroup,lifetime,pinghost,descr,protocol,spd"; + + // 1-on-1 copy + foreach (explode(",", $copy_fields) as $fieldname) { + $fieldname = trim($fieldname); + if (!empty($pconfig[$fieldname])) { + $ph2ent[$fieldname] = $pconfig[$fieldname]; + } + } + + // fields with some logic in them + $ph2ent['disabled'] = $pconfig['disabled'] ? true : false; + if (($ph2ent['mode'] == "tunnel") || ($ph2ent['mode'] == "tunnel6")) { + $ph2ent['localid'] = pconfig_to_idinfo("local", $pconfig); + $ph2ent['remoteid'] = pconfig_to_idinfo("remote", $pconfig); + } elseif ($ph2ent['mode'] == 'route-based') { + $ph2ent['tunnel_local'] = $pconfig['tunnel_local']; + $ph2ent['tunnel_remote'] = $pconfig['tunnel_remote']; + } + + $ph2ent['encryption-algorithm-option'] = pconfig_to_ealgos($pconfig); + + if (!empty($pconfig['hash-algorithm-option'])) { + $ph2ent['hash-algorithm-option'] = $pconfig['hash-algorithm-option']; + } else { + unset($ph2ent['hash-algorithm-option']); + } + + // attach or generate reqid + if ($p2index !== null && !empty($config['ipsec']['phase2'][$p2index]['reqid'])) { + $ph2ent['reqid'] = $config['ipsec']['phase2'][$p2index]['reqid']; + } else { + $reqids = []; + foreach ($config['ipsec']['phase2'] as $tmp) { + if (!empty($tmp['reqid'])) { + $reqids[] = $tmp['reqid']; + } + } + for ($i=1; $i < 65535; $i++) { + if (!in_array($i, $reqids)) { + $ph2ent['reqid'] = $i; + break; + } + } + } + // save to config + if ($p2index !== null) { + $config['ipsec']['phase2'][$p2index] = $ph2ent; + } else { + $config['ipsec']['phase2'][] = $ph2ent; + } + + + write_config(); + mark_subsystem_dirty('ipsec'); + + header(url_safe('Location: /ui/ipsec/tunnels')); + exit; + } +} + +$service_hook = 'strongswan'; + +legacy_html_escape_form_data($pconfig); + +include("head.inc"); + +?> + + + + + + 0) { + print_input_errors($input_errors); +} +?> +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + /> + +
    + +
    + + +
    + +
    + +
    + +
       + + + + + +
    + + + +
    +
    :   + +
    :   + + + + + +
    + + + +
    +
    + +
    + +
    + +
    + +
    + + +
    + + + + + +
    + + +
    + + +
    + +
    + + +
    + + +
      + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/security/stunnel/Makefile b/security/stunnel/Makefile index 6ddef16665..5de6a72ff8 100644 --- a/security/stunnel/Makefile +++ b/security/stunnel/Makefile @@ -1,7 +1,9 @@ PLUGIN_NAME= stunnel -PLUGIN_VERSION= 1.0.3 +PLUGIN_VERSION= 1.0.6 +PLUGIN_REVISION= 1 PLUGIN_COMMENT= Stunnel TLS proxy PLUGIN_MAINTAINER= ad@opnsense.org PLUGIN_DEPENDS= stunnel +PLUGIN_TIER= 2 .include "../../Mk/plugins.mk" diff --git a/security/stunnel/pkg-descr b/security/stunnel/pkg-descr index c25f04950c..b3f0ac87a6 100644 --- a/security/stunnel/pkg-descr +++ b/security/stunnel/pkg-descr @@ -1,2 +1,4 @@ -Stunnel is a proxy designed to add TLS encryption functionality to existing clients and servers without any changes in the programs' code. -(https://www.stunnel.org/) +Stunnel is a proxy designed to add TLS encryption functionality to +existing clients and servers without any changes in the programs' code. + +WWW: https://www.stunnel.org/ diff --git a/security/stunnel/src/etc/inc/plugins.inc.d/stunnel.inc b/security/stunnel/src/etc/inc/plugins.inc.d/stunnel.inc index 374e2ab1a2..af102b1e87 100644 --- a/security/stunnel/src/etc/inc/plugins.inc.d/stunnel.inc +++ b/security/stunnel/src/etc/inc/plugins.inc.d/stunnel.inc @@ -70,7 +70,7 @@ function stunnel_refresh_crls() { $stunnel = new OPNsense\Stunnel\Stunnel(); $configObj = OPNsense\Core\Config::getInstance()->object(); - foreach ($stunnel->services->service->__items as $service) { + foreach ($stunnel->services->service->iterateItems() as $service) { if (!empty((string)$service->enabled) && !empty((string)$service->enableCRL)) { foreach (explode(",", (string)$service->cacert) as $cacert) { $this_ca = null; @@ -94,24 +94,14 @@ function stunnel_refresh_crls() proc_close($process); } if ($ca_hash) { - $crlres = openssl_crl_new($ca_crt, 0, 9999); if (!empty($configObj->crl)) { foreach ($configObj->crl as $crl) { - if ($crl->caref == $cacert && !empty((string)$crl->cert)) { - foreach ($crl->cert as $cert) { - openssl_crl_revoke_cert( - $crlres, - base64_decode((string)$cert->crt), - (string)$cert->revoke_time, - (string)$cert->reason - ); - } + if ($crl->caref == $cacert && !empty((string)$crl->text)) { + file_put_contents("/var/run/stunnel/certs/{$ca_hash}.r0", (string)$crl->text); + break; } } } - $crl_text = ""; - openssl_crl_export($crlres, $crl_text, $ca_key); - file_put_contents("/var/run/stunnel/certs/{$ca_hash}.r0", $crl_text); } } } diff --git a/security/stunnel/src/etc/rc.d/identd_stunnel b/security/stunnel/src/etc/rc.d/identd_stunnel index 003c2c3c4a..19be1bcc58 100755 --- a/security/stunnel/src/etc/rc.d/identd_stunnel +++ b/security/stunnel/src/etc/rc.d/identd_stunnel @@ -1,11 +1,8 @@ #!/bin/sh # -# $FreeBSD$ -# # PROVIDE: identd_stunnel # REQUIRE: SERVERS # KEYWORD: shutdown -# . /etc/rc.subr diff --git a/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServicesController.php b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServicesController.php index edad2be838..5db04eb871 100644 --- a/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServicesController.php +++ b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServicesController.php @@ -35,17 +35,17 @@ class ServicesController extends ApiMutableModelControllerBase protected static $internalModelName = 'stunnel'; protected static $internalModelClass = 'OPNsense\Stunnel\Stunnel'; - protected function save() + protected function save($validateFullModel = false, $disable_validation = false) { // hook service enable status on enabled tunnels $this->getModel()->general->enabled = "0"; - foreach ($this->getModel()->services->service->__items as $service) { + foreach ($this->getModel()->services->service->iterateItems() as $service) { if ((string)$service->enabled == "1") { $this->getModel()->general->enabled = "1"; break; } } - return parent::save(); + return parent::save($validateFullModel, $disable_validation); } public function searchItemAction() diff --git a/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.xml b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.xml index 13e2f02fd6..0b851ec5d0 100644 --- a/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.xml +++ b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.xml @@ -1,28 +1,28 @@ //OPNsense/Stunnel - 1.0.3 + 1.0.4 Stunnel TLS encryption proxy - 1 + 1 Y - 0 + 0 Y - 0 + 0 Y - 1 + 1 Y @@ -34,7 +34,7 @@ Y N - 127.0.0.1 + 127.0.0.1 Y @@ -48,6 +48,8 @@ IMAP + LDAP + NNTP POP3 SMTP @@ -55,25 +57,23 @@ N - Y + Y ca Please select a valid certificate from the list - 0 + 0 Y - 0 + 0 Y - TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,DHE-RSA-AES128-GCM-SHA256,DHE-RSA-AES256-GCM-SHA384 + TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,DHE-RSA-AES128-GCM-SHA256,DHE-RSA-AES256-GCM-SHA384 Y - Y + Y stunnel ssl ciphers - /tmp/stunnel_ciphers_list.json - 360 Please specify valid tls ciphers. @@ -83,7 +83,7 @@ N - /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){0,255}$/u + /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){0,255}$/u Description should be a string between 1 and 255 characters diff --git a/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt b/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt index fe9967c40c..18e1f479a0 100644 --- a/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt +++ b/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt @@ -27,12 +27,12 @@ +
    + {{ partial("layout_partials/base_form",['fields':authenticationForm,'id':'frmAuthentication']) }} +
    +
    +
    +
    +
    + + +

    +
    +
    +
    diff --git a/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/settings.volt b/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/settings.volt new file mode 100644 index 0000000000..e5141b0fcd --- /dev/null +++ b/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/settings.volt @@ -0,0 +1,76 @@ + + +
    +
    + {{ partial("layout_partials/base_form",['fields':settingsForm,'id':'frmSettings'])}} +
    +
    + + + + + + + + + + + + + + + + + +
    {{ lang._('ID') }}{{ lang._('Subnet') }}{{ lang._('Description') }}{{ lang._('Commands') }}
    + +
    +
    +
    +
    +
    +
    +
    + +

    +
    +
    +
    + +{{ partial("layout_partials/base_dialog",['fields':formDialogSubnet,'id':'DialogSubnet','label':lang._('Edit Subnet')])}} diff --git a/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/status.volt b/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/status.volt new file mode 100644 index 0000000000..6366063229 --- /dev/null +++ b/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/status.volt @@ -0,0 +1,146 @@ + + + + + +
    +
    +
    + + + + + + + + + +
    {{ lang._('Name') }}{{ lang._('Value') }}
    +
    +
    + +
    + + + + + + + + + + + +
    {{ lang._('Peer') }}{{ lang._('Tailscale IPs') }}{{ lang._('Last Seen') }}{{ lang._('OS') }}
    +
    + +
    +
    +
    diff --git a/security/tailscale/src/opnsense/service/conf/actions.d/actions_tailscale.conf b/security/tailscale/src/opnsense/service/conf/actions.d/actions_tailscale.conf new file mode 100644 index 0000000000..023ddf0fe6 --- /dev/null +++ b/security/tailscale/src/opnsense/service/conf/actions.d/actions_tailscale.conf @@ -0,0 +1,44 @@ +[start] +command:/usr/local/etc/rc.d/tailscaled start +type: script +message: starting tailscale service + +[stop] +command:/usr/local/etc/rc.d/tailscaled stop; exit 0 +type: script +message: stopping tailscale service + +[status] +command:/usr/local/etc/rc.d/tailscaled status; exit 0 +type: script_output +message: tailscaled status + +[restart] +command:/usr/local/etc/rc.d/tailscaled restart +type: script +message: restarting tailscale services + +[reload] +command:/usr/local/etc/rc.d/tailscaled reload +type: script +message: reload tailscale configuration + +[tailscale-status] +command:/usr/local/bin/tailscale status --json; exit 0 +type:script_output +message: request tailscale status + +[tailscale-ip] +command:/usr/local/bin/tailscale ip; exit 0 +type:script_output +message: request tailscale ip + +[tailscale-version] +command:/usr/local/bin/tailscale version; exit 0 +type:script_output +message: request tailscale version + +[tailscale-netcheck] +command:/usr/local/bin/tailscale netcheck; exit 0 +type:script_output +message: request tailscale netcheck diff --git a/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/+TARGETS b/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/+TARGETS new file mode 100644 index 0000000000..e9380f1c2a --- /dev/null +++ b/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/+TARGETS @@ -0,0 +1 @@ +rc.conf.d:/etc/rc.conf.d/tailscaled diff --git a/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/rc.conf.d b/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/rc.conf.d new file mode 100644 index 0000000000..42c21b5615 --- /dev/null +++ b/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/rc.conf.d @@ -0,0 +1,64 @@ +# DO NOT EDIT +# THIS FILE IS AUTOMATICALLY GENERATED - ANY CHANGES WILL BE OVERWRITTEN +# +{% if not helpers.empty('OPNsense.tailscale.settings.enabled') %} +tailscaled_enable="YES" +{% if helpers.exists('OPNsense.tailscale.settings.disableSNAT') and OPNsense.tailscale.settings.disableSNAT|default("0") == "1" %} +# see - https://github.com/tailscale/tailscale/issues/5573#issuecomment-1584695981 +tailscaled_env="TS_DEBUG_NETSTACK_SUBNETS=0" +{% endif %} +tailscaled_up_args_ext= +{% if helpers.exists('OPNsense.tailscale.settings.listenPort') %} +tailscaled_port="{{ OPNsense.tailscale.settings.listenPort }}" +{% endif %} +{% set up_args = [] %} +{% do up_args.append("--timeout=" + OPNsense.tailscale.settings.loginTimeout + "s") %} +{% if helpers.exists('OPNsense.tailscale.settings.advertiseExitNode') and OPNsense.tailscale.settings.advertiseExitNode|default("0") == "1" %} +{% do up_args.append("--advertise-exit-node") %} +{% else %} +{% do up_args.append("--advertise-exit-node=false") %} +{% endif %} +{% if helpers.exists('OPNsense.tailscale.settings.useExitNode') %} +{% do up_args.append("--exit-node=" + OPNsense.tailscale.settings.useExitNode) %} +{% endif %} +{% if helpers.exists('OPNsense.tailscale.settings.acceptSubnetRoutes') and OPNsense.tailscale.settings.acceptSubnetRoutes|default("0") == "1" %} +{% do up_args.append("--accept-routes") %} +{% else %} +{% do up_args.append("--accept-routes=false") %} +{% endif %} +{% if helpers.exists('OPNsense.tailscale.settings.acceptDNS') and OPNsense.tailscale.settings.acceptDNS|default("0") == "1" %} +{% do up_args.append("--accept-dns") %} +{% else %} +{% do up_args.append("--accept-dns=false") %} +{% endif %} +{% if helpers.exists('OPNsense.tailscale.settings.enableSSH') and OPNsense.tailscale.settings.enableSSH|default("0") == "1" %} +{% do up_args.append("--ssh=true") %} +{% else %} +{% do up_args.append("--ssh=false") %} +{% endif %} +{% if helpers.exists('OPNsense.tailscale.authentication.loginServer') %} +{% do up_args.append("--login-server=" + OPNsense.tailscale.authentication.loginServer) %} +{% endif %} +{# loop through subnets to build list #} +{% if helpers.exists('OPNsense.tailscale.settings.subnets.subnet4') %} +{% set subnets = [] %} +{% for subnet_list in helpers.toList('OPNsense.tailscale.settings.subnets.subnet4') %} +{% do subnets.append(subnet_list.subnet) %} +{% endfor %} +{% set subnetString = subnets|join(',') %} +{% do up_args.append("--advertise-routes=" + subnetString) %} +{% else %} +{% do up_args.append("--advertise-routes=") %} +{% endif %} +{% if helpers.exists('OPNsense.tailscale.authentication.preAuthKey') %} +# Conditionally add auth-key only if not already authenticated +if [ -f /var/db/tailscale/tailscaled.state ]; then + if ! grep -q '"_current-profile"' /var/db/tailscale/tailscaled.state; then + tailscaled_up_args_ext="--auth-key={{ OPNsense.tailscale.authentication.preAuthKey }}" + fi +fi +{% endif %} +tailscaled_up_args="{{ up_args|join(' ') }} ${tailscaled_up_args_ext}" +{% else %} +tailscaled_enable="NO" +{% endif %} diff --git a/security/tailscale/src/opnsense/www/js/widgets/Metadata/Tailscale.xml b/security/tailscale/src/opnsense/www/js/widgets/Metadata/Tailscale.xml new file mode 100644 index 0000000000..12d7f0468d --- /dev/null +++ b/security/tailscale/src/opnsense/www/js/widgets/Metadata/Tailscale.xml @@ -0,0 +1,24 @@ + + + Tailscale.js + + /api/tailscale/service/status + /api/tailscale/status/status + + + Tailscale + Tailscale service is not running + Backend State + DNS Name + Enabled + Exit Node + No + Could not fetch data + Online + Peers + Tailscale IP + Yes + Version + + + diff --git a/security/tailscale/src/opnsense/www/js/widgets/Tailscale.js b/security/tailscale/src/opnsense/www/js/widgets/Tailscale.js new file mode 100644 index 0000000000..c6fd214a68 --- /dev/null +++ b/security/tailscale/src/opnsense/www/js/widgets/Tailscale.js @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2024 Sheridan Computers + * 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. + */ + +export default class Tailscale extends BaseTableWidget { + constructor() { + super(); + } + + getGridOptions() { + return { + // Automatically triggers vertical scrolling after reaching 650px in height + sizeToContent: 650 + }; + } + + getMarkup() { + let $container = $('
    '); + let $tailscaleStatusTable = this.createTable('tailscaleStatusTable', { + headerPosition: 'left' + }); + + $container.append($tailscaleStatusTable); + return $container; + } + + async onWidgetTick() { + // check if Tailscale is enabled + const ServiceStatusData = await this.ajaxCall('/api/tailscale/service/status'); + if (!ServiceStatusData || ServiceStatusData.status !== 'running') { + this.displayError(this.translations.serviceDisabled); + return; + } + + const tsData = await this.ajaxCall('/api/tailscale/status/status'); + if (!tsData) { + this.displayError(this.translations.noData); + return; + } + + let statusData = this.parseData(tsData); + if (!this.dataChanged('tailscale-data', statusData)) { + return; + } + + this.updateWidgetTable(statusData); + } + + parseData(data) { + let result = []; + + result['version'] = data.Version; + result['backendState'] = data.BackendState; + result['dnsName'] = data.Self.DNSName; + + result['online'] = (data.Self.Online === true) ? + this.translations.yes : this.translations.no; + + result['exitNode'] = (data.Self.ExitNodeOption === true) ? + this.translations.yes : this.translations.no; + + result['peerCount'] = Object.keys(data.Peer).length; + + let ipAddresses = []; + data.TailscaleIPs.forEach(ip => { + ipAddresses.push(ip); + }); + result['ipAddresses'] = ipAddresses; + + return result; + } + + updateWidgetTable(data) { + let rows = []; + + let color = "text-success"; + if (data['online'] === false) { + color = "text-danger"; + } + + let row = [ + `
    ${this.translations.online}
    `, + `
    ${data['online']}
    ` + ]; + rows.push(row); + + // version + row = [ + `
    ${this.translations.version}
    `, + `
    ${data['version']}
    ` + ]; + rows.push(row); + + // backend state + row = [ + `
    ${this.translations.backendState}
    `, + `
    ${data['backendState']}
    ` + ]; + rows.push(row); + + // dns name + row = [ + `
    ${this.translations.dnsName}
    `, + `
    ${data['dnsName']}
    ` + ]; + rows.push(row); + + // ip addreses + row = [ + `
    ${this.translations.tailscaleIP}
    `, + `
    ${data['ipAddresses'].join('
    ')}
    ` + ]; + rows.push(row); + + // exit node + row = [ + `
    ${this.translations.exitNode}
    `, + `
    ${data['exitNode']}
    ` + ]; + rows.push(row); + + // peers + row = [ + `
    ${this.translations.peers}
    `, + `
    ${data['peerCount']}
    ` + ]; + rows.push(row); + + super.updateTable('tailscaleStatusTable', rows); + } + + displayError(message) { + $('#tailscaleStatusTable').empty().append( + $(`
    ${message}
    `) + ); + } +} diff --git a/security/tinc/Makefile b/security/tinc/Makefile index e7bcf5a701..c60772668b 100644 --- a/security/tinc/Makefile +++ b/security/tinc/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= tinc -PLUGIN_VERSION= 1.6 -PLUGIN_REVISION= 3 +PLUGIN_VERSION= 1.8 PLUGIN_COMMENT= Tinc VPN PLUGIN_DEPENDS= tinc PLUGIN_MAINTAINER= ad@opnsense.org diff --git a/security/tinc/src/etc/inc/plugins.inc.d/tinc.inc b/security/tinc/src/etc/inc/plugins.inc.d/tinc.inc index e732ece416..e69ef4c442 100644 --- a/security/tinc/src/etc/inc/plugins.inc.d/tinc.inc +++ b/security/tinc/src/etc/inc/plugins.inc.d/tinc.inc @@ -1,30 +1,30 @@ '^tinc', 'volatile' => true]]; +} diff --git a/security/tinc/src/etc/rc.d/opnsense-tincd b/security/tinc/src/etc/rc.d/opnsense-tincd index 6a611bc1b1..c96b3bdec7 100755 --- a/security/tinc/src/etc/rc.d/opnsense-tincd +++ b/security/tinc/src/etc/rc.d/opnsense-tincd @@ -1,11 +1,8 @@ #!/bin/sh # -# $FreeBSD$ -# # PROVIDE: opnsense-tincd # REQUIRE: SERVERS # KEYWORD: shutdown -# . /etc/rc.subr diff --git a/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/Api/ServiceController.php b/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/Api/ServiceController.php index caa0331772..fe8c8f7248 100644 --- a/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/Api/ServiceController.php +++ b/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/Api/ServiceController.php @@ -1,31 +1,29 @@ request->isPost()) { - // close session for long running action - $this->sessionClose(); $backend = new Backend(); $backend->configdRun('template reload OPNsense/Tinc'); return array("status" => "ok"); @@ -62,8 +58,6 @@ public function reconfigureAction() public function startAction() { if ($this->request->isPost()) { - // close session for long running action - $this->sessionClose(); $backend = new Backend(); $response = $backend->configdRun("tinc start"); return array("response" => $response); @@ -79,8 +73,6 @@ public function startAction() public function stopAction() { if ($this->request->isPost()) { - // close session for long running action - $this->sessionClose(); $backend = new Backend(); $response = $backend->configdRun("tinc stop"); return array("response" => $response); @@ -96,8 +88,6 @@ public function stopAction() public function restartAction() { if ($this->request->isPost()) { - // close session for long running action - $this->sessionClose(); $backend = new Backend(); $response = $backend->configdRun("tinc restart"); return array("response" => $response); diff --git a/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/Api/SettingsController.php b/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/Api/SettingsController.php index 4bbbe0395f..c8c7497b0f 100644 --- a/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/Api/SettingsController.php +++ b/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/Api/SettingsController.php @@ -1,31 +1,29 @@ sessionClose(); $grid = new UIModelGrid($this->getModel()->networks->network); return $grid->fetchBindRequest( $this->request, @@ -197,7 +194,6 @@ public function setHostAction($uuid = null) */ public function searchHostAction() { - $this->sessionClose(); $grid = new UIModelGrid($this->getModel()->hosts->host); return $grid->fetchBindRequest( $this->request, diff --git a/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/forms/dialogHost.xml b/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/forms/dialogHost.xml index 0e5bc3c348..b78e2a96d7 100644 --- a/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/forms/dialogHost.xml +++ b/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/forms/dialogHost.xml @@ -50,7 +50,7 @@ dropdown The symmetric cipher algorithm used to encrypt UDP packets. - Any cipher supported by LibreSSL or OpenSSL is recognised. + Any cipher supported by OpenSSL is recognised. Furthermore, specifying "none" will turn off packet encryption. It is best to use only those ciphers which support CBC mode diff --git a/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/forms/dialogNetwork.xml b/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/forms/dialogNetwork.xml index 1c2224c4d8..113023c6a4 100644 --- a/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/forms/dialogNetwork.xml +++ b/security/tinc/src/opnsense/mvc/app/controllers/OPNsense/Tinc/forms/dialogNetwork.xml @@ -32,12 +32,21 @@ If the other end doesn't respond within this time, the connection is terminated, and the others will be notified of this. + + network.StrictSubnets + + checkbox + When this option is enabled tinc will only use Subnet statements which are present in the host config files in the local /etc/tinc/netname/hosts/ directory. + Subnets learned via connections to other nodes and which are not present in the local host config files are ignored. + + true + network.cipher dropdown The symmetric cipher algorithm used to encrypt UDP packets. - Any cipher supported by LibreSSL or OpenSSL is recognised. + Any cipher supported by OpenSSL is recognised. Furthermore, specifying "none" will turn off packet encryption. It is best to use only those ciphers which support CBC mode @@ -87,6 +96,12 @@ true This machines part of the network + + network.disablesubnetroutes + + checkbox + This will prevent installing subnet routes. Usually you only enable this to do own routing decisions via a local gateway and gateway rules. + network.privkey diff --git a/security/tinc/src/opnsense/mvc/app/models/OPNsense/Tinc/Tinc.xml b/security/tinc/src/opnsense/mvc/app/models/OPNsense/Tinc/Tinc.xml index f3aa3805cf..874f967330 100644 --- a/security/tinc/src/opnsense/mvc/app/models/OPNsense/Tinc/Tinc.xml +++ b/security/tinc/src/opnsense/mvc/app/models/OPNsense/Tinc/Tinc.xml @@ -1,6 +1,6 @@ //OPNsense/Tinc - 1.0.3 + 1.0.5 OPNsense Tinc VPN @@ -13,12 +13,12 @@ Y - /^([0-9a-zA-Z]){1,50}$/u + /^([0-9a-zA-Z]){1,50}$/u The name should contain only alphanumeric characters. Y - /^([0-9a-zA-Z\_]){1,1024}$/u + /^([0-9a-zA-Z\_]){1,1024}$/u Please specify a valid hostname. @@ -29,7 +29,7 @@ Y - 655 + 655 1 65535 Port number must be between 1...65535 @@ -55,11 +55,19 @@ Y - 5 + 5 1 65535 Ping timeout must be between 1...65535 + + 0 + Y + + + 0 + Y + Y @@ -68,35 +76,33 @@ Y - N tinc list ciphers - /tmp/tinc_current_cipher_options.index - aes-256-cbc + aes-256-cbc - Y - router - - router - switch - - - - subnet.check001 - - + Y + router + + router + switch + + + + subnet.check001 + + - 1 + 1 Y - 1 + 1 Y Y - ip + ip [0] start/stop, serious errors [1] +all connections @@ -124,12 +130,12 @@ Y - /^([0-9a-zA-Z\_]){1,1024}$/u + /^([0-9a-zA-Z\_]){1,1024}$/u Please specify a valid hostname. Y - 655 + 655 1 65535 Port number must be between 1...65535 @@ -160,13 +166,11 @@ Y - N tinc list ciphers - /tmp/tinc_current_cipher_options.index - aes-256-cbc + aes-256-cbc - 1 + 1 Y @@ -175,7 +179,7 @@ - 1 + 1 Y diff --git a/security/tinc/src/opnsense/mvc/app/views/OPNsense/Tinc/index.volt b/security/tinc/src/opnsense/mvc/app/views/OPNsense/Tinc/index.volt index 9196054553..2c1ff6e8de 100644 --- a/security/tinc/src/opnsense/mvc/app/views/OPNsense/Tinc/index.volt +++ b/security/tinc/src/opnsense/mvc/app/views/OPNsense/Tinc/index.volt @@ -34,22 +34,22 @@ POSSIBILITY OF SUCH DAMAGE. *************************************************************************************************************/ $("#grid-networks").UIBootgrid( - { search:'/api/tinc/settings/searchNetwork', - get:'/api/tinc/settings/getNetwork/', - set:'/api/tinc/settings/setNetwork/', - add:'/api/tinc/settings/setNetwork/', - del:'/api/tinc/settings/delNetwork/', - toggle:'/api/tinc/settings/toggleNetwork/' + { search:'/api/tinc/settings/search_network', + get:'/api/tinc/settings/get_network/', + set:'/api/tinc/settings/set_network/', + add:'/api/tinc/settings/set_network/', + del:'/api/tinc/settings/del_network/', + toggle:'/api/tinc/settings/toggle_network/' } ); $("#grid-hosts").UIBootgrid( - { search:'/api/tinc/settings/searchHost', - get:'/api/tinc/settings/getHost/', - set:'/api/tinc/settings/setHost/', - add:'/api/tinc/settings/setHost/', - del:'/api/tinc/settings/delHost/', - toggle:'/api/tinc/settings/toggleHost/' + { search:'/api/tinc/settings/search_host', + get:'/api/tinc/settings/get_host/', + set:'/api/tinc/settings/set_host/', + add:'/api/tinc/settings/set_host/', + del:'/api/tinc/settings/del_host/', + toggle:'/api/tinc/settings/toggle_host/' } ); diff --git a/security/tinc/src/opnsense/scripts/OPNsense/Tinc/lib/objects.py b/security/tinc/src/opnsense/scripts/OPNsense/Tinc/lib/objects.py index d35db341ac..ff7204ab2c 100755 --- a/security/tinc/src/opnsense/scripts/OPNsense/Tinc/lib/objects.py +++ b/security/tinc/src/opnsense/scripts/OPNsense/Tinc/lib/objects.py @@ -69,6 +69,8 @@ def __init__(self): self._payload['debuglevel'] = 'd0' self._payload['mode'] = 'switch' self._payload['PMTUDiscovery'] = 'yes' + self._payload['StrictSubnets'] = 'no' + self._disablesubnetroutes = False self._hosts = list() def get_id(self): @@ -81,10 +83,10 @@ def get_mode(self): return self._payload['mode'] def get_debuglevel(self): - if len(self._payload['debuglevel']) > 1: - return self._payload['debuglevel'][1] - else: - return '0' + return self._payload['debuglevel'][1] if len(self._payload['debuglevel']) > 1 else '0' + + def get_disablesubnetroutes(self): + return self._disablesubnetroutes def set_hosts(self, hosts): for host in hosts: @@ -94,10 +96,13 @@ def set_hosts(self, hosts): self._hosts.append(hostObj) def set_PMTUDiscovery(self, value): - if value.text != '1': - self._payload['PMTUDiscovery'] = 'no' - else: - self._payload['PMTUDiscovery'] = 'yes' + self._payload['PMTUDiscovery'] = 'no' if value.text != '1' else 'yes' + + def set_StrictSubnets(self, value): + self._payload['StrictSubnets'] = 'no' if value.text != '1' else 'yes' + + def set_disablesubnetroutes(self, value): + self._disablesubnetroutes = value.text == '1' def config_text(self): result = list() @@ -106,6 +111,7 @@ def config_text(self): result.append('PMTUDiscovery=%(PMTUDiscovery)s' % self._payload) result.append('Port=%(port)s' % self._payload) result.append('PingTimeout=%(pingtimeout)s' % self._payload) + result.append('StrictSubnets=%(StrictSubnets)s' % self._payload) for host in self._hosts: if host.connect_to_this_host(): result.append('ConnectTo = %s' % (host.get_hostname(),)) diff --git a/security/tinc/src/opnsense/scripts/OPNsense/Tinc/tincd.py b/security/tinc/src/opnsense/scripts/OPNsense/Tinc/tincd.py index 3f5f887f49..95aadc99b5 100755 --- a/security/tinc/src/opnsense/scripts/OPNsense/Tinc/tincd.py +++ b/security/tinc/src/opnsense/scripts/OPNsense/Tinc/tincd.py @@ -30,13 +30,13 @@ """ import os import sys -import tempfile import glob -import pipes import xml.etree.ElementTree +import shutil import subprocess import ipaddress from lib import objects +from shlex import quote def write_file(filename, content, mode=0o600): dirname = '/'.join(filename.split('/')[0:-1]) @@ -92,19 +92,38 @@ def deploy(config_filename): if_up = list() if_up.append("#!/bin/sh") - if_up.append("ifconfig %s %s %s" % (interface_name, interface_family, pipes.quote(interface_address))) + if_up.append("ifconfig %s %s %s" % (interface_name, interface_family, quote(interface_address))) if_up.append("configctl interface %s %s" % (interface_configd, interface_name)) write_file("%s/tinc-up" % network.get_basepath(), '\n'.join(if_up) + "\n", 0o700) + # write subnet-{up|down} scripts and ship required binaries into the chroot + chroot_needs = set(['/bin/sh', '/sbin/route', '/libexec/ld-elf.so.1']) + for item in list(chroot_needs): + for line in subprocess.run(['/usr/bin/ldd', item], capture_output=True, text=True).stdout.split('\n'): + if line.find('=>') > 0: + chroot_needs.add(line.split('=>')[1].strip().split()[0]) + for filename in chroot_needs: + os.makedirs('%s%s' % (network.get_basepath(), os.path.dirname(filename)), exist_ok=True) + shutil.copy(filename, '%s/%s' % (network.get_basepath(), filename)) + if not network.get_disablesubnetroutes(): + write_file("%s/subnet-up" % network.get_basepath(), '\n'.join([ + "#!/bin/sh", + "route add $SUBNET -iface %s\n" % interface_name + ]), 0o700) + write_file("%s/subnet-down" % network.get_basepath(), '\n'.join([ + "#!/bin/sh", + "route delete $SUBNET -iface %s\n" % interface_name + ]), 0o700) + # configure and rename new tun device, place all in group "tinc" symlink associated tun device if interface_name not in interfaces: + # remove symlink from previus run (created by ifconfig) if it wasn't cleaned up properly on exit + if os.path.islink('/dev/%s' % interface_name): + os.remove('/dev/%s' % interface_name) tundev = subprocess.run(['/sbin/ifconfig', interface_type, 'create'], capture_output=True, text=True).stdout.split()[0] subprocess.run(['/sbin/ifconfig',tundev,'name',interface_name]) subprocess.run(['/sbin/ifconfig',interface_name,'group','tinc']) - if os.path.islink('/dev/%s' % interface_name): - os.remove('/dev/%s' % interface_name) - os.symlink('/dev/%s' % tundev, '/dev/%s' % interface_name) return networks if len(sys.argv) > 1: diff --git a/security/tinc/src/opnsense/service/conf/actions.d/actions_tinc.conf b/security/tinc/src/opnsense/service/conf/actions.d/actions_tinc.conf index 5bf01fa8eb..9a8e107c30 100644 --- a/security/tinc/src/opnsense/service/conf/actions.d/actions_tinc.conf +++ b/security/tinc/src/opnsense/service/conf/actions.d/actions_tinc.conf @@ -8,6 +8,7 @@ message:generate new keypair command:/usr/local/opnsense/scripts/OPNsense/Tinc/list_ciphers.py parameters: type:script_output +cache_ttl:360 message:list ciphers [stop] diff --git a/security/tinc/src/opnsense/service/templates/OPNsense/Tinc/tinc_deploy.xml b/security/tinc/src/opnsense/service/templates/OPNsense/Tinc/tinc_deploy.xml index 746cf3ece4..46c12f1682 100644 --- a/security/tinc/src/opnsense/service/templates/OPNsense/Tinc/tinc_deploy.xml +++ b/security/tinc/src/opnsense/service/templates/OPNsense/Tinc/tinc_deploy.xml @@ -14,6 +14,8 @@ {{network.extport}} {{network.debuglevel}} {{network.pingtimeout}} + {{network.StrictSubnets}} + {{network.disablesubnetroutes}} {{network.hostname}} diff --git a/security/tor/Makefile b/security/tor/Makefile index 5c19670bb9..b6fb0bfe8f 100644 --- a/security/tor/Makefile +++ b/security/tor/Makefile @@ -1,8 +1,7 @@ PLUGIN_NAME= tor -PLUGIN_VERSION= 1.8 -PLUGIN_REVISION= 3 +PLUGIN_VERSION= 1.10 PLUGIN_COMMENT= The Onion Router -PLUGIN_DEPENDS= tor ruby +PLUGIN_DEPENDS= tor ruby rubygem-rexml PLUGIN_MAINTAINER= franz.fabian.94@gmail.com .include "../../Mk/plugins.mk" diff --git a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/ExitaclController.php b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/ExitaclController.php index 0e0beaa381..beb06d1cdc 100644 --- a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/ExitaclController.php +++ b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/ExitaclController.php @@ -43,7 +43,6 @@ public function searchaclAction() } public function getaclAction($uuid = null) { - $this->sessionClose(); return $this->getBase('exitpolicy', 'policy', $uuid); } public function addaclAction() diff --git a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/GeneralController.php b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/GeneralController.php index 2a36f3ab6e..455057eda9 100644 --- a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/GeneralController.php +++ b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/GeneralController.php @@ -2,7 +2,7 @@ /* * Copyright (C) 2017 Fabian Franz - * Copyright (C) 2015 Jos Schellevis + * Copyright (C) 2015 Jos Schellevis * Copyright (C) 2015-2017 Deciso B.V. * All rights reserved. * @@ -82,7 +82,6 @@ public function searchhidservauthAction() public function gethidservauthAction($uuid = null) { - $this->sessionClose(); return $this->getBase('client_auth', 'client_authentications.client_auth', $uuid); } diff --git a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceController.php b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceController.php index 7655546afc..0079f3d531 100644 --- a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceController.php +++ b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceController.php @@ -40,7 +40,6 @@ public function searchserviceAction() } public function getserviceAction($uuid = null) { - $this->sessionClose(); return $this->getBase('hiddenservice', 'service', $uuid); } public function addserviceAction() diff --git a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceaclController.php b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceaclController.php index 42d372b5f0..75e302df97 100644 --- a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceaclController.php +++ b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceaclController.php @@ -40,7 +40,6 @@ public function searchaclAction() } public function getaclAction($uuid = null) { - $this->sessionClose(); return $this->getBase('hiddenserviceacl', 'hiddenserviceacl', $uuid); } public function addaclAction() diff --git a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/ServiceController.php b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/ServiceController.php index a5ae94af95..40d9cc066b 100644 --- a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/ServiceController.php +++ b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/ServiceController.php @@ -132,9 +132,6 @@ public function statusAction() public function reconfigureAction() { if ($this->request->isPost()) { - // close session for long running action - $this->sessionClose(); - $general = new General(); $backend = new Backend(); diff --git a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/SocksaclController.php b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/SocksaclController.php index 7add9c0fab..449fb38de3 100644 --- a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/SocksaclController.php +++ b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/SocksaclController.php @@ -40,7 +40,6 @@ public function searchaclAction() } public function getaclAction($uuid = null) { - $this->sessionClose(); return $this->getBase('policy', 'policy', $uuid); } public function addaclAction() diff --git a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/acl_exitpolicy.xml b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/acl_exitpolicy.xml index 0f1050ea7b..6308f1b9fa 100644 --- a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/acl_exitpolicy.xml +++ b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/acl_exitpolicy.xml @@ -15,7 +15,7 @@ exitpolicy.network text - Network on which this ACL is applied. Enter 'any' to use wildcard adressing. + Network on which this ACL is applied. Enter 'any' to use wildcard addressing. exitpolicy.startport diff --git a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/relay.xml b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/relay.xml index 5997b877d2..40f86a2681 100644 --- a/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/relay.xml +++ b/security/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/relay.xml @@ -54,6 +54,13 @@ text You can enter a publicly visible (obfuscated) email address into this field to allow other users to notify you if there are issues with your relay. + + relay.family + + text + Fingerprints of other relays controlled or administered by the same group or organization. Do not list any bridge relay as it would compromise its concealment. + true + relay.bandwithrate diff --git a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLExitPolicy.xml b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLExitPolicy.xml index 571c1d567b..87e7f68570 100644 --- a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLExitPolicy.xml +++ b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLExitPolicy.xml @@ -1,44 +1,44 @@ - //OPNsense/tor/exitpolicy - ACL for Socks port - - - - 1 - Y - - - both - Y - - both - IPv4 - IPv6 - - - - Y - - - 1 - N - 65535 - A valid Port number must be specified. - - - 1 - N - 65535 - A valid Port number must be specified. - - - accept - Y - - Accept - Reject - - - - + //OPNsense/tor/exitpolicy + ACL for Socks port + + + + 1 + Y + + + both + Y + + both + IPv4 + IPv6 + + + + Y + + + 1 + N + 65535 + A valid Port number must be specified. + + + 1 + N + 65535 + A valid Port number must be specified. + + + accept + Y + + Accept + Reject + + + + diff --git a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLSocksPolicy.xml b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLSocksPolicy.xml index d0b1a98843..2ae8203b71 100644 --- a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLSocksPolicy.xml +++ b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLSocksPolicy.xml @@ -1,31 +1,31 @@ - //OPNsense/tor/aclsockspolicy - ACL for Socks port - - - - 1 - Y - - - v6 - Y - - IPv4 - IPv6 - - - - Y - - - accept - Y - - Accept - Reject - - - - + //OPNsense/tor/aclsockspolicy + ACL for Socks port + + + + 1 + Y + + + v6 + Y + + IPv4 + IPv6 + + + + Y + + + accept + Y + + Accept + Reject + + + + diff --git a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/General.xml b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/General.xml index 36d0f91bce..0be1e3b283 100644 --- a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/General.xml +++ b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/General.xml @@ -1,141 +1,141 @@ - //OPNsense/tor/general - General Tor configuration - 1.0.0 - - - 0 - Y - - - N - Y - - - 9050 - 0 - Y - 65535 - A valid Port number must be specified. - - - 9051 - 1 - N - 65535 - A valid Port number must be specified. - - - N - /^.+$/ - - - N - /^.+$/ - - - 0 - Y - - - 0 - Y - - - Y - N - notifications - - Errors - Warnings - Notifications - Informational - Debugging - - - - 0 - Y - - - Y - N - notifications - - Errors - Warnings - Notifications - Informational - Debugging - - - - Y - N - KISTLiteVanilla - - KISTLite,Vanilla - Vanilla,KISTLite - KISTLite - Vanilla - - - - 0 - Y - - - 80,443 - Y - /^(\d+,)*\d+$/ - - - 0 - Y - - - 9040 - 0 - Y - 65535 - A valid Port number must be specified. - - - 9053 - 0 - Y - 65535 - A valid Port number must be specified. - - - 172.29.0.0/16 - Y - - - 0 - Y - - - + //OPNsense/tor/general + General Tor configuration + 1.0.0 + - 1 - Y + 0 + Y - - Y - exampleexample23.onion - /^[a-z2-7]{16}\.onion$/i - - - Y - 0000000000000000000000 - /^[a-z0-9\+\/]{22}$/i - - - - - 0 - N - - + + N + Y + + + 9050 + 0 + Y + 65535 + A valid Port number must be specified. + + + 9051 + 1 + N + 65535 + A valid Port number must be specified. + + + N + /^.+$/ + + + N + /^.+$/ + + + 0 + Y + + + 0 + Y + + + Y + N + notifications + + Errors + Warnings + Notifications + Informational + Debugging + + + + 0 + Y + + + Y + N + notifications + + Errors + Warnings + Notifications + Informational + Debugging + + + + Y + N + KISTLiteVanilla + + KISTLite,Vanilla + Vanilla,KISTLite + KISTLite + Vanilla + + + + 0 + Y + + + 80,443 + Y + /^(\d+,)*\d+$/ + + + 0 + Y + + + 9040 + 0 + Y + 65535 + A valid Port number must be specified. + + + 9053 + 0 + Y + 65535 + A valid Port number must be specified. + + + 172.29.0.0/16 + Y + + + 0 + Y + + + + + 1 + Y + + + Y + exampleexample23.onion + /^[a-z2-7]{16}\.onion$/i + + + Y + 0000000000000000000000 + /^[a-z0-9\+\/]{22}$/i + + + + + 0 + N + +
    diff --git a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenService.xml b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenService.xml index 19ece3abc1..e373d08f80 100644 --- a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenService.xml +++ b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenService.xml @@ -1,32 +1,32 @@ - //OPNsense/tor/hiddenservice - Tor Onion service configuration - 1.0.0 - - - - 1 - Y - - - Y - /^[a-z0-9_-]+$/i - The name should only consist of alphanumeric characters, dashes and underscores. - - - basic - Y - - Basic - Stealth - - - - Y - N - /^([a-z0-9_+-]+,)*([a-z0-9_+-]*)$/i - The authorized clients should only consist of alphanumeric characters, dashes, underscores and plus sign. - - - + //OPNsense/tor/hiddenservice + Tor Onion service configuration + 1.0.0 + + + + 1 + Y + + + Y + /^[a-z0-9_-]+$/i + The name should only consist of alphanumeric characters, dashes and underscores. + + + basic + Y + + Basic + Stealth + + + + Y + N + /^([a-z0-9_+-]+,)*([a-z0-9_+-]*)$/i + The authorized clients should only consist of alphanumeric characters, dashes, underscores and plus sign. + + + diff --git a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenServiceACL.xml b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenServiceACL.xml index 54dfb89c87..05d230068a 100644 --- a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenServiceACL.xml +++ b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenServiceACL.xml @@ -1,42 +1,42 @@ - //OPNsense/tor/hiddenserviceacl - Tor Onion Service ACL - - - - 1 - Y - - - - - - An Onion service must be set. - N - Y - - - 80 - 1 - Y - 65535 - A valid Port number must be specified. - - - Y - 127.0.0.1 - - - 80 - 1 - Y - 65535 - A valid Port number must be specified. - - - + //OPNsense/tor/hiddenserviceacl + Tor Onion Service ACL + + + + 1 + Y + + + + + + An Onion service must be set. + N + Y + + + 80 + 1 + Y + 65535 + A valid Port number must be specified. + + + Y + 127.0.0.1 + + + 80 + 1 + Y + 65535 + A valid Port number must be specified. + + + diff --git a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/Relay.xml b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/Relay.xml index c81f06f8ec..a9a2bd7ff2 100644 --- a/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/Relay.xml +++ b/security/tor/src/opnsense/mvc/app/models/OPNsense/Tor/Relay.xml @@ -1,88 +1,93 @@ - //OPNsense/tor/relay - 1.0.0 - Tor Relay configuration - - - 0 - Y - - - 0 - Y - - - N - /^([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$/ - - - N - /^[a-f0-9:]{2,}$/i - - - N - /^([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$/ - - - N - /^[a-f0-9:]{2,}$/i - - - 9001 - 0 - Y - 65535 - A valid Port number must be specified. - - - 1 - N - 65535 - A valid Port number must be specified. - -
    - N - - /^[a-z0-9.-]+$/i -
    - - N - - /^[a-zA-Z0-9]+$/ - - - N - ]+$/]]> - - - N - - - N - - - 0 - Y - - - 1 - Y - - - 0 - Y - - - 0 - Y - - - 0 - N - - - 1 - Y - -
    + //OPNsense/tor/relay + 1.0.0 + Tor Relay configuration + + + 0 + Y + + + 0 + Y + + + N + /^([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$/ + + + N + /^[a-f0-9:]{2,}$/i + + + N + /^([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$/ + + + N + /^[a-f0-9:]{2,}$/i + + + 9001 + 0 + Y + 65535 + A valid Port number must be specified. + + + 1 + N + 65535 + A valid Port number must be specified. + +
    + N + + /^[a-z0-9.-]+$/i +
    + + N + + /^[a-zA-Z0-9]+$/ + + + N + ]+$/]]> + + + N + + /^[a-fA-F0-9,]+$/ + + + N + + + N + + + 0 + Y + + + 1 + Y + + + 0 + Y + + + 0 + Y + + + 0 + N + + + 1 + Y + +
    diff --git a/security/tor/src/opnsense/scripts/tor/tor_diag b/security/tor/src/opnsense/scripts/tor/tor_diag index 266644ce0a..4d5a7593ca 100755 --- a/security/tor/src/opnsense/scripts/tor/tor_diag +++ b/security/tor/src/opnsense/scripts/tor/tor_diag @@ -1,6 +1,9 @@ #!/usr/local/bin/ruby + =begin -Copyright 2017 Fabian Franz +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: @@ -22,7 +25,6 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =end - require 'enumerator' require 'json' require 'optparse' @@ -33,7 +35,6 @@ require 'rexml/document' # global for showing debug output if needed $TOR_DEBUG = false - config = REXML::Document.new(File.new("/conf/config.xml")) $TOR_PASSWORD = config.elements['opnsense/OPNsense/tor/general/control_port_password'].text $TOR_CONTROL_PORT = 9051 diff --git a/security/tor/src/opnsense/service/conf/actions.d/actions_tor.conf b/security/tor/src/opnsense/service/conf/actions.d/actions_tor.conf index ad3672a4ff..202c4389cb 100644 --- a/security/tor/src/opnsense/service/conf/actions.d/actions_tor.conf +++ b/security/tor/src/opnsense/service/conf/actions.d/actions_tor.conf @@ -1,5 +1,5 @@ [start] -command:/usr/local/opnsense/scripts/tor/setup.sh;/usr/local/etc/rc.d/tor start +command:/usr/local/etc/rc.d/tor start parameters: type:script message:starting tor @@ -11,13 +11,13 @@ type:script message:stopping tor [restart] -command:/usr/local/opnsense/scripts/tor/setup.sh;/usr/local/etc/rc.d/tor restart +command:/usr/local/etc/rc.d/tor restart parameters: type:script message:restarting tor [status] -command:/usr/local/etc/rc.d/tor status;exit 0 +command:/usr/local/etc/rc.d/tor status; exit 0 parameters: type:script_output message:request tor status diff --git a/security/tor/src/opnsense/service/templates/OPNsense/Tor/tor b/security/tor/src/opnsense/service/templates/OPNsense/Tor/tor index 5816e26026..98de5860c4 100644 --- a/security/tor/src/opnsense/service/templates/OPNsense/Tor/tor +++ b/security/tor/src/opnsense/service/templates/OPNsense/Tor/tor @@ -1,5 +1,5 @@ {% if helpers.exists('OPNsense.tor.general.enabled') and OPNsense.tor.general.enabled == '1' %} -tor_var_script="/usr/local/opnsense/scripts/tor/setup.sh" +tor_setup="/usr/local/opnsense/scripts/tor/setup.sh" tor_enable="YES" {% else %} tor_enable="NO" diff --git a/security/tor/src/opnsense/service/templates/OPNsense/Tor/torrc b/security/tor/src/opnsense/service/templates/OPNsense/Tor/torrc index 04ccca9de0..c92807af0f 100644 --- a/security/tor/src/opnsense/service/templates/OPNsense/Tor/torrc +++ b/security/tor/src/opnsense/service/templates/OPNsense/Tor/torrc @@ -1,4 +1,3 @@ -{% from 'OPNsense/Macros/interface.macro' import physical_interface %} ## ## OPNsense autogenerated config file. ## Don't change it because your changes get lost. @@ -27,7 +26,7 @@ SOCKSPort [{{ interface_ip }}]:{{ OPNsense.tor.general.socks_listen_port|default {% endif %} {% if helpers.exists('virtualip') %} {% for intf_item in helpers.toList('virtualip.vip') %} -{% if intf_item.interface == listen_interface and intf_item.type == 'single' %} +{% if intf_item.interface == listen_interface and intf_item.mode in ['carp', 'ipalias'] %} {% if intf_item.subnet.find(':') > -1 %} # {{ listen_interface }}: IPv6 VIP SOCKSPort [{{ intf_item.subnet }}]:{{ OPNsense.tor.general.socks_listen_port|default('9050') }} @@ -117,6 +116,8 @@ HidServAuth {{ service.onion_service }} {{ service.auth_cookie }} MaxMemInQueues {{ OPNsense.tor.general.max_memory_in_queues }} MB {% endif %} +HardwareAccel 1 + {% if helpers.exists('OPNsense.tor.hiddenservice') and helpers.exists('OPNsense.tor.hiddenserviceacl') and helpers.exists('OPNsense.tor.hiddenserviceacl.hiddenserviceacl') %} ############### This section is just for location-hidden services ### @@ -170,6 +171,12 @@ OutboundBindAddress {{ OPNsense.tor.relay.outboundbindv6 }} Nickname {{ OPNsense.tor.relay.nick }} {% endif %} +{% if helpers.exists('OPNsense.tor.relay.family') and OPNsense.tor.relay.family != '' %} +{% if OPNsense.tor.relay.relay|default('1') != '1' %} +MyFamily {{ OPNsense.tor.relay.family }} +{% endif %} +{% endif %} + {% if helpers.exists('OPNsense.tor.relay.contact_info') and OPNsense.tor.relay.contact_info != '' %} ContactInfo {{ OPNsense.tor.relay.contact_info }} {% endif %} diff --git a/security/wazuh-agent/+POST_DEINSTALL.post b/security/wazuh-agent/+POST_DEINSTALL.post new file mode 100644 index 0000000000..6e1f1bc557 --- /dev/null +++ b/security/wazuh-agent/+POST_DEINSTALL.post @@ -0,0 +1 @@ +rm /var/ossec/active-response/bin/opnsense-fw diff --git a/security/wazuh-agent/+POST_INSTALL.post b/security/wazuh-agent/+POST_INSTALL.post new file mode 100644 index 0000000000..374cc76f41 --- /dev/null +++ b/security/wazuh-agent/+POST_INSTALL.post @@ -0,0 +1,5 @@ +echo -n 'reload filter to register alias: ' +/usr/local/sbin/configctl filter reload +cp /usr/local/opnsense/scripts/wazuh/opnsense-fw /var/ossec/active-response/bin/opnsense-fw +chmod 750 /var/ossec/active-response/bin/opnsense-fw +chown root:wazuh /var/ossec/active-response/bin/opnsense-fw diff --git a/security/wazuh-agent/Makefile b/security/wazuh-agent/Makefile new file mode 100644 index 0000000000..52165d834c --- /dev/null +++ b/security/wazuh-agent/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= wazuh-agent +PLUGIN_VERSION= 1.3 +PLUGIN_REVISION= 1 +PLUGIN_COMMENT= Agent for the open source security platform Wazuh +PLUGIN_DEPENDS= wazuh-agent +PLUGIN_MAINTAINER= ad@opnsense.org + +.include "../../Mk/plugins.mk" diff --git a/security/wazuh-agent/pkg-descr b/security/wazuh-agent/pkg-descr new file mode 100644 index 0000000000..2a7e397c2f --- /dev/null +++ b/security/wazuh-agent/pkg-descr @@ -0,0 +1,28 @@ +Wazuh is a free and open source platform used for threat prevention, detection, +and response. It is capable of protecting workloads across on-premises, +virtualized, containerized, and cloud-based environments. + +Using this plugin you can integrate your OPNsense firewall into the Wazuh +solution. + +Plugin Changelog +================ + +1.3 + +* Fix active response duplicate key causing false aborts (contributed by Michael Bedworth) +* Add repeated_offenders config and fix template issues (contributed by Michael Bedworth) + +1.2 + +* Implement options to change server ports (contributed by 999eagle) +* Make agent_name configurable (contributed by Andy Binder) + +1.1 + +* Add advanced option to specify transport protocol + +1.0 + +* Initial version +* Improve service information fetch by avoiding creation of a model diff --git a/security/wazuh-agent/src/etc/inc/plugins.inc.d/wazuhagent.inc b/security/wazuh-agent/src/etc/inc/plugins.inc.d/wazuhagent.inc new file mode 100644 index 0000000000..ce7d94ade4 --- /dev/null +++ b/security/wazuh-agent/src/etc/inc/plugins.inc.d/wazuhagent.inc @@ -0,0 +1,67 @@ +object(); + $is_enabled = false; + if ($cnf->OPNsense && $cnf->OPNsense->WazuhAgent && $cnf->OPNsense->WazuhAgent->general) { + $is_enabled = $cnf->OPNsense->WazuhAgent->general->enabled == '1'; + } + + if ($is_enabled) { + $service = [ + 'description' => gettext('Wazuh Agent'), + 'configd' => [ + 'restart' => ['wazuh_agent restart'], + 'start' => ['wazuh_agent start'], + 'stop' => ['wazuh_agent stop'], + ], + 'name' => 'wazuh-agentd', + ]; + $services[] = $service; + } + + return $services; +} + +function wazuhagent_firewall($fw) +{ + global $config; + $defaults = ['block' => ['type' => 'block', 'log' => !isset($config['syslog']['nologdefaultblock'])]]; + if ((new \OPNsense\WazuhAgent\WazuhAgent())->general->enabled == '1') { + //$fw->registerFilterRule(); + $fw->registerFilterRule( + 1, + ['from' => '<__wazuh_agent_drop>', 'descr' => 'Wazuh agent blocklist', '#ref' => 'ui/wazuhagent/'], + $defaults['block'] + ); + } +} diff --git a/security/wazuh-agent/src/opnsense/mvc/app/controllers/OPNsense/WazuhAgent/Api/ServiceController.php b/security/wazuh-agent/src/opnsense/mvc/app/controllers/OPNsense/WazuhAgent/Api/ServiceController.php new file mode 100644 index 0000000000..9c74d7431d --- /dev/null +++ b/security/wazuh-agent/src/opnsense/mvc/app/controllers/OPNsense/WazuhAgent/Api/ServiceController.php @@ -0,0 +1,44 @@ +view->formSettings = $this->getForm("settings"); + // choose template + $this->view->pick('OPNsense/WazuhAgent/index'); + } +} diff --git a/security/wazuh-agent/src/opnsense/mvc/app/controllers/OPNsense/WazuhAgent/forms/settings.xml b/security/wazuh-agent/src/opnsense/mvc/app/controllers/OPNsense/WazuhAgent/forms/settings.xml new file mode 100644 index 0000000000..99791664d8 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/mvc/app/controllers/OPNsense/WazuhAgent/forms/settings.xml @@ -0,0 +1,159 @@ +
    + + header + + + + agent.general.enabled + + checkbox + Enable Wazuh Agent + + + agent.general.server_address + + text + Specifies the IP address or the hostname of the Wazuh manager. + + + agent.general.agent_name + + text + true + Specifies the hostname of this agent. + + + agent.general.protocol + + dropdown + true + Specifies the transport protocol to use. + + + agent.general.port + + text + true + Specifies the port to use for communicating with the Wazuh manager. + + + agent.logcollector.syslog_programs + + select_multiple + Choose which applications to forward to Wazuh. + + + agent.logcollector.suricata_eve_log + + checkbox + Send events from the intrusion detection engine to Wazuh (Suricata EVE log) + + + agent.logcollector.remote_commands + + checkbox + true + + Enable remote commands from the log collector, disabling this will ignore command and full_command log sources + and prevents Wazuh manager from running arbitrary commands on this node. + + + + agent.general.debug_level + true + + dropdown + + Debug level for this agents services. + + + + header + + true + + + agent.active_response.enabled + + checkbox + Enable Active response + + + agent.active_response.fw_alias_ignore + + dropdown + + Select an alias from which the items should be ignored when dropping ip addresses using the opnsense-fw + active-response action. + + + + agent.active_response.repeated_offenders + + text + + Comma-separated list of increasing timeout values in minutes for repeat offenders (e.g., 30,60,120,240). + When an IP triggers active response multiple times, each subsequent block uses the next timeout value. + Leave empty to disable repeated offender escalation. + + + + agent.active_response.remote_commands + + checkbox + true + + Toggles whether Command Module should accept commands defined in the shared configuration or not. + + + + header + + true + + + agent.auth.password + + password + Password to use in authd.pass file. + + + agent.auth.port + + text + Specifies the port to use for communicating with the Wazuh manager during enrollment. + + + header + + true + + + agent.rootcheck.enabled + + checkbox + Enable policy monitoring and anomaly detection + + + header + + true + + + agent.syscollector.enabled + + checkbox + Enable syscollector + + + header + + true + + + agent.syscheck.enabled + + checkbox + Enable file integrity monitoring + +
    diff --git a/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/Firewall/static_aliases/wazuh_agent.json b/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/Firewall/static_aliases/wazuh_agent.json new file mode 100644 index 0000000000..94c0bbb040 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/Firewall/static_aliases/wazuh_agent.json @@ -0,0 +1,9 @@ +{ + "__wazuh_agent_drop": { + "enabled": "1", + "name": "__wazuh_agent_drop", + "type": "external", + "description": "Wazuh Agent blocklist (internal)", + "content": "" + } +} diff --git a/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/WazuhAgent/ACL/ACL.xml b/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/WazuhAgent/ACL/ACL.xml new file mode 100644 index 0000000000..cb1abc5c3c --- /dev/null +++ b/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/WazuhAgent/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + Services: Wazuh Agent + + ui/wazuh_agent/* + api/wazuh_agent/* + + + diff --git a/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/WazuhAgent/Menu/Menu.xml b/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/WazuhAgent/Menu/Menu.xml new file mode 100644 index 0000000000..086ffad92b --- /dev/null +++ b/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/WazuhAgent/Menu/Menu.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/WazuhAgent/WazuhAgent.php b/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/WazuhAgent/WazuhAgent.php new file mode 100644 index 0000000000..1e0fe5dbcd --- /dev/null +++ b/security/wazuh-agent/src/opnsense/mvc/app/models/OPNsense/WazuhAgent/WazuhAgent.php @@ -0,0 +1,40 @@ + + //OPNsense/WazuhAgent + 1.0.3 + Wazuh Agent + + + + 1 + Y + + + Y + Y + + + N + + + tcp + Y + + TCP + UDP + + + + 1514 + Y + 1 + 65536 + This must be a valid port number. + + + 0 + Y + + no debug + first level of debug + full debugging + + + + + + + + 1515 + Y + 1 + 65536 + This must be a valid port number. + + + + + 1 + Y + + + Y + syslog list applications + /tmp/syslog_applications.json + 20 + Y + Specify valid source applications. + + + 1 + Y + + + + + 1 + Y + + + + + 1 + Y + + + + + 1 + Y + + + + + 1 + Y + + + 1 + Y + + + + + OPNsense.Firewall.Alias + aliases.alias + name + + /[network|host]/ + + + + + + /^([0-9]+)(,[0-9]+)*$/ + Enter comma-separated timeout values in minutes (e.g., 30,60,120,240). + + + + diff --git a/security/wazuh-agent/src/opnsense/mvc/app/views/OPNsense/WazuhAgent/index.volt b/security/wazuh-agent/src/opnsense/mvc/app/views/OPNsense/WazuhAgent/index.volt new file mode 100644 index 0000000000..4830c677de --- /dev/null +++ b/security/wazuh-agent/src/opnsense/mvc/app/views/OPNsense/WazuhAgent/index.volt @@ -0,0 +1,76 @@ +{# + +OPNsense® is Copyright © 2023 by Deciso B.V. +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':formSettings,'id':'frm_settings'])}} +
    +
    + +
    +
    +
    +
    + +

    +
    +
    +
    diff --git a/security/wazuh-agent/src/opnsense/scripts/syslog/logformats/wazuhagent.py b/security/wazuh-agent/src/opnsense/scripts/syslog/logformats/wazuhagent.py new file mode 100755 index 0000000000..5a903264a2 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/scripts/syslog/logformats/wazuhagent.py @@ -0,0 +1,51 @@ +""" + Copyright (c) 2023 Ad Schellevis + 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. +""" +import re +import datetime +from . import NewBaseLogFormat +ossec_timeformat = r'^(\d{4}/\d{1,2}/\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}).*' + +class OssecLogFormat(NewBaseLogFormat): + def __init__(self, filename): + super().__init__(filename) + self._priority = 100 + + def match(self, line): + return self._filename.find('wazuhagent') > -1 and re.match(ossec_timeformat, line) is not None + + @property + def timestamp(self): + tmp = re.match(ossec_timeformat, self._line) + grp = tmp.group(1) + return datetime.datetime.strptime(grp, "%Y/%m/%d %H:%M:%S").isoformat() + + @property + def process_name(self): + return self._line[19:].strip().split(':', 1)[0] + + @property + def line(self): + return self._line[19:].strip().split(':', 1)[-1] diff --git a/security/wazuh-agent/src/opnsense/scripts/wazuh/opnsense-fw b/security/wazuh-agent/src/opnsense/scripts/wazuh/opnsense-fw new file mode 100755 index 0000000000..aff38306c6 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/scripts/wazuh/opnsense-fw @@ -0,0 +1,164 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2023 Ad Schellevis + 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. +""" + +import argparse +import datetime +import json +import os +import sys +import subprocess +import ipaddress +from select import select +from configparser import ConfigParser + + +def send_log(line): + with open('/var/ossec/logs/active-responses.log', 'a') as handle: + handle.write( + str(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')) + + " " + + os.path.basename(__file__) + + ": " + + line + + "\n" + ) + + +def read_data(filename): + payload = [] + with open(filename, 'rb') as fin: + while True: + rlist, _, _ = select([fin], [], [], 0.5) + if not rlist: + break + line = fin.readline() + if line == b'': + break + payload.append(line) + + return b''.join(payload) + + +def main(params): + send_log('Started') + skip_alias='' + if os.path.isfile('/var/ossec/etc/opnsense-fw.conf'): + cnf = ConfigParser() + cnf.read('/var/ossec/etc/opnsense-fw.conf') + skip_alias = cnf.get('general', 'skip_alias') if cnf.has_option('general', 'skip_alias') else '' + else: + send_log('Skip configuration') + + event=None + try: + event=json.loads(read_data(params.input)) + except ValueError: + pass + if event is None: + send_log('Decoding JSON has failed, invalid input format') + return -1 + else: + send_log('Received : %s' % json.dumps(event)) + + command=event.get('command', None) + srcip=event + for token in ['parameters', 'alert', 'data', 'srcip']: + if type(srcip) is dict and token in srcip: + srcip = srcip[token] + else: + srcip = None + break + + if srcip is None: + send_log('srcip not found') + return -1 + + try: + ipaddress.ip_address(srcip) + except ValueError: + send_log('Unable to process event, invalid srcip (%s)' % srcip) + return -1 + + if skip_alias != '' and command == 'add': + sp = subprocess.run(['/sbin/pfctl', '-t', skip_alias, '-Ttest', srcip], capture_output=True, text=True) + if sp.stderr.strip().find("1/1") == 0: + send_log('Skip event %s in alias %s' % (srcip, skip_alias)) + return 0 + + if command == 'add': + # return rule id for timeout list + try: + unique_key = "%s-%s" % (event['parameters']['alert']['rule']['id'], srcip) + send_log('Sending check_keys for: %s' % unique_key) + print(json.dumps({ + "version": 1, + "origin": { + "name": sys.argv[0], + "module": "active-response" + }, + "command": "check_keys", + "parameters": { + "keys": [unique_key] + } + })) + sys.stdout.flush() + except KeyError: + pass + # When attached to stdin we're likely running inside the agent, in which case we will read a second event which + # may abort the first one. + if params.input == '/dev/stdin': + send_log('Waiting for manager response...') + timeout_event = None + try: + timeout_event=json.loads(read_data(params.input)) + except ValueError: + pass + if timeout_event: + send_log('Received : %s' % json.dumps(timeout_event)) + send_log('Manager says: %s' % timeout_event.get('command')) + if timeout_event.get('command') == 'abort': + send_log('Aborted') + return 0 + elif timeout_event.get('command') != 'continue': + send_log('Invalid command') + return -1 + # add to table and kill active sessions for this ip as well + subprocess.run(['/sbin/pfctl', '-t', '__wazuh_agent_drop', '-T', 'add', srcip], capture_output=True) + subprocess.run(['/sbin/pfctl', '-k', srcip], capture_output=True) + elif command == 'delete': + subprocess.run(['/sbin/pfctl', '-t', '__wazuh_agent_drop', '-T', 'delete', srcip], capture_output=True) + + send_log('Active response executed (%s %s)' % (command, srcip)) + + return 0 + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-input', help='read message from', default='/dev/stdin') + sys.exit(main(parser.parse_args())) diff --git a/security/wazuh-agent/src/opnsense/scripts/wazuh/setup.php b/security/wazuh-agent/src/opnsense/scripts/wazuh/setup.php new file mode 100755 index 0000000000..4b78ab5e0f --- /dev/null +++ b/security/wazuh-agent/src/opnsense/scripts/wazuh/setup.php @@ -0,0 +1,66 @@ +#!/usr/local/bin/php +auth->password)) { + $fhandle = fopen($authd_pass, 'a+'); + if (flock($fhandle, LOCK_EX)) { + chown($authd_pass, 'root'); + chgrp($authd_pass, 'wazuh'); + chmod($authd_pass, 0640); + fseek($fhandle, 0); + ftruncate($fhandle, 0); + fwrite($fhandle, (string)$mdl->auth->password); + flock($fhandle, LOCK_UN); + } +} elseif (file_exists($authd_pass)) { + unlink($authd_pass); +} + +/*** + * Temporary solution to link log files so we can view at least the last items in the file easily for debug purposes + * It looks like ossec is not able to log to syslog directly, which means our log files live outside our normal bounds + **/ +if (!is_dir("/var/log/wazuhagent/ossec/")) { + mkdir("/var/log/wazuhagent/ossec/", 0700, true); +} +if (!is_dir("/var/log/wazuhagent/activeresponses/")) { + mkdir("/var/log/wazuhagent/activeresponses/", 0700, true); +} +@symlink("/var/ossec/logs/ossec.log", "/var/log/wazuhagent/ossec/ossec_99991231.log"); +@symlink("/var/ossec/logs/active-responses.log", "/var/log/wazuhagent/activeresponses/activeresponses_99991231.log"); diff --git a/security/wazuh-agent/src/opnsense/service/conf/actions.d/actions_wazuh_agent.conf b/security/wazuh-agent/src/opnsense/service/conf/actions.d/actions_wazuh_agent.conf new file mode 100644 index 0000000000..33c1e1c4aa --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/conf/actions.d/actions_wazuh_agent.conf @@ -0,0 +1,25 @@ +[start] +command: + /usr/local/sbin/pluginctl -s syslog-ng restart; + /usr/local/etc/rc.d/wazuh-agent onestart +type:script +message:starting wazuh-agent + +[stop] +command:/usr/local/etc/rc.d/wazuh-agent onestop +type:script +message:stopping wazuh-agent + +[status] +command: + /usr/local/etc/rc.d/wazuh-agent status > /dev/null 2>&1 && echo "wazuh is running..." || echo "wazuh is not running..."; + exit 0 +type:script_output +message:get wazuh-agent status + +[restart] +command: + /usr/local/sbin/pluginctl -s syslog-ng restart; + /usr/local/etc/rc.d/wazuh-agent onerestart +type:script +message:restarting wazuh-agent diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/+TARGETS b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/+TARGETS new file mode 100644 index 0000000000..268450b041 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/+TARGETS @@ -0,0 +1,6 @@ +ossec.conf:/var/ossec/etc/ossec.conf +local_internal_options.conf:/var/ossec/etc/local_internal_options.conf +rc.conf.d:/etc/rc.conf.d/wazuh_agent +syslog-ng-wazuh-agent.conf:/usr/local/etc/syslog-ng.conf.d/syslog-ng-wazuh-agent.conf +newsyslog.conf:/etc/newsyslog.conf.d/wazuh-agent.conf +opnsense-fw.conf:/var/ossec/etc/opnsense-fw.conf diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/local_internal_options.conf b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/local_internal_options.conf new file mode 100644 index 0000000000..2ed6e9dded --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/local_internal_options.conf @@ -0,0 +1,28 @@ +# local_internal_options.conf +# +# This file should be handled with care. It contains +# run time modifications that can affect the use +# of OSSEC. Only change it if you know what you +# are doing. Look first at ossec.conf +# for most of the things you want to change. +# +# This file will not be overwritten during upgrades. +logcollector.remote_commands={% if not helpers.empty('OPNsense.WazuhAgent.logcollector.remote_commands') %}1{% else %}0{% endif +%} +wazuh_command.remote_commandss={% if not helpers.empty('OPNsense.WazuhAgent.wazuh_command.remote_commands') %}1{% else %}0{% endif +%} + + +{% if not helpers.empty('OPNsense.WazuhAgent.general.debug_level') %} +windows.debug={{OPNsense.WazuhAgent.general.debug_level}} +syscheck.debug={{OPNsense.WazuhAgent.general.debug_level}} +remoted.debug={{OPNsense.WazuhAgent.general.debug_level}} +analysisd.debug={{OPNsense.WazuhAgent.general.debug_level}} +authd.debug={{OPNsense.WazuhAgent.general.debug_level}} +execd.debug={{OPNsense.WazuhAgent.general.debug_level}} +monitord.debug={{OPNsense.WazuhAgent.general.debug_level}} +logcollector.debug={{OPNsense.WazuhAgent.general.debug_level}} +integrator.debug={{OPNsense.WazuhAgent.general.debug_level}} +agent.debug={{OPNsense.WazuhAgent.general.debug_level}} +wazuh_db.debug={{OPNsense.WazuhAgent.general.debug_level}} +wazuh_modules.debug={{OPNsense.WazuhAgent.general.debug_level}} +wazuh_clusterd.debug={{OPNsense.WazuhAgent.general.debug_level}} +{% endif %} diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/newsyslog.conf b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/newsyslog.conf new file mode 100644 index 0000000000..558d19da0e --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/newsyslog.conf @@ -0,0 +1,4 @@ +# logfilename [owner:group] mode count size when flags [/pid_file] [sig_num] +{% if not helpers.empty('OPNsense.WazuhAgent.general.enabled') %} +/var/ossec/logs/opnsense_syslog.log root:wazuh 660 2 * $D0 Z /var/run/syslog-ng.pid +{% endif %} diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/opnsense-fw.conf b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/opnsense-fw.conf new file mode 100644 index 0000000000..b014fbaaf1 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/opnsense-fw.conf @@ -0,0 +1,4 @@ +[general] +{% if not helpers.empty('OPNsense.WazuhAgent.active_response.fw_alias_ignore') and helpers.getUUID(OPNsense.WazuhAgent.active_response.fw_alias_ignore) %} +skip_alias={{helpers.getUUID(OPNsense.WazuhAgent.active_response.fw_alias_ignore).name}} +{% endif %} diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec.conf b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec.conf new file mode 100644 index 0000000000..c92a5825f8 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec.conf @@ -0,0 +1,30 @@ + + + +
    {{OPNsense.WazuhAgent.general.server_address}}
    + {{OPNsense.WazuhAgent.general.protocol}} + {{OPNsense.WazuhAgent.general.port}} +
    + aes + +{% if not helpers.empty('OPNsense.WazuhAgent.general.agent_name') %} + {{ OPNsense.WazuhAgent.general.agent_name }} +{% endif %} + {{OPNsense.WazuhAgent.auth.port}} + +
    + + + + no + 5000 + 500 + + +{% for sfilename in helpers.glob("OPNsense/WazuhAgent/ossec_config.d/*.conf") %}{% + include sfilename ++%} + +{% endfor %} + +
    diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/000-rootcheck.conf b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/000-rootcheck.conf new file mode 100644 index 0000000000..e0f98ff468 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/000-rootcheck.conf @@ -0,0 +1,16 @@ + + + {% if not helpers.empty('OPNsense.WazuhAgent.rootcheck.enabled') %}no{% else %}yes{% endif %} + + + 43200 + + /var/ossec/etc/shared/rootkit_files.txt + /var/ossec/etc/shared/rootkit_trojans.txt + + /var/ossec/etc/shared/system_audit_rcl.txt + /var/ossec/etc/shared/system_audit_ssh.txt + /var/ossec/etc/shared/cis_debian_linux_rcl.txt + + yes + diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/001-wodle_syscollector.conf b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/001-wodle_syscollector.conf new file mode 100644 index 0000000000..b3d32ce38a --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/001-wodle_syscollector.conf @@ -0,0 +1,13 @@ + + {% if not helpers.empty('OPNsense.WazuhAgent.syscollector.enabled') %}no{% else %}yes{% endif %} + 1h + yes + yes + yes + yes + + + + 10 + + diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/002-syscheck.conf b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/002-syscheck.conf new file mode 100644 index 0000000000..4285d50e07 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/002-syscheck.conf @@ -0,0 +1,55 @@ + + + {% if not helpers.empty('OPNsense.WazuhAgent.syscheck.enabled') %}no{% else %}yes{% endif %} + + + 43200 + + yes + + + /etc,/usr/bin,/usr/sbin + /bin,/sbin,/boot + + + /etc/mtab + /etc/hosts.deny + /etc/mail/statistics + /etc/random-seed + /etc/random.seed + /etc/adjtime + /etc/httpd/logs + /etc/utmpx + /etc/wtmpx + /etc/cups/certs + /etc/dumpdates + /etc/svc/volatile + /sys/kernel/security + /sys/kernel/debug + + + .log$|.swp$ + + + /etc/ssl/private.key + + yes + yes + yes + yes + + + 10 + + + 100 + + + + yes + 5m + 30 + 16384 + 10 + + diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/003-localfile-generic.conf b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/003-localfile-generic.conf new file mode 100644 index 0000000000..6b337fca18 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/003-localfile-generic.conf @@ -0,0 +1,10 @@ + + + syslog + /var/ossec/logs/active-responses.log + + + + syslog + /var/ossec/logs/opnsense_syslog.log + diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/004-localfile-suricata.conf b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/004-localfile-suricata.conf new file mode 100644 index 0000000000..cb627a493a --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/004-localfile-suricata.conf @@ -0,0 +1,9 @@ + +{% if not helpers.empty('OPNsense.WazuhAgent.logcollector.suricata_eve_log') %} + + json + /var/log/suricata/eve.json + +{% else %} + +{% endif %} diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/005-active-response.conf b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/005-active-response.conf new file mode 100644 index 0000000000..711e86fccf --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/ossec_config.d/005-active-response.conf @@ -0,0 +1,7 @@ + + + {% if not helpers.empty('OPNsense.WazuhAgent.active_response.enabled') %}no{% else %}yes{% endif %} +{% if not helpers.empty('OPNsense.WazuhAgent.active_response.repeated_offenders') %} + {{ OPNsense.WazuhAgent.active_response.repeated_offenders }} +{% endif %} + diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/rc.conf.d b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/rc.conf.d new file mode 100644 index 0000000000..c1e4bbe5c2 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/rc.conf.d @@ -0,0 +1,2 @@ +wazuh_agent_setup="/usr/local/opnsense/scripts/wazuh/setup.php" +wazuh_agent_enable={% if not helpers.empty('OPNsense.WazuhAgent.general.enabled') %}"YES"{% else %}"NO"{% endif %} diff --git a/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/syslog-ng-wazuh-agent.conf b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/syslog-ng-wazuh-agent.conf new file mode 100644 index 0000000000..562a646294 --- /dev/null +++ b/security/wazuh-agent/src/opnsense/service/templates/OPNsense/WazuhAgent/syslog-ng-wazuh-agent.conf @@ -0,0 +1,27 @@ +####################################################################################################################### +# This syslog-ng output is a bit of a work-around. As Wazuh does only support RFC3164 format syslog data +# (https://github.com/wazuh/wazuh/issues/2038), we do need to flush our syslog output twice. +# +# Ideally we should then send it to a pipe, but that seems to be a feature +# currenty on the wishlist (https://github.com/wazuh/wazuh/issues/15178) +# +# So, we will flush relevant messages to /var/ossec/logs/opnsense_syslog.log, which newsyslog may rotate +# +####################################################################################################################### +{% if not helpers.empty('OPNsense.WazuhAgent.general.enabled') and not helpers.empty('OPNsense.WazuhAgent.logcollector.syslog_programs') %} +filter f_local_wazuhagent { +{% for prg in OPNsense.WazuhAgent.logcollector.syslog_programs.split(',') %} + program("{{prg}}") {% if loop.last %} ; {% else %} or {% endif +%} +{% endfor %} +}; + +destination d_local_wazuhagent { + file("/var/ossec/logs/opnsense_syslog.log"); +}; + +log { + source(s_all); + filter(f_local_wazuhagent); + destination(d_local_wazuhagent); +}; +{% endif %} diff --git a/sysutils/apcupsd/Makefile b/sysutils/apcupsd/Makefile new file mode 100644 index 0000000000..744bb779ce --- /dev/null +++ b/sysutils/apcupsd/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= apcupsd +PLUGIN_VERSION= 1.2 +PLUGIN_REVISION= 3 +PLUGIN_DEPENDS= apcupsd +PLUGIN_COMMENT= APCUPSD - APC UPS daemon +PLUGIN_MAINTAINER= xbb@xbblabs.com + +.include "../../Mk/plugins.mk" diff --git a/sysutils/apcupsd/pkg-descr b/sysutils/apcupsd/pkg-descr new file mode 100644 index 0000000000..94fb33301b --- /dev/null +++ b/sysutils/apcupsd/pkg-descr @@ -0,0 +1,27 @@ +Apcupsd, short for APC UPS daemon, can be used for controlling all APC UPS models. +It can monitor and log the current power and battery status, perform automatic +shutdown, and can run in network mode in order to power down other hosts on a LAN. + +This plugin allows you to configure an APC UPS for use with OPNsense using the +Apcupsd project. The setup page allows you to set the most common options for +connecting your UPS to your OPNsense router and a status page for the UPS +status. It also includes support to act as an APC Netserver. + +WWW: http://www.apcupsd.org/ + +Plugin Changelog +================ + +1.2 + +* New dashboard widget + +1.1 + +* Add log view + +1.0 (initial release) + +* Apcupsd service control and configuration +* UPS status page +* Dashboard widget diff --git a/sysutils/apcupsd/src/etc/inc/plugins.inc.d/apcupsd.inc b/sysutils/apcupsd/src/etc/inc/plugins.inc.d/apcupsd.inc new file mode 100644 index 0000000000..94a2640164 --- /dev/null +++ b/sysutils/apcupsd/src/etc/inc/plugins.inc.d/apcupsd.inc @@ -0,0 +1,77 @@ + gettext('APC UPS Daemon'), + 'configd' => array( + 'restart' => array('apcupsd restart'), + 'start' => array('apcupsd start'), + 'stop' => array('apcupsd stop'), + ), + 'name' => 'apcupsd', + 'pidfile' => '/var/run/apcupsd.pid' + ); + } + return $services; +} + +/** + * sync configuration via xmlrpc + * @return array + */ +function apcupsd_xmlrpc_sync() +{ + $result = array(); + + $result[] = array( + 'description' => gettext('APC UPS Daemon'), + 'section' => 'OPNsense.apcupsd', + 'id' => 'apcupsd', + 'services' => ["apcupsd"], + ); + + return $result; +} diff --git a/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/Api/ServiceController.php b/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/Api/ServiceController.php new file mode 100644 index 0000000000..89c06c8223 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/Api/ServiceController.php @@ -0,0 +1,153 @@ +getUpsStatusOutput(); + $result['status'] = null; + if (!$result['error']) { + $result['status'] = $this->parseUpsStatus($result['output']); + } + return $result; + } + + private function parseUpsStatus($statusOutput) + { + $status = array(); + foreach (explode("\n", $statusOutput) as $line) { + $kv = array_map('trim', explode(':', $line, 2)); + $key = $kv[0]; + $value = isset($kv[1]) ? $kv[1] : null; + $norm = $value; + if (empty($key)) { + continue; + } + if ($value === 'N/A') { + $norm = null; + } elseif (in_array($key, self::$dateTimeFields, true)) { + $norm = $this->tryParseDateTime($value); + } elseif (in_array($key, self::$dateFields, true)) { + $norm = $this->tryParseDate($value); + } elseif (preg_match('/^((?:[0-9]*[.])?[0-9]+)(?:\s+\w+)?$/i', $value, $matches)) { + $norm = floatval($matches[1]); + } + $status[$key] = array( + 'value' => $value, + 'norm' => $norm + ); + } + return $status; + } + + private function tryParseDateTime($dateTimeString) + { + $formats = [ + 'Y-m-d H:i:s P', // 2021-12-27 17:51:42 +0100 + 'D M d H:i:s T Y' // Sat Sep 16 17:13:00 CEST 2000 + ]; + foreach ($formats as $format) { + $dt = DateTime::createFromFormat($format, $dateTimeString); + if ($dt) { + return $dt->format(DateTimeInterface::RFC3339); + } + } + return $dateTimeString; + } + + private function tryParseDate($dateString) + { + $formats = [ + 'Y-m-d', // 2021-12-27 + 'm/d/y', // 12/27/21 + ]; + foreach ($formats as $format) { + $dt = DateTime::createFromFormat($format, $dateString); + if ($dt) { + return $dt->format('Y-m-d'); + } + } + return $dateString; + } + + private function getUpsStatusOutput() + { + $output = $error = null; + + if ($this->isEnabled()) { + $backend = new Backend(); + $output = trim($backend->configdRun('apcupsd upsstatus')); + if (empty($output)) { + $error = gettext('Error: empty output from apcaccess'); + } + } else { + $error = gettext('Error: apcupsd is disabled'); + } + + return array( + 'error' => $error, + 'output' => $output + ); + } + + private function isEnabled() + { + return $this->getModel()->general->Enabled == '1'; + } +} diff --git a/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/Api/SettingsController.php b/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/Api/SettingsController.php new file mode 100644 index 0000000000..a0132319e8 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/Api/SettingsController.php @@ -0,0 +1,41 @@ +view->pick('OPNsense/Apcupsd/index'); + $this->view->generalForm = $this->getForm("general"); + } +} diff --git a/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/StatusController.php b/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/StatusController.php new file mode 100644 index 0000000000..7930cca276 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/StatusController.php @@ -0,0 +1,47 @@ +view->pick('OPNsense/Apcupsd/status'); + } +} diff --git a/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/forms/general.xml b/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/forms/general.xml new file mode 100644 index 0000000000..7fd4ff0ec7 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/mvc/app/controllers/OPNsense/Apcupsd/forms/general.xml @@ -0,0 +1,185 @@ +
    + + apcupsd.general.Enabled + + checkbox + Enable the APC UPS service. + + + apcupsd.general.UPSName + + text + + Specify a name for the UPS for log files, status reports etc., + between 1 and 99 characters in length. + + Name of the UPS for log files, status reports etc. + + + apcupsd.general.UPSCable + + dropdown + Select the type of cable connecting the UPS to the server. + + + apcupsd.general.UPSType + + dropdown + Select the type of UPS in use. You may also need to specify the UPS device below. + + + apcupsd.general.Device + + text + + + usb: leave blank for auto-detection
    + apcsmart or dumb: /dev/tty** (serial connection)
    + net: host:port (remote apcupsd network information server)
    + snmp: host:port:vendor:community
    + pcnet: ipaddr:username:passphrase (SmartSlot card) + ]]> +
    + Path or address, leave blank for USB +
    + + apcupsd.general.Polltime + + text + + + A low setting will improve the daemon's responsiveness to certain events at the cost of higher CPU + utilisation.
    + The default of 60 is appropriate for most situations. + ]]> +
    +
    + + apcupsd.general.BatteryLevel + + text + + Apcupsd will shutdown the system during a power failure when the remaining battery charge falls below the + specified percentage. Set to -1 to disable. (Default is 5). + + + + apcupsd.general.Minutes + + text + + Apcupsd will shutdown the system during a power failure when the remaining runtime on batteries as + internally calculated by the UPS falls below the specified minutes. Set to -1 to disable. (Default is 3) + + + + apcupsd.general.Timeout + + text + + + For a Smart-UPS, this should normally be set to zero so that the shutdown time will be determined by the + battery level or remaining runtime.
    + It is also useful for testing apcupsd because you can force a rapid shutdown by setting a small value + (eg. 60) and turning off the power to the UPS. + ]]> +
    +
    + + apcupsd.general.OnBatteryDelay + + text + + The number of seconds from when a power failure is detected until apcupsd reacts with an onbattery event. + (Default is 6). + + true + + + apcupsd.general.Annoy + + text + Time in seconds between annoying users to signoff prior to system shutdown. 0 disables. + true + + + apcupsd.general.AnnoyDelay + + text + Initial delay after power failure before warning users to get off the system. + true + + + apcupsd.general.KillDelay + + text + + + After the specified time, apcupsd will attempt to put the UPS into hibernate mode and kill the power to + the computer.
    + This is for use on operating systems where apcupsd cannot regain control after a shutdown (eg. FreeBSD) + to issue an apcupsd --killpower command.
    + Setting the delay to 0 disables it. + ]]> +
    + true +
    + + apcupsd.general.Netserver + + checkbox + Enables the network information server which is required to obtain the current status information + from the local or remote UPS. + true + + + apcupsd.general.NetserverAddress + + text + 127.0.0.1 is the default + + + Default address is 127.0.0.1 which means only local machine connections are accepted. + ]]> + + true + + + apcupsd.general.NetserverPort + + text + Network port used to send status and event data over the network. + Port 3551 is the default as registered with the IANA. + true + + + apcupsd.general.UPSClass + + dropdown + Normally standalone unless you share an UPS using an APC ShareUPS card. + true + + + apcupsd.general.UPSMode + + dropdown + Normally disable unless you share an UPS using an APC ShareUPS card. + true + + + apcupsd.general.NoLogon + + dropdown + The condition which determines when users are prevented from logging in during a power failure. + true + +
    diff --git a/sysutils/apcupsd/src/opnsense/mvc/app/models/OPNsense/Apcupsd/ACL/ACL.xml b/sysutils/apcupsd/src/opnsense/mvc/app/models/OPNsense/Apcupsd/ACL/ACL.xml new file mode 100644 index 0000000000..2633a36fdd --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/mvc/app/models/OPNsense/Apcupsd/ACL/ACL.xml @@ -0,0 +1,10 @@ + + + Services: Apcupsd System Monitoring page + + ui/apcupsd/* + api/apcupsd/service/* + api/apcupsd/settings/* + + + diff --git a/sysutils/apcupsd/src/opnsense/mvc/app/models/OPNsense/Apcupsd/Apcupsd.php b/sysutils/apcupsd/src/opnsense/mvc/app/models/OPNsense/Apcupsd/Apcupsd.php new file mode 100644 index 0000000000..98e12f8a36 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/mvc/app/models/OPNsense/Apcupsd/Apcupsd.php @@ -0,0 +1,39 @@ + + //OPNsense/apcupsd + 0.0.2 + APC UPS configuration + + + + 0 + Y + + + N + /^([0-9a-zA-Z._\- ]){1,99}$/ + + The name should be 1 to 99 characters and contain only alphanumeric characters, + dashes, underscores, dot or space. + + + + smart + Y + + + + + + + + + + + + + + + + + + + + + + + + apcsmart + Y + + apcsmart + usb + net + snmp + netsnmp + dumb + pcnet + modbus + + + + N + + + Y + 60 + 1 + 86400 + Polltime must be between 1 and 86400. + + + 1 + Y + + + 127.0.0.1 + Y + + + 3551 + Y + + + 6 + Y + 0 + 60 + On battery delay must be between 1 and 60. + + + 5 + Y + -1 + 99 + Battery level must be between -1 and 99 percent. + + + 3 + Y + -1 + 60 + Remaining battery minutes must be between -1 and 60 minutes. + + + 0 + Y + 0 + 360 + Timeout must be between 0 and 360 seconds. + + + 300 + Y + 10 + 360 + Annoy time must be between 10 and 360 seconds. + + + 60 + Y + 10 + 360 + Annoy delay time must be between 10 and 360 seconds. + + + 0 + Y + 0 + 360 + Kill delay time must be between 0 and 360 seconds. + + + standalone + Y + + standalone + shareslave + sharemaster + + + + disable + Y + + disable + share + + + + disable + Y + + disable + timeout + percent + minutes + always + + + + + diff --git a/sysutils/apcupsd/src/opnsense/mvc/app/models/OPNsense/Apcupsd/Menu/Menu.xml b/sysutils/apcupsd/src/opnsense/mvc/app/models/OPNsense/Apcupsd/Menu/Menu.xml new file mode 100644 index 0000000000..a0b3895b0a --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/mvc/app/models/OPNsense/Apcupsd/Menu/Menu.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/sysutils/apcupsd/src/opnsense/mvc/app/views/OPNsense/Apcupsd/index.volt b/sysutils/apcupsd/src/opnsense/mvc/app/views/OPNsense/Apcupsd/index.volt new file mode 100644 index 0000000000..ddda5a92ae --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/mvc/app/views/OPNsense/Apcupsd/index.volt @@ -0,0 +1,74 @@ +{# + # Copyright (C) 2021 Dan Lundqvist + # Copyright (C) 2021 David Berry + # Copyright (C) 2021 Nicola Pellegrini + # + # 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':generalForm,'id':'frm_GeneralSettings','apply_btn_id':'saveAct'])}} +
    + diff --git a/sysutils/apcupsd/src/opnsense/mvc/app/views/OPNsense/Apcupsd/status.volt b/sysutils/apcupsd/src/opnsense/mvc/app/views/OPNsense/Apcupsd/status.volt new file mode 100644 index 0000000000..0e3cf28da4 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/mvc/app/views/OPNsense/Apcupsd/status.volt @@ -0,0 +1,42 @@ +{# + # Copyright (C) 2021 Dan Lundqvist + # Copyright (C) 2021 David Berry + # Copyright (C) 2021 Nicola Pellegrini + # + # 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. + #} +
    No status
    + diff --git a/sysutils/apcupsd/src/opnsense/service/conf/actions.d/actions_apcupsd.conf b/sysutils/apcupsd/src/opnsense/service/conf/actions.d/actions_apcupsd.conf new file mode 100644 index 0000000000..03896d5e33 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/service/conf/actions.d/actions_apcupsd.conf @@ -0,0 +1,29 @@ +[start] +command:/usr/local/etc/rc.d/apcupsd start +parameters: +type:script +message:starting apcupsd + +[stop] +command:/usr/local/etc/rc.d/apcupsd stop +parameters: +type:script +message:stopping apcupsd + +[restart] +command:/usr/local/etc/rc.d/apcupsd restart +parameters: +type:script +message:restarting apcupsd + +[status] +command:/usr/local/etc/rc.d/apcupsd status;exit 0 +parameters: +type:script_output +message:requesting apcupsd status + +[upsstatus] +command:/usr/local/sbin/apcaccess +parameters: +type:script_output +message:requesting UPS Status diff --git a/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/+TARGETS b/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/+TARGETS new file mode 100644 index 0000000000..fc80c3aa8b --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/+TARGETS @@ -0,0 +1,3 @@ +apcupsd:/etc/rc.conf.d/apcupsd +apcupsd.conf:/usr/local/etc/apcupsd/apcupsd.conf +syslog-filter.conf:/usr/local/opnsense/service/templates/OPNsense/Syslog/local/apcupsd.conf diff --git a/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/apcupsd b/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/apcupsd new file mode 100644 index 0000000000..cc639badf7 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/apcupsd @@ -0,0 +1,23 @@ +{% if helpers.exists('OPNsense.apcupsd.general.Enabled') and OPNsense.apcupsd.general.Enabled == "1" %} +apcupsd_enable="YES" +{% else %} +apcupsd_enable="NO" +{% endif %} + +# Hook to post start/restart commands waiting for the pid file for max 3 seconds +# this prevents the status command to erroneously report the service as stopped +# since apcupsd detaches before the pid file creation + +start_postcmd="${name}_post_start_restart" +restart_postcmd="${name}_post_start_restart" + +apcupsd_post_start_restart() +{ + if [ -n "$pidfile" ]; then + i=0 + while [ ! -f "$pidfile" ] && [ "$i" -le 5 ]; do + i=$(( i + 1 )) + sleep 0.5 + done + fi +} diff --git a/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/apcupsd.conf b/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/apcupsd.conf new file mode 100644 index 0000000000..730587a3a4 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/apcupsd.conf @@ -0,0 +1,356 @@ +## apcupsd.conf v1.1 ## +# +# for apcupsd release 3.14.14 (31 May 2016) - freebsd + +############################################################## +# +# This file is auto-generated - any changes made will be lost +# +############################################################# + +# "apcupsd" POSIX config file + +# +# Note that the apcupsd daemon must be restarted in order for changes to +# this configuration file to become active. +# + +# +# ========= General configuration parameters ============ +# + +# UPSNAME xxx +# Use this to give your UPS a name in log files and such. This +# is particulary useful if you have multiple UPSes. This does not +# set the EEPROM. It should be 8 characters or less. +{% if not helpers.empty('OPNsense.apcupsd.general.UPSName') %} +UPSNAME {{ OPNsense.apcupsd.general.UPSName }} +{% endif %} + +# UPSCABLE +# Defines the type of cable connecting the UPS to your computer. +# +# Possible generic choices for are: +# simple, smart, ether, usb +# +# Or a specific cable model number may be used: +# 940-0119A, 940-0127A, 940-0128A, 940-0020B, +# 940-0020C, 940-0023A, 940-0024B, 940-0024C, +# 940-1524C, 940-0024G, 940-0095A, 940-0095B, +# 940-0095C, 940-0625A, M-04-02-2000 +# +UPSCABLE {{ OPNsense.apcupsd.general.UPSCable }} + +# To get apcupsd to work, in addition to defining the cable +# above, you must also define a UPSTYPE, which corresponds to +# the type of UPS you have (see the Description for more details). +# You must also specify a DEVICE, sometimes referred to as a port. +# For USB UPSes, please leave the DEVICE directive blank. For +# other UPS types, you must specify an appropriate port or address. +# +# UPSTYPE DEVICE Description +# apcsmart /dev/tty** Newer serial character device, appropriate for +# SmartUPS models using a serial cable (not USB). +# +# usb Most new UPSes are USB. A blank DEVICE +# setting enables autodetection, which is +# the best choice for most installations. +# +# net hostname:port Network link to a master apcupsd through apcupsd's +# Network Information Server. This is used if the +# UPS powering your computer is connected to a +# different computer for monitoring. +# +# snmp hostname:port:vendor:community +# SNMP network link to an SNMP-enabled UPS device. +# Hostname is the ip address or hostname of the UPS +# on the network. Vendor can be can be "APC" or +# "APC_NOTRAP". "APC_NOTRAP" will disable SNMP trap +# catching; you usually want "APC". Port is usually +# 161. Community is usually "private". +# +# netsnmp hostname:port:vendor:community +# OBSOLETE +# Same as SNMP above but requires use of the +# net-snmp library. Unless you have a specific need +# for this old driver, you should use 'snmp' instead. +# +# dumb /dev/tty** Old serial character device for use with +# simple-signaling UPSes. +# +# pcnet ipaddr:username:passphrase:port +# PowerChute Network Shutdown protocol which can be +# used as an alternative to SNMP with the AP9617 +# family of smart slot cards. ipaddr is the IP +# address of the UPS management card. username and +# passphrase are the credentials for which the card +# has been configured. port is the port number on +# which to listen for messages from the UPS, normally +# 3052. If this parameter is empty or missing, the +# default of 3052 will be used. +# +# modbus /dev/tty** Serial device for use with newest SmartUPS models +# supporting the MODBUS protocol. +# modbus Leave the DEVICE setting blank for MODBUS over USB +# or set to the serial number of the UPS to ensure +# that apcupsd binds to that particular unit +# (helpful if you have more than one USB UPS). +# +UPSTYPE {{ OPNsense.apcupsd.general.UPSType|default("apcsmart") }} +DEVICE {{ OPNsense.apcupsd.general.Device }} + +# POLLTIME +# Interval (in seconds) at which apcupsd polls the UPS for status. This +# setting applies both to directly-attached UPSes (UPSTYPE apcsmart, usb, +# dumb) and networked UPSes (UPSTYPE net, snmp). Lowering this setting +# will improve apcupsd's responsiveness to certain events at the cost of +# higher CPU utilization. The default of 60 is appropriate for most +# situations. +POLLTIME {{ OPNsense.apcupsd.general.Polltime|default("60") }} + +# LOCKFILE +# Path for device lock file. This is the directory into which the lock file +# will be written. The directory must already exist; apcupsd will not create +# it. The actual name of the lock file is computed from DEVICE. +# Not used on Win32. +LOCKFILE /var/spool/lock + +# SCRIPTDIR +# Directory in which apccontrol and event scripts are located. +SCRIPTDIR /usr/local/etc/apcupsd + +# PWRFAILDIR +# Directory in which to write the powerfail flag file. This file +# is created when apcupsd initiates a system shutdown and is +# checked in the OS halt scripts to determine if a killpower +# (turning off UPS output power) is required. +PWRFAILDIR /var/run + +# NOLOGINDIR +# Directory in which to write the nologin file. The existence +# of this flag file tells the OS to disallow new logins. +NOLOGINDIR /var/run + + +# +# ======== Configuration parameters used during power failures ========== +# + +# The ONBATTERYDELAY is the time in seconds from when a power failure +# is detected until we react to it with an onbattery event. +# +# This means that, apccontrol will be called with the powerout argument +# immediately when a power failure is detected. However, the +# onbattery argument is passed to apccontrol only after the +# ONBATTERYDELAY time. If you don't want to be annoyed by short +# powerfailures, make sure that apccontrol powerout does nothing +# i.e. comment out the wall. +ONBATTERYDELAY {{ OPNsense.apcupsd.general.OnBatteryDelay|default("6") }} + +# +# Note: BATTERYLEVEL, MINUTES, and TIMEOUT work in conjunction, so +# the first that occurs will cause the initation of a shutdown. +# + +# If during a power failure, the remaining battery percentage +# (as reported by the UPS) is below or equal to BATTERYLEVEL, +# apcupsd will initiate a system shutdown. +BATTERYLEVEL {{ OPNsense.apcupsd.general.BatteryLevel|default("5") }} + +# If during a power failure, the remaining runtime in minutes +# (as calculated internally by the UPS) is below or equal to MINUTES, +# apcupsd, will initiate a system shutdown. +MINUTES {{ OPNsense.apcupsd.general.Minutes|default("3") }} + +# If during a power failure, the UPS has run on batteries for TIMEOUT +# many seconds or longer, apcupsd will initiate a system shutdown. +# A value of 0 disables this timer. +# +# Note, if you have a Smart UPS, you will most likely want to disable +# this timer by setting it to zero. That way, you UPS will continue +# on batteries until either the % charge remaing drops to or below BATTERYLEVEL, +# or the remaining battery runtime drops to or below MINUTES. Of course, +# if you are testing, setting this to 60 causes a quick system shutdown +# if you pull the power plug. +# If you have an older dumb UPS, you will want to set this to less than +# the time you know you can run on batteries. +TIMEOUT {{ OPNsense.apcupsd.general.Timeout|default("0") }} + +# Time in seconds between annoying users to signoff prior to +# system shutdown. 0 disables. +ANNOY {{ OPNsense.apcupsd.general.Annoy|default("300") }} + +# Initial delay after power failure before warning users to get +# off the system. +ANNOYDELAY {{ OPNsense.apcupsd.general.AnnoyDelay|default("60") }} + +# The condition which determines when users are prevented from +# logging in during a power failure. +# NOLOGON [ disable | timeout | percent | minutes | always ] +NOLOGON {{ OPNsense.apcupsd.general.NoLogon|default("disable") }} + +# If KILLDELAY is non-zero, apcupsd will continue running after a +# shutdown has been requested, and after the specified time in +# seconds attempt to kill the power. This is for use on systems +# where apcupsd cannot regain control after a shutdown. +# KILLDELAY 0 disables +KILLDELAY {{ OPNsense.apcupsd.general.KillDelay|default("0") }} + +# +# ==== Configuration statements for Network Information Server ==== +# + +# NETSERVER [ on | off ] on enables, off disables the network +# information server. If netstatus is on, a network information +# server process will be started for serving the STATUS and +# EVENT data over the network (used by CGI programs). +{% if helpers.exists('OPNsense.apcupsd.general.Netserver') and OPNsense.apcupsd.general.Netserver == "1" %} +NETSERVER on +{% else %} +NETSERVER off +{% endif %} + +# NISIP +# IP address on which NIS server will listen for incoming connections. +# This is useful if your server is multi-homed (has more than one +# network interface and IP address). Default value is 0.0.0.0 which +# means any incoming request will be serviced. Alternatively, you can +# configure this setting to any specific IP address of your server and +# NIS will listen for connections only on that interface. Use the +# loopback address (127.0.0.1) to accept connections only from the +# local machine. +NISIP {{ OPNsense.apcupsd.general.NetserverAddress|default("127.0.0.1") }} + +# NISPORT default is 3551 as registered with the IANA +# port to use for sending STATUS and EVENTS data over the network. +# It is not used unless NETSERVER is on. If you change this port, +# you will need to change the corresponding value in the cgi directory +# and rebuild the cgi programs. +NISPORT {{ OPNsense.apcupsd.general.NetserverPort|default("3551") }} + +# If you want the last few EVENTS to be available over the network +# by the network information server, you must define an EVENTSFILE. +EVENTSFILE /var/log/apcupsd.events + +# EVENTSFILEMAX +# By default, the size of the EVENTSFILE will be not be allowed to exceed +# 10 kilobytes. When the file grows beyond this limit, older EVENTS will +# be removed from the beginning of the file (first in first out). The +# parameter EVENTSFILEMAX can be set to a different kilobyte value, or set +# to zero to allow the EVENTSFILE to grow without limit. +EVENTSFILEMAX 10 + +# +# ========== Configuration statements used if sharing ============= +# a UPS with more than one machine + +# +# Remaining items are for ShareUPS (APC expansion card) ONLY +# + +# UPSCLASS [ standalone | shareslave | sharemaster ] +# Normally standalone unless you share an UPS using an APC ShareUPS +# card. +UPSCLASS {{ OPNsense.apcupsd.general.UPSClass|default("standalone") }} + +# UPSMODE [ disable | share ] +# Normally disable unless you share an UPS using an APC ShareUPS card. +UPSMODE {{ OPNsense.apcupsd.general.UPSMode|default("disable") }} + +# +# ===== Configuration statements to control apcupsd system logging ======== +# + +# Time interval in seconds between writing the STATUS file; 0 disables +STATTIME 0 + +# Location of STATUS file (written to only if STATTIME is non-zero) +STATFILE /var/log/apcupsd.status + +# LOGSTATS [ on | off ] on enables, off disables +# Note! This generates a lot of output, so if +# you turn this on, be sure that the +# file defined in syslog.conf for LOG_NOTICE is a named pipe. +# You probably do not want this on. +LOGSTATS off + +# Time interval in seconds between writing the DATA records to +# the log file. 0 disables. +DATATIME 0 + +# FACILITY defines the logging facility (class) for logging to syslog. +# If not specified, it defaults to "daemon". This is useful +# if you want to separate the data logged by apcupsd from other +# programs. +#FACILITY DAEMON + +# +# ========== Configuration statements used in updating the UPS EPROM ========= +# + +# +# These statements are used only by apctest when choosing "Set EEPROM with conf +# file values" from the EEPROM menu. THESE STATEMENTS HAVE NO EFFECT ON APCUPSD. +# + +# UPS name, max 8 characters +#UPSNAME UPS_IDEN + +# Battery date - 8 characters +#BATTDATE mm/dd/yy + +# Sensitivity to line voltage quality (H cause faster transfer to batteries) +# SENSITIVITY H M L (default = H) +#SENSITIVITY H + +# UPS delay after power return (seconds) +# WAKEUP 000 060 180 300 (default = 0) +#WAKEUP 60 + +# UPS Grace period after request to power off (seconds) +# SLEEP 020 180 300 600 (default = 20) +#SLEEP 180 + +# Low line voltage causing transfer to batteries +# The permitted values depend on your model as defined by last letter +# of FIRMWARE or APCMODEL. Some representative values are: +# D 106 103 100 097 +# M 177 172 168 182 +# A 092 090 088 086 +# I 208 204 200 196 (default = 0 => not valid) +#LOTRANSFER 208 + +# High line voltage causing transfer to batteries +# The permitted values depend on your model as defined by last letter +# of FIRMWARE or APCMODEL. Some representative values are: +# D 127 130 133 136 +# M 229 234 239 224 +# A 108 110 112 114 +# I 253 257 261 265 (default = 0 => not valid) +#HITRANSFER 253 + +# Battery charge needed to restore power +# RETURNCHARGE 00 15 50 90 (default = 15) +#RETURNCHARGE 15 + +# Alarm delay +# 0 = zero delay after pwr fail, T = power fail + 30 sec, L = low battery, N = never +# BEEPSTATE 0 T L N (default = 0) +#BEEPSTATE T + +# Low battery warning delay in minutes +# LOWBATT 02 05 07 10 (default = 02) +#LOWBATT 2 + +# UPS Output voltage when running on batteries +# The permitted values depend on your model as defined by last letter +# of FIRMWARE or APCMODEL. Some representative values are: +# D 115 +# M 208 +# A 100 +# I 230 240 220 225 (default = 0 => not valid) +#OUTPUTVOLTS 230 + +# Self test interval in hours 336=2 weeks, 168=1 week, ON=at power on +# SELFTEST 336 168 ON OFF (default = 336) +#SELFTEST 336 diff --git a/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/syslog-filter.conf b/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/syslog-filter.conf new file mode 100644 index 0000000000..16b8a217a3 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/service/templates/OPNsense/Apcupsd/syslog-filter.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [apcupsd]. +################################################################### +filter f_local_apcupsd { + program("apcupsd"); +}; diff --git a/sysutils/apcupsd/src/opnsense/www/js/widgets/Apcupsd.js b/sysutils/apcupsd/src/opnsense/www/js/widgets/Apcupsd.js new file mode 100644 index 0000000000..54f36bd134 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/www/js/widgets/Apcupsd.js @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2024 Nicola Pellegrini + * 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. + */ + +/** + * @typedef ApcUpsdStatusValue + * + * @property {string|number} norm - normalized value + * @property {string|number} value - raw value + */ + +/** + * @typedef {Object.} ApcUpsdStatus + */ + +export default class ApcUpsd extends BaseTableWidget { + constructor() { + super(); + } + + getMarkup() { + let $container = $('
    '); + let $apcupsdTable = this.createTable('apcupsd-table', { + headerPosition: 'left' + }); + $container.append($apcupsdTable); + return $container; + } + + async onWidgetTick() { + const data = await this.ajaxCall('/api/apcupsd/service/get_ups_status'); + + if (data.error) { + this.displayError(data.error); + return; + } + + let rows = this.makeRows(data.status); + super.updateTable('apcupsd-table', rows); + } + + /** + * Displays an error text row + * + * @param {string} text + */ + displayError(text) { + // A row with `flextable-row` class will be auto removed on next tick by + // `this.updateTable(...)`, if the error is gone + const $row = $('
    ').text(text); + // Replace all the content in the table + $(`#apcupsd-table`).html($row); + } + + /** + * @param {ApcUpsdStatus} status + * @returns {Array>} + */ + makeRows(status) { + const rows = []; + + if (status.MODEL) { + rows.push(this.makeTextRow("status_model", status.MODEL)); + } + + if (status.STATUS) { + rows.push( + // Note: text value is not translated because it comes directly from apcupsd output + this.makeColoredTextRow( + "status_status", + status.STATUS.value, + /ONLINE/, // success + /ONBATT|LOWBATT|REPLACEBATT|NOBATT|SLAVEDOWN|COMMLOST/ // error + ) + ); + } + + if (status.SELFTEST) { + const raw_value = status.SELFTEST.value; + + // self test values + const value_keys = { + OK: 'status_selftest_ok', + BT: 'status_selftest_bt', + NG: 'status_selftest_ng', + NO: 'status_selftest_no', + }; + + // Try to translate it, otherwise use the raw value + const text_value = value_keys[raw_value] ? this.translate(value_keys[raw_value]) : raw_value; + + if (raw_value === 'NO') { + // Special case for no results, no color + rows.push(this.makeTextRow('status_selftest', text_value)); + } else { + rows.push(this.makeColoredTextRow( + 'status_selftest', + text_value, + /OK/, // success + /BT|NG/, // error, all other unknown states are "warnings" + raw_value + )); + } + } + + if (status.LINEV) { + rows.push(this.makeTextRow('status_linev', status.LINEV)); + } + + // NOMPOWER not checked because it's optional and it might not be available + if (status.LOADPCT) { + rows.push(this.makeUpsLoadRow('status_load', status.LOADPCT, status.NOMPOWER)) + } + + if (status.BCHARGE) { + rows.push(this.makeProgressBarRow('status_bcharge', status.BCHARGE.norm)); + } + + if (status.TIMELEFT) { + rows.push(this.makeTextRow('status_timeleft', status.TIMELEFT)); + } + + if (status.BATTV) { + rows.push(this.makeTextRow('status_battv', status.BATTV)); + } + + if (status.BATTDATE) { + let date = status.BATTDATE; + date = date.norm || date.value; + rows.push(this.makeDateRow('status_battdate', date, 'll', 'YYYY-MM-DD')); + } + + if (status.ITEMP) { + rows.push(this.makeTextRow("status_itemp", status.ITEMP)); + } + + if (status.DATE) { + let date = status.DATE; + date = date.norm || date.value; + rows.push(this.makeDateRow('status_date', date, 'll LTS')); + } + + return rows; + } + + /** + * Makes a date row, parsing the given date with moment js (if available and the date is valid) + * and shows it with the new given format + * + * @param {string} labelKey - label translation key + * @param {string} dateString - date string + * @param {string} newFormat - new format to use for the date string + * @param {string} [parseFormat] - optional format used to parse the date string with moment js + * + * @returns {Array} + */ + makeDateRow(labelKey, dateString, newFormat, parseFormat) { + const withMomentJs = moment && moment.version && typeof moment === 'function'; + let text = dateString; + if (withMomentJs) { + const m = moment(dateString, parseFormat); + if (m.isValid()) { + m.locale(window.navigator.language); + // override text + text = m.format(newFormat); + } + } + + return this.makeTextRow(labelKey, text); + } + + /** + * Makes a row that shows the load value % and estimated watts usage if NOMPOWER is also given + * + * @param {string} labelKey - label translation key + * @param {ApcUpsdStatusValue} loadpct - LOADPCT status value + * @param {ApcUpsdStatusValue} [nompower] - NOMPOWER status value + * + * @returns {Array} + */ + makeUpsLoadRow(labelKey, loadpct, nompower) { + let text = loadpct.norm.toFixed(1) + ' %'; + if (nompower) { + const watts = Math.round(loadpct.norm * nompower.norm / 100); + text += ` ( ~ ${watts} W )`; + } + return this.makeProgressBarRow(labelKey, loadpct.norm, text); + } + + /** + * Makes a progress bar row + * + * @param {string} labelKey - label translation key + * @param {number} progress - current progress (0-100) + * @param {string} [progressText] - text shown on top of the progress bar, defaults to " %" + * + * @returns {Array} + */ + makeProgressBarRow(labelKey, progress, progressText) { + progressText = progressText || `${progress.toFixed(1)} %`; + const pb = this.makeProgressBar(progress, progressText); + return this.makeRow(labelKey, pb); + } + + /** + * Makes a row with a progress bar value cell + * + * @param {number} progress - current progress (0-100) + * @param {string} text - text shown on top of the progress bar + * + * @returns {Array} + */ + makeProgressBar(progress, text) { + const $textEl = $('').text(text).css({ + position: 'absolute', + left: 0, + right: 0 + }); + + const $barEl = $('
    ').css({ + width: `${progress}%`, + zIndex: 0 + }); + + return $('
    ').append($barEl, $textEl).prop("outerHTML"); + } + + /** + * Makes a text row, coloring the value cell text depending on the regex matches + * + * - No match: warning color + * - Ok regex match: success color + * - Err regex match: danger color + * + * @param {string} labelKey - label translation key + * @param {string} value - a string value that is matched with the regexes for highlighting. + * If the value needs to be translated use `check_value` for matching + * @param {RegExp} okRegexp - regex to apply success state CSS class + * @param {RegExp} errRegexp - regex to apply danger/error state CSS class + * @param {string} [check_value] - value to match against, if not set it defaults to the `value` argument + * + * @returns {Array} + */ + makeColoredTextRow(labelKey, value, okRegexp, errRegexp, check_value) { + check_value = check_value ?? value; + + const textEl = $('').text(value); + if (okRegexp && okRegexp.exec(check_value)) { + textEl.addClass('text-success'); + } else if (errRegexp && errRegexp.exec(check_value)) { + textEl.addClass('text-danger'); + } else { + textEl.addClass('text-warning'); + } + + let html = textEl.prop('outerHTML'); + + return this.makeRow(labelKey, html); + } + + /** + * Makes a standard text row template + * + * @param {string} labelKey - label translation key + * @param {string|ApcUpsdStatusValue} content - string value or an object with a value field + * + * @returns {Array} + */ + makeTextRow(labelKey, content) { + content = typeof content === 'string' ? content : content.value; + + return this.makeRow(labelKey, content); + } + + /** + * Make a row object to use with {@link updateTable} + * + * Auto translates the given label key if possible + * + * @param {string} labelKey - label translation key + * @param {*} content - html content + * + * @returns {Array} + */ + makeRow(labelKey, content) { + return [this.translate(labelKey), content]; + } + + /** + * Tries to translates the given key. + * + * If not found it logs an error and just returns the key as is. + * + * @param {string} key - key to translate + * + * @returns {string} + */ + translate(key) { + let value = this.translations[key]; + if (value === undefined) { + console.error('Missing translation for ' + key); + // Use the key as fallback + value = key; + } + return value; + } +} diff --git a/sysutils/apcupsd/src/opnsense/www/js/widgets/Metadata/Apcupsd.xml b/sysutils/apcupsd/src/opnsense/www/js/widgets/Metadata/Apcupsd.xml new file mode 100644 index 0000000000..17d802a013 --- /dev/null +++ b/sysutils/apcupsd/src/opnsense/www/js/widgets/Metadata/Apcupsd.xml @@ -0,0 +1,26 @@ + + + Apcupsd.js + + /api/apcupsd/service/get_ups_status + + + Apcupsd + Model + Status + Self test + OK + Failed due to battery capacity (BT) + Failed due to overload (NG) + No results + Line voltage + Load + Battery level + Battery runtime + Battery voltage + Battery date + Temperature + Status update + + + diff --git a/sysutils/api-backup/Makefile b/sysutils/api-backup/Makefile deleted file mode 100644 index d1a243984a..0000000000 --- a/sysutils/api-backup/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -PLUGIN_NAME= api-backup -PLUGIN_VERSION= 1.0 -PLUGIN_REVISION= 1 -PLUGIN_COMMENT= Provide the functionality to download the config.xml -PLUGIN_MAINTAINER= franz.fabian.94@gmail.com - -.include "../../Mk/plugins.mk" diff --git a/sysutils/api-backup/pkg-descr b/sysutils/api-backup/pkg-descr deleted file mode 100644 index 45972dda81..0000000000 --- a/sysutils/api-backup/pkg-descr +++ /dev/null @@ -1 +0,0 @@ -Provide the functionality to download the config.xml diff --git a/sysutils/api-backup/src/opnsense/mvc/app/controllers/OPNsense/Backup/Api/BackupController.php b/sysutils/api-backup/src/opnsense/mvc/app/controllers/OPNsense/Backup/Api/BackupController.php deleted file mode 100644 index 192be73400..0000000000 --- a/sysutils/api-backup/src/opnsense/mvc/app/controllers/OPNsense/Backup/Api/BackupController.php +++ /dev/null @@ -1,50 +0,0 @@ -response->setStatusCode(200, "OK"); - $this->response->setContentType('application/xml', 'UTF-8'); - $this->response->setHeader("Content-Disposition", "attachment; filename=\"config.xml\""); - $data = file_get_contents(self::CONFIG_XML); - $this->response->setContent($data); - } - - public function afterExecuteRoute($dispatcher) - { - $this->response->send(); - } -} diff --git a/sysutils/api-backup/src/opnsense/mvc/app/models/OPNsense/Backup/ACL/ACL.xml b/sysutils/api-backup/src/opnsense/mvc/app/models/OPNsense/Backup/ACL/ACL.xml deleted file mode 100644 index 2a1b837657..0000000000 --- a/sysutils/api-backup/src/opnsense/mvc/app/models/OPNsense/Backup/ACL/ACL.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Backup API - - api/backup/* - - - diff --git a/sysutils/apuled/+POST_INSTALL b/sysutils/apuled/+POST_INSTALL deleted file mode 100644 index a2e28a44dc..0000000000 --- a/sysutils/apuled/+POST_INSTALL +++ /dev/null @@ -1,18 +0,0 @@ -LED1="/dev/led/led1" -LED2="/dev/led/led2" -LED3="/dev/led/led3" -LEDKO="/boot/kernel/apuled.ko" - -if [ ! -e /boot/kernel/apuled.ko ]; then - exit 0 -fi - -kldload $LEDKO 2>&1 - -echo "m-" > $LED1 -echo "m -" > $LED2 -echo "m -" > $LED3 -sleep 2 -echo 1 > $LED1 -echo 1 > $LED2 -echo 1 > $LED3 diff --git a/sysutils/apuled/LICENSE b/sysutils/apuled/LICENSE deleted file mode 100644 index 2877b69952..0000000000 --- a/sysutils/apuled/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -BSD 2-Clause License - -Copyright (c) 2019 Cloudfence - Julio Camargo (JCC) -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* 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 BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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. diff --git a/sysutils/apuled/Makefile b/sysutils/apuled/Makefile deleted file mode 100644 index 7dc896fa79..0000000000 --- a/sysutils/apuled/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -PLUGIN_NAME= apuled -PLUGIN_VERSION= 0.2 -PLUGIN_REVISION= 1 -PLUGIN_DEVEL= yes -PLUGIN_COMMENT= PC Engine APU LED control -PLUGIN_MAINTAINER= julio@cloudfence.com.br - -.include "../../Mk/plugins.mk" diff --git a/sysutils/apuled/README.md b/sysutils/apuled/README.md deleted file mode 100644 index ab9d823847..0000000000 --- a/sysutils/apuled/README.md +++ /dev/null @@ -1,18 +0,0 @@ -### APU LED Plugin ### - -#### LED control for PC Engines APU platform OPNsense plugin #### - -**Left -> Right** -LED1|LED2|LED3 - -At system boot, the 3 LEDs will blink (leds test). - -| | LED1 | LED2 | LED3 | -|---------------------------------------|----------|---------------------------|------| -| System booting... | Blinking | OFF | OFF | -| System OK | ON | ON | ON | -| CPU Load > 90% | Blinking | - | - | -| Disk freespace < 10% | - | Blinking (SOS morse code) | - | -| WAN offline (without Internet access) | - | Blinking fast | - | -| WAN packetloss > 30% | - | Blinking slow | - | -| WAN online (Internet access) | - | ON | - | diff --git a/sysutils/apuled/pkg-descr b/sysutils/apuled/pkg-descr deleted file mode 100644 index 78d0bae2e2..0000000000 --- a/sysutils/apuled/pkg-descr +++ /dev/null @@ -1,2 +0,0 @@ -LED control for PC Engines APU platform OPNsense plugin -Cloudfence 2019 - JCC diff --git a/sysutils/apuled/src/etc/rc.syshook.d/early/30-apuled b/sysutils/apuled/src/etc/rc.syshook.d/early/30-apuled deleted file mode 100755 index 182dfdc4e3..0000000000 --- a/sysutils/apuled/src/etc/rc.syshook.d/early/30-apuled +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh -set -x -# APU LED plugin - Cloudfence 2019 JCC - - -# LEDs -LED1="/dev/led/led1" -LED2="/dev/led/led2" -LED3="/dev/led/led3" -LEDKO="/boot/kernel/apuled.ko" - - -messages() -{ -MSG=$1 -echo "$MSG" -logger -t "APU Plugin" "$MSG" -} - -# check if the module file exists -if [ -e /boot/kernel/apuled.ko ];then -# Load module - kldload $LEDKO 2>&1 -else - messages "Error: APU LED module is missing" - exit 1 -fi - -# starting boot LED message -# make the initial test -echo "m-" > $LED1 -echo "m -" > $LED2 -echo "m -" > $LED3 -sleep 2 -echo 1 > $LED1 -echo 1 > $LED2 -echo 1 > $LED3 -sleep 2 -# PWR bliking to show booting state -echo "f6" > $LED1 -echo 0 > $LED2 -echo 0 > $LED3 diff --git a/sysutils/apuled/src/etc/rc.syshook.d/start/60-apuled b/sysutils/apuled/src/etc/rc.syshook.d/start/60-apuled deleted file mode 100755 index ceac7245b9..0000000000 --- a/sysutils/apuled/src/etc/rc.syshook.d/start/60-apuled +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -# LEDs -LED1="/dev/led/led1" -LED2="/dev/led/led2" -LED3="/dev/led/led3" - -#stop blinking PWR LED -echo 1 > $LED1 -echo 1 > $LED2 -echo 1 > $LED3 diff --git a/sysutils/apuled/src/opnsense/scripts/apuled/apuledctl b/sysutils/apuled/src/opnsense/scripts/apuled/apuledctl deleted file mode 100755 index 9ca443e866..0000000000 --- a/sysutils/apuled/src/opnsense/scripts/apuled/apuledctl +++ /dev/null @@ -1,91 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2019 Cloudfence - Julio Camargo (JCC) -# 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. - -# LEDs -LED1="/dev/led/led1" -LED2="/dev/led/led2" -LED3="/dev/led/led3" - -# check if LEDs exists -if [ ! -e "$LED1" ];then - logger -t "APULED Plugin" "Error: LED device not exists" -fi - -check_wan(){ -# LED2 - network notifications -# Fetch from dpinger the list of gateways and the status of each one -GWLIST=$(ls /var/run/dpinger* | grep sock) - -for GW in $GWLIST; do - GWSTATUS=$(nc -U $GW | awk '{print $4}') - # We're ONLINE - if [ "$GWSTATUS" -eq 0 ];then - echo "1" > $LED2 - # Some losses - LATENCY - elif [ "$GWSTATUS" -gt 30 ] && [ "$GWSTATUS" -lt 100 ];then - LATENCY=1 - # One of our Gateways is DOWN - ALARM - # Test if the GWs are OK. If just one of the GWs failed the test, the LED will show DOWN status - elif [ "$GWSTATUS" -eq 100 ];then - echo "f1" > $LED2 - exit 0 - fi -done - -# In case of packets loss, I'll warn! -if [ ! -z "$LATENCY" ];then - echo "f3" > $LED2 -fi -} - -check_load(){ -# check CPU idle -# LED1 - system notifications -CHK_IDLE=$(vmstat 1 2 | tail -1 | awk '{ print $19 }') - -if [ "$CHK_IDLE" -lt "10" ];then - echo "f4" > $LED1 -else - echo "1" > $LED1 -fi -} - -check_disk(){ -# check Disk space -# LED3 - system notifications -DSK_FREE=$(df -kh / | tail -1 | awk '{ print 100-$5 }') - -if [ "$DSK_FREE" -lt "10" ];then - echo "m...---..." > $LED3 -else - echo "1" > $LED3 -fi -} - -# Main routine -check_load -check_disk -check_wan diff --git a/sysutils/apuled/src/opnsense/service/conf/actions.d/actions_apuled.conf b/sysutils/apuled/src/opnsense/service/conf/actions.d/actions_apuled.conf deleted file mode 100644 index a876901a84..0000000000 --- a/sysutils/apuled/src/opnsense/service/conf/actions.d/actions_apuled.conf +++ /dev/null @@ -1,6 +0,0 @@ -[run] -command:/usr/local/opnsense/scripts/apuled/apuledctl -parameters: -type:script -message:APU LEDs control -description:APULED LED status diff --git a/sysutils/beats/Makefile b/sysutils/beats/Makefile new file mode 100644 index 0000000000..1c44aecf4d --- /dev/null +++ b/sysutils/beats/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= beats +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= Send logs, network, metrics and heartbeat to Elasticsearch +PLUGIN_DEPENDS= beats8 +PLUGIN_MAINTAINER= 0xThiebaut + +.include "../../Mk/plugins.mk" diff --git a/sysutils/beats/pkg-descr b/sysutils/beats/pkg-descr new file mode 100644 index 0000000000..f84f52a3b3 --- /dev/null +++ b/sysutils/beats/pkg-descr @@ -0,0 +1,10 @@ +Beats is the platform for building lightweight, open source data +shippers for many types of operational data you want to enrich with +Logstash, search and analyze in Elasticsearch. + +Filebeat is a lightweight, open source shipper for log file data. As the +next-generation Logstash Forwarder, Filebeat tails logs and quickly +sends this information to Logstash for further parsing and enrichment or +to Elasticsearch for centralized storage and analysis. + +WWW: https://www.elastic.co/guide/en/beats diff --git a/sysutils/beats/src/opnsense/mvc/app/controllers/OPNsense/Beats/Api/ServiceController.php b/sysutils/beats/src/opnsense/mvc/app/controllers/OPNsense/Beats/Api/ServiceController.php new file mode 100755 index 0000000000..f0526d7be8 --- /dev/null +++ b/sysutils/beats/src/opnsense/mvc/app/controllers/OPNsense/Beats/Api/ServiceController.php @@ -0,0 +1,48 @@ +view->pick('OPNsense/Beats/filebeat'); + // fetch form data "general" in + $this->view->generalForm = $this->getForm("filebeat"); + } +} diff --git a/sysutils/beats/src/opnsense/mvc/app/controllers/OPNsense/Beats/forms/filebeat.xml b/sysutils/beats/src/opnsense/mvc/app/controllers/OPNsense/Beats/forms/filebeat.xml new file mode 100755 index 0000000000..bb047330bf --- /dev/null +++ b/sysutils/beats/src/opnsense/mvc/app/controllers/OPNsense/Beats/forms/filebeat.xml @@ -0,0 +1,53 @@ +
    + + filebeat.enabled + + checkbox + Enable the Filebeat service. + + + filebeat.modules.enabled + + select_multiple + The Filebeat modules to enable. + + + filebeat.inputs.enabled + + select_multiple + The Filebeat inputs to enable. + + + + header + + + filebeat.output.elasticsearch.hosts + + text + The Elasticsearch host to which Filebeat should send its logs. IPv6 addresses should always be defined as: https://[2001:db8::1]:9200. + http://localhost:9200 + + + filebeat.output.elasticsearch.api_key + + password + The authentication API key in its id:api_key format. + id:api_key + + + filebeat.output.elasticsearch.ssl.verification_mode + + dropdown + Controls the verification of certificates. The full mode verifies that the provided certificate is signed by a trusted authority (CA) and also verifies that the server's hostname (or IP address) matches the names identified within the certificate. The strict mode is similar to full mode, but requires the Subject Alternative Name to be defined as well. The certificate mode verifies that the provided certificate is signed by a trusted authority (CA), but does not perform any hostname verification. + true + + + filebeat.output.elasticsearch.ssl.ca_trusted_fingerprint + + text + A HEX encoded root CA SHA256 fingerprint added to the list of trusted CAs before SSL validation happens. + CA:FE:BA:BE:... + true + +
    diff --git a/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/ACL/ACL.xml b/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/ACL/ACL.xml new file mode 100644 index 0000000000..84efad19ec --- /dev/null +++ b/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + Services: Beats + + ui/beats + api/beats/* + + + diff --git a/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/Filebeat.php b/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/Filebeat.php new file mode 100644 index 0000000000..948db321c5 --- /dev/null +++ b/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/Filebeat.php @@ -0,0 +1,62 @@ +modules->enabled->isFieldChanged() || $this->inputs->enabled->isFieldChanged()) { + if ($this->modules->enabled->isEmpty() && $this->inputs->enabled->isEmpty()) { + $messages->appendMessage( + new Message( + gettext("Either an input or module needs to be specified."), + $this->modules->enabled->__reference + ) + ); + $messages->appendMessage( + new Message( + gettext("Either an input or module needs to be specified."), + $this->inputs->enabled->__reference + ) + ); + } + } + + return $messages; + } +} diff --git a/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/Filebeat.xml b/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/Filebeat.xml new file mode 100644 index 0000000000..e155e047ed --- /dev/null +++ b/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/Filebeat.xml @@ -0,0 +1,52 @@ + + //OPNsense/filebeat + Send logs to Elasticsearch + + + 0 + Y + + + + + Suricata (Intrusion Detection) + + Y + + + + + + Audit + Backend + Boot + General + Web GUI + + Y + + + + + + Y + + + Y + + + + Full + + Strict + Full + Certificate + + Y + + + + + + + diff --git a/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/Menu/Menu.xml b/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/Menu/Menu.xml new file mode 100644 index 0000000000..bf659a0bfa --- /dev/null +++ b/sysutils/beats/src/opnsense/mvc/app/models/OPNsense/Beats/Menu/Menu.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/sysutils/beats/src/opnsense/mvc/app/views/OPNsense/Beats/filebeat.volt b/sysutils/beats/src/opnsense/mvc/app/views/OPNsense/Beats/filebeat.volt new file mode 100644 index 0000000000..b04e78091c --- /dev/null +++ b/sysutils/beats/src/opnsense/mvc/app/views/OPNsense/Beats/filebeat.volt @@ -0,0 +1,51 @@ +{# + # Copyright (C) 2025 Maxime Thiebaut + # 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':generalForm,'id':'frm_GeneralSettings'])}} +
    + +{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/beats/service/reconfigure'}) }} diff --git a/sysutils/beats/src/opnsense/service/conf/actions.d/actions_beats.conf b/sysutils/beats/src/opnsense/service/conf/actions.d/actions_beats.conf new file mode 100644 index 0000000000..4fb0ea897b --- /dev/null +++ b/sysutils/beats/src/opnsense/service/conf/actions.d/actions_beats.conf @@ -0,0 +1,24 @@ +[start] +command:/usr/local/etc/rc.d/filebeat start +parameters: +type:script +message:starting Filebeat + +[stop] +command:/usr/local/etc/rc.d/filebeat stop +parameters: +type:script +message:stopping Filebeat + +[restart] +command:/usr/local/etc/rc.d/filebeat restart +parameters: +type:script +message:restarting Filebeat + +[status] +command:/usr/local/etc/rc.d/filebeat status +parameters: +errors:no +type:script_output +message:requesting Filebeat status diff --git a/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/+TARGETS b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/+TARGETS new file mode 100644 index 0000000000..408300f171 --- /dev/null +++ b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/+TARGETS @@ -0,0 +1,2 @@ +filebeat.yml:/usr/local/etc/beats/filebeat.yml +filebeat:/etc/rc.conf.d/filebeat diff --git a/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat new file mode 100755 index 0000000000..c3a61aa37a --- /dev/null +++ b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat @@ -0,0 +1 @@ +filebeat_enable="{{ 'YES' if not helpers.empty('OPNsense.filebeat.enabled') else 'NO' }}" diff --git a/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat.yml b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat.yml new file mode 100644 index 0000000000..cf893f1e26 --- /dev/null +++ b/sysutils/beats/src/opnsense/service/templates/OPNsense/Beats/filebeat.yml @@ -0,0 +1,460 @@ +######################## Filebeat Configuration ############################ + +#========================== Modules configuration ============================= +{% set filebeat_modules_enabled = (OPNsense.filebeat.modules.enabled|default('')).split(',') %} +filebeat.modules: +{% if 'suricata' in filebeat_modules_enabled %} +#-------------------------------- Suricata Module -------------------------------- +- module: suricata + # EVE + eve: + enabled: true + + # Set custom paths for the log files. If left empty, + # Filebeat will choose the paths depending on your OS. + #var.paths: + + # Internal network configuration (advanced) can be added under this section. + #var.internal_networks: +{% endif %} + + +#=========================== Filebeat inputs ============================= + +# List of inputs to fetch data. +{% set filebeat_inputs_enabled = (OPNsense.filebeat.inputs.enabled|default('')).split(',') %} +filebeat.inputs: +# Each - is an input. Most options can be set at the input level, so +# you can use different inputs for various configurations. +# Below are the input specific configurations. + +# Type of the files. Based on this the way the file is read is decided. +# The different types cannot be mixed in one input +# +# Possible options are: +# * filestream: Reads every line of the log file +# * log: Reads every line of the log file (deprecated) +# * stdin: Reads the standard in + +#--------------------------- Filestream input ---------------------------- +- type: filestream + + # Unique ID among all inputs, an ID is required. + id: audit + tags: ['audit'] + + # Change to true to enable this input configuration. + enabled: {{ 'true' if 'audit' in filebeat_inputs_enabled else 'false' }} + + # Paths that should be crawled and fetched. Glob based paths. + # To fetch all ".log" files from a specific level of subdirectories + # /var/log/*/*.log can be used. + # For each file found under this path, a harvester is started. + # Make sure not file is defined twice as this can lead to unexpected behaviour. + paths: + - /var/log/audit/audit_*.log + + ### Parsers configuration + + #### Syslog configuration + + parsers: + - syslog: + format: auto + log_errors: true + add_error_key: true + +#--------------------------- Filestream input ---------------------------- +- type: filestream + + # Unique ID among all inputs, an ID is required. + id: configd + tags: ['configd'] + + # Change to true to enable this input configuration. + enabled: {{ 'true' if 'configd' in filebeat_inputs_enabled else 'false' }} + + # Paths that should be crawled and fetched. Glob based paths. + # To fetch all ".log" files from a specific level of subdirectories + # /var/log/*/*.log can be used. + # For each file found under this path, a harvester is started. + # Make sure not file is defined twice as this can lead to unexpected behaviour. + paths: + - /var/log/configd/configd_*.log + + ### Parsers configuration + + #### Syslog configuration + + parsers: + - syslog: + format: auto + log_errors: true + add_error_key: true + +#--------------------------- Filestream input ---------------------------- +- type: filestream + + # Unique ID among all inputs, an ID is required. + id: 'boot' + tags: ['boot'] + + # Change to true to enable this input configuration. + enabled: {{ 'true' if 'boot' in filebeat_inputs_enabled else 'false' }} + + # Paths that should be crawled and fetched. Glob based paths. + # To fetch all ".log" files from a specific level of subdirectories + # /var/log/*/*.log can be used. + # For each file found under this path, a harvester is started. + # Make sure not file is defined twice as this can lead to unexpected behaviour. + paths: + - /var/log/boot.log + + close.reader.on_eof: true + prospector: + scanner: + resend_on_touch: true + + ### Parsers configuration + + #### Syslog configuration + + parsers: + - syslog: + format: auto + log_errors: true + add_error_key: true + +#--------------------------- Filestream input ---------------------------- +- type: filestream + + # Unique ID among all inputs, an ID is required. + id: 'system' + tags: ['system'] + + # Change to true to enable this input configuration. + enabled: {{ 'true' if 'system' in filebeat_inputs_enabled else 'false' }} + + # Paths that should be crawled and fetched. Glob based paths. + # To fetch all ".log" files from a specific level of subdirectories + # /var/log/*/*.log can be used. + # For each file found under this path, a harvester is started. + # Make sure not file is defined twice as this can lead to unexpected behaviour. + paths: + - /var/log/system/system_*.log + + ### Parsers configuration + + #### Syslog configuration + + parsers: + - syslog: + format: auto + log_errors: true + add_error_key: true + +#--------------------------- Filestream input ---------------------------- +- type: filestream + + # Unique ID among all inputs, an ID is required. + id: 'lighttpd' + tags: ['lighttpd'] + + # Change to true to enable this input configuration. + enabled: {{ 'true' if 'lighttpd' in filebeat_inputs_enabled else 'false' }} + + # Paths that should be crawled and fetched. Glob based paths. + # To fetch all ".log" files from a specific level of subdirectories + # /var/log/*/*.log can be used. + # For each file found under this path, a harvester is started. + # Make sure not file is defined twice as this can lead to unexpected behaviour. + paths: + - /var/log/lighttpd/lighttpd_*.log + + ### Parsers configuration + + #### Syslog configuration + + parsers: + - syslog: + format: auto + log_errors: true + add_error_key: true + +# ================================== Outputs =================================== + +# Configure what output to use when sending the data collected by the beat. + +# ---------------------------- Elasticsearch Output ---------------------------- +output.elasticsearch: + # Boolean flag to enable or disable the output module. + #enabled: true + + # Array of hosts to connect to. + # Scheme and port can be left out and will be set to the default (http and 9200) + # In case you specify and additional path, the scheme is required: http://localhost:9200/path + # IPv6 addresses should always be defined as: https://[2001:db8::1]:9200 + hosts: ["{{ OPNsense.filebeat.output.elasticsearch.hosts }}"] + + # Performance presets configure other output fields to recommended values + # based on a performance priority. + # Options are "balanced", "throughput", "scale", "latency" and "custom". + # Default if unspecified: "custom" + preset: balanced + + # Set gzip compression level. Set to 0 to disable compression. + # This field may conflict with performance presets. To set it + # manually use "preset: custom". + # The default is 1. + #compression_level: 1 + + # Configure escaping HTML symbols in strings. + #escape_html: false + + # Protocol - either `http` (default) or `https`. + #protocol: "https" + + # Authentication credentials - either API key or username/password. + api_key: "{{ OPNsense.filebeat.output.elasticsearch.api_key }}" + #username: "elastic" + #password: "changeme" + + # Dictionary of HTTP parameters to pass within the URL with index operations. + #parameters: + #param1: value1 + #param2: value2 + + # Number of workers per Elasticsearch host. + # This field may conflict with performance presets. To set it + # manually use "preset: custom". + #worker: 1 + + # If set to true and multiple hosts are configured, the output plugin load + # balances published events onto all Elasticsearch hosts. If set to false, + # the output plugin sends all events to only one host (determined at random) + # and will switch to another host if the currently selected one becomes + # unreachable. The default value is true. + #loadbalance: true + + # Optional data stream or index name. The default is "filebeat-%{[agent.version]}". + # In case you modify this pattern you must update setup.template.name and setup.template.pattern accordingly. + #index: "filebeat-%{[agent.version]}" + + # Optional ingest pipeline. By default, no pipeline will be used. + #pipeline: "" + + # Optional HTTP path + #path: "/elasticsearch" + + # Custom HTTP headers to add to each request + #headers: + # X-My-Header: Contents of the header + + # Proxy server URL + #proxy_url: http://proxy:3128 + + # Whether to disable proxy settings for outgoing connections. If true, this + # takes precedence over both the proxy_url field and any environment settings + # (HTTP_PROXY, HTTPS_PROXY). The default is false. + #proxy_disable: false + + # The number of times a particular Elasticsearch index operation is attempted. If + # the indexing operation doesn't succeed after this many retries, the events are + # dropped. The default is 3. + #max_retries: 3 + + # The maximum number of events to bulk in a single Elasticsearch bulk API index request. + # This field may conflict with performance presets. To set it + # manually use "preset: custom". + # The default is 1600. + #bulk_max_size: 1600 + + # The number of seconds to wait before trying to reconnect to Elasticsearch + # after a network error. After waiting backoff.init seconds, the Beat + # tries to reconnect. If the attempt fails, the backoff timer is increased + # exponentially up to backoff.max. After a successful connection, the backoff + # timer is reset. The default is 1s. + #backoff.init: 1s + + # The maximum number of seconds to wait before attempting to connect to + # Elasticsearch after a network error. The default is 60s. + #backoff.max: 60s + + # The maximum amount of time an idle connection will remain idle + # before closing itself. Zero means use the default of 60s. The + # format is a Go language duration (example 60s is 60 seconds). + # This field may conflict with performance presets. To set it + # manually use "preset: custom". + # The default is 3s. + # idle_connection_timeout: 3s + + # Configure HTTP request timeout before failing a request to Elasticsearch. + #timeout: 90 + + # Prevents filebeat from connecting to older Elasticsearch versions when set to `false` + #allow_older_versions: true + + # Use SSL settings for HTTPS. + #ssl.enabled: true + + # Controls the verification of certificates. Valid values are: + # * full, which verifies that the provided certificate is signed by a trusted + # authority (CA) and also verifies that the server's hostname (or IP address) + # matches the names identified within the certificate. + # * strict, which verifies that the provided certificate is signed by a trusted + # authority (CA) and also verifies that the server's hostname (or IP address) + # matches the names identified within the certificate. If the Subject Alternative + # Name is empty, it returns an error. + # * certificate, which verifies that the provided certificate is signed by a + # trusted authority (CA), but does not perform any hostname verification. + # * none, which performs no verification of the server's certificate. This + # mode disables many of the security benefits of SSL/TLS and should only be used + # after very careful consideration. It is primarily intended as a temporary + # diagnostic mechanism when attempting to resolve TLS errors; its use in + # production environments is strongly discouraged. + # The default value is full. + ssl.verification_mode: {{ OPNsense.filebeat.output.elasticsearch.ssl.verification_mode|default('full') }} + + # List of supported/valid TLS versions. By default all TLS versions from 1.1 + # up to 1.3 are enabled. + #ssl.supported_protocols: [TLSv1.1, TLSv1.2, TLSv1.3] + + # List of root certificates for HTTPS server verifications + #ssl.certificate_authorities: ["/etc/pki/root/ca.pem"] + + # Certificate for SSL client authentication + #ssl.certificate: "/etc/pki/client/cert.pem" + + # Client certificate key + #ssl.key: "/etc/pki/client/cert.key" + + # Optional passphrase for decrypting the certificate key. + #ssl.key_passphrase: '' + + # Configure cipher suites to be used for SSL connections + #ssl.cipher_suites: [] + + # Configure curve types for ECDHE-based cipher suites + #ssl.curve_types: [] + + # Configure what types of renegotiation are supported. Valid options are + # never, once, and freely. Default is never. + #ssl.renegotiation: never + + # Configure a pin that can be used to do extra validation of the verified certificate chain, + # this allow you to ensure that a specific certificate is used to validate the chain of trust. + # + # The pin is a base64 encoded string of the SHA-256 fingerprint. + #ssl.ca_sha256: "" + + # A root CA HEX encoded fingerprint. During the SSL handshake if the + # fingerprint matches the root CA certificate, it will be added to + # the provided list of root CAs (`certificate_authorities`), if the + # list is empty or not defined, the matching certificate will be the + # only one in the list. Then the normal SSL validation happens. +{% if not helpers.empty('OPNsense.filebeat.output.elasticsearch.ssl.ca_trusted_fingerprint') %} + ssl.ca_trusted_fingerprint: "{{ OPNsense.filebeat.output.elasticsearch.ssl.ca_trusted_fingerprint|replace(':','') }}" +{% else %} + #ssl.ca_trusted_fingerprint: "" +{% endif %} + + # Enables restarting filebeat if any file listed by `key`, + # `certificate`, or `certificate_authorities` is modified. + # This feature IS NOT supported on Windows. + #ssl.restart_on_cert_change.enabled: false + + # Period to scan for changes on CA certificate files + #ssl.restart_on_cert_change.period: 1m + + # Enable Kerberos support. Kerberos is automatically enabled if any Kerberos setting is set. + #kerberos.enabled: true + + # Authentication type to use with Kerberos. Available options: keytab, password. + #kerberos.auth_type: password + + # Path to the keytab file. It is used when auth_type is set to keytab. + #kerberos.keytab: /etc/elastic.keytab + + # Path to the Kerberos configuration. + #kerberos.config_path: /etc/krb5.conf + + # Name of the Kerberos user. + #kerberos.username: elastic + + # Password of the Kerberos user. It is used when auth_type is set to password. + #kerberos.password: changeme + + # Kerberos realm. + #kerberos.realm: ELASTIC + + +# ================================== Logging =================================== + +# There are four options for the log output: file, stderr, syslog, eventlog +# The file output is the default. + +# Sets log level. The default log level is info. +# Available log levels are: error, warning, info, debug +#logging.level: info + +# Enable debug output for selected components. To enable all selectors use ["*"] +# Other available selectors are "beat", "publisher", "service" +# Multiple selectors can be chained. +#logging.selectors: [ ] + +# Send all logging output to stderr. The default is false. +#logging.to_stderr: false + +# Send all logging output to syslog. The default is false. +logging.to_syslog: true + +# Send all logging output to Windows Event Logs. The default is false. +#logging.to_eventlog: false + +# If enabled, Filebeat periodically logs its internal metrics that have changed +# in the last period. For each metric that changed, the delta from the value at +# the beginning of the period is logged. Also, the total values for +# all non-zero internal metrics are logged on shutdown. The default is true. +# This is disabled on FreeBSD due to procfs not providing /proc/curproc/stat +logging.metrics.enabled: false + +# The period after which to log the internal metrics. The default is 30s. +#logging.metrics.period: 30s + +# A list of metrics namespaces to report in the logs. Defaults to [stats]. +# `stats` contains general Beat metrics. `dataset` may be present in some +# Beats and contains module or input metrics. +#logging.metrics.namespaces: [stats] + +# Logging to rotating files. Set logging.to_files to false to disable logging to +# files. +logging.to_files: false +logging.files: + # Configure the path where the logs are written. The default is the logs directory + # under the home path (the binary location). + #path: /var/log/filebeat + + # The name of the files where the logs are written to. + #name: filebeat + + # Configure log file size limit. If the limit is reached, log file will be + # automatically rotated. + #rotateeverybytes: 10485760 # = 10MB + + # Number of rotated log files to keep. The oldest files will be deleted first. + #keepfiles: 7 + + # The permissions mask to apply when rotating log files. The default value is 0600. + # Must be a valid Unix-style file permissions mask expressed in octal notation. + #permissions: 0600 + + # Enable log file rotation on time intervals in addition to the size-based rotation. + # Intervals must be at least 1s. Values of 1m, 1h, 24h, 7*24h, 30*24h, and 365*24h + # are boundary-aligned with minutes, hours, days, weeks, months, and years as + # reported by the local system clock. All other intervals are calculated from the + # Unix epoch. Defaults to disabled. + #interval: 0 + + # Rotate existing logs on startup rather than appending them to the existing + # file. Defaults to true. + # rotateonstartup: true diff --git a/sysutils/boot-delay/Makefile b/sysutils/boot-delay/Makefile deleted file mode 100644 index 38582b59c0..0000000000 --- a/sysutils/boot-delay/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -PLUGIN_NAME= boot-delay -PLUGIN_VERSION= 1.0 -PLUGIN_REVISION= 1 -PLUGIN_COMMENT= Apply a persistent 10 second boot delay -PLUGIN_MAINTAINER= franco@opnsense.org - -.include "../../Mk/plugins.mk" diff --git a/sysutils/boot-delay/pkg-descr b/sysutils/boot-delay/pkg-descr deleted file mode 100644 index 498604f4df..0000000000 --- a/sysutils/boot-delay/pkg-descr +++ /dev/null @@ -1,4 +0,0 @@ -This plugin simply adds 10 seconds to the bootup process in order to -wait for e.g. USB drivers / devices to become ready. The behaviour -matches the initial installer media behaviour, which is normally -removed after the first successful boot. diff --git a/sysutils/boot-delay/src/etc/rc.loader.d/50-boot-delay b/sysutils/boot-delay/src/etc/rc.loader.d/50-boot-delay deleted file mode 100644 index cdf2a41964..0000000000 --- a/sysutils/boot-delay/src/etc/rc.loader.d/50-boot-delay +++ /dev/null @@ -1 +0,0 @@ -kern.cam.boot_delay="10000" diff --git a/sysutils/cpu-microcode/Makefile b/sysutils/cpu-microcode/Makefile new file mode 100644 index 0000000000..261093b60b --- /dev/null +++ b/sysutils/cpu-microcode/Makefile @@ -0,0 +1,17 @@ +PLUGIN_NAME= cpu-microcode-amd +PLUGIN_VERSION= 1.1 +PLUGIN_COMMENT= CPU microcode updates +PLUGIN_DEPENDS= x86info +PLUGIN_MAINTAINER= franco@opnsense.org +PLUGIN_VARIANTS= amd intel +PLUGIN_TIER= 2 + +amd_NAME= cpu-microcode-amd +amd_DEPENDS= cpu-microcode-amd +amd_COMMENT:= AMD ${PLUGIN_COMMENT} + +intel_NAME= cpu-microcode-intel +intel_DEPENDS= cpu-microcode-intel +intel_COMMENT:= Intel ${PLUGIN_COMMENT} + +.include "../../Mk/plugins.mk" diff --git a/sysutils/cpu-microcode/pkg-descr b/sysutils/cpu-microcode/pkg-descr new file mode 100644 index 0000000000..c84f699029 --- /dev/null +++ b/sysutils/cpu-microcode/pkg-descr @@ -0,0 +1,6 @@ +Updating your microcode can help to mitigate certain potential security +vulnerabilities in CPUs as well as address certain functional issues that +could, for example, result in unpredictable system behavior such as hangs, +crashes, unexpected reboots, data errors, etc. + +The microcode update will be loaded when the system is rebooted. diff --git a/sysutils/cpu-microcode/src/etc/rc.loader.d/40-cpu-microcode.in b/sysutils/cpu-microcode/src/etc/rc.loader.d/40-cpu-microcode.in new file mode 100644 index 0000000000..b20660e00f --- /dev/null +++ b/sysutils/cpu-microcode/src/etc/rc.loader.d/40-cpu-microcode.in @@ -0,0 +1,2 @@ +cpu_microcode_load="YES" +cpu_microcode_name="/boot/firmware/%%PLUGIN_VARIANT%%-ucode.bin" diff --git a/sysutils/dec-hw/+POST_INSTALL b/sysutils/dec-hw/+POST_INSTALL new file mode 100644 index 0000000000..a9ba080244 --- /dev/null +++ b/sysutils/dec-hw/+POST_INSTALL @@ -0,0 +1 @@ +/usr/local/etc/rc.syshook.d/early/30-powerstat diff --git a/sysutils/dec-hw/Makefile b/sysutils/dec-hw/Makefile new file mode 100644 index 0000000000..aa109fcd9a --- /dev/null +++ b/sysutils/dec-hw/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= dec-hw +PLUGIN_VERSION= 1.1 +PLUGIN_REVISION= 3 +PLUGIN_COMMENT= Deciso hardware specific information +PLUGIN_MAINTAINER= stephan.de.wit@deciso.com +PLUGIN_TIER= 2 + +.include "../../Mk/plugins.mk" diff --git a/sysutils/dec-hw/pkg-descr b/sysutils/dec-hw/pkg-descr new file mode 100644 index 0000000000..30378c6218 --- /dev/null +++ b/sysutils/dec-hw/pkg-descr @@ -0,0 +1,3 @@ +This package allows fetching the current power status for Deciso +appliances with dual power supplies via an API call and includes a simple +dashboard widget. diff --git a/sysutils/dec-hw/src/etc/rc.syshook.d/early/30-powerstat b/sysutils/dec-hw/src/etc/rc.syshook.d/early/30-powerstat new file mode 100755 index 0000000000..df63a9fc32 --- /dev/null +++ b/sysutils/dec-hw/src/etc/rc.syshook.d/early/30-powerstat @@ -0,0 +1,13 @@ +#!/bin/sh + +AMDGPIO="/boot/kernel/amdgpio.ko" + +if [ ! -e "$AMDGPIO" ]; then + echo "Error: amdpgio kernel module missing" + logger -t "dec-hw" "Error: amdpgio kernel module missing" + exit 1 +fi + +kldload "$AMDGPIO" 2>&1 + +exit 0 diff --git a/sysutils/dec-hw/src/opnsense/mvc/app/controllers/OPNsense/dechw/Api/InfoController.php b/sysutils/dec-hw/src/opnsense/mvc/app/controllers/OPNsense/dechw/Api/InfoController.php new file mode 100644 index 0000000000..66b819c93b --- /dev/null +++ b/sysutils/dec-hw/src/opnsense/mvc/app/controllers/OPNsense/dechw/Api/InfoController.php @@ -0,0 +1,48 @@ + "failed"]; + $status = parse_ini_string((new Backend())->configdRun('dechw power')); + + if (!empty($status)) { + $result["status"] = "OK"; + $result = array_merge($result, $status); + } + + return $result; + } +} diff --git a/sysutils/dec-hw/src/opnsense/mvc/app/models/OPNsense/dechw/ACL/ACL.xml b/sysutils/dec-hw/src/opnsense/mvc/app/models/OPNsense/dechw/ACL/ACL.xml new file mode 100644 index 0000000000..3d2b4f028a --- /dev/null +++ b/sysutils/dec-hw/src/opnsense/mvc/app/models/OPNsense/dechw/ACL/ACL.xml @@ -0,0 +1,8 @@ + + + Dashboard: Deciso Hardware Information widget + + api/dechw/info/powerstatus + + + diff --git a/sysutils/dec-hw/src/opnsense/scripts/dec-hw/powerstat b/sysutils/dec-hw/src/opnsense/scripts/dec-hw/powerstat new file mode 100755 index 0000000000..81c24a015f --- /dev/null +++ b/sysutils/dec-hw/src/opnsense/scripts/dec-hw/powerstat @@ -0,0 +1,15 @@ +#!/bin/sh + +GPIOC="/dev/gpioc0" + +if [ ! -e "$GPIOC" ]; then + logger -t "dec-hw" "Error: GPIO device does not exist, exiting." + exit 0 +fi + +i=1 +for PIN in 4 5; do + STATUS=$(gpioctl -f "$GPIOC" "$PIN") + printf "pwr%d=%d\n" "$i" "$STATUS" + i=$((i + 1)) +done diff --git a/sysutils/dec-hw/src/opnsense/service/conf/actions.d/actions_dechw.conf b/sysutils/dec-hw/src/opnsense/service/conf/actions.d/actions_dechw.conf new file mode 100644 index 0000000000..7483a404db --- /dev/null +++ b/sysutils/dec-hw/src/opnsense/service/conf/actions.d/actions_dechw.conf @@ -0,0 +1,5 @@ +[power] +command:/usr/local/opnsense/scripts/dec-hw/powerstat +parameters: +type:script_output +message:Deciso PWR LED state diff --git a/sysutils/dec-hw/src/opnsense/www/js/widgets/DecHW.js b/sysutils/dec-hw/src/opnsense/www/js/widgets/DecHW.js new file mode 100644 index 0000000000..a5c9743747 --- /dev/null +++ b/sysutils/dec-hw/src/opnsense/www/js/widgets/DecHW.js @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 Deciso B.V. + * 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. + */ + +export default class DecHW extends BaseWidget { + constructor() { + super(); + } + + getMarkup() { + const styles = ` + #status { + margin: 10px; + } + .power { + margin: 5px; + float: right; + } + .power:hover { + opacity: 0.5; + } + .pwr-container { + margin: 5px; + display: flex; + justify-content: center; + align-items: center; + } + .data-item { + padding: 10px; + border: 1px solid #ddd; + margin: 5px; + width: 50%; + display: inline-block; + } + `; + + const styleSheet = document.createElement("style"); + styleSheet.innerText = styles; + document.head.appendChild(styleSheet); + + return $(` +
    +
    +
    + ${this.translations.powersupply} 1 +
    +
    + ${this.translations.powersupply} 2 +
    +
    + `); + } + + async onWidgetTick() { + $('.power').tooltip('hide'); + let data = await this.ajaxCall('/api/dechw/info/powerstatus'); + + if (!data || data.status === 'failed') { + $('#status').html(`
    ${this.translations.nopower}
    `); + $('.pwr-container').hide(); + return; + } + + $('.power').remove(); + ['pwr1', 'pwr2'].forEach((key) => { + let status = data[key]; + + let $power = $(``); + $power.css('color', status === '1' ? 'blue' : 'red'); + $power.attr('title', status === '1' ? this.translations.poweron : this.translations.poweroff); + $(`#${key}`).append($power); + }); + + $('.power').tooltip({container: 'body'}); + } +} diff --git a/sysutils/dec-hw/src/opnsense/www/js/widgets/Metadata/DecHW.xml b/sysutils/dec-hw/src/opnsense/www/js/widgets/Metadata/DecHW.xml new file mode 100644 index 0000000000..a4d579970a --- /dev/null +++ b/sysutils/dec-hw/src/opnsense/www/js/widgets/Metadata/DecHW.xml @@ -0,0 +1,15 @@ + + + DecHW.js + + /api/dechw/info/powerstatus + + + Deciso Hardware Information + Power status could not be fetched. This widget is only applicable to Deciso hardware with dual power supplies. + Power is on + Power is off + Power Supply + + + diff --git a/sysutils/dmidecode/Makefile b/sysutils/dmidecode/Makefile index 428d1621a2..bcc369e09f 100644 --- a/sysutils/dmidecode/Makefile +++ b/sysutils/dmidecode/Makefile @@ -1,9 +1,7 @@ PLUGIN_NAME= dmidecode -PLUGIN_VERSION= 1.1 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.2 PLUGIN_COMMENT= Display hardware information on the dashboard PLUGIN_DEPENDS= dmidecode -PLUGIN_MAINTAINER= evbevz@gmail.com PLUGIN_WWW= https://smart-soft.ru .include "../../Mk/plugins.mk" diff --git a/sysutils/dmidecode/src/opnsense/mvc/app/controllers/OPNsense/Dmidecode/Api/ServiceController.php b/sysutils/dmidecode/src/opnsense/mvc/app/controllers/OPNsense/Dmidecode/Api/ServiceController.php new file mode 100644 index 0000000000..6dc1ed86eb --- /dev/null +++ b/sysutils/dmidecode/src/opnsense/mvc/app/controllers/OPNsense/Dmidecode/Api/ServiceController.php @@ -0,0 +1,42 @@ +configdRun('dmidecode system')), false, INI_SCANNER_RAW); + $bios = parse_ini_string(trim((new Backend())->configdRun('dmidecode bios')), false, INI_SCANNER_RAW); + return ['status' => 'ok', 'system' => $system, 'bios' => $bios]; + } +} diff --git a/sysutils/dmidecode/src/opnsense/mvc/app/models/OPNsense/Dmidecode/ACL/ACL.xml b/sysutils/dmidecode/src/opnsense/mvc/app/models/OPNsense/Dmidecode/ACL/ACL.xml new file mode 100644 index 0000000000..d5ef40eb16 --- /dev/null +++ b/sysutils/dmidecode/src/opnsense/mvc/app/models/OPNsense/Dmidecode/ACL/ACL.xml @@ -0,0 +1,8 @@ + + + Service: DMI Data Widget + + api/dmidecode/service/get + + + diff --git a/sysutils/dmidecode/src/opnsense/www/js/widgets/Dmidecode.js b/sysutils/dmidecode/src/opnsense/www/js/widgets/Dmidecode.js new file mode 100644 index 0000000000..32a7dbcd78 --- /dev/null +++ b/sysutils/dmidecode/src/opnsense/www/js/widgets/Dmidecode.js @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2025 Neil Merchant + * 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. + */ + +export default class Dmidecode extends BaseTableWidget { + constructor() { + super(); + this.title = 'DMI Data'; + } + + getMarkup() { + let $container = $('
    '); + // make table for system output, add header + let $system_table = super.createTable('system-table', { headerPosition: 'left' }); + $container.append(`

    ${this.translations.system}

    `) + $container.append($system_table); + // same for bios output + let $bios_table = super.createTable('bios-table', { headerPosition: 'left' }); + $container.append(`

    ${this.translations.bios}

    `) + $container.append($bios_table); + return $container; + } + + async onMarkupRendered() { + const dmiData = await this.ajaxCall('/api/dmidecode/service/get'); + if (!dmiData || dmiData?.status !== 'ok') { + this.displayError('dmi lookup failed'); + return; + } + this.processDMIData(dmiData); + } + + processDMIData(data) { + const sysrows = []; + for (const [key, value] of Object.entries(data.system)) { + const row = []; + // try to find translation for key, fallback to output value + // have to split on spaces here because those aren't valid in xml tags + const translationIndex = key.split(" ")[0] + const dispKey = this.translations[translationIndex] || key + row.push(`
    ${dispKey}
    `, `
    ${value}
    `); + sysrows.push(row); + } + const biosrows = []; + for (const [key, value] of Object.entries(data.bios)) { + const row = []; + // try to find translation for key, fallback to output value + // have to split on spaces here because those aren't valid in xml tags + const translationIndex = key.split(" ")[0] + const dispKey = this.translations[translationIndex] || key + row.push(`
    ${dispKey}
    `, `
    ${value}
    `); + biosrows.push(row); + } + super.updateTable('system-table', sysrows); + super.updateTable('bios-table', biosrows); + } + + displayError(message) { + // if something went wrong, display error message in system table + const $error = $(` +
    + ${message} +
    + `); + $('#system-table').empty().append($error); +} + +} diff --git a/sysutils/dmidecode/src/opnsense/www/js/widgets/Metadata/Dmidecode.xml b/sysutils/dmidecode/src/opnsense/www/js/widgets/Metadata/Dmidecode.xml new file mode 100644 index 0000000000..8bc482bece --- /dev/null +++ b/sysutils/dmidecode/src/opnsense/www/js/widgets/Metadata/Dmidecode.xml @@ -0,0 +1,20 @@ + + + Dmidecode.js + + /api/dmidecode/service/get + + + DMI Data + Platform + BIOS + Manufacturer + Product Name + Version + Serial Number + Family + Vendor + Release Date + + + diff --git a/sysutils/dmidecode/src/www/widgets/include/dmidecode.inc b/sysutils/dmidecode/src/www/widgets/include/dmidecode.inc deleted file mode 100644 index 0fd5de9f66..0000000000 --- a/sysutils/dmidecode/src/www/widgets/include/dmidecode.inc +++ /dev/null @@ -1,3 +0,0 @@ - - - - - $val) { ?> - - - - - - - $val) { ?> - - - - - - -
    diff --git a/sysutils/gdrive-backup/Makefile b/sysutils/gdrive-backup/Makefile new file mode 100644 index 0000000000..ff88386c0c --- /dev/null +++ b/sysutils/gdrive-backup/Makefile @@ -0,0 +1,7 @@ +PLUGIN_NAME= gdrive-backup +PLUGIN_VERSION= 1.0 +PLUGIN_COMMENT= Backup configurations using Google Drive +PLUGIN_DEPENDS= php${PLUGIN_PHP}-google-api-php-client +PLUGIN_MAINTAINER= ad@opnsense.org + +.include "../../Mk/plugins.mk" diff --git a/sysutils/gdrive-backup/pkg-descr b/sysutils/gdrive-backup/pkg-descr new file mode 100644 index 0000000000..68fc6ad30c --- /dev/null +++ b/sysutils/gdrive-backup/pkg-descr @@ -0,0 +1,4 @@ +This plugin adds a backup option using Google Drive. + +Due to the sensitive nature of the data being send to the backup, +we strongly advise to not use a public service to send backups to. diff --git a/sysutils/gdrive-backup/src/etc/inc/plugins.inc.d/gdrive.inc b/sysutils/gdrive-backup/src/etc/inc/plugins.inc.d/gdrive.inc new file mode 100644 index 0000000000..bad2cfa6bf --- /dev/null +++ b/sysutils/gdrive-backup/src/etc/inc/plugins.inc.d/gdrive.inc @@ -0,0 +1,40 @@ + gettext('Backup - Google Drive'), + 'section' => 'system.remotebackup', + 'id' => 'remotebackup', + ]]; +} diff --git a/sysutils/gdrive-backup/src/opnsense/mvc/app/library/Google/API/Drive.php b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/Google/API/Drive.php new file mode 100644 index 0000000000..ef1ffb437b --- /dev/null +++ b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/Google/API/Drive.php @@ -0,0 +1,148 @@ +client = new \Google_Client(); + + $service_account = [ + "type" => "service_account", + "private_key" => $certinfo['pkey'], + "client_email" => $client_id, + "client_id" => $client_id, + "auth_uri" => "https://accounts.google.com/o/oauth2/auth", + "token_uri" => "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url" => "https://www.googleapis.com/oauth2/v1/certs" + ]; + + $this->client->setAuthConfig($service_account); + $this->client->addScope("https://www.googleapis.com/auth/drive"); + $this->client->setApplicationName("OPNsense"); + + $this->service = new \Google_Service_Drive($this->client); + } + + /** + * retrieve directory listing + * @param $directoryId parent directory id + * @param $filename title/filename of object + * @return mixed list of files + */ + public function listFiles($directoryId, $filename = null) + { + $query = "'" . $directoryId . "' in parents "; + if ($filename != null) { + $query .= " and title in '" . $filename . "'"; + } + return $this->service->files->listFiles(['q' => $query, 'supportsAllDrives' => true]); + } + + + /** + * download a file by given GDrive file handle + * @param $fileHandle (object from listFiles) + * @return null|string + */ + public function download($fileHandle) + { + $response = $this->service->files->get($fileHandle->id, ['alt' => 'media', 'supportsAllDrives' => true]); + return $response->getBody()->getContents(); + } + + /** + * Upload file + * @param string $directoryId (parent id) + * @param string $filename + * @param string $content + * @param string $mimetype + * @return \Google_Service_Drive_DriveFile handle + */ + public function upload($directoryId, $filename, $content, $mimetype = 'text/plain') + { + + $file = new \Google_Service_Drive_DriveFile(); + $file->setName($filename); + $file->setDescription($filename); + $file->setMimeType('text/plain'); + $file->setParents([$directoryId]); + + $createdFile = $this->service->files->create($file, [ + 'data' => $content, + 'mimeType' => $mimetype, + 'uploadType' => 'media', + 'supportsAllDrives' => true + ]); + + return $createdFile; + } + + /** + * delete file + * @param $fileHandle (object from listFiles) + */ + public function delete($fileHandle) + { + $this->service->files->delete($fileHandle['id'], ['supportsAllDrives' => true]); + } +} diff --git a/sysutils/gdrive-backup/src/opnsense/mvc/app/library/OPNsense/Backup/GDrive.php b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/OPNsense/Backup/GDrive.php new file mode 100644 index 0000000000..089ff08cd4 --- /dev/null +++ b/sysutils/gdrive-backup/src/opnsense/mvc/app/library/OPNsense/Backup/GDrive.php @@ -0,0 +1,306 @@ + "GDriveEnabled", + "type" => "checkbox", + "label" => gettext("Enable"), + "value" => null + ); + $fields[] = array( + "name" => "GDriveEmail", + "type" => "text", + "label" => gettext("Email Address"), + "help" => gettext("Client-ID in the Google cloud console"), + "value" => null + ); + $fields[] = array( + "name" => "GDriveP12key", + "type" => "file", + "label" => gettext("P12 key"), + "help" => sprintf( + gettext('You need a private key in p12 format to use Google Drive, ' . + 'instructions on how to acquire one can be found %shere%s.'), + '', + '' + ), + "value" => null + ); + $fields[] = array( + "name" => "GDriveFolderID", + "type" => "text", + "label" => gettext("Folder ID"), + "value" => null + ); + $fields[] = array( + "name" => "GDrivePrefixHostname", + "type" => "checkbox", + "label" => gettext("Prefix hostname to backupfile"), + "help" => gettext("Normally the config xml will be written as config-stamp.xml, with this option set " . + "the filename will use the systems host and domain name."), + "value" => null + ); + $fields[] = array( + "name" => "GDriveBackupCount", + "type" => "text", + "label" => gettext("Backup Count"), + "value" => 60 + ); + $fields[] = array( + "name" => "GDrivePassword", + "type" => "password", + "label" => gettext("Password"), + "value" => null + ); + $fields[] = array( + "name" => "GDrivePasswordConfirm", + "type" => "password", + "label" => gettext("Confirm"), + "value" => null + ); + $cnf = Config::getInstance(); + if ($cnf->isValid()) { + $config = $cnf->object(); + foreach ($fields as &$field) { + $fieldname = $field['name']; + if (isset($config->system->remotebackup->$fieldname)) { + $field['value'] = (string)$config->system->remotebackup->$fieldname; + } elseif ( + $fieldname == "GDrivePasswordConfirm" && + isset($config->system->remotebackup->GDrivePassword) + ) { + $field['value'] = (string)$config->system->remotebackup->GDrivePassword; + } + } + } + + return $fields; + } + + /** + * backup provider name + * @return string user friendly name + */ + public function getName() + { + return gettext("Google Drive"); + } + + /** + * validate and set configuration + * @param array $conf configuration array + * @return array of validation errors when not saved + */ + public function setConfiguration($conf) + { + $input_errors = array(); + if ($conf['GDrivePasswordConfirm'] != $conf['GDrivePassword']) { + $input_errors[] = gettext("The supplied 'Password' and 'Confirm' field values must match."); + } + if (count($input_errors) == 0) { + $config = Config::getInstance()->object(); + if (!isset($config->system->remotebackup)) { + $config->system->addChild('remotebackup'); + } + foreach ($this->getConfigurationFields() as $field) { + $fieldname = $field['name']; + if ($field['type'] == 'file') { + if (!empty($conf[$field['name']])) { + $config->system->remotebackup->$fieldname = base64_encode($conf[$field['name']]); + } + } elseif ($field['name'] == 'GDrivePasswordConfirm') { + /* skip password confirm field */ + } elseif (!empty($conf[$field['name']])) { + $config->system->remotebackup->$fieldname = $conf[$field['name']]; + } else { + unset($config->system->remotebackup->$fieldname); + } + } + // remove private key when disabled + if ( + empty($config->system->remotebackup->GDriveEnabled) && + isset($config->system->remotebackup->GDriveP12key) + ) { + unset($config->system->remotebackup->GDriveP12key); + } + Config::getInstance()->save(); + } + + return $input_errors; + } + + /** + * @return array filelist + */ + public function backup() + { + $cnf = Config::getInstance(); + if ($cnf->isValid()) { + $config = $cnf->object(); + if ( + isset($config->system->remotebackup) && isset($config->system->remotebackup->GDriveEnabled) + && !empty($config->system->remotebackup->GDriveEnabled) + ) { + if (!empty($config->system->remotebackup->GDrivePrefixHostname)) { + $fileprefix = (string)$config->system->hostname . "." . (string)$config->system->domain . "-"; + } else { + $fileprefix = "config-"; + } + try { + $client = new \Google\API\Drive(); + $client->login( + (string)$config->system->remotebackup->GDriveEmail, + (string)$config->system->remotebackup->GDriveP12key + ); + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "error connecting to Google Drive"); + return array(); + } + + // backup source data to local strings (plain/encrypted) + $confdata = file_get_contents('/conf/config.xml'); + $confdata_enc = $this->encrypt($confdata, (string)$config->system->remotebackup->GDrivePassword); + + // read filelist ({prefix}*.xml) + try { + $files = $client->listFiles((string)$config->system->remotebackup->GDriveFolderID); + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "error while fetching filelist from Google Drive"); + return array(); + } + + $configfiles = array(); + foreach ($files as $file) { + if (fnmatch("{$fileprefix}*.xml", $file['name'])) { + $configfiles[$file['name']] = $file; + } + } + krsort($configfiles); + + + // backup new file if changed (or if first in backup) + $target_filename = $fileprefix . time() . ".xml"; + if (count($configfiles) > 1) { + // compare last backup with current, only save new + try { + $bck_data_enc = $client->download($configfiles[array_keys($configfiles)[0]]); + if (strpos(substr($bck_data_enc, 0, 100), '---') !== false) { + // base64 string is wrapped into tags + $start_at = strpos($bck_data_enc, "---\n") + 4; + $end_at = strpos($bck_data_enc, "\n---"); + $bck_data_enc = substr($bck_data_enc, $start_at, ($end_at - $start_at)); + } + $bck_data = $this->decrypt( + $bck_data_enc, + (string)$config->system->remotebackup->GDrivePassword + ); + if ($bck_data == $confdata) { + $target_filename = null; + } + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "unable to download " . + $configfiles[array_keys($configfiles)[0]]->description . " from Google Drive (" . $e . ")"); + } + } + if (!is_null($target_filename)) { + syslog(LOG_NOTICE, "backup configuration as " . $target_filename); + try { + $configfiles[$target_filename] = $client->upload( + (string)$config->system->remotebackup->GDriveFolderID, + $target_filename, + $confdata_enc + ); + } catch (\Error | \Exception $e) { + syslog(LOG_ERR, "unable to upload " . $target_filename . " to Google Drive (" . $e . ")"); + return array(); + } + + krsort($configfiles); + } + + // cleanup old files + if ( + isset($config->system->remotebackup->GDriveBackupCount) + && is_numeric((string)$config->system->remotebackup->GDriveBackupCount) + ) { + $fcount = 0; + foreach ($configfiles as $filename => $file) { + if ($fcount >= (string)$config->system->remotebackup->GDriveBackupCount) { + syslog(LOG_NOTICE, "remove " . $filename . " from Google Drive"); + try { + $client->delete($file); + } catch (Google_Service_Exception $e) { + syslog(LOG_ERR, "unable to remove " . $filename . " from Google Drive"); + } + } + $fcount++; + } + } + + // return filelist + return array_keys($configfiles); + } + } + + // not configured / issue, return empty list + return array(); + } + + /** + * Is this provider enabled + * @return boolean enabled status + */ + public function isEnabled() + { + $cnf = Config::getInstance(); + if ($cnf->isValid()) { + $config = $cnf->object(); + return isset($config->system->remotebackup) && isset($config->system->remotebackup->GDriveEnabled) + && !empty($config->system->remotebackup->GDriveEnabled); + } + return false; + } +} diff --git a/sysutils/git-backup/Makefile b/sysutils/git-backup/Makefile index da33116f5d..5ac1f91b85 100644 --- a/sysutils/git-backup/Makefile +++ b/sysutils/git-backup/Makefile @@ -1,8 +1,9 @@ PLUGIN_NAME= git-backup -PLUGIN_VERSION= 1.0 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.1 +PLUGIN_REVISION= 3 PLUGIN_COMMENT= Track config changes using git PLUGIN_DEPENDS= git PLUGIN_MAINTAINER= ad@opnsense.org +PLUGIN_TIER= 2 .include "../../Mk/plugins.mk" diff --git a/sysutils/git-backup/pkg-descr b/sysutils/git-backup/pkg-descr index 58fcb26dc5..f8b99a6f0b 100644 --- a/sysutils/git-backup/pkg-descr +++ b/sysutils/git-backup/pkg-descr @@ -1,3 +1,15 @@ This package adds a backup option using git version control. -Due to the sensitive nature of the data being send to the backup, we strongly advise to not use a public service to send backups to. +Due to the sensitive nature of the data being send to the backup, +we strongly advise to not use a public service to send backups to. + +Plugin Changelog +================ + +1.1 + +* Add a force-push option (contributed by Hleb Shauchenka) + +1.0 + +* Initial release diff --git a/sysutils/git-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Git.php b/sysutils/git-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Git.php index b9626d2119..ab15998383 100644 --- a/sysutils/git-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Git.php +++ b/sysutils/git-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Git.php @@ -1,38 +1,37 @@ gettext("Target branch to push to."), "value" => null ], + [ + "name" => "force_push", + "type" => "checkbox", + "label" => gettext("Force Push"), + "help" => gettext("When enabled, force push to origin if diverged (e.g., after restoring from an earlier backup). Use with caution as this overwrites remote history."), + "value" => null + ], [ "name" => "privkey", - "type" => "textarea", + "type" => "passwordarea", "label" => gettext("SSH private key"), "help" => gettext("When provided, ssh based authentication will be used."), "value" => null @@ -118,7 +123,7 @@ public function setConfiguration($conf) } /** - * Backup is responsible for initialising the local repo and pusing it to upstream. + * Backup is responsible for initialising the local repo and pushing it to upstream. * To ensure initial content, we should trigger a 'system event config_changed' which should enforce a * add + commit in our (newly created) repo. * @@ -130,29 +135,35 @@ public function setConfiguration($conf) */ public function backup() { - $targetdir = "/conf/backup/git"; - $git = "/usr/local/bin/git"; + $targetdir = '/conf/backup/git'; $mdl = new GitSettings(); + if (!is_dir($targetdir)) { mkdir($targetdir); } + if (!is_dir('{$targetdir}/.git')) { - exec("{$git} init {$targetdir}"); + Shell::run_safe('/usr/local/bin/git init %s', $targetdir); } + // XXX: since our git backup is plain text and already contains the private key, it doesn't really matter // to keep the same key in the git directory (we're not going to push it) $ident_file = "{$targetdir}/identity"; $privkey = trim(str_replace("\r", "", (string)$mdl->privkey)) . "\n"; file_put_contents($ident_file, $privkey); chmod("{$targetdir}/identity", 0600); + // When there are unprocessed config backups, flush them out. - (new Backend())->configdRun("system event config_changed"); + (new Backend())->configdRun('system event config_changed'); + // configure upstream - exec("cd {$targetdir} && " . - "{$git} config core.sshCommand " . - "\"ssh -i {$ident_file} -o StrictHostKeyChecking=accept-new -o PasswordAuthentication=no\""); + Shell::run_safe('/usr/local/bin/git -C %s config core.sshCommand %s', [ + $targetdir, "ssh -i {$ident_file} -o StrictHostKeyChecking=accept-new -o PasswordAuthentication=no", + ]); + $url = (string)$mdl->url; $pos = strpos($url, '//'); + // inject credentials in url (either username or username:password, depending on transport) if (stripos(trim((string)$mdl->url), 'http') === 0) { $cred = urlencode((string)$mdl->user) . ":" . urlencode((string)$mdl->password); @@ -160,12 +171,16 @@ public function backup() } else { $url = substr($url, 0, $pos + 2) . urlencode((string)$mdl->user) . "@" . substr($url, $pos + 2); } - exec("cd {$targetdir} && git remote remove origin"); - exec("cd {$targetdir} && git remote add origin " . escapeshellarg($url)); - $pushtxt = shell_exec( - "(cd {$targetdir} && git push origin " . escapeshellarg("master:{$mdl->branch}") . - " && echo '__exit_ok__') 2>&1" - ); + + Shell::run_safe('/usr/local/bin/git -C %s remote remove origin', $targetdir); + Shell::run_safe('/usr/local/bin/git -C %s remote add origin %s', [$targetdir, $url]); + $gitfrmt = ['(/usr/local/bin/git -C %s push']; + if ($mdl->force_push->isEqual('1')) { + $gitfrmt[] = '--force'; + } + $gitfrmt[] = 'origin %s && echo "__exit_ok__") 2>&1'; + $pushtxt = Shell::shell_safe($gitfrmt, [$targetdir, "master:{$mdl->branch}"]); + if (strpos($pushtxt, '__exit_ok__')) { $error_type = null; } elseif (strpos($pushtxt, 'Permission denied') || strpos($pushtxt, 'Authentication failed ')) { @@ -177,12 +192,13 @@ public function backup() } else { $error_type = "unknown error, check log for details"; } + if (!empty($error_type)) { syslog(LOG_ERR, "git-backup {$error_type} (" . str_replace("\n", " ", $pushtxt) . ")"); throw new \Exception($error_type); } else { // return filelist in git - return explode("\n", shell_exec("cd {$targetdir} && git ls-files")); + return Shell::shell_safe('/usr/local/bin/git -C %s ls-files', $targetdir, true); } } diff --git a/sysutils/git-backup/src/opnsense/mvc/app/models/OPNsense/Backup/GitSettings.xml b/sysutils/git-backup/src/opnsense/mvc/app/models/OPNsense/Backup/GitSettings.xml index 52c88d582a..dbd7b4ca8c 100644 --- a/sysutils/git-backup/src/opnsense/mvc/app/models/OPNsense/Backup/GitSettings.xml +++ b/sysutils/git-backup/src/opnsense/mvc/app/models/OPNsense/Backup/GitSettings.xml @@ -1,23 +1,22 @@ //system/backup/git 1.0.0 - OPNsense Git Backup Settings + Git Backup Settings - 0 - Y - - - user.check001 - - - url.check001 - - + 0 + Y + + + user.check001 + + + url.check001 + + - N - /^((https)|(ssh))?:\/\/.*[^\/]$/ + /^((https)|(ssh))?:\/\/.*[^\/]$/ A valid git location must be provided. e.g. ssh://server/project.git, https://server/project.git @@ -30,22 +29,24 @@ - master - Y + master + Y - - N - + + 0 + Y + + - - - A username is required. - DependConstraint - - enabled - - - + + + A username is required. + DependConstraint + + enabled + + + diff --git a/sysutils/hw-probe/src/opnsense/mvc/app/models/OPNsense/Hwprobe/General.xml b/sysutils/hw-probe/src/opnsense/mvc/app/models/OPNsense/Hwprobe/General.xml index 96efb0749d..b2ec998fb7 100644 --- a/sysutils/hw-probe/src/opnsense/mvc/app/models/OPNsense/Hwprobe/General.xml +++ b/sysutils/hw-probe/src/opnsense/mvc/app/models/OPNsense/Hwprobe/General.xml @@ -4,7 +4,7 @@ 0.0.1 - 0 + 0 Y diff --git a/sysutils/lcdproc-sdeclcd/Makefile b/sysutils/lcdproc-sdeclcd/Makefile index a709853da7..73a802ba99 100644 --- a/sysutils/lcdproc-sdeclcd/Makefile +++ b/sysutils/lcdproc-sdeclcd/Makefile @@ -3,6 +3,5 @@ PLUGIN_VERSION= 1.1 PLUGIN_REVISION= 1 PLUGIN_COMMENT= LCDProc for SDEC LCD devices PLUGIN_DEPENDS= lcdproc -PLUGIN_MAINTAINER= ad@opnsense.org .include "../../Mk/plugins.mk" diff --git a/sysutils/mail-backup/src/opnsense/mvc/app/models/OPNsense/Backup/MailerSettings.xml b/sysutils/mail-backup/src/opnsense/mvc/app/models/OPNsense/Backup/MailerSettings.xml index 0a74af8008..301849c9e3 100644 --- a/sysutils/mail-backup/src/opnsense/mvc/app/models/OPNsense/Backup/MailerSettings.xml +++ b/sysutils/mail-backup/src/opnsense/mvc/app/models/OPNsense/Backup/MailerSettings.xml @@ -4,7 +4,7 @@ OPNsense Mailer Backup Settings - 0 + 0 Y @@ -30,11 +30,11 @@ - 25 + 25 Y - 1 + 1 Y diff --git a/sysutils/munin-node/Makefile b/sysutils/munin-node/Makefile index 95702e163b..43d5c15f89 100644 --- a/sysutils/munin-node/Makefile +++ b/sysutils/munin-node/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= munin-node -PLUGIN_VERSION= 1.0 +PLUGIN_VERSION= 1.1 PLUGIN_REVISION= 1 PLUGIN_COMMENT= Munin monitoring agent PLUGIN_DEPENDS= munin-node diff --git a/sysutils/munin-node/pkg-descr b/sysutils/munin-node/pkg-descr index 1ac945582d..c5b0937925 100644 --- a/sysutils/munin-node/pkg-descr +++ b/sysutils/munin-node/pkg-descr @@ -11,3 +11,10 @@ of creating own "plugins" (graphs). This is the node part. It is used on all machines Munin shall watch. WWW: http://munin-monitoring.org/ + +Plugin Changelog +---------------- + +1.1 + +* Remove MFS support for /var/log diff --git a/sysutils/munin-node/src/opnsense/mvc/app/models/OPNsense/Muninnode/General.xml b/sysutils/munin-node/src/opnsense/mvc/app/models/OPNsense/Muninnode/General.xml index 64d22bdd62..3d82dc0b11 100644 --- a/sysutils/munin-node/src/opnsense/mvc/app/models/OPNsense/Muninnode/General.xml +++ b/sysutils/munin-node/src/opnsense/mvc/app/models/OPNsense/Muninnode/General.xml @@ -4,20 +4,17 @@ 0.0.2 - 0 + 0 Y - OPNsense + OPNsense Y - 4949 + 4949 Y - - - N - + diff --git a/sysutils/munin-node/src/opnsense/service/conf/actions.d/actions_muninnode.conf b/sysutils/munin-node/src/opnsense/service/conf/actions.d/actions_muninnode.conf index 3152e2b0d6..693a363984 100644 --- a/sysutils/munin-node/src/opnsense/service/conf/actions.d/actions_muninnode.conf +++ b/sysutils/munin-node/src/opnsense/service/conf/actions.d/actions_muninnode.conf @@ -1,5 +1,5 @@ [start] -command:/usr/local/opnsense/scripts/OPNsense/Muninnode/setup.sh;/usr/local/etc/rc.d/munin-node start +command:/usr/local/etc/rc.d/munin-node start parameters: type:script message:starting munin-node @@ -11,13 +11,13 @@ type:script message:stopping munin-node [restart] -command:/usr/local/opnsense/scripts/OPNsense/Muninnode/setup.sh;/usr/local/etc/rc.d/munin-node restart +command:/usr/local/etc/rc.d/munin-node restart parameters: type:script message:restarting munin-node [status] -command:/usr/local/etc/rc.d/munin-node status;exit 0 +command:/usr/local/etc/rc.d/munin-node status; exit 0 parameters: type:script_output message:request munin-node status diff --git a/sysutils/munin-node/src/opnsense/service/templates/OPNsense/Muninnode/munin_node b/sysutils/munin-node/src/opnsense/service/templates/OPNsense/Muninnode/munin_node index ffcc9469ab..ac231c1b9a 100644 --- a/sysutils/munin-node/src/opnsense/service/templates/OPNsense/Muninnode/munin_node +++ b/sysutils/munin-node/src/opnsense/service/templates/OPNsense/Muninnode/munin_node @@ -1,7 +1,6 @@ {% if helpers.exists('OPNsense.muninnode.general.enabled') and OPNsense.muninnode.general.enabled == '1' %} -munin_node_var_script="/usr/local/opnsense/scripts/OPNsense/Muninnode/setup.sh" +munin_node_setup="/usr/local/opnsense/scripts/OPNsense/Muninnode/setup.sh" munin_node_enable="YES" {% else %} munin_node_enable="NO" {% endif %} -munin_node_var_mfs="/var/cache/munin_node /var/db/munin_node /var/log/munin_node" diff --git a/sysutils/nextcloud-backup/Makefile b/sysutils/nextcloud-backup/Makefile index dd29cd1013..6ec93c476a 100644 --- a/sysutils/nextcloud-backup/Makefile +++ b/sysutils/nextcloud-backup/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= nextcloud-backup -PLUGIN_VERSION= 1.0 +PLUGIN_VERSION= 1.1 PLUGIN_COMMENT= Track config changes using NextCloud -PLUGIN_MAINTAINER= franz.fabian.94@gmail.com .include "../../Mk/plugins.mk" diff --git a/sysutils/nextcloud-backup/pkg-descr b/sysutils/nextcloud-backup/pkg-descr index 23c4f87d84..7a96846941 100644 --- a/sysutils/nextcloud-backup/pkg-descr +++ b/sysutils/nextcloud-backup/pkg-descr @@ -2,3 +2,14 @@ This package adds a backup option using an existing NextCloud instance. Due to the sensitive nature of the data being send to the backup, we strongly advise to not use a public service to send backups to. + +Plugin Changelog +================ + +1.1 + +* Back up the content of /conf/backup (contributed by Daniel Lysfjor) + +1.0 + +* Initial release diff --git a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php index cca8365167..6f72ba479e 100644 --- a/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php +++ b/sysutils/nextcloud-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Nextcloud.php @@ -138,13 +138,7 @@ public function backup() $password = (string)$nextcloud->password; $backupdir = (string)$nextcloud->backupdir; $crypto_password = (string)$nextcloud->password_encryption; - $hostname = $config->system->hostname . '.' . $config->system->domain; - $configname = 'config-' . $hostname . '-' . date('Y-m-d_H_i_s') . '.xml'; - // backup source data to local strings (plain/encrypted) - $confdata = file_get_contents('/conf/config.xml'); - if (!empty($crypto_password)) { - $confdata = $this->encrypt($confdata, $crypto_password); - } + // Check if destination directory exists, create (full path) if not try { $internal_username = $this->getInternalUsername($url, $username, $password); @@ -153,26 +147,53 @@ public function backup() return array(); } - try { - $this->upload_file_content( - $url, - $username, - $password, - $internal_username, - $backupdir, - $configname, - $confdata - ); - // do not list directories - return array_filter( - $this->listFiles($url, $username, $password, $internal_username, "/$backupdir/", false), - function ($filename) { - return (substr($filename, -1) !== '/'); + // Get list of files from local backup system + $local_files = array(); + $tmp_local_files = scandir('/conf/backup/'); + // Remove '.' and '..' + foreach ($tmp_local_files as $tmp_local_file) { + if ($tmp_local_file === '.' || $tmp_local_file === '..') { + continue; + } + $local_files[] = $tmp_local_file; + } + + // Get list of filenames (without path) on remote location + $remote_files = array(); + $tmp_remote_files = $this->listfiles($url, $username, $password, $internal_username, "/$backupdir/", false); + foreach ($tmp_remote_files as $tmp_remote_file) { + $remote_files[] = pathinfo($tmp_remote_file)['basename']; + } + + + $uploaded_files = array(); + + // Loop over each local file, + // see if it's in $remote_files, + // if not, optionally encrypt, and upload + foreach ($local_files as $file_to_upload) { + if (!in_array($file_to_upload, $remote_files)) { + $confdata = file_get_contents("/conf/backup/$file_to_upload"); + if (!empty($crypto_password)) { + $confdata = $this->encrypt($confdata, $crypto_password); } - ); - } catch (\Exception $e) { - return array(); + try { + $this->upload_file_content( + $url, + $username, + $password, + $internal_username, + $backupdir, + $file_to_upload, + $confdata + ); + $uploaded_files[] = $file_to_upload; + } catch (\Exception $e) { + return $uploaded_files; + } + } } + return $uploaded_files; } } @@ -280,15 +301,14 @@ public function create_directory($url, $username, $password, $internal_username, public function getInternalUsername($url, $username, $password): string { - $xml_response = $this->ocs_request( - "$url/ocs/v1.php/cloud/user", - $username, - $password, - "GET", - "Cannot get real username" - ); - try { + $xml_response = $this->ocs_request( + "$url/ocs/v1.php/cloud/user", + $username, + $password, + "GET", + "Cannot get real username" + ); $data = $xml_response->data; if ($data == null) { return $username; // no data found, return the old username diff --git a/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml b/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml index b8c63ab7fe..10a12429e9 100644 --- a/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml +++ b/sysutils/nextcloud-backup/src/opnsense/mvc/app/models/OPNsense/Backup/NextcloudSettings.xml @@ -4,13 +4,12 @@ OPNsense Nextcloud Backup Settings - 0 - Y + 0 + Y - N - /^https?:\/\/.*[^\/]$/ - The url must be valid without a trailing slash. For example: https://nextcloud.example.com or https://example.com/nextcloud + /^https?:\/\/.*[^\/]$/ + The URL must be valid without a trailing slash. An URL for the Nextcloud server must be set. @@ -43,13 +42,11 @@ - - N - + Y - /^([\w%+\-]+\/)*[\w+%\-]+$/ - OPNsense-Backup + /^([\w%+\-]+\/)*[\w+%\-]+$/ + OPNsense-Backup The Backup Directory can only consist of alphanumeric characters, dash, underscores and slash. No leading or trailing slash. diff --git a/sysutils/node_exporter/Makefile b/sysutils/node_exporter/Makefile index 985de5c2e2..bb52769b48 100644 --- a/sysutils/node_exporter/Makefile +++ b/sysutils/node_exporter/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= node_exporter -PLUGIN_VERSION= 1.1 +PLUGIN_VERSION= 1.2 PLUGIN_COMMENT= Prometheus exporter for machine metrics PLUGIN_DEPENDS= node_exporter PLUGIN_MAINTAINER= jkegh@k123.eu diff --git a/sysutils/node_exporter/pkg-descr b/sysutils/node_exporter/pkg-descr index 16c736a183..862f318098 100644 --- a/sysutils/node_exporter/pkg-descr +++ b/sysutils/node_exporter/pkg-descr @@ -7,6 +7,10 @@ WWW: https://github.com/prometheus/node_exporter Changelog --------- +1.2 + +* Allow setting IPv6 addresses as ListenAddress + 1.1 * Allow to toggle the "zfs" collector diff --git a/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.php b/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.php index a049ba655d..075259cefb 100644 --- a/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.php +++ b/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.php @@ -35,5 +35,4 @@ class General extends BaseModel { - } diff --git a/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.xml b/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.xml index c47915f10b..07eb285951 100644 --- a/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.xml +++ b/sysutils/node_exporter/src/opnsense/mvc/app/models/OPNsense/NodeExporter/General.xml @@ -1,70 +1,33 @@ //OPNsense/NodeExporter - - node_exporter - Prometheus exporter for hardware and OS metrics. - - 0.1.0 + node_exporter configuration + 0.2.0 - 0 - Y + 0 + Y - - 0.0.0.0 - Y - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-4]|2[0-5][0-9]|[01]?[0-9][0-9]?)$/ - Please provide a valid IPv4 address. + + 0.0.0.0 + Y + N + Please provide a valid IP address. - - 9100 - Y - 1 - 65535 - Please provide a valid port number between 1 and 65535. Port 9100 is the default. + + 9100 + Y + Please provide a valid port number between 1 and 65535. Port 9100 is the default. - - 1 - N - - - 1 - N - - - 1 - N - - - 1 - N - - - 1 - N - - - 1 - N - - - - 0 - N - - - 0 - N - - - 0 - N - - - 0 - N - + + + + + + + diff --git a/sysutils/node_exporter/src/opnsense/service/templates/OPNsense/NodeExporter/node_exporter b/sysutils/node_exporter/src/opnsense/service/templates/OPNsense/NodeExporter/node_exporter index a5d3b02fcc..e7ea96fbc6 100644 --- a/sysutils/node_exporter/src/opnsense/service/templates/OPNsense/NodeExporter/node_exporter +++ b/sysutils/node_exporter/src/opnsense/service/templates/OPNsense/NodeExporter/node_exporter @@ -46,8 +46,14 @@ {%- set zfs = no_collector + "zfs " -%} {%- endif -%} +{%- if ':' in OPNsense.NodeExporter.listenaddress -%} + {%- set listenaddress = '[' + OPNsense.NodeExporter.listenaddress + ']' -%} +{%- else -%} + {%- set listenaddress = OPNsense.NodeExporter.listenaddress -%} +{%- endif -%} + node_exporter_args="{{ cpu }}{{ exec }}{{ filesystem }}{{ loadavg }}{{ meminfo }}{{ netdev }}{{ ntp }}{{ time }}{{ devstat }}{{ zfs }}" -node_exporter_listen_address="{{ OPNsense.NodeExporter.listenaddress }}:{{ OPNsense.NodeExporter.listenport }}" +node_exporter_listen_address="{{ listenaddress }}:{{ OPNsense.NodeExporter.listenport }}" node_exporter_enable="YES" {%- else -%} diff --git a/sysutils/nut/Makefile b/sysutils/nut/Makefile index 1e9315072c..ed35203b97 100644 --- a/sysutils/nut/Makefile +++ b/sysutils/nut/Makefile @@ -1,5 +1,6 @@ PLUGIN_NAME= nut -PLUGIN_VERSION= 1.8.1 +PLUGIN_VERSION= 1.9 +PLUGIN_REVISION= 1 PLUGIN_COMMENT= Network UPS Tools PLUGIN_DEPENDS= nut PLUGIN_MAINTAINER= m.muenz@gmail.com diff --git a/sysutils/nut/pkg-descr b/sysutils/nut/pkg-descr index 9f9fe6b37a..4018fb54db 100644 --- a/sysutils/nut/pkg-descr +++ b/sysutils/nut/pkg-descr @@ -9,6 +9,11 @@ and management interface. Plugin Changelog ---------------- +1.9 + +* Add dashboard widget +* Fix IPv6 port declaration (contributed by BPplays) + 1.8 * Add apcupsd-ups driver support diff --git a/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/DiagnosticsController.php b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/DiagnosticsController.php index fe002451e0..fe1faf74ac 100644 --- a/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/DiagnosticsController.php +++ b/sysutils/nut/src/opnsense/mvc/app/controllers/OPNsense/Nut/Api/DiagnosticsController.php @@ -1,29 +1,29 @@ - * All rights reserved. + * Copyright (C) 2018 Michael Muenz + * All rights reserved. * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: + * 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. + * 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. + * 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. + * 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. */ namespace OPNsense\Nut\Api; @@ -36,7 +36,6 @@ class DiagnosticsController extends ApiControllerBase { public function upsstatusAction() { - $this->sessionClose(); $mdl = new Nut(); $host = '127.0.0.1'; if (!empty((string)$mdl->netclient->address)) { diff --git a/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml b/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml index 3ee149b386..8ff52d8237 100644 --- a/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml +++ b/sysutils/nut/src/opnsense/mvc/app/models/OPNsense/Nut/Nut.xml @@ -1,156 +1,151 @@ - //OPNsense/Nut - Network UPS Tools - 1.0.4 - - - - 0 - Y - - - standalone - Y - - standalone - netclient - - - - UPSName - Y - /^([0-9a-zA-Z._\-]){1,128}$/u - The name should only contain alphanumeric characters, dashes, underscores or a dot. - - - 127.0.0.1,::1 - Y - - - - - - Y - Password - - - Y - Password - - - - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - Y - localhost - - - N - - - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - -
    - - N -
    - - 3493 - N - - - N - - - N - -
    - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - port=auto - N - - - - - Y - 0 - - - community=public - N - - -
    + //OPNsense/Nut + Network UPS Tools + 1.0.4 + + + + 0 + Y + + + standalone + Y + + standalone + netclient + + + + UPSName + Y + /^([0-9a-zA-Z._\-]){1,128}$/u + The name should only contain alphanumeric characters, dashes, underscores or a dot. + + + 127.0.0.1,::1 + Y + + + + + Y + Password + + + Y + Password + + + + + Y + 0 + + + port=auto + N + + + + + Y + 0 + + + port=auto + N + + + + + Y + 0 + + + Y + localhost + + + N + + + + + Y + 0 + + + port=auto + N + + + + + Y + 0 + + + port=auto + N + + + + + Y + 0 + + + port=auto + N + + + + + Y + 0 + +
    + + 3493 + N + + + N + + + N + + + + + Y + 0 + + + port=auto + N + + + + + Y + 0 + + + port=auto + N + + + + + Y + 0 + + + community=public + N + + + diff --git a/sysutils/nut/src/opnsense/scripts/OPNsense/Nut/setup.sh b/sysutils/nut/src/opnsense/scripts/OPNsense/Nut/setup.sh index 582348f059..2776cc9793 100755 --- a/sysutils/nut/src/opnsense/scripts/OPNsense/Nut/setup.sh +++ b/sysutils/nut/src/opnsense/scripts/OPNsense/Nut/setup.sh @@ -1,4 +1,6 @@ #!/bin/sh -mkdir -p /var/db/nut -chown uucp:uucp /var/db/nut +pw groupmod -n dialer -m nut + +mkdir -p /var/db/nut +chown -R nut:nut /var/db/nut diff --git a/sysutils/nut/src/opnsense/service/conf/actions.d/actions_nut.conf b/sysutils/nut/src/opnsense/service/conf/actions.d/actions_nut.conf index d7ae051a55..65816a99db 100644 --- a/sysutils/nut/src/opnsense/service/conf/actions.d/actions_nut.conf +++ b/sysutils/nut/src/opnsense/service/conf/actions.d/actions_nut.conf @@ -1,23 +1,23 @@ [start] -command:/usr/local/opnsense/scripts/OPNsense/Nut/setup.sh;/usr/local/etc/rc.d/nut start;/usr/local/etc/rc.d/nut_upsmon start +command:/usr/local/etc/rc.d/nut start; /usr/local/etc/rc.d/nut_upsmon start parameters: type:script message:starting nut [stop] -command:/usr/local/etc/rc.d/nut stop;/usr/local/etc/rc.d/nut_upsmon stop +command:/usr/local/etc/rc.d/nut stop; /usr/local/etc/rc.d/nut_upsmon stop parameters: type:script message:stopping nut [restart] -command:/usr/local/opnsense/scripts/OPNsense/Nut/setup.sh;/usr/local/etc/rc.d/nut restart;/usr/local/etc/rc.d/nut_upsmon restart +command:/usr/local/etc/rc.d/nut restart; /usr/local/etc/rc.d/nut_upsmon restart parameters: type:script message:restarting nut [status] -command:/usr/local/etc/rc.d/nut_upsmon status;exit 0 +command:/usr/local/etc/rc.d/nut_upsmon status; exit 0 parameters: type:script_output message:request nut status diff --git a/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/nut b/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/nut index 8015ac6dda..57744d5823 100644 --- a/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/nut +++ b/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/nut @@ -1,5 +1,5 @@ {% if helpers.exists('OPNsense.Nut.general.enable') and OPNsense.Nut.general.enable == '1' %} -nut_var_script="/usr/local/opnsense/scripts/OPNsense/Nut/setup.sh" +nut_setup="/usr/local/opnsense/scripts/OPNsense/Nut/setup.sh" nut_enable="YES" {% else %} nut_enable="NO" diff --git a/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf b/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf index da20af35da..e4d3ab405a 100644 --- a/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf +++ b/sysutils/nut/src/opnsense/service/templates/OPNsense/Nut/upsmon.conf @@ -7,7 +7,7 @@ SHUTDOWNCMD "/usr/local/etc/rc.halt" POWERDOWNFLAG /etc/killpower {% endif %} {% if helpers.exists('OPNsense.Nut.netclient.enable') and OPNsense.Nut.netclient.enable == '1' %} -MONITOR {{ OPNsense.Nut.general.name }}@{{ OPNsense.Nut.netclient.address }}{% if helpers.exists('OPNsense.Nut.netclient.port') %}:{{ OPNsense.Nut.netclient.port }}{% endif %} 1 {{ OPNsense.Nut.netclient.user }} {{ OPNsense.Nut.netclient.password }} slave +MONITOR {{ OPNsense.Nut.general.name }}@{{ helpers.host_with_port('OPNsense.Nut.netclient.address', 'OPNsense.Nut.netclient.port') }} 1 {{ OPNsense.Nut.netclient.user }} {{ OPNsense.Nut.netclient.password }} slave SHUTDOWNCMD "/usr/local/etc/rc.halt" POWERDOWNFLAG /etc/killpower {% endif %} diff --git a/sysutils/nut/src/opnsense/www/js/widgets/Metadata/Nut.xml b/sysutils/nut/src/opnsense/www/js/widgets/Metadata/Nut.xml new file mode 100644 index 0000000000..72d6988cf8 --- /dev/null +++ b/sysutils/nut/src/opnsense/www/js/widgets/Metadata/Nut.xml @@ -0,0 +1,42 @@ + + + Nut.js + + /api/nut/* + + + NUT + UPS Model + UPS Status + Battery status + UPS Load + UPS Efficiency + Battery level + Battery runtime + Output Power + Input Power + Self test + On line + On battery + Low battery + High battery + Battery needs to be replaced + Battery is charging + Battery is discharging + UPS bypass circuit is active + Performing runtime calibration + UPS is offline + UPS is overloaded + UPS is trimming voltage + UPS is boosting voltage + Forced Shutdown + Nut is not started. Click to configure Nut. + Nut is not returning a status. Click to reconfigure Nut. + This widget only works with the Netclient driver. Click to configure the Netclient driver. + Remote NUT Server + h + m + s + + + diff --git a/sysutils/nut/src/opnsense/www/js/widgets/Nut.js b/sysutils/nut/src/opnsense/www/js/widgets/Nut.js new file mode 100644 index 0000000000..6d65644b33 --- /dev/null +++ b/sysutils/nut/src/opnsense/www/js/widgets/Nut.js @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2024 DollarSign23 + * Copyright (C) 2024 Nicola Pellegrini + * 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. + */ + +export default class NutNetclient extends BaseTableWidget { + constructor() { + super(); + this.timeoutPeriod = 1000; // Set a timeout period for AJAX calls or other timed operations. + } + + getGridOptions() { + return { + // Trigger overflow-y:scroll after 650px height + sizeToContent: 650 + }; + } + + // Creates and returns the HTML structure for the widget, including a table without a header. + getMarkup() { + let $container = $('
    '); // Create a container div. + let $nut_table = this.createTable('nut-table', { + headerPosition: 'none', // Disable table headers. + }); + $container.append($nut_table); // Append the table to the container. + return $container; // Return the container with the table. + } + + // Periodically called to update the widget's data and UI. + async onWidgetTick() { + // Fetch the NUT service status from the server. + const nut_service_status = await this.ajaxCall(`/api/nut/${'service/status'}`); + + // If the service is not running, display a message and stop further processing. + if (!nut_service_status || nut_service_status.status !== 'running') { + $('#nut-table').html(`${this.translations.unconfigured}`); + return; + } + + // Fetch the NUT settings from the server. + const nut_settings = await this.ajaxCall(`/api/nut/${'settings/get'}`); + + // // If netclient is not enabled, display a message and stop further processing. + // if (nut_settings.nut?.netclient?.enable !== "1") { + // $('#nut-table').html(`${this.translations.netclient_unconfigured}`); + // return; + // } + + // Fetch the UPS status data from the server. + const { response: nut_ups_status_response } = await this.ajaxCall(`/api/nut/${'diagnostics/upsstatus'}`); + + if (!nut_ups_status_response) { + $('#nut-table').html(`${this.translations.misconfigured}`); + return; + } + + // Parse the UPS status data into a key-value object. + const nut_ups_status = nut_ups_status_response.split('\n').reduce((acc, line) => { + const [key, value] = line.split(': '); + if (key) acc[key] = value; // Only add non-empty keys. + return acc; + }, {}); + + // Use the dataChanged method to check if the data has changed since the last tick + if (!this.dataChanged('ups_status', nut_ups_status)) { + return; + } + + // Prepare the rows for the table based on the fetched data. + const rows = [ + // Display the remote server address if available. + nut_settings.nut?.netclient?.address && nut_settings.nut?.netclient?.address && nut_settings.nut?.netclient?.user && this.makeTextRow("netclient_remote_server", `${nut_settings.nut?.netclient?.user}@${nut_settings.nut?.netclient?.address}:${nut_settings.nut?.netclient?.port}`), + // Display the manufacturer and model if available. + nut_ups_status['device.mfr'] && nut_ups_status['device.model'] && this.makeTextRow("status_model", `${nut_ups_status['device.mfr']} - ${nut_ups_status['device.model']}`), + // Display the UPS Status if available. + nut_ups_status['ups.status'] && this.makeColoredTextRow('status_status', this.nutMapStatus(nut_ups_status['ups.status']), /OL/, /OB|LB|RB|DISCHRG/, nut_ups_status['ups.status']), + // Display the UPS load with percentage and optional nominal power. + nut_ups_status['ups.load'] && nut_ups_status['ups.realpower'] && this.makeUpsLoadRow('status_load', parseFloat(nut_ups_status['ups.load']), parseFloat(nut_ups_status['ups.realpower'])), + // Display the battery charge as a progress bar if available. + nut_ups_status['battery.charge'] && this.makeProgressBarRow("status_bcharge", parseFloat(nut_ups_status['battery.charge'])), + // Display the battery status if available. + nut_ups_status['battery.charger.status'] && this.makeTextRow('status_battery', nut_ups_status['battery.charger.status']), + // Display the formatted battery runtime if available. + nut_ups_status['battery.runtime'] && this.makeTextRow('status_timeleft', this.formatRuntime(parseInt(nut_ups_status['battery.runtime'], 10))), + // Display the input voltage and frequency if available. + nut_ups_status['input.voltage'] && nut_ups_status['input.frequency'] && this.makeTextRow('status_input_power', `${nut_ups_status['input.voltage']} V | ${nut_ups_status['input.frequency']} Hz`), + // Display the output voltage and frequency if available. + nut_ups_status['output.voltage'] && nut_ups_status['output.frequency'] && this.makeTextRow('status_output_power', `${nut_ups_status['output.voltage']} V | ${nut_ups_status['output.frequency']} Hz`), + // Display the result of the UPS efficiency if available. + nut_ups_status['ups.efficiency'] && this.makeTextRow('status_efficiency', `${nut_ups_status['ups.efficiency']}%`), + // Display the result of the UPS self-test if available. + nut_ups_status['ups.test.result'] && this.makeTextRow('status_selftest', nut_ups_status['ups.test.result']), + ].filter(Boolean); // Remove any undefined or null rows. + + // Update the table with the prepared rows. + this.updateTable('nut-table', rows); + } + + // Formats the runtime (in seconds) into a human-readable format (hours, minutes, seconds). + formatRuntime(seconds) { + const hours = Math.floor(seconds / 3600); // Calculate full hours. + const minutes = Math.floor((seconds % 3600) / 60); // Calculate remaining full minutes. + const remainingSeconds = seconds % 60; // Calculate remaining seconds. + + let formattedTime = ''; + + if (hours > 0) { + formattedTime += `${hours}${this.translate('time_hours')} `; + } + if (minutes > 0 || hours > 0) { // Only show minutes if they are > 0 or hours are present. + formattedTime += `${minutes}${this.translate('time_minutes')} `; + } + formattedTime += `${remainingSeconds}${this.translate('time_seconds')}`; // Always show seconds. + + return formattedTime.trim(); // Remove any trailing spaces. + } + + // Create a mapping between UPS status codes and their corresponding translations + nutMapStatus(statusCode) { + const statusMapping = { + 'OL': this.translate('status_ol'), // On line (mains is present) + 'OB': this.translate('status_ob'), // On battery (mains is not present) + 'LB': this.translate('status_lb'), // Low battery + 'HB': this.translate('status_hb'), // High battery + 'RB': this.translate('status_rb'), // Battery needs to be replaced + 'CHRG': this.translate('status_chrg'), // Battery is charging + 'DISCHRG': this.translate('status_dischrg'), // Battery is discharging + 'BYPASS': this.translate('status_bypass'), // UPS bypass circuit is active (no battery protection available) + 'CAL': this.translate('status_cal'), // Performing runtime calibration (on battery) + 'OFF': this.translate('status_off'), // UPS is offline + 'OVER': this.translate('status_over'), // UPS is overloaded + 'TRIM': this.translate('status_trim'), // UPS is trimming incoming voltage + 'BOOST': this.translate('status_boost'), // UPS is boosting incoming voltage + 'FSD': this.translate('status_fsd'), // Forced Shutdown + }; + + // Return the mapped translation or the original status code if no translation is found + return statusMapping[statusCode] || statusCode; + } + + // Creates a row for the UPS load, including the percentage and optional nominal power. + makeUpsLoadRow(labelKey, loadpct, nompower) { + let text = loadpct.toFixed(1) + ' %'; + if (nompower) { + text += ` ( ~ ${nompower} W )`; + } + return this.makeProgressBarRow(labelKey, loadpct, text); + } + + // Creates a row with a progress bar, optionally including custom text. + makeProgressBarRow(labelKey, progress, progressText = `${progress.toFixed(1)} %`) { + const pb = this.makeProgressBar(progress, progressText); // Create the progress bar. + return this.makeRow(labelKey, pb); // Create a row with the progress bar. + } + + // Creates a row with text, applying color based on regular expressions. + makeColoredTextRow(labelKey, value, okRegexp, errRegexp, check_value = value) { + const textEl = $('').text(value); // Create a bold text element with the value. + + // Apply CSS classes based on regex matches. + if (okRegexp?.exec(check_value)) { + textEl.addClass('text-success'); + } else if (errRegexp?.exec(check_value)) { + textEl.addClass('text-danger'); + } else { + textEl.addClass('text-warning'); + } + + return this.makeRow(labelKey, textEl.prop('outerHTML')); // Create a row with the colored text. + } + + // Creates a progress bar with a text overlay. + makeProgressBar(progress, text) { + const $textEl = $('').text(text).css({ + position: 'absolute', + left: 0, + right: 0 + }); + + const $barEl = $('
    ').css({ + width: `${progress}%`, + zIndex: 0 + }); + + return $('
    ').append($barEl, $textEl).prop("outerHTML"); + } + + // Creates a text row for the table. + makeTextRow(labelKey, content) { + content = typeof content === 'string' ? content : content.value; // Ensure content is a string. + return this.makeRow(labelKey, content); // Create a row with the text content. + } + + // Creates a row with a label and content. + makeRow(labelKey, content) { + return [this.translate(labelKey), content]; + } + + // Translates a key into the corresponding text. + translate(key) { + let value = this.translations[key]; + if (value === undefined) { + console.error('Missing translation for ' + key); + value = key; // Fallback to the key itself if translation is missing. + } + return value; + } +} diff --git a/sysutils/puppet-agent/Makefile b/sysutils/puppet-agent/Makefile index 712b01e4fd..f79acafee3 100644 --- a/sysutils/puppet-agent/Makefile +++ b/sysutils/puppet-agent/Makefile @@ -1,7 +1,7 @@ PLUGIN_NAME= puppet-agent -PLUGIN_VERSION= 1.0 +PLUGIN_VERSION= 1.2 PLUGIN_COMMENT= Manage Puppet Agent -PLUGIN_DEPENDS= puppet7 py${PLUGIN_PYTHON}-opn-cli +PLUGIN_DEPENDS= puppet8 py${PLUGIN_PYTHON}-opn-cli PLUGIN_MAINTAINER= jan.wink93@gmail.com .include "../../Mk/plugins.mk" diff --git a/sysutils/puppet-agent/pkg-descr b/sysutils/puppet-agent/pkg-descr index 64ec7afe9d..0cd522fc50 100644 --- a/sysutils/puppet-agent/pkg-descr +++ b/sysutils/puppet-agent/pkg-descr @@ -9,6 +9,21 @@ WWW: https://puppet.com/docs/puppet/latest/man/agent.html Plugin Changelog ================ +1.2 + +Changed: +* switch to Puppet 8 (#4800) + +1.1 + +Added: +* add new options runinterval and runtimeout +* add new option usecacheonfailure + +Changed: +* add default values for Puppet Server + Environment +* don't wipe puppet.conf if service is disabled + 1.0 Added: diff --git a/sysutils/puppet-agent/src/opnsense/mvc/app/controllers/OPNsense/PuppetAgent/forms/general.xml b/sysutils/puppet-agent/src/opnsense/mvc/app/controllers/OPNsense/PuppetAgent/forms/general.xml index 6713c5ec46..ce6b4412b2 100644 --- a/sysutils/puppet-agent/src/opnsense/mvc/app/controllers/OPNsense/PuppetAgent/forms/general.xml +++ b/sysutils/puppet-agent/src/opnsense/mvc/app/controllers/OPNsense/PuppetAgent/forms/general.xml @@ -3,18 +3,36 @@ puppetagent.general.Enabled checkbox - Enable Puppet Agent + Enable the Puppet Agent service. puppetagent.general.FQDN text - Change Puppet Server FQDN + The primary Puppet Server to which the Puppet Agent should connect. puppetagent.general.Environment text - Change Puppet Agent Environment + The environment in which Puppet is running. + + + puppetagent.general.RunInterval + + text + How often Puppet Agent applies the catalog. Enter a number followed by one of the supported suffixes: "y" (years), "d" (days), "h" (hours), "m" (minutes), "s" (seconds). + + + puppetagent.general.RunTimeout + + text + The maximum amount of time an agent run is allowed to take. Enter a number followed by one of the supported suffixes: "y" (years), "d" (days), "h" (hours), "m" (minutes), "s" (seconds). + + + puppetagent.general.UseCacheOnFailure + + checkbox + Whether to use the cached configuration when the remote configuration will not compile. diff --git a/sysutils/puppet-agent/src/opnsense/mvc/app/models/OPNsense/PuppetAgent/PuppetAgent.xml b/sysutils/puppet-agent/src/opnsense/mvc/app/models/OPNsense/PuppetAgent/PuppetAgent.xml index f706d65a59..2e344467de 100644 --- a/sysutils/puppet-agent/src/opnsense/mvc/app/models/OPNsense/PuppetAgent/PuppetAgent.xml +++ b/sysutils/puppet-agent/src/opnsense/mvc/app/models/OPNsense/PuppetAgent/PuppetAgent.xml @@ -1,23 +1,39 @@ //OPNsense/puppetagent - 1.0.0 + 1.1.0 Manage Puppet Agent service - - - 0 + 0 Y + puppet Y + production Y - /^.{1,100}$/u + /^.{1,100}$/u Should be a string between 1 and 100 characters. + + 30m + /^([0-9]{1,8}(?:s|m|h|d|y)?)/u + Should be a number between 1 and 8 characters, optionally followed by either "y", "d", "h", "m" or "s". + Y + + + 1h + /^([0-9]{1,8}(?:s|m|h|d|y)?)/u + Should be a number between 1 and 8 characters, optionally followed by either "y", "d", "h", "m" or "s". + Y + + + 1 + Y + diff --git a/sysutils/puppet-agent/src/opnsense/mvc/app/views/OPNsense/PuppetAgent/index.volt b/sysutils/puppet-agent/src/opnsense/mvc/app/views/OPNsense/PuppetAgent/index.volt index 7e0d533c56..c9ca842c13 100644 --- a/sysutils/puppet-agent/src/opnsense/mvc/app/views/OPNsense/PuppetAgent/index.volt +++ b/sysutils/puppet-agent/src/opnsense/mvc/app/views/OPNsense/PuppetAgent/index.volt @@ -61,8 +61,8 @@ POSSIBILITY OF SUCH DAMAGE.

    {{ lang._("Welcome to the Puppet Agent plugin! This plugin allows you to integrate OPNsense with your Puppet environment.") }}

    {{ lang._("Keep in mind that you should not treat OPNsense like any other operating system. Most notably you should not modify system files or packages. Instead use the OPNsense API to make configuration changes and to manage plugins. The following tools are a good starting point when trying to automate OPNsense with Puppet:") }}

      -
    • {{ lang._("%sopn-cli:%s A command line client to configure OPNsense core and plugin components through their respective APIs.") | format('', '') }}
    • -
    • {{ lang._("%spuppet/opnsense:%s A read-to-use Puppet module for automating the OPNsense firewall.") | format('', '') }}
    • +
    • {{ lang._("%sopn-cli:%s A command line client to configure OPNsense core and plugin components through their respective APIs.") | format('', '') }}
    • +
    • {{ lang._("%spuppet/opnsense:%s A read-to-use Puppet module for automating the OPNsense firewall.") | format('', '') }}

    {{ lang._("Note that these tools are not directly related to this plugin. Please report issues and missing features directly to the author.") }}

    diff --git a/sysutils/puppet-agent/src/opnsense/scripts/systemhealth/logformats/puppet_agent.py b/sysutils/puppet-agent/src/opnsense/scripts/syslog/logformats/puppet_agent.py similarity index 100% rename from sysutils/puppet-agent/src/opnsense/scripts/systemhealth/logformats/puppet_agent.py rename to sysutils/puppet-agent/src/opnsense/scripts/syslog/logformats/puppet_agent.py diff --git a/sysutils/puppet-agent/src/opnsense/service/templates/OPNsense/PuppetAgent/puppetagent.conf b/sysutils/puppet-agent/src/opnsense/service/templates/OPNsense/PuppetAgent/puppetagent.conf index c375941a97..dc3a3649d0 100644 --- a/sysutils/puppet-agent/src/opnsense/service/templates/OPNsense/PuppetAgent/puppetagent.conf +++ b/sysutils/puppet-agent/src/opnsense/service/templates/OPNsense/PuppetAgent/puppetagent.conf @@ -1,10 +1,20 @@ -{% if helpers.exists('OPNsense.puppetagent.general') and OPNsense.puppetagent.general.Enabled|default("0") == "1" %} [main] -certname = {{ system.hostname|lower }}.{{ system.domain|lower }} -server = {{ OPNsense.puppetagent.general.FQDN|default("") }} -logdest = /var/log/puppet-agent.log -{% if helpers.exists('OPNsense.puppetagent.general') and not helpers.empty('OPNsense.puppetagent.general.Environment') %} -[agent] -environment = {{ OPNsense.puppetagent.general.Environment|default("") }} + certname = {{ system.hostname|lower }}.{{ system.domain|lower }} + logdest = /var/log/puppet-agent.log +{% if OPNsense.puppetagent.general.RunInterval|default("") != "" %} + runinterval = {{ OPNsense.puppetagent.general.RunInterval }} +{% endif %} +{% if OPNsense.puppetagent.general.RunTimeout|default("") != "" %} + runtimeout = {{ OPNsense.puppetagent.general.RunTimeout}} {% endif %} + server = {{ OPNsense.puppetagent.general.FQDN|default("") }} +{% if OPNsense.puppetagent.general.UseCacheOnFailure|default("0") == "1" %} + usecacheonfailure = true +{% else %} + usecacheonfailure = false +{% endif %} + +[agent] +{% if not helpers.empty('OPNsense.puppetagent.general.Environment') %} + environment = {{ OPNsense.puppetagent.general.Environment|default("") }} {% endif %} diff --git a/sysutils/sftp-backup/Makefile b/sysutils/sftp-backup/Makefile new file mode 100644 index 0000000000..f5a6986350 --- /dev/null +++ b/sysutils/sftp-backup/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= sftp-backup +PLUGIN_VERSION= 1.1 +PLUGIN_REVISION= 2 +PLUGIN_COMMENT= Backup configurations using SFTP +PLUGIN_MAINTAINER= ad@opnsense.org +PLUGIN_TIER= 2 + +.include "../../Mk/plugins.mk" diff --git a/sysutils/sftp-backup/pkg-descr b/sysutils/sftp-backup/pkg-descr new file mode 100644 index 0000000000..372f032a00 --- /dev/null +++ b/sysutils/sftp-backup/pkg-descr @@ -0,0 +1,4 @@ +This plugin adds a backup option using SFTP (secure copy). + +Due to the sensitive nature of the data being send to the backup, +we strongly advise to not use a public service to send backups to. diff --git a/sysutils/sftp-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Sftp.php b/sysutils/sftp-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Sftp.php new file mode 100644 index 0000000000..e817c00a73 --- /dev/null +++ b/sysutils/sftp-backup/src/opnsense/mvc/app/library/OPNsense/Backup/Sftp.php @@ -0,0 +1,288 @@ +model = new SftpSettings(); + } + + /** + * @inheritdoc + */ + public function getConfigurationFields() + { + $fields = [ + [ + "name" => "enabled", + "type" => "checkbox", + "label" => gettext("Enable"), + "value" => null + ], + [ + "name" => "url", + "type" => "text", + "label" => gettext("URL"), + "help" => gettext( + "Target location, specified as uri, e.g. sftp://user@my.host.at.domain[:port]//path/to/backup" + ), + "value" => null + ], + [ + "name" => "privkey", + "type" => "passwordarea", + "label" => gettext("SSH private key"), + "help" => gettext("The private key used to setup the connection."), + "value" => null + ], + [ + "name" => "prefixhostname", + "type" => "checkbox", + "label" => gettext("Prefix hostname to backupfile"), + "help" => gettext("Normally the config xml will be written as config-stamp.xml, with this option set " . + "the filename will use the systems host and domain name."), + "value" => null + ], + [ + "name" => "backupcount", + "type" => "text", + "label" => gettext("Backup Count"), + "help" => gettext("Amount of backups to be kept at remote location. Set to 0 to upload latest only without housekeeping"), + "value" => 60 + ], + [ + "name" => "password", + "type" => "password", + "label" => gettext("Encrypt Password"), + "value" => null + ], + [ + "name" => "passwordconfirm", + "type" => "password", + "label" => gettext("Confirm"), + "value" => null + ] + ]; + foreach ($fields as &$field) { + if ($field['name'] == 'passwordconfirm') { + $field['value'] = (string)$this->model->getNodeByReference('password'); + } else { + $field['value'] = (string)$this->model->getNodeByReference($field['name']); + } + } + return $fields; + } + + /** + * @inheritdoc + */ + public function getName() + { + return gettext("sftp"); + } + + /** + * @inheritdoc + */ + public function setConfiguration($conf) + { + $this->setModelProperties($this->model, $conf); + $validation_messages = $this->validateModel($this->model); + if ($conf['passwordconfirm'] != $conf['password']) { + $validation_messages[] = gettext("The supplied 'Password' and 'Confirm' field values must match."); + } + if (empty($validation_messages)) { + $this->model->serializeToConfig(); + Config::getInstance()->save(); + } + return $validation_messages; + } + + /** + * sftp command + * @param string $sftpcmd command to execute + * @return array [stdout|stderr|exit_status] + */ + private function sftpCmd($sftpcmd) + { + $cmd = [ + '/usr/local/bin/sftp', + '-o StrictHostKeyChecking=accept-new', + '-o PasswordAuthentication=no', + '-o ChallengeResponseAuthentication=no', + '-i ' . $this->getIdentity(), + escapeshellarg($this->model->url) + ]; + + $result = ['exit_status' => -1, 'stderr' => '', 'stdout' => '']; + $process = proc_open( + implode(' ', $cmd), + [["pipe", "r"], ["pipe", "w"], ["pipe", "w"]], + $pipes + ); + if (is_resource($process)) { + fwrite($pipes[0], $sftpcmd); + fclose($pipes[0]); + $result['stdout'] = stream_get_contents($pipes[1]); + fclose($pipes[1]); + $result['stderr'] = stream_get_contents($pipes[2]); + fclose($pipes[2]); + $result['exit_status'] = proc_close($process); + } + if ($result['exit_status'] !== 0) { + /* always throw on non zero exit status */ + syslog(LOG_ERR, "sftp-backup error (" . str_replace("\n", " ", $result['stderr']) . ")"); + throw new \Exception($result['stderr']); + } + return $result; + } + + /** + * @return identity file, create new when non existent + */ + private function getIdentity() + { + $confdir = "/conf/backup/sftp"; + $identfile = $confdir . '/identity'; + if (!is_dir($confdir)) { + mkdir($confdir); + } + $this_key = trim(str_replace("\r", "", $this->model->privkey)) . "\n"; + if (!is_file($identfile) || file_get_contents($identfile) != $this_key) { + File::file_put_contents($identfile, $this_key, 0600); + } + return $identfile; + } + + /** + * @return list of files on remote location + */ + private function ls($pattern = '') + { + $result = []; + foreach (explode("\n", $this->sftpCmd('ls -lnt ' . $pattern)['stdout']) as $line) { + $parts = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY); + if (count($parts) >= 7) { + $result[] = $parts[count($parts) - 1]; + } + } + return $result; + } + + /** + * @param string $source filename + * @param string $destination filename + */ + private function put($source, $destination) + { + $this->sftpCmd(sprintf('put %s %s', $source, $destination)); + } + + /** + * @param string $filename + */ + private function del($filename) + { + $this->sftpCmd(sprintf('rm %s', $filename)); + } + + /** + * @return array filelist + */ + public function backup() + { + $cnf = Config::getInstance(); + if (!$this->model->enabled->isEmpty() && $cnf->isValid()) { + if ($this->model->prefixhostname->isEmpty()) { + $fileprefix = "config-"; + } else { + $config = $cnf->object(); + $fileprefix = strtolower(sprintf('%s.%s-', (string)$config->system->hostname, (string)$config->system->domain)); + } + /** + * Collect most recent backup, since /conf/backup/ always contains the latests, we can use the filename + * for easy comparison. + **/ + $all_backups = glob('/conf/backup/config-*.xml'); + $most_recent = $all_backups[count($all_backups) - 1]; + $confdata = file_get_contents($most_recent); + if (!$this->model->password->isEmpty()) { + $confdata = $this->encrypt($confdata, (string)$this->model->password); + } + $remote_backups = $this->ls(sprintf('%s*.xml', $fileprefix)); + $target_filename = strtolower(preg_replace('/^config-/', $fileprefix, basename($most_recent))); + + if (!in_array($target_filename, $remote_backups)) { + syslog(LOG_NOTICE, "backup configuration as " . $target_filename); + $tmpfilename = sprintf("/conf/backup/sftp/%s", $target_filename); + File::file_put_contents($tmpfilename, $confdata, 0600); + $this->put($tmpfilename, $target_filename); + unlink($tmpfilename); + $remote_backups = $this->ls(sprintf('%s*.xml', $fileprefix)); + } + /* cleanup only if backup count is > 0*/ + if ($this->model->backupcount->asFloat() > 0) { + rsort($remote_backups); + if (count($remote_backups) > (int)$this->model->backupcount->getValue()) { + for ($i = $this->model->backupcount->getValue(); $i < count($remote_backups); $i++) { + $this->del($remote_backups[$i]); + } + $remote_backups = $this->ls(sprintf('%s*.xml', $fileprefix)); + } + return $remote_backups; + } else { + return $this->ls(sprintf('%s*.xml', $fileprefix)) ?: []; + } + } else { + /* disabled */ + return; + } + } + + /** + * @inheritdoc + */ + public function isEnabled() + { + return !$this->model->enabled->isEmpty(); + } +} diff --git a/sysutils/sftp-backup/src/opnsense/mvc/app/models/OPNsense/Backup/SftpSettings.php b/sysutils/sftp-backup/src/opnsense/mvc/app/models/OPNsense/Backup/SftpSettings.php new file mode 100644 index 0000000000..4cad4acf76 --- /dev/null +++ b/sysutils/sftp-backup/src/opnsense/mvc/app/models/OPNsense/Backup/SftpSettings.php @@ -0,0 +1,39 @@ + + //system/backup/sftp + 1.0.0 + OPNsense sftp Backup Settings + + + 0 + Y + + + privkey.check001 + + + url.check001 + + + + + N + /^((sftp))?:\/\/.*[^\/]$/ + A valid location must be provided. + + + A backup location (url) is required. + DependConstraint + + enabled + + + + + + N + + + A private key is required. + DependConstraint + + enabled + + + + + + + + 60 + Y + 0 + + + 0 + N + + + diff --git a/sysutils/smart/Makefile b/sysutils/smart/Makefile index ccc3474b27..d09cf52331 100644 --- a/sysutils/smart/Makefile +++ b/sysutils/smart/Makefile @@ -1,7 +1,6 @@ PLUGIN_NAME= smart -PLUGIN_VERSION= 2.2 +PLUGIN_VERSION= 2.4 PLUGIN_COMMENT= SMART tools PLUGIN_DEPENDS= smartmontools -PLUGIN_MAINTAINER= franco@opnsense.org .include "../../Mk/plugins.mk" diff --git a/sysutils/smart/src/opnsense/mvc/app/controllers/OPNsense/Smart/Api/ServiceController.php b/sysutils/smart/src/opnsense/mvc/app/controllers/OPNsense/Smart/Api/ServiceController.php index 874ba272f2..2f039c551f 100644 --- a/sysutils/smart/src/opnsense/mvc/app/controllers/OPNsense/Smart/Api/ServiceController.php +++ b/sysutils/smart/src/opnsense/mvc/app/controllers/OPNsense/Smart/Api/ServiceController.php @@ -43,10 +43,15 @@ private function getDevices() return $devices; } - public function listAction() + public function listAction($details = null) { if ($this->request->isPost()) { - return array("devices" => $this->getDevices()); + $backend = new Backend(); + + $devices = empty($details) ? $this->getDevices() : + json_decode(trim($backend->configdRun('smart detailed list')), true); + + return ['devices' => $devices]; } return array("message" => "Unable to run list action"); @@ -63,7 +68,7 @@ public function infoAction() return array("message" => "Invalid device name"); } - $valid_info_types = array("i", "H", "c", "A", "a"); + $valid_info_types = array("i", "H", "c", "A", "a", "x"); if (!in_array($type, $valid_info_types)) { return array("message" => "Invalid info type"); diff --git a/sysutils/smart/src/opnsense/mvc/app/models/OPNsense/Smart/ACL/ACL.xml b/sysutils/smart/src/opnsense/mvc/app/models/OPNsense/Smart/ACL/ACL.xml index d728c58976..d7fb0e73c5 100644 --- a/sysutils/smart/src/opnsense/mvc/app/models/OPNsense/Smart/ACL/ACL.xml +++ b/sysutils/smart/src/opnsense/mvc/app/models/OPNsense/Smart/ACL/ACL.xml @@ -3,7 +3,7 @@ Services: SMART ui/smart/* - api/smart/* + api/smart/service/* diff --git a/sysutils/smart/src/opnsense/mvc/app/views/OPNsense/Smart/index.volt b/sysutils/smart/src/opnsense/mvc/app/views/OPNsense/Smart/index.volt index 7a7beb14dd..1e770f836e 100644 --- a/sysutils/smart/src/opnsense/mvc/app/views/OPNsense/Smart/index.volt +++ b/sysutils/smart/src/opnsense/mvc/app/views/OPNsense/Smart/index.volt @@ -32,9 +32,9 @@ // Highlights the words "PASSED", "FAILED", and "WARNING". function add_colors(text) { return text - .replace(/PASSED/g, '{{ lang._('PASSED') }}') - .replace(/FAILED/g, '{{ lang._('FAILED') }}') - .replace(/WARNING/g, '{{ lang._('WARNING') }}'); + .replace(/\bPASSED\b/g, '{{ lang._('PASSED') }}') + .replace(/\bFAILED\b/g, '{{ lang._('FAILED') }}') + .replace(/\bWARNING\b/g, '{{ lang._('WARNING') }}'); }; // Appends options to select device. @@ -138,7 +138,8 @@       - +   +
    diff --git a/sysutils/smart/src/opnsense/scripts/OPNsense/Smart/detailed_list.sh b/sysutils/smart/src/opnsense/scripts/OPNsense/Smart/detailed_list.sh index d8a3b55c18..7f0a8a5042 100755 --- a/sysutils/smart/src/opnsense/scripts/OPNsense/Smart/detailed_list.sh +++ b/sysutils/smart/src/opnsense/scripts/OPNsense/Smart/detailed_list.sh @@ -30,9 +30,9 @@ RESULT= for DEV in $(sysctl -n kern.disks); do IDENT=$(/usr/sbin/diskinfo -s ${DEV}) - if [ "${DEV#nvd}" != "${DEV}" ]; then - # the disk formerly know as nvdX - DEV="nvme${DEV#nvd}" + if [ "${DEV#nda}" != "${DEV}" ]; then + # the disk formerly know as ndaX + DEV="nvme${DEV#nda}" fi STATE=$(/usr/local/sbin/smartctl -jH /dev/${DEV}) diff --git a/sysutils/smart/src/opnsense/service/conf/actions.d/actions_smart.conf b/sysutils/smart/src/opnsense/service/conf/actions.d/actions_smart.conf index 7085815254..b24bc6d56f 100644 --- a/sysutils/smart/src/opnsense/service/conf/actions.d/actions_smart.conf +++ b/sysutils/smart/src/opnsense/service/conf/actions.d/actions_smart.conf @@ -1,5 +1,5 @@ [list] -command:sysctl -n kern.disks | sed s:nvd:nvme:g +command:sysctl -n kern.disks | sed s:nda:nvme:g parameters: type:script_output message:list installed devices @@ -12,59 +12,68 @@ message:list installed devices [info] command:/usr/local/sbin/smartctl -parameters:-%s %s; exit 0 +parameters:-%s %s +errors:no type:script_output message:exec smartctl -%s for device %s [info_json] command:/usr/local/sbin/smartctl -parameters:-%s --json=c %s; exit 0 +parameters:-%s --json=c %s +errors:no type:script_output message:exec smartctl -%s (JSON) for device %s [log.error] command:/usr/local/sbin/smartctl -l error -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Get error log for device %s [log.selftest] command:/usr/local/sbin/smartctl -l selftest -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Get selftest log for device %s [test.offline] command:/usr/local/sbin/smartctl -t offline -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Testing device %s (offline) description:Run SMART test (offline) [test.short] command:/usr/local/sbin/smartctl -t short -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Testing device %s (short) description:Run SMART test (short) [test.long] command:/usr/local/sbin/smartctl -t long -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Testing device %s (long) description:Run SMART test (long) [test.conveyance] command:/usr/local/sbin/smartctl -t conveyance -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Testing device %s (conveyance) description:Run SMART test (conveyance) [abort] command:/usr/local/sbin/smartctl -X -parameters:%s; exit 0 +parameters:%s +errors:no type:script_output message:Abort test on device %s description:Abort SMART tests diff --git a/sysutils/smart/src/opnsense/www/js/widgets/Metadata/Smart.xml b/sysutils/smart/src/opnsense/www/js/widgets/Metadata/Smart.xml new file mode 100644 index 0000000000..eb36c75fe9 --- /dev/null +++ b/sysutils/smart/src/opnsense/www/js/widgets/Metadata/Smart.xml @@ -0,0 +1,14 @@ + + + Smart.js + /ui/smart + + /api/smart/service/* + + + SMART Status + Error fetching disk list + Error fetching SMART info for device + + + diff --git a/sysutils/smart/src/opnsense/www/js/widgets/Smart.js b/sysutils/smart/src/opnsense/www/js/widgets/Smart.js new file mode 100644 index 0000000000..30b2d5986e --- /dev/null +++ b/sysutils/smart/src/opnsense/www/js/widgets/Smart.js @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 Francisco Dimattia + * 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. + */ + +export default class Smart extends BaseTableWidget { + constructor() { + super(); + this.tickTimeout = 300; + this.disks = null; + } + + getMarkup() { + const $container = $('
    '); + const $smarttable = this.createTable('smart-table', { + headerPosition: 'left', + }); + $container.append($smarttable); + return $container; + } + + async onWidgetTick() { + let disks = await this.ajaxCall(`/api/smart/service/${'list/detailed'}`, {}, 'POST'); + if (disks && disks.devices) { + const rows = []; + for (const device of disks.devices) { + try { + const health = device.state.smart_status.passed; + const text = health ? "OK" : "FAILED"; + const css = { color: health ? "green" : "red", fontSize: '150%' }; + const field = $(``).text(text).css(css).prop('outerHTML'); + rows.push([[device.device], field]); + } catch (error) { + super.updateTable('smart-table', [[["Error"], $(`${this.translations.nosmart} ${device.device}: ${error}`).prop('outerHTML')]]); + } + } + super.updateTable('smart-table', rows); + } + } +} diff --git a/sysutils/smart/src/www/widgets/include/smart_status.inc b/sysutils/smart/src/www/widgets/include/smart_status.inc deleted file mode 100644 index 5080309907..0000000000 --- a/sysutils/smart/src/www/widgets/include/smart_status.inc +++ /dev/null @@ -1,4 +0,0 @@ - - - - - - - - - -state->smart_status->passed)) { - if ($dev->state->smart_status->passed) { - $dev_state_translated = gettext('OK'); - $color = 'success'; - } else { - $dev_state_translated = gettext('FAILED'); - $color = 'danger'; - } - } - -?> - - - - - - - - - - -
    - - -
    device) ?>ident) ?>
    diff --git a/sysutils/virtualbox/Makefile b/sysutils/virtualbox/Makefile index 19a947dbc8..c8025aada2 100644 --- a/sysutils/virtualbox/Makefile +++ b/sysutils/virtualbox/Makefile @@ -2,7 +2,7 @@ PLUGIN_NAME= virtualbox PLUGIN_VERSION= 1.0 PLUGIN_REVISION= 1 PLUGIN_COMMENT= VirtualBox guest additions -PLUGIN_DEPENDS= virtualbox-ose-additions-nox11 +PLUGIN_DEPENDS= virtualbox-ose-additions-nox11-72 PLUGIN_MAINTAINER= franco@opnsense.org .include "../../Mk/plugins.mk" diff --git a/sysutils/vmware/Makefile b/sysutils/vmware/Makefile index 307d0f9bf7..92bf19ab64 100644 --- a/sysutils/vmware/Makefile +++ b/sysutils/vmware/Makefile @@ -4,5 +4,6 @@ PLUGIN_REVISION= 1 PLUGIN_COMMENT= VMware tools PLUGIN_DEPENDS= open-vm-tools-nox11 PLUGIN_MAINTAINER= franco@opnsense.org +PLUGIN_TIER= 2 .include "../../Mk/plugins.mk" diff --git a/vendor/sunnyvalley/Makefile b/vendor/sunnyvalley/Makefile index 6d082409e3..7b8e3158fb 100644 --- a/vendor/sunnyvalley/Makefile +++ b/vendor/sunnyvalley/Makefile @@ -1,9 +1,9 @@ PLUGIN_NAME= sunnyvalley -PLUGIN_VERSION= 1.2 -PLUGIN_REVISION= 1 -PLUGIN_COMMENT= Vendor repository for Sensei (Next Generation Firewall Extensions) -PLUGIN_MAINTAINER= opensource@sunnyvalley.io -PLUGIN_WWW= https://www.sunnyvalley.io -PLUGIN_DEPENDS= ${PLUGIN_FLAVOUR:tl} +PLUGIN_VERSION= 1.5 +PLUGIN_REVISION= 2 +PLUGIN_COMMENT= Vendor Repository for Zenarmor - Enterprise SASE & SSE platform (NGFW, SWG, CASB, ZTNA, SD-WAN) +PLUGIN_MAINTAINER= oss@zenarmor.com +PLUGIN_WWW= https://www.zenarmor.com +PLUGIN_TIER= 2 .include "../../Mk/plugins.mk" diff --git a/vendor/sunnyvalley/pkg-descr b/vendor/sunnyvalley/pkg-descr index 6f3b0fa84d..57e5012d9d 100644 --- a/vendor/sunnyvalley/pkg-descr +++ b/vendor/sunnyvalley/pkg-descr @@ -1,18 +1,26 @@ -This plugin adds a proprietary repository to install Sensei, a plugin -for OPNsense, complementing the firewall with state of the art -next generation firewall features. +This is the vendor repository package, please also install the actual +os-sensei package which delivers the below-mentioned capabilities. -You will need to install os-sensei plugin after installing this repo plugin. +Zenarmor is an enterprise network security and secure access platform, +delivering advanced threat protection, content filtering, application-aware +inspection, identity-based policy enforcement, and high-performance secure +connectivity in a single, plug-and-secure deployment. -Sensei features: +Zenarmor integrates instantly and directly with OPNsense and can also be +centrally managed through Zenconsole, enabling centralized policy management, +multi-tenant administration, and identity provider (IdP) integration for +consistent security enforcement across your organization's networks. -* Application Control -* Advanced Network Analytics -* All-ports Full TLS Inspection -* Cloud Threat Intelligence -* Web Security & Web Filtering -* User based filtering -* Policy based filtering -* Active Directory/LDAP Integration +Features and capabilities include: -WWW: https://www.sunnyvalley.io/sensei +* Secure Web Gateway (Application and Web Traffic Control) +* Zero-Trust Network Access (ZTNA) +* Full Mesh Business VPN/SD-WAN with micro-segmentation +* Advanced Network Analytics and Integrated Threat Intelligence +* Cloud Access Security Broker (CASB) - Granular Cloud Application Control +* Automated threat blocking using real-time threat intelligence +* Full TLS inspection across all ports +* Advanced Content Inspection (e.g. File Scanning, Deep DNS Inspection) +* User-, group-, and identity-based policy enforcement (IdP, AD / LDAP) +* Identity and Access Management & Single Sign-on (SSO, Entra ID, Okta etc.) +* Centralized policy & configuration management and multi-tenant administration diff --git a/vendor/sunnyvalley/src/etc/pkg/repos/SunnyValley.conf.in b/vendor/sunnyvalley/src/etc/pkg/repos/SunnyValley.conf.in deleted file mode 100644 index 7b4ba66e73..0000000000 --- a/vendor/sunnyvalley/src/etc/pkg/repos/SunnyValley.conf.in +++ /dev/null @@ -1,7 +0,0 @@ -SunnyValley: { - fingerprints: "/usr/local/etc/pkg/fingerprints/SunnyValley", - url: "https://updates.sunnyvalley.io/opnsense/${ABI}/%%PLUGIN_ABI%%/%%PLUGIN_FLAVOUR%%/latest", - signature_type: "fingerprints", - priority: 7, - enabled: yes -} diff --git a/vendor/sunnyvalley/src/etc/pkg/repos/SunnyValley.conf.shadow.in b/vendor/sunnyvalley/src/etc/pkg/repos/SunnyValley.conf.shadow.in new file mode 100644 index 0000000000..8aced05ba6 --- /dev/null +++ b/vendor/sunnyvalley/src/etc/pkg/repos/SunnyValley.conf.shadow.in @@ -0,0 +1,7 @@ +SunnyValley: { + fingerprints: "/usr/local/etc/pkg/fingerprints/SunnyValley", + url: "https://updates.zenarmor.net/opnsense/${ABI}/%%PLUGIN_ABI%%/latest", + signature_type: "fingerprints", + priority: 7, + enabled: yes +} diff --git a/vendor/sunnyvalley/src/opnsense/scripts/firmware/repos/SunnyValley.php b/vendor/sunnyvalley/src/opnsense/scripts/firmware/repos/SunnyValley.php new file mode 100755 index 0000000000..30c902a561 --- /dev/null +++ b/vendor/sunnyvalley/src/opnsense/scripts/firmware/repos/SunnyValley.php @@ -0,0 +1,47 @@ +#!/usr/local/bin/php + + * 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. + */ + +require_once 'util.inc'; + +$conf = '/usr/local/etc/pkg/repos/SunnyValley.conf'; + +if (!file_exists($conf . '.sample')) { + exit(0); +} + +$fileContents = file_get_contents($conf . '.sample'); + +if (file_exists('/usr/local/zenarmor/bin/eastpect')) { + $uuid = shell_safe('/usr/local/zenarmor/bin/eastpect -s'); + if ($uuid != '') { + $fileContents = str_replace('/latest"', '/' . $uuid . '"', $fileContents); + } +} + +file_put_contents($conf, $fileContents); diff --git a/www/OPNProxy/+POST_DEINSTALL.post b/www/OPNProxy/+POST_DEINSTALL.post new file mode 100644 index 0000000000..5630e0114a --- /dev/null +++ b/www/OPNProxy/+POST_DEINSTALL.post @@ -0,0 +1,2 @@ +#!/bin/sh +rm /usr/local/etc/squid/auth/10-opnproxy-ext.auth.conf diff --git a/www/OPNProxy/Makefile b/www/OPNProxy/Makefile new file mode 100644 index 0000000000..d134b25984 --- /dev/null +++ b/www/OPNProxy/Makefile @@ -0,0 +1,9 @@ +PLUGIN_NAME= OPNProxy +PLUGIN_VERSION= 1.0.5 +PLUGIN_REVISION= 4 +PLUGIN_COMMENT= OPNsense proxy additions +PLUGIN_DEPENDS= os-redis${PLUGIN_PKGSUFFIX} \ + os-squid${PLUGIN_PKGSUFFIX} \ + py${PLUGIN_PYTHON}-redis + +.include "../../Mk/plugins.mk" diff --git a/www/OPNProxy/pkg-descr b/www/OPNProxy/pkg-descr new file mode 100644 index 0000000000..c5033a435d --- /dev/null +++ b/www/OPNProxy/pkg-descr @@ -0,0 +1,9 @@ +OPNsense proxy additions to support more fine grained access management + +1.0.5 + +* Prepare for community release + +1.0.4: + +* Remove ident support as by default it is denied anyway nowadays diff --git a/www/OPNProxy/src/etc/inc/plugins.inc.d/opnproxy.inc b/www/OPNProxy/src/etc/inc/plugins.inc.d/opnproxy.inc new file mode 100644 index 0000000000..bf63709460 --- /dev/null +++ b/www/OPNProxy/src/etc/inc/plugins.inc.d/opnproxy.inc @@ -0,0 +1,49 @@ + ['opnproxy_user_changed:2'], + 'webproxy' => ['opnproxy_webproxy:2'], + ]; +} + +function opnproxy_user_changed($unused, $username = '') +{ + mwexecf('/usr/local/opnsense/scripts/OPNProxy/redis_sync_users.py %', $username); +} + +function opnproxy_webproxy($verbose = false, $action = null) +{ + $response = configd_run('template reload Deciso/Proxy'); + + if ($verbose) { + printf("template reload Deciso/Proxy: %s\n", trim($response)); + } +} diff --git a/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/AclController.php b/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/AclController.php new file mode 100644 index 0000000000..c0be1fa08c --- /dev/null +++ b/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/AclController.php @@ -0,0 +1,40 @@ +view->pick('Deciso/Proxy/acl'); + $this->view->formDialogDefaultPolicy = $this->getForm("dialogDefaultPolicy"); + $this->view->formDialogCustomPolicy = $this->getForm("dialogCustomPolicy"); + } +} diff --git a/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/AclController.php b/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/AclController.php new file mode 100644 index 0000000000..4af0a6586b --- /dev/null +++ b/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/AclController.php @@ -0,0 +1,129 @@ +searchBase("policies.policy", array('enabled', 'description', 'action'), "description"); + } + + public function setPolicyAction($uuid) + { + return $this->setBase("policy", "policies.policy", $uuid); + } + + public function addPolicyAction() + { + return $this->addBase("policy", "policies.policy"); + } + + public function getPolicyAction($uuid = null) + { + return $this->getBase("policy", "policies.policy", $uuid); + } + + public function delPolicyAction($uuid) + { + return $this->delBase("policies.policy", $uuid); + } + + public function togglePolicyAction($uuid, $enabled = null) + { + return $this->toggleBase("policies.policy", $uuid, $enabled); + } + public function searchCustomPolicyAction() + { + return $this->searchBase("custom_policies.policy", array('enabled', 'description', 'action'), "description"); + } + + public function setCustomPolicyAction($uuid) + { + return $this->setBase("custom_policy", "custom_policies.policy", $uuid); + } + + public function addCustomPolicyAction() + { + return $this->addBase("custom_policy", "custom_policies.policy"); + } + + public function getCustomPolicyAction($uuid = null) + { + return $this->getBase("custom_policy", "custom_policies.policy", $uuid); + } + + public function delCustomPolicyAction($uuid) + { + return $this->delBase("custom_policies.policy", $uuid); + } + + public function toggleCustomPolicyAction($uuid, $enabled = null) + { + return $this->toggleBase("custom_policies.policy", $uuid, $enabled); + } + + public function applyAction() + { + if ($this->request->isPost()) { + $backend = new Backend(); + $backend->configdRun('template reload Deciso/Proxy'); + $backend->configdRun('opnproxy sync_users'); + return array("status" => trim($backend->configdRun('opnproxy apply_policies'))); + } else { + return array("status" => "error"); + } + } + + public function testAction() + { + if ($this->request->isPost() && $this->request->hasPost('uri')) { + $src = $this->request->getPost('src', 'striptags', ''); + $src = !empty($src) ? $src : "-"; + $user = $this->request->getPost('user', null, ''); + $user = !empty($user) ? $user : "-"; + $backend = new Backend(); + $response = $backend->configdpRun('opnproxy user test', [ + $user, $this->request->getPost('uri'), $src + ]); + $respose = json_decode($response, true); + if (!empty($response)) { + return $respose; + } + } + return array("status" => "error"); + } +} diff --git a/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/dialogCustomPolicy.xml b/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/dialogCustomPolicy.xml new file mode 100644 index 0000000000..f7ad32c89b --- /dev/null +++ b/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/dialogCustomPolicy.xml @@ -0,0 +1,40 @@ +
    + + custom_policy.enabled + + checkbox + Enable this item + + + custom_policy.applies_on + + select_multiple + ACL applies on selected users and groups. Users are prefixed with *, best use groups to structure policies + + + custom_policy.source_net + + select_multiple + + true + source ip or network, examples 10.0.0.0/24, 10.0.0.1 + + + custom_policy.action + + dropdown + Action to perform. + + + custom_policy.content + + textbox + List of domains and path entries, prefix with . to include subdomains (e.g. .com to block all .com domains). To match all use * + true + + + custom_policy.description + + text + +
    diff --git a/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/dialogDefaultPolicy.xml b/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/dialogDefaultPolicy.xml new file mode 100644 index 0000000000..275b6b6576 --- /dev/null +++ b/www/OPNProxy/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/dialogDefaultPolicy.xml @@ -0,0 +1,39 @@ +
    + + policy.enabled + + checkbox + Enable this item + + + policy.applies_on + + select_multiple + ACL applies on selected users and groups. Users are prefixed with *, best use groups to structure policies + + + policy.source_net + + select_multiple + + true + source ip or network, examples 10.0.0.0/24, 10.0.0.1 + + + policy.action + + dropdown + Action to perform. + + + policy.content + + select_multiple + List of standard categories + + + policy.description + + text + +
    diff --git a/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/ACL.php b/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/ACL.php new file mode 100644 index 0000000000..3f5bfa84f7 --- /dev/null +++ b/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/ACL.php @@ -0,0 +1,35 @@ + + //Deciso/Proxy/ACL + 1.0.0 + + OPNsense central management / Proxy module + + + + + + 1 + Y + + + Y + deny + + Deny + Allow + + + + Y + Y + + adult + aggressive + astrology + audio-video + bank + bitcoin + blog + celebrity + chat + child + cleaning + cooking + cryptojacking + dangerous_material + dating + ddos + doh + download + drugs + educational_games + filehosting + financial + forums + gambling + games + hacking + jobsearch + lingerie + malware + manga + marketingware + mixed_adult + mobile-phone + phishing + press + advertisements + radio + redirector + remote-control + sexual_education + shopping + shortener + social_networks + sports + stalkerware + translation + update + vpn + warez + webmail + + + + Y + N + You need to select at least one user or group for who this list applies + + + N + N + , + Y + + + Y + /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){1,255}$/u + Description should be a string between 1 and 255 characters + + + + + + + 1 + Y + + + Y + deny + + Deny + Allow + + + + Y + + + Y + N + You need to select at least one user or group for who this list applies + + + N + N + , + Y + + + Y + /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){1,255}$/u + Description should be a string between 1 and 255 characters + + + + + diff --git a/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/FieldTypes/CustomPolicyField.php b/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/FieldTypes/CustomPolicyField.php new file mode 100644 index 0000000000..15637602a2 --- /dev/null +++ b/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/FieldTypes/CustomPolicyField.php @@ -0,0 +1,94 @@ +separatorchar, trim($data)) as $value) { + yield $value; + } + } + + /** + * retrieve field validators for this field type + * @return array + */ + public function getValidators() + { + $validators = parent::getValidators(); + if ($this->internalValue != null) { + $validators[] = new CallbackValidator(["callback" => function ($data) { + $messages = array(); + foreach ($this->getItems($data) as $item) { + $parts = explode("/", $item, 2); + $domain = substr($parts[0], 0, 1) == "." ? substr($parts[0], 1) : $parts[0]; + if ($item == "*") { + // explicit wildcard + continue; + } elseif ( + filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false && + filter_var($domain, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === false + ) { + $messages[] = sprintf( + gettext('Entry "%s" does not contain a valid domain or address.'), + $item + ); + } elseif (filter_var("https://{$domain}", FILTER_VALIDATE_URL) === false) { + $messages[] = sprintf( + gettext('Entry "%s" does not contain a valid path.'), + $item + ); + continue; + } + } + return $messages; + } + ]); + } + return $validators; + } +} diff --git a/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/FieldTypes/UserGroupField.php b/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/FieldTypes/UserGroupField.php new file mode 100644 index 0000000000..b50ce32192 --- /dev/null +++ b/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/FieldTypes/UserGroupField.php @@ -0,0 +1,76 @@ +optionSetId(); + if (!isset(self::$internalCacheOptionList[$setid])) { + self::$internalCacheOptionList[$setid] = array(); + } + if (empty(self::$internalCacheOptionList[$setid])) { + $cnf = Config::getInstance()->object(); + foreach (['group', 'user'] as $topic) { + if (!empty($cnf->system->$topic)) { + foreach ($cnf->system->$topic as $node) { + $prefix = $topic == "user" ? "*" : ""; + $tp = $topic == "user" ? "u" : "g"; + self::$internalCacheOptionList[$setid][$tp . ":" . $node->name] = $prefix . $node->name; + } + } + } + ksort(self::$internalCacheOptionList[$setid]); + } + $this->internalOptionList = self::$internalCacheOptionList[$setid]; + } +} diff --git a/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/Menu/Menu.xml b/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/Menu/Menu.xml new file mode 100644 index 0000000000..54400fe64e --- /dev/null +++ b/www/OPNProxy/src/opnsense/mvc/app/models/Deciso/Proxy/Menu/Menu.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/www/OPNProxy/src/opnsense/mvc/app/views/Deciso/Proxy/acl.volt b/www/OPNProxy/src/opnsense/mvc/app/views/Deciso/Proxy/acl.volt new file mode 100644 index 0000000000..1abc4ff78a --- /dev/null +++ b/www/OPNProxy/src/opnsense/mvc/app/views/Deciso/Proxy/acl.volt @@ -0,0 +1,191 @@ + + + + +
    +
    + + + + + + + + + + + + + + + + + + + +
    {{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Description') }}{{ lang._('Action') }}{{ lang._('Commands') }}
    + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + +
    {{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Description') }}{{ lang._('Action') }}{{ lang._('Commands') }}
    + + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{ lang._('Property') }}{{ lang._('Value') }}
    {{ lang._('Username') }}
    {{ lang._('Source') }}
    {{ lang._('Uri') }}
    + +
    +
    +
    + +
    +
    +
    + +
    + +

    +
    +
    + + +{{ partial("layout_partials/base_dialog",['fields':formDialogDefaultPolicy,'id':'DialogDefaultPolicy','label':lang._('Edit List')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogCustomPolicy,'id':'DialogCustomPolicy','label':lang._('Edit List')])}} diff --git a/www/OPNProxy/src/opnsense/scripts/OPNProxy/download_cleanse_ut1.py b/www/OPNProxy/src/opnsense/scripts/OPNProxy/download_cleanse_ut1.py new file mode 100755 index 0000000000..75cf4d94e6 --- /dev/null +++ b/www/OPNProxy/src/opnsense/scripts/OPNProxy/download_cleanse_ut1.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# coding=utf-8 +""" + Copyright (c) 2023 Ad Schellevis + 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. +""" +import argparse +import os +import shutil +import sys +import tempfile +import tarfile +import io +import requests + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('filename', help='output filename') + cmd_args = parser.parse_args() + + req_opts = { + 'url': 'http://dsi.ut-capitole.fr/blacklists/download/blacklists.tar.gz', + 'timeout': 120, + 'stream': True + } + try: + req = requests.get(**req_opts) + except Exception as e: + print("unable to download %s" % req_opts['url']) + sys.exit(99) + + directory_map = { + 'blacklists/agressif': 'blacklists/aggressive', + 'blacklists/publicite': 'blacklists/advertisements', + 'blacklists/drogue': 'blacklists/drugs', + 'blacklists/tricheur': None, + 'blacklists/arjel': None, + 'blacklists/associations_religieuses': None, + 'blacklists/dialer': None, + 'blacklists/liste_bu': None, + 'blacklists/reaffected': None, + 'blacklists/strict_redirector': None, + 'blacklists/strong_redirector': None, + 'blacklists/sect': None, + + } + filenames = ['urls', 'domains', 'README', 'global_usage', 'cc-by-sa-4-0.pdf', 'LICENSE.pdf'] + + if 200 <= req.status_code <= 299: + with tempfile.NamedTemporaryFile() as tmp_stream: + shutil.copyfileobj(req.raw, tmp_stream) + tmp_stream.seek(0) + tf = tarfile.open(fileobj=tmp_stream) + with tarfile.open(cmd_args.filename, "w:gz") as tar_handle: + for tf_file in tf.getmembers(): + filename = os.path.basename(tf_file.name) + if tf_file.isreg() and filename in filenames: + target = tf_file.name + dirname = os.path.dirname(tf_file.name) + if dirname in directory_map: + if directory_map[dirname] is None: + continue + else: + target = "%s/%s" % (directory_map[dirname], filename) + fhandle = tf.extractfile(tf_file) + info = tarfile.TarInfo(target) + fhandle.seek(0, io.SEEK_END) + info.size = fhandle.tell() + fhandle.seek(0, io.SEEK_SET) + tar_handle.addfile(info, fhandle) + + tar_handle.close() diff --git a/www/OPNProxy/src/opnsense/scripts/OPNProxy/lib/__init__.py b/www/OPNProxy/src/opnsense/scripts/OPNProxy/lib/__init__.py new file mode 100755 index 0000000000..95988b247e --- /dev/null +++ b/www/OPNProxy/src/opnsense/scripts/OPNProxy/lib/__init__.py @@ -0,0 +1,135 @@ +""" + Copyright (c) 2023 Ad Schellevis + 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. +""" +import copy +import tarfile +import os +import stat +import syslog +import time +import requests +from configparser import ConfigParser + + +class Policy: + def __init__(self, policy_filename): + self._policy_config = policy_filename + self._domain_entries = dict() + self._policy_settings = dict() + self._tf = None + self.load() + + def load(self): + """ load policy database + :return: + """ + self._domain_entries = dict() + self._policy_settings = dict() + # collect all policies per domain, so we can safely overwrite existing content when it exists + cnf = ConfigParser() + cnf.read(self._policy_config) + if cnf.has_section('source'): + blocklist_filename = cnf.get('source', 'blocklist') + if cnf.has_option('source', 'blocklist_download_uri'): + blocklist_ttl = cnf.getint('source', 'blocklist_ttl') + if not os.path.isfile(blocklist_filename) or \ + time.time() - os.stat(blocklist_filename)[stat.ST_MTIME] > blocklist_ttl: + try: + response = requests.get(cnf.get('source', 'blocklist_download_uri'), stream=True) + response.raise_for_status() + with open(blocklist_filename, 'wb') as handle: + for block in response.iter_content(1024): + handle.write(block) + except requests.exceptions.RequestException as e: + # we are unable to download a new blocklist, if a previous version still exists keep using that + syslog.syslog(syslog.LOG_ERR, 'unable to download new blocklist (%s)' % e) + + if os.path.isfile(blocklist_filename) and tarfile.is_tarfile(blocklist_filename): + self._tf = tarfile.open(fileobj=open(blocklist_filename, "rb")) + else: + syslog.syslog(syslog.LOG_ERR, 'default policy rules not available (%s missing)' % blocklist_filename) + + for section in cnf.sections(): + if cnf.has_option(section, 'policy_type') and cnf.has_option(section, 'content'): + self._policy_settings[section] = { + 'action': cnf.get(section, 'action'), + 'id': section.split('_', 1)[-1], + 'applies_on': cnf.get(section, 'applies_on').split(','), + 'source_net': cnf.get(section, 'source_net').split(','), + 'policy_type': cnf.get(section, 'policy_type'), + 'description': cnf.get(section, 'description') + } + ittr_method = self._itr_default if cnf.get(section, 'policy_type') == "default" else self._itr_custom + split_char = ',' if cnf.get(section, 'policy_type') == "default" else '\n' + for is_wildcard, item in ittr_method(cnf.get(section, 'content').split(split_char)): + parts = item.split('/', 1) + domain = parts[0] + if domain not in self._domain_entries: + self._domain_entries[domain] = list() + self._domain_entries[domain].append([ + section, + "/%s" % parts[1] if len(parts) > 1 else "/", + is_wildcard + ]) + + def _itr_default(self, items: list): + if self._tf: + for tf_file in self._tf.getmembers(): + if tf_file.isreg(): + fhandle = self._tf.extractfile(tf_file) + if tf_file.name.count('/') >= 2 and tf_file.name.split('/')[-2] in items: + filename = os.path.basename(tf_file.name) + if filename in ['urls', 'domains']: + for line in fhandle.read().decode().split('\n'): + line = line.strip() + if line: + # assume domains are wildcards (e.g. youtube.com --> .youtube.com) + yield line.find('/') == -1, line + + @staticmethod + def _itr_custom(items: list): + for line in items: + if line.startswith('.') or line.startswith('*'): + # wildcard search, e.g. matches all subdomains of given domain, where * is the absolute toplevel (root) + yield True, line.lstrip('.') + else: + yield False, line + + def __iter__(self): + for domain in self._domain_entries: + # prepare domain policies + policy = { + 'domain': domain, + 'items': [] + } + for entry in self._domain_entries[domain]: + politem = copy.deepcopy(self._policy_settings[entry[0]]) + politem['path'] = entry[1] + politem['wildcard'] = entry[2] + policy['items'].append(politem) + yield policy + + def exists(self, domain): + return domain.split(':')[-1] in self._domain_entries diff --git a/www/OPNProxy/src/opnsense/scripts/OPNProxy/policies_to_redis_proto.py b/www/OPNProxy/src/opnsense/scripts/OPNProxy/policies_to_redis_proto.py new file mode 100755 index 0000000000..0e4599d6c4 --- /dev/null +++ b/www/OPNProxy/src/opnsense/scripts/OPNProxy/policies_to_redis_proto.py @@ -0,0 +1,100 @@ +#!/usr/local/bin/python3 +# -*- coding: utf-8 -*- +""" + Copyright (c) 2023 Ad Schellevis + 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. +""" +import argparse +import fcntl +import time +import ujson +from lib import Policy +import redis + + +def redis_proto_parser(*args): + """ + https://redis.io/topics/protocol + :return: + """ + response = ["*%d\r\n$%d\r\n%s\r\n" % (len(args), len(args[0]), args[0])] + for item in args[1:]: + response.append("$%d\r\n%s\r\n" % (len(item), item)) + return "".join(response) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + '--redis_host', + help='redis hostname to read keys from (default: 127.0.0.1)', + default='127.0.0.1' + ) + parser.add_argument( + '--redis_port', + help='redis port number (default: 6379)', + type=int, + default=6379 + ) + parser.add_argument( + '--proxy_policies', + help='proxy policies configuration file', + default='/usr/local/etc/squid/proxy_policies.conf' + ) + parser.add_argument('--output', help='output filename', default='/dev/stdout') + + cmd_args = parser.parse_args() + + try: + lck = open('/tmp/policies_to_redis_proto.LCK', 'w+') + fcntl.flock(lck, fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError: + # already running, exit status 99 + sys.exit(99) + + policy = Policy(cmd_args.proxy_policies) + + # fetch current domain keys from redis + try: + existing_domains = redis.StrictRedis( + host=cmd_args.redis_host, port=cmd_args.redis_port, db=0, decode_responses=True + ).keys('domain:*') + except (redis.exceptions.ConnectionError, redis.exceptions.BusyLoadingError) as e: + existing_domains = list() + + with open(cmd_args.output, 'w') as output_stream: + statistics = {'domains': 0, 'policies': 0, 'generated': time.time()} + # generate delete statements for non existing keys + for domain in existing_domains: + domain = domain.split(':')[-1] + if not policy.exists(domain): + output_stream.write(redis_proto_parser("DEL", "domain:%s" % domain)) + + # generate set statements for new data (upsert) + for item in policy: + statistics['domains'] += 1 + statistics['policies'] += len(item['items']) + output_stream.write(redis_proto_parser("SET", "domain:%s" % item['domain'], ujson.dumps(item))) + + output_stream.write(redis_proto_parser("SET", "domain_statistics", ujson.dumps(statistics))) diff --git a/www/OPNProxy/src/opnsense/scripts/OPNProxy/redis_sync_users.py b/www/OPNProxy/src/opnsense/scripts/OPNProxy/redis_sync_users.py new file mode 100755 index 0000000000..30098333ea --- /dev/null +++ b/www/OPNProxy/src/opnsense/scripts/OPNProxy/redis_sync_users.py @@ -0,0 +1,79 @@ +#!/usr/local/bin/python3 +# -*- coding: utf-8 -*- +""" + Copyright (c) 2023 Ad Schellevis + 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. +""" +import argparse +import fcntl +import sys +import syslog +import redis +import ujson +import xml.etree.ElementTree as ET + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--redis_host', help='redis hostname (default: 127.0.0.1)', default='127.0.0.1') + parser.add_argument('--redis_port', help='redis port number (default: 6379)', type=int, default=6379) + parser.add_argument('username', help='optional username', nargs='?', default=None) + args = parser.parse_args() + + # wait for other redis_sync_users sync events to complete + lck = open('/tmp/redis_sync_users.LCK', 'w+') + fcntl.flock(lck, fcntl.LOCK_EX) + + redisdb = redis.Redis(host=args.redis_host, port=args.redis_port, db=0) + + # ideally we would flush config data using the template system first, but since user settings may change + # more rappidly we opt to read the raw source here. + try: + tree = ET.parse('/conf/config.xml') + xmlroot = tree.getroot() + except (FileNotFoundError, ET.ParseError): + syslog.syslog(syslog.LOG_ERR, 'enable to open /conf/config.xml') + sys.exit(1) + + # merge group membership into user object and flush to redis + membership = dict() + for group in xmlroot.findall('./system/group'): + for member in group.findall('member'): + if member.text is None: + continue + for item in member.text.split(','): + if item not in membership: + membership[item] = list() + membership[item].append(group.findtext('name')) + + for user in xmlroot.findall('./system/user'): + if args.username is None or args.username == user.findtext('name'): + user_object = dict() + user_object['uid'] = user.findtext('name') + user_object['id'] = user.findtext('uid') + user_object['applies_on'] = ["u:%s" % user.findtext('name')] + if user_object['id'] in membership: + for group in membership[user_object['id']]: + user_object['applies_on'].append("g:%s" % group) + redisdb.set('user:%s' % user_object['uid'], ujson.dumps(user_object)) diff --git a/www/OPNProxy/src/opnsense/scripts/OPNProxy/squid_acl_helper.py b/www/OPNProxy/src/opnsense/scripts/OPNProxy/squid_acl_helper.py new file mode 100755 index 0000000000..17d4d87fa6 --- /dev/null +++ b/www/OPNProxy/src/opnsense/scripts/OPNProxy/squid_acl_helper.py @@ -0,0 +1,217 @@ +#!/usr/local/bin/python3 +# -*- coding: utf-8 -*- +""" + Copyright (c) 2023 Ad Schellevis + 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. +""" +import argparse +import decimal +import sys +import syslog +import traceback +from urllib.parse import urlparse +import redis +import ujson +import ipaddress + + +class RedisAuth: + def __init__(self, host, port): + self._redis = redis.Redis(host=host, port=port, db=0) + + def domain_policy_iterator(self, r_fqdn): + """ traverse domain policies + :param r_fqdn: fqdn + :return: + """ + try: + tmp = self._redis.get("domain:%s" % r_fqdn) + if tmp: + domain_policy = ujson.loads(tmp.decode()) + else: + return + except Exception as e: + # connectivity or parse issue, log and return + syslog.syslog(syslog.LOG_ERR, traceback.format_exc().replace('\n', ' ')) + return + + if type(domain_policy.get('items', None)) is list: + for policy in domain_policy['items']: + if type(policy) is dict: + for fieldname in ['id', 'path', 'wildcard', 'action', 'applies_on', 'source_net']: + if fieldname not in policy: + policy[fieldname] = None + yield policy + + def get_user(self, uid): + if uid == "-": + return {'applies_on': set('-')} + try: + tmp = self._redis.get("user:%s" % uid) + if not tmp: + return None + udata = ujson.loads(tmp.decode()) + # cleanse data + udata['applies_on'] = set(udata['applies_on']) if 'applies_on' in udata else set() + except Exception: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc().replace('\n', ' ')) + return None + + return udata + +def in_network(src, networks): + if networks is None or type(networks) is not list or src == '-': + return True + try: + src_net = ipaddress.ip_network(src) + except ValueError: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc().replace('\n', ' ')) + return False + for network in networks: + try: + if src_net.overlaps(ipaddress.ip_network(network)): + return True + except ValueError: + syslog.syslog(syslog.LOG_ERR, traceback.format_exc().replace('\n', ' ')) + + return False + +def match_policy(acl, ident, src, method, uri, sslurlonly=False): + # default response, invalid user + match_res = {'message': "ERR message=\"no (valid) IDENT %s\"\n" % ident} + if uri.find('://') == -1: + base_domain = uri.split(':')[0] + request_path = '/' + else: + uri_parsed = urlparse(uri) + base_domain = uri_parsed.netloc.split(':')[0] + request_path = uri_parsed.path if uri_parsed.path else '/' + + syslog.syslog( + syslog.LOG_NOTICE, + "ACL-REQ |%s| |%s| |%s| |%s| |%s| %s" % (acl, ident, src, method, uri, 'SNI only' if sslurlonly else '') + ) + fqdn = base_domain + user_data = redis_auth.get_user(ident) + if user_data: + acl_decisions = dict() + # traverse domain upwards until either a policy is found or no matches are possible + # matches are prioritized on best path match and accept (higher) or deny. + while len(acl_decisions) == 0: + for this_policy in redis_auth.domain_policy_iterator(fqdn): + is_parent = base_domain != fqdn + match_parent = this_policy['path'] == '/' and is_parent and this_policy['wildcard'] + match_main = request_path.find(this_policy['path']) == 0 and not is_parent + if (match_parent or match_main) and set(this_policy['applies_on']) & user_data['applies_on']: + if not in_network(src, this_policy['source_net']): + continue + tp = 0 if this_policy['action'] == 'deny' else 1 + this_prio = decimal.Decimal("%d.%d" % (len(this_policy['path']), tp)) + acl_decisions[this_prio] = this_policy + acl_decisions[this_prio]['domain'] = fqdn + + if fqdn.find('.') == -1: + if fqdn == '*': + break + else: + # top level wildcard (add extra level) + fqdn = '*' + else: + fqdn = fqdn.split('.', maxsplit=1)[1] + + match_res['user'] = user_data + match_res['user']['applies_on'] = list(user_data['applies_on']) + + if not sslurlonly and method.lower() == 'connect': + # skip connect when full ssl bump is enabled + match_res['policy'] = {'action': 'allow', 'policy_type': 'fallback'} + match_res['message'] = "OK user=\"%s\"\n" % ident + elif len(acl_decisions) > 0: + acl_decision = acl_decisions[sorted(acl_decisions.keys(), reverse=True)[0]] + match_res['policy'] = acl_decision + if match_res['policy']['action'] == 'deny': + match_res['message'] = "ERR message=\"reason:%s policy_type:%s\" user=\"%s\"\n" % ( + acl_decision['id'], acl_decision['policy_type'], ident + ) + else: + match_res['message'] = "OK message=\"whitelisted %s\" user=\"%s\"\n" % (acl_decision['id'], ident) + elif ident != '-': + # network only authentication needs an explicit policy, user-based allows by default + match_res['policy'] = {'action': 'allow', 'policy_type': 'fallback'} + match_res['message'] = "OK user=\"%s\"\n" % ident + + return match_res + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--test_user', help='test mode (singleshot), username') + parser.add_argument('--test_uri', help='test mode (singleshot), uri') + parser.add_argument('--test_src', help='test mode (singleshot), source address', default='-') + parser.add_argument('--redis_host', help='redis hostname (default: 127.0.0.1)', default='127.0.0.1') + parser.add_argument('--redis_port', help='redis port number (default: 6379)', type=int, default=6379) + parser.add_argument('--sslurlonly', help='Log SNI information only enabled', action="store_true", default=False) + parser.add_argument( + '--no_ident', + help='Do not expect iden/user information in the message line', + action="store_true", + default=False + ) + + args = parser.parse_args() + syslog.openlog('squid', facility=syslog.LOG_LOCAL2) + redis_auth = RedisAuth(args.redis_host, args.redis_port) + if args.test_user and args.test_uri: + # test mode, dump raw json object to stdout + result = match_policy(acl='-', ident=args.test_user, src=args.test_src, method='-', uri=args.test_uri) + print (ujson.dumps(result)) + else: + # squid worker mode + while True: + try: + # accept messages like: + # my_ext_acl user 127.0.0.2 GET https://requested.domain/path/ + line = sys.stdin.readline().strip() + if line == "": + sys.exit() + if line: + try: + acl_parts = line.split() + except ValueError: + sys.stdout.write("ERR message=\"missing input\"\n") + break + offset = -1 if args.no_ident else 0 + result = match_policy( + acl=acl_parts[0], + ident='-' if args.no_ident else acl_parts[1], + src=acl_parts[2+offset], + method=acl_parts[3+offset], + uri=acl_parts[4+offset], + sslurlonly=args.sslurlonly + ) + sys.stdout.write(result['message']) + + sys.stdout.flush() + except IOError: + pass diff --git a/www/OPNProxy/src/opnsense/service/conf/actions.d/actions_opnproxy.conf b/www/OPNProxy/src/opnsense/service/conf/actions.d/actions_opnproxy.conf new file mode 100644 index 0000000000..33d981cbfc --- /dev/null +++ b/www/OPNProxy/src/opnsense/service/conf/actions.d/actions_opnproxy.conf @@ -0,0 +1,21 @@ +[apply_policies] +command: + /usr/local/opnsense/scripts/OPNProxy/policies_to_redis_proto.py | redis-cli --pipe && + /usr/local/etc/rc.d/squid reload +parameters: +type:script +message:download proxy policies and apply to redisdb +description:OPNProxy apply policies + + +[sync_users] +command: /usr/local/opnsense/scripts/OPNProxy/redis_sync_users.py +parameters: +type:script +message:synchronise proxy users + +[user.test] +command: /usr/local/opnsense/scripts/OPNProxy/squid_acl_helper.py +parameters: --test_user %s --test_uri %s --test_src %s +type:script_output +message:test user login diff --git a/www/OPNProxy/src/opnsense/service/templates/Deciso/Proxy/+TARGETS b/www/OPNProxy/src/opnsense/service/templates/Deciso/Proxy/+TARGETS new file mode 100644 index 0000000000..b9419e8aee --- /dev/null +++ b/www/OPNProxy/src/opnsense/service/templates/Deciso/Proxy/+TARGETS @@ -0,0 +1,2 @@ +proxy_policies.conf:/usr/local/etc/squid/proxy_policies.conf +10-opnproxy-ext.auth.conf:/usr/local/etc/squid/auth/10-opnproxy-ext.auth.conf diff --git a/www/OPNProxy/src/opnsense/service/templates/Deciso/Proxy/10-opnproxy-ext.auth.conf b/www/OPNProxy/src/opnsense/service/templates/Deciso/Proxy/10-opnproxy-ext.auth.conf new file mode 100644 index 0000000000..408895ad80 --- /dev/null +++ b/www/OPNProxy/src/opnsense/service/templates/Deciso/Proxy/10-opnproxy-ext.auth.conf @@ -0,0 +1,43 @@ +external_acl_type ext_opnproxy_helper_net ttl=30 negative_ttl=5 %ACL %SRC %METHOD %URI /usr/local/opnsense/scripts/OPNProxy/squid_acl_helper.py --no_ident {% if not helpers.empty('OPNsense.proxy.forward.sslurlonly') %} --sslurlonly {% endif %} + +acl opnproxy_ext_acl_net external ext_opnproxy_helper_net +http_access allow opnproxy_ext_acl_net + +{% if not helpers.empty('OPNsense.proxy.forward.authentication.method') %} +# Login based authentication +external_acl_type ext_opnproxy_helper_usr ttl=30 negative_ttl=5 %ACL %LOGIN %SRC %METHOD %URI /usr/local/opnsense/scripts/OPNProxy/squid_acl_helper.py {% if not helpers.empty('OPNsense.proxy.forward.sslurlonly') %} --sslurlonly {% endif %} + +acl opnproxy_ext_acl_usr external ext_opnproxy_helper_usr +http_access allow opnproxy_ext_acl_usr +{% endif %} + + +{% if not helpers.empty('OPNsense.proxy.forward.icap.enable') %} +{% if not helpers.empty('OPNsense.proxy.forward.icap.ResponseURL') %} +adaptation_access response_mod allow opnproxy_ext_acl_net +{% if not helpers.empty('OPNsense.proxy.forward.authentication.method') %} +adaptation_access response_mod allow opnproxy_ext_acl_usr +{% endif %} +{% endif %} +{% if not helpers.empty('OPNsense.proxy.forward.icap.RequestURL') %} +adaptation_access request_mod allow opnproxy_ext_acl_net +{% if not helpers.empty('OPNsense.proxy.forward.authentication.method') %} +adaptation_access request_mod allow opnproxy_ext_acl_usr +{% endif %} +{% endif %} +{% endif %} + +{% if not helpers.empty('OPNsense.proxy.forward.authentication.method') %} +# explicit disable default allow authenticated users clause +http_access deny local_auth all +{% if not helpers.empty('OPNsense.proxy.forward.icap.enable') %} +{% if not helpers.empty('OPNsense.proxy.forward.icap.ResponseURL') %} +adaptation_access response_mod deny local_auth +{% endif %} +{% if not helpers.empty('OPNsense.proxy.forward.icap.RequestURL') %} +adaptation_access request_mod deny local_auth +{% endif %} +{% endif %} +{% else %} +http_access deny localnet +{% endif %} diff --git a/www/OPNProxy/src/opnsense/service/templates/Deciso/Proxy/proxy_policies.conf b/www/OPNProxy/src/opnsense/service/templates/Deciso/Proxy/proxy_policies.conf new file mode 100644 index 0000000000..ce330f22d5 --- /dev/null +++ b/www/OPNProxy/src/opnsense/service/templates/Deciso/Proxy/proxy_policies.conf @@ -0,0 +1,30 @@ +{% for policy in helpers.toList('Deciso.Proxy.ACL.policies.policy') %} +{% if policy.enabled|default('0') == '1' %} +[policy_{{ policy['@uuid'] }}] +policy_type=default +description={{ policy.description }} +content={{ policy.content }} +applies_on={{ policy.applies_on|default('-') }} +source_net={{ policy.source_net }} +action={{ policy.action }} +{% endif %} + +{% endfor %} + +{% for policy in helpers.toList('Deciso.Proxy.ACL.custom_policies.policy') %} +{% if policy.enabled|default('0') == '1' %} +[policy_{{ policy['@uuid'] }}] +policy_type=custom +description={{ policy.description }} +content={{ policy.content.replace('\n', '\n\t') }} +applies_on={{ policy.applies_on|default('-') }} +source_net={{ policy.source_net }} +action={{ policy.action }} +{% endif %} + +{% endfor %} + +[source] +blocklist=/usr/local/opnsense/data/proxy/blocklists.tar.gz +blocklist_download_uri=https://rulesets.opnsense.org/proxy/blocklists.tar.gz +blocklist_ttl=86300 diff --git a/www/c-icap/Makefile b/www/c-icap/Makefile index 9f1fe36abe..c18f9d74ba 100644 --- a/www/c-icap/Makefile +++ b/www/c-icap/Makefile @@ -1,8 +1,7 @@ PLUGIN_NAME= c-icap -PLUGIN_VERSION= 1.7 -PLUGIN_REVISION= 2 +PLUGIN_VERSION= 1.9 PLUGIN_COMMENT= c-icap connects the web proxy with a virus scanner PLUGIN_DEPENDS= c-icap c-icap-modules -PLUGIN_MAINTAINER= m.muenz@gmail.com +PLUGIN_MAINTAINER= andybinder@gmx.de .include "../../Mk/plugins.mk" diff --git a/www/c-icap/pkg-descr b/www/c-icap/pkg-descr index 5ae299e86b..2369c75a18 100644 --- a/www/c-icap/pkg-descr +++ b/www/c-icap/pkg-descr @@ -2,4 +2,14 @@ c-icap is an implementation of an ICAP server. It can be used with HTTP proxies that support the ICAP protocol to implement content adaptation and filtering services. -WWW: http://c-icap.sourceforge.net/ +Plugin Changelog +================ + +1.9 + +* Fix issue with localserver ACL defintion +* Yield maintainership to Andy Binder + +1.8 + +* Move logging to syslog (contributed by Andy Binder) diff --git a/www/c-icap/src/etc/inc/plugins.inc.d/cicap.inc b/www/c-icap/src/etc/inc/plugins.inc.d/cicap.inc index 305408d15a..d2528499d2 100644 --- a/www/c-icap/src/etc/inc/plugins.inc.d/cicap.inc +++ b/www/c-icap/src/etc/inc/plugins.inc.d/cicap.inc @@ -26,6 +26,15 @@ POSSIBILITY OF SUCH DAMAGE. */ +function cicap_syslog() +{ + return [ + 'cicap' => [ + 'facility' => ['c-icap'] + ] + ]; +} + function cicap_services() { global $config; diff --git a/www/c-icap/src/etc/rc.syshook.d/start/50-c-icap b/www/c-icap/src/etc/rc.syshook.d/start/50-c-icap index bb12c91336..ace732728a 100755 --- a/www/c-icap/src/etc/rc.syshook.d/start/50-c-icap +++ b/www/c-icap/src/etc/rc.syshook.d/start/50-c-icap @@ -1,4 +1,4 @@ #!/bin/sh -# start again to fix race with clamav (no need to restart) -/usr/local/etc/rc.d/c-icap start +# restart service to make sure c-icap starts after clamav is ready +/usr/local/etc/rc.d/c-icap restart diff --git a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Antivirus.xml b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Antivirus.xml index a0310b9df1..cd45fe479e 100644 --- a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Antivirus.xml +++ b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Antivirus.xml @@ -4,42 +4,42 @@ 1.0.0 - 0 + 0 Y - TEXT,DATA,EXECUTABLE,ARCHIVE,GIF,JPEG,MSOFFICE - Y + TEXT,DATA,EXECUTABLE,ARCHIVE,GIF,JPEG,MSOFFICE + Y Y - - Text files - Binary files - Executables - Archives - GIF animations - JPEG pictures - Microsoft office files - + + Text files + Binary files + Executables + Archives + GIF animations + JPEG pictures + Microsoft office files + - 5 + 5 Y - 2M + 2M Y - 1 + 1 Y - 0 + 0 Y - 5M + 5M Y - + diff --git a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/General.xml b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/General.xml index 7060936c04..ba65b5243b 100644 --- a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/General.xml +++ b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/General.xml @@ -4,66 +4,60 @@ 1.0.1 - 0 + 0 Y - 300 + 300 Y - 100 + 100 Y - 600 + 600 Y - 3 + 3 Y - 10 + 10 Y - 10 + 10 Y - 20 - Y - + 20 + Y + - 10 - Y - + 10 + Y + - 0 - Y - + 0 + Y + - ::1 - N - N - , + ::1 + N + N + , Y - - - N - - - - N - + + - 1 + 1 Y - 1 + 1 Y diff --git a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Menu/Menu.xml b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Menu/Menu.xml index 326a689d0b..f408f8109b 100644 --- a/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Menu/Menu.xml +++ b/www/c-icap/src/opnsense/mvc/app/models/OPNsense/CICAP/Menu/Menu.xml @@ -2,7 +2,7 @@ - + diff --git a/www/c-icap/src/opnsense/scripts/OPNsense/CICAP/setup.sh b/www/c-icap/src/opnsense/scripts/OPNsense/CICAP/setup.sh index 8e29386a1a..b5b2195631 100755 --- a/www/c-icap/src/opnsense/scripts/OPNsense/CICAP/setup.sh +++ b/www/c-icap/src/opnsense/scripts/OPNsense/CICAP/setup.sh @@ -4,11 +4,5 @@ mkdir -p /var/run/c-icap chown -R c_icap:c_icap /var/run/c-icap chmod 750 /var/run/c-icap -mkdir -p /var/log/c-icap -chown -R c_icap:c_icap /var/log/c-icap -chmod 750 /var/log/c-icap -(cd /var/log && ln -s c-icap cicap) -chown -R c_icap:c_icap /var/log/cicap - mkdir -p /tmp/c-icap/templates/virus_scan/en chmod -R 755 /tmp/c-icap/ diff --git a/www/c-icap/src/opnsense/service/conf/actions.d/actions_cicap.conf b/www/c-icap/src/opnsense/service/conf/actions.d/actions_cicap.conf index 0d564d26ad..db3586e14f 100644 --- a/www/c-icap/src/opnsense/service/conf/actions.d/actions_cicap.conf +++ b/www/c-icap/src/opnsense/service/conf/actions.d/actions_cicap.conf @@ -1,23 +1,23 @@ [start] -command:/usr/local/opnsense/scripts/OPNsense/CICAP/setup.sh;/usr/local/etc/rc.d/c-icap start +command:/usr/local/etc/rc.d/c-icap start parameters: type:script message:starting c-icap [stop] -command:/usr/local/etc/rc.d/c-icap stop; exit 0 +command:/usr/local/etc/rc.d/c-icap stop parameters: type:script message:stopping c-icap [restart] -command:/usr/local/opnsense/scripts/OPNsense/CICAP/setup.sh;/usr/local/etc/rc.d/c-icap restart +command:/usr/local/etc/rc.d/c-icap restart parameters: type:script message:restarting c-icap [status] -command:/usr/local/etc/rc.d/c-icap status;exit 0 +command:/usr/local/etc/rc.d/c-icap status; exit 0 parameters: type:script_output message:request c-icap status diff --git a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/+TARGETS b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/+TARGETS index 4be92f8264..07687be790 100644 --- a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/+TARGETS +++ b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/+TARGETS @@ -1,6 +1,5 @@ c_icap:/etc/rc.conf.d/c_icap c-icap.conf:/usr/local/etc/c-icap/c-icap.conf -newsyslog.conf:/etc/newsyslog.conf.d/c-icap virus_scan.conf:/usr/local/etc/c-icap/virus_scan.conf VIRUS_FOUND:/tmp/c-icap/templates/virus_scan/en/VIRUS_FOUND VIR_MODE_HEAD:/tmp/c-icap/templates/virus_scan/en/VIR_MODE_HEAD diff --git a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c-icap.conf b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c-icap.conf index 37580540ae..f2ea03556f 100644 --- a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c-icap.conf +++ b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c-icap.conf @@ -42,11 +42,16 @@ ServerName {{ OPNsense.cicap.general.servername }} {% else %} ServerName {{ system.hostname }} {% endif %} +{% if not helpers.empty('OPNsense.cicap.general.listenaddress') %} +acl localserver srvip {{ OPNsense.cicap.general.listenaddress }} +{% else %} +acl localserver srvip ::1 +{% endif %} {% if helpers.exists('OPNsense.cicap.general.localSquid') and OPNsense.cicap.general.localSquid == '1' %} {% if helpers.exists('OPNsense.proxy.forward.icap.SendUsername') and OPNsense.proxy.forward.icap.SendUsername == '1' %} RemoteProxyUsers on acl AUTH auth * -icap_access allow AUTH 127.0.0.1 +icap_access allow AUTH localserver {% else %} RemoteProxyUsers off {% endif %} @@ -61,7 +66,7 @@ RemoteProxyUserHeader {{OPNsense.proxy.forward.icap.UsernameHeader}} {% else %} RemoteProxyUsers on acl AUTH auth * -icap_access allow AUTH 127.0.0.1 +icap_access allow AUTH localserver RemoteProxyUserHeaderEncoded on RemoteProxyUserHeader X-Authenticated-User {% endif %} @@ -75,9 +80,11 @@ ServicesDir /usr/local/lib/c_icap TemplateDir /tmp/c-icap/templates/ TemplateDefaultLanguage en LoadMagicFile /usr/local/etc/c-icap/c-icap.magic -ServerLog /var/log/c-icap/server.log -{% if helpers.exists('OPNsense.cicap.general.enable_accesslog') and OPNsense.cicap.general.enable_accesslog == '1' %} -AccessLog /var/log/c-icap/access.log +Module logger sys_logger.so +Logger sys_logger +sys_logger.Prefix "c-icap" +{% if helpers.exists('OPNsense.cicap.general.enable_accesslog') and OPNsense.cicap.general.enable_accesslog == '0' %} +sys_logger.access !localserver {% endif %} Service echo srv_echo.so Include virus_scan.conf diff --git a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c_icap b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c_icap index 4be32766df..f8da98c947 100644 --- a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c_icap +++ b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/c_icap @@ -1,5 +1,5 @@ {% if helpers.exists('OPNsense.cicap.general.enabled') and OPNsense.cicap.general.enabled == '1' %} -c_icap_var_script="/usr/local/opnsense/scripts/OPNsense/CICAP/setup.sh" +c_icap_setup="/usr/local/opnsense/scripts/OPNsense/CICAP/setup.sh" c_icap_enable="YES" {% else %} c_icap_enable="NO" diff --git a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/newsyslog.conf b/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/newsyslog.conf deleted file mode 100644 index 1edd4401f9..0000000000 --- a/www/c-icap/src/opnsense/service/templates/OPNsense/CICAP/newsyslog.conf +++ /dev/null @@ -1,7 +0,0 @@ -# logfilename [owner:group] mode count size when flags [/pid_file] [sig_num] -{% if helpers.exists('OPNsense.cicap.general.enabled') and OPNsense.cicap.general.enabled|default("0") == "1" %} -{% if helpers.exists('OPNsense.cicap.general.enable_accesslog') and OPNsense.cicap.general.enable_accesslog == '1' %} -/var/log/c-icap/access.log c_icap:c_icap 644 7 * @T00 ZB /var/run/c-icap/c-icap.pid -{% endif %} -/var/log/c-icap/server.log c_icap:c_icap 644 7 * @T00 ZB /var/run/c-icap/c-icap.pid -{% endif %} diff --git a/www/c-icap/src/opnsense/service/templates/OPNsense/Syslog/local/cicap.conf b/www/c-icap/src/opnsense/service/templates/OPNsense/Syslog/local/cicap.conf new file mode 100644 index 0000000000..9eee81b260 --- /dev/null +++ b/www/c-icap/src/opnsense/service/templates/OPNsense/Syslog/local/cicap.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [cicap]. +################################################################### +filter f_local_cicap { + program("c-icap"); +}; diff --git a/www/caddy/Makefile b/www/caddy/Makefile new file mode 100644 index 0000000000..344360c87d --- /dev/null +++ b/www/caddy/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= caddy +PLUGIN_VERSION= 2.0.4 +PLUGIN_REVISION= 3 +PLUGIN_DEPENDS= caddy-custom +PLUGIN_COMMENT= Modern Reverse Proxy with Automatic HTTPS, Dynamic DNS and Layer4 Routing +PLUGIN_MAINTAINER= cedrik@pischem.com + +.include "../../Mk/plugins.mk" diff --git a/www/caddy/pkg-descr b/www/caddy/pkg-descr new file mode 100644 index 0000000000..71e6740172 --- /dev/null +++ b/www/caddy/pkg-descr @@ -0,0 +1,352 @@ +Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS + +WWW: https://caddyserver.com/ +DOC: https://docs.opnsense.org/manual/how-tos/caddy.html + +Plugin Changelog +================ + +2.0.4 + +Add: DNS-01 challenge delegation via CNAME (contributed by sdsys-ch) (opnsense/plugins/pull/4950) +Fix: Enabling HTTP access log wrongly excluded the process logs (opnsense/plugins/pull/4974) +Fix: fix setup.sh script not setting correct ownership in www user mode (opnsense/plugins/pull/4976) +Fix: Prevent sudo on startup via skip_install_trust (opnsense/plugins/pull/5015) +Fix: Fix race condition that moved domain filter selectpicker into invisible tab (opnsense/plugins/pull/5076) + +2.0.3 + +Add: Tabulator groupBy of domain and subdomain (opnsense/plugins/pull/4909) +Cleanup: Grid HTML and style +Fix: Tabulator 'Data Load Response Blocked' warning +Fix: setup.sh interaction with caddy storage and permissions (opnsense/plugins/pull/4911) +Fix: Emit subdomain http access logs when wildcard parent has logging enabled (opnsense/plugins/issues/4914) + +2.0.2 + +Add: Global server timeout options (opnsense/plugins/pull/4778) +Cleanup: Change all camelCase to snake_case in api notations (opnsense/plugins/pull/4767,4768,4776) +Cleanup: Use SimpleActionButton in general.volt (opnsense/plugins/pull/4779) +Cleanup: Change em into px, fix key/value display in formatter (opnsense/plugins/pull/4807) +Cleanup: Remove obsolete model_relation_domain formatter (opnsense/plugins/pull/4813) + +2.0.1 + +Add: Active health checks to handlers (contributed by zaben903) (opnsense/plugins/pull/4721) +Fix: Add handler command sometimes clearing domain selection in open dialogue (opnsense/plugins/pull/4748) + +2.0.0 + +* Attention - Breaking Change: All DNS Providers except Cloudflare have been removed and will not come back (opnsense/plugins/pull/4691) + Going forward, if DNS-01 challenge or Dynamic DNS is a requirement, you could use os-acme-client and os-ddclient. + Another option is to migrate to Cloudflare, as this module will stay a core component. + +* Add: Request Matcher option to Access List (contributed by spawntty) (opnsense/plugins/pull/4680) +* Add: ECH can configure encrypted client hello with Cloudflare (opnsense/plugins/pull/4673) +* Add: Client Auth (mTLS) to subdomains (opnsense/plugins/pull/4673) +* Build: Update to caddy-v2.10.0 (opnsense/tools/pull/469) +* Change: Render subdomains in a new pattern in the Caddyfile to adhere to caddy-v2.10.0 standard (opnsense/plugins/pull/4673) +* Change: Show only wildcard domains in subdomain selection (opnsense/plugins/pull/4646) +* Fix: Missing translation and toggle filter icon (opnsense/plugins/pull/4648) + +1.8.5 + +* Add: basic_auth per handler (opnsense/plugins/issues/4619) +* Change: the ACL has been changed to "page-caddy" in "System: Access: Privileges". Privilege must be reassigned if used. (opnsense/plugins/issues/4623) +* Change: standalone certificate widget has been removed, use system default certificate widget instead. (opnsense/plugins/pull/4637) +* Cleanup: UI improvements (opnsense/plugins/pull/4634) + * Cleanup of styles, formatters, and dialog layouts + * Dialog element hiding logic was improved + * "Filter by Domain" can filter subdomains + * "Filter by Domain" will preselect domain and subdomain in handler add dialog + * Grids are now responsive with automatic overflow on small screens + * Grids now lazy load on tab switch for improved performance + * Grids format domains and upstreams with protocol, domain, port and path in a single line + * "Layer4 Proxy" and "Reverse Proxy" now use the same volt template + * "Search Handler" and "Add Handler" buttons added to domain and subdomain grids + * Step buttons (Add Domain, Add Handler) have been removed since they are redundant + * Success messages on configuration apply have been removed, only errors will be shown + * Tabs use URL hash to preserve selection on refresh + +1.8.4 + +* Add: Client Auth (mTLS) to domains (opnsense/plugins/issues/4089) +* Fix: Some selectpickers had incorrect style (opnsense/plugins/pull/4616) + +1.8.3 + +* Add: Update DNS Providers with new optional choices (opnsense/plugins/issues/4543) +* Add: propagation_timeout and propagation_delay (opnsense/plugins/issues/4544) + +1.8.2 + +* Add: client_ip_headers (opnsense/plugins/issues/4517) +* Add: CloudDNS provider (opnsense/plugins/pull/4507) +* Change: Generalize forward_auth copy_headers directive. Existing configuration from (issues/4488) will be emptied. (opnsense/plugins/pull/4519) +* Fix: Shortcut buttons in reverse_proxy.volt (opnsense/plugins/pull/4525) + +1.8.1 + +* Add: Optional "Authorization" header to forward_auth (opnsense/plugins/issues/4488) +* Add: Persistent banner notification if custom imports are used (opnsense/plugins/issues/4244) +* Cleanup: Implement reusable grid template in views (opnsense/plugins/pull/4454) + +1.8.0 + +* Build: Update Caddy to version 2.9.x and update dependencies (opnsense/plugins/issues/4437) +* Build: Fix caddy-l4 timeout issue (opnsense/plugins/issues/4384) +* Build: Mark DNS Providers optional that are not included per default (opnsense/plugins/pull/4441) + digitalocean, route53, googleclouddns, netlify, ddnss, njalla, tencentcloud + dinahosting, civo, easydns, hosttech; must be added via https://caddyserver.com/docs/command-line#caddy-add-package +* Cleanup: Refactor caddy.inc and add syslog function, change name from Caddy Web Server to Caddy (opnsense/plugins/issues/4426) +* Cleanup: Some small UI tweaks (opnsense/plugins/pull/4442) +* Change: Disable HTTP/3 to mitigate status 400 issue when reverse proxying the OPNsense WebGUI (opnsense/plugins/issues/4471) + +1.7.6 + +* Fix: Web UI can still restart/stop Caddy when running as `www` user (opnsense/plugins/pull/4403) +* Fix: Wildcard certificates displaying in Certificate Widget (opnsense/plugins/pull/4390) +* Fix: http being correctly prepended on redirects (opnsense/plugins/pull/4385) + +1.7.5 + +* Add: Load Balancing options to Layer 4 Proxy and HTTP Handle +* Add: Layer4 TLS Termination +* Add: h2c protocol to HTTP Handler +* Cleanup: Caddy Certificate widget hides unused automatic certificates +* Cleanup: Widgets error handling improved +* Cleanup: Refactor caddy_certs.php to Trust model + +1.7.4 + +* Add: Layer4 OpenVPN matcher with mode, digest and static key support +* Add: Layer4 Winbox matcher +* Add: Layer4 QUIC matcher +* Build: Update dependency to lang/go123 +* Build: Update caddy-l4 and caddy-dynamicdns module +* Build: Fix that caddy-l4 does not stop when ssh is proxied +* Build: DNS Providers: Update porkbun, dnsmadeeasy +* Cleanup: Layer4 default route in listener_wrappers has been removed (obsolete) +* Fix: Error when same Access List is set to wildcard and subdomain + +1.7.3 + +* Add: Clear All button to Filter by Domain selectpicker +* Change: Layer4 Proxy has own menu entry +* Change: Domains and Subdomains are in same tab, Subdomains are not initially hidden anymore +* Fix: WebGUI port conflict validation triggers too aggressively + +1.7.2 + +* Add: Directive in HTTP Handler can be chosen, "reverse_proxy" and "redir" +* Add: Layer4 routes feature. Routing Type "global" or "listener_wrapper" can be chosen +* Add: Any Layer4 TCP/UDP traffic can be proxied without choosing a Layer 7 protocol matcher +* Change: Layer4 routes are not ordered automatically anymore +* Change: Layer4 "not tls sni" matcher has been replaced by generalized "Invert Matchers" checkbox +* Build: Update Caddy Layer4 module, fixes TLS matcher in chromium based browsers +* Fix: When Apply takes longer than 20 seconds, Caddy will be forcefully restarted + +1.7.1 + +* Add: Frontend HTTP Version can be selected in General Settings, can be used to disable QUIC protocol +* Add: Access Lists can now be defined per HTTP Handler in advanced mode +* Change: Caddy Domains widget will now open links to managed websites in new browser tabs +* Change: To select tls_insecure_skip_verify in a HTTP Handler, the protocol https:// must be chosen +* Change: Abort from General Settings has been removed, it is now automatically added to all Access Lists +* Cleanup: Caddyfile template logic for Access Lists has been rewritten +* Cleanup: TLS checkboxes have been converted to dropdowns with http/https for clarity +* Cleanup: Layer4, Domain and Handle dialogues have been cleaned up, some options are now hidden in advanced mode +* Fix: Layer4 default ports did not render due to regression in previous version +* Fix: Invert in Access Lists did not render due to regression in previous version + +1.7.0 + +* Add: Layer4 protocols: DNS +* Add: DNS Providers: Hetzner +* Change: DNS Providers: Route53 field "max_retries" has been renamed to "hosted_zone_id" +* Cleanup: Refactor "general.volt" from "layout_partials/base_form" to "layout_partials/base_tabs" +* Cleanup: Refactor "general.volt", "reverse_proxy.volt" and "diagnostics.volt" to imported ajaxGet() and ajaxCall() +* Cleanup: Adjust style of all views +* Cleanup: Restructure "general.xml" to include tabs, add new "Advanced Settings" Tab +* Cleanup: Remove old custom migrations +* Cleanup: Refactor Caddyfile template, extracting duplicate logic into macros +* Fix: Removed email validation from Caddy.php since it made the default migration fail + +1.6.3 + +* Add: Disable Propagation Timeout in DNS Provider Tab. This can help if the DNS Challenge fails due to DNS Propagation being too slow. +* Add: Set custom resolver for the DNS Challenge in the DNS Provider tab. + +1.6.2 + +* Add: Authentik as authentication provider (contributed by Tim-Sc) +* Add: Feature Preview - Layer4 routing to proxy traffic without TLS termination. Can be enabled in advanced mode of "General Settings" +* Add: Layer4 protocols: HTTP, Postgres, Proxy Protocol, RDP, SOCKS4, SOCKS5, SSH, TLS, XMPP +* Add: Layer4 Remote IP matcher to restrict access to proxied services + +1.6.1 + +* Add: Run Caddy as "www" user and group, by enabling "Disable Superuser" in General Settings +* Add: Shortcuts to add new Domains and Handlers +* Change: Description Fields are optional now, reducing the steps needed to create new entries +* Change: Subdomain Tab only appears when a wildcard domain has been configured +* Cleanup: Validations in general.volt are all appended to their form keys, instead of triggering a Bootstrap Dialog + +1.6.0 + +* Add: New Dashboard widgets for 24.7, showing domain status and certificate validity status +* Add: forward_auth directive with Authelia as Authz Provider +* Add: Default HTTP and HTTPS ports can be changed in general settings +* Add: Introduce HTTP version to handler. HTTP/1.1, HTTP/2 and HTTP/3 can be chosen +* Add: HTTP Keepalive can be set in a handler +* Add: TLS can be deactivated in a domain +* Build: Update caddy-dns Cloudflare and Route53. Route53 field names changed due to upstream changes +* Change: Option "tls_trusted_ca_certs" is now "tls_trust_pool" +* Cleanup: PHP files refactored for PSR-12, Python files refactored for PEP-8 +* Cleanup: Templates Caddyfile and rc.conf.d/caddy refactored for maintainability +* Cleanup: Spurious keys removed from Caddy.xml model +* Cleanup: Unused code removed from Caddy.php +* Fix: Caddyfile template fixed when IPv6 addresses and ports are used in Upstream. IPv6 address wraps into brackets now + +1.5.7 + +* Add: Error message when OPNsense WebGUI settings conflict with Auto HTTPS +* Add: Error message when Auto HTTPS is enabled, and ACME email field is empty, for caddy v2.8. +* Add: Dynamic DNS now supports "Update Only", only updating existing records without creating new ones +* Add: DNS Providers: dnsmadeeasy, bunny, civo, scaleway, acmeproxy, inwx, namedotcom, easydns, infomaniak, directadmin, hosttech, vult +* Build: Update to Caddy v2.8.4 + caddy-dns plugins updated to latest upstream version +* Change: Dynamic DNS "TTL" and "Check Interval" have been changed to seconds. Existing values have been reset to use the defaults of the implementation +* Cleanup: Fix crash of searchAction when reverseUuids is null +* Cleanup: basicauth directive is now basic_auth in the Caddyfile template, for caddy v2.8 +* Cleanup: Refactor dns provider configuration in Caddyfile template +* Fix: The subdomain port field has been removed, since it is unsupported. Subdomains track their ports from their parent wildcard domain +* Remove: DNS Providers: godaddy + +1.5.6 + +* Add: DNS Providers: Netcup, RFC2136 +* Fix: Wildcard domains with activated "Dynamic DNS" update their base domain with * instead of @ + +1.5.5 + +* Add: In Reverse Proxy, a new dropdown can select one or multiple domains, filtering the Bootgrids of Domains, Subdomains and Handlers for the selected Domain +* Add: Global Log Level can be set in Log Settings +* Add: Diagnostics view added where the current Caddyfile and JSON configuration can be displayed, validated and downloaded +* Add: HTTP-01 Challenge Redirection can also be configured for subdomains +* Change: ACME Email should be filled out since it's a requirement for ZeroSSL +* Cleanup: Javascript variables have been changed from var to let to reduce scope +* Cleanup: lang() and gettext() functions added for translations +* Cleanup: Rewritten most help texts in forms for consistency +* Fix: "Apply" could hang when websockets are in use by clients. A grace period of 10s has been added in General Settings that forces to close all connections on config changes +* Fix: "Apply" will always read all certificates from the filesystem, even if the Caddy configuration has remained unchanged. "reload" has been changed to "reloadssl" +* Fix: "Save" and "Apply" buttons in General Settings have been improved to reliably trigger validation of form +* Fix: Template has been fixed to allow any TLS option in Handlers to appear independant when filled out. This increases flexibility with the "tls_server_name" option +* Fix: The newly introduced "configctl caddy reload" action, which calls the "service caddy reloadssl" command, will now also trigger the setup.sh script + +1.5.4 + +* Add: Simple Load Balancing support with the default random policy, by allowing to add multiple Upstream Domains in Handlers +* Add: Passive Health check for load balancing (Upstream Fail Duration) in Handlers +* Add: HTTP response code and HTTP response message can be set per access list in advanced mode +* Add: Header functionality added. Multiple header manipulations can be set per handler +* Change: All Description Fields are now required to be populated +* Change: Model Relation Fields now display two values instead of one to make most options appear unique +* Cleanup: Update searchBase() in ReverseProxyController.php for easier maintainability +* Fix: When pressing Apply, the Caddy service will be reloaded instead of restarted. This fixes long restart times and service interruptions +* Fix: Move selectpicker empty option to model in general.volt, using BlankDesc. This fixes the option IPv4+IPv6 not appearing in Dynamic DNS +* Fix: Input validation so a base domain like "example.com" and a wildcard domain like "*.example.com" can now be created at the same time in domains + +1.5.3 + +* Add: tls_insecure_skip_verify to handlers +* Add: Possibility to restart Caddy with the ACME Client by using "Automations - Run Command - System or Plugin Command" +* Add: Option to redirect the ACME HTTP-01 challenge to an upstream destination as advanced option in domains +* Change: From "Phalcon Messages" to "OPNsense Messages" in Caddy.php +* Change: Default storage location from /usr/local/etc/caddy to /var/db/caddy/data/caddy/ +* Change: Description from "TextField" to "DescriptionField" in Caddy.xml model +* Cleanup: Dialogs and UI to present all options better +* Remove: Unmaintained DNS Providers: dnspod, hetzner, namesilo, vercel, alidns, metaname, openstack-designate + +1.5.2 + +* Build: When selecting an interface in Dynamic DNS, at most one IPv6 GUA and IPv4 non-RFC1918 address will be extracted. Fixes all IP addresses being read +* Cleanup: Increased timeout of message area in reverse_proxy.volt and general.volt to 15 seconds +* Cleanup: Caddy.xml model to satisfy make lint +* Fix: When pressing Apply, the form is saved automatically before the reconfigure action + +1.5.1 + +* Add: More DNS Providers: netlify, namesilo, njalla, vercel, googleclouddns, alidns, powerdns, tencentcloud, dinahosting, metaname, hexonet, ddnss, linode, mailinabox, ovh, namecheap, azure, openstack-designate +* Add: More input fields and better documentation for the DNS Provider API Keys +* Change: rc.d script to standard freebsd poudriere one packaged with the caddy-custom binary, included setup.sh script to rc.conf.d/caddy +* Change: Updated dependancy to caddy-custom instead of caddy +* Change: Enable $internalModelUseSafeDelete in ReverseProxyController.php - Items can only be deleted when they are not referenced by other items, making deleting in the GUI safer since there can't be any orphaned configuration left behind +* Cleanup: Turned syslog-ng configuration from template to static file +* Cleanup: A few typos in the general.volt and reverse_proxy.volt corrected +* Cleanup: The RealInterfaceField custom Fieldtype was removed and replaced with an OPNsense integrated template function to read the interface name +* Cleanup: Migration script M1_1_3 from "Description" to "description" added. Lower case description is needed to be in line with some OPNsense integrated functions +* Remove: +POST_DEINSTALL.post and +POST_INSTALL.post + +1.5.0 + +* Add: ACME-DNS Provider for custom ACME Server support +* Add: When pressing save, a hint will appear that changes should be applied. Apply and Save now give feedback on success +* Add: Create ACL for Caddy +* Cleanup: General view cleanup by seperating options into different tabs +* Cleanup: Code consistency improved +* Remove: vultr from DNS-Providers since it can't be built without errors + +1.4.5 + +* Add: New validate api action when pressing apply that validates the Caddyfile + Validation model fix +* Add: Configuration option to log HTTP access to plain JSON files (contributed by pmhausen) +* Add: Backend path prepend feature to handler configuration (contributed by pmhausen) + +1.4.4 + +* Add: Route53 DNS Provider +* Cleanup: Dark Mode GUI + +1.4.2 + +* Add: Basic Auth as additional access restriction, multiple users can be set per domain and subdomain +* Add: Porkbun DNS Provider for GUI configuration with additional DNS Secret Api Key input field +* Cleanup: Improve views with seperated General Settings and DNS Provider Settings, joined Access List and Basic Auth in new Access Tab +* Cleanup: Fixed some typos in forms. +* Fix: Template generation of Caddyfile for new DNS Provider deSEC + +1.4.0 + +* Add: DynDNS (Dynamic DNS) Feature +* Add: HTTP Access Logs +* Add: More DNS Providers: Cloudflare, Duck DNS, DigitalOcean, DNSPod, Hetzner, GoDaddy, Gandi, Vultr, IONOS, deSEC +* Cleanup: Logging refactored to syslog-ng. + +1.3.4 + +* Add: abort statement to close all connections that do not match any handle +* Add: tls_server_name to model +* Fix: DNS Challenge not adhering to the status of the DNS-01 Checkbox + +1.3.3 + +* Change: Swapped processing order in template from wildcard domains -> subdomains to subdomains -> wildcard domains + +1.3.2 + +* Add: Inline loop logic to template to always print empty handles last in the Caddyfile before specific handles + +1.3.1 + +* Add: Access Lists +* Add: Reload for the ServiceControlUI after pressing Apply +* Fix: Small constraint to handle added. Handles have to start at least with / when they're not empty, or Caddy won't start + +1.3.0 - Initial release + +* Create easy Reverse Proxy entries, use the HTTP or DNS-01 Challenge to get quick and easy ACME Certificates with Let's Encrypt +* Control Caddy and view the Logfile in the OPNsense GUI +* Create more complicated nested handle structures with wildcard domains, subdomains, handles and catch all handles +* Use your own certificates where needed +* Use TLS, NTLM and CA certificates to communicate with your Backend Servers +* Use Caddy with two OPNsense Firewalls in HA - only fully supported with custom certificates from OPNsense trust store diff --git a/www/caddy/src/etc/inc/plugins.inc.d/caddy.inc b/www/caddy/src/etc/inc/plugins.inc.d/caddy.inc new file mode 100644 index 0000000000..5e116933a2 --- /dev/null +++ b/www/caddy/src/etc/inc/plugins.inc.d/caddy.inc @@ -0,0 +1,71 @@ + gettext('Caddy'), + 'configd' => array( + 'restart' => array('caddy restart'), + 'start' => array('caddy start'), + 'stop' => array('caddy stop'), + ), + 'name' => 'caddy', + 'pidfile' => '/var/run/caddy/caddy.pid' + ); + } + + return $services; +} + +function caddy_xmlrpc_sync() +{ + $result = array(); + + $result[] = array( + 'description' => gettext('Caddy'), + 'section' => 'Pischem.caddy', + 'id' => 'caddy', + 'services' => ["caddy"], + ); + + return $result; +} + +function caddy_syslog() +{ + return ['caddy' => ['facility' => ['caddy']]]; +} diff --git a/www/caddy/src/etc/ssl/ext_sources/caddy.conf b/www/caddy/src/etc/ssl/ext_sources/caddy.conf new file mode 100644 index 0000000000..08b1368b56 --- /dev/null +++ b/www/caddy/src/etc/ssl/ext_sources/caddy.conf @@ -0,0 +1,4 @@ +[location] +base=/var/db/caddy/data/caddy/certificates +pattern=.*\.crt$ +description=Caddy diff --git a/www/caddy/src/etc/syslog-ng.conf.d/caddy.conf b/www/caddy/src/etc/syslog-ng.conf.d/caddy.conf new file mode 100644 index 0000000000..0a5eb450a3 --- /dev/null +++ b/www/caddy/src/etc/syslog-ng.conf.d/caddy.conf @@ -0,0 +1,48 @@ +################################################################### +# Local syslog-ng configuration [caddy]. +################################################################### +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file +# +# Define Unix socket source for Caddy +source s_caddy { + unix-dgram("/var/run/caddy/log.sock"); +}; + +# Parser for Caddy log levels +parser p_caddy_levels { + channel { + filter { + message(".*debug.*") or + message(".*info.*") or + message(".*warn.*") or + message(".*error.*") or + message(".*panic.*") or + message(".*fatal.*"); + }; + rewrite { + set-severity("7" condition(message(".*debug.*"))); # DEBUG -> Debug + set-severity("6" condition(message(".*info.*"))); # INFO -> Informational + set-severity("4" condition(message(".*warn.*"))); # WARN -> Warning + set-severity("3" condition(message(".*error.*"))); # ERROR -> Error + set-severity("2" condition(message(".*panic.*"))); # PANIC -> Critical + set-severity("1" condition(message(".*fatal.*"))); # FATAL -> Alert + }; + }; +}; + +# Destination for Caddy logs +destination d_local_caddy { + file( + "/var/log/caddy/caddy_${YEAR}${MONTH}${DAY}.log" + create-dirs(yes) + flags(syslog-protocol) + ); +}; + +# Log path for processing Caddy logs +log { + source(s_caddy); + parser(p_caddy_levels); + rewrite { set("caddy" value("PROGRAM")); }; + destination(d_local_caddy); +}; diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/DiagnosticsController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/DiagnosticsController.php new file mode 100644 index 0000000000..f8de1f2c13 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/DiagnosticsController.php @@ -0,0 +1,84 @@ +configdRun('caddy config'); + + // Decode JSON to PHP array + $responseArray = json_decode($response, true); + + // Errors are handled by the caddy_diagnostics script and returned, check for an error key in the response + if (isset($responseArray['error'])) { + return ["status" => "failed", "message" => $responseArray['message']]; + } + + // Prepare the response array + $response = ['status' => 'success', 'content' => $responseArray]; + // Set the content type + $this->response->setContentType('application/json', 'UTF-8'); + // Encode and set the content + $this->response->setContent(json_encode($response, JSON_PRETTY_PRINT)); + } + + /** + * Fetch and display the contents of the Caddyfile as JSON. + */ + public function caddyfileAction() + { + $backend = new Backend(); + $response = $backend->configdRun('caddy caddyfile'); + + // Decode JSON to PHP array + $responseArray = json_decode($response, true); + + if (isset($responseArray['error'])) { + return ["status" => "failed", "message" => $responseArray['message']]; + } + + // Return the response as an array which gets automatically encoded to JSON + return ["status" => "success", "content" => $responseArray['content']]; + } +} diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/GeneralController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/GeneralController.php new file mode 100644 index 0000000000..299603a905 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/GeneralController.php @@ -0,0 +1,41 @@ + [ + 'label' => gettext('Domains'), + 'icon' => 'fa fa-fw fa-globe text-success', + 'items' => [] + ], + 'subdomains' => [ + 'label' => gettext('Subdomains'), + 'icon' => 'fa fa-fw fa-globe text-warning', + 'items' => [] + ], + ]; + + foreach ((new \OPNsense\Caddy\Caddy())->reverseproxy->reverse->iterateItems() as $reverse) { + if (!empty($reverse->FromDomain)) { + $port = (string)$reverse->FromPort; + $combined = (string)$reverse->FromDomain . ($port !== '' ? ':' . $port : ''); + + $result['domains']['items'][] = [ + 'value' => (string)$reverse->getAttributes()['uuid'], + 'label' => $combined + ]; + } + } + + foreach ((new \OPNsense\Caddy\Caddy())->reverseproxy->subdomain->iterateItems() as $subdomain) { + if (!empty($subdomain->FromDomain)) { + $result['subdomains']['items'][] = [ + 'value' => (string)$subdomain->getAttributes()['uuid'], + 'label' => (string)$subdomain->FromDomain + ]; + } + } + + foreach (array_keys($result) as $key) { + usort($result[$key]['items'], fn($a, $b) => strcasecmp($a['label'], $b['label'])); + } + + return $result; + } + + /** + * Build a UUID filter function. + * + * @return callable|null + */ + private function buildFilterFunction(): ?callable + { + $domainUuids = $this->request->get('domainUuids'); + if (empty($domainUuids)) { + return null; + } + + return function ($record) use ($domainUuids): bool { + $fieldsToCheck = ['reverse', 'subdomain']; + $uuids = []; + + // Add the record's own UUID + $uuidAttr = $record->getAttributes()['uuid'] ?? null; + if (is_scalar($uuidAttr)) { + $uuids[] = trim((string)$uuidAttr); + } + + // Add UUIDs from model relation fields + foreach ($fieldsToCheck as $field) { + $value = $record->{$field} ?? null; + + foreach ((array)$value as $item) { + if (is_scalar($item)) { + $uuids[] = trim((string)$item); + } + } + } + + return (bool) array_intersect($uuids, $domainUuids); + }; + } + + // ReverseProxy Section + + public function searchReverseProxyAction() + { + return $this->searchBase('reverseproxy.reverse', null, null, $this->buildFilterFunction()); + } + + public function setReverseProxyAction($uuid) + { + return $this->setBase("reverse", "reverseproxy.reverse", $uuid); + } + + public function addReverseProxyAction() + { + return $this->addBase("reverse", "reverseproxy.reverse"); + } + + public function getReverseProxyAction($uuid = null) + { + return $this->getBase("reverse", "reverseproxy.reverse", $uuid); + } + + public function delReverseProxyAction($uuid) + { + return $this->delBase("reverseproxy.reverse", $uuid); + } + + public function toggleReverseProxyAction($uuid, $enabled = null) + { + return $this->toggleBase("reverseproxy.reverse", $uuid, $enabled); + } + + + // Subdomain Section + + public function searchSubdomainAction() + { + return $this->searchBase('reverseproxy.subdomain', null, null, $this->buildFilterFunction()); + } + + public function setSubdomainAction($uuid) + { + return $this->setBase("subdomain", "reverseproxy.subdomain", $uuid); + } + + public function addSubdomainAction() + { + return $this->addBase("subdomain", "reverseproxy.subdomain"); + } + + public function getSubdomainAction($uuid = null) + { + return $this->getBase("subdomain", "reverseproxy.subdomain", $uuid); + } + + public function delSubdomainAction($uuid) + { + return $this->delBase("reverseproxy.subdomain", $uuid); + } + + public function toggleSubdomainAction($uuid, $enabled = null) + { + return $this->toggleBase("reverseproxy.subdomain", $uuid, $enabled); + } + + + // Handler Section + + public function searchHandleAction() + { + return $this->searchBase('reverseproxy.handle', null, null, $this->buildFilterFunction()); + } + + public function setHandleAction($uuid) + { + return $this->setBase("handle", "reverseproxy.handle", $uuid); + } + + public function addHandleAction() + { + return $this->addBase("handle", "reverseproxy.handle"); + } + + public function getHandleAction($uuid = null) + { + return $this->getBase("handle", "reverseproxy.handle", $uuid); + } + + public function delHandleAction($uuid) + { + return $this->delBase("reverseproxy.handle", $uuid); + } + + public function toggleHandleAction($uuid, $enabled = null) + { + return $this->toggleBase("reverseproxy.handle", $uuid, $enabled); + } + + // AccessList Section + + public function searchAccessListAction() + { + return $this->searchBase("reverseproxy.accesslist"); + } + + public function setAccessListAction($uuid) + { + return $this->setBase("accesslist", "reverseproxy.accesslist", $uuid); + } + + public function addAccessListAction() + { + return $this->addBase("accesslist", "reverseproxy.accesslist"); + } + + public function getAccessListAction($uuid = null) + { + return $this->getBase("accesslist", "reverseproxy.accesslist", $uuid); + } + + public function delAccessListAction($uuid) + { + return $this->delBase("reverseproxy.accesslist", $uuid); + } + + + // BasicAuth Section + + public function searchBasicAuthAction() + { + return $this->searchBase("reverseproxy.basicauth"); + } + + public function setBasicAuthAction($uuid) + { + if ($this->request->isPost()) { + $postData = $this->request->getPost(); + if ( + isset($postData['basicauth']['basicauthpass']) + && !empty(trim($postData['basicauth']['basicauthpass'])) + ) { + $plainPassword = $postData['basicauth']['basicauthpass']; + $hashedPassword = password_hash($plainPassword, PASSWORD_BCRYPT); + $_POST['basicauth']['basicauthpass'] = $hashedPassword; + } + } + + return $this->setBase("basicauth", "reverseproxy.basicauth", $uuid); + } + + public function addBasicAuthAction() + { + if ($this->request->isPost()) { + $postData = $this->request->getPost(); + if ( + isset($postData['basicauth']['basicauthpass']) + && !empty(trim($postData['basicauth']['basicauthpass'])) + ) { + $plainPassword = $postData['basicauth']['basicauthpass']; + $hashedPassword = password_hash($plainPassword, PASSWORD_BCRYPT); + $_POST['basicauth']['basicauthpass'] = $hashedPassword; + } + } + + return $this->addBase("basicauth", "reverseproxy.basicauth"); + } + + public function getBasicAuthAction($uuid = null) + { + return $this->getBase("basicauth", "reverseproxy.basicauth", $uuid); + } + + public function delBasicAuthAction($uuid) + { + return $this->delBase("reverseproxy.basicauth", $uuid); + } + + + // Header Section + + public function searchHeaderAction() + { + return $this->searchBase("reverseproxy.header"); + } + + public function setHeaderAction($uuid) + { + return $this->setBase("header", "reverseproxy.header", $uuid); + } + + public function addHeaderAction() + { + return $this->addBase("header", "reverseproxy.header"); + } + + public function getHeaderAction($uuid = null) + { + return $this->getBase("header", "reverseproxy.header", $uuid); + } + + public function delHeaderAction($uuid) + { + return $this->delBase("reverseproxy.header", $uuid); + } + + + // Layer4 Proxy Section + + public function searchLayer4Action() + { + return $this->searchBase("reverseproxy.layer4"); + } + + public function setLayer4Action($uuid) + { + return $this->setBase("layer4", "reverseproxy.layer4", $uuid); + } + + public function addLayer4Action() + { + return $this->addBase("layer4", "reverseproxy.layer4"); + } + + public function getLayer4Action($uuid = null) + { + return $this->getBase("layer4", "reverseproxy.layer4", $uuid); + } + + public function delLayer4Action($uuid) + { + return $this->delBase("reverseproxy.layer4", $uuid); + } + + public function toggleLayer4Action($uuid, $enabled = null) + { + return $this->toggleBase("reverseproxy.layer4", $uuid, $enabled); + } + + + // Layer4 OpenVPN Section + + public function searchLayer4OpenvpnAction() + { + return $this->searchBase("reverseproxy.layer4openvpn"); + } + + public function setLayer4OpenvpnAction($uuid) + { + return $this->setBase("layer4openvpn", "reverseproxy.layer4openvpn", $uuid); + } + + public function addLayer4OpenvpnAction() + { + return $this->addBase("layer4openvpn", "reverseproxy.layer4openvpn"); + } + + public function getLayer4OpenvpnAction($uuid = null) + { + return $this->getBase("layer4openvpn", "reverseproxy.layer4openvpn", $uuid); + } + + public function delLayer4OpenvpnAction($uuid) + { + return $this->delBase("reverseproxy.layer4openvpn", $uuid); + } + + public function toggleLayer4OpenvpnAction($uuid, $enabled = null) + { + return $this->toggleBase("reverseproxy.layer4openvpn", $uuid, $enabled); + } +} diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ServiceController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ServiceController.php new file mode 100644 index 0000000000..6aaa5a578e --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ServiceController.php @@ -0,0 +1,73 @@ +configdRun("template reload " . self::$internalServiceTemplate); + + // Validate the Caddyfile + $validateResult = trim($backend->configdRun('caddy validate')); + + // Attempt to parse the JSON output from the validation result + if (($jsonStartPos = strpos($validateResult, '{"message":')) !== false) { + $jsonOutput = substr($validateResult, $jsonStartPos); + $result = json_decode($jsonOutput, true); + + if (is_array($result) && isset($result['status'])) { + return ["status" => $result['status'], "message" => $result['message']]; + } + } + + // If unable to parse the expected JSON output, return a generic error message + return ["status" => "failed", "message" => gettext("Unable to parse the validation result.")]; + } +} diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/DiagnosticsController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/DiagnosticsController.php new file mode 100644 index 0000000000..55e7bbcc6f --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/DiagnosticsController.php @@ -0,0 +1,41 @@ +view->pick('OPNsense/Caddy/diagnostics'); + } +} diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/GeneralController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/GeneralController.php new file mode 100644 index 0000000000..0986a01dfb --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/GeneralController.php @@ -0,0 +1,44 @@ +view->pick('OPNsense/Caddy/general'); + $this->view->generalForm = $this->getForm("general"); + } +} diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Layer4Controller.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Layer4Controller.php new file mode 100644 index 0000000000..88de730d27 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Layer4Controller.php @@ -0,0 +1,48 @@ +view->entrypoint = 'layer4'; + $this->view->pick('OPNsense/Caddy/reverse_proxy'); + + $this->view->formDialogLayer4 = $this->getForm('dialogLayer4'); + $this->view->formGridLayer4 = $this->getFormGrid('dialogLayer4', 'layer4'); + + $this->view->formDialogLayer4Openvpn = $this->getForm('dialogLayer4Openvpn'); + $this->view->formGridLayer4Openvpn = $this->getFormGrid('dialogLayer4Openvpn', 'layer4_openvpn'); + } +} diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/ReverseProxyController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/ReverseProxyController.php new file mode 100644 index 0000000000..49a08c4c41 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/ReverseProxyController.php @@ -0,0 +1,60 @@ +view->entrypoint = 'reverse_proxy'; + $this->view->pick('OPNsense/Caddy/reverse_proxy'); + + $this->view->formDialogReverseProxy = $this->getForm("dialogReverseProxy"); + $this->view->formGridReverseProxy = $this->getFormGrid('dialogReverseProxy', 'reverse_proxy'); + + $this->view->formDialogSubdomain = $this->getForm("dialogSubdomain"); + $this->view->formGridSubdomain = $this->getFormGrid('dialogSubdomain', 'subdomain'); + + $this->view->formDialogHandle = $this->getForm("dialogHandle"); + $this->view->formGridHandle = $this->getFormGrid('dialogHandle', 'handle'); + + $this->view->formDialogAccessList = $this->getForm("dialogAccessList"); + $this->view->formGridAccessList = $this->getFormGrid('dialogAccessList', 'access_list'); + + $this->view->formDialogBasicAuth = $this->getForm("dialogBasicAuth"); + $this->view->formGridBasicAuth = $this->getFormGrid('dialogBasicAuth', 'basic_auth'); + + $this->view->formDialogHeader = $this->getForm("dialogHeader"); + $this->view->formGridHeader = $this->getFormGrid('dialogHeader', 'header'); + } +} diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogAccessList.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogAccessList.xml new file mode 100644 index 0000000000..9c9fe2077c --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogAccessList.xml @@ -0,0 +1,63 @@ +
    + + accesslist.accesslistName + + text + + + + accesslist.clientIps + + select_multiple + + true + + + + accesslist.accesslistInvert + + checkbox + + + boolean + boolean + + + + accesslist.HttpResponseCode + + text + abort + + true + + false + + + + accesslist.HttpResponseMessage + + text + + true + + false + + + + accesslist.RequestMatcher + + dropdown + + true + + false + + + + accesslist.description + + text + + +
    diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogBasicAuth.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogBasicAuth.xml new file mode 100644 index 0000000000..72862167b4 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogBasicAuth.xml @@ -0,0 +1,23 @@ +
    + + basicauth.basicauthuser + + text + + + + basicauth.basicauthpass + + text + + + true + + + + basicauth.description + + text + + +
    diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHandle.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHandle.xml new file mode 100644 index 0000000000..3e72c80e26 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHandle.xml @@ -0,0 +1,450 @@ +
    + + handle.enabled + + checkbox + + + 20 + boolean + rowtoggle + + + + header + + + + handle.reverse + + dropdown + + + false + + + + handle.subdomain + + dropdown + + + false + + + + header + + + + handle.HandleType + + dropdown + + true + + false + + + + handle.HandlePath + + text + any + + + false + + + + header + + true + + + + handle.accesslist + + dropdown + + + + false + + + + handle.basicauth + + select_multiple + 5 + + + + false + + + + handle.ForwardAuth + + checkbox + + + + false + boolean + boolean + + + + header + + + + handle.HandleDirective + + dropdown + + + + header + + true + + + + handle.HttpVersion + + dropdown + + + + false + + + + handle.header + + dropdown + select_multiple + 5 + + + + false + + + + handle.HttpKeepalive + + text + 120 + + + + false + + + + header + + + + handle.HttpTls + + dropdown + + + false + + + + handle.ToDomain + + select_multiple + + true + + + to_domain + + + + handle.ToPort + + text + + + false + + + + handle.ToPath + + text + + + false + + + + handle.HttpTlsInsecureSkipVerify + + checkbox + + + + false + boolean + boolean + + + + handle.HttpTlsTrustedCaCerts + + dropdown + + + + false + + + + handle.HttpTlsServerName + + text + + + + false + + + + handle.HttpNtlm + + checkbox + + + + false + boolean + boolean + + + + handle.description + + text + + + + header + + true + + + + handle.lb_policy + + dropdown + + + + false + + + + handle.lb_retries + + text + + off + + + false + + + + handle.lb_try_duration + + text + + 0 + + + false + + + + handle.lb_try_interval + + text + + 250 + + + false + + + + handle.PassiveHealthFailDuration + + text + + off + + + false + + + + handle.PassiveHealthMaxFails + + text + + 1 + + + false + + + + handle.PassiveHealthUnhealthyStatus + + text + + off + + + false + + + + handle.PassiveHealthUnhealthyLatency + + text + + off + + + false + + + + handle.PassiveHealthUnhealthyRequestCount + + text + + off + + + false + + + + handle.health_uri + + text + + + + false + + + + handle.health_upstream + + text + + + + false + + + + handle.health_port + + text + + + + false + + + + handle.health_interval + + text + + 30 + + + false + + + + handle.health_passes + + text + + 1 + + + false + + + + handle.health_fails + + text + + 1 + + + false + + + + handle.health_timeout + + text + + 5 + + + false + + + + handle.health_status + + text + + 200 + + + false + + + + handle.health_body + + text + + + + false + + + + handle.health_follow_redirects + + checkbox + + + + false + boolean + boolean + + +
    diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHeader.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHeader.xml new file mode 100644 index 0000000000..163b644517 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogHeader.xml @@ -0,0 +1,41 @@ +
    + + header.HeaderUpDown + + dropdown + + + 120 + + + + header.HeaderType + + text + + + + header.HeaderValue + + text + + + false + + + + header.HeaderReplace + + text + + + false + + + + header.description + + text + + +
    diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4.xml new file mode 100644 index 0000000000..259cca425f --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4.xml @@ -0,0 +1,210 @@ +
    + + layer4.enabled + + checkbox + + + 20 + boolean + rowtoggle + + + + layer4.Sequence + + text + + + false + + + + header + + + + layer4.Type + + dropdown + + + false + + + + layer4.Protocol + + dropdown + + + + false + + + + layer4.FromPort + + text + + + + false + + + + header + + + + layer4.Matchers + + dropdown + + + + layer4.FromDomain + + select_multiple + + true + + + + layer4.FromOpenvpnModes + + dropdown + + + + false + + + + layer4.FromOpenvpnStaticKey + + select_multiple + + Any + 5 + + + false + + + + layer4.InvertMatchers + + checkbox + + true + + false + boolean + boolean + + + + layer4.TerminateTls + + checkbox + + + + false + boolean + boolean + + + + header + + + + layer4.ToDomain + + select_multiple + + true + + + to_domain + + + + layer4.ToPort + + text + + + false + + + + layer4.ProxyProtocol + + dropdown + + + false + + + + layer4.description + + text + + + + header + + true + + + layer4.lb_policy + + dropdown + + + + false + + + + layer4.PassiveHealthFailDuration + + text + off + + true + + false + + + + layer4.PassiveHealthMaxFails + + text + 1 + + true + + false + + + + header + + + + layer4.RemoteIp + + select_multiple + + true + + + false + + +
    diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4Openvpn.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4Openvpn.xml new file mode 100644 index 0000000000..2ac8521216 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4Openvpn.xml @@ -0,0 +1,16 @@ + + + layer4openvpn.description + + text + + + layer4openvpn.StaticKey + + textbox + Paste an OpenVPN Static key. + + true + + + diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogReverseProxy.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogReverseProxy.xml new file mode 100644 index 0000000000..ed431a6dbe --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogReverseProxy.xml @@ -0,0 +1,156 @@ +
    + + reverse.enabled + + checkbox + + + 20 + boolean + rowtoggle + + + + header + + + + reverse.DisableTls + + dropdown + + + false + + + + reverse.FromDomain + + text + + + from_domain + + + + reverse.FromPort + + text + + + false + + + + reverse.CustomCertificate + + dropdown + + + + false + + + + reverse.AcmePassthrough + + text + + true + + false + + + + reverse.DnsChallenge + + checkbox + + + + false + boolean + boolean + + + + reverse.DnsChallengeOverrideDomain + + text + + true + + false + + + + reverse.DynDns + + checkbox + + + false + boolean + boolean + + + + reverse.description + + text + + + + header + + + + reverse.accesslist + + dropdown + + + false + + + + reverse.basicauth + + select_multiple + 5 + + + false + + + + reverse.ClientAuthTrustPool + + select_multiple + + + false + + + + reverse.ClientAuthMode + + dropdown + true + + + false + + + + reverse.AccessLog + + checkbox + + + false + boolean + boolean + + +
    diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml new file mode 100644 index 0000000000..4cdceffe36 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogSubdomain.xml @@ -0,0 +1,101 @@ +
    + + subdomain.enabled + + checkbox + + + 20 + boolean + rowtoggle + + + + header + + + + subdomain.reverse + + dropdown + + + false + + + + subdomain.FromDomain + + text + + + + subdomain.DynDns + + checkbox + + + false + boolean + boolean + + + + subdomain.AcmePassthrough + + text + + true + + false + + + + subdomain.description + + text + + + + header + + + + subdomain.accesslist + + dropdown + + + false + + + + subdomain.basicauth + + select_multiple + 5 + + + false + + + + subdomain.ClientAuthTrustPool + + select_multiple + + + false + + + + subdomain.ClientAuthMode + + dropdown + true + + + false + + +
    diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml new file mode 100644 index 0000000000..bb7d29b7b0 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/general.xml @@ -0,0 +1,283 @@ +
    + + + caddy.general.enabled + + checkbox + + + + caddy.general.EnableLayer4 + + checkbox + + + + caddy.general.TlsEmail + + text + + + + caddy.general.TlsAutoHttps + + dropdown + + + + + + header + + + + caddy.general.DisableSuperuser + + dropdown + + + + header + + + + caddy.general.HttpVersions + + select_multiple + + + + caddy.general.HttpPort + + text + 80 + + + + caddy.general.HttpsPort + + text + 443 + + + + header + + + + caddy.general.accesslist + + dropdown + + + + caddy.general.ClientIpHeaders + + dropdown + select_multiple + 5 + + X-Forwarded-For + + + + caddy.general.GracePeriod + + text + 10 + + + + caddy.general.timeout_read_body + + text + no timout + + + + caddy.general.timeout_read_header + + text + no timout + + + + caddy.general.timeout_write + + text + no timout + + + + caddy.general.timeout_idle + + text + 300 + + + + + + caddy.general.LogLevel + + dropdown + + + + caddy.general.LogCredentials + + checkbox + + + + caddy.general.LogAccessPlain + + checkbox + + + + caddy.general.LogAccessPlainKeep + + 10 + text + + + + + + caddy.general.TlsDnsProvider + + dropdown + + + + caddy.general.TlsDnsApiKey + + text + + + + header + + + + caddy.general.TlsDnsPropagationResolvers + + text + + + + caddy.general.TlsDnsPropagationTimeout + + checkbox + + + + caddy.general.TlsDnsPropagationTimeoutPeriod + + text + 120 + + + + caddy.general.TlsDnsPropagationDelay + + text + 0 + + + + header + + + + caddy.general.TlsDnsEchDomain + + text + + + + header + + + + caddy.general.DynDnsIpVersions + + dropdown + + + + caddy.general.DynDnsUpdateOnly + + checkbox + + + + caddy.general.DynDnsInterval + + text + 1800 + + + + caddy.general.DynDnsTtl + + text + + + + caddy.general.DynDnsSimpleHttp + + text + + + + caddy.general.DynDnsInterface + + dropdown + + + + + + caddy.general.AuthProvider + + dropdown + + + + caddy.general.AuthToTls + + dropdown + + + + caddy.general.AuthToDomain + + text + + + + caddy.general.AuthToPort + + text + + + + caddy.general.AuthToUri + + text + + + + caddy.general.CopyHeaders + + dropdown + select_multiple + 5 + + + + + general-settings +
    diff --git a/www/caddy/src/opnsense/mvc/app/library/OPNsense/System/Status/CaddyOverrideStatus.php b/www/caddy/src/opnsense/mvc/app/library/OPNsense/System/Status/CaddyOverrideStatus.php new file mode 100644 index 0000000000..73b383cb06 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/library/OPNsense/System/Status/CaddyOverrideStatus.php @@ -0,0 +1,60 @@ +internalPriority = 2; + $this->internalPersistent = true; + $this->internalIsBanner = true; + $this->internalTitle = gettext('Caddy config override'); + $this->internalScope = [ + '/ui/caddy/layer4', + '/ui/caddy/diagnostics', + '/ui/caddy/reverse_proxy', + '/ui/caddy/general' + ]; + } + + public function collectStatus() + { + if (count(glob('/usr/local/etc/caddy/caddy.d/*'))) { + $this->internalMessage = gettext( + 'The configuration contains manual overwrites, these may interfere with the settings configured here.' + ); + $this->internalStatus = SystemStatusCode::NOTICE; + } + } +} diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/ACL/ACL.xml b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/ACL/ACL.xml new file mode 100644 index 0000000000..56bd973028 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/ACL/ACL.xml @@ -0,0 +1,12 @@ + + + Services: Caddy + Allow access to Caddy + + ui/caddy/* + ui/diagnostics/log/core/caddy + api/caddy/* + api/diagnostics/log/core/caddy/* + + + diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.php b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.php new file mode 100644 index 0000000000..2be38c8bb6 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.php @@ -0,0 +1,318 @@ +reverseproxy->reverse->iterateItems() as $item) { + $key = $item->__reference; + $fromDomain = (string) $item->FromDomain; + $fromPort = (string) $item->FromPort; + + if ($fromPort === '') { + $defaultPorts = ['80', '443']; + } else { + $defaultPorts = [$fromPort]; + } + + foreach ($defaultPorts as $port) { + $comboKey = $fromDomain . ':' . $port; + + if (isset($combos[$comboKey])) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'Duplicate entry: The combination of %s and port %s is already used. ' . + 'Each combination of domain and port must be unique.' + ), + $fromDomain, + $port + ), + $key . ".FromDomain" + )); + } else { + $combos[$comboKey] = true; + } + } + } + } + + // Prevent the usage of conflicting options when TLS is deactivated for a Domain + private function checkDisableTlsConflicts($messages) + { + foreach ($this->reverseproxy->reverse->iterateItems() as $item) { + if ($item->isFieldChanged()) { + if ((string) $item->DisableTls === '1') { + $conflictChecks = [ + 'DnsChallenge' => (string) $item->DnsChallenge === '1', + 'AcmePassthrough' => !empty((string) $item->AcmePassthrough), + 'CustomCertificate' => !empty((string) $item->CustomCertificate) + ]; + + $conflictFields = array_keys(array_filter($conflictChecks)); + + if (!empty($conflictFields)) { + $messages->appendMessage(new Message( + gettext( + 'TLS cannot be disabled if one of the following options are used: ' . + '"DNS-01 Challenge", "HTTP-01 Challenge Redirection" and "Custom Certificate"' + ), + $item->__reference . ".DisableTls" + )); + } + } + } + } + } + + /** + * Check that when Superuser is disabled, all ports are 1024 and above. + * In General settings where this triggers, a validation dialog will show the hidden validation of the domain ports. + * The default HTTP and HTTPS ports are not allowed to be empty, since then they are 80 and 443. + * Domain ports are allowed to be empty, since then they have the same value as the HTTP and HTTPS default ports. + * Any value that is below 1024 will trigger the validation. + */ + private function checkSuperuserPorts($messages) + { + if ((string)$this->general->DisableSuperuser === '1') { + $httpPort = !empty((string)$this->general->HttpPort) ? (string)$this->general->HttpPort : 80; + $httpsPort = !empty((string)$this->general->HttpsPort) ? (string)$this->general->HttpsPort : 443; + + // Check default HTTP port + if ($httpPort < 1024) { + $messages->appendMessage(new Message( + gettext( + 'www user is active, HTTP port must not be empty and must be 1024 or above.' + ), + "general.HttpPort" + )); + } + + // Check default HTTPS port + if ($httpsPort < 1024) { + $messages->appendMessage(new Message( + gettext( + 'www user is active, HTTPS port must not be empty and must be 1024 or above.' + ), + "general.HttpsPort" + )); + } + + // Check ports under domain configurations + foreach ($this->reverseproxy->reverse->iterateItems() as $item) { + $fromPort = !empty((string)$item->FromPort) ? (string)$item->FromPort : null; + + if ($fromPort !== null && $fromPort < 1024) { + $messages->appendMessage(new Message( + gettext( + 'www user is active, port must be empty or must be 1024 or above.' + ), + $item->__reference . ".FromPort" + )); + $messages->appendMessage(new Message( + gettext( + 'Ports in "Reverse Proxy - Domains" must be empty or must be 1024 or above.' + ), + "general.DisableSuperuser" + )); + } + } + + foreach ($this->reverseproxy->layer4->iterateItems() as $item) { + $fromPort = !empty((string)$item->FromPort) ? (string)$item->FromPort : null; + + if ($fromPort !== null && $fromPort < 1024) { + $messages->appendMessage(new Message( + gettext( + 'www user is active, port must be empty or must be 1024 or above.' + ), + $item->__reference . ".FromPort" + )); + $messages->appendMessage(new Message( + gettext( + 'Ports in "Reverse Proxy - Layer4 Routes" must be empty or must be 1024 or above.' + ), + "general.DisableSuperuser" + )); + } + } + } + } + + private function checkLayer4Matchers($messages) + { + foreach ($this->reverseproxy->layer4->iterateItems() as $item) { + if ($item->isFieldChanged()) { + $key = $item->__reference; + if ( + in_array((string)$item->Matchers, ['httphost', 'tlssni', 'quicsni']) && + empty((string)$item->FromDomain) + ) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'When "%s" matcher is selected, domain is required.' + ), + $item->Matchers + ), + $key . ".FromDomain" + )); + } elseif ( + !in_array((string)$item->Matchers, ['httphost', 'tlssni', 'quicsni']) && + ( + !empty((string)$item->FromDomain) && + (string)$item->FromDomain != '*' + ) + ) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'When "%s" matcher is selected, domain must be empty or *.' + ), + $item->Matchers + ), + $key . ".FromDomain" + )); + } + + if (!in_array((string)$item->Matchers, ['tlssni', 'quicsni']) && !empty((string)$item->TerminateTls)) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'When "%s" matcher is selected, TLS can not be terminated.' + ), + $item->Matchers + ), + $key . ".TerminateTls" + )); + } + + if ((string)$item->Matchers !== 'openvpn' && !empty((string)$item->FromOpenvpnModes)) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'When "%s" matcher is selected, field must be empty.' + ), + $item->Matchers + ), + $key . ".FromOpenvpnModes" + )); + } + + if ((string)$item->Matchers !== 'openvpn' && !empty((string)$item->FromOpenvpnStaticKey)) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'When "%s" matcher is selected, field must be empty.' + ), + $item->Matchers + ), + $key . ".FromOpenvpnStaticKey" + )); + } + + if ((string)$item->Type === 'global' && empty((string)$item->FromPort)) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'When routing type is "%s", port is required.' + ), + $item->Type + ), + $key . ".FromPort" + )); + } elseif ((string)$item->Type !== 'global' && !empty((string)$item->FromPort)) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'When routing type is "%s", port must be empty.' + ), + $item->Type + ), + $key . ".FromPort" + )); + } + + if ((string)$item->Type !== 'global' && ((string)$item->Protocol !== 'tcp')) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'When routing type is "%s", protocol must be TCP.' + ), + $item->Type + ), + $key . ".Protocol" + )); + } + + if ( + (string)$item->Type !== 'global' && + ( + (string)$item->Matchers == 'tls' || + (string)$item->Matchers == 'http' || + (string)$item->Matchers == 'quic' + ) + ) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'When routing type is "%s", matchers "HTTP", "TLS" or "QUIC" cannot be chosen.' + ), + $item->Type + ), + $key . ".Matchers" + )); + } + } + } + } + + // Perform the actual validation + public function performValidation($validateFullModel = false) + { + $messages = parent::performValidation($validateFullModel); + + $this->checkForUniquePortCombos($messages); + $this->checkDisableTlsConflicts($messages); + $this->checkSuperuserPorts($messages); + $this->checkLayer4Matchers($messages); + + return $messages; + } +} diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml new file mode 100644 index 0000000000..8a1ffeb463 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml @@ -0,0 +1,720 @@ + + //Pischem/caddy + Caddy Reverse Proxy + 1.3.7 + + + + 0 + Y + + + + + + + On (default) + + Off + Disable Redirects + Disable Certs + Ignore Loaded Certs + + + + None (default) + + Cloudflare + + + + + + 1 + Please enter a minimum number of 1 or leave empty for default. + + + 1 + Please enter a minimum number of 1 or leave empty for default. + + + N + + + N + + + + + OPNsense.Caddy.Caddy + reverseproxy.accesslist + accesslistName,description + %s %s + + + + + + + OPNsense.Caddy.Caddy + reverseproxy.header + HeaderType,description + %s %s + + + Y + + + Y + 0 + + root (default) + www + + + + 10 + 1 + 20 + Please enter a valid grace period between 1 and 20 seconds. + Y + + + Y + h1,h2 + Y + +

    HTTP/1.1

    +

    HTTP/2

    +

    HTTP/3

    +
    +
    + + 0 + Please enter a minimum value of 0 or leave empty for defaults. + + + 0 + Please enter a minimum value of 0 or leave empty for defaults. + + + 0 + Please enter a minimum value of 0 or leave empty for defaults. + + + 0 + Please enter a minimum value of 0 or leave empty for defaults. + + + + + 10 + 1 + Please enter a valid number of 1 or larger. + Y + + + INFO + + DEBUG + WARN + ERROR + PANIC + FATAL + + + + + + 1 + 2147483647 + Please enter a valid number from 1 to 2147483647 seconds or leave empty for default. + + + IPv4+IPv6 + + IPv4 only + IPv6 only + + + + 0 + 2147483647 + Please enter a valid number from 0 to 2147483647 seconds or leave empty for default. + + + + None (default) + + Authelia + Authentik + + + + + + Y + 0 + + http:// + https:// + + + + /^(\/.*)?$/u + Please enter a valid URI that starts with '/'. + + + + + OPNsense.Caddy.Caddy + reverseproxy.header + HeaderType,description + %s %s + + + Y + +
    + + + + 1 + Y + + + Y + N + Y + Please enter a valid domain name. + + + + + + OPNsense.Caddy.Caddy + reverseproxy.accesslist + accesslistName,description + %s %s + + + + + + + OPNsense.Caddy.Caddy + reverseproxy.basicauth + basicauthuser,description + %s %s + + + Y + + + + + N + Please enter a valid domain name. + + + Auto HTTPS + + + + + + Y + 0 + + https:// + http:// + + + + require_and_verify + + request + require + verify_if_given + + + + ca + Y + + + + + 1 + Y + + + Y + + + OPNsense.Caddy.Caddy + reverseproxy.reverse + FromDomain,FromPort + %s %s + + /^\*\./ + + + + + + Y + N + Please enter a valid domain name. + + + + + OPNsense.Caddy.Caddy + reverseproxy.accesslist + accesslistName,description + %s %s + + + + + + + OPNsense.Caddy.Caddy + reverseproxy.basicauth + basicauthuser,description + %s %s + + + Y + + + + + + require_and_verify + + request + require + verify_if_given + + + + ca + Y + + + + + 1 + Y + + + Y + + + OPNsense.Caddy.Caddy + reverseproxy.reverse + DisableTls,FromDomain,FromPort + %s%s %s + + + + + + + OPNsense.Caddy.Caddy + reverseproxy.subdomain + FromDomain,description + %s %s + + + + + Y + handle + + handle + handle_path + + + + /^(\/.*)?$/u + Please enter a valid path that starts with '/'. + + + + + OPNsense.Caddy.Caddy + reverseproxy.accesslist + accesslistName,description + %s %s + + + + + + + OPNsense.Caddy.Caddy + reverseproxy.basicauth + basicauthuser,description + %s %s + + + Y + +
    + + + OPNsense.Caddy.Caddy + reverseproxy.header + HeaderUpDown,HeaderType,HeaderValue,description + %s %s %s %s + + + Y +
    + + Y + reverse_proxy + + reverse_proxy + redir + + + + Y + Y + Please enter one or multiple valid IP addresses, hostnames or FQDNs. + + + + /^(\/.*)?$/u + Please enter a valid path that starts with '/'. + + + + Y + 0 + + http:// + https:// + h2c:// + + + + HTTPS and NTLM must be enabled at the same time. + DependConstraint + + HttpNtlm + + + + + + HTTP/1.1, HTTP/2 (default) + + HTTP/1.1 + HTTP/2 + HTTP/3 + + + + 0 + 86400 + + + + + HttpNtlm.check001 + + + + + + ca + + + + random + + first + round_robin + least_conn + ip_hash + client_ip_hash + uri_hash + + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + 2 + Please enter a minimum value of 2 or leave empty for defaults. + + + /^(100|[1-5][0-9]{2}|[1-5]xx)$/u + Please enter a valid HTTP response code like 404 a status code class like 4xx. + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + + /^(\/.*)?$/u + Please enter a valid URI that starts with '/'. + + + + 1 + 65535 + Please enter a minimum value of 1-65535 or leave empty for defaults. + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + /^(100|[1-5][0-9]{2}|[1-5]xx)$/u + Please enter a 3-digit status code or a status code ending in xx or leave empty for defaults. + + + +
    + + + Y + + + Y + Y + Y + Please enter one or multiple valid IP addresses or networks. + + + + 100 + 599 + Please enter a valid HTTP response code between 100 and 599. + + + + Y + client_ip + + client_ip + remote_ip + + + + + + + Y + /^([0-9a-zA-Z]{2,72})$/u + A user name must only contain numbers and letters and must be between 2 and 72 characters. + + + Y + + + +
    + + header_up + + header_up + header_down + + Y + + + Y + /^([^"]{0,1024})$/u + The header type must not contain quotation marks (") and must be less than 1024 characters. + + + /^([^"]{0,1024})$/u + The header value must not contain quotation marks (") and must be less than 1024 characters. + + + /^([^"]{0,1024})$/u + The header replacement must not contain quotation marks (") and must be less than 1024 characters. + + +
    + + + 1 + Y + + + 1 + 99999 + Please enter a value between 1 and 99999 or leave empty. + + + Sequence value has to be unique or empty. + UniqueConstraint + + + + + Y + listener_wrappers + + listener_wrappers + global + + + + Y + tcp + + TCP + UDP + + + + + N + Y + Y + Y + Please enter one or multiple hostnames or FQDNs. + + + Any + + tls-auth_sha256_normal + tls-auth_sha256_inverse + tls-auth_sha512_normal + tls-auth_sha512_inverse + tls-crypt + tls-crypt2_client + tls-crypt2_server + + + + + + OPNsense.Caddy.Caddy + reverseproxy.layer4openvpn + description + %s + + + Y + + + Y + tlssni + + ANY + DNS + HTTP + HTTP (Host Header) + OpenVPN + Postgres + Proxy Protocol + QUIC + QUIC (SNI Client Hello) + RDP + SOCKSv4 + SOCKSv5 + SSH + TLS + TLS (SNI Client Hello) + Winbox + Wireguard + XMPP + + + + + Y + Y + Please enter one or multiple valid IP addresses, hostnames or FQDNs. + + + + Y + + + Off (default) + + v1 + v2 + + + + random + + first + round_robin + least_conn + ip_hash + client_ip_hash + uri_hash + + + + 1 + Please enter a minimum value of 1 or leave empty for defaults. + + + 2 + Please enter a minimum value of 2 or leave empty for defaults. + + + Y + Y + Please enter one or multiple valid IP addresses or networks. + + + + + + Y + + + Y + + + Description must be unique. + UniqueConstraint + + + + +
    +
    +
    diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Menu/Menu.xml b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Menu/Menu.xml new file mode 100644 index 0000000000..421560b530 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Menu/Menu.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/diagnostics.volt b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/diagnostics.volt new file mode 100644 index 0000000000..4d9a3dc9f8 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/diagnostics.volt @@ -0,0 +1,178 @@ +{# + # Copyright (c) 2024 Cedrik Pischem + # 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. + #} + + + + + + + + + +
    + +
    +
    +
    
    +            
    +            
    +            

    +
    +
    + +
    +
    +
    
    +            
    +            

    +
    +
    +
    diff --git a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt new file mode 100644 index 0000000000..fd56411d1f --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt @@ -0,0 +1,118 @@ +{# + # Copyright (c) 2023-2025 Cedrik Pischem + # 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_tabs_content", ['formData': generalForm]) }} + +
    + +
    +
    diff --git a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt new file mode 100644 index 0000000000..9ea070147d --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt @@ -0,0 +1,579 @@ +{# + # Copyright (c) 2023-2025 Cedrik Pischem + # 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. + #} + + + + + + + + + +
    + +{% if entrypoint == 'reverse_proxy' %} + + +
    + +

    {{ lang._('Domains') }}

    + {{ partial('layout_partials/base_bootgrid_table', formGridReverseProxy + {'command_width': '160'})}} + + +

    {{ lang._('Subdomains') }}

    + {{ partial('layout_partials/base_bootgrid_table', formGridSubdomain + {'command_width': '160'})}} +
    + + +
    +

    {{ lang._('Handlers') }}

    + {{ partial('layout_partials/base_bootgrid_table', formGridHandle)}} +
    + + +
    + +

    {{ lang._('Access Lists') }}

    + {{ partial('layout_partials/base_bootgrid_table', formGridAccessList)}} + + +

    {{ lang._('Basic Auth') }}

    + {{ partial('layout_partials/base_bootgrid_table', formGridBasicAuth)}} +
    + + +
    +

    {{ lang._('Headers') }}

    + {{ partial('layout_partials/base_bootgrid_table', formGridHeader)}} +
    + +{% elseif entrypoint == 'layer4' %} + + +
    +

    {{ lang._('Layer4 Routes') }}

    + {{ partial('layout_partials/base_bootgrid_table', formGridLayer4)}} +
    + + +
    +

    {{ lang._('OpenVPN Static Keys') }}

    + {{ partial('layout_partials/base_bootgrid_table', formGridLayer4Openvpn)}} +
    + +{% endif %} + +
    + +{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/caddy/service/reconfigure', 'data_service_widget': 'caddy'}) }} + +{% if entrypoint == 'reverse_proxy' %} + +{{ partial("layout_partials/base_dialog",['fields':formDialogReverseProxy,'id':formGridReverseProxy['edit_dialog_id'],'label':lang._('Edit Domain')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogSubdomain,'id':formGridSubdomain['edit_dialog_id'],'label':lang._('Edit Subdomain')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogHandle,'id':formGridHandle['edit_dialog_id'],'label':lang._('Edit Handler')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogAccessList,'id':formGridAccessList['edit_dialog_id'],'label':lang._('Edit Access List')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogBasicAuth,'id':formGridBasicAuth['edit_dialog_id'],'label':lang._('Edit Basic Auth')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogHeader,'id':formGridHeader['edit_dialog_id'],'label':lang._('Edit Header')])}} + +{% elseif entrypoint == 'layer4' %} + +{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4,'id':formGridLayer4['edit_dialog_id'],'label':lang._('Edit Layer4 Route')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4Openvpn,'id':formGridLayer4Openvpn['edit_dialog_id'],'label':lang._('Edit OpenVPN Static Key')])}} + +{% endif %} diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php new file mode 100755 index 0000000000..bcf424ece4 --- /dev/null +++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php @@ -0,0 +1,133 @@ +#!/usr/local/bin/php +reverseproxy->reverse->iterateItems() as $reverseItem) { + $certRef = (string)$reverseItem->CustomCertificate; + if (!empty($certRef)) { + $certificateRefs[] = $certRef; + } +} + +$certificateRefs = array_unique($certificateRefs); + +foreach ((new Cert())->cert->iterateItems() as $cert) { + $refid = (string)$cert->refid; + + if (in_array($refid, $certificateRefs, true)) { + $certChain = base64_decode((string)$cert->crt); + $certKey = base64_decode((string)$cert->prv); + + if (!empty((string)$cert->caref)) { + $ca = CertStore::getCaChain((string)$cert->caref); + if ($ca) { + $certChain .= "\n" . $ca; + } + } + + $writeFileIfChanged($tempDir . $refid . '.pem', $certChain); + $writeFileIfChanged($tempDir . $refid . '.key', $certKey); + } +} + +// ca certificate +$caCertRefs = []; + +foreach ((new Caddy())->reverseproxy->handle->iterateItems() as $handleItem) { + $caCertField = (string)$handleItem->HttpTlsTrustedCaCerts; + + if (!empty($caCertField)) { + $caCertRefs[] = $caCertField; + } +} + +foreach ((new Caddy())->reverseproxy->reverse->iterateItems() as $reverseItem) { + $caCertField = (string)$reverseItem->ClientAuthTrustPool; + + if (!empty($caCertField)) { + $refs = array_map('trim', explode(',', $caCertField)); + foreach ($refs as $ref) { + if (!empty($ref)) { + $caCertRefs[] = $ref; + } + } + } +} + +foreach ((new Caddy())->reverseproxy->subdomain->iterateItems() as $subdomainItem) { + $caCertField = (string)$subdomainItem->ClientAuthTrustPool; + + if (!empty($caCertField)) { + $refs = array_map('trim', explode(',', $caCertField)); + foreach ($refs as $ref) { + if (!empty($ref)) { + $caCertRefs[] = $ref; + } + } + } +} + +$caCertRefs = array_unique($caCertRefs); + +foreach ((new Ca())->ca->iterateItems() as $caItem) { + $refid = (string)$caItem->refid; + if (in_array($refid, $caCertRefs, true)) { + $caCert = base64_decode((string)$caItem->crt); + $writeFileIfChanged($tempDir . $refid . '.pem', $caCert); + } +} + +// openvpn static keys +foreach ((new Caddy())->reverseproxy->layer4openvpn->iterateItems() as $openvpnItem) { + $writeFileIfChanged( + $tempDir . (string)$openvpnItem->getAttributes()['uuid'] . '.key', + (string)$openvpnItem->StaticKey + ); +} diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_control.py b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_control.py new file mode 100755 index 0000000000..8acc99f2b8 --- /dev/null +++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_control.py @@ -0,0 +1,135 @@ +#!/usr/local/bin/python3 + +# +# Copyright (c) 2023-2024 Cedrik Pischem +# 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. +# + +import subprocess +import json +import sys +import os +import signal +import time + + +def kill_and_start_caddy(pidfile): + """ + Caddy can fail to reload in rare circumstances when + persistent keepalive connections are open with the NTLM + module active + """ + if os.path.exists(pidfile): + try: + with open(pidfile, 'r') as f: + pid = int(f.read().strip()) + + os.kill(pid, signal.SIGKILL) + time.sleep(2) + subprocess.run(["service", "caddy", "start"], check=True) + except Exception as e: + print(f"Error: {str(e)}") + else: + subprocess.run(["service", "caddy", "start"], check=True) + + +def run_service_command(service_action, action_message): + """ + Includes special actions like a validation and + timeouts that force a restart when caddy is unresponsive + """ + result = {"message": action_message} + pidfile = "/var/run/caddy/caddy.pid" + + if service_action == "validate": + try: + validation_output = subprocess.check_output( + ["caddy", "validate", "--config", "/usr/local/etc/caddy/Caddyfile"], stderr=subprocess.STDOUT, + text=True) + if "Valid configuration" in validation_output: + result["status"] = "ok" + result["message"] = "Caddy configuration is valid." + else: + error_msg = next((line for line in validation_output.split('\n') if line.startswith("Error:")), + "Caddy configuration is not valid.") + result["status"] = "failed" + result["message"] = error_msg + except subprocess.CalledProcessError as e: + error_msg = next((line for line in e.output.split('\n') if line.startswith("Error:")), "Validation failed.") + result["status"] = "failed" + result["message"] = error_msg + elif service_action in ["stop", "restart", "reloadssl"]: + try: + proc = subprocess.Popen(["service", "caddy", service_action]) + try: + proc.wait(timeout=20) + result["status"] = "ok" + except subprocess.TimeoutExpired: + kill_and_start_caddy(pidfile) + result["status"] = "ok" + result["message"] = f"{service_action.capitalize()} took too long, Caddy was forcefully restarted." + except subprocess.CalledProcessError as e: + result["status"] = "failed" + result["message"] = str(e) + else: + try: + subprocess.run(["service", "caddy", service_action], check=True) + result["status"] = "ok" + except subprocess.CalledProcessError as e: + result["status"] = "failed" + result["message"] = str(e) + + return json.dumps(result) + + +# "cmd_action": "service_action" +actions = { + "start": "start", + "stop": "stop", + "restart": "restart", + "reload": "reloadssl", + "validate": "validate" +} + +if __name__ == "__main__": + if len(sys.argv) > 1: + action = sys.argv[1] + if action in actions: + cmd_action = action + service_action = actions[action] + message = f"{cmd_action.capitalize()} Caddy service" + + # Call setup script for 'validate' and 'reloadssl' actions. This is needed because the setup script triggers + # the caddy_certs.php script, which exports all certificates into the filesystem. Caddy reloads certificates + # when reloadssl is used. Because it is a non-standard command, the caddy_setup script will not be triggered + # in /etc/rc.conf.d/caddy. The validate command needs it to make sure all certificates are in the filesystem, + # because otherwise the validation fails. + if service_action in ["validate", "reloadssl"]: + subprocess.run(["/usr/local/opnsense/scripts/OPNsense/Caddy/setup.sh"], check=True) + + print(run_service_command(service_action, message)) + else: + print(json.dumps({"status": "failed", "message": f"Unknown action: {action}"})) + else: + print(json.dumps({"status": "failed", "message": "No action provided"})) diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py new file mode 100755 index 0000000000..27f933d996 --- /dev/null +++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py @@ -0,0 +1,90 @@ +#!/usr/local/bin/python3 + +# +# Copyright (c) 2024 Cedrik Pischem +# 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. +# + +import sys +import json +import os +import subprocess + + +# Function to show the Caddy configuration from a JSON file +def show_caddy_config(): + config_path = "/var/db/caddy/config/caddy/autosave.json" + + try: + # Open and read the JSON configuration file directly into a Python dictionary + with open(config_path, "r") as file: + config_data = json.load(file) # Load the JSON directly + + # Print the JSON to stdout using json.dumps to ensure it's a JSON string and nicely formatted + print(json.dumps(config_data)) # Output the JSON directly + # Output error details in JSON format so that the API can consume them + except FileNotFoundError: + print(json.dumps({"error": "File not found", "message": "Caddy autosave.json configuration file not found"})) + except json.JSONDecodeError: + print(json.dumps( + {"error": "Invalid JSON", "message": "Error decoding the Caddy autosave.json, the file is not valid JSON"})) + except Exception as e: + print(json.dumps({"error": "General Error", "message": str(e)})) + + +def show_caddyfile(): + caddyfile_path = "/usr/local/etc/caddy/Caddyfile" + + try: + with open(caddyfile_path, "r") as file: + caddyfile_data = file.read() + # Output the Caddyfile data directly as a JSON object with a generic key like "content" + print(json.dumps({"content": caddyfile_data})) + except FileNotFoundError: + print(json.dumps({"error": "Caddyfile not found", "message": "Caddyfile not found"})) + except Exception as e: + print(json.dumps({"error": "General Error", "message": str(e)})) + + +# Action handler +def perform_action(cmd_action): + actions = { + "config": show_caddy_config, + "caddyfile": show_caddyfile + } + + action_func = actions.get(cmd_action) + if action_func: + action_func() + else: + # Output error details in JSON format if action is unknown + print(json.dumps({"error": "Unknown Action", "message": f"Unknown action: {cmd_action}"})) + + +if __name__ == "__main__": + if len(sys.argv) > 1: + perform_action(sys.argv[1]) + else: + # Output error details in JSON format if no action is specified + print(json.dumps({"error": "No Action Specified", "message": "No action specified"})) diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/setup.sh b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/setup.sh new file mode 100755 index 0000000000..8cbc15e24f --- /dev/null +++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/setup.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +# +# Copyright (c) 2023-2025 Cedrik Pischem +# 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. +# + +# Detect configured caddy_user/group (defaults) +. /etc/rc.conf.d/caddy +CADDY_USER="${caddy_user:-root}" +CADDY_GROUP="${caddy_group:-wheel}" + +# Define directories +CADDY_CONF_DIR="/usr/local/etc/caddy" +CADDY_DATA_DIR="/var/db/caddy" +CADDY_LOG_DIR="/var/log/caddy" +CADDY_CONF_CUSTOM_DIR="${CADDY_CONF_DIR}/caddy.d" +CADDY_CONF_CERT_DIR="${CADDY_CONF_DIR}/certificates" +CADDY_LOG_CUSTOM_DIR="${CADDY_LOG_DIR}/access" + +# Group the main directories +CADDY_DIRS=" +${CADDY_CONF_DIR} +${CADDY_DATA_DIR} +${CADDY_LOG_DIR} +${CADDY_CONF_CERT_DIR} +${CADDY_CONF_CUSTOM_DIR} +${CADDY_LOG_CUSTOM_DIR} +" + +# No inode changes occur when directory already exists or permissions are correct. +# Always running these when caddy starts guarantees correct ownership with minimal read and writes. +mkdir -p ${CADDY_DIRS} +chown -R "${CADDY_USER}:${CADDY_GROUP}" ${CADDY_DIRS} + +# Format and overwrite the Caddyfile +( cd "${CADDY_CONF_DIR}" && /usr/local/bin/caddy fmt --overwrite ) + +# Write custom certs from the OPNsense Trust Store +/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_certs.php diff --git a/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf b/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf new file mode 100644 index 0000000000..0d768ce42b --- /dev/null +++ b/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf @@ -0,0 +1,49 @@ +[start] +command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py start +parameters: +type:script +message:Starting Caddy service + +[stop] +command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py stop +parameters: +type:script +message:Stopping Caddy service + +[restart] +command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py restart +parameters: +type:script +message:Restarting Caddy service +description:Restart Caddy service + +[reload] +command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py reload +parameters: +type:script +message:Reloading Caddy configuration +description:Reload Caddy service + +[validate] +command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py validate +parameters: +type:script_output +message:Validating Caddy configuration + +[status] +command:/usr/local/sbin/pluginctl -s caddy status +parameters: +type:script_output +message:Request Caddy status + +[config] +command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py config +parameters: +type:script_output +message:Request Caddy JSON configuration + +[caddyfile] +command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py caddyfile +parameters: +type:script_output +message:Request Caddyfile diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/+TARGETS b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/+TARGETS new file mode 100644 index 0000000000..194242c0ab --- /dev/null +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/+TARGETS @@ -0,0 +1,2 @@ +Caddyfile:/usr/local/etc/caddy/Caddyfile +rc.conf.d/caddy:/etc/rc.conf.d/caddy diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile new file mode 100644 index 0000000000..d0b58323c4 --- /dev/null +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile @@ -0,0 +1,672 @@ +{# +# Copyright (c) 2023-2025 Cedrik Pischem +# 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. +#} + +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file + +{% set generalSettings = helpers.getNodeByTag('Pischem.caddy.general') %} + +{# Print as comments if Caddy runs as root or www user, as information in support cases. #} +{% if generalSettings.DisableSuperuser|default("0") == "1" %} +# caddy_user=www +{% else %} +# caddy_user=root +{% endif %} + +# Global Options +{ + {# + # Section: Global Log Settings + # Purpose: Sets up global log settings. The time format and unix socket make Caddy compatible + # with the syslog-ng instance running on the OPNsense. + #} + log { + output net unixgram//var/run/caddy/log.sock { + } + format json { + time_format rfc3339 + } + {% if generalSettings.LogLevel %} + level {{ generalSettings.LogLevel }} + {% endif %} + } + + {# Change default ports on demand #} + {% set httpPort = generalSettings.HttpPort %} + {% set httpsPort = generalSettings.HttpsPort %} + + {% if httpPort %} + http_port {{ httpPort }} + {% endif %} + {% if httpsPort %} + https_port {{ httpsPort }} + {% endif %} + + {# + # Section: Global Trusted Proxy and Credential Logging + # Purpose: The trusted proxy section is important when using CDNs so that headers are trusted. + # Credential logging is useful for troubleshooting basic auth. + #} + {% if generalSettings.accesslist %} + {% set accessList = helpers.toList('Pischem.caddy.reverseproxy.accesslist') | selectattr('@uuid', 'equalto', generalSettings.accesslist) | first %} + {% endif %} + + servers { + protocols {{ generalSettings.HttpVersions.split(',') | join(' ') }} + {% if accessList %} + trusted_proxies static {{ accessList.clientIps.split(',') | join(' ') }} + {% endif %} + {% if generalSettings.ClientIpHeaders %} + {% for header_uuid in generalSettings.ClientIpHeaders.split(',') %} + {% set header = helpers.toList('Pischem.caddy.reverseproxy.header') | selectattr('@uuid', 'equalto', header_uuid) | first %} + {% if header and header.HeaderType %} + client_ip_headers {{ header.HeaderType }} + {% endif %} + {% endfor %} + {% endif %} + + {% if generalSettings.LogCredentials|default("0") == "1" %} + log_credentials + {% endif %} + {% if generalSettings.timeout_read_body or + generalSettings.timeout_read_header or + generalSettings.timeout_write or + generalSettings.timeout_idle %} + timeouts { + {% if generalSettings.timeout_read_body %} + read_body {{ generalSettings.timeout_read_body }}s + {% endif %} + {% if generalSettings.timeout_read_header %} + read_header {{ generalSettings.timeout_read_header }}s + {% endif %} + {% if generalSettings.timeout_write %} + write {{ generalSettings.timeout_write }}s + {% endif %} + {% if generalSettings.timeout_idle %} + idle {{ generalSettings.timeout_idle }}s + {% endif %} + } + {% endif %} + {% if generalSettings.EnableLayer4|default("0") == "1" %} + listener_wrappers { + layer4 { + import /usr/local/etc/caddy/caddy.d/*.layer4listener + {% set context_var = "listener_wrappers" %} + {% include "OPNsense/Caddy/includeLayer4" %} + } + {# Route all other traffic to HTTP App #} + tls + } + {% endif %} + } + + {% if generalSettings.EnableLayer4|default("0") == "1" %} + layer4 { + import /usr/local/etc/caddy/caddy.d/*.layer4global + {% set context_var = "global" %} + {% include "OPNsense/Caddy/includeLayer4" %} + } + {% endif %} + + {# + # Section: Dynamic DNS Global Configuration + # Purpose: Sets up global configuration for Dynamic DNS. Caddy needs to be compiled with + # https://github.com/mholt/caddy-dynamicdns and https://github.com/caddy-dns. Otherwise the + # generated Caddyfile won't run. Each DNS Provider that is added below has to be compiled in. + #} + {% set dnsProvider = helpers.toList('Pischem.caddy.general.TlsDnsProvider') | first %} + {% set dnsApiKey = generalSettings.TlsDnsApiKey %} + {% set dynDnsSimpleHttp = generalSettings.DynDnsSimpleHttp %} + {% set dynDnsInterface = generalSettings.DynDnsInterface %} + {% set dynDnsUpdateOnly = generalSettings.DynDnsUpdateOnly %} + {% set dynDnsCheckInterval = generalSettings.DynDnsInterval %} + {% set dynDnsIpVersions = generalSettings.DynDnsIpVersions %} + {% set dynDnsTtl = generalSettings.DynDnsTtl %} + {% set dynDnsDomains = [] %} + + {% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %} + {% if reverse.enabled|default("0") == "1" and reverse.DynDns|default("0") == "1" %} + {% set cleanedDomain = reverse.FromDomain | replace("*.","") %} + {% if reverse.FromDomain.startswith("*.") %} + {% do dynDnsDomains.append(cleanedDomain + " *") %} + {% else %} + {% do dynDnsDomains.append(cleanedDomain + " @") %} + {% endif %} + {% endif %} + + {% for subdomain in helpers.toList('Pischem.caddy.reverseproxy.subdomain') %} + {% if subdomain.enabled|default("0") == "1" and subdomain.DynDns|default("0") == "1" and subdomain.reverse == reverse['@uuid'] %} + {% set fullSubdomain = subdomain.FromDomain %} + {% set baseDomain = fullSubdomain.split('.')[1:] | join('.') %} + {% set subDomainPart = fullSubdomain.split('.')[0] %} + {% set subdomainEntry = baseDomain + " " + subDomainPart %} + {% do dynDnsDomains.append(subdomainEntry) %} + {% endif %} + {% endfor %} + {% endfor %} + + {% if dnsProvider and dynDnsDomains|length > 0 %} + dynamic_dns { + provider {{ dnsProvider }} {{ dnsApiKey }} + domains { + {% for domain in dynDnsDomains %} + {{ domain }} + {% endfor %} + } + {% if dynDnsSimpleHttp %} + ip_source simple_http {{ dynDnsSimpleHttp }} + {% endif %} + {% if dynDnsInterface %} + {% set physicalInterfaceNames = [] %} + {% for intfName in dynDnsInterface.split(',') %} + {% do physicalInterfaceNames.append(helpers.physical_interface(intfName)) %} + {% endfor %} + ip_source interface {{ physicalInterfaceNames | join(',') }} + {% endif %} + {% if dynDnsCheckInterval %} + check_interval {{ dynDnsCheckInterval }}s + {% endif %} + {% if dynDnsIpVersions %} + versions {{ dynDnsIpVersions }} + {% endif %} + {% if dynDnsTtl %} + ttl {{ dynDnsTtl }}s + {% endif %} + {% if dynDnsUpdateOnly|default("0") == "1" %} + update_only + {% endif %} + } + {% endif %} + + {# + # Section: Encrypted ClientHello (ECH) Configuration + # https://caddyserver.com/docs/caddyfile/options#ech + #} + {% if generalSettings.TlsDnsEchDomain|default("") and dnsProvider %} + dns {{ dnsProvider }} {{ dnsApiKey }} + ech {{ generalSettings.TlsDnsEchDomain }} + {% endif %} + + {# + # Section: ACME Email, Auto HTTPS selection and global import statement + # Purpose: The ACME email is optional for receiving certificate notices. + # Auto HTTPS is optional, the default is on (which means the section is empty). + # The import statement is for user specific configuration out of scope of this template. + #} + {% set emailValue = helpers.toList('Pischem.caddy.general.TlsEmail') | first %} + {% if emailValue %} + email {{ emailValue }} + {% endif %} + {% set autoHttpsValue = helpers.toList('Pischem.caddy.general.TlsAutoHttps') | first %} + {% if autoHttpsValue %} + auto_https {{ autoHttpsValue }} + {% endif %} + {# + # Important: Grace Period influences how fast the server can finish reloads with open connections, by forcing termination. + # Default of Caddy is to wait for all connections to close before allowing reload, meaning the higher the value, the longer applies take. + #} + grace_period {{ generalSettings.GracePeriod }}s + skip_install_trust + import /usr/local/etc/caddy/caddy.d/*.global +} + +# Reverse Proxy Configuration + +{# +# When Layer4 is active, set default ports. +# Caddy does not set them up automatically under certain conditions. +# For example when AutoHTTPS would be off, or no domains have been configured. +# There are no regressions to set these ports up. +#} + +{% if generalSettings.EnableLayer4|default("0") == "1" %} + # Layer4 default HTTP port + {% if httpPort %} + :{{ httpPort }} { + } + {% else %} + :80 { + } + {% endif %} + # Layer4 default HTTPS port + {% if httpsPort %} + :{{ httpsPort }} { + } + {% else %} + :443 { + } + {% endif %} +{% endif %} + +{# +# Section: HTTP-01 Challenge Redirection +# Purpose: Redirects HTTP-01 challenges to a different webserver for reverse proxies and subdomains. +#} +{% macro http01_challenge_redirection(domain, acme_passthrough) %} +http://{{ domain }} { + handle /.well-known/acme-challenge/* { + reverse_proxy {{ acme_passthrough }} + } + handle { + redir https://{host}{uri} 308 + } +} +{% endmacro %} + +{% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %} + {% if reverse.enabled|default("0") == "1" and reverse.AcmePassthrough %} + {{ http01_challenge_redirection(reverse.FromDomain|default(""), reverse.AcmePassthrough) }} + {% endif %} +{% endfor %} + +{% for subdomain in helpers.toList('Pischem.caddy.reverseproxy.subdomain') %} + {% if subdomain.enabled|default("0") == "1" and subdomain.AcmePassthrough %} + {{ http01_challenge_redirection(subdomain.FromDomain|default(""), subdomain.AcmePassthrough) }} + {% endif %} +{% endfor %} + +{# +# Macro: tls_configuration +# Purpose: Configures TLS settings based on the DNS provider, API keys, and optional fields. +# Sets up the Caddyfile to update TXT Records with the chosen DNS Provider and receive +# certificates with the DNS-01 challenge. Refer to Dynamic DNS section for more details. +#} +{% macro tls_configuration( + customCert="", + dnsChallenge="0", + dnsChallengeOverrideDomain="", + clientAuthTrustPool="", + clientAuthMode="", + dnsProvider="", + dnsApiKey="", + tlsDnsPropagationTimeout="", + tlsDnsPropagationTimeoutPeriod="", + tlsDnsPropagationDelay="", + tlsDnsPropagationResolvers="" +) %} + {% if customCert or (dnsChallenge == "1" and dnsProvider) or clientAuthTrustPool %} + tls {% if customCert %}/usr/local/etc/caddy/certificates/{{ customCert }}.pem /usr/local/etc/caddy/certificates/{{ customCert }}.key{% endif %} { + {% if not customCert and (dnsChallenge == "1" and dnsProvider) %} + issuer acme { + dns {{ dnsProvider }} {{ dnsApiKey }} + {% if dnsChallengeOverrideDomain %} + dns_challenge_override_domain {{ dnsChallengeOverrideDomain }} + {% endif %} + + {% if tlsDnsPropagationResolvers %} + resolvers {{ tlsDnsPropagationResolvers }} + {% endif %} + {% if tlsDnsPropagationTimeout|default("0") == "1" %} + propagation_timeout -1 + {% elif tlsDnsPropagationTimeoutPeriod %} + propagation_timeout {{ tlsDnsPropagationTimeoutPeriod }}s + {% endif %} + {% if tlsDnsPropagationDelay %} + propagation_delay {{ tlsDnsPropagationDelay }}s + {% endif %} + } + {% endif %} + + {% if clientAuthTrustPool %} + client_auth { + {% for ca in clientAuthTrustPool.split(',') %} + trust_pool file /usr/local/etc/caddy/certificates/{{ ca.strip() }}.pem + {% endfor %} + {% if clientAuthMode %} + mode {{ clientAuthMode }} + {% endif %} + } + {% endif %} + } + {% endif %} +{% endmacro %} + +{# +# Macro: header_manipulation +# Purpose: Customizes HTTP headers for requests or responses; to add, remove, or modify headers. +# It uses a 'handle' object that specifies which headers to manipulate based on their @UUIDs. +# Each handle can have multiple of these HTTP headers assigned. +# Parameters: +# @param handle (object): +# - @uuid (string) +# - HeaderUpDown (string): Determines the direction of the header. +# - HeaderType (string): Specifies the name of the header. +# - HeaderValue (string, optional): The new value to set for the header, if any. +# - HeaderReplace (string, optional): Specifies a value to replace in the header. +#} +{% macro header_manipulation(handle) %} + {% if handle.header %} + {% for header_uuid in handle.header.split(',') %} + {% set header = helpers.toList('Pischem.caddy.reverseproxy.header') | selectattr('@uuid', 'equalto', header_uuid) | first %} + {# Generate directive only if HeaderUpDown and HeaderType are present #} + {% if header.HeaderUpDown and header.HeaderType %} + {# Prepare variables, making HeaderValue and HeaderReplace optional #} + {% set header_value = header.HeaderValue | default('') %} + {% set header_replace = header.HeaderReplace | default('') %} + {# Adjust output formatting based on the presence and style of HeaderValue #} + {% if header.HeaderReplace and header.HeaderValue %} + {% if header_value.startswith('{') %} + {{ header.HeaderUpDown }} {{ header.HeaderType }} {{ header_value }} "{{ header_replace }}" + {% else %} + {{ header.HeaderUpDown }} {{ header.HeaderType }} "{{ header_value }}" "{{ header_replace }}" + {% endif %} + {% elif header.HeaderValue %} + {% if header_value.startswith('{') %} + {{ header.HeaderUpDown }} {{ header.HeaderType }} {{ header_value }} + {% else %} + {{ header.HeaderUpDown }} {{ header.HeaderType }} "{{ header_value }}" + {% endif %} + {% else %} + {{ header.HeaderUpDown }} {{ header.HeaderType }} + {% endif %} + {% endif %} + {% endfor %} + {% endif %} +{% endmacro %} + +{# +# Macro: reverse_proxy_configuration +# Purpose: Sets up the handle with the reverse proxy configurations. +# Parameters: +# @param handle (object) +#} +{% macro reverse_proxy_configuration(handle) %} + {{ handle.HandleType }} {{ handle.HandlePath|default("") }} { + {{ handle_accesslist(handle.accesslist) }} + {{ render_basic_auth(handle.basicauth) }} + {# All IPs not matched by accesslist will continue processing #} + {% if handle.ForwardAuth|default("0") == "1" %} + {% include "OPNsense/Caddy/includeAuthProvider" %} + {% endif %} + {% if handle.HandleDirective == "reverse_proxy" and handle.ToPath|default("") != "" %} + rewrite * {{ handle.ToPath }}{uri} + {% endif %} + {# http:// is the empty default for reverse_proxy #} + {% set protocol = ( + "http://" if handle.HttpTls == "0" and handle.HandleDirective == "redir" else + "https://" if handle.HttpTls == "1" else + "h2c://" if handle.HttpTls == "2" else + '' + ) %} + {% set formatted_domains = [] -%} + {% for domain in handle.ToDomain.split(',') -%} + {% set is_ipv6 = (':' in domain and domain.count(':') >= 2) -%} + {% set formatted_domain = + ( '[' ~ domain ~ ']' if is_ipv6 else domain ) ~ + ( ':' ~ handle.ToPort if handle.ToPort else '' ) -%} + {% set _ = formatted_domains.append(protocol ~ formatted_domain) -%} + {% endfor -%} + {% if handle.HandleDirective == "reverse_proxy" %} + {{ handle.HandleDirective }} {{ formatted_domains | join(' ') }} { + {{ header_manipulation(handle) }} + {% if handle.lb_policy|default("") %} + lb_policy {{ handle.lb_policy }} + {% endif %} + {% if handle.lb_retries|default("") %} + lb_retries {{ handle.lb_retries }} + {% endif %} + {% if handle.lb_try_duration|default("") %} + lb_try_duration {{ handle.lb_try_duration }}s + {% endif %} + {% if handle.lb_try_interval|default("") %} + lb_try_interval {{ handle.lb_try_interval }}ms + {% endif %} + {% if handle.health_uri|default("") %} + health_uri {{ handle.health_uri }} + {% endif %} + {% if handle.health_upstream|default("") %} + health_upstream {{ handle.health_upstream }} + {% endif %} + {% if handle.health_port|default("") %} + health_port {{ handle.health_port }} + {% endif %} + {% if handle.health_interval|default("") %} + health_interval {{ handle.health_interval }}s + {% endif %} + {% if handle.health_passes|default("") %} + health_passes {{ handle.health_passes }} + {% endif %} + {% if handle.health_fails|default("") %} + health_fails {{ handle.health_fails }} + {% endif %} + {% if handle.health_timeout|default("") %} + health_timeout {{ handle.health_timeout }}s + {% endif %} + {% if handle.health_status|default("") %} + health_status {{ handle.health_status }} + {% endif %} + {% if handle.health_body|default("") %} + health_body {{ handle.health_body }} + {% endif %} + {% if handle.health_follow_redirects|default("0") == "1" %} + health_follow_redirects + {% endif %} + {% if handle.PassiveHealthFailDuration|default("") %} + fail_duration {{ handle.PassiveHealthFailDuration }}s + {% endif %} + {% if handle.PassiveHealthMaxFails|default("") %} + max_fails {{ handle.PassiveHealthMaxFails }} + {% endif %} + {% if handle.PassiveHealthUnhealthyStatus|default("") %} + unhealthy_status {{ handle.PassiveHealthUnhealthyStatus }} + {% endif %} + {% if handle.PassiveHealthUnhealthyLatency|default("") %} + unhealthy_latency {{ handle.PassiveHealthUnhealthyLatency }}ms + {% endif %} + {% if handle.PassiveHealthUnhealthyRequestCount|default("") %} + unhealthy_request_count {{ handle.PassiveHealthUnhealthyRequestCount }} + {% endif %} + {% set has_transport_options = + handle.HttpVersion or + handle.HttpKeepalive or + handle.HttpTlsInsecureSkipVerify|default("0") == "1" or + handle.HttpTlsTrustedCaCerts or + handle.HttpTlsServerName -%} + {% if has_transport_options %} + {% if handle.HttpNtlm|default("0") == "1" %} + transport http_ntlm { + {% else %} + transport http { + {% endif %} + {% if handle.HttpVersion %} + {% set version_map = {'http1': 1.1, 'http2': 2, 'http3': 3} %} + versions {{ version_map[handle.HttpVersion] }} + {% endif %} + {% if handle.HttpKeepalive %} + {% if handle.HttpKeepalive == "0" %} + keepalive off + {% else %} + keepalive {{ handle.HttpKeepalive }}s + {% endif %} + {% endif %} + {% if handle.HttpTls == "1" %} + {% if handle.HttpTlsInsecureSkipVerify|default("0") == "1" %} + tls_insecure_skip_verify + {% endif %} + {% if handle.HttpTlsTrustedCaCerts %} + tls_trust_pool file /usr/local/etc/caddy/certificates/{{ handle.HttpTlsTrustedCaCerts }}.pem + {% endif %} + {% if handle.HttpTlsServerName %} + tls_server_name {{ handle.HttpTlsServerName }} + {% endif %} + {% endif %} + } + {% endif %} + } + {% elif handle.HandleDirective == "redir" %} + {{ handle.HandleDirective }} {{ formatted_domains[0] }}{{ handle.ToPath|default("{uri}") }} + {% endif %} + } +{% endmacro %} + +{# +# Macro: handle_accesslist +# Purpose: Manages the logic for access lists, used for handlers, domains and subdomains +# Parameters: +# @param accesslist (string): The UUID of the access list to be applied. +# @param unique_suffix (string): A unique string to append to the access list, stripped of non-alphanumeric characters. +#} +{% macro handle_accesslist(accesslist, unique_suffix='') %} + {# Access list logic for dropping connections based on IP #} + {% if accesslist %} + {% set sanitized_suffix = unique_suffix | regex_replace('[^a-zA-Z0-9]', '') %} + {% set unique_identifier = '@' + accesslist + ('_' + sanitized_suffix if sanitized_suffix else '') %} + + {% set accesslist_obj = helpers.toList('Pischem.caddy.reverseproxy.accesslist') | selectattr('@uuid', 'equalto', accesslist) | first %} + {% if accesslist_obj %} + {% set client_ips = accesslist_obj.clientIps.split(',') | join(' ') %} + {{ unique_identifier }} { + {# Non-inverted access lists have "not" as default, inverted ones '' #} + {{ 'not' if accesslist_obj.accesslistInvert|default("0") == "0" else '' }} {{ accesslist_obj.RequestMatcher }} {{ client_ips }} + } + {# When IP is matched, abort or send response code. This will end processing and drop the request #} + handle {{ unique_identifier }} { + {% if accesslist_obj.HttpResponseCode %} + respond {{ '"' + accesslist_obj.HttpResponseMessage + '"' if accesslist_obj.HttpResponseMessage else '' }} {{ accesslist_obj.HttpResponseCode }} + {% else %} + abort + {% endif %} + } + {% endif %} + {% endif %} +{% endmacro %} + +{# +# Macro: render_basic_auth +# Purpose: Renders the basic authentication configuration. +# Parameters: +# @param basicauth_uuids (string): A comma-separated list of UUIDs for basic authentication. +#} +{% macro render_basic_auth(basicauth_uuids) %} + {% if basicauth_uuids %} + basic_auth { + {% for uuid in basicauth_uuids.split(',') %} + {% set basicauth = helpers.toList('Pischem.caddy.reverseproxy.basicauth') | selectattr('@uuid', 'equalto', uuid) | first %} + {% if basicauth %} + {{ basicauth.basicauthuser }} {{ basicauth.basicauthpass }} + {% endif %} + {% endfor %} + } + {% endif %} +{% endmacro %} + +{# +# Macro: render_handles +# Purpose: Renders the handles in the correct order (path-specific first, then catch-all). +# Parameters: +# @param handles (list): A list of handle objects to be rendered. +#} +{% macro render_handles(handles) %} + {% for handle in handles %} + {% if handle.enabled|default("0") == "1" and handle.HandlePath %} + {{ reverse_proxy_configuration(handle) }} + {% endif %} + {% endfor %} + {% for handle in handles %} + {% if handle.enabled|default("0") == "1" and not handle.HandlePath %} + {{ reverse_proxy_configuration(handle) }} + {% endif %} + {% endfor %} +{% endmacro %} + +{# +# Section: Reverse Proxy Configurations +# Purpose: Assembles reverse proxy configurations using predefined macros. +# This is the main logic of the whole template, handle with care. +# Macros Used: +# - tls_configuration: Configures TLS settings for the domain. +# - handle_accesslist: Manages the logic for access lists, including configuration. +# - render_handles: Renders the handles in the correct order, including basic authentication. +# - reverse_proxy_configuration: Sets up the handle with reverse proxy configurations. +# - handle_response: Manages the response logic for access lists and aborts. +#} + +{% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %} + {% if reverse.enabled|default("0") == "1" %} + {% if reverse.DisableTls|default("0") == "1" %}http://{% endif %}{{ reverse.FromDomain|default("") }}{% if reverse.FromPort %}:{{ reverse.FromPort }}{% endif %} { + {% if reverse.AccessLog|default("0") == "1" %} + {% if generalSettings.LogAccessPlain|default("0") == "0" %} + log default + {% else %} + log { + output file /var/log/caddy/access/{{ reverse['@uuid'] }}.log { + roll_keep_for {{ generalSettings.LogAccessPlainKeep|default("10") }}d + } + } + {% endif %} + {% endif %} + {{ tls_configuration( + customCert=reverse.CustomCertificate|default(""), + dnsChallenge=reverse.DnsChallenge|default("0"), + dnsChallengeOverrideDomain=reverse.DnsChallengeOverrideDomain|default(""), + clientAuthTrustPool=reverse.ClientAuthTrustPool|default(""), + clientAuthMode=reverse.ClientAuthMode|default(""), + dnsProvider=generalSettings.TlsDnsProvider, + dnsApiKey=generalSettings.TlsDnsApiKey, + tlsDnsPropagationTimeout=generalSettings.TlsDnsPropagationTimeout, + tlsDnsPropagationTimeoutPeriod=generalSettings.TlsDnsPropagationTimeoutPeriod, + tlsDnsPropagationDelay=generalSettings.TlsDnsPropagationDelay, + tlsDnsPropagationResolvers=generalSettings.TlsDnsPropagationResolvers + ) }} + {% set domain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('reverse', 'equalto', reverse['@uuid']) | selectattr('subdomain', 'undefined') | list %} + {{ handle_accesslist(reverse.accesslist, reverse.FromDomain) }} + {{ render_basic_auth(reverse.basicauth) }} + {{ render_handles(domain_handles) }} + } + + {% endif %} +{% endfor %} + +{% for subdomain in helpers.toList('Pischem.caddy.reverseproxy.subdomain') %} + {% if subdomain.enabled|default("0") == "1" %} + {% set reverse = helpers.toList('Pischem.caddy.reverseproxy.reverse') | selectattr('@uuid', 'equalto', subdomain.reverse) | first %} + {% if reverse and reverse.enabled|default("0") == "1" %} + {% if reverse.DisableTls|default("0") == "1" %}http://{% endif %}{{ subdomain.FromDomain|default("") }}{% if reverse.FromPort %}:{{ reverse.FromPort }}{% endif %} { + {% if reverse.AccessLog|default("0") == "1" %} + {% if generalSettings.LogAccessPlain|default("0") == "0" %} + log default + {% else %} + log { + output file /var/log/caddy/access/{{ subdomain['@uuid'] }}.log { + roll_keep_for {{ generalSettings.LogAccessPlainKeep|default("10") }}d + } + } + {% endif %} + {% endif %} + {{ tls_configuration( + clientAuthTrustPool=subdomain.ClientAuthTrustPool|default(""), + clientAuthMode=subdomain.ClientAuthMode|default("") + ) }} + {% set subdomain_handles = helpers.toList('Pischem.caddy.reverseproxy.handle') | selectattr('subdomain', 'equalto', subdomain['@uuid']) | list %} + {{ handle_accesslist(subdomain.accesslist, subdomain.FromDomain) }} + {{ render_basic_auth(subdomain.basicauth) }} + {{ render_handles(subdomain_handles) }} + } + + {% endif %} + {% endif %} +{% endfor %} + +import /usr/local/etc/caddy/caddy.d/*.conf diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeAuthProvider b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeAuthProvider new file mode 100644 index 0000000000..1c6beb34b9 --- /dev/null +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeAuthProvider @@ -0,0 +1,54 @@ +{# +# This file gets imported to configure forward auth in handlers. +# - Section: Reverse Proxy Configurations +#} +{% if generalSettings.AuthProvider %} + {# Check if the domain is IPv6 and wrap in square brackets if necessary #} + {% set is_ipv6 = (':' in generalSettings.AuthToDomain and generalSettings.AuthToDomain.count(':') >= 2) %} + {% set auth_url = (generalSettings.AuthToTls|default("0") == "1" and 'https://' or 'http://') + (is_ipv6 and '[' or '') + generalSettings.AuthToDomain|default("") + (is_ipv6 and ']' or '') + (generalSettings.AuthToPort and ':' + generalSettings.AuthToPort or '') %} +{% endif %} +{% macro generate_copy_headers() %} + {% if generalSettings.CopyHeaders %} + {% for header_uuid in generalSettings.CopyHeaders.split(',') %} + {% set header = helpers.toList('Pischem.caddy.reverseproxy.header') | selectattr('@uuid', 'equalto', header_uuid) | first %} + {% if header and header.HeaderType %} + copy_headers {{ header.HeaderType }} + {% endif %} + {% endfor %} + {% endif %} +{% endmacro %} +{% if generalSettings.AuthProvider == 'authelia' %} + forward_auth {{ auth_url }} { + {% if generalSettings.AuthToUri %} + uri {{ generalSettings.AuthToUri|default("") }} + {% endif %} + copy_headers Remote-User + copy_headers Remote-Groups + copy_headers Remote-Name + copy_headers Remote-Email + {{ generate_copy_headers() }} + } +{% elif generalSettings.AuthProvider == 'authentik' %} + reverse_proxy /outpost.goauthentik.io/* {{ auth_url }} { + {% if generalSettings.AuthToTls|default("0") == "1" %} + header_up Host {http.reverse_proxy.upstream.hostport} + {% endif %} + } + forward_auth {{ auth_url }} { + {% if generalSettings.AuthToUri %} + uri {{ generalSettings.AuthToUri|default("") }} + {% endif %} + copy_headers X-Authentik-Username + copy_headers X-Authentik-Groups + copy_headers X-Authentik-Email + copy_headers X-Authentik-Name + copy_headers X-Authentik-Uid + copy_headers X-Authentik-Jwt + copy_headers X-Authentik-Meta-Jwks + copy_headers X-Authentik-Meta-Outpost + copy_headers X-Authentik-Meta-Provider + copy_headers X-Authentik-Meta-App + copy_headers X-Authentik-Meta-Version + {{ generate_copy_headers() }} + } +{% endif %} diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeLayer4 b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeLayer4 new file mode 100644 index 0000000000..53b04b1a9b --- /dev/null +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeLayer4 @@ -0,0 +1,177 @@ +{# +# This file sets up layer4 routing support. +# There are two contexts: "listener_wrappers" and "global" +# +# "listener_wrappers" multiplexes on OSI Layer 7 on the default HTTP and HTTPS ports and requires a traffic matcher since +# otherwise the "reverse_proxy" would stop receiving any requests. The "any" Layer 7 matcher is not allowed here. +# This context allows for matching domains via SNI and route them without terminating TLS. +# +# "global" can set up custom ports and also route any OSI Layer 4 TCP/UDP traffic without a matcher. +# They will be grouped under the same protocol/port combination +# to allow multiple Layer 7 matchers inside the scope of the same Layer 4 matcher. +# This context is for advanced usecases where raw TCP/UDP traffic on custom ports should be proxied or load balanced. +#} + +{% set unsorted_layer4_configs = helpers.toList('Pischem.caddy.reverseproxy.layer4') %} + +{# Ensure that 'Sequence' is present and converted to an integer in each item #} +{% for item in unsorted_layer4_configs %} + {% set _ = item.update({'Sequence': item.get('Sequence', '0') | int}) %} +{% endfor %} + +{# Sort the configurations based on 'Sequence' #} +{% set layer4_configs = unsorted_layer4_configs | sort(attribute='Sequence') %} + +{% macro define_proxy(layer4) %} + {% if layer4.TerminateTls|default("0") == "1" %} + tls + {% endif %} + proxy {% for domain in layer4.ToDomain.split(',') %} + {% set is_ipv6 = (':' in domain) %} + {{ layer4.Protocol }}/{{ '[' if is_ipv6 }}{{ domain }}{{ ']' if is_ipv6 }}:{{ layer4.ToPort }}{% if not loop.last %} {% endif %} + {% endfor %} { + {% if layer4.lb_policy|default("") %} + lb_policy {{ layer4.lb_policy }} + {% endif %} + {% if layer4.PassiveHealthFailDuration|default("") %} + fail_duration {{ layer4.PassiveHealthFailDuration }}s + {% endif %} + {% if layer4.PassiveHealthMaxFails|default("") %} + max_fails {{ layer4.PassiveHealthMaxFails }} + {% endif %} + {% if layer4.ProxyProtocol %} + proxy_protocol {{ layer4.ProxyProtocol }} + {% endif %} + } +{% endmacro %} + +{% macro configure_proxy(layer4) %} + {% set content %} + {% if layer4.RemoteIp %} + {% set ip_list = layer4.RemoteIp.split(',') %} + subroute { + @allowed_ips remote_ip {{ ip_list|join(' ') }} + route @allowed_ips { + {{ define_proxy(layer4) }} + } + } + {% else %} + {{ define_proxy(layer4) }} + {% endif %} + {% endset %} + {{ content|trim }} +{% endmacro %} + +{% set grouped_configs = {} %} +{% for layer4 in layer4_configs %} + {% if layer4.FromPort and layer4.Protocol and layer4.enabled == "1" %} + {% set key = layer4.Protocol ~ '/:' ~ layer4.FromPort %} + {% if not key in grouped_configs %} + {% set _ = grouped_configs.update({key: []}) %} + {% endif %} + {% set _ = grouped_configs[key].append(layer4) %} + {% endif %} +{% endfor %} + +{% macro handle_special_matchers(layer4) %} + {% set invert_prefix = 'not ' if layer4.InvertMatchers == '1' else '' %} + {% if layer4.Matchers == 'httphost' %} + {{ invert_prefix }}http host {{ layer4.FromDomain.replace(',', ' ') }} + {% elif layer4.Matchers == 'tlssni' %} + {{ invert_prefix }}tls sni {{ layer4.FromDomain.replace(',', ' ') }} + {% elif layer4.Matchers == 'quicsni' %} + {{ invert_prefix }}quic sni {{ layer4.FromDomain.replace(',', ' ') }} + {% elif layer4.Matchers == 'openvpn' and layer4.FromOpenvpnModes %} + {% for mode in layer4.FromOpenvpnModes.split(',') %} + {% set mode_clean = mode.strip() %} + {% if layer4.FromOpenvpnStaticKey %} + {% set key_list = layer4.FromOpenvpnStaticKey.split(',') %} + {% endif %} + {% if mode_clean.startswith('auth') %} + {% if key_list|length > 1 %} + {% set key_list = key_list[:1] %} + {% endif %} + {% set digest = 'sha256' if 'sha256' in mode_clean else 'sha512' %} + {% set direction = 'normal' if 'normal' in mode_clean else 'inverse' %} + {{ invert_prefix }}openvpn { + modes auth + auth_digest {{ digest }} + {% if layer4.FromOpenvpnStaticKey %} + group_key_direction {{ direction }} + {% for key_uuid in key_list %} + group_key_file /usr/local/etc/caddy/certificates/{{ key_uuid.strip() }}.key + {% endfor %} + {% endif %} + } + {% elif mode_clean == 'crypt' %} + {% if key_list|length > 1 %} + {% set key_list = key_list[:1] %} + {% endif %} + {{ invert_prefix }}openvpn { + modes crypt + {% if layer4.FromOpenvpnStaticKey %} + {% for key_uuid in key_list %} + group_key_file /usr/local/etc/caddy/certificates/{{ key_uuid.strip() }}.key + {% endfor %} + {% endif %} + } + {% elif mode_clean == 'crypt2_client' %} + {# Multiple keys are allowed for crypt2_client #} + {{ invert_prefix }}openvpn { + modes crypt2 + {% if layer4.FromOpenvpnStaticKey %} + {% for key_uuid in key_list %} + client_key_file /usr/local/etc/caddy/certificates/{{ key_uuid.strip() }}.key + {% endfor %} + {% endif %} + } + {% elif mode_clean == 'crypt2_server' %} + {% if key_list|length > 1 %} + {% set key_list = key_list[:1] %} + {% endif %} + {{ invert_prefix }}openvpn { + modes crypt2 + {% if layer4.FromOpenvpnStaticKey %} + {% for key_uuid in key_list %} + server_key_file /usr/local/etc/caddy/certificates/{{ key_uuid.strip() }}.key + {% endfor %} + {% endif %} + } + {% endif %} + {% endfor %} + {% else %} + {{ invert_prefix }}{{ layer4.Matchers }} + {% endif %} +{% endmacro %} + +{% if context_var == 'listener_wrappers' %} + {% for layer4 in layer4_configs %} + {% if layer4.enabled == "1" and layer4.Type == 'listener_wrappers' %} + {% if layer4.Matchers != 'any' %} + @{{ layer4['@uuid'] }} {{ handle_special_matchers(layer4) }} + route @{{ layer4['@uuid'] }} { + {{ configure_proxy(layer4) }} + } + {% endif %} + {% endif %} + {% endfor %} +{% elif context_var == 'global' %} + {% for key, layers in grouped_configs.items() %} + {{ key }} { + {% for layer4 in layers %} + {% if layer4.enabled == "1" and layer4.Type == 'global' %} + {% if layer4.Matchers != 'any' %} + @{{ layer4['@uuid'] }} {{ handle_special_matchers(layer4) }} + route @{{ layer4['@uuid'] }} { + {{ configure_proxy(layer4) }} + } + {% else %} + route { + {{ configure_proxy(layer4) }} + } + {% endif %} + {% endif %} + {% endfor %} + } + {% endfor %} +{% endif %} diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/rc.conf.d/caddy b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/rc.conf.d/caddy new file mode 100644 index 0000000000..6464fe130b --- /dev/null +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/rc.conf.d/caddy @@ -0,0 +1,14 @@ +# DO NOT EDIT THIS FILE -- OPNsense auto-generated file + +{% set generalSettings = helpers.getNodeByTag('Pischem.caddy.general') %} +{% if generalSettings.enabled|default("0") == "1" %} +caddy_enable="YES" +caddy_admin="unix//var/run/caddy/caddy.sock|0220" +caddy_setup="/usr/local/opnsense/scripts/OPNsense/Caddy/setup.sh" +{% if generalSettings.DisableSuperuser|default("0") == "1" %} +caddy_user=www +caddy_group=www +{% endif %} +{% else %} +caddy_enable="NO" +{% endif %} diff --git a/www/caddy/src/opnsense/www/js/widgets/CaddyDomain.js b/www/caddy/src/opnsense/www/js/widgets/CaddyDomain.js new file mode 100644 index 0000000000..63fe9ee754 --- /dev/null +++ b/www/caddy/src/opnsense/www/js/widgets/CaddyDomain.js @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 Cedrik Pischem + * 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. + */ + +export default class CaddyDomain extends BaseTableWidget { + constructor() { + super(); + } + + getGridOptions() { + return { + // Trigger overflow-y:scroll after 650px height + sizeToContent: 650 + }; + } + + getMarkup() { + const $container = $('
    '); + const $caddyDomainTable = this.createTable('caddyDomainTable', { + headerPosition: 'none' + }); + + $container.append($caddyDomainTable); + return $container; + } + + async onWidgetTick() { + // Check if caddy is enabled + const data = await this.ajaxCall(`/api/caddy/reverse_proxy/${'get'}`); + if (!data.caddy.general || data.caddy.general.enabled === "0") { + this.displayError(`${this.translations.unconfigured}`); + return; + } + + // Process domains if caddy is enabled + const domains = { ...data.caddy.reverseproxy.reverse, ...data.caddy.reverseproxy.subdomain }; + + if (Object.keys(domains).length === 0) { + this.displayError(`${this.translations.nodomains}`); + return; + } + + this.clearError(); + this.processDomains(domains); + } + + // Utility function to display errors within the widget + displayError(message) { + const $error = $(``); + $('#caddyDomainTable').empty().append($error); + } + + clearError() { + $('#caddyDomainTable .error-message').remove(); + } + + processDomains(domains) { + if (!this.dataChanged('domains', domains)) { + return; + } + + $('.caddy-domain-tooltip').tooltip('hide'); + + const rows = []; + for (const key in domains) { + const domain = domains[key]; + const colorClass = domain.enabled === "1" ? 'text-success' : 'text-danger'; + const tooltipText = domain.enabled === "1" ? this.translations.enabled : this.translations.disabled; + let domainPort = domain.FromDomain; + + if (domain.FromPort) { + domainPort += `:${domain.FromPort}`; + } + + const row = $(` +
    +
    + + +   + + ${domainPort} + +
    +
    + `).prop('outerHTML'); + + rows.push({ html: row, enabled: domain.enabled }); + } + + // Sort rows by their enabled status + rows.sort((a, b) => a.enabled - b.enabled); + + // Update table with sorted rows + super.updateTable('caddyDomainTable', rows.map(row => [row.html])); + + // Initialize tooltips for interactivity + $('.caddy-domain-tooltip').tooltip({ container: 'body' }); + } +} diff --git a/www/caddy/src/opnsense/www/js/widgets/Metadata/Caddy.xml b/www/caddy/src/opnsense/www/js/widgets/Metadata/Caddy.xml new file mode 100644 index 0000000000..230bb1aeed --- /dev/null +++ b/www/caddy/src/opnsense/www/js/widgets/Metadata/Caddy.xml @@ -0,0 +1,16 @@ + + + CaddyDomain.js + /ui/caddy/reverse_proxy + + /api/caddy/reverse_proxy/* + + + Caddy Domains + Enabled + Disabled + Caddy is disabled or not configured. + Caddy does not manage any domains. + + + diff --git a/www/nginx/Makefile b/www/nginx/Makefile index 1fa6c45f02..1d8bbd8b7d 100644 --- a/www/nginx/Makefile +++ b/www/nginx/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= nginx -PLUGIN_VERSION= 1.24 +PLUGIN_VERSION= 1.36 PLUGIN_COMMENT= Nginx HTTP server and reverse proxy PLUGIN_DEPENDS= nginx PLUGIN_MAINTAINER= franz.fabian.94@gmail.com diff --git a/www/nginx/README.md b/www/nginx/README.md index e094ccd0f6..e90253a919 100644 --- a/www/nginx/README.md +++ b/www/nginx/README.md @@ -33,11 +33,11 @@ Most are standard but some endpoints support maps, which are not supported by OPNsense core. You can detect them simply as they are doing more than just a mapping -to the *base methods. +to the \*base methods. Such mappings work in the way that they catch up the request, map the internal data first, and then forward their UUIDs -to the *base method. +to the \*base method. ## The nginx plugin as infrastucture diff --git a/www/nginx/pkg-descr b/www/nginx/pkg-descr index 3afedea298..0ff2ddd42a 100644 --- a/www/nginx/pkg-descr +++ b/www/nginx/pkg-descr @@ -5,11 +5,112 @@ NGINX functionality includes HTTP server, HTTP and mail reverse proxy, caching, load balancing, compression, request throttling, connection multiplexing and reuse, SSL offload and HTTP media streaming. -WWW: https://nginx.org/ - Plugin Changelog ================ +1.36 + +* Add optional HTTP/3 support with dynamic Alt-Svc (contributed by Jan Chlouba) + +1.35 + +* Global options sendfile directive typo fix +* Add HTTP/2 option to GUI +* Add multiple client authentication trusted CA support +* Add proxy_intercept_errors directive support +* Add Variables hashes size support +* Add proxy_cache_valid directive support with response codes and multiple selection options +* Clean up model definition file +* Change ban_ttl default value to avoid unintentional system slowdown +* Fix NAXSI rules install + +1.34 + +* Add the option to not log TLS handshakes +* Remove obsolete http2_push_preload directive +* Migrate from the deprecated 'listen … http2' directive to the 'http2' directive +* Limit CSP log file size (migration notice: if you want to keep CSP violations logged, you will need to enable logging in the security policy) +* Add basic support for forced caching (contributed by Eirik Øverby) +* Add ability to override SNI for streams (contributed by DodoLeDev) + +1.33 + +* Add the "resolver" directive support +* Add config preview/test/copy to GUI +* Add gzip disabling support +* Move bot UA settings to GUI +* Add auto-ban logging support +* Add Upstream keepalive support +* Add Stream proxy_connect_timeout and proxy_timeout directives support +* Convert epoch time to readable format on "Banned" page +* Cosmetic: add table header borders on Traffic Statistic page for better reading + +1.32.2 + +* Add columns select button to the log viewer (contributed by kulikov-a) + +1.32.1 + +* add "Host header port" and "use original Host header" options +* Fix width of commands column (contributed by krbrs) + +1.32 + +* add support for resetting timed out connections and 444 responses (reset_timedout_connection) +* add option to change autoban response code to 444 (NGX_HTTP_CLOSE) +* add ssl_reject_handshake directive support +* add error log severity level support for HTTP and Stream servers +* add logging of possible errors causes to the setup script +* migrate general error log to syslog +* $internalModelUseSafeDelete enabled in API Settings controller to check item references before delete +* minor style adjustments for IP ACL and SNI Based Routing forms +* handle possible remaining vts socket after nginx start failure +* add uuid columns to the grids and a button to copy its value to clipboard +* add Cache Path columns naming +* fix: add the PROXY protocol for the HTTPS listener too, if it is set for the server +* fix: set Stream server outbound PROXY protocol based on Upstream settings +* fix: set Trusted Proxies (set_real_ip_from) for Stream server only with PROXY protocol enabled +* fix: do not include commented out core rules in naxsi policies when importing +* fix: fixed a typo in setting the proxy_ssl_session_reuse directive value (thanks to Sigurd Våg Aaknes) + +1.31 + +* Allow dynamic proxy_ssl_name (contributed by Mike Reiche) + +1.30 + +* add support for autoblock TTL + +1.29 + +* fixed a typo in the trusted tls fingerprints db creation part of setup.php +* rfc5746, rfc7507 and rfc8701 are taken into account on compiling and comparing tls fingerprints +* the reason for scoring the connection as intercepted is added to the X-TLS-Client-Intercepted header. check backend settings if using this feature +* http_post hook added to be able to map global variables +* PHP 8 compatibility: change crypt() to password_hash() + +1.28 + +* add support for connect-src and worker-src in content security policy +* add support for proxy_responses property in streams +* bugfix: trusted proxies field is now correctly named which makes it usable + +1.27 + +* add support for custom configuration in stream server (contributed by Fabio Castagnino) +* add headers_more support (contributed by kulikov-a) + +1.26 + +* Enhancement of security headers (contributed by Manuel Faux) +* Add Frame-Ancestors, add "preload", removed deprecated HPKP +* Performance enhancements for log display +* Fixed display of vts and logs for non-default styles + +1.25 + +* Reworked logging frontent to support filtering and historic view (contributed by Manuel Faux) + 1.24 * Change all Listen Port directives to Listen Address and migrate the Port data to Addresses @@ -68,8 +169,8 @@ Plugin Changelog 1.14 * add load balancer algorithm option (ip_hash) -* fix URL flag not stored (contributed by jkellerer) [1] -* allow to whitelist specific source IPs in the web application firewall (contributed by Julio Cesar Camargo) [2] +* fix URL flag not stored (contributed by jkellerer)[1] +* allow to whitelist specific source IPs in the web application firewall (contributed by Julio Cesar Camargo)[2] [1] https://github.com/opnsense/plugins/pull/1375 [2] https://github.com/opnsense/plugins/pull/1310 @@ -132,7 +233,7 @@ Plugin Changelog * Add proxy options for ignore client abort and disabling buffering * Add logviewer support for streams -* Fix charset is not defined bug (contributed by ccesario [1]) +* Fix charset is not defined bug (contributed by ccesario)[1] * Add an existence check for locations * Add PROXY protocol for HTTP and Streams frontend * Add PROXY backend support for Streams @@ -143,14 +244,14 @@ Plugin Changelog 1.4 (Development only) * move upstreams from HTTP to their own menu because they are used for TCP load balancing as well -* add TCP load balancing [1] +* add TCP load balancing[1] * add support for IP based ACLs -* add log rotation (contributed by Julius Cesar Camargo [2]) +* add log rotation (contributed by Julius Cesar Camargo)[2] * add support for satisfy, body size limitation -* change: allow to disable internal bot protection (contributed by @fzoske) [3] +* change: allow to disable internal bot protection (contributed by fzoske)[3] * change: do not save when no change in the list happened to prevent filling the log history * fix: translate a german string in upstream server to english -* replace headers instead of just adding our own (duplication issue #971), suppress X-Powered-By from Upstream [4] +* replace headers instead of just adding our own (duplication issue #971), suppress X-Powered-By from Upstream[4] [1] https://github.com/opnsense/plugins/pull/930 [2] https://github.com/opnsense/plugins/pull/982 @@ -166,13 +267,13 @@ Plugin Changelog * add limiter support * add permanent ban feature (use alias in firewall rules) * add caching feature (local file system) -* add path prefix support (contributed by @ccesario ) +* add path prefix support (contributed by ccesario) * add port to upstream server table * add support to download waf rules * improvement: export full chain instead of only the server certificate (fixes some issues with some tools which need it) * web interface (allow HTTP/2 server push to increase performance) * support for (secure) web sockets in reverse proxy mode -* bugfix: fix issues with multiple hostnames (IPv6 issues with TLS SNI by @ccesario ) +* bugfix: fix issues with multiple hostnames due to IPv6 issues with TLS SNI (contributed by ccesario) * bugfix: warning in certificate rendering 1.1 diff --git a/www/nginx/src/etc/inc/plugins.inc.d/nginx.inc b/www/nginx/src/etc/inc/plugins.inc.d/nginx.inc index cf35e06139..ad9cd4ea3f 100644 --- a/www/nginx/src/etc/inc/plugins.inc.d/nginx.inc +++ b/www/nginx/src/etc/inc/plugins.inc.d/nginx.inc @@ -26,6 +26,21 @@ POSSIBILITY OF SUCH DAMAGE. */ +/** + * register syslog facilities + * @return array + */ +function nginx_syslog() +{ + $syslogconf = array(); + + $syslogconf['nginx'] = array( + 'facility' => array('nginx'), + ); + + return $syslogconf; +} + function nginx_cron() { return array( diff --git a/www/nginx/src/etc/nginx/views/waf_denied.html b/www/nginx/src/etc/nginx/views/waf_denied.html index 7fdcc343c8..3dc3c0b35d 100644 --- a/www/nginx/src/etc/nginx/views/waf_denied.html +++ b/www/nginx/src/etc/nginx/views/waf_denied.html @@ -51,7 +51,7 @@

    Request Denied For Security Reasons

    The request has been denied by the web application firewall due to a security policy violation.

    Request information have been logged to investigate the incident.

    -

    If you think this is an error on our side, please contact us.

    +

    Please contact your system administrator if you believe these policies are incorrect.


    Web Application Protection by OPNsense
    diff --git a/www/nginx/src/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919 b/www/nginx/src/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919 new file mode 100644 index 0000000000..3cf0fcbc01 --- /dev/null +++ b/www/nginx/src/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919 @@ -0,0 +1,13 @@ +-----BEGIN DH PARAMETERS----- +MIICCAKCAgEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz ++8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a +87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 +YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi +7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD +ssbzSibBsu/6iGtCOGEfz9zeNVs7ZRkDW7w09N75nAI4YbRvydbmyQd62R0mkff3 +7lmMsPrBhtkcrv4TCYUTknC0EwyTvEN5RPT9RFLi103TZPLiHnH1S/9croKrnJ32 +nuhtK8UiNjoNq8Uhl5sN6todv5pC1cRITgq80Gv6U93vPBsg7j/VnXwl5B0rZp4e +8W5vUsMWTfT7eTDp5OWIV7asfV9C1p9tGHdjzx1VA0AEh/VbpX4xzHpxNciG77Qx +iu1qHgEtnmgyqQdgCpGBMMRtx3j5ca0AOAkpmaMzy4t6Gh25PXFAADwqTs6p+Y0K +zAqCkc3OyX3Pjsm1Wn+IpGtNtahR9EGC4caKAH5eZV9q//////////8CAQI= +-----END DH PARAMETERS----- diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php index 765c1be820..41b65da289 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php @@ -2,7 +2,8 @@ /* - Copyright (C) 2018 Fabian Franz + Copyright (C) 2018-2020 Fabian Franz + Copyright (C) 2020 Manuel Faux All rights reserved. Redistribution and use in source and binary forms, with or without @@ -39,19 +40,30 @@ class LogsController extends ApiControllerBase /** * "/" -> list of access logs * "/uuid" -> conent of access log - * @param null|string $uuid log uuid of the HTTP server from which the error log should be returned + * @param null|string $uuid log uuid of the HTTP server from which the access log should be returned + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply * @return array if feasible, otherwise null and the data is sent directly back * @throws \OPNsense\Base\ModelException ? */ - public function accessesAction($uuid = null) + public function accessesAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "") { $this->nginx = new Nginx(); if (!isset($uuid)) { // emulate REST API -> /accesses delivers a list of servers with access logs - return $this->list_vhosts(); + // attach special vhost for perm_ban log if needed + $data = $this->list_vhosts(); + if ((string)$this->nginx->http->log_perm_ban == "1") { + $data[] = array('id' => 'perm_ban', 'server_name' => 'Auto-ban'); + } + return $data; + } elseif (!isset($fileno)) { + return $this->list_logfiles('access', $uuid); } else { // emulate REST call for a specific log /accesses/uuid - $this->call_configd('access', $uuid); + $this->get_logs('access', $uuid, $fileno, $page, $perPage, $query); } } @@ -68,18 +80,24 @@ public function tlsHandshakesAction() * "/" -> list of error logs * "/uuid" -> conent of error log * @param null|string $uuid uuid of the HTTP server from which the error log should be returned + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply * @return array if feasible, otherwise null and the data is sent directly back * @throws \OPNsense\Base\ModelException ? */ - public function errorsAction($uuid = null) + public function errorsAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "") { $this->nginx = new Nginx(); if (!isset($uuid)) { // emulate REST API -> /errors delivers a list of servers with error logs return $this->list_vhosts(); + } elseif (!isset($fileno)) { + return $this->list_logfiles('error', $uuid); } else { // emulate REST call for a specific log /errors/uuid - $this->call_configd('error', $uuid); + $this->get_logs('error', $uuid, $fileno, $page, $perPage, $query); } } @@ -87,18 +105,24 @@ public function errorsAction($uuid = null) * "/" -> list of access logs * "/uuid" -> conent of access log * @param null|string $uuid log uuid of the stream server from which the error log should be returned + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply * @return array if feasible, otherwise null and the data is sent directly back * @throws \OPNsense\Base\ModelException ? */ - public function streamAccessesAction($uuid = null) + public function streamaccessesAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "") { $this->nginx = new Nginx(); if (!isset($uuid)) { // emulate REST API -> /stream_accesses delivers a list of servers with access logs return $this->list_streams(); + } elseif (!isset($fileno)) { + return $this->list_stream_logfiles('streamaccess', $uuid); } else { // emulate REST call for a specific log /stream_accesses/uuid - $this->call_configd_stream('streamaccess', $uuid); + $this->get_stream_logs('streamaccess', $uuid, $fileno, $page, $perPage, $query); } } @@ -106,49 +130,110 @@ public function streamAccessesAction($uuid = null) * "/" -> list of access logs * "/uuid" -> conent of error log * @param null $uuid uuid of the stream server from which the error log should be returned + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply * @return array if feasible, otherwise null and the data is sent directly back * @throws \OPNsense\Base\ModelException ? */ - public function streamErrorsAction($uuid = null) + public function streamerrorsAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "") { $this->nginx = new Nginx(); if (!isset($uuid)) { // emulate REST API -> /stream_errors delivers a list of servers with error logs return $this->list_streams(); + } elseif (!isset($fileno)) { + return $this->list_stream_logfiles('streamerror', $uuid); } else { // emulate REST call for a specific log /stream_errors/uuid - $this->call_configd_stream('streamerror', $uuid); + $this->get_stream_logs('streamerror', $uuid, $fileno, $page, $perPage, $query); + } + } + + + /** + * Retrieve log content for HTTP server. + * + * @param $type string access or error for the used log type + * @param $uuid string uuid of the server + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply + * @return |null + * @throws \Exception ? + */ + private function get_logs($type, $uuid, $fileno, $page, $perPage, $query) + { + if (!($this->vhost_exists($uuid) || $uuid == 'global' || $uuid == 'perm_ban')) { + return $this->response->setStatusCode(404, "Not Found"); } + + $page = intval($page); + $perPage = intval($perPage); + $query = base64_encode(urldecode($query)); + + return $this->sendConfigdToClient("nginx log $type $uuid $fileno $page $perPage $query"); } + /** + * Retrieve available log files for specific HTTP server uuid. + * + * @param $type string access or error for the used log type + * @param $uuid string uuid of the server + * @return |null + * @throws \Exception ? + */ + private function list_logfiles($type, $uuid) + { + if (!($this->vhost_exists($uuid) || $uuid == 'global' || $uuid == 'perm_ban')) { + return $this->response->setStatusCode(404, "Not Found"); + } + + return $this->sendConfigdToClient("nginx listlogs $type $uuid"); + } /** + * Retrieve log content for stream server. + * * @param $type string access or error for the used log type * @param $uuid string uuid of the server + * @param $fileno int number of logfile to retrieve + * @param $page int pagination page to retrieve + * @param $perPage int number of entries per page + * @param $query string filter string to apply * @return |null * @throws \Exception ? */ - private function call_configd($type, $uuid) + private function get_stream_logs($type, $uuid, $fileno, $page, $perPage, $query) { - if (!($this->vhost_exists($uuid) || $uuid == 'global')) { - $this->response->setStatusCode(404, "Not Found"); + if (!$this->stream_exists($uuid)) { + return $this->response->setStatusCode(404, "Not Found"); } - return $this->sendConfigdToClient('nginx log ' . $type . ' ' . $uuid); + $page = intval($page); + $perPage = intval($perPage); + $query = base64_encode(urldecode($query)); + + return $this->sendConfigdToClient("nginx log $type $uuid $fileno $page $perPage $query"); } + /** + * Retrieve available log files for specific stream server uuid. + * * @param $type string access or error for the used log type * @param $uuid string uuid of the server * @return |null * @throws \Exception ? */ - private function call_configd_stream($type, $uuid) + private function list_stream_logfiles($type, $uuid) { if (!$this->stream_exists($uuid)) { - $this->response->setStatusCode(404, "Not Found"); + return $this->response->setStatusCode(404, "Not Found"); } - return $this->sendConfigdToClient('nginx log ' . $type . ' ' . $uuid); + return $this->sendConfigdToClient("nginx listlogs $type $uuid"); } /** @@ -202,11 +287,12 @@ private function stream_exists($uuid) */ private function sendConfigdToClient($command) { - $backend = new Backend(); // must be passed directly -> OOM Problem - $this->response->setContent($backend->configdRun($command)); - $this->response->setStatusCode(200, "OK"); - $this->response->setContentType('application/json', 'UTF-8'); - return $this->response->send(); + if (!$this->response->isSent()) { + $backend = new Backend(); + $this->response->setContent($backend->configdRun($command)); + $this->response->setStatusCode(200, "OK"); + $this->response->setContentType('application/json', 'UTF-8'); + } } } diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php index 8cc0f45871..7b2043b904 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php @@ -35,6 +35,7 @@ class SettingsController extends ApiMutableModelControllerBase { protected static $internalModelClass = '\OPNsense\Nginx\Nginx'; protected static $internalModelName = 'nginx'; + protected static $internalModelUseSafeDelete = true; // download rules public function downloadrulesAction() @@ -55,7 +56,6 @@ public function searchuserlistAction() public function getuserlistAction($uuid = null) { - $this->sessionClose(); return $this->getBase('userlist', 'userlist', $uuid); } @@ -82,7 +82,6 @@ public function searchcredentialAction() public function getcredentialAction($uuid = null) { - $this->sessionClose(); return $this->getBase('credential', 'credential', $uuid); } @@ -104,12 +103,11 @@ public function setcredentialAction($uuid) // Upstream public function searchupstreamAction() { - return $this->searchBase('upstream', array('description', 'serverentries', 'tls_enable', 'load_balancing_algorithm')); + return $this->searchBase('upstream', array('uuid', 'description', 'serverentries', 'tls_enable', 'load_balancing_algorithm')); } public function getupstreamAction($uuid = null) { - $this->sessionClose(); return $this->getBase('upstream', 'upstream', $uuid); } @@ -131,12 +129,11 @@ public function setupstreamAction($uuid) // Upstream Server public function searchupstreamserverAction() { - return $this->searchBase('upstream_server', array('description', 'server', 'port', 'priority')); + return $this->searchBase('upstream_server', array('uuid', 'description', 'server', 'port', 'priority')); } public function getupstreamserverAction($uuid = null) { - $this->sessionClose(); return $this->getBase('upstream_server', 'upstream_server', $uuid); } @@ -159,8 +156,8 @@ public function setupstreamserverAction($uuid) public function searchlocationAction() { $data = $this->searchBase('location', array( - 'description','urlpattern', 'path_prefix', 'matchtype', 'upstream', - 'enable_secrules', 'enable_learning_mode', 'force_https', + 'uuid', 'description', 'urlpattern', 'path_prefix', 'matchtype', + 'upstream', 'enable_secrules', 'enable_learning_mode', 'force_https', 'xss_block_score', 'sqli_block_score', 'custom_policy' )); @@ -182,7 +179,6 @@ public function searchlocationAction() public function getlocationAction($uuid = null) { - $this->sessionClose(); return $this->getBase('location', 'location', $uuid); } @@ -209,7 +205,6 @@ public function searchcustompolicyAction() public function getcustompolicyAction($uuid = null) { - $this->sessionClose(); return $this->getBase('custompolicy', 'custom_policy', $uuid); } @@ -228,18 +223,69 @@ public function setcustompolicyAction($uuid) return $this->setBase('custompolicy', 'custom_policy', $uuid); } + // Resolver + public function searchresolverAction() + { + return $this->searchBase('resolver', array('uuid', 'description', 'address', 'valid', 'timeout')); + } + + public function getresolverAction($uuid = null) + { + return $this->getBase('resolver', 'resolver', $uuid); + } + + public function addresolverAction() + { + return $this->addBase('resolver', 'resolver'); + } + + public function delresolverAction($uuid) + { + return $this->delBase('resolver', $uuid); + } + + public function setresolverAction($uuid) + { + return $this->setBase('resolver', 'resolver', $uuid); + } + + // proxy_cache_valid + public function searchproxyCacheValidAction() + { + return $this->searchBase('proxy_cache_valid', array('uuid', 'description', 'code', 'valid')); + } + + public function getproxyCacheValidAction($uuid = null) + { + return $this->getBase('proxy_cache_valid', 'proxy_cache_valid', $uuid); + } + + public function addproxyCacheValidAction() + { + return $this->addBase('proxy_cache_valid', 'proxy_cache_valid'); + } + + public function delproxyCacheValidAction($uuid) + { + return $this->delBase('proxy_cache_valid', $uuid); + } + + public function setproxyCacheValidAction($uuid) + { + return $this->setBase('proxy_cache_valid', 'proxy_cache_valid', $uuid); + } + // http server public function searchhttpserverAction() { return $this->searchBase('http_server', array( - 'servername', 'locations', 'root', 'https_only', 'certificate', + 'uuid', 'servername', 'locations', 'root', 'https_only', 'certificate', 'listen_http_address', 'listen_https_address', 'default_server' )); } public function gethttpserverAction($uuid = null) { - $this->sessionClose(); return $this->getBase('httpserver', 'http_server', $uuid); } @@ -261,12 +307,11 @@ public function sethttpserverAction($uuid) // stream server public function searchstreamserverAction() { - return $this->searchBase('stream_server', array('description', 'certificate', 'udp', 'listen_address')); + return $this->searchBase('stream_server', array('uuid', 'description', 'certificate', 'udp', 'listen_address')); } public function getstreamserverAction($uuid = null) { - $this->sessionClose(); return $this->getBase('streamserver', 'stream_server', $uuid); } @@ -293,7 +338,6 @@ public function searchnaxsiruleAction() public function getnaxsiruleAction($uuid = null) { - $this->sessionClose(); return $this->getBase('naxsi_rule', 'naxsi_rule', $uuid); } @@ -320,7 +364,6 @@ public function searchhttprewriteAction() public function gethttprewriteAction($uuid = null) { - $this->sessionClose(); return $this->getBase('httprewrite', 'http_rewrite', $uuid); } @@ -342,12 +385,84 @@ public function sethttprewriteAction($uuid) // http security headers public function searchsecurityHeaderAction() { - return $this->searchBase('security_header', array('description')); + $data = $this->searchBase( + 'security_header', + ['description', 'referrer', 'xssprotection', 'strict_transport_security_time', + 'enable_csp', 'csp_report_only', 'csp_default_src_enabled', 'csp_script_src_enabled', 'csp_img_src_enabled', + 'csp_style_src_enabled', 'csp_media_src_enabled', 'csp_font_src_enabled', 'csp_frame_src_enabled', + 'csp_frame_ancestors_enabled', + 'csp_form_action_enabled'] + ); + + // Create "hsts" column (disabled/time) + foreach ($data['rows'] as &$row) { + if (intval($row['strict_transport_security_time']) > 0) { + $row['hsts'] = sprintf(gettext("%d sec"), intval($row['strict_transport_security_time'])); + } else { + $row['hsts'] = gettext('disabled'); + } + } + + // Create "csp" column (enabled/report only/disabled) + foreach ($data['rows'] as &$row) { + if ($row['enable_csp']) { + if ($row['csp_report_only']) { + $row['csp'] = gettext('report only'); + } else { + $row['csp'] = gettext('enabled'); + } + } else { + $row['csp'] = gettext('disabled'); + } + } + + // Create "csp_details" column + foreach ($data['rows'] as &$row) { + if ($row['enable_csp']) { + $enabled = []; + if ($row['csp_default_src_enabled']) { + $enabled[] = gettext("Default Source"); + } + if ($row['csp_script_src_enabled']) { + $enabled[] = gettext("Script Source"); + } + if ($row['csp_img_src_enabled']) { + $enabled[] = gettext("Image Source"); + } + if ($row['csp_style_src_enabled']) { + $enabled[] = gettext("Style Source"); + } + if ($row['csp_media_src_enabled']) { + $enabled[] = gettext("Media Source"); + } + if ($row['csp_font_src_enabled']) { + $enabled[] = gettext("Font Source"); + } + if ($row['csp_frame_src_enabled']) { + $enabled[] = gettext("Frame Source"); + } + if ($row['csp_frame_ancestors_enabled']) { + $enabled[] = gettext("Frame Ancestors"); + } + if ($row['csp_form_action_enabled']) { + $enabled[] = gettext("Form Action"); + } + + if (count($enabled)) { + $row['csp_details'] = implode(', ', $enabled); + } else { + $row['csp_details'] = gettext('none'); + } + } else { + $row['csp_details'] = ''; + } + } + + return $data; } public function getsecurityHeaderAction($uuid = null) { - $this->sessionClose(); return $this->getBase('security_header', 'security_header', $uuid); } @@ -377,7 +492,6 @@ public function searchlimitZoneAction() public function getlimitZoneAction($uuid = null) { - $this->sessionClose(); return $this->getBase('limit_zone', 'limit_zone', $uuid); } @@ -404,7 +518,6 @@ public function searcherrorpageAction() public function geterrorpageAction($uuid = null) { - $this->sessionClose(); $data = $this->getBase('errorpage', 'errorpage', $uuid); // Decode base64 encoded page content $data['errorpage']['pagecontent'] = base64_decode($data['errorpage']['pagecontent']); @@ -440,7 +553,6 @@ public function searchtlsFingerprintAction() public function gettlsFingerprintAction($uuid = null) { - $this->sessionClose(); return $this->getBase('tls_fingerprint', 'tls_fingerprint', $uuid); } @@ -470,7 +582,6 @@ public function searchlimitRequestConnectionAction() public function getlimitRequestConnectionAction($uuid = null) { - $this->sessionClose(); return $this->getBase('limit_request_connection', 'limit_request_connection', $uuid); } @@ -499,7 +610,6 @@ public function searchcachePathAction() public function getcachePathAction($uuid = null) { - $this->sessionClose(); return $this->getBase('cache_path', 'cache_path', $uuid); } @@ -526,7 +636,6 @@ public function searchsnifwdAction() public function getsnifwdAction($uuid = null) { - $this->sessionClose(); $base = $this->getBase('snihostname', 'sni_hostname_upstream_map', $uuid); return $this->convert_sni_fwd_for_client($base); } @@ -571,7 +680,6 @@ public function searchipaclAction() public function getipaclAction($uuid = null) { - $this->sessionClose(); $base = $this->getBase('ipacl', 'ip_acl', $uuid); return $this->convert_ipacl_for_client($base); } @@ -726,7 +834,6 @@ public function searchsyslogTargetAction() public function getsyslogTargetAction($uuid = null) { - $this->sessionClose(); return $this->getBase('syslog_target', 'syslog_target', $uuid); } @@ -744,4 +851,18 @@ public function setsyslogTargetAction($uuid) { return $this->setBase('syslog_target', 'syslog_target', $uuid); } + + public function showconfigAction() + { + $backend = new Backend(); + $response = json_decode($backend->configdRun("nginx show_config"), true); + return $response; + } + + public function testconfigAction() + { + $backend = new Backend(); + $response = trim($backend->configdRun("nginx test_config")); + return array("response" => $response); + } } diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php index e80537625b..01ad3330a2 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php @@ -63,6 +63,8 @@ public function indexAction() $this->view->ipacl = $this->getForm("ipacl"); $this->view->errorpage = $this->getForm("errorpage"); $this->view->tls_fingerprint = $this->getForm("tls_fingerprint"); + $this->view->resolver = $this->getForm("resolver"); + $this->view->proxy_cache_valid = $this->getForm("proxy_cache_valid"); $this->view->syslog_target = $this->getForm("syslog_target"); $nginx = new Nginx(); $this->view->show_naxsi_download_button = @@ -71,14 +73,6 @@ public function indexAction() $this->view->pick('OPNsense/Nginx/index'); } - /** - * show the nginx logs page /ui/nginx/index/logs - */ - public function logsAction() - { - $this->view->pick('OPNsense/Nginx/logs'); - } - /** * show the nginx TLS handshakes page /ui/nginx/index/tls_handshakes */ diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/LogsController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/LogsController.php new file mode 100644 index 0000000000..fec65f18d4 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/LogsController.php @@ -0,0 +1,87 @@ +view->log = 'global'; + $this->view->pick('OPNsense/Nginx/logs'); + } + + /** + * show the nginx logs page /ui/nginx/logs/accesses + */ + public function accessesAction() + { + $this->view->log = 'accesses'; + $this->view->pick('OPNsense/Nginx/logs'); + } + + /** + * show the nginx logs page /ui/nginx/logs/errors + */ + public function errorsAction() + { + $this->view->log = 'errors'; + $this->view->pick('OPNsense/Nginx/logs'); + } + + /** + * show the nginx logs page /ui/nginx/logs/stream_accesses + */ + public function streamaccessesAction() + { + $this->view->log = 'stream_accesses'; + $this->view->pick('OPNsense/Nginx/logs'); + } + + /** + * show the nginx logs page /ui/nginx/logs/stream_errors + */ + public function streamerrorsAction() + { + $this->view->log = 'stream_errors'; + $this->view->pick('OPNsense/Nginx/logs'); + } +} diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httprewrite.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httprewrite.xml index eda6fc20c4..fbc06bb5b9 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httprewrite.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httprewrite.xml @@ -22,6 +22,6 @@ dropdown - Stop rule processing (break) and perform (internal) redirect (last). Return a moved permanently status code (301) or a teporary redirect (302). + Stop rule processing (break) or perform (internal) redirect (last). Return a moved permanently status code (301) or a temporary redirect (302). diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml index e4a925509d..cc441d1a1e 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml @@ -20,6 +20,13 @@ checkbox + + httpserver.tls_reject_handshake + + checkbox + If enabled, TLS handshakes for this server will be rejected. + true + httpserver.syslog_targets @@ -106,7 +113,8 @@ httpserver.ca - dropdown + select_multiple + Trusted CA certificates httpserver.verify_client @@ -127,6 +135,21 @@ dropdown + + httpserver.error_log_level + + + dropdown + Select Error Log Level. Log levels are listed in the order of increasing verbosity. Setting a certain log level will cause all messages of the specified and more severe log levels to be logged. + true + + + httpserver.log_handshakes + + checkbox + Log TLS handshakes to fill the User Agent fingerprint database and detect MITM attacks. + true + httpserver.enable_acme_support @@ -141,7 +164,21 @@ httpserver.https_only checkbox - If you check this box, a TLS encrypted connection is enforced. + If the request scheme is not HTTPS, redirect to use HTTPS for this server. + + + httpserver.http2 + + checkbox + Enable the HTTP/2 protocol. + true + + + httpserver.enable_http3 + + checkbox + Enable HTTP/3/QUIC for this server (adds QUIC listeners and Alt-Svc header). + true httpserver.tls_protocols @@ -172,6 +209,13 @@ true Prefers server ciphers over client ciphers. + + httpserver.resolver + + dropdown + true + + httpserver.ocsp_stapling @@ -193,6 +237,13 @@ true Blocks files like .htaccess files or other files not intended for the public. + + httpserver.disable_gzip + + checkbox + true + Disables responses gzipping. Makes sense if TLS is used and this method of BREACH attack protection is preferred. + httpserver.disable_bot_protection @@ -284,4 +335,11 @@ select_multiple Select custom error pages to display instead of the default builtin error pages. If at least one error page is selected here, all default error pages will be disabled. + + httpserver.proxy_intercept_errors + + checkbox + Intercept responses with codes greater than or equal to 300 and redirect to processing with custom error pages. + true + diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml index 97b247d9b7..23fdccff54 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml @@ -96,6 +96,21 @@ true Enter how often the resource must be hit before adding it to the cache. + + location.cache_valid + + text + true + Force caching of 200, 301 and 302 responses according to the request methods enabled for caching. Given in minutes; leave empty to rely on request/response headers from client and upstream. + + + location.proxy_cache_valid + + select_multiple + + true + Force caching of response codes specified at Response Code Caching page. + location.cache_background_update @@ -204,13 +219,7 @@ location.force_https checkbox - Force encrypted connections. - - - location.http2_push_preload - - checkbox - If you check this box, you can use the link header to send resources to the client before they are requested. You can boost your performance with this setting. This requires that your application sets the "Link" header correctly. + If the request scheme is not HTTPS, redirect to use HTTPS for this location. location.php_enable @@ -241,7 +250,14 @@ checkbox true - If you enable the WebSocket Support option, nginx will pass the upgrade header to the backed server. + If you enable the WebSocket Support option, nginx will pass the upgrade header to the backed server. Mutually exclusive with Keepalive support. + + + location.upstream_keepalive + + checkbox + true + Adds the directives required for upstream keepalive to work (enables HTTP 1.1 and clears the Connection header). Mutually exclusive with WebSocket support. Keepalive parameters must be set in Upstream settings too. location.proxy_read_timeout @@ -255,7 +271,7 @@ checkbox true - Check this box, if you want the client SNI header to be used instead of your backend hostname. This settings overrides the configured hostname in the upstream configuration. + Check this box, if you want to passthrough the hostname you get from the downstream to the upstream connection. Can be overriden in the upstream's hostname configuration. Please note, that TLS server certificates are validated against this servername. If the server name passed by the client leads to different processing on the upstream server (i.e. different virtual host is choosen), this may cause security leaks. location.proxy_buffer_size @@ -290,7 +306,7 @@ text true - Enter a custom timout between data it sent to the client after which the connection is closed. + Enter a custom timout between data it sent to the client after which the connection is closed. location.proxy_buffering @@ -327,4 +343,12 @@ select_multiple Select custom error pages to display instead of the default builtin error pages. Selection will override error pages configured on HTTP server. + + location.proxy_intercept_errors + + dropdown + + Intercept responses with codes greater than or equal to 300 and redirect to processing with custom error pages. + true + diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/proxy_cache_valid.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/proxy_cache_valid.xml new file mode 100644 index 0000000000..e37702e9d3 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/proxy_cache_valid.xml @@ -0,0 +1,22 @@ +
    + + proxy_cache_valid.description + + text + Brief description for reference. + + + proxy_cache_valid.code + + true + + select_multiple + Enter a Respone codes or use "any" to cache any responses. + + + proxy_cache_valid.valid + + text + Specify caching time in minutes. + +
    diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/resolver.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/resolver.xml new file mode 100644 index 0000000000..e224326092 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/resolver.xml @@ -0,0 +1,44 @@ +
    + + resolver.description + + text + Brief description for reference. + + + resolver.address + + true + + select_multiple + Enter a list of IP addresses and ports (optional). Uses port 53 if omitted. Like "8.8.8.8, [2001:4860:4860::8888], 8.8.8.8:5353". + + + resolver.ipv4_off + + checkbox + Do not request IPv4 addresses. + true + + + resolver.ipv6_off + + checkbox + Do not request IPv6 addresses. + true + + + resolver.valid + + text + Override response TTL in seconds. + true + + + resolver.timeout + + text + Resolver time out in seconds. NGINX default (if empty) is 30 seconds. + true + +
    diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/security_headers.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/security_headers.xml index f4004ea58c..a35677195c 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/security_headers.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/security_headers.xml @@ -1,553 +1,786 @@
    - - security_header.description - - text - This is only for your reference. - - - security_header.referrer - - - dropdown - -
  • Same Origin: The header will be sent if you stay on the same server using the same protocol (no data leak)
  • -
  • No Referrer When Downgrade: Prevents sending a referrer when switching from HTTPS to HTTP
  • -
  • Origin, Strict-Origin: Always send the header but no path or query information. Strict Origin additionally suppressed the header on downgrades.
  • -
  • (Strict) Origin When Cross Origin: Full Referrer on the same origin, and like (Strict) Origin when cross domain.
  • -
  • Unsafe URL: Sends the full URL to all pages
  • - ]]>
    -
    - - security_header.xssprotection - - - dropdown - -
  • Block: The browser should block the response
  • -
  • Off: Allow Anything
  • -
  • On: The Browser decides how to handle it.
  • - ]]>
    -
    - - security_header.content_type_options - - checkbox - - - security_header.strict_transport_security_time - - text - A time in seconds in which the transport security (TLS) should be enforced. - - - security_header.strict_transport_security_include_subdomains - - checkbox - If checked, also subdomains are affected. - - - security_header.hpkp_keys - - select_multiple - - true - Mozilla Wiki. - It is not recommended to use this feature with short lived certificates.]]> - - - security_header.hpkp_report_only - - checkbox - If you only want to test it, you can check this box (policy will be deployed but not enforced). - - - security_header.hpkp_time - - text - - - security_header.hpkp_include_subdomains - - checkbox - If checked, also subdomains are affected. - - - security_header.enable_csp - - checkbox - If checked, the CSP is enabled. - - - security_header.csp_report_only - - checkbox - If checked, the CSP is not enforced (learning mode). - - - header - - - - security_header.csp_default_src_enabled - - checkbox - If checked, this part of the CSP is enabled. - - - security_header.csp_default_src_data_urls - - Data URLs are used to embed files into HTML (for example images written directly into the src attribute). - checkbox - - - security_header.csp_default_src_http_urls - - select_multiple - true - - Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. - You can use wildcards here like https://*.exmaple.com. - - - security_header.csp_default_src_inline - - checkbox - Checking this directive allows to use scripts or styles directly embedded in in the HTML content. - Examples are the script and the style tags. - - - security_header.csp_default_src_eval - - checkbox - Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. - - - security_header.csp_default_src_self - - checkbox - Allows everything from the same site (path can differ, but host, protocol and port need to be the same). - - - security_header.csp_default_src_blob - - checkbox - Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. - - - security_header.csp_default_src_mediastream - - checkbox - - - security_header.csp_default_src_filesystem - - checkbox - - - security_header.csp_default_src_none - - checkbox - If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. - - - header - - - - security_header.csp_script_src_enabled - - checkbox - If checked, this part of the CSP is enabled. - - - security_header.csp_script_src_data_urls - - Data URLs are used to embed files into HTML (for example images written directly into the src attribute). - checkbox - - - security_header.csp_script_src_http_urls - - select_multiple - true - - Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. - You can use wildcards here like https://*.exmaple.com. - - - security_header.csp_script_src_inline - - checkbox - Checking this directive allows to use scripts or styles directly embedded in in the HTML content. - Examples are the script and the style tags. - - - security_header.csp_script_src_eval - - checkbox - Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. - - - security_header.csp_script_src_self - - checkbox - Allows everything from the same site (path can differ, but host, protocol and port need to be the same). - - - security_header.csp_script_src_blob - - checkbox - Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. - - - security_header.csp_script_src_mediastream - - checkbox - - - security_header.csp_script_src_filesystem - - checkbox - - - security_header.csp_script_src_none - - checkbox - If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. - - - header - - - - security_header.csp_img_src_enabled - - checkbox - If checked, this part of the CSP is enabled. - - - security_header.csp_img_src_data_urls - - Data URLs are used to embed files into HTML (for example images written directly into the src attribute). - checkbox - - - security_header.csp_img_src_http_urls - - select_multiple - true - - Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. - You can use wildcards here like https://*.exmaple.com. - - - security_header.csp_img_src_inline - - checkbox - Checking this directive allows to use scripts or styles directly embedded in in the HTML content. - Examples are the script and the style tags. - - - security_header.csp_img_src_eval - - checkbox - Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. - - - security_header.csp_img_src_self - - checkbox - Allows everything from the same site (path can differ, but host, protocol and port need to be the same). - - - security_header.csp_img_src_blob - - checkbox - Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. - - - security_header.csp_img_src_mediastream - - checkbox - - - security_header.csp_img_src_filesystem - - checkbox - - - security_header.csp_img_src_none - - checkbox - If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. - - - header - - - - security_header.csp_style_src_enabled - - checkbox - If checked, this part of the CSP is enabled. - - - security_header.csp_style_src_data_urls - - Data URLs are used to embed files into HTML (for example images written directly into the src attribute). - checkbox - - - security_header.csp_style_src_http_urls - - select_multiple - true - - Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. - You can use wildcards here like https://*.exmaple.com. - - - security_header.csp_style_src_inline - - checkbox - Checking this directive allows to use scripts or styles directly embedded in in the HTML content. - Examples are the script and the style tags. - - - security_header.csp_style_src_eval - - checkbox - Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. - - - security_header.csp_style_src_self - - checkbox - Allows everything from the same site (path can differ, but host, protocol and port need to be the same). - - - security_header.csp_style_src_blob - - checkbox - Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. - - - security_header.csp_style_src_mediastream - - checkbox - - - security_header.csp_style_src_filesystem - - checkbox - - - security_header.csp_style_src_none - - checkbox - If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. - - - header - - - - security_header.csp_media_src_enabled - - checkbox - If checked, this part of the CSP is enabled. - - - security_header.csp_media_src_data_urls - - Data URLs are used to embed files into HTML (for example images written directly into the src attribute). - checkbox - - - security_header.csp_media_src_http_urls - - select_multiple - true - - Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. - You can use wildcards here like https://*.exmaple.com. - - - security_header.csp_media_src_inline - - checkbox - Checking this directive allows to use scripts or styles directly embedded in in the HTML content. - Examples are the script and the style tags. - - - security_header.csp_media_src_eval - - checkbox - Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. - - - security_header.csp_media_src_self - - checkbox - Allows everything from the same site (path can differ, but host, protocol and port need to be the same). - - - security_header.csp_media_src_blob - - checkbox - Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. - - - security_header.csp_media_src_mediastream - - checkbox - - - security_header.csp_media_src_filesystem - - checkbox - - - security_header.csp_media_src_none - - checkbox - If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. - - - header - - - - security_header.csp_font_src_enabled - - checkbox - If checked, this part of the CSP is enabled. - - - security_header.csp_font_src_data_urls - - Data URLs are used to embed files into HTML (for example images written directly into the src attribute). - checkbox - - - security_header.csp_font_src_http_urls - - select_multiple - true - - Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. - You can use wildcards here like https://*.exmaple.com. - - - security_header.csp_font_src_inline - - checkbox - Checking this directive allows to use scripts or styles directly embedded in in the HTML content. - Examples are the script and the style tags. - - - security_header.csp_font_src_eval - - checkbox - Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. - - - security_header.csp_font_src_self - - checkbox - Allows everything from the same site (path can differ, but host, protocol and port need to be the same). - - - security_header.csp_font_src_blob - - checkbox - Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. - - - security_header.csp_font_src_mediastream - - checkbox - - - security_header.csp_font_src_filesystem - - checkbox - - - security_header.csp_font_src_none - - checkbox - If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. - - - header - - - - security_header.csp_form_action_enabled - - checkbox - If checked, this part of the CSP is enabled. - - - security_header.csp_form_action_data_urls - - Data URLs are used to embed files into HTML (for example images written directly into the src attribute). - checkbox - - - security_header.csp_form_action_http_urls - - select_multiple - true - - Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. - You can use wildcards here like https://*.exmaple.com. - - - security_header.csp_form_action_inline - - checkbox - Checking this directive allows to use scripts or styles directly embedded in in the HTML content. - Examples are the script and the style tags. - - - security_header.csp_form_action_eval - - checkbox - Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. - - - security_header.csp_form_action_self - - checkbox - Allows everything from the same site (path can differ, but host, protocol and port need to be the same). - - - security_header.csp_form_action_blob - - checkbox - Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. - - - security_header.csp_form_action_mediastream - - checkbox - - - security_header.csp_form_action_filesystem - - checkbox - - - security_header.csp_form_action_none - - checkbox - If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. - + + + header + + + + security_header.description + + text + This is only for your reference. + + + security_header.referrer + + + dropdown + +
  • Same Origin: The header will be sent if you stay on the same server using the same protocol (no data leak)
  • +
  • No Referrer When Downgrade: Prevents sending a referrer when switching from HTTPS to HTTP
  • +
  • Origin, Strict-Origin: Always send the header but no path or query information. Strict Origin additionally suppressed the header on downgrades.
  • +
  • (Strict) Origin When Cross Origin: Full Referrer on the same origin, and like (Strict) Origin when cross domain.
  • +
  • Unsafe URL: Sends the full URL to all pages
  • + ]]>
    +
    + + security_header.xssprotection + + + dropdown + +
  • Block: The browser should block the response
  • +
  • Off: Allow Anything
  • +
  • On: The Browser decides how to handle it.
  • + ]]>
    +
    + + security_header.content_type_options + + checkbox + + + header + + + + security_header.strict_transport_security_time + + text + A time in seconds in which the transport security (TLS) should be enforced. + + + security_header.strict_transport_security_include_subdomains + + checkbox + If checked, also subdomains are affected. + + + security_header.strict_transport_security_preload + + checkbox + an HSTS preload service. By following the guidelines and successfully submitting your domain, browsers will never connect to your domain using an insecure connection. While the service is hosted by Google, all browsers have stated an intent to use (or actually started using) the preload list. However, it is not part of the HSTS specification and should not be treated as official.]]> + + + header + + + + security_header.enable_csp + + checkbox + If checked, the Content Security Policy (CSP) header is enabled. A detailed configuration is still required via the other tabs of this sheet. + + + security_header.csp_log_violations + + checkbox + If checked, the plugin collects CSP violation reports and stores one JSON document per line under /var/log/nginx/csp_violations.log. You can use that file to check for XSS attempts or broken web pages where the CSP denied access to a resource. + + + security_header.csp_report_only + + checkbox + If checked, the CSP is not enforced (learning mode). + +
    + + + header + + Content Security Policy: Enable on the General tab needs to be enabled to activate this header.]]> + + + security_header.csp_default_src_enabled + + checkbox + If checked, this part of the CSP is enabled. + + + security_header.csp_default_src_data_urls + + Data URLs are used to embed files into HTML (for example images written directly into the src attribute). + checkbox + + + security_header.csp_default_src_http_urls + + select_multiple + true + + Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. + You can use wildcards here like https://*.exmaple.com. + + + security_header.csp_default_src_inline + + checkbox + Checking this directive allows to use scripts or styles directly embedded in in the HTML content. + Examples are the script and the style tags. + + + security_header.csp_default_src_eval + + checkbox + Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. + + + security_header.csp_default_src_self + + checkbox + Allows everything from the same site (path can differ, but host, protocol and port need to be the same). + + + security_header.csp_default_src_blob + + checkbox + Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. + + + security_header.csp_default_src_mediastream + + checkbox + + + security_header.csp_default_src_filesystem + + checkbox + + + security_header.csp_default_src_none + + checkbox + If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. + + + + + header + + Content Security Policy: Enable on the General tab needs to be enabled to activate this header.]]> + + + security_header.csp_script_src_enabled + + checkbox + If checked, this part of the CSP is enabled. + + + security_header.csp_script_src_data_urls + + Data URLs are used to embed files into HTML (for example images written directly into the src attribute). + checkbox + + + security_header.csp_script_src_http_urls + + select_multiple + true + + Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. + You can use wildcards here like https://*.exmaple.com. + + + security_header.csp_script_src_inline + + checkbox + Checking this directive allows to use scripts or styles directly embedded in in the HTML content. + Examples are the script and the style tags. + + + security_header.csp_script_src_eval + + checkbox + Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. + + + security_header.csp_script_src_self + + checkbox + Allows everything from the same site (path can differ, but host, protocol and port need to be the same). + + + security_header.csp_script_src_blob + + checkbox + Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. + + + security_header.csp_script_src_mediastream + + checkbox + + + security_header.csp_script_src_filesystem + + checkbox + + + security_header.csp_script_src_none + + checkbox + If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. + + + + + header + + Content Security Policy: Enable on the General tab needs to be enabled to activate this header.]]> + + + security_header.csp_img_src_enabled + + checkbox + If checked, this part of the CSP is enabled. + + + security_header.csp_img_src_data_urls + + Data URLs are used to embed files into HTML (for example images written directly into the src attribute). + checkbox + + + security_header.csp_img_src_http_urls + + select_multiple + true + + Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. + You can use wildcards here like https://*.exmaple.com. + + + security_header.csp_img_src_inline + + checkbox + Checking this directive allows to use scripts or styles directly embedded in in the HTML content. + Examples are the script and the style tags. + + + security_header.csp_img_src_eval + + checkbox + Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. + + + security_header.csp_img_src_self + + checkbox + Allows everything from the same site (path can differ, but host, protocol and port need to be the same). + + + security_header.csp_img_src_blob + + checkbox + Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. + + + security_header.csp_img_src_mediastream + + checkbox + + + security_header.csp_img_src_filesystem + + checkbox + + + security_header.csp_img_src_none + + checkbox + If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. + + + + + header + + Content Security Policy: Enable on the General tab needs to be enabled to activate this header.]]> + + + security_header.csp_style_src_enabled + + checkbox + If checked, this part of the CSP is enabled. + + + security_header.csp_style_src_data_urls + + Data URLs are used to embed files into HTML (for example images written directly into the src attribute). + checkbox + + + security_header.csp_style_src_http_urls + + select_multiple + true + + Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. + You can use wildcards here like https://*.exmaple.com. + + + security_header.csp_style_src_inline + + checkbox + Checking this directive allows to use scripts or styles directly embedded in in the HTML content. + Examples are the script and the style tags. + + + security_header.csp_style_src_eval + + checkbox + Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. + + + security_header.csp_style_src_self + + checkbox + Allows everything from the same site (path can differ, but host, protocol and port need to be the same). + + + security_header.csp_style_src_blob + + checkbox + Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. + + + security_header.csp_style_src_mediastream + + checkbox + + + security_header.csp_style_src_filesystem + + checkbox + + + security_header.csp_style_src_none + + checkbox + If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. + + + + + header + + Content Security Policy: Enable on the General tab needs to be enabled to activate this header.]]> + + + security_header.csp_media_src_enabled + + checkbox + If checked, this part of the CSP is enabled. + + + security_header.csp_media_src_data_urls + + Data URLs are used to embed files into HTML (for example images written directly into the src attribute). + checkbox + + + security_header.csp_media_src_http_urls + + select_multiple + true + + Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. + You can use wildcards here like https://*.exmaple.com. + + + security_header.csp_media_src_inline + + checkbox + Checking this directive allows to use scripts or styles directly embedded in in the HTML content. + Examples are the script and the style tags. + + + security_header.csp_media_src_eval + + checkbox + Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. + + + security_header.csp_media_src_self + + checkbox + Allows everything from the same site (path can differ, but host, protocol and port need to be the same). + + + security_header.csp_media_src_blob + + checkbox + Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. + + + security_header.csp_media_src_mediastream + + checkbox + + + security_header.csp_media_src_filesystem + + checkbox + + + security_header.csp_media_src_none + + checkbox + If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. + + + + + header + + Content Security Policy: Enable on the General tab needs to be enabled to activate this header.]]> + + + security_header.csp_frame_src_enabled + + checkbox + If checked, this part of the CSP is enabled. + + + security_header.csp_frame_src_data_urls + + Data URLs are used to embed files into HTML (for example images written directly into the src attribute). + checkbox + + + security_header.csp_frame_src_http_urls + + select_multiple + true + + Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. + You can use wildcards here like https://*.exmaple.com. + + + security_header.csp_frame_src_inline + + checkbox + Checking this directive allows to use scripts or styles directly embedded in in the HTML content. + Examples are the script and the style tags. + + + security_header.csp_frame_src_eval + + checkbox + Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. + + + security_header.csp_frame_src_self + + checkbox + Allows everything from the same site (path can differ, but host, protocol and port need to be the same). + + + security_header.csp_frame_src_blob + + checkbox + Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. + + + security_header.csp_frame_src_mediastream + + checkbox + + + security_header.csp_frame_src_filesystem + + checkbox + + + security_header.csp_frame_src_none + + checkbox + If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. + + + header + + Content Security Policy: Enable on the General tab needs to be enabled to activate this header.]]> + + + security_header.csp_frame_ancestors_enabled + + checkbox + If checked, this part of the CSP is enabled. + + + security_header.csp_frame_ancestors_data_urls + + Data URLs are used to embed files into HTML (for example images written directly into the src attribute). + checkbox + + + security_header.csp_frame_ancestors_http_urls + + select_multiple + true + + Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. + You can use wildcards here like https://*.exmaple.com. + + + security_header.csp_frame_ancestors_self + + checkbox + Allows everything from the same site (path can differ, but host, protocol and port need to be the same). + + + security_header.csp_frame_ancestors_blob + + checkbox + Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. + + + security_header.csp_frame_ancestors_mediastream + + checkbox + + + security_header.csp_frame_ancestors_filesystem + + checkbox + + + security_header.csp_frame_ancestors_none + + checkbox + If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. + + + + + header + + Content Security Policy: Enable on the General tab needs to be enabled to activate this header.]]> + + + security_header.csp_font_src_enabled + + checkbox + If checked, this part of the CSP is enabled. + + + security_header.csp_font_src_data_urls + + Data URLs are used to embed files into HTML (for example images written directly into the src attribute). + checkbox + + + security_header.csp_font_src_http_urls + + select_multiple + true + + Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. + You can use wildcards here like https://*.exmaple.com. + + + security_header.csp_font_src_inline + + checkbox + Checking this directive allows to use scripts or styles directly embedded in in the HTML content. + Examples are the script and the style tags. + + + security_header.csp_font_src_eval + + checkbox + Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. + + + security_header.csp_font_src_self + + checkbox + Allows everything from the same site (path can differ, but host, protocol and port need to be the same). + + + security_header.csp_font_src_blob + + checkbox + Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. + + + security_header.csp_font_src_mediastream + + checkbox + + + security_header.csp_font_src_filesystem + + checkbox + + + security_header.csp_font_src_none + + checkbox + If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. + + + + + header + + Content Security Policy: Enable on the General tab needs to be enabled to activate this header.]]> + + + security_header.csp_connect_src_enabled + + checkbox + If checked, this part of the CSP is enabled. + + + security_header.csp_connect_src_http_urls + + select_multiple + true + + Allow connecting to websockets. You can use wildcards here like wss://*.exmaple.com. + + + security_header.csp_connect_src_none + + checkbox + If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. + + + + + header + + Content Security Policy: Enable on the General tab needs to be enabled to activate this header.]]> + + + security_header.csp_worker_src_enabled + + checkbox + If checked, this part of the CSP is enabled. + + + security_header.csp_worker_src_data_urls + + Data URLs are used to embed files into HTML (for example images written directly into the src attribute). + checkbox + + + security_header.csp_worker_src_http_urls + + select_multiple + true + + Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. + You can use wildcards here like https://*.exmaple.com. + + + security_header.csp_worker_src_inline + + checkbox + Checking this directive allows to use scripts or styles directly embedded in in the HTML content. + Examples are the script and the style tags. + + + security_header.csp_worker_src_eval + + checkbox + Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. + + + security_header.csp_worker_src_self + + checkbox + Allows everything from the same site (path can differ, but host, protocol and port need to be the same). + + + security_header.csp_worker_src_blob + + checkbox + Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. + + + security_header.csp_worker_src_filesystem + + checkbox + + + security_header.csp_worker_src_none + + checkbox + If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. + + + + + header + + Content Security Policy: Enable on the General tab needs to be enabled to activate this header.]]> + + + security_header.csp_form_action_enabled + + checkbox + If checked, this part of the CSP is enabled. + + + security_header.csp_form_action_data_urls + + Data URLs are used to embed files into HTML (for example images written directly into the src attribute). + checkbox + + + security_header.csp_form_action_http_urls + + select_multiple + true + + Allow loading files over HTTP(S) allows downloading of content over other domains or CDNs. + You can use wildcards here like https://*.exmaple.com. + + + security_header.csp_form_action_inline + + checkbox + Checking this directive allows to use scripts or styles directly embedded in in the HTML content. + Examples are the script and the style tags. + + + security_header.csp_form_action_eval + + checkbox + Checking this box allows functions like eval or createFunction in JS, or style attributes for CSS. + + + security_header.csp_form_action_self + + checkbox + Allows everything from the same site (path can differ, but host, protocol and port need to be the same). + + + security_header.csp_form_action_blob + + checkbox + Allows to use blobs as a data source. This usually is content, which is somehow generated in JavaScript. + + + security_header.csp_form_action_mediastream + + checkbox + + + security_header.csp_form_action_filesystem + + checkbox + + + security_header.csp_form_action_none + + checkbox + If this checkbox is checked, all other settings for this directive are ignored and everything will be forbidden. + +
    diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/settings.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/settings.xml index 98cbd71d6c..1fc2c9c9c7 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/settings.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/settings.xml @@ -7,6 +7,13 @@ checkbox Enable configured services. + + nginx.general.ban_ttl + + text + Set autoblock lifetime in minutes. 72 hours by default. Set to 0 for infinite. Please note that setting this to 0 may result in gradual system slowdown and the need to manually clear the entries. + true + @@ -24,7 +31,7 @@ true - nginx.http.enabled + nginx.http.sendfile checkbox Enable sendfile support (faster). @@ -35,6 +42,12 @@ text After this idle time, the client gets disconnected. + + nginx.http.reset_timedout + + checkbox + Reset timed out connections and connections closed with the non-standard code 444. When the socket is closed, TCP RST is sent to the client, and all memory occupied by this socket is released. This helps avoid keeping an already closed socket with filled buffers in a FIN_WAIT1 state for a long time. + nginx.http.default_type @@ -43,7 +56,7 @@ nginx.http.server_names_hash_bucket_size - + text true @@ -53,6 +66,46 @@ text true + + nginx.http.variables_hash_bucket_size + + text + true + + + nginx.http.variables_hash_max_size + + text + true + + + nginx.http.bots_ua + + select_multiple + + true + List of bot user agents that will be used in bot protection. Uses a well known list by default. + + + nginx.http.log_perm_ban + + checkbox + Log requests that led to auto-blocking (bots and honeypots). + + + nginx.http.ban_response + + + dropdown + Select a response code for auto-blocking requests (bot user-agent or honeypot location). The default code is 403. 444 is a special response code that closes the connection without a response to the client. + true + + + nginx.http.headers_more_enable + + checkbox + Enhanced version of the standard headers module. Allows to add, set, or clear any output or input header. + diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/streamserver.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/streamserver.xml index a76c22e977..4f70528367 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/streamserver.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/streamserver.xml @@ -26,7 +26,7 @@ If you enable the proxy protocol, a downstream proxy can send the client IP and port before the real traffic is set. - httpserver.trusted_proxies + streamserver.trusted_proxies true @@ -34,6 +34,27 @@ true Enter a list of IP addresses or CIDR networks which are allowed to override the source IP address using the specified header. + + streamserver.proxy_responses + + text + true + Due to the nature of UDP, nginx cannot know, when the communication ends and this helps as it tells nginx the number of datagrams the communication is expected to last on server side and it is expected to be closed afterwards. If you enter 0, it is expected, that the server never responds to a datagram. If nginx gets a datagram, it will still get forwarded to the client. Setting this option might be useful in (mostly) unidirectional communication as well. + + + streamserver.proxy_connect_timeout + + text + true + Defines a timeout (in seconds) for establishing a connection with a proxied server. + + + streamserver.proxy_timeout + + text + true + Sets the timeout (in seconds) between two successive read or write operations on client or proxied server connections. + streamserver.certificate @@ -56,6 +77,14 @@ dropdown + + streamserver.error_log_level + + + dropdown + Select Error Log Level. Log levels are listed in the order of increasing verbosity. Setting a certain log level will cause all messages of the specified and more severe log levels to be logged. + true + streamserver.route_field diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream.xml index 65ba0b5736..3f9bd6af5b 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/upstream.xml @@ -23,6 +23,40 @@ true If you enable the proxy protocol, an upstream proxy or server will get the client IP and the server port before the real traffic is sent. + + upstream.keepalive + + text + Activates the connections cache for upstream server and sets the max number of idle connections that are preserved in the cache of each worker process. Leave blank or set to 0 to disable. Keepalive support should be enabled at Location settings also. + + + upstream.keepalive_requests + + text + true + Sets the maximum number of requests that can be served through one keepalive connection. NGINX default is 1000. + + + upstream.keepalive_timeout + + text + true + Sets a timeout in seconds during which an idle keepalive connection to an upstream server will stay open. NGINX default is 75s. + + + upstream.host_port + + text + true + Add this port value to the Host header. Not used by default. + + + upstream.x_forwarded_host_verbatim + + Use Host header value from the client request ($http_host) for X-Forwarded-Host header. $host variable is used by default. Enabling this may cause incorrect behavior in case of malicious requests such as incorrect hostnames being logged or invalid redirects being performed. + true + checkbox + upstream.tls_enable @@ -40,7 +74,7 @@ upstream.tls_name_override - Use another hostname for SNI and certificate validation. + Force a specific hostname for the backend connection instead of passing the hostname from the downstream connection to the upstream connection. text diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php index 5f6ed187c1..f2d94de63c 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php @@ -2,7 +2,8 @@ /* - Copyright (C) 2018 Fabian Franz + Copyright (C) 2018-2020 Fabian Franz + Copyright (C) 2020 Manuel Faux All rights reserved. Redistribution and use in source and binary forms, with or without @@ -29,32 +30,11 @@ namespace OPNsense\Nginx; -class AccessLogParser +class AccessLogParser extends LogParserBase { - private $file_name; - private $result; - private const LogLineRegex = '/(\S+) - (\S+) \[([\d\sa-z\:\-\/\+]+)\] "([^"]+?)" (\d+) (\d+) "([^"]*?)" "([^"]*?)" "([^"]*?)"/i'; - function __construct($file_name) - { - $this->file_name = $file_name; - $this->result = array(); - $this->parse_file(); - } - - private function parse_file() - { - $handle = @fopen($this->file_name, 'r'); - if ($handle) { - while (($buffer = fgets($handle)) !== false) { - $this->result[] = $this->parse_line($buffer); - } - fclose($handle); - } - } - - private function parse_line($line) + protected function parse_line($line) { $container = new AccessLogLine(); if (preg_match(self::LogLineRegex, $line, $data)) { @@ -70,9 +50,4 @@ private function parse_line($line) } return $container; } - - public function get_result() - { - return $this->result; - } } diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php index fd272a4d0c..99336e2b62 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php @@ -2,7 +2,8 @@ /* - Copyright (C) 2018 Fabian Franz + Copyright (C) 2018-2020 Fabian Franz + Copyright (C) 2020 Manuel Faux All rights reserved. Redistribution and use in source and binary forms, with or without @@ -29,21 +30,11 @@ namespace OPNsense\Nginx; -class ErrorLogParser +class ErrorLogParser extends LogParserBase { - private $file_name; - private $lines; - private $result; - private const LogLineRegex = '/(\S+) (\S+) \[([\d\sa-z\:\-\/\+\#]+)\] ([\S:]+): (.+)/i'; - function __construct($file_name) - { - $this->file_name = $file_name; - $this->lines = file($this->file_name); - $this->result = array_map([$this, 'parse_line'], $this->lines); - } - private function parse_line($line) + protected function parse_line($line) { $container = new ErrorLogLine(); if (preg_match(self::LogLineRegex, $line, $data)) { @@ -55,9 +46,4 @@ private function parse_line($line) } return $container; } - - public function get_result() - { - return $this->result; - } } diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/LogParserBase.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/LogParserBase.php new file mode 100644 index 0000000000..9f8df92965 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/LogParserBase.php @@ -0,0 +1,122 @@ +file_name = $file_name; + $this->page = $page; + $this->per_page = $per_page; + $this->query = $query; + $this->page_count = 0; + $this->total_lines = 0; + $this->query_lines = 0; + $this->result = array(); + + $this->parse_file(); + } + + /** + * Forward read complete gz compressed logfile into memory, reverse file, + * count lines, save lines which match filter and count matching lines. + */ + private function parse_file() + { + $lines = gzfile($this->file_name); + if ($lines !== false && count($lines) > 0) { + $lines = array_reverse($lines); + + $filtering = false; + // Did we receive a non-zero filtering query? + foreach ($this->query as $key => $val) { + if (strlen($val) > 0) { + $filtering = true; + } + } + + $cnt = 0; + foreach ($lines as $line) { + $pass = true; + + $parsed_line = ''; + // Perform filtering if needed + if ($filtering) { + $parsed_line = $this->parse_line($line); + + foreach ($this->query as $key => $val) { + $val = (string)$val; + if (!empty($val) && strpos($parsed_line->{$key}, (string)$val) === false) { + $pass = false; + } + } + } + + if ($pass) { + if ($this->per_page <= 0 || floor($cnt / $this->per_page) == $this->page) { + // Only parse line if not already parsed due to filtering + if (!$filtering) { + $parsed_line = $this->parse_line($line); + } + + $this->result[] = $parsed_line; + } + $cnt++; + } + } + + $this->page_count = $this->per_page > 0 ? ceil($cnt / $this->per_page) : 1; + $this->total_lines = count($lines); + $this->query_lines = $cnt; + } + + unset($lines); + } + + abstract protected function parse_line($line); + + public function get_result() + { + return $this->result; + } +} diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php index 37726d7047..f6974d6e0a 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php @@ -2,7 +2,8 @@ /* - Copyright (C) 2018 Fabian Franz + Copyright (C) 2018-2020 Fabian Franz + Copyright (C) 2020 Manuel Faux All rights reserved. Redistribution and use in source and binary forms, with or without @@ -29,33 +30,11 @@ namespace OPNsense\Nginx; -class StreamAccessLogParser +class StreamAccessLogParser extends LogParserBase { - private $file_name; - private $result; - private const LogLineRegex = '/(\S+) \[([\d\sa-z\:\-\/\+]+)\] (\S+?) (\d+) (\d+) (\d+) (\d+(?:\.\d+)?)/i'; - - function __construct($file_name) - { - $this->file_name = $file_name; - $this->result = array(); - $this->parse_file(); - } - - private function parse_file() - { - $handle = @fopen($this->file_name, 'r'); - if ($handle) { - while (($buffer = fgets($handle)) !== false) { - $this->result[] = $this->parse_line($buffer); - } - fclose($handle); - } - } - - private function parse_line($line) + protected function parse_line($line) { $container = new StreamAccessLogLine(); if (preg_match(self::LogLineRegex, $line, $data)) { @@ -68,9 +47,4 @@ private function parse_line($line) } return $container; } - - public function get_result() - { - return $this->result; - } } diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NaxsiIdentifierConstraint.php b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NaxsiIdentifierConstraint.php index b40c832dd2..8a774fd132 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NaxsiIdentifierConstraint.php +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NaxsiIdentifierConstraint.php @@ -28,7 +28,7 @@ namespace OPNsense\Base\Constraints; -use Phalcon\Messages\Message; +use OPNsense\Base\Messages\Message; /** * a very specific nginx check for Naxsi rule IDs - not reusable @@ -38,7 +38,7 @@ */ class NaxsiIdentifierConstraint extends BaseConstraint { - public function validate(\Phalcon\Validation $validator, $attribute): bool + public function validate($validator, $attribute): bool { $node = $this->getOption('node'); if ($node) { diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NgxBusyBufferConstraint.php b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NgxBusyBufferConstraint.php index 6067ad0644..fe84e2ffd8 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NgxBusyBufferConstraint.php +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NgxBusyBufferConstraint.php @@ -28,7 +28,7 @@ namespace OPNsense\Base\Constraints; -use Phalcon\Messages\Message; +use OPNsense\Base\Messages\Message; /** * a very specific nginx check - not reusable @@ -38,29 +38,32 @@ */ class NgxBusyBufferConstraint extends BaseConstraint { - public function validate(\Phalcon\Validation $validator, $attribute): bool + public function validate($validator, $attribute): bool { $node = $this->getOption('node'); if ($node) { $parentNode = $node->getParentNode(); - if (!$this->isEmpty($node)) { + if (!$node->isEmpty()) { $proxy_buffer_size_node = $parentNode->proxy_buffer_size; $proxy_buffers_count_node = $parentNode->proxy_buffers_count; $proxy_buffers_size_node = $parentNode->proxy_buffers_size; $proxy_busy_buffers_size_node = $parentNode->proxy_busy_buffers_size; - if (!$this->isEmpty($proxy_buffers_count_node) && !$this->isEmpty($proxy_buffers_size_node)) { + if ( + !is_null($proxy_buffers_count_node) && !$proxy_buffers_count_node->isEmpty() && + !is_null($proxy_buffers_size_node) && !$proxy_buffers_size_node->isEmpty() + ) { $proxy_buffers_count_int = intval((string) $proxy_buffers_count_node); $proxy_buffers_size_int = intval((string) $proxy_buffers_size_node); $proxy_buffers_total_minus1_size = ($proxy_buffers_count_int - 1) * $proxy_buffers_size_int; } - if (!$this->isEmpty($proxy_busy_buffers_size_node)) { + if (!is_null($proxy_busy_buffers_size_node) && !$proxy_busy_buffers_size_node->isEmpty()) { $proxy_busy_buffers_size = intval((string) $proxy_busy_buffers_size_node); } - if (!$this->isEmpty($proxy_buffer_size_node)) { + if (!is_null($proxy_buffer_size_node) && !$proxy_buffer_size_node->isEmpty()) { $proxy_buffer_size_int = intval((string) $proxy_buffer_size_node); } diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NgxUniqueDefaultServerConstraint.php b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NgxUniqueDefaultServerConstraint.php index 9534251250..4393425f4f 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NgxUniqueDefaultServerConstraint.php +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Base/Constraints/NgxUniqueDefaultServerConstraint.php @@ -28,7 +28,7 @@ namespace OPNsense\Base\Constraints; -use Phalcon\Messages\Message; +use OPNsense\Base\Messages\Message; /** * a very specific nginx check - not reusable @@ -39,13 +39,13 @@ */ class NgxUniqueDefaultServerConstraint extends BaseConstraint { - public function validate(\Phalcon\Validation $validator, $attribute): bool + public function validate($validator, $attribute): bool { $node = $this->getOption('node'); if ($node) { $httpServerNode = $node->getParentNode(); $defaultServerNode = $httpServerNode->getChild("default_server"); - if (!$this->isEmpty($defaultServerNode)) { + if (!$defaultServerNode->isEmpty()) { $myUUID = $httpServerNode->getAttribute("uuid"); $myListenHTTPAddress = $httpServerNode->getChild("listen_http_address"); @@ -58,7 +58,7 @@ public function validate(\Phalcon\Validation $validator, $attribute): bool $uuid = $httpServer->getAttribute("uuid"); if ($uuid != $myUUID) { $defaultServerNode = $httpServer->getChild("default_server"); - if (!$this->isEmpty($defaultServerNode)) { + if (!$defaultServerNode->isEmpty()) { $listenHTTPAddressNode = $httpServer->getChild("listen_http_address"); if ($this->compareListenAddresses($myListenHTTPAddress, $listenHTTPAddressNode, $msg)) { $validator->appendMessage(new Message( diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml index d9f5822205..74100456fa 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml @@ -5,7 +5,11 @@ - + + + + + diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Migrations/M1_24_0.php b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Migrations/M1_24_0.php index 981fbd99a4..1bdb907287 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Migrations/M1_24_0.php +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Migrations/M1_24_0.php @@ -29,26 +29,33 @@ namespace OPNsense\Nginx\Migrations; use OPNsense\Base\BaseModelMigration; +use OPNsense\Core\Config; class M1_24_0 extends BaseModelMigration { + /** + * Listen ports to listen addresses movements + * @param Nginx $model + */ public function run($model) { - foreach ($model->getNodeByReference('http_server')->iterateItems() as $http_server) { - if ($http_server->listen_http_port != '') { - $http_server->listen_http_address = $http_server->listen_http_port . ',[::]:' . $http_server->listen_http_port; - $http_server->listen_http_port = null; - } - if ($http_server->listen_https_port != '') { - $http_server->listen_https_address = $http_server->listen_https_port . ',[::]:' . $http_server->listen_https_port; - $http_server->listen_https_port = null; - } + $cfgObj = Config::getInstance()->object(); + $ports = array(); + foreach ($cfgObj->OPNsense->Nginx->http_server ?? [] as $cfg_http_server) { + $uuid = (string)$cfg_http_server->attributes()['uuid']; + $ports['http_port'] = (isset($cfg_http_server->listen_http_port) && $cfg_http_server->listen_http_port != '') ? $cfg_http_server->listen_http_port : null; + $ports['https_port'] = (isset($cfg_http_server->listen_https_port) && $cfg_http_server->listen_https_port != '') ? $cfg_http_server->listen_https_port : null; + $http_server = $model->getNodeByReference('http_server.' . $uuid); + $http_server->listen_http_address = (isset($ports['http_port'])) ? $ports['http_port'] . ',[::]:' . $ports['http_port'] : null; + $http_server->listen_https_address = (isset($ports['https_port'])) ? $ports['https_port'] . ',[::]:' . $ports['https_port'] : null; } - foreach ($model->getNodeByReference('stream_server')->iterateItems() as $server) { - if ($server->listen_port != '') { - $server->listen_address = $server->listen_port . ',[::]:' . $server->listen_port; - $server->listen_port = null; - } + foreach ($cfgObj->OPNsense->Nginx->stream_server ?? [] as $cfg_stream_server) { + $uuid = (string)$cfg_stream_server->attributes()['uuid']; + $port = (isset($cfg_stream_server->listen_port) && $cfg_stream_server->listen_port != '') ? $cfg_stream_server->listen_port : null; + $server = $model->getNodeByReference('stream_server.' . $uuid); + $server->listen_address = (isset($port)) ? $port . ',[::]:' . $port : null; } + // run default migration actions + parent::run($model); } } diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Migrations/M1_35_1.php b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Migrations/M1_35_1.php new file mode 100644 index 0000000000..8f545c7fbd --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Migrations/M1_35_1.php @@ -0,0 +1,46 @@ + + * 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. + */ + +namespace OPNsense\Nginx\Migrations; + +use OPNsense\Base\BaseModelMigration; + +class M1_35_1 extends BaseModelMigration +{ + // Rewrite default ban_ttl value + public function run($model) + { + $general_node = $model->getNodeByReference('general'); + + if ($general_node->ban_ttl->isEqual('0')) { + $general_node->ban_ttl = '4320'; + } + // run default migration actions + parent::run($model); + } +} diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml index 07411f8568..f740f7297d 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml @@ -1,1069 +1,1091 @@ - //OPNsense/Nginx - 1.24.0 - nginx web server, reverse proxy and waf - - - - 0 - Y - - - - - - 0 - Y - - - - - - 1 - 1 - Y - - - 1024 - 1 - Y - - - 0 - N - - - 60 - N - - - N - - - N - 1 - - - N - 1 - - - - - - Y - - - - - - Selected user not found - Y - Y - - - - - - Y - - - Y - - - - - - Y - - - - - - Selected server not found - Y - Y - - - - IP Hash - - Weighted Round Robin - N - - - 0 - Y - - - Y - 0 - - - Y - 0 - - - cert - N - - - N - - - Y - - TLSv1 - TLSv1.1 - TLSv1.2 - TLSv1.3 - - N - - - Y - 1 - - - ca - Y - N - - - Y - 1 - - - N - 1 - 1 - - - - - - Y - - - Y - - - Y - - - 0 - Y - - - N - - - N - - - N - - - - Permanently Unreachable - Backup Server - - N - - - - - - Y - - - Y - - - - Exact Match ("=") - Case Sensitive Match ("~") - Case Insensitive Match ("~*") - Don't check regular expressions on logest prefix match ("^~") - - N - - - 0 - Y - - - 0 - Y - - - - - - Selected error page(s) not found - N - N - - - N - - - N - - - - - - Selected server not found - N - Y - - - - - - Selected upstream not found - N - N - - - N - /^[^" \t]+$/ - - - - - - Selected cache directory not found - N - N - - - Y - - Error - Timeout - Invalid_header - Updating - HTTP Status Code 403 - HTTP Status Code 404 - HTTP Status Code 429 - HTTP Status Code 500 - HTTP Status Code 502 - HTTP Status Code 503 - HTTP Status Code 504 - - N - - - Y - - Post - - - N - - - Y - 1 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - - - - Selected rewrite(s) not found - N - Y - - - N - - - N - - - N - - - - - - Selected user file not found - N - N - - - 0 - Y - - - N - - - Y - 0 - - - N - - - - - - Selected limit zone not found - N - Y - - - N - /^\d+[kmg]$/i - Enter a number followed by k, m or g. - - - N - /^\d+[kmg]$/i - Enter a number followed by k, m or g. - - - Y - 0 - - - Y - 0 - - - N - 1 - - - NgxBusyBufferConstraint - - - - - N - 1 - - - NgxBusyBufferConstraint - - - - - N - 1 - - - NgxBusyBufferConstraint - - - - - N - 1 - - - NgxBusyBufferConstraint - - - - - Y - 0 - - - Y - 1 - - - Y - 1 - - - N - 1 - - - N - 1 - - - Y - 0 - - - - - - Selected ACL not found - N - N - - - - Any - All - - N - - - N - 0 - - - Y - 0 - - - - - - Selected error page(s) not found - N - Y - - - - - - Y - - - - - - Selected rule not found - Y - Y - - - Y - - - Y - >= - - Bigger or Equal - Bigger - Lesser - Lesser or Equal - - - - Y - BLOCK - - Block Request - Allow Request - Drop The Connection - Log Request - - - - - - - Y - - - -
    Main Rule
    - Basic Rule -
    - Y -
    - - N - /^[^"]+$/ - - - This field must be set. - SetIfConstraint - match_type - id - - - - - Y - - - NaxsiIdentifierConstraint - - - - - N - /^[^"]+$/ - - - N - /^[^"]+$/ - - - This field must be set. - SetIfConstraint - match_type - id - - - - - Y - id - - Blacklist - Whitelist - - - - identifier.check001 - - - - - Y - - - N - 8 - - - This field must be set. - SetIfConstraint - match_type - id - - - - - Y - - - Y - - - 0 - Y - - - Y - - - 0 - Y - - - /^[^"]+$/ - - - /^[^"]+$/ - - - /^[^"]+$/ - - - Y - - - Y - - - Y - -
    - - - - Y - - - - - - Selected SYSLOG target not found - N - Y - - - N - Y - 80,[::]:80 - /^(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+)(?:\s*,\s*(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+))*$/i - Please provide a valid listen address or port, i.e. 127.0.0.1:8080, [::1]:8080, 8080. - - - NgxUniqueDefaultServerConstraint - - - - - N - Y - 443,[::]:443 - /^(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+)(?:\s*,\s*(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+))*$/i - Please provide a valid listen address or port, i.e. 127.0.0.1:8080, [::1]:8080, 8080. - - - Y - 0 - - - NgxUniqueDefaultServerConstraint - - - - - 0 - Y - - - N - /^((?:\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?(,?(?:(?:(\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?))*$/i - Y - - - - - - Selected alias not found - N - N - - - - X-Real-IP (default) - X-Forwarded-For - PROXY Protocol - CloudFlare Connecting IP - - N - - - - - - Selected location(s) not found - N - Y - - - - - - Selected rewrite(s) not found - N - Y - - - N - - - cert - N - - - ca - N - - - Off - - Off - On - Optional - Optional, don't verify - - Y - - - main - -
    Default
    - Extended - Anonymized - Disabled -
    - Y -
    - - Y - 1 - - - utf-8 - - utf-8 - - N - - - 0 - Y - - - Y - Y - - TLSv1.2 - TLSv1.3 - - Y - TLSv1.2,TLSv1.3 - - - ECDHE-ECDSA-CAMELLIA256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CAMELLIA256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CAMELLIA128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CAMELLIA128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-CAMELLIA256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-CAMELLIA256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-CAMELLIA128-SHA256:ECDHE-RSA-AES128-SHA256 - N - /^((((!|\+|-)?[A-Z][A-Z\d\+-]+)|(@STRENGTH)):?)*$/i - - - N - /^(([A-Z\d-]+):?)*$/i - - - 1 - Y - - - 0 - Y - - - 0 - Y - - - 0 - Y - - - 0 - Y - - - N - /^((?:\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?(,?(?:(?:(\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?))*$/i - Y - - - 0 - Y - - - 1 - Y - - - Y - 1 - 1 - - - Y - 4 - 1 - - - Y - 8 - 1 - - - - - - Selected security rule not found - N - N - - - - - - Selected limit zone not found - N - Y - - - N - /^\d+[kmg]$/i - Enter a number followed by k, m or g. - - - N - /^\d+[kmg]$/i - Enter a number followed by k, m or g. - - - - - - Selected ACL not found - N - N - - - N - N - Local Database - - - - Any - All - - N - - - Y - 0 - - - - - - Selected error page(s) not found - N - Y - -
    - - - - N - Y - /^(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+)(?:\s*,\s*(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\])):\d+|:?\d+))*$/i - Please provide a valid listen address or port, i.e. 127.0.0.1:8080, [::1]:8080, 8080. - - - - - - Selected SYSLOG target not found - N - Y - - - Y - 0 - - - N - /^((?:\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?(,?(?:(?:(\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?))*$/i - Y - - - 0 - Y - - - cert - N - - - ca - N - - - Off - - Off - On - Optional - Optional, don't verify - - Y - - - main - -
    Default
    - Extended - Anonymized - Disabled -
    - Y -
    - - upstream - - Upstream - SNI Upstream Mapping - - Y - - - - - - Selected upstream not found - N - N - - - This field must be set. - SetIfConstraint - route_field - upstream - - - - - - - - Selected upstream not found - N - N - - - This field must be set. - SetIfConstraint - route_field - sni_upstream_map - - - - - - - - Selected ACL not found - N - N - -
    - - - - Y - - - - Y - - - - - - Y - - - - - - Y - N - - - - - - Y - - - - Y - - - - Deny Access - Allow Access - - N - - - - - - Y - - - deny - - Deny Access - Allow Access - - Y - - - - - - Y - /^[^" \t]+$/i - - - Y - - - Y - /^[^" \t]+$/i - - - - Stop processing rules - Stop processing rules and find location - Redirect - Permanent - - N - - - - - - Y - - - N - - No Referrer - No Referrer When Downgrading - Same Origin (recommended) - Origin - Strict Origin - Strict Origin When Cross Origin - Origin When Cross Origin - Unsafe URL - - N - - - N - - Block - Off - On - - N - - - Y - - - N - - - Y - 1 - - - N - /[a-z0-9\+\/=]+(,[a-z0-9\+\/=]+)*/i - - - Y - - - N - - - Y - - - Y - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - N - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - Y - 0 - - - - - - Y - - - binary_remote_addr - - Remote IP Address - - Y - - - r/s - - Requests Per Second - Requests Per Minute - - Y - - - Y - 10 - 1 - - - Y - 20 - 1 - - - - - - Y - - - Y - - 400 Bad Request - 401 Unauthorized - 403 Forbidden - 404 Not Found - 405 Method Not Allowed - 407 Proxy Authentication Required - 408 Request Timeout - 410 Gone - 415 Unsupported Media Type - 429 Too Many Requests - 431 Request Header Fields Too Large - 500 Internal Server Error - 501 Not Implemented - 502 Bad Gateway - 503 Service Unavailable - 504 Gateway Timeout - - Y - - - PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KICAgIDxtZXRhIGNoYXJzZXQ9IlVURi04Ij4KICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsaW5pdGlhbC1zY2FsZT0xIiAvPgogICAgPHRpdGxlPkVycm9yPC90aXRsZT4KPC9oZWFkPgo8Ym9keT4KICAgIDxoMT5FcnJvcjwvaDE+CiAgICA8cD5Tb3JyeSwgYnV0IHNvbWV0aGluZyB3ZW50IHdyb25nLjwvcD4KPC9ib2R5Pgo8L2h0bWw+ - - - SingleSelectConstraint - Page content is required if redirect is not used and needs to be empty otherwise. - - redirect - - Y - - - - - N - - - pagecontent.check001 - - - - - N - - 200 OK - 300 Multiple Choice - 301 Moved Permanently - 302 Found - 401 Unauthorized - 403 Forbidden - 404 Not Found - 451 Unavailable For Legal Reasons - 500 Internal Server Error - 503 Service Unavailable - - N - - - - - - Y - - - Y - - - 0 - Y - - - - N - /^(0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)(?::((0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)))*$/ - - - - Y - /^(0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)(?::((0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)))*$/ - - - - - - - - - Selected limit zone not found - Y - Y - - - 1 - Y - 5 - - - 1 - N - 20 - - - 1 - Y - - - Y - - - - - - Y - - - - - - - Y - /\/(srv|var|tmp|mnt)[a-z0-9\-\._\:\,\/]+[a-z0-9\-\._\:\,]+/i - - - 10 - 10 - - - N - 1 - - - Y - 0 - - - N - 1 - - - - - - Y - - - Y - - - N - - - - kern - user - mail - daemon - auth - intern - lpr - news - uucp - clock - authpriv - ftp - ntp - audit - alert - cron - local0 - local1 - local2 - local3 - local4 - local5 - local6 - local7 - - Y - local7 - - - - debug - info - notice - warn - error - crit - alert - emerg - - Y - error - - - N - /[a-z][a-z0-9]*/i - - - Y - N - - -
    + Y + + + + Deny Access + Allow Access + + + + + + Y + + + deny + + Deny Access + Allow Access + + Y + + + + + Y + /^[^" \t]+$/i + +
    + Y + Y + /^(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\]))(:\d+|:?\d+)*)(?:\s*,\s*(?:(?:(?:\d{1,3}(?:\.\d{1,3}){3})|(?:\[[a-f0-9:]{1,4}(?::[a-f0-9:]{0,4}){1,7}\]))(:\d+|:?\d+)*))*$/i + Please provide a valid resolver address, i.e. 8.8.8.8, [2001:4860:4860::8888], 8.8.8.8:5353. +
    + + 1 + + + + + Can not disable all record types. + SingleSelectConstraint + + ipv6_off + + + + + + + + ipv4_off.check001 + + + + + 1 + +
    + + + Y + /^[^" \t]+$/i + + + Y + + + Y + /^[^" \t]+$/i + + + + Stop processing rules + Stop processing rules and find location + Redirect + Permanent + + + + + + Y + + + + No Referrer + No Referrer When Downgrading + Same Origin (recommended) + Origin + Strict Origin + Strict Origin When Cross Origin + Origin When Cross Origin + Unsafe URL + + + + + Block + Off + On + + + + Y + + + + Y + 1 + + + Y + 0 + + + Y + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + Y + 0 + + + + + Y + + + binary_remote_addr + + Remote IP Address + + Y + + + r/s + + Requests Per Second + Requests Per Minute + + Y + + + Y + 10 + 1 + + + Y + 20 + 1 + + + + + Y + + + Y + + 400 Bad Request + 401 Unauthorized + 403 Forbidden + 404 Not Found + 405 Method Not Allowed + 407 Proxy Authentication Required + 408 Request Timeout + 410 Gone + 415 Unsupported Media Type + 429 Too Many Requests + 431 Request Header Fields Too Large + 500 Internal Server Error + 501 Not Implemented + 502 Bad Gateway + 503 Service Unavailable + 504 Gateway Timeout + + Y + + + + + SingleSelectConstraint + Page content is required if redirect is not used and needs to be empty otherwise. + + redirect + + Y + + + + + + + pagecontent.check001 + + + + + + 200 OK + 300 Multiple Choice + 301 Moved Permanently + 302 Found + 401 Unauthorized + 403 Forbidden + 404 Not Found + 451 Unavailable For Legal Reasons + 500 Internal Server Error + 503 Service Unavailable + + + + + + Y + + + Y + + + 0 + Y + + + + /^(0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)(?::((0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)))*$/ + + + + Y + /^(0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)(?::((0x[0-9a-fA-F]{4,4}|[a-zA-Z_\-0-9]+)))*$/ + + + + + + + + Selected limit zone not found. + Y + Y + + + 1 + Y + 5 + + + 1 + + + 1 + Y + + + Y + + + + + Y + + + + + + Y + /\/(srv|var|tmp|mnt)[a-z0-9\-\._\:\,\/]+[a-z0-9\-\._\:\,]+/i + + + 10 + + + 1 + + + Y + 0 + + + 1 + + + + + Y + /^[^" \t]+$/i + + + Y + any + Y + /(^\d{3}(,\d{3})*$)|(^any$)/ + Please use three digit response code(s) or use "any" word. + + + 1 + Y + + + + + Y + + + Y + + + + + kern + user + mail + daemon + auth + intern + lpr + news + uucp + clock + authpriv + ftp + ntp + audit + alert + cron + local0 + local1 + local2 + local3 + local4 + local5 + local6 + local7 + + Y + local7 + + + + debug + info + notice + warn + error + crit + alert + emerg + + Y + error + + + /[a-z][a-z0-9]*/i + + + Y + N + + +
    diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/ban.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/ban.volt index 45284c7425..f5830cc435 100644 --- a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/ban.volt +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/ban.volt @@ -46,6 +46,11 @@ $(function () { 'options': { selection:false, multiSelect:false, + converters: { + timestamp: { + to: function (value) { return (new Date(value*1000)).toLocaleString(); } + } + }, formatters: { "delbtn": function (column, row) { return ``; diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt index 72bd40d91f..f64990afb1 100644 --- a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt @@ -26,6 +26,10 @@ #} - - - - + $(function() { + $("#nginx_config_copy").click(function () { + if (ngnx_config.length) { + $(this).fadeOut(); + navigator.clipboard.writeText(ngnx_config.join('\n')); + $(this).fadeIn(); + } + }); + $("#subtab_item_nginx-other-config-preview").click(function () { + $("#nginx_conf tbody").empty().append('{{ lang._(placeholder_txt) }}'); + }); + $("#conf_show_btn").click(function () { + ngnx_show_conf(); + }); + $("#conf_test_btn").click(function () { + ngnx_test_conf(); + }); + }); + + + + +
  • {{ lang._('SYSLOG Targets')}}
  • +
  • + {{ lang._('Config Preview')}} +
  • @@ -216,17 +294,18 @@ - - - - - - - - - - - + + + + + + + + + + + + @@ -247,11 +326,12 @@
    {{ lang._('Description') }}{{ lang._('URL Pattern') }}{{ lang._('URL Path Prefix') }}{{ lang._('Match Type') }}{{ lang._('Upstream') }}{{ lang._('WAF Status') }}{{ lang._('XSS Score') }}{{ lang._('SQLi Score') }}{{ lang._('WAF Policies') }}{{ lang._('Force HTTPS') }}{{ lang._('Commands') }}{{ lang._('ID') }}{{ lang._('Description') }}{{ lang._('URL Pattern') }}{{ lang._('URL Path Prefix') }}{{ lang._('Match Type') }}{{ lang._('Upstream') }}{{ lang._('WAF Status') }}{{ lang._('XSS Score') }}{{ lang._('SQLi Score') }}{{ lang._('WAF Policies') }}{{ lang._('Force HTTPS') }}{{ lang._('Commands') }}
    - - - - - + + + + + + @@ -273,11 +353,12 @@
    {{ lang._('Description') }}{{ lang._('Server') }}{{ lang._('Port') }}{{ lang._('Priority') }}{{ lang._('Commands') }}{{ lang._('ID') }}{{ lang._('Description') }}{{ lang._('Server') }}{{ lang._('Port') }}{{ lang._('Priority') }}{{ lang._('Commands') }}
    - - - - - + + + + + + @@ -297,8 +378,8 @@
    {{ lang._('Description') }}{{ lang._('Servers') }}{{ lang._('Load Balancing') }}{{ lang._('TLS Enabled') }}{{ lang._('Commands') }}{{ lang._('ID') }}{{ lang._('Description') }}{{ lang._('Servers') }}{{ lang._('Load Balancing') }}{{ lang._('TLS Enabled') }}{{ lang._('Commands') }}
    - - + + @@ -320,7 +401,7 @@ - + @@ -340,15 +421,16 @@
    {{ lang._('Username') }}{{ lang._('Commands') }}{{ lang._('Username') }}{{ lang._('Commands') }}
    {{ lang._('Name') }} {{ lang._('Users') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    + - + - - + + @@ -368,10 +450,11 @@
    {{ lang._('ID') }} {{ lang._('Servername') }} {{ lang._('Locations') }} {{ lang._('File System Root') }} {{ lang._('Certificate') }}{{ lang._('HTTPS Only') }}{{ lang._('HTTPS Only') }} {{ lang._('HTTP Address') }} {{ lang._('HTTPS Address') }}{{ lang._('Default') }}{{ lang._('Commands') }}{{ lang._('Default') }}{{ lang._('Commands') }}
    + - + - + @@ -392,10 +475,10 @@ - + - + @@ -426,11 +509,11 @@ - + - + @@ -450,14 +533,14 @@
    {{ lang._('ID') }} {{ lang._('Certificate') }}{{ lang._('UDP') }}{{ lang._('UDP') }} {{ lang._('Address') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    {{ lang._('Description') }}{{ lang._('Source URL') }}{{ lang._('Source URL') }} {{ lang._('Destination URL') }} {{ lang._('Flag') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    {{ lang._('Name') }}{{ lang._('Operator') }}{{ lang._('Operator') }} {{ lang._('Value') }} {{ lang._('Rules') }} {{ lang._('Action') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    + - + - - + - + @@ -478,7 +561,12 @@ - + + + + + + @@ -499,10 +587,34 @@ - - - - + + + + + + + + + + + + + + +
    {{ lang._('ID') }} {{ lang._('Description') }}{{ lang._('Rule Type') }}{{ lang._('Rule Type') }} {{ lang._('Match Type') }}{{ lang._('ID') }}{{ lang._('Score') }}{{ lang._('Score') }} {{ lang._('Value') }} {{ lang._('Message') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    {{ lang._('Description') }}{{ lang._('Commands') }}{{ lang._('Referrer') }}{{ lang._('XSS Protection') }}{{ lang._('HSTS') }}{{ lang._('CSP') }}{{ lang._('CSP Rules') }}{{ lang._('Commands') }}
    {{ lang._('Path') }}{{ lang._('Description') }}{{ lang._('Description') }}{{ lang._('Description') }}{{ lang._('Commands') }}{{ lang._('Size') }}{{ lang._('Inactive') }}{{ lang._('Max Size') }}{{ lang._('Commands') }}
    + + +
    +
    +
    + + + + + + + + @@ -527,7 +639,7 @@ - + @@ -552,7 +664,7 @@ - + @@ -573,7 +685,7 @@ - + @@ -594,7 +706,7 @@ - + @@ -617,7 +729,7 @@ - + @@ -638,7 +750,30 @@ - + + + + + + + + + + + +
    {{ lang._('ID') }}{{ lang._('Description') }}{{ lang._('Codes') }}{{ lang._('Time') }}{{ lang._('Commands') }}
    {{ lang._('Size') }} {{ lang._('Rate') }} {{ lang._('Rate Unit') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    {{ lang._('Connection Count') }} {{ lang._('Burst') }} {{ lang._('No Delay') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    {{ lang._('Description') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    {{ lang._('Description') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    {{ lang._('Name') }} {{ lang._('Status Codes') }} {{ lang._('Response') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    {{ lang._('Description') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    + + +
    +
    +
    + + + + + + + @@ -662,7 +797,7 @@ - + @@ -678,9 +813,31 @@
    {{ lang._('ID') }}{{ lang._('Description') }}{{ lang._('Address') }}{{ lang._('Commands') }}
    {{ lang._('Host') }} {{ lang._('Facility') }} {{ lang._('Severity') }}{{ lang._('Commands') }}{{ lang._('Commands') }}
    +
    +
    + + +
    + + + + + + +
    + +
    + + +
    +
    +
    +
    - {{ partial("layout_partials/base_dialog",['fields': upstream,'id':'upstreamdlg', 'label':lang._('Edit Upstream')]) }} {{ partial("layout_partials/base_dialog",['fields': upstream_server,'id':'upstreamserverdlg', 'label':lang._('Edit Upstream')]) }} {{ partial("layout_partials/base_dialog",['fields': location,'id':'locationdlg', 'label':lang._('Edit Location')]) }} @@ -691,12 +848,14 @@ {{ partial("layout_partials/base_dialog",['fields': httprewrite,'id':'httprewritedlg', 'label':lang._('Edit URL Rewrite')]) }} {{ partial("layout_partials/base_dialog",['fields': naxsi_custom_policy,'id':'custompolicydlg', 'label':lang._('Edit WAF Policy')]) }} {{ partial("layout_partials/base_dialog",['fields': naxsi_rule,'id':'naxsiruledlg', 'label':lang._('Edit Naxsi Rule')]) }} -{{ partial("layout_partials/base_dialog",['fields': security_headers,'id':'security_headersdlg', 'label':lang._('Edit Security Headers')]) }} +{{ partial("OPNsense/Nginx/tabbed_dialog",['fields': security_headers,'id':'security_headersdlg', 'label':lang._('Edit Security Headers')]) }} {{ partial("layout_partials/base_dialog",['fields': limit_request_connection,'id':'limit_request_connectiondlg', 'label':lang._('Edit Request Connection Limit')]) }} {{ partial("layout_partials/base_dialog",['fields': limit_zone,'id':'limit_zonedlg', 'label':lang._('Edit Limit Zone')]) }} {{ partial("layout_partials/base_dialog",['fields': cache_path,'id':'cache_pathdlg', 'label':lang._('Edit Cache Path')]) }} +{{ partial("layout_partials/base_dialog",['fields': proxy_cache_valid,'id':'proxy_cache_validdlg', 'label':lang._('Edit Response Code Caching')]) }} {{ partial("layout_partials/base_dialog",['fields': sni_hostname_map,'id':'sni_hostname_mapdlg', 'label':lang._('Edit SNI Hostname Mapping')]) }} {{ partial("layout_partials/base_dialog",['fields': ipacl,'id':'ipacl_dlg', 'label':lang._('Edit IP ACL')]) }} {{ partial("layout_partials/base_dialog",['fields': errorpage,'id':'errorpage_dlg', 'label':lang._('Edit Error Page')]) }} {{ partial("layout_partials/base_dialog",['fields': tls_fingerprint,'id':'tls_fingerprint_dlg', 'label':lang._('Edit TLS Fingerprint')]) }} +{{ partial("layout_partials/base_dialog",['fields': resolver,'id':'resolverdlg', 'label':lang._('Edit Resolver')]) }} {{ partial("layout_partials/base_dialog",['fields': syslog_target,'id':'syslog_target_dlg', 'label':lang._('Edit SYSLOG Target')]) }} diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt index 125fc5d63f..b920f706b6 100644 --- a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt @@ -25,7 +25,9 @@ # POSSIBILITY OF SUCH DAMAGE. #} -
    + + +
    diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/tabbed_dialog.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/tabbed_dialog.volt new file mode 100644 index 0000000000..0d5e75e9f0 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/tabbed_dialog.volt @@ -0,0 +1,183 @@ +{# + # Copyright (c) 2021 Manuel Faux + # OPNsense® is Copyright © 2014-2021 by Deciso B.V. + # 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. + #} + +{# + # Generate input dialog, uses the following parameters (as associative array): + # + # fields : list of field type objects, see form_input_tr tag for details + # id : form id, used as unique id for this modal form. inner form to place data is called frm_[id] + # save button is identified by btn_[id]_save + # label : dialog label + #} + +{# Volt templates in php7 have issues with scope sometimes, copy input values to make them more unique #} +{% set base_dialog_id=id %} +{% set base_dialog_fields=fields %} +{% set base_dialog_label=label %} + +{# Find if there are help supported or advanced field on this page #} +{% set base_dialog_help=false %} +{% set base_dialog_advanced=false %} +{% for field in base_dialog_fields|default({})%} + {% for name,element in field %} + {% if name=='help' %} + {% set base_dialog_help=true %} + {% endif %} + {% if name=='advanced' %} + {% set base_dialog_advanced=true %} + {% endif %} + {% endfor %} + {% if base_dialog_help|default(false) and base_dialog_advanced|default(false) %} + {% break %} + {% endif %} +{% endfor %} + + + + diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/vts.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/vts.volt index e76b581cc8..ba65faa935 100644 --- a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/vts.volt +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/vts.volt @@ -108,6 +108,10 @@ filter:"filterZones", upstream:"upstreamZones", cache:"cacheZones" + }, + classes: { + table: "table table-condensed table-hover table-striped", + div: "panel" } }; @@ -274,23 +278,21 @@ aHe('strong', it.sharedZones.name), byteFormat(it.sharedZones.maxSize), byteFormat(it.sharedZones.usedSize), it.sharedZones.usedNode]); body = aHe('tbody', aHe('tr', bodys[0])); - o = aHe('h2', vtsStatusVars.titles.main) + aHe('table', `${head}${body}`); - o = aHe(`div id="${vtsStatusVars.ids.main}"`, o); + o = aHe('h2', vtsStatusVars.titles.main) + aHe(`table class="${vtsStatusVars.classes.table}"`, `${head}${body}`); + o = aHe(`div id="${vtsStatusVars.ids.main}" class="${vtsStatusVars.classes.div}"`, o); return o; } function templateServerZone(filter, group, id, cache) { - let n = 0, s, o = ''; + let s, o = ''; for(const name in filter) { if (filter.hasOwnProperty(name)) { const zone = filter[name]; const uniq = `${id}.${group}.${name}`; - let clas = ''; let flag = ''; let responseCount = 0; let responseTotal = 0; let cacheCount = 0; let cacheTotal = 0; - clas = (n++ % 2) ? 'odd' : ''; flag = (group.indexOf("country") !== -1 && name.length === 2) ? `flag ${name.toLowerCase()}` : ''; @@ -322,7 +324,7 @@ } s += aHe('td', cacheTotal); } - o += aHe(`tr class="${clas}"`, s); + o += aHe('tr', s); } } return o; @@ -334,10 +336,9 @@ while (n < filter.length) { const peer = filter[n]; const uniq = `${id}.${group}.${peer.server}`; - let clas = ''; let responseCount = 0; let responseTotal = 0; - clas = (n++ % 2) ? 'odd' : ''; + n++; s = aHe('th', peer.server) + aHe('td', [formatAvailability(peer.backup, peer.down), formatTime(peer.responseMsec), peer.weight, peer.maxFails, peer.failTimeout, @@ -358,22 +359,19 @@ byteFormat(aPs.getValue(`${uniq}.outBytes`, peer.outBytes)), byteFormat(aPs.getValue(`${uniq}.inBytes`, peer.inBytes)) ]); - o += aHe(`tr class="${clas}"`, s); + o += aHe('tr', s); } return o; } function templateCacheZone(filter, group, id) { - let n = 0; let s; let o = ''; for(const name in filter) { if (filter.hasOwnProperty(name)) { const zone = filter[name]; const uniq = `${id}.${group}.${name}`; - let clas = ''; let cacheCount = 0; let cacheTotal = 0; - clas = (n++ % 2) ? 'odd' : ''; s = aHe('th', name) + aHe('td', [byteFormat(zone.maxSize), byteFormat(zone.usedSize), @@ -390,7 +388,7 @@ } } s += aHe('td', cacheTotal); - o += aHe(`tr class="${clas}"`, s); + o += aHe('tr', s); } } return o; @@ -414,7 +412,7 @@ head = templateServerHeader(cache); bodys[0] = templateServerZone(it.serverZones, 'server', vtsStatusVars.ids.server, cache); body = aHe('tbody', bodys[0]); - out += aHe(`div id="${vtsStatusVars.ids.server}"`, aHe('h2', vtsStatusVars.titles.server) + aHe('table', head + body)); + out += aHe(`div id="${vtsStatusVars.ids.server}" class="${vtsStatusVars.classes.div}"`, aHe('h2', vtsStatusVars.titles.server) + aHe(`table class="${vtsStatusVars.classes.table}"`, head + body)); /* filterZones */ if (vtsStatusVars.ids.filter in it) { tmp = ''; @@ -424,7 +422,7 @@ head = templateServerHeader(cache); bodys[0] = templateServerZone(filter, group, vtsStatusVars.ids.filter, cache); body = aHe('tbody', bodys[0]); - tmp += aHe('h3', group) + aHe('table', head + body); + tmp += aHe('h3', group) + aHe(`table class="${vtsStatusVars.classes.table}"`, head + body); } } out += aHe(`div id="${vtsStatusVars.ids.filter}"`, aHe('h2', vtsStatusVars.titles.filter) + tmp); @@ -444,10 +442,10 @@ group = g2.get('description'); } } - tmp += aHe('h3', group) + aHe('table', head + body); + tmp += aHe('h3', group) + aHe(`table class="${vtsStatusVars.classes.table}"`, head + body); } } - out += aHe(`div id="${vtsStatusVars.ids.upstream}"`, aHe('h2', vtsStatusVars.titles.upstream) + tmp); + out += aHe(`div id="${vtsStatusVars.ids.upstream}" class="${vtsStatusVars.classes.div}"`, aHe('h2', vtsStatusVars.titles.upstream) + tmp); } /* cacheZones */ if (vtsStatusVars.ids.cache in it) { @@ -455,7 +453,7 @@ bodys[0] = templateCacheZone(it.cacheZones, 'cache', vtsStatusVars.ids.cache); body = aHe('tbody', bodys[0]); out += aHe(`div id="${vtsStatusVars.ids.cache}"`, - aHe('h2', vtsStatusVars.titles.cache) + aHe('table', head + body)); + aHe('h2', vtsStatusVars.titles.cache) + aHe(`table class="${vtsStatusVars.classes.table}"`, head + body)); } return out; } diff --git a/www/nginx/src/opnsense/scripts/nginx/csp_report.php b/www/nginx/src/opnsense/scripts/nginx/csp_report.php index 116707a096..47352bfc3a 100755 --- a/www/nginx/src/opnsense/scripts/nginx/csp_report.php +++ b/www/nginx/src/opnsense/scripts/nginx/csp_report.php @@ -27,6 +27,8 @@ */ $log_file = '/var/log/nginx/csp_violations.log'; +$max_file_size = 1024 * 1024 * 30; // 30 MiB +$max_single_record_size = 1024 * 20; // 20 KiB // make sure we don't have any formatting issues here if (stristr($_SERVER['CONTENT_TYPE'], 'csp-report') === false) { @@ -41,6 +43,18 @@ $json_data['server_time'] = time(); $json_data['server_uuid'] = $_SERVER['SERVER-UUID']; $json_data = json_encode($json_data); + if (strlen($json_data) > $max_single_record_size) { + echo "The payload is too large"; + http_response_code(413); + exit(0); + } + if (file_exists($log_file)) { + if ((filesize($log_file) + strlen($json_data)) > $max_file_size) { + // silently drop the data + http_response_code(200); + exit(0); + } + } file_put_contents($log_file, $json_data . PHP_EOL, FILE_APPEND | LOCK_EX); } else { http_response_code(400); diff --git a/www/nginx/src/opnsense/scripts/nginx/list_logs.php b/www/nginx/src/opnsense/scripts/nginx/list_logs.php new file mode 100755 index 0000000000..93c951c348 --- /dev/null +++ b/www/nginx/src/opnsense/scripts/nginx/list_logs.php @@ -0,0 +1,107 @@ +#!/usr/local/bin/php + strlen($filename)) ? substr($file, strlen($filename) + 1, -3) : -1; + $result[$number] = array( + 'filename' => substr($file, strlen($log_prefix)), + 'date' => ($number >= 0) ? date('d/M/Y', filemtime($file) - 3600) : 'current', + 'number' => $number + ); + } + + ksort($result, SORT_NUMERIC); + $result = array_values($result); + + return $result; +} + +if ($_SERVER['argc'] < 3) { + die('{"error": "Incorrect amount of parameters given"}'); +} + +// first parameter: error|access +$mode = $_SERVER['argv'][1]; +// second parameter: uuid of server +$server = $_SERVER['argv'][2]; +$nginx = new Nginx(); + +$result = []; +// special cases: the global error log and the perm_ban access log +if ($server == 'global') { + $result = list_logfiles('error.log'); +} elseif ($server == 'perm_ban') { + $result = list_logfiles('perm_ban.access.log'); +} else { + switch ($mode) { + case 'error': + case 'access': + if ($data = $nginx->getNodeByReference('http_server.' . $server)) { + $server_names = (string)$data->servername; + if (empty($server_names)) { + die('{"error": "The server entry has no server name"}'); + } + + $log_file_name = basename($server_names) . '.' . $mode . $log_suffix; + $result = list_logfiles($log_file_name); + } else { + die('{"error": "UUID not found"}'); + } + break; + case 'streamerror': + case 'streamaccess': + if ($data = $nginx->getNodeByReference('stream_server.' . $server)) { + $mode = str_replace('stream', '', $mode); + $log_file_name = 'stream_' . $server . '.' . $mode . $log_suffix; + $result = list_logfiles($log_file_name); + } else { + die('{"error": "UUID not found"}'); + } + break; + default: + die('{"error": "action (' . $mode . ') not found"}'); + } +} + +echo json_encode($result); diff --git a/www/nginx/src/opnsense/scripts/nginx/naxsi_rule_download.php b/www/nginx/src/opnsense/scripts/nginx/naxsi_rule_download.php index 3eca0a62e0..00a8e7aec0 100755 --- a/www/nginx/src/opnsense/scripts/nginx/naxsi_rule_download.php +++ b/www/nginx/src/opnsense/scripts/nginx/naxsi_rule_download.php @@ -81,7 +81,7 @@ function parse_rules($data) } $tmp = trim($matches[1]); $parsed[$tmp] = []; - } elseif (preg_match('/\S+ "(str|rx):([^\"]+)" "msg:([^\\"]*)" "mz:([^\"]*)" "s:([^\"]*):(\d+)" id:(\d+);/', $line, $matches)) { + } elseif (preg_match('/\S+ "(str|rx):(.+)" "msg:([^\\"]*)" +"mz:([^\"]*)" "s:([^\"]*):(\d+)" id:(\d+);/', $line, $matches)) { $parsed[$tmp][] = prepare_values(array_combine($description, $matches)); } } @@ -100,8 +100,13 @@ function save_to_model($data) $policy->action = 'BLOCK'; // create new values for policy $rule_list = []; + $dis_rules = []; foreach ($rules as $rule) { $rule_mdl = $model->naxsi_rule->Add(); + // exclude commented rules from policy + if (str_starts_with($rule['rule'], '#')) { + $dis_rules[] = (string)$rule_mdl->getAttributes()["uuid"]; + } $rule_mdl->description = $rule['message']; $rule_mdl->message = $rule['message']; $rule_mdl->ruletype = 'main'; @@ -160,20 +165,12 @@ function save_to_model($data) } $rule_list[] = $rule_mdl->getAttributes()["uuid"]; } - $policy->naxsi_rules = implode(',', $rule_list); - } - - $val_result = $model->performValidation(false); - if (count($val_result) !== 0) { - print_r($val_result); - exit(1); + $policy->naxsi_rules = implode(',', array_diff($rule_list, $dis_rules)); } - - $model->serializeToConfig(); + // skip validation on serialization. possible warnings and errors will still end up in the syslog + $model->serializeToConfig(false, true); Config::getInstance()->save(); } - -#$data = parse_rules(file('./naxsi_core.rules')); $data = parse_rules(explode("\n", download_rules())); save_to_model($data); diff --git a/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php b/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php index dd953087f7..93bfab68bc 100755 --- a/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php +++ b/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php @@ -76,7 +76,7 @@ function modify_blocklist($tablename, array $allIps, $operation = "add"): void foreach (array_chunk($allIps, $chunkSize) as $ips) { $escapedIps = join(" ", array_map("escapeshellarg", $ips)); - exec_hidden("/sbin/pfctl -t ${tablename} -T ${operation} ${escapedIps}"); + exec_hidden("/sbin/pfctl -t {$tablename} -T {$operation} {$escapedIps}"); } } @@ -89,7 +89,7 @@ function read_all_from_blocklist($tablename) 2 => ['file', "/dev/null", "w"], ]; - $process = proc_open("/sbin/pfctl -t ${tablename} -T show", $descriptorspec, $pipes); + $process = proc_open("/sbin/pfctl -t {$tablename} -T show", $descriptorspec, $pipes); if (is_resource($process)) { $ips = []; while ($ip = fgets($pipes[1], 96)) { @@ -113,7 +113,7 @@ function get_files_lastmodified(array $files): array // - No content => -1 $times = []; foreach ($files as $file) { - $mtime = @filemtime($file) ?: rand(); + $mtime = @filemtime($file) ?: random_int(0, getrandmax()); $times[$file] = @filesize($file) === 0 ? -1 : $mtime; } return $times; @@ -166,12 +166,9 @@ function create_work_files($include_tls_handshake) @touch($target); $work_files[] = $target; } else { - log_error("Failed renaming '$source' to '$target'. Skipping source for next run."); + log_msg("Failed renaming '$source' to '$target'. Skipping source for next run."); } } - } else { - //Concurrent invocation. Can be silently ignored since no work files are collected. - //log_error("Skipping processing. Missing: " . join(", ", array_diff(array_keys($mapping), $existing_sources))); } reopen_logs(); @@ -219,7 +216,7 @@ function cleanup_work_files($work_files) // Triggering TLS-handshake processor when corresponding work file exists. if (in_array(TLS_HANDSHAKE_FILE_WORK, $work_files)) { - mwexec(TLS_HANDSHAKE_PROCESSING_TASK); + mwexecf(TLS_HANDSHAKE_PROCESSING_TASK); } // Abort if permanent ban file is missing @@ -259,8 +256,19 @@ function cleanup_work_files($work_files) // Reading stored banned IPs from config $model = new Nginx(); $alias_ips = []; - foreach ($model->ban->iterateItems() as $entry) { - $alias_ips[] = (string)$entry->ip; + $ban_ttl = intval((string)$model->general->ban_ttl); + if ($ban_ttl && ($ban_ttl > 0)) { + $min_timestamp = time() - 60 * $ban_ttl; + } + $change_required = false; + foreach ($model->ban->iterateItems() as $id => $entry) { + if ($min_timestamp && (intval((string)$entry->time) < $min_timestamp)) { + // Delete expired records from config + $model->ban->Del($id); + $change_required = true; + } else { + $alias_ips[] = (string)$entry->ip; + } } // Collecting all new IPs from ban file not yet in $alias_ips. @@ -284,8 +292,7 @@ function cleanup_work_files($work_files) })(); // Transfering new IPs into $alias_ips and store them permanently. - $new_and_alias_ips = (function () use ($model, $new_ips, $alias_ips) { - $change_required = false; + $new_and_alias_ips = (function () use ($model, $new_ips, $alias_ips, $change_required) { foreach ($new_ips as $new_ip) { $alias_ips[] = $new_ip; diff --git a/www/nginx/src/opnsense/scripts/nginx/ngx_functions.js b/www/nginx/src/opnsense/scripts/nginx/ngx_functions.js index 5f17d76ee9..bdfdbbbcb7 100755 --- a/www/nginx/src/opnsense/scripts/nginx/ngx_functions.js +++ b/www/nginx/src/opnsense/scripts/nginx/ngx_functions.js @@ -1,13 +1,17 @@ var fs = require('fs'); var tls_fingerprints = JSON.parse(fs.readFileSync('/usr/local/etc/nginx/tls_fingerprints.json')); +// ignore GREASE cipher suite values when compiling a browser fingerprint (see rfc8701) +const GREASE = ["0x0a0a", "0x1a1a", "0x2a2a", "0x3a3a", "0x4a4a", "0x5a5a", "0x6a6a", "0x7a7a", "0x8a8a", "0x9a9a", "0xaaaa", "0xbaba", "0xcaca", "0xdada", "0xeaea", "0xfafa"]; +// ignore SCSV cipher suite values when compiling a browser fingerprint (see rfc5746 and rfc7507) +const SCSV = ["TLS_EMPTY_RENEGOTIATION_INFO_SCSV", "TLS_FALLBACK_SCSV"]; function check_cipher_array(r, browser_ciphers, fingerprint_ciphers, result) { - if (result.status == 'Intercepted') { + if (result.status.includes('Intercepted')) { return; } if (browser_ciphers.length > fingerprint_ciphers.length) { - // the proxy supports more cipers than the browser -> intercepted - result.status = "Intercepted"; + // the proxy supports more ciphers than the browser -> intercepted + result.status = "Intercepted; Reason=\"excess suite\""; return; } var browser_cipher; @@ -18,9 +22,9 @@ function check_cipher_array(r, browser_ciphers, fingerprint_ciphers, result) { browser_cipher = browser_ciphers[browser_cipher_index]; current_index = fingerprint_ciphers.indexOf(browser_cipher); if (current_index === -1 || current_index <= last_index) { - // a cipher has been found, which is not supported by the browser + // a cipher has been found, which is not supported by the browser or order of preference changed // such a connection is definitly intercepted - result.status = "Intercepted"; + result.status = "Intercepted; Reason=\"excess suite or wrong order\""; return; } last_index = current_index; @@ -36,14 +40,28 @@ function check_intercept(r) { var ua = r.headersIn['User-Agent']; if (ua in tls_fingerprints) { var fp = tls_fingerprints[ua]; + fp.ciphers = fp.ciphers.filter( function( el ) { + return ((GREASE.indexOf( el ) < 0) && (SCSV.indexOf( el ) < 0)); + } ); + fp.curves = fp.curves.filter( function( el ) { + return GREASE.indexOf( el ) < 0; + } ); var browser_ciphers = r.variables.ssl_ciphers.split(':'); + browser_ciphers = browser_ciphers.filter( function( el ) { + return ((GREASE.indexOf( el ) < 0) && (SCSV.indexOf( el ) < 0)); + } ); check_cipher_array(r, browser_ciphers, fp.ciphers, tls_result); if (r.variables.ssl_curves != '') { var browser_curves = r.variables.ssl_curves.split(':'); + browser_curves = browser_curves.filter( function( el ) { + return GREASE.indexOf( el ) < 0; + } ); check_cipher_array(r, browser_curves, fp.curves, tls_result); } } } return tls_result.status; } + +export default { check_intercept }; diff --git a/www/nginx/src/opnsense/scripts/nginx/ngx_showConfig.py b/www/nginx/src/opnsense/scripts/nginx/ngx_showConfig.py new file mode 100755 index 0000000000..986cece76f --- /dev/null +++ b/www/nginx/src/opnsense/scripts/nginx/ngx_showConfig.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +# nginx -T shows the config only if the test succeeds +# grab nginx config from file(s) and send to stdout + +import os.path +import glob +import ujson + +result = dict() +nginx_config = [] +nginx_config_root = '/usr/local/etc/nginx/' +nginx_config_file = nginx_config_root + 'nginx.conf' + +def load_config_file(config_path): + """ load config with all inclusions + """ + config_incs = [] + # mimic 'nginx -T' syntax for config files references + nginx_config.append('# configuration file ' + config_path + ':') + for line in open(config_path, 'r').read().split('\n'): + nginx_config.append(line.rstrip()) + line = line.strip() + if line.startswith('include '): + # only '*' mask is supported/used in plugin + if '*' not in line: + # it's a file relative path + incfilepath = nginx_config_root + line.split(' ')[-1][:-1] + if os.path.isfile(incfilepath): + config_incs.append(incfilepath) + else: + # it's a path with a file mask + incdir = nginx_config_root + line.split(' ')[-1][:-1] + for incfilepath in glob.glob(incdir): + config_incs.append(incfilepath) + for inc in list(dict.fromkeys(config_incs)): + load_config_file(inc) + +if os.path.isfile(nginx_config_file): + result['time'] = os.path.getmtime(nginx_config_file) + load_config_file(nginx_config_file) + result['config'] = nginx_config +print(ujson.dumps(result)) diff --git a/www/nginx/src/opnsense/scripts/nginx/ngx_testConfig.sh b/www/nginx/src/opnsense/scripts/nginx/ngx_testConfig.sh new file mode 100755 index 0000000000..8b085a1f09 --- /dev/null +++ b/www/nginx/src/opnsense/scripts/nginx/ngx_testConfig.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# run nginx config test. return error text if any. always exit with 0 +if conf_test_errors=$(nginx -t -q 2>&1); then + echo "config is ok" +else + echo "$conf_test_errors" +fi + +exit 0 diff --git a/www/nginx/src/opnsense/scripts/nginx/read_log.php b/www/nginx/src/opnsense/scripts/nginx/read_log.php index 488c018efc..ac11b9f1ec 100755 --- a/www/nginx/src/opnsense/scripts/nginx/read_log.php +++ b/www/nginx/src/opnsense/scripts/nginx/read_log.php @@ -2,7 +2,8 @@ 0) ? max(intval($_SERVER['argv'][3]), -1) : -1; +// third parameter: current page +$page = max(intval($_SERVER['argv'][4]), 0); +// fourth parameter: lines per page +$per_page = max(intval($_SERVER['argv'][5]), 0); +// fifth parameter: filter query +$query = json_decode(base64_decode($_SERVER['argv'][6]), true); $nginx = new Nginx(); +if (!is_array($query)) { + $query = array(); +} + +if ($file_no >= 0) { + $log_suffix .= ".$file_no.gz"; +} + +$result = []; // special case: the global error log if ($server == 'global') { - $logparser = new ErrorLogParser($log_prefix . 'error.log'); - echo json_encode(empty($logparser->get_result()) ? - array('error' => 'no lines found') : - $logparser->get_result()); - exit(0); -} + $logparser = new ErrorLogParser($log_prefix . 'error' . $log_suffix, $page, $per_page, $query); +} elseif ($server == 'perm_ban') { + $logparser = new AccessLogParser($log_prefix . 'perm_ban.access' . $log_suffix, $page, $per_page, $query); +} else { + switch ($mode) { + case 'error': + case 'access': + if ($data = $nginx->getNodeByReference('http_server.' . $server)) { + $server_names = (string)$data->servername; + if (empty($server_names)) { + die('{"error": "The server entry has no server name"}'); + } + $log_file_name = $log_prefix . basename($server_names) . '.' . $mode . $log_suffix; + // this entry has no log file, ignore it + if (!file_exists($log_file_name)) { + break; + } + $logparser = null; -switch ($mode) { - case 'error': - case 'access': - if ($data = $nginx->getNodeByReference('http_server.' . $server)) { - $server_names = (string)$data->servername; - if (empty($server_names)) { - die('{"error": "The server entry has no server name"}'); - } - $lines = []; - $log_file_name = $log_prefix . basename($server_names) . '.' . $mode . $log_suffix; - // this entry has no log file, ignore it - if (!file_exists($log_file_name)) { - break; + if ($mode == 'error') { + $logparser = new ErrorLogParser($log_file_name, $page, $per_page, $query); + } elseif ($mode == 'access') { + $logparser = new AccessLogParser($log_file_name, $page, $per_page, $query); + } + } else { + die('{"error": "UUID not found"}'); } - $logparser = null; + break; + case 'streamerror': + case 'streamaccess': + if ($data = $nginx->getNodeByReference('stream_server.' . $server)) { + $mode = str_replace('stream', '', $mode); + $log_file_name = $log_prefix . 'stream_' . $server . '.' . $mode . $log_suffix; + // this entry has no log file, ignore it + if (!file_exists($log_file_name)) { + die('{"error": "file not found"}'); + } + $logparser = null; - if ($mode == 'error') { - $logparser = new ErrorLogParser($log_file_name); - } elseif ($mode == 'access') { - $logparser = new AccessLogParser($log_file_name); - } - // we cannot parse the file - something went wrong - if ($logparser == null) { - break; - } - $lines = array_merge($lines, $logparser->get_result()); - if (empty($lines)) { - $lines['error'] = 'no lines found'; + if ($mode == 'error') { + $logparser = new ErrorLogParser($log_file_name, $page, $per_page, $query); + } elseif ($mode == 'access') { + $logparser = new StreamAccessLogParser($log_file_name, $page, $per_page, $query); + } + } else { + die('{"error": "UUID not found"}'); } - echo json_encode($lines); - } else { - die('{"error": "UUID not found"}'); - } - break; - case 'streamerror': - case 'streamaccess': - if ($data = $nginx->getNodeByReference('stream_server.' . $server)) { - $lines = []; - $mode = str_replace('stream', '', $mode); - $log_file_name = $log_prefix . 'stream_' . $server . '.' . $mode . $log_suffix; - // this entry has no log file, ignore it - if (!file_exists($log_file_name)) { - die('{"error": "file not found"}'); - } - $logparser = null; + break; + default: + die('{"error": "action (' . $mode . ') not found"}'); + } +} - if ($mode == 'error') { - $logparser = new ErrorLogParser($log_file_name); - } elseif ($mode == 'access') { - $logparser = new StreamAccessLogParser($log_file_name); - } - // we cannot parse the file - something went wrong - if ($logparser == null) { - break; - } - $lines = array_merge($lines, $logparser->get_result()); - if (empty($lines)) { - $lines['error'] = 'no lines found'; - } - echo json_encode($lines); - } else { - die('{"error": "UUID not found"}'); - } - break; - default: - die('{"error": "action (' . $mode . ') not found"}'); + +// we cannot parse the file - something went wrong +if ($logparser === null) { + $result['error'] = 'cannot retrieve requested logs'; +} else { + $result['lines'] = $logparser->get_result(); + $result['pages'] = $logparser->page_count; + $result['total'] = $logparser->total_lines; + $result['found'] = $logparser->query_lines; + $result['returned'] = count($result['lines']); + $result['query'] = json_encode($query); } + +echo json_encode($result); diff --git a/www/nginx/src/opnsense/scripts/nginx/setup.php b/www/nginx/src/opnsense/scripts/nginx/setup.php index 849b482572..8d1c7af503 100755 --- a/www/nginx/src/opnsense/scripts/nginx/setup.php +++ b/www/nginx/src/opnsense/scripts/nginx/setup.php @@ -29,8 +29,11 @@ const KEY_DIRECTORY = '/usr/local/etc/nginx/key/'; const GROUP_OWNER = 'staff'; + require_once('config.inc'); require_once('certs.inc'); +require_once('util.inc'); + use OPNsense\Nginx\Nginx; function export_pem_file($filename, $data, $post_append = null) @@ -68,12 +71,22 @@ function find_ca($refid) if (!isset($config['OPNsense']['Nginx'])) { die("nginx is not configured"); } +openlog("nginx", LOG_ODELAY, LOG_USER); +syslog(LOG_DEBUG, "NGINX setup routine started."); @mkdir('/usr/local/etc/nginx/key', 0750, true); @mkdir("/var/db/nginx/auth", 0750, true); @mkdir("/var/log/nginx", 0750, true); +// check logs dir permissions. in case if logs moved to tmpfs +@chmod('/var/log/nginx', 0750); @chgrp('/var/db/nginx', GROUP_OWNER); @chgrp('/var/db/nginx/auth', GROUP_OWNER); @chgrp('/var/log/nginx', GROUP_OWNER); +// unlink VTS socket if nginx didn't +$vts_socket = '/var/run/nginx_status.sock'; +if (!isvalidpid('/var/run/nginx.pid') && file_exists($vts_socket)) { + syslog(LOG_WARNING, "NGINX setup: nginx not running but VTS socket exists. Unlinking."); + @unlink($vts_socket); +} $nginx = $config['OPNsense']['Nginx']; if (isset($nginx['http_server'])) { if (is_array($nginx['http_server']) && !isset($nginx['http_server']['servername'])) { @@ -84,8 +97,10 @@ function find_ca($refid) foreach ($http_servers as $http_server) { if (!empty($http_server['listen_https_address']) && !empty($http_server['certificate'])) { // try to find the reference + $hostname = explode(',', $http_server['servername'])[0]; $cert = find_cert($http_server['certificate']); if (!isset($cert)) { + syslog(LOG_ERR, "NGINX setup: Certificate is set but not found in config for server {$hostname}."); continue; } $chain = []; @@ -94,8 +109,9 @@ function find_ca($refid) foreach ($ca_chain as $entry) { $chain[] = base64_decode($entry['crt']); } + } else { + syslog(LOG_WARNING, "NGINX setup: Certificate chain is empty for server {$hostname}."); } - $hostname = explode(',', $http_server['servername'])[0]; export_pem_file( KEY_DIRECTORY . $hostname . '.pem', $cert['crt'], @@ -106,15 +122,25 @@ function find_ca($refid) $cert['prv'] ); if (!empty($http_server['ca'])) { - foreach ($http_server['ca'] as $caref) { - $ca = find_ca($caref); - if (isset($ca)) { - export_pem_file( - KEY_DIRECTORY . $hostname . '_ca.pem', - $ca['crt'] - ); + syslog(LOG_DEBUG, "NGINX setup: Setting up the CA certs for {$hostname}."); + $ca_certs = []; + foreach ($http_server['ca'] as $carefs) { + foreach (explode(',', $carefs) as $caref) { + syslog(LOG_DEBUG, "NGINX setup: Searching for {$caref} CA data"); + $ca = find_ca($caref); + if (isset($ca)) { + syslog(LOG_DEBUG, "NGINX setup: client auth CA found. Adding to the list"); + $ca_certs[] = base64_decode($ca['crt']); + } } } + if (count($ca_certs) > 0) { + export_pem_file( + KEY_DIRECTORY . $hostname . '_ca.pem', + '', + implode("\n", $ca_certs) + ); + } } } } @@ -131,6 +157,7 @@ function find_ca($refid) // try to find the reference $cert = find_cert($stream_server['certificate']); if (!isset($cert)) { + syslog(LOG_ERR, "NGINX setup: Certificate is set but not found in config for stream server {$stream_server['listen_address']}."); continue; } $chain = []; @@ -139,6 +166,8 @@ function find_ca($refid) foreach ($ca_chain as $entry) { $chain[] = base64_decode($entry['crt']); } + } else { + syslog(LOG_WARNING, "NGINX setup: Certificate chain is empty for stream server {$stream_server['listen_address']}."); } export_pem_file( KEY_DIRECTORY . $stream_server['@attributes']['uuid'] . '.pem', @@ -194,6 +223,8 @@ function find_ca($refid) KEY_DIRECTORY . $upstream['tls_client_certificate'] . '.key', $cert['prv'] ); + } else { + syslog(LOG_ERR, "NGINX setup: Client certificate is set but not found in config for upstream {$upstream['description']}."); } } if (!empty($upstream['tls_trusted_certificate'])) { @@ -203,6 +234,8 @@ function find_ca($refid) $ca = find_ca($caref); if (isset($ca)) { $cas[] = base64_decode($ca['crt']); + } else { + syslog(LOG_ERR, "NGINX setup: Trusted CA certificate is set but not found in config for upstream {$upstream['description']}."); } } export_pem_file( @@ -228,7 +261,7 @@ function find_ca($refid) foreach ($users as $user) { $user_node = $nginx->getNodeByReference("credential." . $user); $username = (string)$user_node->username; - $password = crypt((string)$user_node->password); + $password = password_hash((string)$user_node->password, PASSWORD_DEFAULT); fwrite($file, $username . ':' . $password . "\n"); } } finally { @@ -243,6 +276,7 @@ function find_ca($refid) // create directories for cache foreach ($nginx->cache_path->iterateItems() as $cache_path) { @mkdir((string)$cache_path->path, 0755, true); + @chgrp((string)$cache_path->path, GROUP_OWNER); } // create custom error pages @@ -304,7 +338,7 @@ function find_ca($refid) if ((string)$tls_fingerprint->trusted == '1') { $ciphers = explode(':', (string)$tls_fingerprint->ciphers); if (!empty((string)$tls_fingerprint->curves)) { - $curves = explode(':', (string)$tls_fingerprint->ciphers); + $curves = explode(':', (string)$tls_fingerprint->curves); } else { $curves = array(); } @@ -318,3 +352,16 @@ function find_ca($refid) empty($tls_fingerprint_database) ? '{}' : json_encode($tls_fingerprint_database) ); chmod('/usr/local/etc/nginx/tls_fingerprints.json', 0644); + +// test config and exit early if it not good +$conf_test_errors = shell_safe('nginx -t -q 2>&1'); +if (!empty($conf_test_errors)) { + syslog(LOG_EMERG, $conf_test_errors); + closelog(); + exit(1); +} + +syslog(LOG_DEBUG, "NGINX setup routine completed."); +closelog(); + +pass_safe('/usr/local/etc/rc.d/php-fpm start'); diff --git a/www/nginx/src/opnsense/scripts/nginx/tls_ua_fingerprint.php b/www/nginx/src/opnsense/scripts/nginx/tls_ua_fingerprint.php index 7ba8f1b16d..89fd7148e6 100755 --- a/www/nginx/src/opnsense/scripts/nginx/tls_ua_fingerprint.php +++ b/www/nginx/src/opnsense/scripts/nginx/tls_ua_fingerprint.php @@ -31,13 +31,25 @@ function parse_line($line) { + // ignore GREASE cipher suite values when compiling a browser fingerprint (see rfc8701) + $GREASE = array("0x0a0a", "0x1a1a", "0x2a2a", "0x3a3a", "0x4a4a", "0x5a5a", "0x6a6a", "0x7a7a", "0x8a8a", "0x9a9a", "0xaaaa", "0xbaba", "0xcaca", "0xdada", "0xeaea", "0xfafa"); + // ignore SCSV cipher suite values when compiling a browser fingerprint (see rfc5746 and rfc7507) + $SCSV = array("TLS_EMPTY_RENEGOTIATION_INFO_SCSV", "TLS_FALLBACK_SCSV"); $tmp = explode('"', trim($line)); - return array( + $fp = array( 'ua' => $tmp[1], 'ciphers' => $tmp[3], 'curves' => $tmp[5] == '-' ? '' : $tmp[5], 'count' => 1 ); + // exclude GREASE and SCSV suits from fingerprint + $fp_ciphers = explode(':', $fp['ciphers']); + $fp_ciphers = array_diff($fp_ciphers, $GREASE, $SCSV); + $fp['ciphers'] = implode(':', $fp_ciphers); + $fp_curves = explode(':', $fp['curves']); + $fp_curves = array_diff($fp_curves, $GREASE); + $fp['curves'] = implode(':', $fp_curves); + return $fp; } function filter_ua($key) { @@ -61,13 +73,13 @@ function filter_ua($key) $handle = @fopen($tls_logfile, 'r'); if ($handle) { while (($buffer = fgets($handle)) !== false) { - $md5line = md5($buffer); - if (array_key_exists($md5line, $fingerprints)) { - $fingerprints[$md5line]['count']++; - } else { - $parsed_line = parse_line($buffer); - if ($parsed_line['ciphers'] != '-') { - $fingerprints[$md5line] = $parsed_line; + $parsed_line = parse_line($buffer); + if ($parsed_line['ciphers'] != '-') { + $md5fp = md5($parsed_line['ua'] . $parsed_line['ciphers'] . $parsed_line['curves']); + if (array_key_exists($md5fp, $fingerprints)) { + $fingerprints[$md5fp]['count']++; + } else { + $fingerprints[$md5fp] = $parsed_line; } } } diff --git a/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf b/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf index 1cb7927e53..ce44adf64d 100644 --- a/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf +++ b/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf @@ -1,5 +1,5 @@ [start] -command:/usr/local/opnsense/scripts/nginx/setup.php;/usr/local/etc/rc.d/php-fpm start;/usr/local/etc/rc.d/nginx start +command:/usr/local/etc/rc.d/nginx start parameters: type:script message:starting nginx @@ -11,26 +11,33 @@ type:script message:stopping nginx [restart] -command:/usr/local/opnsense/scripts/nginx/setup.php;/usr/local/etc/rc.d/php-fpm restart;/usr/local/etc/rc.d/nginx restart +command:/usr/local/etc/rc.d/nginx restart parameters: type:script message:restarting nginx -[status] -command:/usr/local/etc/rc.d/nginx status;exit 0 +[reload] +command:/usr/local/etc/rc.d/nginx reload parameters: -type:script_output +type:script +message:reloading nginx -[phpstatus] -command:/usr/local/etc/rc.d/php-fpm status; exit 0 +[status] +command:/usr/local/etc/rc.d/nginx status; exit 0 parameters: type:script_output [log] command:/usr/local/opnsense/scripts/nginx/read_log.php +parameters: %s %s %s %s %s %s +type:script_output +message:querying nginx %s log for %s rotate %s (page %s of %s with filter %s) + +[listlogs] +command:/usr/local/opnsense/scripts/nginx/list_logs.php parameters: %s %s type:script_output -message:restarting nginx +message:listing nginx %s log for %s [tls_handshakes] command:cat /var/log/nginx/handshakes.json @@ -61,8 +68,12 @@ command:/usr/local/opnsense/scripts/nginx/vts.php parameters: type:script_output -[reload] -command:/usr/local/opnsense/scripts/nginx/setup.php;/usr/local/etc/rc.d/php-fpm restart;/usr/local/etc/rc.d/nginx reload +[test_config] +command:/usr/local/opnsense/scripts/nginx/ngx_testConfig.sh +parameters: +type:script_output + +[show_config] +command:/usr/local/opnsense/scripts/nginx/ngx_showConfig.py parameters: type:script_output -message:reloading nginx diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf index 287b6893af..cc316538ac 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf @@ -14,6 +14,11 @@ log_format main_ext '$remote_addr - $remote_user [$time_local] "$request" ' 'ua="$upstream_addr" us="$upstream_status" ' 'ut="$upstream_response_time" ul="$upstream_response_length" ' 'cs=$upstream_cache_status'; +{% if OPNsense.Nginx.http.log_perm_ban is defined and OPNsense.Nginx.http.log_perm_ban == '1' %} +log_format main_ban '$remote_addr - $remote_user [$time_local] "$scheme://$host$request_uri" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; +{% endif %} log_format handshake '"$http_user_agent" "$ssl_ciphers" "$ssl_curves"'; log_format anonymized ':: - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' @@ -21,8 +26,8 @@ log_format anonymized ':: - $remote_user [$time_local] "$request" ' #tcp_nopush on; # https intercept detection -js_include /usr/local/opnsense/scripts/nginx/ngx_functions.js; -js_set $tls_intercepted check_intercept; +js_import /usr/local/opnsense/scripts/nginx/ngx_functions.js; +js_set $tls_intercepted ngx_functions.check_intercept; # 200M should be big enough for file servers etc. client_max_body_size 200M; @@ -43,15 +48,32 @@ server_names_hash_max_size {{ OPNsense.Nginx.http.server_names_hash_max_size }}; {% if OPNsense.Nginx.http.server_names_hash_bucket_size is defined and OPNsense.Nginx.http.server_names_hash_bucket_size != '' %} server_names_hash_bucket_size {{ OPNsense.Nginx.http.server_names_hash_bucket_size }}; {% endif %} +{% if OPNsense.Nginx.http.variables_hash_max_size is defined and OPNsense.Nginx.http.variables_hash_max_size != '' %} +variables_hash_max_size {{ OPNsense.Nginx.http.variables_hash_max_size }}; +{% endif %} +{% if OPNsense.Nginx.http.variables_hash_bucket_size is defined and OPNsense.Nginx.http.variables_hash_bucket_size != '' %} +variables_hash_bucket_size {{ OPNsense.Nginx.http.variables_hash_bucket_size }}; +{% endif %} {% if OPNsense.Nginx.http.keepalive_timeout is defined and OPNsense.Nginx.http.keepalive_timeout != '' %} keepalive_timeout {{ OPNsense.Nginx.http.keepalive_timeout }}; {% endif %} +{% if OPNsense.Nginx.http.reset_timedout is defined and OPNsense.Nginx.http.reset_timedout == '1' %} +reset_timedout_connection on; +{% endif %} map $http_upgrade $connection_upgrade { default upgrade; '' close; } +# Map used in location.conf for proxy_ssl_name +map $ssl_server_name $upstream_sni_name { + default $ssl_server_name; + '' $host; +} + +include http_post/*.conf; + # TODO add when core is ready for allowing nginx to serve the web interface # include nginx_web.conf; @@ -97,39 +119,71 @@ server { {% endfor %} {% endif %} -{% if server.listen_https_address is defined and server.listen_https_address != '' and server.certificate is defined %} +{% if server.listen_https_address is defined and server.listen_https_address != '' %} +{% set http3_alt_svc_ports = [] %} {% for listen_address in server.listen_https_address.split(',') %} - listen {{ listen_address }} http2 ssl; + listen {{ listen_address }} ssl{% if server.proxy_protocol is defined and server.proxy_protocol == '1' %} proxy_protocol{% endif %}{% if server.default_server is defined and server.default_server == '1' %} default_server{% endif %}; +{% if server.enable_http3|default("0") == "1" %} + listen {{ listen_address }} quic reuseport{% if server.default_server is defined and server.default_server == '1' %} default_server{% endif %}; +{% set listen_address_clean = listen_address.replace(' ', '') %} +{% if listen_address_clean != '' %} +{% set listen_port = listen_address_clean.split(':')[-1] %} +{% if listen_port not in http3_alt_svc_ports %} +{% do http3_alt_svc_ports.append(listen_port) %} +{% endif %} +{% endif %} +{% endif %} {% endfor %} -{% if server.ca is defined %} + http2 {% if server.http2|default("1") == "1" %}on{% else %}off{% endif %}; +{% if server.tls_reject_handshake is defined and server.tls_reject_handshake == '1'%} + ssl_reject_handshake on; +{% endif %} +{% if server.certificate is defined %} +{% if server.ca is defined %} ssl_client_certificate /usr/local/etc/nginx/key/{{ single_servername }}_ca.pem; ssl_verify_client {{ server.verify_client }}; -{% endif %} -{% if server.zero_rtt == '1' %} +{% endif %} +{% if server.zero_rtt == '1' %} ssl_early_data on; -{% endif %} +{% endif %} ssl_certificate_key /usr/local/etc/nginx/key/{{ single_servername }}.key; ssl_certificate /usr/local/etc/nginx/key/{{ single_servername }}.pem; ssl_protocols {{ server.tls_protocols.replace(',', ' ') }}; - ssl_dhparam /usr/local/etc/dh-parameters.4096; -{% if server.tls_ciphers is defined and server.tls_ciphers != '' %} + ssl_dhparam /usr/local/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919; +{% if server.tls_ciphers is defined and server.tls_ciphers != '' %} ssl_ciphers {{ server.tls_ciphers }}; -{% endif %} -{% if server.tls_ecdh_curve is defined and server.tls_ecdh_curve != '' %} +{% endif %} +{% if server.tls_ecdh_curve is defined and server.tls_ecdh_curve != '' %} ssl_ecdh_curve {{ server.tls_ecdh_curve }}; -{% endif %} +{% endif %} ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_session_tickets off; ssl_prefer_server_ciphers {% if server.tls_prefer_server_ciphers is defined and server.tls_prefer_server_ciphers == '0'%}off{% else %}on{% endif %}; -{% if server.ocsp_stapling is defined and server.ocsp_stapling == '1'%} +{% if server.ocsp_stapling is defined and server.ocsp_stapling == '1'%} ssl_stapling on; ssl_stapling_verify {% if server.ocsp_verify is defined and server.ocsp_verify == '1' %}On{% else %}Off{% endif %}; -{% else %} +{% else %} ssl_stapling off; +{% endif %} +{% if server.enable_http3|default("0") == "1" and http3_alt_svc_ports|length > 0 %} + add_header Alt-Svc '{% for listen_port in http3_alt_svc_ports %}h3=":{{ listen_port }}"; ma=86400{% if not loop.last %}, {% endif %}{% endfor %}' always; +{% endif %} +{% endif %} +{% endif %} +{% if server.resolver is defined and server.resolver != '' %} +{% set resolver = helpers.getUUID(server.resolver) %} +{% if resolver is defined %} + resolver {{ resolver.address.replace(',', ' ') }}{% if resolver.valid is defined and resolver.valid != '' %} valid={{ resolver.valid}}s{% endif %}{% if resolver.ipv4_off is defined and resolver.ipv4_off == '1' %} ipv4=off{% endif %}{% if resolver.ipv6_off is defined and resolver.ipv6_off == '1' %} ipv6=off{% endif %}; +{% if resolver.timeout is defined and resolver.timeout !='' %} + resolver_timeout {{ resolver.timeout }}s; +{% endif %} {% endif %} {% endif %} +{% if server.disable_gzip is defined and server.disable_gzip == '1' %} + gzip off; +{% endif %} sendfile {% if server.sendfile is defined and server.sendfile == '1' %}On{% else %}Off{% endif %}; server_name {{ server.servername.replace(',', ' ') }}; {% if server.real_ip_source is defined and server.real_ip_source != '' %} @@ -163,8 +217,10 @@ server { {% set syslog_targets = server.syslog_targets.split(',') %} {% include "OPNsense/Nginx/syslog_targets.conf" %} {% endif %} +{% if server.log_handshakes|default("1") == "1" %} access_log /var/log/nginx/tls_handshake.log handshake; - error_log /var/log/nginx/{{ server.servername }}.error.log; +{% endif %} + error_log /var/log/nginx/{{ server.servername }}.error.log{% if server.error_log_level is defined %} {{ server.error_log_level }}{% endif %}; {% if server.root is defined and server.root != '' %} root "{{server.root}}"; {% endif %} @@ -210,6 +266,7 @@ server { root /usr/local/etc/nginx/views; } {% endif %} + proxy_intercept_errors {% if server.proxy_intercept_errors|default("0") == "1" %}on{% else %}off{% endif %}; {% if server.security_header is defined and server.security_header != '' %} {% set security_rule = helpers.getUUID(server.security_header) %} {% if security_rule is defined %} @@ -224,10 +281,12 @@ server { set $naxsi_extensive_log {% if server.naxsi_extensive_log is defined and server.naxsi_extensive_log == '1' %}1{% else %}0{% endif %}; location @permanentban { access_log /var/log/nginx/permanentban.access.log main; +{% if OPNsense.Nginx.http.log_perm_ban is defined and OPNsense.Nginx.http.log_perm_ban == '1' %} + access_log /var/log/nginx/perm_ban.access.log main_ban; +{% endif %} internal; - add_header Content-Type text/plain; - add_header Charset utf-8; - return 403 "You got banned permanently from this server."; + add_header "Content-Type" "text/plain; charset=UTF-8" always; + return {% if OPNsense.Nginx.http.ban_response is defined and OPNsense.Nginx.http.ban_response != '403' %}{{OPNsense.Nginx.http.ban_response}}{% else %}403 "You got banned permanently from this server."{% endif %}; } error_page 418 = @permanentban; location = /waf_denied.html { @@ -245,25 +304,20 @@ server { } {% endif %} {% if server.disable_bot_protection is not defined or server.disable_bot_protection != '1' %} - # block based on User Agents - stuff I have found over the years in my server log - if ($http_user_agent ~* Python-urllib|Nmap|python-requests|libwww-perl|MJ12bot|Jorgee|fasthttp|libwww|Telesphoreo|A6-Indexer|ltx71|okhttp|ZmEu|sqlmap|LMAO/2.0|ltx71|zgrab|Ronin/2.0|Hakai/2.0) { - return 418; - } - {# MSIE 7 cannot be blocked - used for compatibility mode - https://blogs.msdn.microsoft.com/ieinternals/2013/09/21/internet-explorer-11s-many-user-agent-strings/ #} - if ($http_user_agent ~ "Indy\sLibrary|Morfeus Fucking Scanner|MSIE [0-6]\.\d+") - { - return 418; - } - if ($http_user_agent ~ ^Mozilla/[\d\.]+$) - { - return 418; +{% if OPNsense.Nginx.http.bots_ua is defined and OPNsense.Nginx.http.bots_ua|default("") != "" %} + # block based on User Agents defined in global http settings + if ($http_user_agent ~* {{ OPNsense.Nginx.http.bots_ua|replace(',','|') }}) { + return 418; } +{% endif %} {% endif %} {% if server.ip_acl is defined %} {% set ip_acl = server.ip_acl %} {% include "OPNsense/Nginx/ipacl.conf" %} {% endif %} - +{% if server.security_header is defined and server.security_header != '' %} +{% set security_rule = helpers.getUUID(server.security_header) %} +{% if security_rule is defined and security_rule.csp_log_violations is defined and security_rule.csp_log_violations == '1' %} location = /opnsense-report-csp-violation { include fastcgi_params; fastcgi_param QUERY_STRING $query_string; @@ -275,6 +329,8 @@ server { fastcgi_intercept_errors on; fastcgi_pass unix:/var/run/php-webgui.socket; } +{% endif %} +{% endif %} location /opnsense-auth-request { internal; fastcgi_pass unix:/var/run/php-webgui.socket; diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf index b18872e4d4..71529378b3 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf @@ -47,6 +47,9 @@ location {{ location.matchtype }} {{ location.urlpattern }} { error_page {{ errorpage.statuscodes.replace(',', ' ') }} {% if errorpage.response is defined and errorpage.response != '' %}={{ errorpage.response }} {% endif %}{% if errorpage.redirect is defined and errorpage.redirect != '' %}{{ errorpage.redirect }}{% else %}/error_{{ errorpage_uuid.replace('-', '') }}.html{% endif %}; {% endfor %} {% endif %} +{% if location.proxy_intercept_errors is defined and location.proxy_intercept_errors != 'Inherit' %} + proxy_intercept_errors {{ location.proxy_intercept_errors }}; +{% endif %} {% if location.force_https is defined and location.force_https == '1' %} if ($scheme != "https") { return 302 https://$host$request_uri; @@ -88,7 +91,6 @@ location {{ location.matchtype }} {{ location.urlpattern }} { auth_request /opnsense-auth-request; {% endif %} {% endif %} - http2_push_preload {% if location.http2_push_preload is defined and location.http2_push_preload == '1' %}on{% else %}off{% endif %}; {% if location.php_enable is defined and location.php_enable == '1' %} fastcgi_split_path_info ^(.+\.php)(/.+)$; include fastcgi_params; @@ -120,12 +122,16 @@ location {{ location.matchtype }} {{ location.urlpattern }} { {% endif%} {% if location.upstream is defined and (location.php_enable is not defined or location.php_enable != '1') %} {% set upstream = helpers.getUUID(location.upstream) %} - proxy_set_header Host $host; + proxy_set_header Host $host{% if upstream.host_port is defined and upstream.host_port != '' %}:{{ upstream.host_port }}{% endif %}; {% if location.websocket is defined and location.websocket == '1' %} proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; {% endif %} +{% if location.upstream_keepalive is defined and location.upstream_keepalive == '1' %} + proxy_http_version 1.1; + proxy_set_header Connection ""; +{% endif %} {% if location.proxy_buffer_size is defined and location.proxy_buffer_size != '' %} proxy_buffer_size {{ location.proxy_buffer_size }}k; {% endif %} @@ -139,6 +145,15 @@ location {{ location.matchtype }} {{ location.urlpattern }} { proxy_cache {{ location.cache_path.replace('-', '') }}; {% if location.cache_use_stale is defined and location.cache_use_stale != '' %} proxy_cache_use_stale {{ location.cache_use_stale.replace(',', ' ') }}; +{% endif %} +{% if location.cache_valid is defined and location.cache_valid != '' %} + proxy_cache_valid {{ location.cache_valid }}m; +{% endif %} +{% if location.proxy_cache_valid is defined and location.proxy_cache_valid != '' %} +{% for pcache_valid_uuid in location.proxy_cache_valid.split(',') %} +{% set pcache_valid = helpers.getUUID(pcache_valid_uuid) %} + proxy_cache_valid {{ pcache_valid.code.replace(',', ' ') }} {{ pcache_valid.valid }}m; +{% endfor %} {% endif %} proxy_cache_min_uses {{ location.cache_min_uses|default('1') }}; proxy_cache_background_update {% if location.cache_background_update is defined and location.cache_background_update == '1' %}on{% else %}off{% endif %}; @@ -164,7 +179,7 @@ location {{ location.matchtype }} {{ location.urlpattern }} { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; - proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Host {% if upstream.x_forwarded_host_verbatim is defined and upstream.x_forwarded_host_verbatim == '1'%}$http_host{% else %}$host{% endif %}; proxy_set_header X-TLS-Client-Intercepted $tls_intercepted; {% if location.proxy_read_timeout is defined and location.proxy_read_timeout != '' %} proxy_read_timeout {{ location.proxy_read_timeout }}s; @@ -193,17 +208,19 @@ location {{ location.matchtype }} {{ location.urlpattern }} { {% endif %} {% if upstream.tls_name_override is defined and upstream.tls_name_override != '' %} proxy_ssl_name {{ upstream.tls_name_override }}; +{% elif location.proxy_ssl_server_name is defined and location.proxy_ssl_server_name == '1' %} + proxy_ssl_name $upstream_sni_name; {% endif %} {% if upstream.tls_protocol_versions is defined and upstream.tls_protocol_versions != '' %} proxy_ssl_protocols {{ upstream.tls_protocol_versions.replace(',', ' ') }}; {% endif %} -{% if upstream.tls_name_override is defined %} - proxy_ssl_session_reuse {% if upstream.tls_name_override != '0' %}off{% else %}on{% endif %}; +{% if upstream.tls_session_reuse is defined %} + proxy_ssl_session_reuse {% if upstream.tls_session_reuse == '1' %}on{% else %}off{% endif %}; {% endif %} {% if upstream.tls_trusted_certificate is defined and upstream.tls_trusted_certificate != '' %} proxy_ssl_trusted_certificate /usr/local/etc/nginx/key/trust_upstream_{{ location.upstream }}.pem; {% else %} - proxy_ssl_trusted_certificate /etc/ssl/cert.pem; + proxy_ssl_trusted_certificate /usr/local/etc/ssl/cert.pem; {% endif %} proxy_ssl_verify {% if upstream.tls_verify == '1' %}on{% else %}off{% endif %}; {% if upstream.tls_verify_depth is defined and upstream.tls_verify_depth != '' %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx index 598e6444f4..c80d61e8dd 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx @@ -1,2 +1,2 @@ +nginx_setup="/usr/local/opnsense/scripts/nginx/setup.php" nginx_enable="YES" -nginx_var_script="/usr/local/opnsense/scripts/nginx/setup.php" diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx.conf index 0be1a6a941..3c8f912b90 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx.conf @@ -5,11 +5,16 @@ load_module /usr/local/libexec/nginx/ngx_mail_module.so; load_module /usr/local/libexec/nginx/ngx_http_brotli_filter_module.so; load_module /usr/local/libexec/nginx/ngx_http_brotli_static_module.so; load_module /usr/local/libexec/nginx/ngx_http_js_module.so; +load_module /usr/local/libexec/nginx/ngx_http_vhost_traffic_status_module.so; +{% if OPNsense.Nginx.http.headers_more_enable is defined and OPNsense.Nginx.http.headers_more_enable == '1' %} +load_module /usr/local/libexec/nginx/ngx_http_headers_more_filter_module.so; +{% endif %} user www staff; worker_processes {{ OPNsense.Nginx.http.workerprocesses }}; -error_log /var/log/nginx/error.log; +#error_log /var/log/nginx/error.log; +error_log syslog:server=unix:/var/run/log,facility=local6,nohostname warn; events { worker_connections {{ OPNsense.Nginx.http.workerconnections }}; diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/security_rule.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/security_rule.conf index 73ab27993e..b7bb71e042 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/security_rule.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/security_rule.conf @@ -1,3 +1,4 @@ + # security rules {% if security_rule.referrer is defined %} {% do our_headers.append('Referrer-Policy') %} add_header Referrer-Policy "{{ security_rule.referrer }}" always; @@ -13,21 +14,12 @@ {% if security_rule.strict_transport_security_time is defined %} {% do our_headers.append('Strict-Transport-Security') %} add_header Strict-Transport-Security "max-age={{ security_rule.strict_transport_security_time }}{% - if security_rule.strict_transport_security_include_subdomains is defined and - security_rule.strict_transport_security_include_subdomains == '1' %}; includeSubDomains{% endif %}" always; -{% endif %} -{% if security_rule.hpkp_keys is defined and security_rule.hpkp_time is defined %} -{% do our_headers.append('Public-Key-Pins') %} -{% do our_headers.append('Public-Key-Pins-Report-Only') %} - add_header Public-Key-Pins{% if security_rule.hpkp_report_only is defined and security_rule.hpkp_report_only == '1' - %}-Report-Only{% endif %} "{% for key in security_rule.hpkp_keys.split(',') - %}pin-sha256={{ key }}; {% endfor %}max-age={{ security_rule.hpkp_time }}{% - if security_rule.hpkp_include_subdomains is defined and - security_rule.hpkp_include_subdomains == '1' %}; includeSubDomains{% endif %}" always; + if security_rule.strict_transport_security_include_subdomains|default('0') == '1' %}; includeSubDomains{% endif %}{% + if security_rule.strict_transport_security_preload|default('0') == '1' %}; preload{% endif %}" always; {% endif %} {% if security_rule.enable_csp is defined and security_rule.enable_csp == '1' %} {% set hash_csp = {} %} -{% for csp_category in ['default-src', 'script-src', 'img-src', 'style-src', 'media-src', 'font-src', 'form-action'] %} +{% for csp_category in ['default-src', 'script-src', 'img-src', 'style-src', 'media-src', 'font-src', 'frame-src', 'frame-ancestors', 'form-action', 'connect-src', 'worker-src'] %} {% set prefix = 'csp_' + csp_category.replace('-', '_') + '_' %} {% if security_rule[prefix + 'enabled'] == '1' %} {% set current_list = [] %} @@ -69,5 +61,5 @@ {% do our_headers.append('Content-Security-Policy-Report-Only') %} add_header Content-Security-Policy{% if security_rule.csp_report_only is defined and security_rule.csp_report_only == '1' %}-Report-Only{% endif %} "{% for key, value in hash_csp.items() %}{{ key }} {{ value|join(' ') }}; {% endfor %}{# - #} report-uri /opnsense-report-csp-violation" always; + #}{% if security_rule.csp_log_violations is defined and security_rule.csp_log_violations == '1' %} report-uri /opnsense-report-csp-violation{% endif %}" always; {% endif %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/streams.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/streams.conf index f19d6a6cd5..9904736ce5 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/streams.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/streams.conf @@ -58,7 +58,7 @@ {% set syslog_targets = server.syslog_targets.split(',') %} {% include "OPNsense/Nginx/syslog_targets.conf" %} {% endif %} - error_log /var/log/nginx/stream_{{ server['@uuid'] }}.error.log info; + error_log /var/log/nginx/stream_{{ server['@uuid'] }}.error.log {% if server.error_log_level is defined and server.error_log_level != 'info'%}{{ server.error_log_level }}{% else %}info{% endif %}; {% if server.route_field == 'sni_upstream_map' %} ssl_preread on; @@ -75,7 +75,7 @@ ssl_certificate_key /usr/local/etc/nginx/key/{{ server['@uuid'] }}.key; ssl_certificate /usr/local/etc/nginx/key/{{ server['@uuid'] }}.pem; ssl_protocols TLSv1.2 TLSv1.3; - ssl_dhparam /usr/local/etc/dh-parameters.4096; + ssl_dhparam /usr/local/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919; ssl_ciphers 'ECDHE-ECDSA-CAMELLIA256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CAMELLIA256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CAMELLIA128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CAMELLIA128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-CAMELLIA256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-CAMELLIA256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-CAMELLIA128-SHA256:ECDHE-RSA-AES128-SHA256'; ssl_session_timeout 1d; ssl_session_cache shared:sslcache{{ server['@uuid'].replace('-','') }}:50m; @@ -83,6 +83,8 @@ ssl_prefer_server_ciphers on; {% endif %} + include {{ server['@uuid'] }}_pre/*.conf; + {% if server.route_field == 'upstream' %} {% if server.upstream is defined %} {% set upstream = helpers.getUUID(server.upstream) %} @@ -91,20 +93,35 @@ proxy_ssl_certificate_key /usr/local/etc/nginx/key/{{ upstream.tls_client_certificate }}.key; proxy_ssl_certificate /usr/local/etc/nginx/key/{{ upstream.tls_client_certificate }}.pem; {% endif %} +{% if upstream.tls_name_override is defined and upstream.tls_name_override != '' %} + proxy_ssl_server_name on; + proxy_ssl_name {{ upstream.tls_name_override }}; +{% endif %} {% endif %} proxy_ssl {% if upstream.tls_enable == '1' %}on{% else %}off{% endif %}; proxy_pass upstream{{ server.upstream.replace('-','') }}; + proxy_protocol {% if upstream.proxy_protocol == '1' %}on{% else %}off{% endif %}; {% endif %} {% elif server.route_field == 'sni_upstream_map' %} proxy_pass $hostmap{{ server.sni_upstream_map.replace('-','') }}; {% endif %} - proxy_protocol {% if server.proxy_protocol == '1' %}on{% else %}off{% endif %}; -{% if server.trusted_proxies is defined and server.trusted_proxies != '' %} +{% if server.proxy_responses is defined and server.proxy_responses != '' %} + proxy_responses {{ server.proxy_responses }}; +{% endif%} +{% if server.proxy_connect_timeout is defined and server.proxy_connect_timeout != '' %} + proxy_connect_timeout {{ server.proxy_connect_timeout }}s; +{% endif%} +{% if server.proxy_timeout is defined and server.proxy_timeout != '' %} + proxy_timeout {{ server.proxy_timeout }}s; +{% endif%} +{% if server.trusted_proxies is defined and server.trusted_proxies != '' and server.proxy_protocol is defined and server.proxy_protocol == '1' %} {% for trusted_proxy in server.trusted_proxies.split(',') %} set_real_ip_from {{ trusted_proxy }}; {% endfor %} {% endif%} + include {{ server['@uuid'] }}_post/*.conf; + } {% endfor %} {% endif %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/upstream.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/upstream.conf index b5226fa908..3f07f37175 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/upstream.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/upstream.conf @@ -12,6 +12,15 @@ upstream upstream{{ upstream_uuid.replace('-','') }} { ip_hash; {% endif %} {% endif %} +{% if upstream.keepalive is defined and upstream.keepalive|int > 0 %} +keepalive {{ upstream.keepalive }}; +{% if upstream.keepalive_requests is defined and upstream.keepalive_requests != '' %} +keepalive_requests {{ upstream.keepalive_requests }}; +{% endif %} +{% if upstream.keepalive_timeout is defined and upstream.keepalive_timeout != '' %} +keepalive_timeout {{ upstream.keepalive_timeout }}s; +{% endif %} +{% endif %} {% for upstream_serveruuid in upstream.serverentries.split(',') %} {% set upstream_server = helpers.getUUID(upstream_serveruuid) %} server {% if ':' in upstream_server.server %}[{% endif %}{{ upstream_server.server }}{% if ':' in upstream_server.server %}]{% endif diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/webgui.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/webgui.conf index 0990b5393c..f3be95044e 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/webgui.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/webgui.conf @@ -9,9 +9,10 @@ server { return 302 https://$host$request_uri; } listen 80 default_server; # if redirect is enabled - listen {% if system.webgui.port is defined and system.webgui.port != '' %}{{ system.webgui.port }}{% else %}443{% endif %} ssl http2 default_server; + listen {% if system.webgui.port is defined and system.webgui.port != '' %}{{ system.webgui.port }}{% else %}443{% endif %} ssl default_server; + http2 on; ## TLS configuration - ssl_dhparam /usr/local/etc/dh-parameters.4096; + ssl_dhparam /usr/local/opnsense/data/OPNsense/Nginx/dh-parameters.4096.rfc7919; ssl_ecdh_curve secp384r1; ssl_certificate /var/etc/cert.pem; ssl_certificate_key /var/etc/cert.pem; @@ -27,7 +28,6 @@ server { {% endif %} autoindex off; - http2_push_preload on; # gzip compression gzip_static on; @@ -147,7 +147,7 @@ server { {% endfor %} {% if helpers.exists('virtualip') %} {% for intf_item in helpers.toList('virtualip.vip') %} -{% if intf_item.type == 'single' %} +{% if intf_item.mode in ['carp', 'ipalias'] %} {% set cidr = intf_item.subnet + '/' + intf_item.subnet_bits %} {% if cidr not in whitelisted_networks %} allow {{ cidr }}; diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Syslog/local/nginx.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Syslog/local/nginx.conf new file mode 100644 index 0000000000..421c505283 --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Syslog/local/nginx.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [nginx]. +################################################################### +filter f_local_nginx { + program("nginx"); +}; diff --git a/www/nginx/src/opnsense/www/css/nginx/index.css b/www/nginx/src/opnsense/www/css/nginx/index.css new file mode 100644 index 0000000000..a680bfef07 --- /dev/null +++ b/www/nginx/src/opnsense/www/css/nginx/index.css @@ -0,0 +1,88 @@ +/* +* Copyright (C) 2023 A. Kulikov +* Copyright (C) 2017-2018 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. +*/ + +#frm_sni_hostname_mapdlg .col-md-4, +#frm_ipacl_dlg .col-md-4 { + width: 50%; +} +#frm_sni_hostname_mapdlg td > input[type="text"], +#frm_ipacl_dlg td > input[type="text"] { + width: 100%; + max-width: 100%; +} +#frm_sni_hostname_mapdlg .col-md-5, +#frm_ipacl_dlg .col-md-5 { + width: 25%; +} +#row_snihostname\.data .row, +#row_ipacl\.data .row { + padding-top: 5px; +} +#row_snihostname\.data .row div, +#row_ipacl\.data .row div { + padding: 0; +} +#sni_hostname_mapdlg .bootstrap-select, +#frm_ipacl_dlg .bootstrap-select { + width: 100% !important; +} +#nginx_conf_container { + overflow-x: auto; +} +.ngx_conf_table { + white-space: pre-wrap; + color: #333333; + background-color: #f5f5f5; + word-break: break-all; + word-wrap: break-word; +} +.ngx_conf_table_body { + display: grid; + height: 400px; + overflow-y: auto; + font-family: ui-monospace,monospace; + font-size: 13px; +} +.l-number { + position: relative; + width: 1%; + min-width: 50px; + padding-right: 20px; + padding-left: 1px; + text-align: right; + white-space: nowrap; + vertical-align: top; + user-select: none; + filter: brightness(2.0); + filter: contrast(0.3); +} +.placeholdertd { + padding: 10px; +} +#nginx_config_copy { + cursor: pointer; +} diff --git a/www/nginx/src/opnsense/www/css/nginx/logs.css b/www/nginx/src/opnsense/www/css/nginx/logs.css new file mode 100644 index 0000000000..9501c1b0d5 --- /dev/null +++ b/www/nginx/src/opnsense/www/css/nginx/logs.css @@ -0,0 +1,58 @@ +/* +* Copyright (C) 2020 Manuel Faux +* 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. +*/ + +thead.sticky-top th { + position: sticky; + top: 50px; +} + +tr.filter > th { + padding: 10px 10px 10px 10px !important; +} + +thead.sticky-top tr:first-child th { + top: 50px; +} + +tfoot.sticky-bottom th { + position: sticky; + bottom: 50px; + font-weight: normal; + font-family: inherit; + padding: 10px 10px 10px 10px !important; +} + +td.referer, td.request_line { + word-break: break-all; +} + +.ngx-dropdown-item { + cursor: pointer; + display: block; + margin: 0; + padding: 3px 20px; + white-space: nowrap; +} diff --git a/www/nginx/src/opnsense/www/css/nginx/vts.css b/www/nginx/src/opnsense/www/css/nginx/vts.css index 310d2fb667..60f08183af 100644 --- a/www/nginx/src/opnsense/www/css/nginx/vts.css +++ b/www/nginx/src/opnsense/www/css/nginx/vts.css @@ -24,7 +24,9 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ - +#update { + margin-bottom: 1em; +} #monitor h1 { margin: .5em 0 0 0; } @@ -38,15 +40,11 @@ font-size: .8em; margin: .5em 0; border-collapse: collapse; - border-bottom: 1px #DED solid; } #monitor thead th { font-size: 1em; padding: .1em .3em; - border: .2em solid #FFF; -} -#monitor tbody tr.odd { - background: #F5F5F5; + border: 1px solid; } #monitor tbody th { text-align: left; @@ -58,11 +56,6 @@ #monitor tbody td.key { font-size: 1em; padding: .1em .3em; - border: .2em solid #FFF; -} -#monitor div.update { - margin-top: 32px; - color: #696969; } /* from https://www.flag-sprites.com */ .flag { diff --git a/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js b/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js index c6a86734e3..d25e073871 100644 --- a/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js +++ b/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js @@ -1 +1 @@ -!function(e){var t={};function n(s){if(t[s])return t[s].exports;var i=t[s]={i:s,l:!1,exports:{}};return e[s].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,s){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(n.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(s,i,function(t){return e[t]}.bind(null,i));return s},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=27)}({27:function(e,t,n){"use strict";n.r(t);var s=Backbone.View.extend({tagName:"div",attributes:{class:"container-fluid"},child_views:[],createModel:null,upstreamCollection:null,initialize:function(e){this.dataField=$(e.dataField),this.entryclass=e.entryclass,this.createModel=e.createModel,this.upstreamCollection=e.upstreamCollection,this.listenTo(this.collection,"add remove reset",this.render),this.listenTo(this.collection,"change",this.update),this.dataField.after(this.$el)},events:{"click .add":"addEntry"},render:function(){this.child_views.forEach(e=>e.remove()),this.$el.html(""),this.child_views=[],this.update(),this.collection.each(e=>{const t=new this.entryclass({model:e,collection:this.collection,upstreamCollection:this.upstreamCollection});this.child_views.push(t),this.$el.append(t.$el),t.render()}),this.$el.append($('\n
    \n \n
    '))},update:function(){this.dataField.data("data",this.collection.toJSON())},addEntry:function(e){e.preventDefault(),this.collection.add(this.createModel())}});var i=Backbone.Collection.extend({url:"/api/nginx/settings/searchupstream",parse:function(e){return e.rows}});const a=Backbone.View.extend({tagName:"div",attributes:{class:"row"},events:{"keyup .key":function(){this.model.set("hostname",this.key.value)},"change .value":function(){this.model.set("upstream",this.value.value)},"click .delete":"deleteEntry"},key:null,value:null,delBtn:null,first:null,second:null,third:null,upstreamCollection:null,initialize:function(e){this.upstreamCollection=e.upstreamCollection,this.listenTo(this.upstreamCollection,"update reset add remove",this.regenerate_list),this.first=document.createElement("div"),this.first.classList.add("col-sm-5"),this.key=document.createElement("input"),this.first.append(this.key),this.key.type="text",this.key.classList.add("key"),this.key.value=this.model.get("hostname"),this.second=document.createElement("div"),this.second.classList.add("col-sm-5"),this.value=document.createElement("select"),this.second.append(this.value),this.value.classList.add("value"),this.value.classList.add("form-control"),this.value.value=this.model.get("upstream"),this.third=document.createElement("div"),this.third.classList.add("col-sm-2"),this.third.style.textAlign="right",this.delBtn=document.createElement("button"),this.delBtn.classList.add("delete"),this.delBtn.classList.add("btn"),this.delBtn.innerHTML='',this.third.append(this.delBtn),this.model.has("upstream")&&0!==this.upstreamCollection.where({uuid:this.model.get("upstream")}).length||this.upstreamCollection.length>0&&this.model.set("upstream",this.upstreamCollection.at(0).get("uuid")),this.$el.append(this.first).append(this.second).append(this.third)},render:function(){$(this.key).val(this.model.get("hostname")),this.regenerate_list(),$(this.value).val(this.model.get("upstream"))},deleteEntry:function(e){e.preventDefault(),this.collection.remove(this.model)},regenerate_list:function(){const e=$(this.value);e.html(""),this.upstreamCollection.each(t=>e.append(``)),e.val(this.model.get("upstream")),e.selectpicker("refresh")}}),l=Backbone.View.extend({tagName:"div",attributes:{class:"row"},events:{"keyup .key":function(){this.model.set("network",this.key.value)},"change .value":function(){this.model.set("action",this.value.value)},"click .delete":"deleteEntry"},key:null,value:null,delBtn:null,first:null,second:null,third:null,upstreamCollection:null,initialize:function(e){this.upstreamCollection=e.upstreamCollection,this.listenTo(this.upstreamCollection,"update reset add remove",this.regenerate_list),this.first=document.createElement("div"),this.first.classList.add("col-sm-5"),this.key=document.createElement("input"),this.first.append(this.key),this.key.type="text",this.key.classList.add("key"),this.key.value=this.model.get("network"),this.second=document.createElement("div"),this.second.classList.add("col-sm-5"),this.value=document.createElement("select"),this.second.append(this.value),this.value.classList.add("value"),this.value.classList.add("form-control"),this.value.value=this.model.get("action"),this.third=document.createElement("div"),this.third.classList.add("col-sm-2"),this.third.style.textAlign="right",this.delBtn=document.createElement("button"),this.delBtn.classList.add("delete"),this.delBtn.classList.add("btn"),this.delBtn.innerHTML='',this.third.append(this.delBtn),this.$el.append(this.first).append(this.second).append(this.third)},render:function(){$(this.key).val(this.model.get("network")),this.regenerate_list(),$(this.value).val(this.model.get("action"))},deleteEntry:function(e){e.preventDefault(),this.collection.remove(this.model)},regenerate_list:function(){const e=$(this.value);e.html(""),this.upstreamCollection.each(t=>e.append(``)),e.val(this.model.get("action")),e.selectpicker("refresh")}});var o=Backbone.Collection.extend({initialize:function(){let e=this;$("#snihostname\\.data").change(function(){e.regenerateFromView()})},regenerateFromView:function(){let e=$("#snihostname\\.data").data("data");_.isArray(e)||(e=[]),this.reset(e)}}),r=Backbone.Model.extend({}),c=Backbone.Model.extend({}),d=Backbone.Collection.extend({initialize:function(){let e=this;$("#ipacl\\.data").change(function(){e.regenerateFromView()})},regenerateFromView:function(){let e=$("#ipacl\\.data").data("data");_.isArray(e)||(e=[]),this.reset(e)}});const u=new i,h=new Backbone.Collection([{name:"Deny",value:"deny"},{name:"Allow",value:"allow"}]);$(document).ready(function(){mapDataToFormUI({frm_nginx:"/api/nginx/settings/get"}).done(function(){formatTokenizersUI(),$('select[data-allownew="false"]').selectpicker("refresh"),updateServiceControlUI("nginx")}),""!==window.location.hash&&$('a[href="'+window.location.hash+'"]').click(),$(".nav-tabs a").on("shown.bs.tab",function(e){history.pushState(null,null,e.target.hash)}),$(".reload_btn").click(function(){$(".reloadAct_progress").addClass("fa-spin"),ajaxCall("/api/nginx/service/reconfigure",{},function(){$(".reloadAct_progress").removeClass("fa-spin")})}),$('[id*="save_"]').each(function(){$(this).click(function(){let e=$(this).closest("form").attr("id"),t=$(this).closest("form").attr("data-title");saveFormToEndpoint("/api/nginx/settings/set",e,function(){$("#"+e+"_progress").addClass("fa fa-spinner fa-pulse"),ajaxCall("/api/nginx/service/reconfigure",{},function(n,s){$("#"+e+"_progress").removeClass("fa fa-spinner fa-pulse"),void 0===n||"success"===s&&"ok"===n.status?updateServiceControlUI("nginx"):BootstrapDialog.show({type:BootstrapDialog.TYPE_WARNING,title:t,message:JSON.stringify(n),draggable:!0})})})})}),["upstream","upstreamserver","location","credential","userlist","httpserver","streamserver","httprewrite","custompolicy","security_header","ipacl","limit_zone","cache_path","limit_request_connection","snifwd","errorpage","tls_fingerprint","syslog_target","naxsirule"].forEach(function(e){$("#grid-"+e).UIBootgrid({search:"/api/nginx/settings/search"+e,get:"/api/nginx/settings/get"+e+"/",set:"/api/nginx/settings/set"+e+"/",add:"/api/nginx/settings/add"+e+"/",del:"/api/nginx/settings/del"+e+"/",options:{selection:!1,multiSelect:!1,formatters:{commands:function(e,t){return' '},response:function(e,t){return"none"==t.response?"unchanged":t.response},statuscodes:function(e,t){const n=[],s=t.statuscodes.split(",");for(let e of s)n.push(e.substr(0,3));return n.join(", ")}}}})}),bind_naxsi_rule_dl_button(),function(){let e=new s({dataField:document.getElementById("snihostname.data"),upstreamCollection:u,entryclass:a,collection:new o,createModel:function(){return new r({hostname:"localhost"})}});window.snifield=e,e.render(),$("#grid-upstream").on("loaded.rs.jquery.bootgrid",function(){u.fetch()}),u.fetch()}();let e=new s({dataField:document.getElementById("ipacl.data"),upstreamCollection:h,entryclass:l,collection:new d,createModel:function(){return new c({network:"::",action:"deny"})}});window.ipaclfield=e,e.render()})}}); \ No newline at end of file +!function(t){var e={};function n(i){if(e[i])return e[i].exports;var s=e[i]={i:i,l:!1,exports:{}};return t[i].call(s.exports,s,s.exports,n),s.l=!0,s.exports}n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var s in t)n.d(i,s,function(e){return t[e]}.bind(null,s));return i},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=25)}({25:function(t,e,n){"use strict";n.r(e);var i=Backbone.View.extend({tagName:"div",attributes:{class:"container-fluid"},child_views:[],createModel:null,upstreamCollection:null,initialize:function(t){this.dataField=$(t.dataField),this.entryclass=t.entryclass,this.createModel=t.createModel,this.upstreamCollection=t.upstreamCollection,this.listenTo(this.collection,"add remove reset",this.render),this.listenTo(this.collection,"change",this.update),this.dataField.after(this.$el)},events:{"click .add":"addEntry"},render:function(){this.child_views.forEach(t=>t.remove()),this.$el.html(""),this.child_views=[],this.update(),this.collection.each(t=>{const e=new this.entryclass({model:t,collection:this.collection,upstreamCollection:this.upstreamCollection});this.child_views.push(e),this.$el.append(e.$el),e.render()}),this.$el.append($('\n
    \n \n
    '))},update:function(){this.dataField.data("data",this.collection.toJSON())},addEntry:function(t){t.preventDefault(),this.collection.add(this.createModel())}});var s=Backbone.Collection.extend({url:"/api/nginx/settings/searchupstream",parse:function(t){return t.rows}});const a=Backbone.View.extend({tagName:"div",attributes:{class:"row"},events:{"keyup .key":function(){this.model.set("hostname",this.key.value)},"change .value":function(){this.model.set("upstream",this.value.value)},"click .delete":"deleteEntry"},key:null,value:null,delBtn:null,first:null,second:null,third:null,upstreamCollection:null,initialize:function(t){this.upstreamCollection=t.upstreamCollection,this.listenTo(this.upstreamCollection,"update reset add remove",this.regenerate_list),this.first=document.createElement("div"),this.first.classList.add("col-sm-5"),this.key=document.createElement("input"),this.first.append(this.key),this.key.type="text",this.key.classList.add("key"),this.key.value=this.model.get("hostname"),this.second=document.createElement("div"),this.second.classList.add("col-sm-5"),this.value=document.createElement("select"),this.second.append(this.value),this.value.classList.add("value"),this.value.classList.add("form-control"),this.value.value=this.model.get("upstream"),this.third=document.createElement("div"),this.third.classList.add("col-sm-2"),this.third.style.textAlign="right",this.delBtn=document.createElement("button"),this.delBtn.classList.add("delete"),this.delBtn.classList.add("btn"),this.delBtn.innerHTML='',this.third.append(this.delBtn),this.model.has("upstream")&&0!==this.upstreamCollection.where({uuid:this.model.get("upstream")}).length||this.upstreamCollection.length>0&&this.model.set("upstream",this.upstreamCollection.at(0).get("uuid")),this.$el.append(this.first).append(this.second).append(this.third)},render:function(){$(this.key).val(this.model.get("hostname")),this.regenerate_list(),$(this.value).val(this.model.get("upstream"))},deleteEntry:function(t){t.preventDefault(),this.collection.remove(this.model)},regenerate_list:function(){const t=$(this.value);t.html(""),this.upstreamCollection.each(e=>t.append(``)),t.val(this.model.get("upstream")),t.selectpicker("refresh")}}),l=Backbone.View.extend({tagName:"div",attributes:{class:"row"},events:{"keyup .key":function(){this.model.set("network",this.key.value)},"change .value":function(){this.model.set("action",this.value.value)},"click .delete":"deleteEntry"},key:null,value:null,delBtn:null,first:null,second:null,third:null,upstreamCollection:null,initialize:function(t){this.upstreamCollection=t.upstreamCollection,this.listenTo(this.upstreamCollection,"update reset add remove",this.regenerate_list),this.first=document.createElement("div"),this.first.classList.add("col-sm-5"),this.key=document.createElement("input"),this.first.append(this.key),this.key.type="text",this.key.classList.add("key"),this.key.value=this.model.get("network"),this.second=document.createElement("div"),this.second.classList.add("col-sm-5"),this.value=document.createElement("select"),this.second.append(this.value),this.value.classList.add("value"),this.value.classList.add("form-control"),this.value.value=this.model.get("action"),this.third=document.createElement("div"),this.third.classList.add("col-sm-2"),this.third.style.textAlign="right",this.delBtn=document.createElement("button"),this.delBtn.classList.add("delete"),this.delBtn.classList.add("btn"),this.delBtn.innerHTML='',this.third.append(this.delBtn),this.$el.append(this.first).append(this.second).append(this.third)},render:function(){$(this.key).val(this.model.get("network")),this.regenerate_list(),$(this.value).val(this.model.get("action"))},deleteEntry:function(t){t.preventDefault(),this.collection.remove(this.model)},regenerate_list:function(){const t=$(this.value);t.html(""),this.upstreamCollection.each(e=>t.append(``)),t.val(this.model.get("action")),t.selectpicker("refresh")}});var o=Backbone.Collection.extend({initialize:function(){let t=this;$("#snihostname\\.data").change(function(){t.regenerateFromView()})},regenerateFromView:function(){let t=$("#snihostname\\.data").data("data");_.isArray(t)||(t=[]),this.reset(t)}}),r=Backbone.Model.extend({}),d=Backbone.Model.extend({}),c=Backbone.Collection.extend({initialize:function(){let t=this;$("#ipacl\\.data").change(function(){t.regenerateFromView()})},regenerateFromView:function(){let t=$("#ipacl\\.data").data("data");_.isArray(t)||(t=[]),this.reset(t)}});const u=new s,h=new Backbone.Collection([{name:"Deny",value:"deny"},{name:"Allow",value:"allow"}]);$(document).ready(function(){mapDataToFormUI({frm_nginx:"/api/nginx/settings/get"}).done(function(){formatTokenizersUI(),$('select[data-allownew="false"]').selectpicker("refresh"),updateServiceControlUI("nginx")}),""!==window.location.hash&&$('a[href="'+window.location.hash+'"]').click(),$(".nav-tabs a").on("shown.bs.tab",function(t){history.pushState(null,null,t.target.hash)}),$(".reload_btn").click(function(){$(".reloadAct_progress").addClass("fa-spin"),ajaxCall("/api/nginx/service/reconfigure",{},function(){$(".reloadAct_progress").removeClass("fa-spin")})}),$('[id*="save_"]').each(function(){$(this).click(function(){let t=$(this).closest("form").attr("id"),e=$(this).closest("form").attr("data-title");saveFormToEndpoint("/api/nginx/settings/set",t,function(){$("#"+t+"_progress").addClass("fa fa-spinner fa-pulse"),ajaxCall("/api/nginx/service/reconfigure",{},function(n,i){$("#"+t+"_progress").removeClass("fa fa-spinner fa-pulse"),void 0===n||"success"===i&&"ok"===n.status?updateServiceControlUI("nginx"):BootstrapDialog.show({type:BootstrapDialog.TYPE_WARNING,title:e,message:JSON.stringify(n),draggable:!0})})})})}),["upstream","upstreamserver","location","credential","userlist","httpserver","streamserver","httprewrite","custompolicy","security_header","ipacl","limit_zone","cache_path","proxy_cache_valid","limit_request_connection","snifwd","errorpage","tls_fingerprint","resolver","syslog_target","naxsirule"].forEach(function(t){$("#grid-"+t).UIBootgrid({search:"/api/nginx/settings/search"+t,get:"/api/nginx/settings/get"+t+"/",set:"/api/nginx/settings/set"+t+"/",add:"/api/nginx/settings/add"+t+"/",del:"/api/nginx/settings/del"+t+"/",commands:{copy_uuid:{method:function(t){navigator.clipboard.writeText($(this).data("row-id"))}}},options:{selection:!1,multiSelect:!1,formatters:{commands:function(t,e){return' '},response:function(t,e){return"none"==e.response?"unchanged":e.response},statuscodes:function(t,e){const n=[],i=e.statuscodes.split(",");for(let t of i)n.push(t.substr(0,3));return n.join(", ")}}}})}),bind_naxsi_rule_dl_button(),function(){let t=new i({dataField:document.getElementById("snihostname.data"),upstreamCollection:u,entryclass:a,collection:new o,createModel:function(){return new r({hostname:"localhost"})}});window.snifield=t,t.render(),$("#grid-upstream").on("loaded.rs.jquery.bootgrid",function(){u.fetch()}),u.fetch()}();let t=new i({dataField:document.getElementById("ipacl.data"),upstreamCollection:h,entryclass:l,collection:new c,createModel:function(){return new d({network:"::",action:"deny"})}});window.ipaclfield=t,t.render()})}}); diff --git a/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js b/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js index 8bdd4e6c74..9833831fb4 100644 --- a/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js +++ b/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js @@ -1 +1 @@ -!function(e){var t={};function n(l){if(t[l])return t[l].exports;var i=t[l]={i:l,l:!1,exports:{}};return e[l].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,l){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:l})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var l=Object.create(null);if(n.r(l),Object.defineProperty(l,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(l,i,function(t){return e[t]}.bind(null,i));return l},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=26)}([function(e,t,n){var l=n(2),i=n(4),o=/[&<>"']/g,r=RegExp(o.source);e.exports=function(e){return(e=i(e))&&r.test(e)?e.replace(o,l):e}},function(e,t,n){var l=n(6).Symbol;e.exports=l},function(e,t,n){var l=n(3)({"&":"&","<":"<",">":">",'"':""","'":"'"});e.exports=l},function(e,t){e.exports=function(e){return function(t){return null==e?void 0:e[t]}}},function(e,t,n){var l=n(5);e.exports=function(e){return null==e?"":l(e)}},function(e,t,n){var l=n(1),i=n(9),o=n(10),r=n(11),_=1/0,a=l?l.prototype:void 0,s=a?a.toString:void 0;e.exports=function e(t){if("string"==typeof t)return t;if(o(t))return i(t,e)+"";if(r(t))return s?s.call(t):"";var n=t+"";return"0"==n&&1/t==-_?"-0":n}},function(e,t,n){var l=n(7),i="object"==typeof self&&self&&self.Object===Object&&self,o=l||i||Function("return this")();e.exports=o},function(e,t,n){(function(t){var n="object"==typeof t&&t&&t.Object===Object&&t;e.exports=n}).call(this,n(8))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){e.exports=function(e,t){for(var n=-1,l=null==e?0:e.length,i=Array(l);++n\n \n\n'+(null==(__t=_.escape(name))?"":__t)+'\n\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)__p+='\n\n '+(null==(__t=_.escape(name))?"":__t)+"\n\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("username"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("size"))?"":__t)+'\n'+(null==(__t=model.escape("http_referer"))?"":__t)+'\n'+(null==(__t=model.escape("user_agent"))?"":__t)+'\n'+(null==(__t=model.escape("forwarded_for"))?"":__t)+'\n'+(null==(__t=model.escape("request_line"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("bytes_sent"))?"":__t)+'\n'+(null==(__t=model.escape("bytes_received"))?"":__t)+'\n'+(null==(__t=model.escape("session_time"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("date"))?"":__t)+'\n'+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("severity"))?"":__t)+'\n'+(null==(__t=model.escape("number"))?"":__t)+'\n'+(null==(__t=model.escape("message"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj){let count=0;__p+='\n \n \n ',"errors"===log_type||"stream_errors"===log_type?(count=5,__p+="\n \n \n \n \n \n "):"accesses"===log_type?(count=9,__p+="\n \n \n \n \n \n \n \n \n \n "):(count=6,__p+="\n \n \n \n \n \n \n "),__p+='\n \n \n ',"errors"===log_type||"stream_errors"===log_type?__p+='\n \n \n \n \n \n ':"accesses"===log_type?__p+='\n \n \n \n \n \n \n \n \n \n ':__p+='\n \n \n \n \n \n \n ',__p+='\n \n \n \n \n \n \n \n \n \n \n \n
    DateTimeSeverityNumberMessageTimeRemote IPUsernameStatusSizeRefererUser AgentForwarded ForRequest LineTimeRemote IPStatusBytes SentBytes ReceivedSession Time
    \n \n \n \n Entries per page:\n \n \n \n
    \n'}return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+='
    \n No data available...\n
    \n';return __p}},,,,function(e,t,n){"use strict";n.r(t);const l=new Backbone.Collection([{name:"HTTP Access Logs",logType:"accesses"},{name:"HTTP Error Logs",logType:"errors"},{name:"Stream Access Logs",logType:"stream_accesses"},{name:"Stream Error Logs",logType:"stream_errors"}]);var i=Backbone.Model.extend({});var o=Backbone.Collection.extend({model:i,url:function(){return"/api/nginx/logs/"+this.logType},initialize:function(e){this.logType=e.logType}}),r=n(16),_=n.n(r);var a=Backbone.View.extend({tagName:"li",events:{"click .mainentry":"mainMenuClick","click .menuEntry":"menuEntryClick"},initialize:function(e){this.listenTo(this.collection,"sync",this.render),this.listenTo(this.collection,"update",this.render),this.logview=e.logview},render:function(){this.$el.html(""),this.renderCollection()},renderCollection:function(){this.$el.addClass("dropdown"),this.$el.html(""),this.$el.append(_()({model:this.collection,name:this.model.attributes.name}))},mainMenuClick:function(){this.collection.models[0]&&this.handleElementClick(this.collection.models[0].id)},menuEntryClick:function(e){this.handleElementClick(e.target.dataset.modelUuid)},handleElementClick:function(e){this.logview.get_log(this.model.get("logType"),e)}}),s=n(17),c=n.n(s);var u=Backbone.View.extend({tagName:"li",initialize:function(e){this.logview=e.logview,this.log_name=e.log_name,this.visible_name=e.visible_name,this.log_type=e.log_type},events:{"click .mainentry":"handleElementClick"},log_name:null,log_type:null,visible_name:null,render:function(){this.$el.html(c()({name:this.visible_name}))},handleElementClick:function(){this.logview.get_log(this.log_type,this.log_name)}});var d=Backbone.View.extend({tagName:"ul",className:"nav nav-tabs",initialize:function(e){this.listenTo(this.collection,"sync",this.render),this.listenTo(this.collection,"update",this.render),this.logview=e.logview},render:function(){this.$el.attr("role","tablist"),this.$el.html(""),this.collection.forEach(e=>this.render_one(e)),this.render_single_tabs()},render_one:function(e){const t=new o({uuid:e.get("url"),logType:e.get("logType")}),n=new a({collection:t,model:e,logview:this.logview});this.$el.append(n.$el),t.fetch()},render_single_tabs:function(){const e=new u({logview:this.logview,log_name:"global",visible_name:"Global Error Log",log_type:"errors"});e.render(),this.$el.append(e.$el)}}),p=n(18),m=n.n(p),h=n(19),f=n.n(h),b=n(20),g=n.n(b),v=n(21),y=n.n(v);var x=Backbone.Model.extend({});var w=Backbone.Collection.extend({model:x,url:function(){return`/api/nginx/logs/${this.logType}/${this.uuid}`},initialize:function(){this.logType="none",this.uuid="none"},parse:function(e){return"error"in e?[]:e},filter_collection:function(e){const t=e.keys();return this.filter(function(n){if(!n)return!1;for(let l=0;lthis.render_one(e,t))}},render_one:function(e,t){const n=new T({type:this.type,model:t});n.render(),e.append(n.$el)},get_log:function(e,t){this.collection.uuid=t,this.collection.logType=e,this.type=e,this.$el.html(""),this.filter_model.clear(),this.update()},update:function(){this.collection.fetch()},clear_and_render:function(){this.current_filtered_collection=null,this.render()},update_filter:function(e){const t=e.target;this.filter_model.set(t.name,$(t).val())},page_back:function(){this.current_page>0&&(this.current_page--,this.render())},page_forward:function(){(this.current_page+1)*this.page_entry_count"']/g,a=RegExp(o.source);e.exports=function(e){return(e=l(e))&&a.test(e)?e.replace(o,n):e}},function(e,t,i){var n=i(6).Symbol;e.exports=n},function(e,t,i){var n=i(3)({"&":"&","<":"<",">":">",'"':""","'":"'"});e.exports=n},function(e,t){e.exports=function(e){return function(t){return null==e?void 0:e[t]}}},function(e,t,i){var n=i(5);e.exports=function(e){return null==e?"":n(e)}},function(e,t,i){var n=i(1),l=i(9),o=i(10),a=i(11),s=1/0,r=n?n.prototype:void 0,c=r?r.toString:void 0;e.exports=function e(t){if("string"==typeof t)return t;if(o(t))return l(t,e)+"";if(a(t))return c?c.call(t):"";var i=t+"";return"0"==i&&1/t==-s?"-0":i}},function(e,t,i){var n=i(7),l="object"==typeof self&&self&&self.Object===Object&&self,o=n||l||Function("return this")();e.exports=o},function(e,t,i){(function(t){var i="object"==typeof t&&t&&t.Object===Object&&t;e.exports=i}).call(this,i(8))},function(e,t){var i;i=function(){return this}();try{i=i||new Function("return this")()}catch(e){"object"==typeof window&&(i=window)}e.exports=i},function(e,t){e.exports=function(e,t){for(var i=-1,n=null==e?0:e.length,l=Array(n);++i\n \n\n'+(null==(__t=_.escape(name))?"":__t)+'\n\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)__p+='\n\n '+(null==(__t=_.escape(name))?"":__t)+"\n\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)log_fields_visible.forEach(e=>{__p+='\n'+(null==(__t=model.escape(e.id))?"":__t)+"\n"}),__p+="\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+='\n
    \n \n \n
    \n\n';return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)__p+='\n \n \n \n \n \n \n \n \n \n
    \n
    \n \n
    \n \n \n \n \n \n Page 1/0\n
    \n \n \n \n
    \n Found lines: 0/0\n
    \n';return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+='
    \n No data available...\n
    \n';return __p}},,,,function(e,t,i){"use strict";i.r(t);var n=Backbone.Model.extend({});var l=Backbone.Collection.extend({model:n,url:function(){return`/api/nginx/logs/${this.logType}`},initialize:function(e){this.logType=e.logType}}),o=Backbone.Model.extend({});var a=Backbone.Collection.extend({model:o,url:function(){return`/api/nginx/logs/${this.logType}/${this.uuid}`},initialize:function(e){this.logType=e.logType,this.uuid=e.uuid}}),s=i(16),r=i.n(s);var c=Backbone.View.extend({tagName:"li",events:{"click .mainentry":"mainMenuClick","click .menuEntry":"menuEntryClick","click .dropdown-toggle":"menuDropdownClick"},initialize:function(e){this.listenTo(this.collection,"update",this.render),this.logType=e.logType,this.logview=e.logview,this.render()},render:function(){this.$el.html(""),this.renderCollection()},renderCollection:function(){this.$el.addClass("dropdown"),"global"==this.model.get("id")&&(this.$el.addClass("active"),this.logview.get_log("errors","global",-1)),this.$el.html(""),this.$el.append(r()({model:this.collection,id:this.model.get("id"),name:this.model.has("server_name")?this.model.get("server_name"):"Port "+this.model.get("port")}))},mainMenuClick:function(){this.collection.models[0]&&(this.handleElementClick(this.model.get("id"),-1),$(`#tab_${this.model.get("id")} li`).removeClass("active"),$(`#subtab_item_${this.model.get("id")}_${this.collection.models[0].get("number")}`).parent().addClass("active"))},menuDropdownClick:function(e){this.collection.fetch()},menuEntryClick:function(e){this.handleElementClick(e.target.dataset.modelUuid,e.target.dataset.modelFileno)},handleElementClick:function(e,t){this.logview.get_log(this.logType,e,t)}}),d=i(17),u=i.n(d);Backbone.View.extend({tagName:"li",initialize:function(e){this.logview=e.logview,this.log_name=e.log_name,this.visible_name=e.visible_name,this.log_type=e.log_type},events:{"click .mainentry":"handleElementClick"},log_name:null,log_type:null,visible_name:null,render:function(){this.$el.html(u()({name:this.visible_name}))},handleElementClick:function(){this.logview.get_log(this.log_type,this.log_name)}});var p=Backbone.View.extend({tagName:"ul",className:"nav nav-tabs",initialize:function(e){this.listenTo(this.collection,"update",this.render),this.logview=e.logview,this.logType=e.logType},render:function(){this.$el.attr("role","tablist"),this.$el.html(""),"global"==this.logType?this.render_global_error_tab():this.collection.forEach(e=>this.render_one_server(e))},render_one_server:function(e){const t=new a({uuid:e.get("id"),logType:this.logType}),i=new c({collection:t,model:e,logType:this.logType,logview:this.logview});this.$el.append(i.$el)},render_global_error_tab:function(){const e=new a({uuid:"global",logType:"errors"}),t=new c({collection:e,model:new Backbone.Model({server_name:"Global Error Log",id:"global"}),logType:"errors",logview:this.logview});this.$el.append(t.$el),e.fetch()}}),h=i(18),g=i.n(h),b=i(19),f=i.n(b),m=i(20),v=i.n(m);var y=Backbone.Model.extend({});var w=Backbone.Collection.extend({model:y,url:function(){return`/api/nginx/logs/${this.logType}/${this.uuid}/${this.fileNo}/${this.page}/${this.pageSize}/${this.create_filter()}`},initialize:function(){this.logType="none",this.uuid="none",this.fileNo=-1,this.page=0,this.pageSize=0,this.filter_model=new Backbone.Model},parse:function(e){return"error"in e?[]:(this.page_count=e.pages,this.total_entries=e.total,this.displayed_entries=e.found,e.lines)},create_filter:function(){return encodeURIComponent(JSON.stringify(this.filter_model))}});Backbone.Model.extend({});var k=i(21),x=i.n(k);const j=Backbone.View.extend({tagName:"tr",initialize:function(e){this.type=e.type,this.log_fields_visible=e.log_fields_visible},render:function(){this.$el.html(g()({log_fields_visible:this.log_fields_visible,model:this.model}))}}),T=Backbone.View.extend({tagName:"tr",className:"filter",initialize:function(e){this.log_fields_visible=e.log_fields_visible},render:function(){this.log_fields_visible.forEach(e=>this.$el.append(f()({field:e,model:this.model})))}});const C=new(Backbone.View.extend({tagName:"div",className:"content-box tab-content",events:{"keyup .filter input":"update_filter","click #paging_first":"page_first","click #paging_back":"page_back","click #refresh":"update","click #paging_forward":"page_forward","click #paging_last":"page_last","change #entrycount":"change_entry_count","click .ngx-dropdown-item":"toggle_column"},page_entry_count:100,filter_delay:-1,initialize:function(){this.collection=new w,this.listenTo(this.collection,"sync",this.render),this.listenTo(this.collection,"update",this.render),this.listenTo(this.collection.filter_model,"change",this.render),this.type=""},render:function(){this.logFields=[];let e=this.collection.uuid,t=this.type;switch(t){case"accesses":this.logFields.push({id:"time",header:"Time"},{id:"remote_ip",header:"Remote IP"},{id:"username",header:"Username"},{id:"status",header:"Status"},{id:"size",header:"Size"},{id:"referer",header:"Referer"},{id:"user_agent",header:"User Agent"},{id:"forwarded_for",header:"Forwarded For"},{id:"request_line",header:"Request Line"});break;case"errors":case"stream_errors":this.logFields.push({id:"date",header:"Date"},{id:"time",header:"Time"},{id:"severity",header:"Severity"},{id:"number",header:"Number"},{id:"message",header:"Message"});break;default:this.logFields.push({id:"time",header:"Time"},{id:"remote_ip",header:"Remote IP"},{id:"status",header:"Status"},{id:"bytes_sent",header:"Bytes Sent"},{id:"bytes_received",header:"Bytes Rcvd"},{id:"session_time",header:"Session Time"})}this.logFields.forEach(i=>{i.visible="false"!==localStorage.getItem("visibleColumns["+t+"]["+e+"]["+i.id+"]")}),this.logFieldsVisible=_.filter(this.logFields,["visible",!0]);let i=this.$("thead");if(i.children().length<1){const e=new T({log_fields_visible:this.logFieldsVisible,model:this.collection.filter_model});e.render(),i.html(e.$el)}let n=this.$("tbody");n.length<1?0!==this.collection.length?(this.$el.html(v()({log_type:this.type,log_fields:this.logFields,log_fields_visible:this.logFieldsVisible,model:this.collection.filter_model})),n=this.$("tbody")):this.$el.html(x.a):n.html(""),0!==this.collection.length&&null==this.current_filtered_collection&&this.collection.forEach(e=>this.render_one(n,e)),this.$("#entrycountdisplay").html(this.page_entry_count),this.$("#currentpage").html(this.current_page+1),this.$("#pagecount").html(this.collection.page_count),this.$("#totalcount").html(this.collection.total_entries),this.$("#resultcount").html(this.collection.displayed_entries),this.current_page>=this.collection.page_count-1?(this.$("#paging_last").addClass("disabled"),this.$("#paging_forward").addClass("disabled")):(this.$("#paging_last").removeClass("disabled"),this.$("#paging_forward").removeClass("disabled")),this.current_page<=0?(this.$("#paging_back").addClass("disabled"),this.$("#paging_first").addClass("disabled")):(this.$("#paging_back").removeClass("disabled"),this.$("#paging_first").removeClass("disabled"))},render_one:function(e,t){const i=new j({type:this.type,log_fields_visible:this.logFieldsVisible,model:t});i.render(),e.append(i.$el)},get_log:function(e,t,i){this.collection.uuid=t,this.collection.logType=e,this.collection.fileNo=i,this.type=e,this.current_page=0,this.$el.html(""),this.collection.filter_model.clear(),this.update()},update:function(){this.collection.page=this.current_page,this.collection.pageSize=this.page_entry_count,this.collection.fetch()},update_filter:function(e){clearTimeout(this.filter_delay);const t=e.target;this.collection.filter_model.set(t.name,$(t).val()),this.current_page=0,this.filter_delay=setTimeout(function(e){e.update()},500,this)},page_first:function(){this.current_page=0,this.update()},page_back:function(){this.current_page>0&&(this.current_page--,this.update())},page_forward:function(){this.current_page this.render_one(element)); - this.render_single_tabs(); + + if (this.logType == 'global') { + this.render_global_error_tab(); + } + else { + this.collection.forEach((element) => this.render_one_server(element)); + } }, - render_one: function(element) { - const servers = new LogCollection( + render_one_server: function(element) { + const files = new LogCollection( { - uuid: element.get('url'), - logType: element.get('logType') + uuid: element.get('id'), + logType: this.logType } ); const logList = new TabLogList({ - collection: servers, + collection: files, model: element, + logType: this.logType, logview: this.logview }); this.$el.append(logList.$el); - servers.fetch(); }, - render_single_tabs: function () { - const single_tab = new SingleTab({ - logview: this.logview, - log_name: 'global', - visible_name: 'Global Error Log', - log_type: 'errors'}); - single_tab.render(); - this.$el.append(single_tab.$el); + render_global_error_tab: function () { + const files = new LogCollection( + { + uuid: 'global', + logType: 'errors' + } + ); + const logList = new TabLogList({ + collection: files, + model: new Backbone.Model({ + server_name: 'Global Error Log', + id: 'global' + }), + logType: 'errors', + logview: this.logview + }); + this.$el.append(logList.$el); + files.fetch(); } }); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js b/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js index fe82540f8e..d00a0ccc62 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js @@ -1,30 +1,32 @@ -import accessLogLine from '../templates/AccessLogLine.html'; -import streamAccessLogLine from '../templates/StreamAccessLogLine.html'; -import errorLogLine from '../templates/ErrorLogLine.html'; +import LogLine from '../templates/LogLine.html'; +import LogColumn from '../templates/LogColumn.html'; import logViewer from '../templates/logviewer.html'; import LogLinesCollection from "../models/LogLinesCollection"; +import LogColumnModel from "../models/LogColumn"; import noDataAvailable from '../templates/noDataAvailable.html'; - const LogViewLine = Backbone.View.extend({ tagName: 'tr', initialize: function (data) { this.type = data.type; + this.log_fields_visible = data.log_fields_visible; }, render: function () { - this.$el.html(this.get_template()({model: this.model})); + this.$el.html(LogLine({log_fields_visible: this.log_fields_visible, model: this.model})); }, +}); - get_template: function() { - if (this.type === 'accesses') { - return accessLogLine; - } else if (this.type === 'stream_accesses') { - return streamAccessLogLine; - } else { - return errorLogLine; - } +const LogViewColumns = Backbone.View.extend({ + tagName: 'tr', + className: 'filter', + initialize: function (data) { + this.log_fields_visible = data.log_fields_visible; }, + + render: function() { + this.log_fields_visible.forEach((field) => this.$el.append(LogColumn({field: field, model: this.model}))); + } }); const LogView = Backbone.View.extend({ @@ -33,29 +35,81 @@ const LogView = Backbone.View.extend({ className: 'content-box tab-content', events: { "keyup .filter input": "update_filter", + "click #paging_first": "page_first", "click #paging_back": "page_back", + "click #refresh": "update", "click #paging_forward": "page_forward", + "click #paging_last": "page_last", "change #entrycount": "change_entry_count", + "click .ngx-dropdown-item": "toggle_column", }, page_entry_count: 100, - current_page: 0, - current_filtered_collection: null, + filter_delay: -1, initialize: function() { this.collection = new LogLinesCollection(); - this.filter_model = new Backbone.Model(); - this.listenTo(this.collection, "sync", this.clear_and_render); - this.listenTo(this.collection, "update", this.clear_and_render); - this.listenTo(this.filter_model, "change", this.clear_and_render); + this.listenTo(this.collection, "sync", this.render); + this.listenTo(this.collection, "update", this.render); + this.listenTo(this.collection.filter_model, "change", this.render); this.type = ''; }, render: function() { - let tbody = this.$el.find('tbody'); + // set logline fields + // all the fields are visible by default + // users choice is stored in browser localStorage + this.logFields = []; + let uid = this.collection.uuid; + let type = this.type; + switch (type) { + case 'accesses': + this.logFields.push({id: "time", header: "Time"}, + {id: "remote_ip", header: "Remote IP"}, + {id: "username", header: "Username"}, + {id: "status", header: "Status"}, + {id: "size", header: "Size"}, + {id: "referer", header: "Referer"}, + {id: "user_agent", header: "User Agent"}, + {id: "forwarded_for", header: "Forwarded For"}, + {id: "request_line", header: "Request Line"}); + break; + case 'errors': + case 'stream_errors': + this.logFields.push({id: "date", header: "Date"}, + {id: "time", header: "Time"}, + {id: "severity", header: "Severity"}, + {id: "number", header: "Number"}, + {id: "message", header: "Message"}); + break; + default: + // stream access + this.logFields.push({id: "time", header: "Time"}, + {id: "remote_ip", header: "Remote IP"}, + {id: "status", header: "Status"}, + {id: "bytes_sent", header: "Bytes Sent"}, + {id: "bytes_received", header: "Bytes Rcvd"}, + {id: "session_time", header: "Session Time"}); + } + this.logFields.forEach( (field) => { + field.visible = localStorage.getItem('visibleColumns[' + type + '][' + uid + '][' + field.id + ']') !== 'false'; + }); + + this.logFieldsVisible = _.filter(this.logFields, ['visible', true]); + // fields are ready + + // create/update column headers + let thead = this.$('thead'); + if (thead.children().length < 1) { + const logColumns = new LogViewColumns({log_fields_visible: this.logFieldsVisible, model: this.collection.filter_model}); + logColumns.render(); + thead.html(logColumns.$el); + } + + let tbody = this.$('tbody'); if (tbody.length < 1) { if (this.collection.length !== 0) { - this.$el.html(logViewer({log_type: this.type, model: this.filter_model})); - tbody = this.$el.find('tbody'); + this.$el.html(logViewer({log_type: this.type, log_fields: this.logFields, log_fields_visible: this.logFieldsVisible, model: this.collection.filter_model})); + tbody = this.$('tbody'); } else { this.$el.html(noDataAvailable); } @@ -63,57 +117,118 @@ const LogView = Backbone.View.extend({ else { tbody.html(''); } + if (this.collection.length !== 0) { if (this.current_filtered_collection == null) { - this.current_filtered_collection = this.collection.filter_collection(this.filter_model); + this.collection.forEach( + (model) => this.render_one(tbody, model) + ); } - const index_begin = this.current_page * this.page_entry_count; - const index_end = index_begin + this.page_entry_count; - this.current_filtered_collection.slice(index_begin, index_end).forEach( - (model) => this.render_one(tbody, model) - ); + } + + this.$('#entrycountdisplay').html(this.page_entry_count); + this.$('#currentpage').html(this.current_page + 1); + this.$('#pagecount').html(this.collection.page_count); + this.$('#totalcount').html(this.collection.total_entries); + this.$('#resultcount').html(this.collection.displayed_entries); + + if (this.current_page >= this.collection.page_count - 1) { + this.$('#paging_last').addClass("disabled"); + this.$('#paging_forward').addClass("disabled"); + } + else { + this.$('#paging_last').removeClass("disabled"); + this.$('#paging_forward').removeClass("disabled"); + } + + if (this.current_page <= 0) { + this.$('#paging_back').addClass("disabled"); + this.$('#paging_first').addClass("disabled"); + } + else { + this.$('#paging_back').removeClass("disabled"); + this.$('#paging_first').removeClass("disabled"); } }, + render_one: function(parent_element, model) { - const logline = new LogViewLine({type: this.type, model: model}); + const logline = new LogViewLine({type: this.type, log_fields_visible: this.logFieldsVisible, model: model}); logline.render(); parent_element.append(logline.$el); }, - get_log: function(type, uuid) { + + get_log: function(type, uuid, fileNo) { this.collection.uuid = uuid; this.collection.logType = type; + this.collection.fileNo = fileNo; this.type = type; + this.current_page = 0; this.$el.html(''); - this.filter_model.clear(); + this.collection.filter_model.clear(); this.update(); }, + update: function () { + this.collection.page = this.current_page; + this.collection.pageSize = this.page_entry_count; this.collection.fetch(); }, - clear_and_render: function() { - this.current_filtered_collection = null; - this.render(); - }, + update_filter: function (event) { + clearTimeout(this.filter_delay); const element = event.target; - this.filter_model.set(element.name, $(element).val()); + this.collection.filter_model.set(element.name, $(element).val()); + this.current_page = 0; + + // Delay update to avoid multiple requests during typing + this.filter_delay = setTimeout(function(instance) { + instance.update(); + }, 500, this); + }, + + page_first: function () { + this.current_page = 0; + this.update(); }, + page_back: function () { if (this.current_page > 0) { this.current_page--; - this.render(); + this.update(); } }, + page_forward: function () { - if ((this.current_page + 1) * this.page_entry_count < this.collection.length) { + if (this.current_page < this.collection.page_count) { this.current_page++; - this.render(); + this.update(); } }, + + page_last: function () { + this.current_page = this.collection.page_count - 1; + this.update(); + }, + change_entry_count: function (event) { this.page_entry_count = event.target.value; this.current_page = 0; - this.render(); + this.update(); + }, + + toggle_column: function (event) { + event.stopPropagation(); + let uid = this.collection.uuid; + let type = this.type; + let field = $(event.currentTarget).find('input').prop('value'); + // toggle visibility + localStorage.setItem('visibleColumns[' + type + '][' + uid + '][' + field + ']', !_.find(this.logFields, { 'id': field }).visible); + // unset filter for this column (if any) so as not to confuse the user + this.collection.filter_model.unset(field, {silent: true}); + // reset table header and update data + this.$('thead').html(''); + this.update(); } }); + export default LogView; diff --git a/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js b/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js index 60e1d3ba28..5e6777317f 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js @@ -5,13 +5,15 @@ let TabLogList = Backbone.View.extend({ tagName: 'li', events: { "click .mainentry": "mainMenuClick", - "click .menuEntry": "menuEntryClick" + "click .menuEntry": "menuEntryClick", + "click .dropdown-toggle": "menuDropdownClick" }, initialize: function(data) { - this.listenTo(this.collection, "sync", this.render); this.listenTo(this.collection, "update", this.render); + this.logType = data.logType; this.logview = data.logview; + this.render(); }, render: function() { @@ -21,21 +23,34 @@ let TabLogList = Backbone.View.extend({ renderCollection: function() { this.$el.addClass('dropdown'); + if (this.model.get('id') == "global") { + this.$el.addClass('active'); + this.logview.get_log('errors', 'global', -1); + } this.$el.html(''); this.$el.append( - TabTemplateCollection({model: this.collection, name: this.model.attributes.name}) + TabTemplateCollection({ + model: this.collection, + id: this.model.get('id'), + name: this.model.has('server_name') ? this.model.get('server_name') : "Port " + this.model.get('port') + }) ); }, mainMenuClick: function () { if (this.collection.models[0]) { - this.handleElementClick(this.collection.models[0].id); + this.handleElementClick(this.model.get('id'), -1); + $(`#tab_${this.model.get('id')} li`).removeClass('active'); + $(`#subtab_item_${this.model.get('id')}_${this.collection.models[0].get('number')}`).parent().addClass('active'); } }, + menuDropdownClick: function (event) { + this.collection.fetch(); + }, menuEntryClick: function (event) { - this.handleElementClick(event.target.dataset['modelUuid']); + this.handleElementClick(event.target.dataset['modelUuid'], event.target.dataset['modelFileno']); }, - handleElementClick: function (uuid) { - this.logview.get_log(this.model.get('logType'), uuid); + handleElementClick: function (uuid, fileNo) { + this.logview.get_log(this.logType, uuid, fileNo); } }); export default TabLogList; diff --git a/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js b/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js index dad07be5f7..0051e00e42 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js @@ -1,15 +1,33 @@ -import {defaultEndpoints} from './config'; +import LogServerCollection from './models/LogServerCollection'; import LogCategoryList from './controller/LogCategoryList'; import LogView from './controller/LogView'; +// Skeleton with header (navigation) and footer (pagination) const logview = new LogView(); - +// Get type of log to display from volt view (data-log HTML attribute) +const type = $('#logapplication').data('log'); +// Query (HTTP or stream) server list +const servers = new LogServerCollection({ + logType: type +}); +// Render tabs with server logs (one tab per server) const menu = new LogCategoryList({ - collection: defaultEndpoints, - logview: logview + collection: servers, + logview: logview, + logType: type // 'errors', 'accesses' or 'global' }); -$(document.getElementById('logapplication')) +// Place log application to volt template +$('#logapplication') .append(menu.$el) .append(logview.$el); -menu.render(); + +if (type != 'global') { + // Global error log does not require server list + servers.fetch(); +} +else { + // Update of server list triggers render() which does not + // occur for global error log + menu.render(); +} diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js index 19febb1f38..0221235e1c 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js @@ -3,10 +3,11 @@ import LogFileMenuEntry from './LogFileMenuEntry'; const LogCollection = Backbone.Collection.extend({ model: LogFileMenuEntry, url: function () { - return '/api/nginx/logs/' + this.logType; + return `/api/nginx/logs/${this.logType}/${this.uuid}`; }, initialize: function (params) { this.logType = params.logType; + this.uuid = params.uuid; } }); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogColumn.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogColumn.js new file mode 100644 index 0000000000..40e01a1556 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogColumn.js @@ -0,0 +1,5 @@ + +const LogColumnModel = Backbone.Model.extend({ +}); + +export default LogColumnModel; diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js index 2e3eb2921d..806f0c0775 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js @@ -3,39 +3,29 @@ import LogLine from "./LogLine"; const LogLinesCollection = Backbone.Collection.extend({ model: LogLine, url: function () { - return `/api/nginx/logs/${this.logType}/${this.uuid}`; + return `/api/nginx/logs/${this.logType}/${this.uuid}/${this.fileNo}/${this.page}/${this.pageSize}/${this.create_filter()}`; }, initialize: function () { this.logType = 'none'; this.uuid = 'none'; + this.fileNo = -1; + this.page = 0; + this.pageSize = 0; + this.filter_model = new Backbone.Model(); }, parse: function(response) { if ('error' in response) { return []; } - return response; + else { + this.page_count = response.pages; + this.total_entries = response.total; + this.displayed_entries = response.found; + return response.lines; + } }, - filter_collection: function(filter_model) { - const filter_model_keys = filter_model.keys(); - return this.filter(function (model) { - if (!model) { - return false; - } - for (let i = 0; i < filter_model_keys.length; i++) { - const property = filter_model_keys[i]; - if (typeof(filter_model.get(property)) !== "string" - || filter_model.get(property).length === 0) { - continue; - } - if (!model.has(property)) { - return false; - } - if (!model.get(property).includes(filter_model.get(property))) { - return false; - } - } - return true; - }); + create_filter: function() { + return encodeURIComponent(JSON.stringify(this.filter_model)); } }); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerCollection.js new file mode 100644 index 0000000000..9fdfdc75b3 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerCollection.js @@ -0,0 +1,13 @@ +import LogServerMenu from './LogServerMenu'; + +const LogServerCollection = Backbone.Collection.extend({ + model: LogServerMenu, + url: function () { + return `/api/nginx/logs/${this.logType}`; + }, + initialize: function (params) { + this.logType = params.logType; + } +}); + +export default LogServerCollection; diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerMenu.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerMenu.js new file mode 100644 index 0000000000..170a06af68 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerMenu.js @@ -0,0 +1,2 @@ +export default Backbone.Model.extend({ +}); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js b/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js index 0e69aaf179..4272efdaa1 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js @@ -66,10 +66,12 @@ function init_grids() { 'ipacl', 'limit_zone', 'cache_path', + 'proxy_cache_valid', 'limit_request_connection', 'snifwd', 'errorpage', 'tls_fingerprint', + 'resolver', 'syslog_target', 'naxsirule'].forEach(function (element) { $("#grid-" + element).UIBootgrid( @@ -79,14 +81,20 @@ function init_grids() { 'set': '/api/nginx/settings/set' + element + '/', 'add': '/api/nginx/settings/add' + element + '/', 'del': '/api/nginx/settings/del' + element + '/', + 'commands': { + copy_uuid: { + method: function(e) { navigator.clipboard.writeText($(this).data("row-id")); } + } + }, 'options': { selection: false, multiSelect: false, formatters: { "commands": function (column, row) { - return " " + - "" + - ""; + return " " + + " " + + " " + + ""; }, "response": function (column, row) { return ((row.response == "none") ? "unchanged" : row.response); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/templates/AccessLogLine.html b/www/nginx/src/opnsense/www/js/nginx/src/templates/AccessLogLine.html deleted file mode 100644 index adde3d4d7c..0000000000 --- a/www/nginx/src/opnsense/www/js/nginx/src/templates/AccessLogLine.html +++ /dev/null @@ -1,9 +0,0 @@ -<%= model.escape('time') %> -<%= model.escape('remote_ip') %> -<%= model.escape('username') %> -<%= model.escape('status') %> -<%= model.escape('size') %> -<%= model.escape('http_referer') %> -<%= model.escape('user_agent') %> -<%= model.escape('forwarded_for') %> -<%= model.escape('request_line') %> diff --git a/www/nginx/src/opnsense/www/js/nginx/src/templates/ErrorLogLine.html b/www/nginx/src/opnsense/www/js/nginx/src/templates/ErrorLogLine.html deleted file mode 100644 index ec99b42328..0000000000 --- a/www/nginx/src/opnsense/www/js/nginx/src/templates/ErrorLogLine.html +++ /dev/null @@ -1,5 +0,0 @@ -<%= model.escape('date') %> -<%= model.escape('time') %> -<%= model.escape('severity') %> -<%= model.escape('number') %> -<%= model.escape('message') %> diff --git a/www/nginx/src/opnsense/www/js/nginx/src/templates/LogColumn.html b/www/nginx/src/opnsense/www/js/nginx/src/templates/LogColumn.html new file mode 100644 index 0000000000..92f303ef29 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/templates/LogColumn.html @@ -0,0 +1,6 @@ + +
    + + +
    + diff --git a/www/nginx/src/opnsense/www/js/nginx/src/templates/LogLine.html b/www/nginx/src/opnsense/www/js/nginx/src/templates/LogLine.html new file mode 100644 index 0000000000..b7b7881f41 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/templates/LogLine.html @@ -0,0 +1,3 @@ +<% log_fields_visible.forEach( (field) => { %> +<%= model.escape(field.id) %> +<% }) %> diff --git a/www/nginx/src/opnsense/www/js/nginx/src/templates/StreamAccessLogLine.html b/www/nginx/src/opnsense/www/js/nginx/src/templates/StreamAccessLogLine.html deleted file mode 100644 index 54cfd87895..0000000000 --- a/www/nginx/src/opnsense/www/js/nginx/src/templates/StreamAccessLogLine.html +++ /dev/null @@ -1,6 +0,0 @@ -<%= model.escape('time') %> -<%= model.escape('remote_ip') %> -<%= model.escape('status') %> -<%= model.escape('bytes_sent') %> -<%= model.escape('bytes_received') %> -<%= model.escape('session_time') %> diff --git a/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html b/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html index ee317a10c9..d5ea04354a 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html +++ b/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html @@ -37,15 +37,16 @@ <%= _.escape(name) %> -