Feature: added SOCKS protocol support

Signed-off-by: LorenEteval <loren.eteval@proton.me>
This commit is contained in:
LorenEteval
2026-06-21 15:56:20 +08:00
committed by Loren Eteval
parent b2ff2ffd3c
commit 9c7ca9611c
7 changed files with 288 additions and 0 deletions
+22
View File
@@ -441,6 +441,7 @@ TRANSLATION = {
"Furious.Widget.GuiHysteria1",
"Furious.Widget.GuiHysteria2",
"Furious.Widget.GuiShadowsocks",
"Furious.Widget.GuiSocks",
"Furious.Widget.GuiTrojan",
"Furious.Widget.GuiVLESS",
"Furious.Widget.GuiVMess",
@@ -464,6 +465,7 @@ TRANSLATION = {
"Address": {
"source": [
"Furious.Widget.GuiShadowsocks",
"Furious.Widget.GuiSocks",
"Furious.Widget.GuiTrojan",
"Furious.Widget.GuiVLESS",
"Furious.Widget.GuiVMess",
@@ -476,6 +478,7 @@ TRANSLATION = {
"Port": {
"source": [
"Furious.Widget.GuiShadowsocks",
"Furious.Widget.GuiSocks",
"Furious.Widget.GuiTrojan",
"Furious.Widget.GuiVLESS",
"Furious.Widget.GuiVMess",
@@ -1265,6 +1268,7 @@ TRANSLATION = {
"Furious.Widget.GuiHysteria1",
"Furious.Widget.GuiHysteria2",
"Furious.Widget.GuiShadowsocks",
"Furious.Widget.GuiSocks",
"Furious.Widget.GuiTrojan",
"Furious.Widget.GuiTUNSettings",
"Furious.Widget.GuiVLESS",
@@ -1279,6 +1283,7 @@ TRANSLATION = {
"Furious.Widget.GuiHysteria1",
"Furious.Widget.GuiHysteria2",
"Furious.Widget.GuiShadowsocks",
"Furious.Widget.GuiSocks",
"Furious.Widget.GuiTrojan",
"Furious.Widget.GuiVLESS",
"Furious.Widget.GuiVMess"
@@ -1317,6 +1322,7 @@ TRANSLATION = {
"Password": {
"source": [
"Furious.Widget.GuiShadowsocks",
"Furious.Widget.GuiSocks",
"Furious.Widget.GuiTrojan"
],
"RU": "Пароль",
@@ -2173,5 +2179,21 @@ TRANSLATION = {
"RU": "Сгенерировать",
"ZH": "生成",
"isReviewed": "True"
},
"Username": {
"source": [
"Furious.Widget.GuiSocks"
],
"RU": "Имя пользователя",
"ZH": "用户名",
"isReviewed": "True"
},
"Add SOCKS Server...": {
"source": [
"Furious.Window.AppMainWindow"
],
"RU": "Добавить сервер SOCKS...",
"ZH": "添加SOCKS服务器...",
"isReviewed": "True"
}
}
+4
View File
@@ -51,6 +51,7 @@ class Protocol(Enum):
VMess = 'VMess'
VLESS = 'VLESS'
Shadowsocks = 'Shadowsocks'
Socks = 'SOCKS'
Trojan = 'Trojan'
Hysteria1 = 'hysteria1'
Hysteria2 = 'hysteria2'
@@ -70,6 +71,9 @@ class Protocol(Enum):
if protocol.lower() == 'shadowsocks':
return Protocol.Shadowsocks
if protocol.lower() == 'socks':
return Protocol.Socks
if protocol.lower() == 'trojan':
return Protocol.Trojan
+99
View File
@@ -174,6 +174,33 @@ class ConfigXrayProxyOutboundObjectSS(dict):
)
class ConfigXrayProxyOutboundObjectSocks(dict):
def __init__(self, address, port, user='', password=''):
settings = {
'address': address,
'port': int(port),
}
if user != '' or password != '':
settings['user'] = user
settings['pass'] = password
super().__init__(
**{
'tag': 'proxy',
'protocol': 'socks',
'settings': settings,
'streamSettings': {
'network': 'tcp',
},
'mux': {
'enabled': False,
'concurrency': -1,
},
}
)
class ConfigXrayProxyOutboundObjectTrojan(dict):
def __init__(self, password, address, port, type_, security, **kwargs):
networkObjectArgs, securityArgs, TLSObjectArgs = {}, {}, {}
@@ -353,6 +380,8 @@ class ConfigXray(ConfigFactory):
try:
if value == 'vless' or value == 'vmess':
server = proxyOutbound['settings']['vnext'][0]
elif value == 'socks':
server = proxyOutbound['settings']
elif value == 'shadowsocks' or value == 'trojan':
server = proxyOutbound['settings']['servers'][0]
else:
@@ -411,6 +440,9 @@ class ConfigXray(ConfigFactory):
if proxyProtocol == 'vmess' or proxyProtocol == 'vless':
return self.proxyOutboundObject['settings']['vnext'][0]
if proxyProtocol == 'socks':
return self.proxyOutboundObject['settings']
if proxyProtocol == 'shadowsocks' or proxyProtocol == 'trojan':
return self.proxyOutboundObject['settings']['servers'][0]
@@ -423,6 +455,9 @@ class ConfigXray(ConfigFactory):
@property
def proxyUserObject(self) -> dict:
try:
if self.proxyProtocol.lower() == 'socks':
return self.proxyServerObject
return self.proxyServerObject['users'][0]
except Exception:
# Any non-exit exceptions
@@ -1224,6 +1259,24 @@ class ConfigXray(ConfigFactory):
ConfigXrayProxyOutboundObjectSS(*getSSParams()),
)
@staticmethod
def URI2ProxyOutboundObjectSocks(URI: str) -> Tuple[str, dict]:
result = urlparse(URI)
remark = unquote(result.fragment)
address = result.hostname or ''
port = result.port or 0
user = unquote(result.username or '')
password = unquote(result.password or '')
if address == '' or port == 0:
raise ValueError(f'Invalid SOCKS URI format {URI}')
return (
remark,
ConfigXrayProxyOutboundObjectSocks(address, port, user, password),
)
@staticmethod
def URI2ProxyOutboundObjectTrojan(URI: str) -> Tuple[str, dict]:
result = urlparse(URI)
@@ -1256,6 +1309,9 @@ class ConfigXray(ConfigFactory):
if URI.startswith('ss://'):
return ConfigXray.URI2ProxyOutboundObjectSS(URI)
if URI.startswith(('socks://', 'socks5://', 'socks5h://')):
return ConfigXray.URI2ProxyOutboundObjectSocks(URI)
if URI.startswith('trojan://'):
return ConfigXray.URI2ProxyOutboundObjectTrojan(URI)
@@ -1378,6 +1434,20 @@ class ConfigXray(ConfigFactory):
return urlunparse(['ss', netloc, '', '', '', quote(override)])
if protocol == 'socks':
address, port = list(
self.proxyServerObject[value] for value in ['address', 'port']
)
user = self.proxyUserObject.get('user', '')
password = self.proxyUserObject.get('pass', '')
if user != '' or password != '':
netloc = f'{quote(user)}:{quote(password)}@{address}:{port}'
else:
netloc = f'{address}:{port}'
return urlunparse(['socks5', netloc, '', '', '', quote(override)])
if protocol == 'trojan':
password, address, port = list(
self.proxyServerObject[value]
@@ -2185,6 +2255,22 @@ def configXrayEmptyProxyOutboundObject(protocol: Protocol) -> dict:
'concurrency': -1,
},
}
elif value == 'socks':
return {
'tag': 'proxy',
'protocol': 'socks',
'settings': {
'address': '',
'port': 0,
},
'streamSettings': {
'network': 'tcp',
},
'mux': {
'enabled': False,
'concurrency': -1,
},
}
elif value == 'trojan':
return {
'tag': 'proxy',
@@ -2266,6 +2352,9 @@ def configFactoryFromAny(config: Union[str, dict], **kwargs) -> ConfigFactory:
'vless://',
'ss://',
'trojan://',
'socks://',
'socks5://',
'socks5h://',
)
):
return ConfigXray(config, **kwargs)
@@ -2309,6 +2398,16 @@ def configFactoryBlank(protocol: Protocol) -> ConfigFactory:
return factory
if protocol == Protocol.Socks:
factory = configFactoryFromDict(BLANK_CONFIG_XRAY)
factory['outbounds'][0]['protocol'] = protocol.value.lower()
factory['outbounds'][0]['settings'] = {
'address': '',
'port': 0,
}
return factory
if protocol == Protocol.Shadowsocks or protocol == Protocol.Trojan:
factory = configFactoryFromDict(BLANK_CONFIG_XRAY)
factory['outbounds'][0]['protocol'] = protocol.value.lower()
+152
View File
@@ -0,0 +1,152 @@
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
#
# This file is part of Furious.
#
# 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 <https://www.gnu.org/licenses/>.
from __future__ import annotations
from Furious.Frozenlib import *
from Furious.Interface import *
from Furious.Library import *
from Furious.Qt import *
from Furious.Qt import gettext as _
from Furious.Widget.GuiVTransport import *
from Furious.Widget.GuiVTLS import *
import functools
__all__ = ['GuiSocks']
getProxyOutboundServer = functools.partial(
ConfigXray.getProxyOutboundServer,
protocol=Protocol.Socks,
default=configXrayEmptyProxyOutboundObject(Protocol.Socks),
)
class GuiSocksItemTextInput(GuiEditorItemTextInput):
def __init__(self, *args, **kwargs):
key = kwargs.pop('key', '')
super().__init__(*args, **kwargs)
self.key = key
def inputToFactory(self, config: ConfigFactory) -> bool:
proxyOutboundServer = getProxyOutboundServer(config)
oldValue = proxyOutboundServer.get(self.key, '')
newValue = self.text()
if isinstance(oldValue, str):
if newValue != oldValue:
if newValue == '':
proxyOutboundServer.pop(self.key, None)
else:
proxyOutboundServer[self.key] = newValue
return True
return False
proxyOutboundServer[self.key] = newValue
return True
def factoryToInput(self, config: ConfigFactory):
try:
proxyOutboundServer = getProxyOutboundServer(config)
self.setText(proxyOutboundServer.get(self.key, ''))
except Exception:
# Any non-exit exceptions
self.setText('')
class GuiSocksItemBasicPort(GuiEditorItemTextSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setRange(0, 65535)
def inputToFactory(self, config: ConfigFactory) -> bool:
proxyOutboundServer = getProxyOutboundServer(config)
oldPort = proxyOutboundServer.get('port')
newPort = self.value()
if isinstance(oldPort, int):
if newPort != oldPort:
proxyOutboundServer['port'] = newPort
return True
return False
proxyOutboundServer['port'] = newPort
return True
def factoryToInput(self, config: ConfigFactory):
try:
proxyOutboundServer = getProxyOutboundServer(config)
self.setValue(proxyOutboundServer.get('port', 0))
except Exception:
# Any non-exit exceptions
self.setValue(0)
class GuiSocksGroupBoxBasic(GuiEditorWidgetQGroupBox):
def __init__(self, **kwargs):
super().__init__(_('Basic Configuration'), **kwargs)
def containerSequence(self):
return [
GuiEditorItemBasicRemark(title=_('Remark')),
GuiSocksItemTextInput(title=_('Address'), key='address'),
GuiSocksItemBasicPort(title=_('Port')),
GuiSocksItemTextInput(title=_('Username'), key='user'),
GuiSocksItemTextInput(title=_('Password'), key='pass'),
]
class GuiSocksGroupBoxProxy(GuiEditorWidgetQGroupBox):
def __init__(self, **kwargs):
super().__init__(_('Proxy'), **kwargs)
def containerSequence(self):
return [
GuiEditorItemProxyHttp(title='http', translatable=False),
GuiEditorItemProxySocks(title='socks', translatable=False),
]
class GuiSocks(GuiEditorWidgetQDialog):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setTabText(Protocol.Socks.value)
@functools.lru_cache(None)
def groupBoxSequence(self):
return [
GuiSocksGroupBoxBasic(),
GuiSocksGroupBoxProxy(),
GuiVTransportQGroupBox(),
GuiVTLSQGroupBox(),
]
+3
View File
@@ -27,6 +27,7 @@ from Furious.TrayActions.Import import *
from Furious.Widget.GuiHysteria1 import *
from Furious.Widget.GuiHysteria2 import *
from Furious.Widget.GuiShadowsocks import *
from Furious.Widget.GuiSocks import *
from Furious.Widget.GuiTrojan import *
from Furious.Widget.GuiVLESS import *
from Furious.Widget.GuiVMess import *
@@ -1762,6 +1763,8 @@ class UserServersQTableView(
return GuiVLESS(parent=self, **kwargs)
if protocol == Protocol.Shadowsocks:
return GuiShadowsocks(parent=self, **kwargs)
if protocol == Protocol.Socks:
return GuiSocks(parent=self, **kwargs)
if protocol == Protocol.Trojan:
return GuiTrojan(parent=self, **kwargs)
if protocol == Protocol.Hysteria2:
+1
View File
@@ -24,6 +24,7 @@ from .GuiCustomizeProxyBypass import *
from .GuiHysteria1 import *
from .GuiHysteria2 import *
from .GuiShadowsocks import *
from .GuiSocks import *
from .GuiTrojan import *
from .GuiTUNSettings import *
from .GuiVLESS import *
+7
View File
@@ -332,6 +332,13 @@ class AppMainWindow(AppQMainWindow):
),
),
AppQSeperator(),
AppQAction(
_('Add SOCKS Server...'),
callback=lambda: self.userServersQTableWidget.addServerViaGui(
Protocol.Socks, _('Add SOCKS Server...')
),
),
AppQSeperator(),
AppQAction(
_('New Empty Configuration'),
callback=lambda: self.userServersQTableWidget.newEmptyItem(),