Skip to content

feat: add rematch outbound type and REMATCH-NAME rule type#2862

Merged
wwqgtxx merged 7 commits into
MetaCubeX:Alphafrom
Medium1992:feature/loopback-proxy-rematch
Jun 12, 2026
Merged

feat: add rematch outbound type and REMATCH-NAME rule type#2862
wwqgtxx merged 7 commits into
MetaCubeX:Alphafrom
Medium1992:feature/loopback-proxy-rematch

Conversation

@Medium1992

@Medium1992 Medium1992 commented Jun 2, 2026

Copy link
Copy Markdown

Summary

This PR introduces a new outbound type: rematch.

The design is conceptually similar to Xray's outbound loopback routing, allowing a connection to re-enter the routing engine instead of immediately establishing an outbound connection.

The implementation was developed with the help of Codex based on the routing behavior described in the issue and refined through local testing.

The primary goal is to reduce proxy group complexity while preserving routing flexibility.

Without loopback, users often need large proxy groups containing many routing-specific choices. With rematch, a user can expose only a small number of high-level selections while still applying arbitrary routing logic afterwards. This makes it possible to build a single convenient proxy group with minimal UI buttons while retaining full rule-based routing capabilities.

loopback is a virtual outbound that does not establish a network connection. Instead, when selected, it sends the connection back for another rule matching pass.

During rematching, it can optionally:

  • override REMATCH-NAME;
  • use a specific sub-rule set.

As a result, selecting an item inside a proxy group no longer has to determine the final outbound. It can instead switch routing context and let rules decide the final path.

Motivation

Currently, when traffic is routed to a proxy group and the user selects an item inside that group, the selected proxy becomes the final outbound.

This limits the ability to create compact user-facing proxy groups while keeping sophisticated routing policies underneath.

loopback allows a proxy group selection to act as a routing context switch rather than a final outbound, enabling:

  • simpler proxy group layouts;
  • fewer user-facing selections;
  • reusable routing policies;
  • additional rule evaluation after proxy selection.

Example

proxy-groups:
  - name: GLOBAL
    type: select
    proxies:
      - RM
      - DIRECT

proxies:
  - name: RM
    type: rematch
    target-rematch-name: RM_TEST

rules:
  - REMATCH-NAME,RM_TEST,SPECIAL_GROUP
  - IN-NAME,mixed-in,GLOBAL
  - MATCH,DIRECT

Flow:

  1. Traffic enters through mixed-in.
  2. Rules route it to GLOBAL.
  3. User selects RM.
  4. RM rematches the connection with REMATCH-NAME=RM_TEST.
  5. Rules are evaluated again and determine the final outbound.

Configuration

target-rematch-name

Overrides metadata.RematchName before rematching.

target-sub-rule

Uses the specified sub-rule set for the second routing pass.

target-rematch-name + target-sub-rule

Both options can be used together.

Validation

A loopback outbound must define at least one of:

  • target-rematch-name
  • target-sub-rule

Otherwise it is considered invalid.

Loop Protection

Only one loopback rematch is allowed per connection.

If rematching selects another loopback outbound, the connection is rejected and a warning is logged, preventing infinite routing loops.

@Medium1992

Copy link
Copy Markdown
Author

@wwqgtxx Hello.
Please let me know if there are any plans to implement this type of proxies?
If not, or if my PR isn't suitable, please let me know so you can close it, or I'll close it myself. Most of the ideas I've suggested in the “Ideas” section are ignored once they're moved to “Issues.” Why is that?

@wwqgtxx

wwqgtxx commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

We neither have the time nor the inclination to expend the effort to review PRs/issues primarily generated by AI.

The reason we haven't closed the PR is that the idea itself is quite interesting. However, the code implementation is terrible.

You can simply leave once the code generated by AI is merged, while we have to continuously pay the maintenance costs for these inelegant implementations.

Therefore, unless we immediately deem the AI-generated code "elegant and maintainable enough," it will most likely remain shelved.

@Medium1992

Copy link
Copy Markdown
Author

We neither have the time nor the inclination to expend the effort to review PRs/issues primarily generated by AI.

The reason we haven't closed the PR is that the idea itself is quite interesting. However, the code implementation is terrible.

You can simply leave once the code generated by AI is merged, while we have to continuously pay the maintenance costs for these inelegant implementations.

Therefore, unless we immediately deem the AI-generated code "elegant and maintainable enough," it will most likely remain shelved.

I didn't try to hide the fact that it was written using AI; I was just hoping for some feedback—maybe some comments on what to fix, or perhaps you could make the corrections yourselves.

