Skip to content

Conversation

@Max-Sum
Copy link

@Max-Sum Max-Sum commented Oct 31, 2025

This is a prove of concept implementation.

SS-2022 has a feature called Extensible Identity Headers. It allows relays without giving them the user PSKs. xray-core supports this feature and have examples on configuration file.

This PR adds support for relay only for SS-2022.

To use this feature, one have to add inbounds specifically for relay:

"inbounds": [
     {
       "tag": "SS2022-relay",
       "port": 1234,
       "protocol": "shadowsocks",
       "settings": {
         "method": "2022-blake3-aes-128-gcm",
         "password": "{{ relay psk }}",
         "clients": [
           {
             "address": "server",
             "port": 1234,
             "password": "{{ next hop server psk }}",
             "email": "my server"
           }
         ],
         "network": "tcp,udp"
       }
     }
   ],

In the host settings, there is a field to select relay inbounds.
The final password for user subscription will be {{ relay psk 0 }}:{{ relay psk 1 }}:..:{{ server spk }}:{{ user psk }}.

Summary by CodeRabbit

  • New Features

    • Added Shadowsocks 2022 relay inbounds selection and management in host configuration.
    • Enabled ordered relay password support for Shadowsocks 2022 subscription generation.
    • Added validation to ensure relay inbound chains are valid.
  • Bug Fixes

    • Removed duplicate relay settings block in the host modal.
  • Documentation

    • Added localization for relay settings (EN, FA, RU, ZH).

@coderabbitai
Copy link

coderabbitai bot commented Oct 31, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds Shadowsocks 2022 relay inbound tag support: database migration and model field, host validation and relay-password resolution, propagation of ordered relay_passwords into subscription payloads and handlers, plus frontend UI, types, and localization updates.

Changes

Cohort / File(s) Summary
Database Schema & Model
app/db/migrations/versions/4bbb43d025bc_add_ss2022_relay_tags_to_hosts.py, app/db/models.py
Adds migration to add ss2022_relay_inbound_tags column and adds ss2022_relay_inbound_tags field to ProxyHost (optional string array).
Backend Data Models
app/models/host.py, app/models/subscription.py
BaseHost gains optional ss2022_relay_inbound_tags; SubscriptionInboundData gains relay_passwords: list[str] (ordered relay passwords).
Backend Relay Logic
app/core/hosts.py
Resolves ordered relay passwords by collecting passwords from inbound configs referenced in ss2022_relay_inbound_tags, skips failures, and includes relay_passwords in SubscriptionInboundData.
Backend Validation
app/operation/host.py
Adds core_manager import and _validate_ss2022_relay_chain() to validate relay tag chains (existence, no self-reference, each tag maps to a 2022 inbound with password); invoked on create/modify flows.
Subscription Base & Handlers
app/subscription/base.py, app/subscription/clash.py, app/subscription/links.py, app/subscription/outline.py, app/subscription/singbox.py, app/subscription/xray.py
password_to_2022() and detect_shadowsocks_2022() accept optional relay_passwords; handlers pass inbound relay_passwords through to detection so relay chain affects method/password construction.
Frontend Modal & Components
dashboard/src/components/dialogs/HostModal.tsx, dashboard/src/components/hosts/Hosts.tsx, dashboard/src/components/hosts/SortableHost.tsx, dashboard/src/pages/_dashboard._index.tsx, dashboard/src/pages/_dashboard.hosts.tsx
Adds UI controls (relay settings accordion and icon), form/schema/type updates to include ss2022_relay_inbound_tags, and includes the field in create/modify/duplicate/bulk payloads. (HostModal contains a duplicated relay block.)
Frontend API Types
dashboard/src/service/api/index.ts
Adds CreateHostSs2022RelayInboundTags and BaseHostSs2022RelayInboundTags types and optional ss2022_relay_inbound_tags properties on CreateHost and BaseHost interfaces.
Localization
dashboard/public/statics/locales/{en,fa,ru,zh}.json
Adds four new hostsDialog keys per language: ss2022RelayInbounds, relaySettings, selectRelayInbounds, clearAllRelayInbounds.

Sequence Diagram

sequenceDiagram
    participant UI as Frontend HostModal
    participant API as Backend API
    participant Validate as HostOperation Validator
    participant Core as CoreHosts PrepareData
    participant Sub as Subscription Handler

    UI->>API: create_host(payload with ss2022_relay_inbound_tags)
    API->>Validate: _validate_ss2022_relay_chain(host)
    Note over Validate: Check tags exist, not self-referential,\nand each maps to a 2022 inbound with password
    Validate-->>API: validation result

    API->>Core: _prepare_subscription_inbound_data(host)
    Note over Core: Resolve relay_passwords by\ncollecting passwords from referenced inbounds
    Core-->>API: SubscriptionInboundData(relay_passwords=[...])

    API->>Sub: detect_shadowsocks_2022(..., relay_passwords)
    Note over Sub: Prepend relay_passwords into\nSS-2022 password chain when applicable
    Sub-->>API: (method, password)

    API-->>UI: host created / updated
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

  • Review relay password collection in app/core/hosts.py for ordering, error handling, and skipped failures.
  • Verify _validate_ss2022_relay_chain() in app/operation/host.py correctly enforces no self-reference and requires 2022 inbounds with passwords.
  • Confirm all subscription handlers consistently pass relay_passwords and that password_to_2022()/detect_shadowsocks_2022() handle empty/null lists.
  • Inspect HostModal.tsx for the duplicated relay settings block (likely unintended).

Poem

🐰 I hop along the relay row,
Passing passwords soft and slow,
From tags to chain they gently flow,
SS‑2022 — onward we go! 🔐✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.37% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: Add SS-2022 relay support" is directly related to and clearly summarizes the main change in the changeset. The pull request comprehensively adds relay support for Shadowsocks-2022 across backend (database migration, models, validation logic, password resolution), API/data models (new relay_passwords field), frontend UI components (relay inbound selection), and localization strings. The title concisely communicates the primary feature being added without unnecessary noise, and a teammate scanning the commit history would immediately understand that this PR introduces relay functionality for SS-2022.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
dashboard/src/components/dialogs/HostModal.tsx (1)

3008-3014: Consider adding an info popover for relay settings.

For consistency with other sections (network, security, transport, etc.), consider adding an info icon with a popover explaining what SS-2022 relay inbounds are and how they work. This would help users understand this feature, especially since it's a new addition.

