Skip to content

feat: group listener ports (binds inbound ports to proxy groups)#57

Open
sodiseng wants to merge 2 commits into
SubBoost:mainfrom
sodiseng:feat/group-listeners
Open

feat: group listener ports (binds inbound ports to proxy groups)#57
sodiseng wants to merge 2 commits into
SubBoost:mainfrom
sodiseng:feat/group-listeners

Conversation

@sodiseng

Copy link
Copy Markdown

Stacked on #56 — this branch includes the proxy-providers commit; once #56 merges, the diff here reduces to the group-listener changes only.

依赖 #56(stacked PR):本分支包含 proxy-providers 的提交,#56 合并后本 PR 的 diff 只剩分组监听部分。

What

Bind local inbound ports to existing proxy groups, generating mihomo listeners entries with proxy: <group name> — traffic entering that port goes directly out through the bound policy group.

给已存在的策略组绑定本地 inbound 监听端口,生成 mihomo listeners 条目(proxy: 组名):进入该端口的流量直接走绑定的策略组出站。

Features / 功能

  • core: GroupListenerBinding {id, target, port} model; normalization (trim, drop invalid, dedupe by target — one port per group); generator merges entries after base-config and node listeners, validating target against all policy groups and deduping ports against node listeners / mixed-port / each other. Fixed type: mixed, listen: 0.0.0.0, udp: true, auto names group-in-{i} (mirrors the existing node listener-ports convention).
  • UI: a collapsible "group listener management" card in the proxy groups section — rows of group select + port input, with conflict hints naming the conflicting side, invalid-target placeholder, and the same first-use exposure warning as node listener ports. Bound groups show a :{port} badge on their cards (module / custom / dialer / provider group cards).
  • Persistence: store actions, subscription save/load, auth handoff. Not added to the template schema (same convention as node listenerPorts).

mihomo reference

Listener proxy field ("使用出站代理或策略组直接出站") per https://wiki.metacubex.one/config/inbound/listeners/ (mixed listener fields: name/type/port/listen/udp).

Tests

  • 1136 unit tests pass (vitest), tsc clean, eslint --max-warnings=0 clean.
  • New tests cover normalization, generator validation/dedupe, merge order with base-config listeners, UI card interactions, and serialization round-trip.

- Per-source provider settings: custom key, attach mode (grouped/inline/bare),
  emoji group name, node filter regex; persisted end-to-end (store, handoff,
  subscription save/load, server-side saved-sources normalization)
- Grouped mode generates an airport group (select + use) that joins all
  manual-select groups as a regular member: excludable, sortable, and
  manually attachable to any group via new provider member refs
- Fixed provider defaults (interval, health-check), provider path follows key
- UI: inline status bar on URL source editors, provider settings in advanced
  dialog, read-only airport group card, provider members in group editor,
  airport groups in visual preview
Bind local inbound ports to existing proxy groups, generating mihomo
listeners entries (proxy: <group name>, fixed mixed/0.0.0.0/udp).

- core: GroupListenerBinding model, normalization (dedupe by target),
  generator merge with target/port/name validation against node
  listeners and mixed-port
- UI: group listener management card in the proxy groups section
  (group select + port input, conflict hints, exposure warning),
  port badges on bound group cards
- store actions + subscription save/load + auth handoff serialization

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0862e5d5a4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +50 to +52
...(opts.customProxyGroups ?? [])
.map((group) => (typeof group.name === "string" ? group.name.trim() : ""))
.filter(Boolean),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Filter disabled groups before reserving provider names

When a disabled custom proxy group has the same name as a grouped provider's requested group name, this helper still reserves that disabled name, but generateClashConfig builds the reserved custom set from only enabled groups. Panels that use buildProviderGroupInfo can therefore show/select a suffixed provider group such as ✈️ Air 2, while the generated YAML actually creates ✈️ Air; selecting the displayed name for features like group listeners then points at a group that does not exist in the output.

Useful? React with 👍 / 👎.

if (list.length === 0) return [] as Array<Record<string, unknown>>;

