Skip to content

feat(outbound): introduce generic plugin outbound and dynamic handler registry#6298

Open
hossinasaadi wants to merge 5 commits into
XTLS:mainfrom
hossinasaadi:feat/plugin-outbound
Open

feat(outbound): introduce generic plugin outbound and dynamic handler registry#6298
hossinasaadi wants to merge 5 commits into
XTLS:mainfrom
hossinasaadi:feat/plugin-outbound

Conversation

@hossinasaadi

Copy link
Copy Markdown
Contributor

Introduce a generic Plugin Outbound Protocol and a dynamic Handler Registry to Xray-core. This enables embedding parent applications (like mobile app wrappers) to register custom, packet-preserving network tunnels (e.g. ICMP tunnels, DNS/custom VPN transports) dynamically at runtime, avoiding localhost socket overhead and port collisions.

Proposal: #6297

{
  "outbounds": [
    {
      "tag": "proxy",
      "protocol": "plugin",
      "settings": {
        "name": "customtunnel",
        "params": {
          "password": "je101kgas",
          "target": "1.1.1.1",
          "raw_mode": true
        }
      }
    }
  ]
}

Wrapper Registration & Initialization

import "github.com/xtls/xray-core/proxy/plugin"

// 1. Listen for plugin instantiations
plugin.SetOnPluginRegistered(func(tag string, name string, params string) {
	log.Printf("Plugin '%s' registered with tag '%s'. Params JSON: %s", name, tag, params)
	if name == "customtunnel" {
		go startCustomTunnel(tag, params)
	}
})

// 2. Register the processing handler
plugin.RegisterHandler("customtunnel", func(ctx context.Context, dest v2net.Destination, link *transport.Link) error {
	conn, err := customtunnel.Dial(ctx, dest)
	if err != nil { return err }
	defer conn.Close()

	requestFunc := func() error { return buf.Copy(link.Reader, buf.NewWriter(conn)) }
	responseFunc := func() error { return buf.Copy(buf.NewReader(conn), link.Writer) }
	return task.Run(ctx, requestFunc, responseFunc)
})

@RPRX

@Fangliding

Copy link
Copy Markdown
Member

1 最适合给外部注册的接口是 finalmask 之前就提到过 我看还有一堆乱七八糟的counter包装
2 json.RawMessage 就是 []byte 它有直接的类型 你还把它转成了string 声明到proto里

@hossinasaadi

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback!

Fixed. Updated params in the proto from string to bytes.

  1. The most suitable interface for external registration is finalmask. I've mentioned this before, but I've also seen a bunch of other messy counter wrappers.

finalmask and the proposed plugin operate at different layers. finalmask is a transport-layer wrapper, it takes an existing net.Conn and transforms the bytes flowing through it.

The proposed plugin is a full outbound handler, it owns the entire connection lifecycle, receives traffic from Xray, establishes communication however it wants (e.g. ICMP tunnel), and manages the full request/response flow.

The statistics gap exists for the same reason: plugins do not go through dialer.Dial(), which is where Xray normally wraps connections with stat.CounterConnection.

@Fangliding

Fangliding commented Jun 9, 2026

Copy link
Copy Markdown
Member

你列举的icmp/dns隧道都是finalmask实现的 一个vless出站+kcp传输+你这里提到的自定义基本可以满足其他需求

@hossinasaadi

Copy link
Copy Markdown
Contributor Author

你列举的icmp/dns隧道都是finalmask实现的 一个vless出站+kcp传输+你这里提到的自定义基本可以满足其他需求

I agree that many tunneling use cases can already be implemented with FinalMask, and that’s a valuable capability.

However, the goal of this proposal is not to replace FinalMask. The goal is to provide a generic integration point for applications embedding Xray-core.

With FinalMask, the transport still needs to fit into Xray’s transport layer and connection model. The proposed plugin allows the host application to completely own the transport implementation and lifecycle while still benefiting from Xray’s routing, balancers, DNS, policies, and statistics.

In other words, this proposal is less about supporting a specific protocol like ICMP or DNS tunneling, and more about allowing external, application-defined transports to integrate with Xray without requiring a fork, localhost proxy, or upstream protocol implementation.

@RPRX

RPRX commented Jun 9, 2026

Copy link
Copy Markdown
Member

又想了下一旦有了这东西像是 v2rayNG 第一时间就会接个 AnyTLS 甚至 Snell 进来,烦

@hossinasaadi

Copy link
Copy Markdown
Contributor Author

又想了下一旦有了这东西像是 v2rayNG 第一时间就会接个 AnyTLS 甚至 Snell 进来,烦

I understand the concern. My intention is not to encourage adding specific protocols such as AnyTLS or Snell to Xray-core.

Today, if developers want to add their own transport or protocol, they usually have to modify Xray-core or maintain a fork. With this proposal, they can keep using the official Xray-core and extend it externally.

The biggest benefit is that custom protocols can still use Xray’s existing features such as routing, balancers, DNS, policies, statistics, and APIs instead of reimplementing everything themselves.

I believe this makes Xray-core a more flexible platform for developers while keeping the core codebase clean. It also encourages people to stay on upstream Xray rather than maintaining separate modified versions.

@YellowscorpionDPIII

Copy link
Copy Markdown
  1. Scope of the plugin outbound
    This proposal is not intended to introduce new protocols into Xray-core or encourage clients to bolt on AnyTLS, Snell, etc. The goal is much narrower:

Provide a clean integration point for applications that embed Xray-core.

Today these applications often need to maintain a fork, run localhost loopback proxies, or patch transports directly into the core.
A minimal outbound‑level hook avoids all of that while keeping the core codebase clean.

  1. Why this is different from FinalMask
    FinalMask is excellent for transforming an existing net.Conn, but it still requires the transport to fit Xray’s connection model.
    The plugin outbound serves a different purpose:
    the host application owns the entire transport lifecycle
    Xray still provides routing, DNS, balancers, policies, and stats
    no need to implement a full outbound protocol inside Xray
    This is specifically for cases where the transport is external (mobile OS APIs, ICMP, DNS, system VPN tunnels, etc.) and cannot be expressed as a normal Xray transport.

  2. Stats wrapping / counter concerns
    The counter wrappers exist only because plugin outbounds bypass dialer.Dial(), which is where stat.CounterConnection is normally attached.
    If preferred, this can be refactored to reuse the existing counter helpers so the implementation stays consistent with the rest of the codebase.

  3. Guardrails
    To avoid the concern that clients will start injecting arbitrary protocols:
    the API is intentionally minimal
    it is not exposed as a general plugin ecosystem
    it is primarily for embedding scenarios (mobile wrappers, desktop apps)
    it does not add any new protocol logic to Xray-core itself

@runetfreedom

Copy link
Copy Markdown

I would like to add that this plugin system essentially only addresses the need to maintain forks.

Apple and Google's policies prohibit the dynamic downloading and execution of any executable files or libraries.

This can be a problem. For example, Apple removes any branded VPN apps in Russia, leaving only generic clients like Streisand and Happ.

Obviously, such clients will not be able to download custom versions of xray, regardless of whether it is a fork or a plugin.

The only risk-free workaround I know of is JS based code (which is an exception to the rules). But this option is obviously poor in terms of performance and access to sockets.

Nevertheless, there would be a huge advantage here - it would allow the obfuscator code to be updated on the fly, without the need to update the application.

I’m not suggesting implementing a JS subsystem in xray. I mean that this PR for mobile devices would still require a separate xray executable and a separate client, which significantly reduces the potential of this feature. From this perspective, it’s no different from a fork.

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.

5 participants