Thanks for the feedback. I'm glad you liked the idea, and of course I'm looking forward to seeing it implemented.

@Medium1992

Copy link
Copy Markdown
Author

We neither have the time nor the inclination to expend the effort to review PRs/issues primarily generated by AI.

The reason we haven't closed the PR is that the idea itself is quite interesting. However, the code implementation is terrible.

You can simply leave once the code generated by AI is merged, while we have to continuously pay the maintenance costs for these inelegant implementations.

Therefore, unless we immediately deem the AI-generated code "elegant and maintainable enough," it will most likely remain shelved.

I tried to simplify the implementation and remove the parts that looked like a proof-of-concept workaround.

The latest commit is:

e2ef6f99 refactor: simplify loopback routing

The old implementation used a global callback bridge through constant/loopback.go so that outbound/loopback could call back into tunnel. I agree this was not a good design, so that file and the callback mechanism were removed.

Current implementation overview:

1. Loopback outbound

Added:

adapter/outbound/loopback.go

loopback is treated as a virtual outbound. It stores only:

  • in-name
  • sub-rule
  • a C.Tunnel reference passed through the existing BasicOption.TunnelForAPI

The core method is:

func (l *Loopback) ApplyLoopback(metadata *C.Metadata)

It only updates routing metadata:

metadata.InName = l.inName
metadata.SpecialRules = l.subRule

So the actual rematching still uses the existing rule matching flow.

For direct DialContext / ListenPacketContext calls, mainly health-check / url-test, loopback does not dial by itself. It applies its metadata changes and then asks the tunnel to resolve the actual outbound:

proxy, _, err := l.tunnel.ResolveProxy(metadata)

Then it dials through the resolved proxy.

There is also a self-resolution guard to avoid url-test recursion if the resolved chain still points back to the same loopback proxy.

2. Tunnel resolver

Changed:

constant/tunnel.go

Added one method to the existing C.Tunnel interface:

ResolveProxy(metadata *Metadata) (Proxy, Rule, error)

Implemented in:

tunnel/tunnel.go

func (t tunnel) ResolveProxy(metadata *C.Metadata) (C.Proxy, C.Rule, error) {
    return resolveMetadata(metadata)
}

This is used by loopback only for direct dial paths such as health checks. It replaces the previous global callback approach.

3. Rule matcher integration

Changed:

tunnel/tunnel.go

The normal traffic path is handled directly inside match().

When a rule or proxy group resolves to a chain containing C.Loopback, tunnel does not dial it. Instead it:

  1. checks for loopback cycles;
  2. records the virtual loopback hop for connection chains;
  3. calls ApplyLoopback(metadata);
  4. restarts the existing rule matching loop.

So the second pass still uses the existing logic:

getRules(metadata)
rule.Match(metadata, helper)

This means sub-rule support is not implemented separately. It reuses the existing metadata.SpecialRules and getRules() behavior.

4. Loop protection

Loop protection is now based on visited loopback names, not a numeric hop limit.

This allows a chain like:

LB1 -> LB2 -> final proxy

but rejects:

LB1 -> LB2 -> LB1

The repeated loopback is dropped with REJECT-DROP.

5. Config validation

Changed:

config/config.go

Loopback validation is strict now.

A loopback proxy must define at least one of:

  • in-name
  • sub-rule

If sub-rule is set, it must exist in sub-rules.

There is no longer any runtime fallback, disabled state, or mutation of the loopback config after parsing.

6. Connection chains

Changed:

constant/metadata.go
tunnel/tunnel.go

A small internal field was added:

LoopbackChain []string `json:"-"`

This is only used to append the virtual loopback hop to connection chains before logging/statistics, so the API/UI can show that the connection passed through the loopback step.

It does not affect routing or matching.

7. Parser changes

Changed:

adapter/parser.go

Added parsing for:

type: loopback

Also, loopback is not wrapped with NewAutoCloseProxyAdapter.

Reason: loopback is virtual and does not own network resources. The tunnel matcher also needs access to its ApplyLoopback behavior.

8. Tests

I ran:

go test ./...

and it passes locally.

Current trade-offs / possible review points

The implementation still changes C.Tunnel by adding ResolveProxy. I chose this instead of a global callback because it makes the dependency explicit and keeps the rematching logic in the tunnel/rule-matching layer.

If this direction is still not acceptable, I would appreciate guidance on where you would prefer the metadata-to-proxy resolving hook to live.

@wwqgtxx wwqgtxx force-pushed the Alpha branch 3 times, most recently from f513c49 to d67572b Compare June 12, 2026 02:06
@wwqgtxx

wwqgtxx commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

This implementation is still too complex.

First, Loopback itself doesn't need to support real DialContext and ListenPacketContext, because performing URL tests on them is meaningless. Similarly, ResolveProxy doesn't need to be added.

Second, we don't need a separate ApplyLoopback function, because the previous two functions already take *C.Metadata as input. Therefore, we only need to return a nopConn like Reject, the difference being that we modify the metadata before returning, and then call one of them in the tunnel's match function.

Third, since re-matching is initiated within the tunnel's match function, it can itself record whether multiple re-matches have occurred, so there's no need to implement containsSelf on the outbound side.

@Medium1992

Copy link
Copy Markdown
Author

This implementation is still too complex.

First, Loopback itself doesn't need to support real DialContext and ListenPacketContext, because performing URL tests on them is meaningless. Similarly, ResolveProxy doesn't need to be added.

Second, we don't need a separate ApplyLoopback function, because the previous two functions already take *C.Metadata as input. Therefore, we only need to return a nopConn like Reject, the difference being that we modify the metadata before returning, and then call one of them in the tunnel's match function.

Third, since re-matching is initiated within the tunnel's match function, it can itself record whether multiple re-matches have occurred, so there's no need to implement containsSelf on the outbound side.

Thanks, I simplified the implementation according to your suggestion.

Latest commit:

8ba1fa09 refactor: reduce loopback rematch plumbing

Current design:

  • loopback is a virtual outbound.
  • It does not resolve or dial the final proxy itself.
  • DialContext / ListenPacketContext only update *C.Metadata and return nopConn / nopPacketConn.
  • tunnel.match() owns the rematch flow and loop detection.
  • The previous ResolveProxy hook was removed.
  • The loopback URL-test resolving logic was removed.

Routing flow:

rule/group selects loopback
-> tunnel.match() detects C.Loopback in the selected chain
-> calls loopback DialContext/ListenPacketContext to update metadata
-> restarts matching with updated InName / SpecialRules
-> returns the final non-loopback proxy

Loop protection:

  • tunnel.match() records visited loopback proxy names during the rematch chain.
  • If the same loopback is selected again, the connection is dropped with REJECT-DROP.

One UX question:

I removed URL-test support for loopback as suggested, because loopback is not a real outbound.

The original reason I tried to support it was UX: users often expect proxy-group buttons to show latency numbers. The intended use case is to have fewer buttons on clients, especially mobile clients, where one loopback button can represent a routing profile handled by rules. Without URL-test numbers, users may think that the button is broken even if routing works correctly.

Do you prefer loopback to remain untestable because it is virtual, or would you consider a separate lightweight way to expose the health of the resolved outbound later?

@wwqgtxx wwqgtxx force-pushed the feature/loopback-proxy-rematch branch from 8ba1fa0 to 9183e38 Compare June 12, 2026 11:05
@wwqgtxx wwqgtxx changed the title Feature/loopback proxy rematch rules feat: add rematch proxy to rematch rules Jun 12, 2026
@wwqgtxx wwqgtxx changed the title feat: add rematch proxy to rematch rules feat: add rematch outbound type to rematch rules Jun 12, 2026
@wwqgtxx wwqgtxx changed the title feat: add rematch outbound type to rematch rules feat: add rematch outbound type and REMATCH-NAME rule type Jun 12, 2026
@wwqgtxx

wwqgtxx commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

We ultimately determined that directly modifying the IN-NAME field was not optimal, as it would overwrite the original inbound information. Instead, we provided a new REMATCH-NAME rule for matching.

@Medium1992

Copy link
Copy Markdown
Author

We ultimately determined that directly modifying the IN-NAME field was not optimal, as it would overwrite the original inbound information. Instead, we provided a new REMATCH-NAME rule for matching.

Thanks, this makes sense.

I updated my test config to use target-rematch-name + REMATCH-NAME and will test this version locally.

One small note: the debug log still says in-name here:

log.Debugln("[Rule] rematch proxy %s update metadata to in-name=%q sub-rule=%q", rematchProxy.Name(), metadata.InName, metadata.SpecialRules)

Should it now use rematch-name / metadata.RematchName instead?

@Medium1992

Copy link
Copy Markdown
Author

I tested it, and everything works as you intended. And yes, with REMATCH-NAME, my original logic for building routing rules has changed slightly. But I like everything—thanks.

@wwqgtxx wwqgtxx merged commit ea19cda into MetaCubeX:Alpha Jun 12, 2026
icy37785 pushed a commit to icy37785/mihomo that referenced this pull request Jun 27, 2026
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.

2 participants