const validTargets = new Set(availablePolicyTargets);
const usedPorts = new Set<number>(Array.isArray(listeners) ? listeners.map((l) => l.port) : []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include base listeners when rejecting duplicate ports

When the base YAML already declares a listener on the same port as a new group listener, usedPorts only contains generated node-listener ports and mixed-port. The base listeners are merged back in later, so the final YAML can contain two listener entries with the same port, causing the client to fail binding that inbound; seed this set from baseTopLevelPatch.listeners as well before accepting group listener ports.

Useful? React with 👍 / 👎.

Comment on lines +364 to +366
const groupedAttachments = groupedProviderKeys
.map((key) => attachmentByKey.get(key))
.filter((attachment): attachment is ProxyProviderAttachment => Boolean(attachment));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve attachment order for grouped providers

Grouped provider names are assigned after groupedProviderKeys has been derived from the alphabetically sorted provider keys, while the UI/provider plan computes those same names in source attachment order. If two grouped providers request the same group name, or collide with a reserved name, the suffixes can be assigned to different provider keys in the UI versus the generated YAML, so advanced member selections or group listeners saved against one provider key end up pointing at a different airport group.

Useful? React with 👍 / 👎.

Comment on lines +531 to +532
if (typeof config.mixedPort === "number" && Number.isInteger(config.mixedPort)) {
usedPorts.add(config.mixedPort);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reserve mixed-port from the merged base YAML

When mixed-port is set only in explicit base/DNS YAML, config.mixedPort still holds the default/user-config value, so this check reserves the wrong port. A group listener using the YAML-defined mixed-port is accepted and then merged into the final config alongside that same top-level port, which makes the client fail to bind one of the inbounds.

Useful? React with 👍 / 👎.

const customKey = typeof item.providerKey === "string" ? item.providerKey.trim() : "";
const key = customKey || defaultKey;
// key 重复时跳过后者(与旧行为一致);UI 层负责重复提示
if (Object.prototype.hasOwnProperty.call(providers, key)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prevent duplicate provider keys from dropping sources

When two URL sources are given the same custom providerKey (or one custom key collides with another source's default key), this branch silently skips the later provider. The new UI exposes free-form key editing but does not block or warn about duplicates, so a user can enable proxy-providers for multiple subscriptions and have one omitted from the generated YAML entirely.

Useful? React with 👍 / 👎.

@Ryson-32

Copy link
Copy Markdown
Contributor

感谢你实现策略组监听端口,这个功能方向我们希望保留,也愿意继续 review。

不过当前 #57 stacked on #56,而我们目前的产品决定是不引入 #56 的 proxy-providers 扩展。因此按现状无法直接合并。能否请你把 #57 调整为一个基于最新 ryan/dev、不依赖 #56 的独立改动,并按下面的范围优化?

功能范围

  • 只保留“策略组监听端口”,不包含 provider 接入模式、provider 机场组或 provider 组监听。
  • 内置分流组、自定义分流组和中转组都需要支持。
  • 每个策略组最多配置一个 mixed 监听端口。

UI 调整

目前单独的“分组监听管理”区域会继续增加页面密度,希望改为:

  • 将代理组行头现有的类型按钮升级为“高级设置”按钮,保持原来的图标尺寸和位置。
  • 点击后打开统一设置弹窗,在弹窗内配置:
    • 代理组类型;
    • 负载均衡策略(仅负载均衡类型显示);
    • 是否启用监听端口;
    • 监听端口。
  • 主行不要显示 自动测速 · :7891:7891 等额外文字,也不要保留单独的端口 badge。
  • 存在非默认类型或监听配置时,设置按钮可以使用轻量高亮或状态点提示,但不要继续挤占行头文字空间。
  • 设置弹窗使用“保存 / 取消”模式;取消时不能留下部分修改。
  • 原有行内展开的筛选条件、成员管理和分流规则保持不变,不移动到弹窗。

状态与生成逻辑

  • 监听目标不要保存策略组显示名称,应保存稳定 ID,至少区分内置组、自定义组和中转组。策略组改名后监听必须继续有效。
  • 代理组关闭时保留监听配置,但暂停生成对应 listener;重新启用后恢复生成。
  • 目标只允许真实策略组,不能把节点、DIRECTREJECT 当成策略组。
  • 默认使用 127.0.0.1,不要默认暴露到 0.0.0.0。如果提供“允许局域网访问”,应由用户明确开启并显示安全提示。
  • 端口冲突检查需要覆盖:
    • 最终生效的 mixed-port,包括基础 YAML 中的覆盖值;
    • 节点监听端口;
    • 其他策略组监听端口;
    • 基础 YAML 中已有的 listeners
  • 无效目标或端口冲突不能静默跳过,应在 UI 中明确提示并阻止保存或生成。

测试

希望补充以下回归场景:

  • 内置组、自定义组和中转组分别生成 listener;
  • 策略组改名后监听仍有效;
  • 关闭后暂停生成、重新启用后恢复;
  • 与基础 YAML mixed-port / listeners 冲突;
  • 删除监听目标后的明确错误;
  • 保存、重新编辑和订阅配置恢复;
  • 窄屏下弹窗和代理组主行不重叠、不产生横向滚动。

如果你愿意按这个范围调整,我们会优先继续 review #57。功能方向本身很有价值,感谢你的贡献。

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants