From f97da38688bbb7d7d8a964e81cc0f2e4fc1182e9 Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Sun, 2 Aug 2026 23:42:40 +0800 Subject: [PATCH] Add native Xray TUN support Signed-off-by: Loren Eteval --- Furious/Core/CoreManager.py | 6 +- Furious/Externals/GenTranslation.py | 128 +++++++++ Furious/Plugins/API.py | 4 + Furious/Plugins/Official/Hysteria1/Plugin.py | 5 + Furious/Plugins/Official/Hysteria2/Plugin.py | 5 + .../Plugins/Official/Xray/GuiTUNSettings.py | 242 ++++++++++++++++++ Furious/Plugins/Official/Xray/Plugin.py | 74 +++++- Furious/Plugins/Official/Xray/TUN.py | 130 ++++++++++ Furious/Plugins/Registry.py | 17 ++ 9 files changed, 607 insertions(+), 4 deletions(-) create mode 100644 Furious/Plugins/Official/Xray/GuiTUNSettings.py create mode 100644 Furious/Plugins/Official/Xray/TUN.py diff --git a/Furious/Core/CoreManager.py b/Furious/Core/CoreManager.py index 96fe572..d02fde5 100644 --- a/Furious/Core/CoreManager.py +++ b/Furious/Core/CoreManager.py @@ -149,6 +149,10 @@ class CoreManager(Mixins.CleanupOnExit): return False + pluginTUN = False + if not proxyModeOnly and SystemRuntime.isTUNMode(): + pluginTUN = getPluginRegistry().prepareTUN(configcopy) + process, success = self._startCore( configcopy, routing, @@ -171,7 +175,7 @@ class CoreManager(Mixins.CleanupOnExit): return False # TUN Mode handling - if not proxyModeOnly and SystemRuntime.isTUNMode(): + if not proxyModeOnly and SystemRuntime.isTUNMode() and not pluginTUN: if PLATFORM == 'Windows': # cleanup first SystemRoutingTable.delete('0.0.0.0', APPLICATION_TUN_GATEWAY_ADDRESS) diff --git a/Furious/Externals/GenTranslation.py b/Furious/Externals/GenTranslation.py index d1a8a77..36506e6 100644 --- a/Furious/Externals/GenTranslation.py +++ b/Furious/Externals/GenTranslation.py @@ -2154,5 +2154,133 @@ TRANSLATION = { "RU": "Редактировать маршрут...", "ZH": "编辑路由...", "isReviewed": "True" + }, + "Add VMess Server...": { + "source": [ + "Furious.Plugins.Official.Xray.Plugin" + ], + "RU": "Добавить сервер VMess...", + "ZH": "添加VMess服务器...", + "isReviewed": "True" + }, + "Add VLESS Server...": { + "source": [ + "Furious.Plugins.Official.Xray.Plugin" + ], + "RU": "Добавить сервер VLESS...", + "ZH": "添加VLESS服务器...", + "isReviewed": "True" + }, + "Add Shadowsocks Server...": { + "source": [ + "Furious.Plugins.Official.Xray.Plugin" + ], + "RU": "Добавить сервер Shadowsocks...", + "ZH": "添加Shadowsocks服务器...", + "isReviewed": "True" + }, + "Add Trojan Server...": { + "source": [ + "Furious.Plugins.Official.Xray.Plugin" + ], + "RU": "Добавить сервер Trojan...", + "ZH": "添加Trojan服务器...", + "isReviewed": "True" + }, + "Add Hysteria1 Server...": { + "source": [ + "Furious.Plugins.Official.Hysteria1.Plugin" + ], + "RU": "Добавить сервер Hysteria 1...", + "ZH": "添加Hysteria1服务器...", + "isReviewed": "True" + }, + "Add Hysteria2 Server...": { + "source": [ + "Furious.Plugins.Official.Hysteria2.Plugin" + ], + "RU": "Добавить сервер Hysteria 2...", + "ZH": "添加Hysteria2服务器...", + "isReviewed": "True" + }, + "Add SOCKS Server...": { + "source": [ + "Furious.Plugins.Official.Xray.Plugin" + ], + "RU": "Добавить сервер SOCKS...", + "ZH": "添加SOCKS服务器...", + "isReviewed": "True" + }, + "Interface": { + "source": [ + "Furious.Plugins.Official.Xray.GuiTUNSettings" + ], + "RU": "Интерфейс", + "ZH": "接口", + "isReviewed": "True" + }, + "Network": { + "source": [ + "Furious.Plugins.Official.Xray.GuiTUNSettings" + ], + "RU": "Сеть", + "ZH": "网络", + "isReviewed": "True" + }, + "Use Xray-core TUN": { + "source": [ + "Furious.Plugins.Official.Xray.Plugin" + ], + "RU": "Использовать TUN Xray-core", + "ZH": "使用Xray-core TUN", + "isReviewed": "True" + }, + "Customize Xray-core TUN Settings": { + "source": [ + "Furious.Plugins.Official.Xray.GuiTUNSettings" + ], + "RU": "Настроить параметры TUN Xray-core", + "ZH": "自定义Xray-core TUN设置", + "isReviewed": "True" + }, + "Customize Xray-core TUN Settings...": { + "source": [ + "Furious.Plugins.Official.Xray.Plugin" + ], + "RU": "Настроить параметры TUN Xray-core...", + "ZH": "自定义Xray-core TUN设置...", + "isReviewed": "True" + }, + "Gateway (separated by commas)": { + "source": [ + "Furious.Plugins.Official.Xray.GuiTUNSettings" + ], + "RU": "Gateway (через запятую)", + "ZH": "Gateway(以逗号分隔)", + "isReviewed": "True" + }, + "DNS (separated by commas)": { + "source": [ + "Furious.Plugins.Official.Xray.GuiTUNSettings" + ], + "RU": "DNS (через запятую)", + "ZH": "DNS(以逗号分隔)", + "isReviewed": "True" + }, + "AutoSystemRoutingTable (separated by commas)": { + "source": [ + "Furious.Plugins.Official.Xray.GuiTUNSettings" + ], + "RU": "AutoSystemRoutingTable (через запятую)", + "ZH": "AutoSystemRoutingTable(以逗号分隔)", + "isReviewed": "True" + }, + "TUN Documentation": { + "source": [ + "Furious.Plugins.Official.Xray.GuiTUNSettings" + ], + "RU": "Документация TUN", + "ZH": "TUN文档", + "isReviewed": "True" } } diff --git a/Furious/Plugins/API.py b/Furious/Plugins/API.py index 23af227..7731545 100644 --- a/Furious/Plugins/API.py +++ b/Furious/Plugins/API.py @@ -69,6 +69,10 @@ class FuriousPlugin: """Return optional actions for this plugin's management submenu.""" return tuple() + def prepareTUN(self, config) -> bool: + """Prepare plugin-native TUN and return whether the plugin handles it.""" + return False + def routingOptions(self, config=None): """Return routing modes supported for a plugin configuration.""" return tuple() diff --git a/Furious/Plugins/Official/Hysteria1/Plugin.py b/Furious/Plugins/Official/Hysteria1/Plugin.py index bb45150..b1bd106 100644 --- a/Furious/Plugins/Official/Hysteria1/Plugin.py +++ b/Furious/Plugins/Official/Hysteria1/Plugin.py @@ -3,6 +3,7 @@ from __future__ import annotations from Furious.Frozenlib import * +from Furious.Qt.DynamicTranslate import gettext as _ from Furious.Plugins.API import FuriousPlugin, PluginProtocol, PluginRouting from Furious.Plugins.Official.Configuration import ( BLANK_CONFIG_HYSTERIA1, @@ -18,6 +19,10 @@ __all__ = ['Hysteria1Plugin'] logger = logging.getLogger(__name__) +_TRANSLATABLE_ACTION_TEXT = [ + _('Add Hysteria1 Server...'), +] + class Hysteria1Plugin(FuriousPlugin): """Provide official Hysteria 1 support.""" diff --git a/Furious/Plugins/Official/Hysteria2/Plugin.py b/Furious/Plugins/Official/Hysteria2/Plugin.py index 062c24a..6820256 100644 --- a/Furious/Plugins/Official/Hysteria2/Plugin.py +++ b/Furious/Plugins/Official/Hysteria2/Plugin.py @@ -2,6 +2,7 @@ from __future__ import annotations +from Furious.Qt.DynamicTranslate import gettext as _ from Furious.Plugins.API import FuriousPlugin, PluginProtocol from Furious.Plugins.Official.Configuration import ( BLANK_CONFIG_HYSTERIA2, @@ -17,6 +18,10 @@ __all__ = ['Hysteria2Plugin'] logger = logging.getLogger(__name__) +_TRANSLATABLE_ACTION_TEXT = [ + _('Add Hysteria2 Server...'), +] + class Hysteria2Plugin(FuriousPlugin): """Provide official Hysteria 2 support.""" diff --git a/Furious/Plugins/Official/Xray/GuiTUNSettings.py b/Furious/Plugins/Official/Xray/GuiTUNSettings.py new file mode 100644 index 0000000..a128a00 --- /dev/null +++ b/Furious/Plugins/Official/Xray/GuiTUNSettings.py @@ -0,0 +1,242 @@ +"""Provide the editor for Xray-core native TUN settings.""" + +from __future__ import annotations + +from Furious.Frozenlib import * +from Furious.Qt import * +from Furious.Qt import gettext as _ + +from PySide6.QtWidgets import * + +import copy +import functools +import logging +import re + +from .TUN import getXrayTUNSettings, saveXrayTUNSettings + +__all__ = ['GuiXrayTUNSettings'] + +logger = logging.getLogger(__name__) + + +class GuiXrayTUNItemText(GuiEditorItemTextInput): + """Edit one string-valued Xray TUN setting.""" + + def __init__(self, *args, **kwargs): + """Initialize an Xray TUN string editor.""" + self.key = kwargs.pop('key') + maximumWidth = kwargs.pop('maximumWidth', None) + super().__init__(*args, **kwargs) + + if maximumWidth is not None: + self._input.setMaximumWidth(maximumWidth) + + def inputToFactory(self, config: dict) -> bool: + """Store the current text in an Xray TUN settings mapping.""" + value = self.text().strip() + if config.get(self.key, '') == value: + return False + + config[self.key] = value + + return True + + def factoryToInput(self, config: dict): + """Load one Xray TUN string setting into the editor.""" + self.setText(config.get(self.key, '')) + + +class GuiXrayTUNItemList(GuiXrayTUNItemText): + """Edit one list-valued Xray TUN setting as comma-separated text.""" + + def inputToFactory(self, config: dict) -> bool: + """Store comma- or line-separated values in the settings mapping.""" + value = [ + item.strip() for item in re.split(r'[,\n]', self.text()) if item.strip() + ] + if config.get(self.key, []) == value: + return False + + config[self.key] = value + + return True + + def factoryToInput(self, config: dict): + """Load one Xray TUN list setting into the editor.""" + value = config.get(self.key, []) + self.setText(','.join(value) if isinstance(value, list) else '') + + +class GuiXrayTUNItemNumber(GuiEditorItemTextSpinBox): + """Edit one integer-valued Xray TUN setting.""" + + def __init__(self, *args, **kwargs): + """Initialize an Xray TUN numeric editor.""" + self.key = kwargs.pop('key') + self.default = kwargs.pop('default') + minimum = kwargs.pop('minimum') + maximum = kwargs.pop('maximum') + super().__init__(*args, **kwargs) + self.setRange(minimum, maximum) + + def inputToFactory(self, config: dict) -> bool: + """Store the current number in an Xray TUN settings mapping.""" + value = self.value() + if config.get(self.key, self.default) == value: + return False + + config[self.key] = value + + return True + + def factoryToInput(self, config: dict): + """Load one Xray TUN numeric setting into the editor.""" + value = config.get(self.key, self.default) + self.setValue(value if isinstance(value, int) else self.default) + + +class GuiXrayTUNSettingsGroupBoxInterface(GuiEditorWidgetQGroupBox): + """Edit Xray TUN interface identity and packet settings.""" + + def __init__(self, **kwargs): + """Initialize the interface settings group.""" + super().__init__(_('Interface'), **kwargs) + + def containerSequence(self): + """Return interface-setting editors in display order.""" + return [ + GuiXrayTUNItemText( + title='Name', + translatable=False, + key='name', + maximumWidth=360, + ), + GuiXrayTUNItemText( + title='Description', + translatable=False, + key='desc', + maximumWidth=360, + ), + GuiXrayTUNItemNumber( + title='MTU', + key='mtu', + default=1500, + minimum=1, + maximum=65535, + ), + GuiXrayTUNItemNumber( + title='UserLevel', + key='userLevel', + default=0, + minimum=0, + maximum=65535, + ), + ] + + +class XrayTUNDocumentationURL(AppQLabel): + """Link to the official Xray-core TUN documentation.""" + + URL = 'https://xtls.github.io/config/inbounds/tun.html' + + def __init__(self, *args, **kwargs): + """Initialize the Xray TUN documentation link.""" + super().__init__(*args, **kwargs) + + self.setWebsiteURL() + self.linkActivated.connect(self.handleLinkActivated) + + def setWebsiteURL(self): + """Set the translated documentation hyperlink text.""" + self.setText( + '

' + f'' + '' + + _('TUN Documentation') + + '

' + ) + + @staticmethod + def handleLinkActivated(link: str): + """Open the Xray TUN documentation in the default browser.""" + if QDesktopServices.openUrl(QtCore.QUrl(link)): + logger.info(f'open link \'{link}\' success') + else: + logger.error(f'open link \'{link}\' failed') + + def retranslate(self): + """Refresh the translated documentation hyperlink text.""" + self.setWebsiteURL() + + +class GuiXrayTUNSettingsGroupBoxNetwork(GuiEditorWidgetQGroupBox): + """Edit Xray TUN addresses, DNS, and automatic routing settings.""" + + def __init__(self, **kwargs): + """Initialize the network settings group.""" + super().__init__(_('Network'), **kwargs) + + def containerSequence(self): + """Return network-setting editors in display order.""" + return [ + GuiXrayTUNItemList( + title=_('Gateway (separated by commas)'), + key='gateway', + ), + GuiXrayTUNItemList( + title=_('DNS (separated by commas)'), + key='dns', + ), + GuiXrayTUNItemList( + title=_('AutoSystemRoutingTable (separated by commas)'), + key='autoSystemRoutingTable', + ), + GuiXrayTUNItemText( + title='AutoOutboundsInterface', + translatable=False, + key='autoOutboundsInterface', + ), + ] + + +class GuiXrayTUNSettings(GuiEditorWidgetQDialog): + """Edit and persist Xray-core native TUN settings.""" + + def __init__(self, *args, **kwargs): + """Initialize the Xray-core TUN settings dialog.""" + kwargs.setdefault('style', 'portrait') + kwargs.setdefault('tabTranslatable', True) + super().__init__(*args, **kwargs) + + self.setTabText(_('Customize Xray-core TUN Settings')) + self.setFixedSize(int(650 * GOLDEN_RATIO), 650) + self._settings = getXrayTUNSettings() + self.factoryToInput(self._settings) + self.accepted.connect(self.handleAccepted) + + self.layout().takeRow(self.layout().rowCount() - 1) + bottomLayout = QHBoxLayout() + bottomLayout.addWidget(XrayTUNDocumentationURL()) + bottomLayout.addStretch(1) + bottomLayout.addWidget(self.dialogBtns) + self.layout().addRow(bottomLayout) + + def handleAccepted(self): + """Persist changed settings and offer to reconnect when connected.""" + oldSettings = copy.deepcopy(self._settings) + self.inputToFactory(self._settings) + if self._settings == oldSettings: + return + + saveXrayTUNSettings(self._settings) + if SystemRuntime.isTUNMode(): + showMBoxNewChangesNextTime() + + @functools.lru_cache(None) + def groupBoxSequence(self): + """Return Xray TUN setting groups in display order.""" + return [ + GuiXrayTUNSettingsGroupBoxInterface(), + GuiXrayTUNSettingsGroupBoxNetwork(), + ] diff --git a/Furious/Plugins/Official/Xray/Plugin.py b/Furious/Plugins/Official/Xray/Plugin.py index c839b3e..fbbd396 100644 --- a/Furious/Plugins/Official/Xray/Plugin.py +++ b/Furious/Plugins/Official/Xray/Plugin.py @@ -4,6 +4,7 @@ from __future__ import annotations from Furious.Core.CoreProcessWorker import ProcessOutputRedirector from Furious.Frozenlib import * +from Furious.Qt.DynamicTranslate import gettext as _ from Furious.Library.Storage import Storage from Furious.Plugins.API import FuriousPlugin, PluginProtocol, PluginRouting from Furious.Plugins.Official.Configuration import ( @@ -19,6 +20,12 @@ import uuid from .Core import XrayCore from .Routing import customRoutingObjectFromSettings +from .TUN import ( + buildXrayTUNInbound, + hasXrayTUNInbound, + isXrayTUNEnabled, + setXrayTUNEnabled, +) __all__ = ['XrayPlugin'] @@ -63,6 +70,15 @@ def fixLogObjectPath(config, attr: str, value: str, log=True): ) +_TRANSLATABLE_ACTION_TEXT = [ + _('Add VMess Server...'), + _('Add VLESS Server...'), + _('Add Shadowsocks Server...'), + _('Add Trojan Server...'), + _('Add SOCKS Server...'), +] + + class XrayPlugin(FuriousPlugin): """Provide official Xray-core support.""" @@ -159,17 +175,38 @@ class XrayPlugin(FuriousPlugin): return self.createEditorForProtocol(config.proxyProtocol, parent, **kwargs) def createManagementActions(self, parent=None, **kwargs): - """Create Xray routing and asset-management actions.""" + """Create Xray routing, TUN, and asset-management actions.""" # These modules require a fully initialized Furious.Qt package. - from Furious.Qt import AppQAction, AppQSeperator, bootstrapIcon + from Furious.Qt import ( + AppQAction, + AppQSeperator, + bootstrapIcon, + showMBoxNewChangesNextTime, + ) from Furious.Qt import gettext as _ + from .GuiTUNSettings import GuiXrayTUNSettings from .UserRoutingWindow import UserRoutingWindow from .XrayAssetViewerWindow import XrayAssetViewerWindow routingEditor = UserRoutingWindow(parent=parent, **kwargs) assetViewer = XrayAssetViewerWindow(parent=parent) + useXrayTUNAction = AppQAction( + _('Use Xray-core TUN'), + checkable=True, + checked=isXrayTUNEnabled(), + ) + + def updateUseXrayTUN(): + """Persist the native Xray TUN action state.""" + setXrayTUNEnabled(useXrayTUNAction.isChecked()) + + if SystemRuntime.isTUNMode(): + showMBoxNewChangesNextTime() + + useXrayTUNAction.callback = updateUseXrayTUN + return ( AppQAction( _('Edit Routing...'), @@ -177,12 +214,39 @@ class XrayPlugin(FuriousPlugin): callback=routingEditor.show, ), AppQSeperator(), + useXrayTUNAction, + AppQAction( + _('Customize Xray-core TUN Settings...'), + callback=lambda: GuiXrayTUNSettings(parent=parent).open(), + ), + AppQSeperator(), AppQAction( _('Manage Xray-core Asset File...'), callback=assetViewer.show, ), ) + def prepareTUN(self, config) -> bool: + """Add the configured Xray native TUN inbound when enabled.""" + if not isXrayTUNEnabled(): + return False + + inbounds = config.get('inbounds') + if not isinstance(inbounds, list): + inbounds = [] + + config['inbounds'] = [ + inbound + for inbound in inbounds + if not ( + isinstance(inbound, dict) + and str(inbound.get('protocol', '')).casefold() == 'tun' + ) + ] + config['inbounds'].append(buildXrayTUNInbound()) + + return True + def routingOptions(self, config=None): """Return built-in and named routing modes supported by Xray.""" options = [ @@ -241,7 +305,11 @@ class XrayPlugin(FuriousPlugin): fixLogObjectPath(config, attr, logRedirectValue, log) if routing == AppBuiltinRouting.BypassMainlandChina.value: - if not proxyModeOnly and SystemRuntime.isTUNMode(): + if ( + not proxyModeOnly + and SystemRuntime.isTUNMode() + and not hasXrayTUNInbound(config) + ): # Defer Qt access until the application has finished importing it. from Furious.Qt.QtWidgets import showMBoxDirectRulesNotAllowed diff --git a/Furious/Plugins/Official/Xray/TUN.py b/Furious/Plugins/Official/Xray/TUN.py new file mode 100644 index 0000000..3ffd896 --- /dev/null +++ b/Furious/Plugins/Official/Xray/TUN.py @@ -0,0 +1,130 @@ +"""Store and apply Xray-core native TUN settings.""" + +from __future__ import annotations + +from Furious.Frozenlib import * + +import copy +import json + +__all__ = [ + 'DEFAULT_XRAY_TUN_SETTINGS', + 'buildXrayTUNInbound', + 'getXrayTUNSettings', + 'hasXrayTUNInbound', + 'isXrayTUNEnabled', + 'saveXrayTUNSettings', + 'setXrayTUNEnabled', +] + + +DEFAULT_XRAY_TUN_SETTINGS = { + 'name': '', + 'desc': '', + 'mtu': 1500, + 'gateway': ['10.0.0.1/16', 'fc00::1/64'], + 'dns': ['1.1.1.1', '8.8.8.8'], + 'userLevel': 0, + 'autoSystemRoutingTable': ['0.0.0.0/0', '::/0'], + 'autoOutboundsInterface': 'auto', +} + +registerAppSettings('useXrayTUN', isBinary=True) +registerAppSettings('XrayTUNSettings') + + +def isXrayTUNEnabled() -> bool: + """Return whether Xray-core should provide TUN mode.""" + return AppSettings.isStateON_('useXrayTUN') + + +def setXrayTUNEnabled(enabled: bool): + """Persist whether Xray-core should provide TUN mode.""" + AppSettings.set( + 'useXrayTUN', + AppBinarySettings.ON_ if enabled else AppBinarySettings.OFF, + ) + + +def _normalizedXrayTUNSettings(settings) -> dict: + """Return validated Xray TUN settings with defaults for invalid fields.""" + result = copy.deepcopy(DEFAULT_XRAY_TUN_SETTINGS) + if not isinstance(settings, dict): + return result + + for key in ('name', 'desc', 'autoOutboundsInterface'): + value = settings.get(key) + if isinstance(value, str): + result[key] = value.strip() + + for key in ('gateway', 'dns', 'autoSystemRoutingTable'): + value = settings.get(key) + if isinstance(value, (list, tuple)): + result[key] = [ + item.strip() for item in value if isinstance(item, str) and item.strip() + ] + + mtu = settings.get('mtu') + if isinstance(mtu, int) and not isinstance(mtu, bool) and mtu > 0: + result['mtu'] = mtu + + userLevel = settings.get('userLevel') + if ( + isinstance(userLevel, int) + and not isinstance(userLevel, bool) + and userLevel >= 0 + ): + result['userLevel'] = userLevel + + return result + + +def getXrayTUNSettings() -> dict: + """Return a validated copy of the persisted Xray TUN settings.""" + try: + settings = json.loads(AppSettings.get('XrayTUNSettings')) + except Exception: + settings = None + + return _normalizedXrayTUNSettings(settings) + + +def saveXrayTUNSettings(settings: dict): + """Validate and persist Xray TUN settings.""" + AppSettings.set( + 'XrayTUNSettings', + json.dumps(_normalizedXrayTUNSettings(settings), ensure_ascii=False), + ) + + +def buildXrayTUNInbound(settings=None) -> dict: + """Build an Xray TUN inbound from persisted or supplied settings.""" + settings = _normalizedXrayTUNSettings( + getXrayTUNSettings() if settings is None else settings + ) + settings = { + key: value for key, value in settings.items() if value != '' and value != [] + } + + return { + 'tag': 'tun', + 'protocol': 'tun', + 'settings': settings, + 'sniffing': { + 'enabled': True, + 'destOverride': ['http', 'tls', 'quic'], + }, + } + + +def hasXrayTUNInbound(config) -> bool: + """Return whether an Xray configuration contains a TUN inbound.""" + inbounds = config.get('inbounds', []) + if not isinstance(inbounds, list): + return False + + return any( + isinstance(inbound, dict) + and str(inbound.get('protocol', '')).casefold() == 'tun' + for inbound in inbounds + ) diff --git a/Furious/Plugins/Registry.py b/Furious/Plugins/Registry.py index 2754cad..2e5c127 100644 --- a/Furious/Plugins/Registry.py +++ b/Furious/Plugins/Registry.py @@ -205,6 +205,23 @@ class PluginRegistry: return tuple() + def prepareTUN(self, config) -> bool: + """Ask a configuration's plugin to prepare its native TUN support.""" + plugin = self.pluginForConfig(config) + if plugin is None: + return False + + try: + handled = plugin.prepareTUN(config) + if not isinstance(handled, bool): + raise TypeError('plugin TUN preparation result must be a boolean') + + return handled + except Exception as ex: + logger.error(f'TUN preparation failed for {plugin.pluginId!r}: {ex}') + + return False + def routingOptions(self, config): """Return validated routing modes supported by a configuration's plugin.""" plugin = self.pluginForConfig(config)