Example placement:

                  <AccordionTrigger>
                    <div className="flex items-center gap-2">
                      <CircleArrowRight className="h-4 w-4" />
                      <span>{t('hostsDialog.relaySettings')}</span>
                    </div>
                  </AccordionTrigger>
                  <AccordionContent className="px-2">
                    <FormField
                    control={form.control}
                    name="ss2022_relay_inbound_tags"
                    render={({ field }) => (
                      <FormItem>
-                       <FormLabel>{t('hostsDialog.ss2022RelayInbounds')}</FormLabel>
+                       <div className="flex items-center gap-2">
+                         <FormLabel>{t('hostsDialog.ss2022RelayInbounds')}</FormLabel>
+                         <Popover>
+                           <PopoverTrigger asChild>
+                             <Button type="button" variant="ghost" size="icon" className="h-4 w-4 p-0 hover:bg-transparent">
+                               <Info className="h-4 w-4 text-muted-foreground" />
+                             </Button>
+                           </PopoverTrigger>
+                           <PopoverContent className="w-[320px] p-3" side="right" align="start" sideOffset={5}>
+                             <p className="text-[11px] text-muted-foreground">{t('hostsDialog.relaySettings.info')}</p>
+                           </PopoverContent>
+                         </Popover>
+                       </div>
                        <div className="flex flex-col gap-2">

Don't forget to add the corresponding translation keys to your locale files.

dashboard/src/components/hosts/Hosts.tsx (1)

607-607: Inconsistent nullish handling operators.

Line 607 uses the nullish coalescing operator (??), while lines 643 and 750 use the logical OR operator (||). For consistency and correctness, prefer ?? throughout, as it only checks for null or undefined without treating other falsy values (like 0 or '') as nullish.

Apply this diff to standardize the operator:

-        ss2022_relay_inbound_tags: host.ss2022_relay_inbound_tags || [],
+        ss2022_relay_inbound_tags: host.ss2022_relay_inbound_tags ?? [],

Also applies to: 643-643, 750-750

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11d743d and 5c72831.

📒 Files selected for processing (22)
  • app/core/hosts.py (2 hunks)
  • app/db/migrations/versions/4bbb43d025bc_add_ss2022_relay_tags_to_hosts.py (1 hunks)
  • app/db/models.py (1 hunks)
  • app/models/host.py (1 hunks)
  • app/models/subscription.py (1 hunks)
  • app/operation/host.py (5 hunks)
  • app/subscription/base.py (1 hunks)
  • app/subscription/clash.py (1 hunks)
  • app/subscription/links.py (1 hunks)
  • app/subscription/outline.py (1 hunks)
  • app/subscription/singbox.py (1 hunks)
  • app/subscription/xray.py (1 hunks)
  • dashboard/public/statics/locales/en.json (1 hunks)
  • dashboard/public/statics/locales/fa.json (1 hunks)
  • dashboard/public/statics/locales/ru.json (1 hunks)
  • dashboard/public/statics/locales/zh.json (1 hunks)
  • dashboard/src/components/dialogs/HostModal.tsx (2 hunks)
  • dashboard/src/components/hosts/Hosts.tsx (5 hunks)
  • dashboard/src/components/hosts/SortableHost.tsx (1 hunks)
  • dashboard/src/pages/_dashboard._index.tsx (1 hunks)
  • dashboard/src/pages/_dashboard.hosts.tsx (1 hunks)
  • dashboard/src/service/api/index.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
app/operation/host.py (3)
dashboard/src/service/api/index.ts (1)
  • CreateHost (1546-1571)
app/operation/__init__.py (2)
  • check_inbound_tags (155-158)
  • raise_error (40-49)
app/core/manager.py (1)
  • get_inbound_by_tag (80-85)
app/db/models.py (1)
app/db/compiles_types.py (1)
  • StringArray (51-71)
dashboard/src/components/dialogs/HostModal.tsx (4)
dashboard/src/components/ui/accordion.tsx (3)
  • AccordionItem (44-44)
  • AccordionTrigger (44-44)
  • AccordionContent (44-44)
dashboard/src/components/ui/form.tsx (5)
  • FormField (104-104)
  • FormItem (104-104)
  • FormLabel (104-104)
  • FormControl (104-104)
  • FormMessage (104-104)
dashboard/src/components/ui/select.tsx (5)
  • Select (105-105)
  • SelectTrigger (105-105)
  • SelectValue (105-105)
  • SelectContent (105-105)
  • SelectItem (105-105)
app/db/models.py (1)
  • inbounds (196-206)
app/core/hosts.py (1)
app/core/manager.py (1)
  • get_inbound_by_tag (80-85)
🔇 Additional comments (23)
app/subscription/outline.py (1)

28-28: LGTM! Relay password support added.

The addition of relay_passwords parameter using getattr with a None fallback safely handles inbounds without relay configuration.

dashboard/public/statics/locales/ru.json (1)

961-964: LGTM! Translation keys added for relay settings.

The four new Russian translation keys for SS-2022 relay inbound UI elements are correctly placed and align with the feature implementation.

dashboard/src/pages/_dashboard._index.tsx (1)

222-222: LGTM! Default value added for relay inbound tags.

The empty array default for ss2022_relay_inbound_tags is consistent with other array fields in the form and aligns with the new host configuration schema.

dashboard/src/components/hosts/SortableHost.tsx (1)

98-98: LGTM! Relay tags preserved during status toggle.

The addition of ss2022_relay_inbound_tags with a fallback to empty array ensures relay configuration is preserved when toggling the host's disabled state.

app/subscription/clash.py (1)

352-352: LGTM! SS-2022 relay support added to Clash Meta.

The relay password parameter is safely retrieved and passed to the SS-2022 detection logic, consistent with the implementation in other subscription handlers.

dashboard/public/statics/locales/en.json (1)

765-768: LGTM! English translations added for relay settings.

The four new translation keys for SS-2022 relay inbound UI elements are clear, consistent with existing terminology, and properly positioned in the locale file.

app/subscription/links.py (1)

280-280: LGTM! Relay password support added to link generation.

The relay password parameter is properly integrated into the Shadowsocks link builder using the same safe attribute access pattern as other handlers.

app/subscription/singbox.py (1)

260-260: LGTM! SS-2022 relay support added to Sing-box.

The relay password parameter is consistently integrated into the Sing-box Shadowsocks outbound builder, completing support across all subscription formats.

app/subscription/xray.py (1)

417-417: LGTM: Clean integration of relay passwords into SS-2022 detection.

The use of getattr with a None default is appropriate defensive programming for the optional relay_passwords attribute.

app/models/host.py (1)

202-203: LGTM: Well-documented field addition for SS-2022 relay support.

The field definition is clean with appropriate typing, default value, and a clear comment explaining its purpose as an ordered relay chain.

app/models/subscription.py (1)

217-218: LGTM: Proper field definition with correct default factory.

Using Field(default_factory=list) avoids the mutable default argument antipattern, and the documentation clearly explains the ordered relay password chain concept.

dashboard/src/pages/_dashboard.hosts.tsx (1)

152-155: LGTM: Appropriate conditional inclusion of relay tags.

The logic correctly includes ss2022_relay_inbound_tags only when populated, avoiding unnecessary empty arrays in the payload. This is consistent with the optional nature of the field.

dashboard/public/statics/locales/fa.json (1)

645-648: LGTM: Localization keys added for relay settings UI.

The four new translation keys are appropriately placed within the hostsDialog section and align with the relay inbound management feature. The keys are consistent with similar additions in other locale files (en, zh, ru).

app/db/models.py (1)

451-454: LGTM: Database field definition aligns with the data model.

The column definition is consistent with similar fields in the model, using StringArray(256) which properly handles list-to-string serialization. The optional typing and clear comment make the purpose explicit.

app/core/hosts.py (2)

99-108: LGTM: Relay password resolution with graceful error handling.

The logic correctly iterates through relay inbound tags, fetches their configurations, validates they are SS-2022 inbounds with passwords, and builds the ordered relay password chain. The broad except Exception: continue is acceptable here for graceful degradation—invalid or missing relay inbounds are simply skipped rather than failing the entire operation.


228-228: LGTM: Relay passwords properly threaded into subscription data.

The relay_passwords list is correctly included in the SubscriptionInboundData payload, enabling downstream subscription handlers to construct SS-2022 relay chains.

dashboard/public/statics/locales/zh.json (1)

1004-1007: LGTM: Chinese localization for relay settings UI.

The translation keys mirror those added in other locale files (fa, en, ru), ensuring consistent UI labeling across all supported languages for the SS-2022 relay feature.

dashboard/src/components/dialogs/HostModal.tsx (1)

18-18: LGTM!

The CircleArrowRight icon import is correctly added and used in the relay settings accordion trigger on line 3011.

dashboard/src/service/api/index.ts (1)

1544-1571: Relay tag plumbing matches the backend schema.
The new type aliases and optional payload fields line up with the SS2022 relay additions elsewhere, so the generated client remains consistent.

app/subscription/base.py (1)

125-158: Password assembly handles relay chains cleanly.
Gracefully prepending relay PSKs while retaining the original two-part format keeps compatibility and matches the expected SS-2022 layout.

app/operation/host.py (1)

116-130: Relay chain validation looks solid.
Checking existence, avoiding self-reference, and asserting 2022/password requirements should prevent misconfigured relay chains from slipping through.

dashboard/src/components/hosts/Hosts.tsx (1)

150-150: LGTM! Proper typing and validation.

The addition of ss2022_relay_inbound_tags to both the interface and schema is correct. The optional array of strings is appropriately typed and validated.

Also applies to: 404-404

app/db/migrations/versions/4bbb43d025bc_add_ss2022_relay_tags_to_hosts.py (1)

28-30: LGTM! Proper downgrade implementation.

Using batch_alter_table ensures SQLite compatibility when dropping the column.

Comment on lines +3044 to +2771
<Select
value={''}
onValueChange={(value: string) => {
if (!value || value.trim() === '') return
const current = Array.isArray(field.value) ? field.value : []
field.onChange([...current, value])
}}
>
<FormControl>
<SelectTrigger dir={dir} className="w-full py-5">
<SelectValue placeholder={t('hostsDialog.selectRelayInbounds')} />
</SelectTrigger>
</FormControl>
<SelectContent dir={dir} className="bg-background">
{inbounds.map(tag => (
<SelectItem key={tag} value={tag} className="cursor-pointer">
{tag}
</SelectItem>
))}
</SelectContent>
</Select>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add duplicate prevention for relay inbound tags.

The relay tag selection allows adding the same inbound tag multiple times. The status field (lines 637-708) demonstrates the pattern used elsewhere in this file to prevent duplicates by disabling already-selected options.

Apply this diff to prevent duplicate relay tags:

                          <Select
                            value={''}
                            onValueChange={(value: string) => {
                              if (!value || value.trim() === '') return
                              const current = Array.isArray(field.value) ? field.value : []
+                             // Prevent duplicates
+                             if (current.includes(value)) return
                              field.onChange([...current, value])
                            }}
                          >
                            <FormControl>
                              <SelectTrigger dir={dir} className="w-full py-5">
                                <SelectValue placeholder={t('hostsDialog.selectRelayInbounds')} />
                              </SelectTrigger>
                            </FormControl>
                            <SelectContent dir={dir} className="bg-background">
                              {inbounds.map(tag => (
-                               <SelectItem key={tag} value={tag} className="cursor-pointer">
+                               <SelectItem 
+                                 key={tag} 
+                                 value={tag} 
+                                 className="cursor-pointer"
+                                 disabled={field.value?.includes(tag)}
+                               >
                                  {tag}
                                </SelectItem>
                              ))}
                            </SelectContent>
                          </Select>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Select
value={''}
onValueChange={(value: string) => {
if (!value || value.trim() === '') return
const current = Array.isArray(field.value) ? field.value : []
field.onChange([...current, value])
}}
>
<FormControl>
<SelectTrigger dir={dir} className="w-full py-5">
<SelectValue placeholder={t('hostsDialog.selectRelayInbounds')} />
</SelectTrigger>
</FormControl>
<SelectContent dir={dir} className="bg-background">
{inbounds.map(tag => (
<SelectItem key={tag} value={tag} className="cursor-pointer">
{tag}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={''}
onValueChange={(value: string) => {
if (!value || value.trim() === '') return
const current = Array.isArray(field.value) ? field.value : []
// Prevent duplicates
if (current.includes(value)) return
field.onChange([...current, value])
}}
>
<FormControl>
<SelectTrigger dir={dir} className="w-full py-5">
<SelectValue placeholder={t('hostsDialog.selectRelayInbounds')} />
</SelectTrigger>
</FormControl>
<SelectContent dir={dir} className="bg-background">
{inbounds.map(tag => (
<SelectItem
key={tag}
value={tag}
className="cursor-pointer"
disabled={field.value?.includes(tag)}
>
{tag}
</SelectItem>
))}
</SelectContent>
</Select>
🤖 Prompt for AI Agents
In dashboard/src/components/dialogs/HostModal.tsx around lines 3044 to 3064, the
relay inbound Select allows adding the same tag multiple times; update the
SelectItem rendering to detect the current selected tags
(Array.isArray(field.value) ? field.value : []) and disable (or skip) any
SelectItem whose tag is already included so the option cannot be re-selected;
keep the Select value and onValueChange behavior but use the current selection
to set a disabled prop (or filter the inbounds list) so duplicate tags cannot be
added.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
dashboard/src/components/dialogs/HostModal.tsx (1)

3045-3062: Block duplicate relay selection entries.

onValueChange still pushes the chosen tag without checking whether it’s already present, so users can add the same relay inbound repeatedly. This is the same duplicate-selection bug that was highlighted previously and it breaks the assumption that the relay chain is an ordered set of distinct hops. Please add an early return when the tag is already selected and disable the corresponding dropdown option so it can’t be re-picked.

                               if (!value || value.trim() === '') return
                               const current = Array.isArray(field.value) ? field.value : []
-                              field.onChange([...current, value])
+                              if (current.includes(value)) return
+                              field.onChange([...current, value])
 ...
-                              {inbounds.map(tag => (
-                                <SelectItem key={tag} value={tag} className="cursor-pointer">
+                              {inbounds.map(tag => (
+                                <SelectItem
+                                  key={tag}
+                                  value={tag}
+                                  className="cursor-pointer"
+                                  disabled={Array.isArray(field.value) && field.value.includes(tag)}
+                                >
🧹 Nitpick comments (2)
dashboard/public/statics/locales/fa.json (1)

645-648: Minor typography: add space before (SS-2022).

Prefer "ورودی‌های رله (SS-2022)" for readability and consistency with other locales.

dashboard/public/statics/locales/ru.json (1)

961-964: Minor wording for natural Russian.

Consider:

  • ss2022RelayInbounds: "Релейные входы (SS-2022)"
  • relaySettings: "Настройки ретрансляции"
  • selectRelayInbounds: "Выберите релейные входы"
  • clearAllRelayInbounds: "Очистить все релейные входы"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c72831 and 7cf18af.

📒 Files selected for processing (22)
  • app/core/hosts.py (2 hunks)
  • app/db/migrations/versions/4bbb43d025bc_add_ss2022_relay_tags_to_hosts.py (1 hunks)
  • app/db/models.py (1 hunks)
  • app/models/host.py (1 hunks)
  • app/models/subscription.py (1 hunks)
  • app/operation/host.py (5 hunks)
  • app/subscription/base.py (1 hunks)
  • app/subscription/clash.py (1 hunks)
  • app/subscription/links.py (1 hunks)
  • app/subscription/outline.py (1 hunks)
  • app/subscription/singbox.py (1 hunks)
  • app/subscription/xray.py (1 hunks)
  • dashboard/public/statics/locales/en.json (1 hunks)
  • dashboard/public/statics/locales/fa.json (1 hunks)
  • dashboard/public/statics/locales/ru.json (1 hunks)
  • dashboard/public/statics/locales/zh.json (1 hunks)
  • dashboard/src/components/dialogs/HostModal.tsx (2 hunks)
  • dashboard/src/components/hosts/Hosts.tsx (5 hunks)
  • dashboard/src/components/hosts/SortableHost.tsx (1 hunks)
  • dashboard/src/pages/_dashboard._index.tsx (1 hunks)
  • dashboard/src/pages/_dashboard.hosts.tsx (1 hunks)
  • dashboard/src/service/api/index.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • dashboard/src/components/hosts/SortableHost.tsx
  • dashboard/public/statics/locales/en.json
  • app/core/hosts.py
  • app/subscription/base.py
  • dashboard/src/service/api/index.ts
  • app/subscription/outline.py
  • app/subscription/links.py
  • app/subscription/clash.py
  • app/models/subscription.py
  • dashboard/src/pages/_dashboard._index.tsx
  • app/operation/host.py
🧰 Additional context used
🧬 Code graph analysis (2)
app/db/models.py (1)
app/db/compiles_types.py (1)
  • StringArray (51-71)
dashboard/src/components/dialogs/HostModal.tsx (4)
dashboard/src/components/ui/accordion.tsx (3)
  • AccordionItem (44-44)
  • AccordionTrigger (44-44)
  • AccordionContent (44-44)
dashboard/src/components/ui/form.tsx (5)
  • FormField (104-104)
  • FormItem (104-104)
  • FormLabel (104-104)
  • FormControl (104-104)
  • FormMessage (104-104)
dashboard/src/components/ui/select.tsx (5)
  • Select (105-105)
  • SelectTrigger (105-105)
  • SelectValue (105-105)
  • SelectContent (105-105)
  • SelectItem (105-105)
app/db/models.py (1)
  • inbounds (196-206)
🔇 Additional comments (4)
dashboard/public/statics/locales/zh.json (1)

1004-1007: LGTM — keys and phrasing are consistent.

app/subscription/singbox.py (1)

252-261: No issues found — relay_passwords properly propagated across all call sites.

Verification confirms:

  • The detect_shadowsocks_2022 signature includes relay_passwords: list[str] | None = None parameter (base.py:151)
  • All 5 call sites (xray.py, outline.py, singbox.py, links.py, clash.py) consistently pass getattr(inbound, "relay_passwords", None)
  • The relay_passwords is correctly used in the password_to_2022() invocation (base.py:156)
  • The singbox.py changes are correct and consistent with existing implementations
app/subscription/xray.py (1)

409-418: LGTM — relay_passwords integration verified end-to-end.

All subscription modules (xray, singbox, clash, outline, links) call detect_shadowsocks_2022 with relay_passwords as the final parameter in identical order. Function signature matches all call sites. SubscriptionInboundData model includes relay_passwords field. Implementation in password_to_2022 correctly chains relay passwords before user password. Ordering preserved: DB → model → core/hosts → subscription.

app/db/migrations/versions/4bbb43d025bc_add_ss2022_relay_tags_to_hosts.py (1)

19-25: Schema mismatch and loss of ordering — use JSON (list) to match ORM and feature semantics.

String(2048) can’t safely round‑trip an ordered string list and diverges from the Python type. Store as JSON (list) to preserve order.

-from alembic import op
-import sqlalchemy as sa
+from alembic import op
+import sqlalchemy as sa

 def upgrade() -> None:
-    op.add_column('hosts', sa.Column('ss2022_relay_inbound_tags', sa.String(length=2048), nullable=True))
+    bind = op.get_bind()
+    # Prefer JSON to preserve order; fallback to text on dialects without native JSON
+    if bind.dialect.name in ("postgresql", "mysql", "sqlite"):
+        op.add_column(
+            "hosts",
+            sa.Column("ss2022_relay_inbound_tags", sa.JSON(none_as_null=True), nullable=True),
+        )
+    else:
+        op.add_column("hosts", sa.Column("ss2022_relay_inbound_tags", sa.Text(), nullable=True))

 def downgrade() -> None:
-    with op.batch_alter_table('hosts') as batch_op:
-        batch_op.drop_column('ss2022_relay_inbound_tags')
+    with op.batch_alter_table("hosts") as batch_op:
+        batch_op.drop_column("ss2022_relay_inbound_tags")

Likely an incorrect or invalid review comment.

Comment on lines +202 to +203
# Ordered relay chain for Shadowsocks 2022: list of inbound tags to be used as relays
ss2022_relay_inbound_tags: list[str] | None = Field(default=None)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve order + validate relay tags.

Add a validator to trim, drop empties, enforce max length (≤256), and de‑dup while preserving order. Also consider verifying tags exist among known inbounds.

 class CreateHost(BaseHost):
+    @field_validator("ss2022_relay_inbound_tags", mode="after")
+    def validate_ss2022_relay_inbound_tags(cls, v):
+        if v is None:
+            return None
+        seen = set()
+        cleaned: list[str] = []
+        for tag in v:
+            tag = (tag or "").strip()
+            if not tag:
+                continue
+            if len(tag) > 256:
+                raise ValueError("Relay inbound tag length must be ≤ 256")
+            if tag not in seen:
+                seen.add(tag)
+                cleaned.append(tag)
+        return cleaned

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
app/models/host.py around lines 202 to 203, the ss2022_relay_inbound_tags field
needs a Pydantic validator that trims whitespace from each tag, drops empty
strings, enforces a max length of 256 characters per tag (raise ValueError on
violation), and de-duplicates tags while preserving their original order;
implement a @validator("ss2022_relay_inbound_tags", pre=True, always=False) that
returns None if input is None, otherwise processes the list accordingly and
raises clear errors for invalid items, and — if feasible in this model — verify
each tag exists among known inbounds (e.g., compare against self.inbounds or a
passed-in list of inbound tags) and raise a validation error for unknown tags.

@ImMohammad20000
Copy link
Contributor

ImMohammad20000 commented Oct 31, 2025

Do you test this?
Is it work currectly?

@ImMohammad20000 ImMohammad20000 changed the title Add SS-2022 relay support feat: Add SS-2022 relay support Oct 31, 2025
@ImMohammad20000 ImMohammad20000 added the enhancement New feature or request label Oct 31, 2025
app/db/models.py Outdated
ech_config_list: Mapped[Optional[str]] = mapped_column(String(512), default=None)
# Ordered relay inbound tags for Shadowsocks 2022
ss2022_relay_inbound_tags: Mapped[Optional[list[str]]] = mapped_column(
StringArray(256), default=None, nullable=True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you set 256 length here but in migration is 2048, choose one

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

256 is the length of array item, which is the same as "inbound_tag". While 2048 is the total length, including all items and separators.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, StringArray type don't work this way

status: set[UserStatus] | None = Field(default_factory=set)
ech_config_list: str | None = Field(default=None)
# Ordered relay chain for Shadowsocks 2022: list of inbound tags to be used as relays
ss2022_relay_inbound_tags: list[str] | None = Field(default=None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

total length should be checked here

return await get_hosts(db=db)

async def _validate_ss2022_relay_chain(self, host: CreateHost) -> None:
tags = getattr(host, "ss2022_relay_inbound_tags", None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you have a pydantic model in here and you can just use the values directly, no need for getattr

if not cfg or not cfg.get("is_2022"):
await self.raise_error(f"{tag} is not a Shadowsocks 2022 inbound", 400)
if not cfg.get("password"):
await self.raise_error(f"{tag} has no inbound password configured", 400)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we need an extra check in here. both inbound should be in same core config if im not wrong

@Max-Sum
Copy link
Author

Max-Sum commented Nov 1, 2025

Do you test this?
Is it work currectly?

Yes I have deployed it on my server.
However there are still some works to be done. A relay inbound have to be set as excluded in config but it have be to be selected in UI. So an API to get excluded inbounds are required.

@M03ED
Copy link
Contributor

M03ED commented Nov 1, 2025

also why target branch is main ? change it to next

@ImMohammad20000 ImMohammad20000 changed the base branch from main to dev November 2, 2025 10:27
@M03ED
Copy link
Contributor

M03ED commented Nov 4, 2025

we wait few days for changes, after that if there is no response i close this pr

@Max-Sum
Copy link
Author

Max-Sum commented Nov 4, 2025

we wait few days for changes, after that if there is no response i close this pr

Will push changes this week. I was busy working on other projects.

return set[str](v for v in value.split(",") if v)
return set[str](value)

class StringList(TypeDecorator):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you adding an extra model for no reason?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StringArray is not order-preserving because it decodes to set instead of list.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are already a few uses of StringArray and they uses set[str]. So it's better to keep it there and create a new one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why order-preserving is important here when inbounds are excluded?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relay servers are chained to provide a path to final server. The order reflects how the packets go through, therefore order is inherently important.
Inbounds that act as relay need to be excluded because the "client" field in them is filled with the information of next hop instead of the panel managed clients. User will need to fill them manually.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like my description on the PR is not clear enough to illustrate how relay works.
I will put a working example later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants