mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
feat(sub): modern xray JSON format with unified finalmask editor
Drop the legacy JSON subscription format entirely and always emit the modern xray shape: - Flatten proxy outbounds (no vnext/servers) for vless/vmess/trojan/ shadowsocks; hysteria was already flat. - Express fragment/noise via streamSettings.finalmask instead of the legacy direct_out freedom dialer + dialerProxy sockopt. The global finalmask (tcp/udp masks + quicParams) is stored as a single setting (subJsonFinalMask) and merged into every generated stream, replacing the separate subJsonFragment/subJsonNoises/subJsonQuicParams settings. Reuse the existing FinalMaskForm (used by inbound/outbound) for the settings UI via a small bridge component; add a showAll prop so all TCP/UDP/QUIC sections render for the global case. This supersedes the hand-rolled Fragment/Noises/quicParams tabs with the full mask editor (all mask types). Note: this is a breaking change — JSON subscriptions now require a recent xray client on the consumer side.
This commit is contained in:
parent
f4a07121a9
commit
08fca9ed66
26 changed files with 276 additions and 445 deletions
|
|
@ -42,9 +42,8 @@ export interface AllSetting {
|
|||
subEnableRouting: boolean;
|
||||
subEncrypt: boolean;
|
||||
subJsonEnable: boolean;
|
||||
subJsonFragment: string;
|
||||
subJsonFinalMask: string;
|
||||
subJsonMux: string;
|
||||
subJsonNoises: string;
|
||||
subJsonPath: string;
|
||||
subJsonRules: string;
|
||||
subJsonURI: string;
|
||||
|
|
@ -129,9 +128,8 @@ export interface AllSettingView {
|
|||
subEnableRouting: boolean;
|
||||
subEncrypt: boolean;
|
||||
subJsonEnable: boolean;
|
||||
subJsonFragment: string;
|
||||
subJsonFinalMask: string;
|
||||
subJsonMux: string;
|
||||
subJsonNoises: string;
|
||||
subJsonPath: string;
|
||||
subJsonRules: string;
|
||||
subJsonURI: string;
|
||||
|
|
|
|||
|
|
@ -44,9 +44,8 @@ export const AllSettingSchema = z.object({
|
|||
subEnableRouting: z.boolean(),
|
||||
subEncrypt: z.boolean(),
|
||||
subJsonEnable: z.boolean(),
|
||||
subJsonFragment: z.string(),
|
||||
subJsonFinalMask: z.string(),
|
||||
subJsonMux: z.string(),
|
||||
subJsonNoises: z.string(),
|
||||
subJsonPath: z.string(),
|
||||
subJsonRules: z.string(),
|
||||
subJsonURI: z.string(),
|
||||
|
|
@ -132,9 +131,8 @@ export const AllSettingViewSchema = z.object({
|
|||
subEnableRouting: z.boolean(),
|
||||
subEncrypt: z.boolean(),
|
||||
subJsonEnable: z.boolean(),
|
||||
subJsonFragment: z.string(),
|
||||
subJsonFinalMask: z.string(),
|
||||
subJsonMux: z.string(),
|
||||
subJsonNoises: z.string(),
|
||||
subJsonPath: z.string(),
|
||||
subJsonRules: z.string(),
|
||||
subJsonURI: z.string(),
|
||||
|
|
|
|||
|
|
@ -6,21 +6,15 @@ import type { NamePath } from 'antd/es/form/interface';
|
|||
import { RandomUtil } from '@/utils';
|
||||
import { OutboundProtocols } from '@/schemas/primitives';
|
||||
|
||||
// Pattern A FinalMaskForm. Renders a Fragment of Form.Items at absolute
|
||||
// paths under `name`; the parent modal owns the Form instance.
|
||||
//
|
||||
// Naming convention inside Form.List: AntD prefixes Form.Item `name`
|
||||
// with the Form.List's own `name`. So Form.Items inside the render
|
||||
// prop use RELATIVE paths (e.g. `[field.name, 'type']`). Nested
|
||||
// Form.Lists also use relative names. Using absolute paths here would
|
||||
// double up the prefix and silently route reads/writes to the wrong
|
||||
// storage path.
|
||||
|
||||
export interface FinalMaskFormProps {
|
||||
name: NamePath;
|
||||
network: string;
|
||||
protocol: string;
|
||||
form: FormInstance;
|
||||
// When true, all sections (TCP / UDP / QUIC) are shown regardless of
|
||||
// network/protocol. Used by the global sub-JSON finalmask editor where
|
||||
// the masks apply to every stream rather than one specific transport.
|
||||
showAll?: boolean;
|
||||
}
|
||||
|
||||
const TCP_NETWORKS = ['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp'];
|
||||
|
|
@ -99,12 +93,12 @@ function defaultUdpHop(): Record<string, unknown> {
|
|||
return { ports: '20000-50000', interval: '5-10' };
|
||||
}
|
||||
|
||||
export default function FinalMaskForm({ name, network, protocol, form }: FinalMaskFormProps) {
|
||||
export default function FinalMaskForm({ name, network, protocol, form, showAll = false }: FinalMaskFormProps) {
|
||||
const base = asPath(name);
|
||||
const isHysteria = protocol === OutboundProtocols.Hysteria || protocol === 'hysteria';
|
||||
const showTcp = TCP_NETWORKS.includes(network);
|
||||
const showUdp = isHysteria || network === 'kcp';
|
||||
const showQuic = isHysteria || network === 'xhttp';
|
||||
const showTcp = showAll || TCP_NETWORKS.includes(network);
|
||||
const showUdp = showAll || isHysteria || network === 'kcp';
|
||||
const showQuic = showAll || isHysteria || network === 'xhttp';
|
||||
const quicParams = Form.useWatch([...base, 'quicParams'], { form, preserve: true });
|
||||
const hasQuicParams = quicParams != null;
|
||||
|
||||
|
|
@ -392,13 +386,13 @@ function UdpMaskItem({
|
|||
const options = isHysteria
|
||||
? [{ value: 'salamander', label: 'Salamander (Hysteria2)' }]
|
||||
: [
|
||||
{ value: 'mkcp-legacy', label: 'mKCP Legacy' },
|
||||
{ value: 'xdns', label: 'xDNS' },
|
||||
{ value: 'xicmp', label: 'xICMP' },
|
||||
{ value: 'realm', label: 'Realm' },
|
||||
{ value: 'header-custom', label: 'Header Custom' },
|
||||
{ value: 'noise', label: 'Noise' },
|
||||
];
|
||||
{ value: 'mkcp-legacy', label: 'mKCP Legacy' },
|
||||
{ value: 'xdns', label: 'xDNS' },
|
||||
{ value: 'xicmp', label: 'xICMP' },
|
||||
{ value: 'realm', label: 'Realm' },
|
||||
{ value: 'header-custom', label: 'Header Custom' },
|
||||
{ value: 'noise', label: 'Noise' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -55,10 +55,9 @@ export class AllSetting {
|
|||
subURI = '';
|
||||
subJsonURI = '';
|
||||
subClashURI = '';
|
||||
subJsonFragment = '';
|
||||
subJsonNoises = '';
|
||||
subJsonMux = '';
|
||||
subJsonRules = '';
|
||||
subJsonFinalMask = '';
|
||||
|
||||
timeLocation = 'Local';
|
||||
|
||||
|
|
|
|||
55
frontend/src/pages/settings/SubJsonFinalMaskForm.tsx
Normal file
55
frontend/src/pages/settings/SubJsonFinalMaskForm.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Form } from 'antd';
|
||||
|
||||
import { FinalMaskForm } from '@/lib/xray/forms/transport';
|
||||
import type { FinalMaskStreamSettings } from '@/schemas/protocols/stream/finalmask';
|
||||
|
||||
interface SubJsonFinalMaskFormProps {
|
||||
value: string;
|
||||
onChange: (next: string) => void;
|
||||
}
|
||||
|
||||
function hasValue(v: unknown): boolean {
|
||||
if (v == null) return false;
|
||||
if (Array.isArray(v)) return v.some(hasValue);
|
||||
if (typeof v === 'object') return Object.values(v as Record<string, unknown>).some(hasValue);
|
||||
if (typeof v === 'string') return v.length > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseFinalMask(raw: string): FinalMaskStreamSettings {
|
||||
try {
|
||||
if (raw) return JSON.parse(raw) as FinalMaskStreamSettings;
|
||||
} catch {
|
||||
return { tcp: [], udp: [] };
|
||||
}
|
||||
return { tcp: [], udp: [] };
|
||||
}
|
||||
|
||||
export default function SubJsonFinalMaskForm({ value, onChange }: SubJsonFinalMaskFormProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [initial] = useState(() => parseFinalMask(value));
|
||||
const onChangeRef = useRef(onChange);
|
||||
onChangeRef.current = onChange;
|
||||
|
||||
const finalmask = Form.useWatch('finalmask', form) as FinalMaskStreamSettings | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (finalmask === undefined) return;
|
||||
const next = hasValue(finalmask) ? JSON.stringify(finalmask) : '';
|
||||
if (next !== value) onChangeRef.current(next);
|
||||
}, [finalmask, value]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="horizontal"
|
||||
labelCol={{ flex: '160px' }}
|
||||
wrapperCol={{ flex: 'auto' }}
|
||||
colon={false}
|
||||
initialValues={{ finalmask: initial }}
|
||||
>
|
||||
<FinalMaskForm name="finalmask" network="" protocol="" form={form} showAll />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
|
|
@ -10,19 +8,17 @@ import {
|
|||
Tabs,
|
||||
} from 'antd';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
PartitionOutlined,
|
||||
PlusOutlined,
|
||||
ScissorOutlined,
|
||||
RocketOutlined,
|
||||
SendOutlined,
|
||||
SettingOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import { SettingListItem } from '@/components/ui';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { catTabLabel } from './catTabLabel';
|
||||
import { sanitizePath, normalizePath } from './uriPath';
|
||||
import SubJsonFinalMaskForm from './SubJsonFinalMaskForm';
|
||||
import './SubscriptionFormatsTab.css';
|
||||
|
||||
interface SubscriptionFormatsTabProps {
|
||||
|
|
@ -30,15 +26,6 @@ interface SubscriptionFormatsTabProps {
|
|||
updateSetting: (patch: Partial<AllSetting>) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_FRAGMENT = {
|
||||
packets: 'tlshello',
|
||||
length: '100-200',
|
||||
interval: '10-20',
|
||||
maxSplit: '300-400',
|
||||
};
|
||||
const DEFAULT_NOISES: { type: string; packet: string; delay: string; applyTo: string }[] = [
|
||||
{ type: 'rand', packet: '10-20', delay: '10-16', applyTo: 'ip' },
|
||||
];
|
||||
const DEFAULT_MUX = {
|
||||
enabled: true,
|
||||
concurrency: 8,
|
||||
|
|
@ -85,55 +72,9 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
|||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
|
||||
const fragment = allSetting.subJsonFragment !== '';
|
||||
const noisesEnabled = allSetting.subJsonNoises !== '';
|
||||
const muxEnabled = allSetting.subJsonMux !== '';
|
||||
const directEnabled = allSetting.subJsonRules !== '';
|
||||
|
||||
const fragmentObj = useMemo(
|
||||
() => (fragment ? readJson<typeof DEFAULT_FRAGMENT>(allSetting.subJsonFragment, DEFAULT_FRAGMENT) : DEFAULT_FRAGMENT),
|
||||
[allSetting.subJsonFragment, fragment],
|
||||
);
|
||||
|
||||
function setFragmentEnabled(v: boolean) {
|
||||
updateSetting({ subJsonFragment: v ? JSON.stringify(DEFAULT_FRAGMENT) : '' });
|
||||
}
|
||||
|
||||
function setFragmentField<K extends keyof typeof DEFAULT_FRAGMENT>(key: K, value: string) {
|
||||
if (value === '') return;
|
||||
const next = { ...fragmentObj, [key]: value };
|
||||
updateSetting({ subJsonFragment: JSON.stringify(next) });
|
||||
}
|
||||
|
||||
const noisesArray = useMemo(
|
||||
() => (noisesEnabled ? readJson<typeof DEFAULT_NOISES>(allSetting.subJsonNoises, DEFAULT_NOISES) : []),
|
||||
[allSetting.subJsonNoises, noisesEnabled],
|
||||
);
|
||||
|
||||
function setNoisesEnabled(v: boolean) {
|
||||
updateSetting({ subJsonNoises: v ? JSON.stringify(DEFAULT_NOISES) : '' });
|
||||
}
|
||||
|
||||
function setNoisesArray(next: typeof DEFAULT_NOISES) {
|
||||
if (noisesEnabled) updateSetting({ subJsonNoises: JSON.stringify(next) });
|
||||
}
|
||||
|
||||
function addNoise() {
|
||||
setNoisesArray([...noisesArray, { ...DEFAULT_NOISES[0] }]);
|
||||
}
|
||||
|
||||
function removeNoise(index: number) {
|
||||
const next = [...noisesArray];
|
||||
next.splice(index, 1);
|
||||
setNoisesArray(next);
|
||||
}
|
||||
|
||||
function updateNoiseField(index: number, field: keyof typeof DEFAULT_NOISES[number], value: string) {
|
||||
const next = [...noisesArray];
|
||||
next[index] = { ...next[index], [field]: value };
|
||||
setNoisesArray(next);
|
||||
}
|
||||
|
||||
const muxObj = useMemo(
|
||||
() => (muxEnabled ? readJson<typeof DEFAULT_MUX>(allSetting.subJsonMux, DEFAULT_MUX) : DEFAULT_MUX),
|
||||
[allSetting.subJsonMux, muxEnabled],
|
||||
|
|
@ -251,98 +192,19 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
|||
},
|
||||
{
|
||||
key: '2',
|
||||
label: catTabLabel(<ScissorOutlined />, t('pages.settings.fragment'), isMobile),
|
||||
label: catTabLabel(<RocketOutlined />, t('pages.settings.subFormats.finalMask'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.fragment')} description={t('pages.settings.fragmentDesc')}>
|
||||
<Switch checked={fragment} onChange={setFragmentEnabled} />
|
||||
</SettingListItem>
|
||||
{fragment && (
|
||||
<div className="format-settings">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.packets')}>
|
||||
<Input value={fragmentObj.packets} placeholder="1-1 | 1-3 | tlshello | …"
|
||||
onChange={(e) => setFragmentField('packets', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.length')}>
|
||||
<Input value={fragmentObj.length} placeholder="100-200"
|
||||
onChange={(e) => setFragmentField('length', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.interval')}>
|
||||
<Input value={fragmentObj.interval} placeholder="10-20"
|
||||
onChange={(e) => setFragmentField('interval', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.maxSplit')}>
|
||||
<Input value={fragmentObj.maxSplit} placeholder="300-400"
|
||||
onChange={(e) => setFragmentField('maxSplit', e.target.value)} />
|
||||
</SettingListItem>
|
||||
</div>
|
||||
)}
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.finalMask')} description={t('pages.settings.subFormats.finalMaskDesc')} />
|
||||
<SubJsonFinalMaskForm
|
||||
value={allSetting.subJsonFinalMask}
|
||||
onChange={(v) => updateSetting({ subJsonFinalMask: v })}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: catTabLabel(<ThunderboltOutlined />, t('pages.settings.subFormats.noises'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.noises')} description={t('pages.settings.noisesDesc')}>
|
||||
<Switch checked={noisesEnabled} onChange={setNoisesEnabled} />
|
||||
</SettingListItem>
|
||||
{noisesEnabled && (
|
||||
<div className="format-settings-list">
|
||||
{noisesArray.map((noise, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
size="small"
|
||||
className="noise-card"
|
||||
title={t('pages.settings.subFormats.noiseItem', { n: index + 1 })}
|
||||
extra={noisesArray.length > 1 ? (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
aria-label={t('delete')}
|
||||
onClick={() => removeNoise(index)}
|
||||
/>
|
||||
) : null}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.type')}>
|
||||
<Select
|
||||
value={noise.type}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(v) => updateNoiseField(index, 'type', v)}
|
||||
options={['rand', 'base64', 'str', 'hex'].map((p) => ({ value: p, label: p }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.packet')}>
|
||||
<Input value={noise.packet} placeholder="5-10"
|
||||
onChange={(e) => updateNoiseField(index, 'packet', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.delayMs')}>
|
||||
<Input value={noise.delay} placeholder="10-20"
|
||||
onChange={(e) => updateNoiseField(index, 'delay', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.applyTo')}>
|
||||
<Select
|
||||
value={noise.applyTo}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(v) => updateNoiseField(index, 'applyTo', v)}
|
||||
options={['ip', 'ipv4', 'ipv6'].map((p) => ({ value: p, label: p }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</Card>
|
||||
))}
|
||||
<Button type="dashed" block icon={<PlusOutlined />} onClick={addNoise}>
|
||||
{t('pages.settings.subFormats.addNoise')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
label: catTabLabel(<PartitionOutlined />, t('pages.settings.mux'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
|
|
@ -373,7 +235,7 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
|||
),
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
key: '4',
|
||||
label: catTabLabel(<SendOutlined />, t('pages.settings.direct'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -59,10 +59,9 @@ export const AllSettingSchema = z.object({
|
|||
subURI: z.string().optional(),
|
||||
subJsonURI: z.string().optional(),
|
||||
subClashURI: z.string().optional(),
|
||||
subJsonFragment: z.string().optional(),
|
||||
subJsonNoises: z.string().optional(),
|
||||
subJsonMux: z.string().optional(),
|
||||
subJsonRules: z.string().optional(),
|
||||
subJsonFinalMask: z.string().optional(),
|
||||
timeLocation: z.string().optional(),
|
||||
ldapEnable: z.boolean().optional(),
|
||||
ldapHost: z.string().optional(),
|
||||
|
|
|
|||
17
sub/sub.go
17
sub/sub.go
|
|
@ -120,16 +120,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
SubUpdates = "10"
|
||||
}
|
||||
|
||||
SubJsonFragment, err := s.settingService.GetSubJsonFragment()
|
||||
if err != nil {
|
||||
SubJsonFragment = ""
|
||||
}
|
||||
|
||||
SubJsonNoises, err := s.settingService.GetSubJsonNoises()
|
||||
if err != nil {
|
||||
SubJsonNoises = ""
|
||||
}
|
||||
|
||||
SubJsonMux, err := s.settingService.GetSubJsonMux()
|
||||
if err != nil {
|
||||
SubJsonMux = ""
|
||||
|
|
@ -140,6 +130,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
SubJsonRules = ""
|
||||
}
|
||||
|
||||
SubJsonFinalMask, err := s.settingService.GetSubJsonFinalMask()
|
||||
if err != nil {
|
||||
SubJsonFinalMask = ""
|
||||
}
|
||||
|
||||
SubTitle, err := s.settingService.GetSubTitle()
|
||||
if err != nil {
|
||||
SubTitle = ""
|
||||
|
|
@ -226,7 +221,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
|
||||
s.sub = NewSUBController(
|
||||
g, LinksPath, JsonPath, ClashPath, subJsonEnable, subClashEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,
|
||||
SubJsonMux, SubJsonRules, SubJsonFinalMask, SubTitle, SubSupportUrl,
|
||||
SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)
|
||||
|
||||
return engine, nil
|
||||
|
|
|
|||
|
|
@ -62,10 +62,9 @@ func NewSUBController(
|
|||
showInfo bool,
|
||||
rModel string,
|
||||
update string,
|
||||
jsonFragment string,
|
||||
jsonNoise string,
|
||||
jsonMux string,
|
||||
jsonRules string,
|
||||
jsonFinalMask string,
|
||||
subTitle string,
|
||||
subSupportUrl string,
|
||||
subProfileUrl string,
|
||||
|
|
@ -90,7 +89,7 @@ func NewSUBController(
|
|||
updateInterval: update,
|
||||
|
||||
subService: sub,
|
||||
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
||||
subJsonService: NewSubJsonService(jsonMux, jsonRules, jsonFinalMask, sub),
|
||||
subClashService: NewSubClashService(sub),
|
||||
}
|
||||
a.initRouter(g)
|
||||
|
|
|
|||
|
|
@ -21,9 +21,7 @@ var defaultJson string
|
|||
type SubJsonService struct {
|
||||
configJson map[string]any
|
||||
defaultOutbounds []json_util.RawMessage
|
||||
fragmentOrNoises bool
|
||||
fragment string
|
||||
noises string
|
||||
finalMask string
|
||||
mux string
|
||||
|
||||
inboundService service.InboundService
|
||||
|
|
@ -31,7 +29,7 @@ type SubJsonService struct {
|
|||
}
|
||||
|
||||
// NewSubJsonService creates a new JSON subscription service with the given configuration.
|
||||
func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService {
|
||||
func NewSubJsonService(mux string, rules string, finalMask string, subService *SubService) *SubJsonService {
|
||||
var configJson map[string]any
|
||||
var defaultOutbounds []json_util.RawMessage
|
||||
json.Unmarshal([]byte(defaultJson), &configJson)
|
||||
|
|
@ -42,31 +40,6 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string,
|
|||
}
|
||||
}
|
||||
|
||||
fragmentOrNoises := false
|
||||
if fragment != "" || noises != "" {
|
||||
fragmentOrNoises = true
|
||||
defaultOutboundsSettings := map[string]any{
|
||||
"domainStrategy": "UseIP",
|
||||
"redirect": "",
|
||||
}
|
||||
|
||||
if fragment != "" {
|
||||
defaultOutboundsSettings["fragment"] = json_util.RawMessage(fragment)
|
||||
}
|
||||
|
||||
if noises != "" {
|
||||
defaultOutboundsSettings["noises"] = json_util.RawMessage(noises)
|
||||
}
|
||||
|
||||
defaultDirectOutbound := map[string]any{
|
||||
"protocol": "freedom",
|
||||
"settings": defaultOutboundsSettings,
|
||||
"tag": "direct_out",
|
||||
}
|
||||
jsonBytes, _ := json.MarshalIndent(defaultDirectOutbound, "", " ")
|
||||
defaultOutbounds = append(defaultOutbounds, jsonBytes)
|
||||
}
|
||||
|
||||
if rules != "" {
|
||||
var newRules []any
|
||||
routing, _ := configJson["routing"].(map[string]any)
|
||||
|
|
@ -80,9 +53,7 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string,
|
|||
return &SubJsonService{
|
||||
configJson: configJson,
|
||||
defaultOutbounds: defaultOutbounds,
|
||||
fragmentOrNoises: fragmentOrNoises,
|
||||
fragment: fragment,
|
||||
noises: noises,
|
||||
finalMask: finalMask,
|
||||
mux: mux,
|
||||
SubService: subService,
|
||||
}
|
||||
|
|
@ -234,9 +205,8 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
|
|||
}
|
||||
delete(streamSettings, "sockopt")
|
||||
|
||||
if s.fragmentOrNoises {
|
||||
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "direct_out", "tcpKeepAliveIdle": 100}`)
|
||||
s.applySubJsonFinalMask(streamSettings)
|
||||
if s.finalMask != "" {
|
||||
s.applyGlobalFinalMask(streamSettings)
|
||||
}
|
||||
|
||||
// remove proxy protocol
|
||||
|
|
@ -260,102 +230,15 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
|
|||
return streamSettings
|
||||
}
|
||||
|
||||
func (s *SubJsonService) applySubJsonFinalMask(streamSettings map[string]any) {
|
||||
finalmask, _ := streamSettings["finalmask"].(map[string]any)
|
||||
if finalmask == nil {
|
||||
finalmask = map[string]any{}
|
||||
func (s *SubJsonService) applyGlobalFinalMask(streamSettings map[string]any) {
|
||||
var fm map[string]any
|
||||
if err := json.Unmarshal([]byte(s.finalMask), &fm); err != nil || len(fm) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
changed := false
|
||||
if tcpMask, ok := buildSubJsonFragmentFinalMask(s.fragment); ok {
|
||||
tcpMasks, _ := finalmask["tcp"].([]any)
|
||||
finalmask["tcp"] = append(tcpMasks, tcpMask)
|
||||
changed = true
|
||||
merged := mergeFinalMask(streamSettings["finalmask"], fm)
|
||||
if len(merged) > 0 {
|
||||
streamSettings["finalmask"] = merged
|
||||
}
|
||||
if udpMask, ok := buildSubJsonNoisesFinalMask(s.noises); ok {
|
||||
udpMasks, _ := finalmask["udp"].([]any)
|
||||
finalmask["udp"] = append(udpMasks, udpMask)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
streamSettings["finalmask"] = finalmask
|
||||
}
|
||||
}
|
||||
|
||||
func buildSubJsonFragmentFinalMask(fragment string) (map[string]any, bool) {
|
||||
if fragment == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var settings map[string]any
|
||||
if err := json.Unmarshal([]byte(fragment), &settings); err != nil || len(settings) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if interval, ok := settings["interval"]; ok {
|
||||
if _, hasDelay := settings["delay"]; !hasDelay {
|
||||
settings["delay"] = interval
|
||||
}
|
||||
delete(settings, "interval")
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": "fragment",
|
||||
"settings": settings,
|
||||
}, true
|
||||
}
|
||||
|
||||
func buildSubJsonNoisesFinalMask(noises string) (map[string]any, bool) {
|
||||
if noises == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var rawNoises []map[string]any
|
||||
if err := json.Unmarshal([]byte(noises), &rawNoises); err != nil || len(rawNoises) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
noiseItems := make([]any, 0, len(rawNoises))
|
||||
for _, rawNoise := range rawNoises {
|
||||
item := map[string]any{}
|
||||
noiseType, _ := rawNoise["type"].(string)
|
||||
packet, hasPacket := rawNoise["packet"]
|
||||
|
||||
if noiseType == "rand" {
|
||||
if !hasPacket {
|
||||
continue
|
||||
}
|
||||
item["rand"] = packet
|
||||
} else if hasPacket {
|
||||
if noiseType != "" {
|
||||
item["type"] = noiseType
|
||||
}
|
||||
item["packet"] = packet
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if delay, ok := rawNoise["delay"]; ok {
|
||||
item["delay"] = delay
|
||||
}
|
||||
if randRange, ok := rawNoise["randRange"]; ok {
|
||||
item["randRange"] = randRange
|
||||
}
|
||||
|
||||
noiseItems = append(noiseItems, item)
|
||||
}
|
||||
|
||||
if len(noiseItems) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": "noise",
|
||||
"settings": map[string]any{
|
||||
"noise": noiseItems,
|
||||
},
|
||||
}, true
|
||||
}
|
||||
|
||||
func (s *SubJsonService) removeAcceptProxy(setting any) map[string]any {
|
||||
|
|
@ -410,17 +293,6 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
|
|||
|
||||
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
usersData := make([]UserVnext, 1)
|
||||
|
||||
usersData[0].ID = client.ID
|
||||
usersData[0].Email = client.Email
|
||||
usersData[0].Security = client.Security
|
||||
vnextData := make([]VnextSetting, 1)
|
||||
vnextData[0] = VnextSetting{
|
||||
Address: inbound.Listen,
|
||||
Port: inbound.Port,
|
||||
Users: usersData,
|
||||
}
|
||||
|
||||
outbound.Protocol = string(inbound.Protocol)
|
||||
outbound.Tag = "proxy"
|
||||
|
|
@ -428,8 +300,17 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
|||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
|
||||
security := client.Security
|
||||
if security == "" {
|
||||
security = "auto"
|
||||
}
|
||||
outbound.Settings = map[string]any{
|
||||
"vnext": vnextData,
|
||||
"address": inbound.Listen,
|
||||
"port": inbound.Port,
|
||||
"id": client.ID,
|
||||
"security": security,
|
||||
"level": 8,
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
|
|
@ -450,24 +331,17 @@ func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_ut
|
|||
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
|
||||
encryption, _ := inboundSettings["encryption"].(string)
|
||||
|
||||
user := map[string]any{
|
||||
settings := map[string]any{
|
||||
"address": inbound.Listen,
|
||||
"port": inbound.Port,
|
||||
"id": client.ID,
|
||||
"level": 8,
|
||||
"encryption": encryption,
|
||||
"level": 8,
|
||||
}
|
||||
if client.Flow != "" {
|
||||
user["flow"] = client.Flow
|
||||
}
|
||||
|
||||
vnext := map[string]any{
|
||||
"address": inbound.Listen,
|
||||
"port": inbound.Port,
|
||||
"users": []any{user},
|
||||
}
|
||||
|
||||
outbound.Settings = map[string]any{
|
||||
"vnext": []any{vnext},
|
||||
settings["flow"] = client.Flow
|
||||
}
|
||||
outbound.Settings = settings
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
return result
|
||||
}
|
||||
|
|
@ -503,9 +377,17 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
|
|||
outbound.Mux = json_util.RawMessage(s.mux)
|
||||
}
|
||||
outbound.StreamSettings = streamSettings
|
||||
outbound.Settings = map[string]any{
|
||||
"servers": serverData,
|
||||
|
||||
settings := map[string]any{
|
||||
"address": serverData[0].Address,
|
||||
"port": serverData[0].Port,
|
||||
"password": serverData[0].Password,
|
||||
"level": 8,
|
||||
}
|
||||
if inbound.Protocol == model.Shadowsocks {
|
||||
settings["method"] = serverData[0].Method
|
||||
}
|
||||
outbound.Settings = settings
|
||||
|
||||
result, _ := json.MarshalIndent(outbound, "", " ")
|
||||
return result
|
||||
|
|
@ -600,18 +482,6 @@ type Outbound struct {
|
|||
Settings map[string]any `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
type VnextSetting struct {
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Users []UserVnext `json:"users"`
|
||||
}
|
||||
|
||||
type UserVnext struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Security string `json:"security,omitempty"`
|
||||
}
|
||||
|
||||
type ServerSetting struct {
|
||||
Password string `json:"password"`
|
||||
Level int `json:"level"`
|
||||
|
|
|
|||
|
|
@ -3,31 +3,47 @@ package sub
|
|||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
)
|
||||
|
||||
func TestSubJsonServiceKeepsDirectOutAndAddsFinalMask(t *testing.T) {
|
||||
fragment := `{"packets":"1-3","length":"100-200","interval":"10-20","maxSplit":"100-200"}`
|
||||
noises := `[{"type":"rand","packet":"10-20","delay":"10-16","applyTo":"ip"},{"type":"base64","packet":"SGVsbG8=","delay":"5"}]`
|
||||
svc := NewSubJsonService(fragment, noises, "", "", nil)
|
||||
func hasDirectOutOutbound(svc *SubJsonService) bool {
|
||||
for _, raw := range svc.defaultOutbounds {
|
||||
var outbound map[string]any
|
||||
if err := json.Unmarshal(raw, &outbound); err != nil {
|
||||
continue
|
||||
}
|
||||
if outbound["tag"] == "direct_out" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var directOut map[string]any
|
||||
if err := json.Unmarshal(svc.defaultOutbounds[len(svc.defaultOutbounds)-1], &directOut); err != nil {
|
||||
t.Fatalf("failed to unmarshal compatibility direct_out: %v", err)
|
||||
func outboundSettings(t *testing.T, raw []byte) map[string]any {
|
||||
t.Helper()
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
t.Fatalf("failed to unmarshal outbound: %v", err)
|
||||
}
|
||||
if directOut["tag"] != "direct_out" {
|
||||
t.Fatalf("direct_out tag = %v, want direct_out", directOut["tag"])
|
||||
settings, _ := parsed["settings"].(map[string]any)
|
||||
if settings == nil {
|
||||
t.Fatal("outbound has no settings")
|
||||
}
|
||||
directSettings, _ := directOut["settings"].(map[string]any)
|
||||
if _, ok := directSettings["fragment"]; !ok {
|
||||
t.Fatal("compatibility direct_out is missing freedom fragment")
|
||||
}
|
||||
if _, ok := directSettings["noises"]; !ok {
|
||||
t.Fatal("compatibility direct_out is missing freedom noises")
|
||||
return settings
|
||||
}
|
||||
|
||||
func TestSubJsonServiceInjectsGlobalFinalMask(t *testing.T) {
|
||||
finalMask := `{"tcp":[{"type":"fragment","settings":{"packets":"tlshello","length":"100-200","delay":"10-20"}}],"udp":[{"type":"noise","settings":{"noise":[{"type":"base64","packet":"SGVsbG8="}]}}],"quicParams":{"congestion":"bbr"}}`
|
||||
svc := NewSubJsonService("", "", finalMask, nil)
|
||||
|
||||
if hasDirectOutOutbound(svc) {
|
||||
t.Fatal("direct_out outbound must never be emitted")
|
||||
}
|
||||
|
||||
stream := svc.streamData(`{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}}}`)
|
||||
if _, ok := stream["sockopt"]; !ok {
|
||||
t.Fatal("streamSettings is missing direct_out sockopt compatibility path")
|
||||
if _, ok := stream["sockopt"]; ok {
|
||||
t.Fatal("legacy direct_out dialerProxy sockopt must never be set")
|
||||
}
|
||||
|
||||
finalmask, _ := stream["finalmask"].(map[string]any)
|
||||
|
|
@ -35,72 +51,98 @@ func TestSubJsonServiceKeepsDirectOutAndAddsFinalMask(t *testing.T) {
|
|||
t.Fatal("streamSettings is missing finalmask")
|
||||
}
|
||||
|
||||
tcpMasks, _ := finalmask["tcp"].([]any)
|
||||
if len(tcpMasks) != 1 {
|
||||
t.Fatalf("finalmask tcp masks len = %d, want 1", len(tcpMasks))
|
||||
tcp, _ := finalmask["tcp"].([]any)
|
||||
if len(tcp) != 1 {
|
||||
t.Fatalf("tcp masks len = %d, want 1", len(tcp))
|
||||
}
|
||||
fragmentMask, _ := tcpMasks[0].(map[string]any)
|
||||
if fragmentMask["type"] != "fragment" {
|
||||
t.Fatalf("tcp mask type = %v, want fragment", fragmentMask["type"])
|
||||
}
|
||||
fragmentSettings, _ := fragmentMask["settings"].(map[string]any)
|
||||
if fragmentSettings["delay"] != "10-20" {
|
||||
t.Fatalf("fragment delay = %v, want 10-20", fragmentSettings["delay"])
|
||||
}
|
||||
if _, ok := fragmentSettings["interval"]; ok {
|
||||
t.Fatal("finalmask fragment should use delay, not interval")
|
||||
if first, _ := tcp[0].(map[string]any); first["type"] != "fragment" {
|
||||
t.Fatalf("tcp[0] type = %v, want fragment", first["type"])
|
||||
}
|
||||
|
||||
udpMasks, _ := finalmask["udp"].([]any)
|
||||
if len(udpMasks) != 1 {
|
||||
t.Fatalf("finalmask udp masks len = %d, want 1", len(udpMasks))
|
||||
udp, _ := finalmask["udp"].([]any)
|
||||
if len(udp) != 1 {
|
||||
t.Fatalf("udp masks len = %d, want 1", len(udp))
|
||||
}
|
||||
noiseMask, _ := udpMasks[0].(map[string]any)
|
||||
if noiseMask["type"] != "noise" {
|
||||
t.Fatalf("udp mask type = %v, want noise", noiseMask["type"])
|
||||
}
|
||||
noiseSettings, _ := noiseMask["settings"].(map[string]any)
|
||||
noiseItems, _ := noiseSettings["noise"].([]any)
|
||||
if len(noiseItems) != 2 {
|
||||
t.Fatalf("noise items len = %d, want 2", len(noiseItems))
|
||||
}
|
||||
randItem, _ := noiseItems[0].(map[string]any)
|
||||
if randItem["rand"] != "10-20" {
|
||||
t.Fatalf("rand noise item rand = %v, want 10-20", randItem["rand"])
|
||||
}
|
||||
if _, ok := randItem["applyTo"]; ok {
|
||||
t.Fatal("finalmask noise should not carry freedom noises applyTo")
|
||||
}
|
||||
packetItem, _ := noiseItems[1].(map[string]any)
|
||||
if packetItem["type"] != "base64" || packetItem["packet"] != "SGVsbG8=" {
|
||||
t.Fatalf("packet noise item = %#v, want base64 packet", packetItem)
|
||||
|
||||
quic, _ := finalmask["quicParams"].(map[string]any)
|
||||
if quic == nil || quic["congestion"] != "bbr" {
|
||||
t.Fatalf("quicParams missing/wrong: %#v", finalmask["quicParams"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubJsonServiceAppendsFinalMaskToExistingMasks(t *testing.T) {
|
||||
fragment := `{"packets":"tlshello","length":"100-200","interval":"0"}`
|
||||
svc := NewSubJsonService(fragment, "", "", "", nil)
|
||||
func TestSubJsonServiceMergesWithExistingFinalMask(t *testing.T) {
|
||||
finalMask := `{"tcp":[{"type":"fragment","settings":{"packets":"tlshello"}}]}`
|
||||
svc := NewSubJsonService("", "", finalMask, nil)
|
||||
|
||||
stream := svc.streamData(`{
|
||||
"network":"tcp",
|
||||
"security":"none",
|
||||
"tcpSettings":{"header":{"type":"none"}},
|
||||
"finalmask":{"tcp":[{"type":"sudoku"}],"udp":[{"type":"salamander","settings":{"password":"secret"}}]}
|
||||
"network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}},
|
||||
"finalmask":{"tcp":[{"type":"sudoku"}]}
|
||||
}`)
|
||||
|
||||
finalmask, _ := stream["finalmask"].(map[string]any)
|
||||
tcpMasks, _ := finalmask["tcp"].([]any)
|
||||
if len(tcpMasks) != 2 {
|
||||
t.Fatalf("finalmask tcp masks len = %d, want 2", len(tcpMasks))
|
||||
tcp, _ := finalmask["tcp"].([]any)
|
||||
if len(tcp) != 2 {
|
||||
t.Fatalf("tcp masks len = %d, want 2 (existing + global)", len(tcp))
|
||||
}
|
||||
firstTCP, _ := tcpMasks[0].(map[string]any)
|
||||
secondTCP, _ := tcpMasks[1].(map[string]any)
|
||||
if firstTCP["type"] != "sudoku" || secondTCP["type"] != "fragment" {
|
||||
t.Fatalf("tcp masks = %#v, want existing mask followed by subscription fragment", tcpMasks)
|
||||
}
|
||||
|
||||
udpMasks, _ := finalmask["udp"].([]any)
|
||||
if len(udpMasks) != 1 {
|
||||
t.Fatalf("finalmask udp masks len = %d, want existing udp mask preserved", len(udpMasks))
|
||||
a, _ := tcp[0].(map[string]any)
|
||||
b, _ := tcp[1].(map[string]any)
|
||||
if a["type"] != "sudoku" || b["type"] != "fragment" {
|
||||
t.Fatalf("tcp masks = %#v, want existing sudoku then global fragment", tcp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubJsonServiceNoFinalMaskWhenEmpty(t *testing.T) {
|
||||
svc := NewSubJsonService("", "", "", nil)
|
||||
stream := svc.streamData(`{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}}}`)
|
||||
if _, ok := stream["finalmask"]; ok {
|
||||
t.Fatal("no finalmask should be emitted when subJsonFinalMask is empty")
|
||||
}
|
||||
if _, ok := stream["sockopt"]; ok {
|
||||
t.Fatal("legacy direct_out sockopt must never be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubJsonServiceVlessFlattened(t *testing.T) {
|
||||
inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VLESS, Settings: `{"encryption":"none"}`}
|
||||
client := model.Client{ID: "uuid-1", Flow: "xtls-rprx-vision"}
|
||||
|
||||
settings := outboundSettings(t, NewSubJsonService("", "", "", nil).genVless(inbound, nil, client))
|
||||
if _, ok := settings["vnext"]; ok {
|
||||
t.Fatal("vless outbound must not use vnext")
|
||||
}
|
||||
if settings["address"] != "1.2.3.4" || settings["id"] != "uuid-1" || settings["encryption"] != "none" || settings["flow"] != "xtls-rprx-vision" {
|
||||
t.Fatalf("flat vless settings wrong: %#v", settings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubJsonServiceVmessFlattened(t *testing.T) {
|
||||
inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VMESS, Settings: `{}`}
|
||||
client := model.Client{ID: "uuid-2"}
|
||||
|
||||
settings := outboundSettings(t, NewSubJsonService("", "", "", nil).genVnext(inbound, nil, client))
|
||||
if _, ok := settings["vnext"]; ok {
|
||||
t.Fatal("vmess outbound must not use vnext")
|
||||
}
|
||||
if settings["id"] != "uuid-2" || settings["security"] != "auto" {
|
||||
t.Fatalf("flat vmess settings wrong: %#v", settings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubJsonServiceServerFlattened(t *testing.T) {
|
||||
trojan := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.Trojan, Settings: `{}`}
|
||||
client := model.Client{Password: "p4ss"}
|
||||
|
||||
settings := outboundSettings(t, NewSubJsonService("", "", "", nil).genServer(trojan, nil, client))
|
||||
if _, ok := settings["servers"]; ok {
|
||||
t.Fatal("trojan outbound must not use servers array")
|
||||
}
|
||||
if settings["password"] != "p4ss" || settings["address"] != "1.2.3.4" {
|
||||
t.Fatalf("flat trojan settings wrong: %#v", settings)
|
||||
}
|
||||
|
||||
ss := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.Shadowsocks, Settings: `{"method":"aes-256-gcm"}`}
|
||||
ssSettings := outboundSettings(t, NewSubJsonService("", "", "", nil).genServer(ss, nil, client))
|
||||
if ssSettings["method"] != "aes-256-gcm" {
|
||||
t.Fatalf("flat shadowsocks must carry method: %#v", ssSettings)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,10 +83,9 @@ type AllSetting struct {
|
|||
SubClashEnable bool `json:"subClashEnable" form:"subClashEnable"` // Enable Clash/Mihomo subscription endpoint
|
||||
SubClashPath string `json:"subClashPath" form:"subClashPath"` // Path for Clash/Mihomo subscription endpoint
|
||||
SubClashURI string `json:"subClashURI" form:"subClashURI"` // Clash/Mihomo subscription server URI
|
||||
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` // JSON subscription fragment configuration
|
||||
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` // JSON subscription noise configuration
|
||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` // JSON subscription mux configuration
|
||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
||||
SubJsonFinalMask string `json:"subJsonFinalMask" form:"subJsonFinalMask"` // JSON subscription global finalmask (tcp/udp masks + quicParams)
|
||||
|
||||
// LDAP settings
|
||||
LdapEnable bool `json:"ldapEnable" form:"ldapEnable"`
|
||||
|
|
|
|||
|
|
@ -79,10 +79,9 @@ var defaultValueMap = map[string]string{
|
|||
"subClashEnable": "false",
|
||||
"subClashPath": "/clash/",
|
||||
"subClashURI": "",
|
||||
"subJsonFragment": "",
|
||||
"subJsonNoises": "",
|
||||
"subJsonMux": "",
|
||||
"subJsonRules": "",
|
||||
"subJsonFinalMask": "",
|
||||
"datepicker": "gregorian",
|
||||
"warp": "",
|
||||
"nord": "",
|
||||
|
|
@ -658,14 +657,6 @@ func (s *SettingService) GetSubClashURI() (string, error) {
|
|||
return s.getString("subClashURI")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubJsonFragment() (string, error) {
|
||||
return s.getString("subJsonFragment")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubJsonNoises() (string, error) {
|
||||
return s.getString("subJsonNoises")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubJsonMux() (string, error) {
|
||||
return s.getString("subJsonMux")
|
||||
}
|
||||
|
|
@ -674,6 +665,10 @@ func (s *SettingService) GetSubJsonRules() (string, error) {
|
|||
return s.getString("subJsonRules")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubJsonFinalMask() (string, error) {
|
||||
return s.getString("subJsonFinalMask")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetDatepicker() (string, error) {
|
||||
return s.getString("datepicker")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "حد IP الافتراضي"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "أقنعة finalmask في xray (TCP/UDP) وضبط QUIC تُضاف إلى كل تدفق اشتراك JSON. يتطلب إصدار xray حديثًا على العميل.",
|
||||
"packets": "الحزم",
|
||||
"length": "الطول",
|
||||
"interval": "الفاصل",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "Default IP limit"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "xray finalmask masks (TCP/UDP) and QUIC tuning injected into every JSON subscription stream. Requires a recent xray client.",
|
||||
"packets": "Packets",
|
||||
"length": "Length",
|
||||
"interval": "Interval",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "Límite IP por defecto"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Máscaras finalmask de xray (TCP/UDP) y ajustes QUIC inyectados en cada flujo de suscripción JSON. Requiere un cliente xray reciente.",
|
||||
"packets": "Paquetes",
|
||||
"length": "Longitud",
|
||||
"interval": "Intervalo",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "محدودیت IP پیشفرض"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "ماسکهای finalmask ایکسری (TCP/UDP) و تنظیمات QUIC که داخل همهی stream های اشتراک JSON تزریق میشوند. به نسخهی جدید هستهی xray در کلاینت نیاز دارد.",
|
||||
"packets": "بستهها",
|
||||
"length": "طول",
|
||||
"interval": "بازه",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "Batas IP default"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Mask finalmask xray (TCP/UDP) dan penyetelan QUIC yang disuntikkan ke setiap stream langganan JSON. Membutuhkan klien xray terbaru.",
|
||||
"packets": "Paket",
|
||||
"length": "Panjang",
|
||||
"interval": "Interval",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "デフォルト IP 制限"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "すべての JSON サブスクリプションストリームに注入される xray finalmask マスク(TCP/UDP)と QUIC チューニング。新しい xray クライアントが必要です。",
|
||||
"packets": "パケット",
|
||||
"length": "長さ",
|
||||
"interval": "間隔",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "Limite de IP padrão"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Máscaras finalmask do xray (TCP/UDP) e ajustes QUIC injetados em cada fluxo de assinatura JSON. Requer um cliente xray recente.",
|
||||
"packets": "Pacotes",
|
||||
"length": "Comprimento",
|
||||
"interval": "Intervalo",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "Лимит IP по умолчанию"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Маски finalmask xray (TCP/UDP) и настройки QUIC, добавляемые в каждый поток JSON-подписки. Требуется свежая версия xray на клиенте.",
|
||||
"packets": "Пакеты",
|
||||
"length": "Длина",
|
||||
"interval": "Интервал",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "Varsayılan IP limiti"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Her JSON abonelik akışına eklenen xray finalmask maskeleri (TCP/UDP) ve QUIC ayarları. Güncel bir xray istemcisi gerektirir.",
|
||||
"packets": "Paketler",
|
||||
"length": "Uzunluk",
|
||||
"interval": "Aralık",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "Ліміт IP за замовч."
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Маски finalmask xray (TCP/UDP) і налаштування QUIC, що додаються до кожного потоку JSON-підписки. Потрібна свіжа версія xray на клієнті.",
|
||||
"packets": "Пакети",
|
||||
"length": "Довжина",
|
||||
"interval": "Інтервал",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "Giới hạn IP mặc định"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Mask finalmask của xray (TCP/UDP) và tinh chỉnh QUIC được thêm vào mọi luồng đăng ký JSON. Yêu cầu client xray mới hơn.",
|
||||
"packets": "Gói",
|
||||
"length": "Độ dài",
|
||||
"interval": "Khoảng",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "默认 IP 限制"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "注入到每个 JSON 订阅流的 xray finalmask 掩码(TCP/UDP)和 QUIC 调优。需要较新的 xray 客户端。",
|
||||
"packets": "数据包",
|
||||
"length": "长度",
|
||||
"interval": "间隔",
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,8 @@
|
|||
"defaultIpLimit": "預設 IP 限制"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "注入到每個 JSON 訂閱串流的 xray finalmask 遮罩(TCP/UDP)與 QUIC 調校。需要較新的 xray 用戶端。",
|
||||
"packets": "封包",
|
||||
"length": "長度",
|
||||
"interval": "間隔",
|
||||
|
|
|
|||
Loading…
Reference in a new issue