mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-29 18:38:02 +03:00
Add native Xray TUN support
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -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)
|
||||
|
||||
Vendored
+128
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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(
|
||||
'<html><head/><body><p>'
|
||||
f'<a href="{self.URL}">'
|
||||
'<span style=" text-decoration: underline; color:#007ad6;">'
|
||||
+ _('TUN Documentation')
|
||||
+ '</span></a></p></body></html>'
|
||||
)
|
||||
|
||||
@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(),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user