XTLS / XTLS/Xray-core

Proposal: Plugin Outbound Protocol

Open
#6,297 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement PR welcome
Dominant language
Go
Stars
41.7k
Forks
5.9k
Avg merge
3d 10h
Merged PRs (30d)
29

Description

1. Goal

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.

It includes a registration callback that passes the tag and config params (custom JSON) back to the wrapper app upon instantiation.


2. Benefits & Advantages

  • Brings Xray Features to External Transports:
    Custom and third-party protocols can take advantage of Xray’s routing, balancers, DNS, statistics, and policy system while keeping their transport implementation entirely outside Xray-core.

  • Optimized for Mobile Operating Systems (iOS & Android):
    Avoids unnecessary local sockets and background listeners while allowing the host application to fully control tunnel lifecycle, reconnection logic and platform-specific networking constraints.

  • Supports Custom and Experimental Transports:
    Enables native integration of non-standard transports (e.g., ICMP-based ping tunnels, DNS tunnels, custom obfuscation layers).


3. API Design

3.1. Handler Registry (proxy/plugin/plugin.go)
package plugin

import (
	"context"
	"sync"
	v2net "github.com/xtls/xray-core/common/net"
	"github.com/xtls/xray-core/transport"
)

type OutboundHandlerFunc func(ctx context.Context, dest v2net.Destination, link *transport.Link) error
type OnPluginRegisteredFunc func(tag string, name string, params string)

var (
	handlersMu sync.RWMutex
	handlers   = make(map[string]OutboundHandlerFunc)
	onPluginRegisteredMu sync.Mutex
	onPluginRegistered   OnPluginRegisteredFunc
)

func RegisterHandler(name string, handler OutboundHandlerFunc) {
	handlersMu.Lock()
	defer handlersMu.Unlock()
	handlers[name] = handler
}

func GetHandler(name string) OutboundHandlerFunc {
	handlersMu.RLock()
	defer handlersMu.RUnlock()
	return handlers[name]
}

func SetOnPluginRegistered(cb OnPluginRegisteredFunc) {
	onPluginRegisteredMu.Lock()
	defer onPluginRegisteredMu.Unlock()
	onPluginRegistered = cb
}

func TriggerOnPluginRegistered(tag string, name string, params string) {
	onPluginRegisteredMu.Lock()
	cb := onPluginRegistered
	onPluginRegisteredMu.Unlock()
	if cb != nil {
		cb(tag, name, params)
	}
}
3.2. Protobuf Config (proxy/plugin/config.proto)
syntax = "proto3";
package xray.proxy.plugin;
option go_package = "github.com/xtls/xray-core/proxy/plugin";

message ClientConfig {
  string name = 1;   // e.g. "customtunnel"
  string params = 2; // raw custom settings JSON string
}
Json
{
  "outbounds": [
    {
      "tag": "proxy",
      "protocol": "plugin",
      "settings": {
        "name": "customtunnel",
        "params": {
          "password": "je101kgas",
          "target": "1.1.1.1",
          "raw_mode": true
        }
      }
    }
  ]
}
3.3. Outbound Client (proxy/plugin/client.go)

Retrieves the handler and executes it directly, wrapping the Link with traffic statistics if enabled:

package plugin

import (
	"context"
	"github.com/xtls/xray-core/common"
	"github.com/xtls/xray-core/common/buf"
	"github.com/xtls/xray-core/common/errors"
	"github.com/xtls/xray-core/common/session"
	"github.com/xtls/xray-core/core"
	"github.com/xtls/xray-core/features/policy"
	"github.com/xtls/xray-core/features/stats"
	"github.com/xtls/xray-core/transport"
	"github.com/xtls/xray-core/transport/internet"
)

type Client struct {
	name, params string
}

func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) {
	var tag string
	if handler := session.FullHandlerFromContext(ctx); handler != nil {
		tag = handler.Tag()
	}
	TriggerOnPluginRegistered(tag, config.Name, config.Params)
	return &Client{name: config.Name, params: config.Params}, nil
}

type sizeStatReader struct { buf.Reader; counter stats.Counter }
func (r *sizeStatReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
	mb, err := r.Reader.ReadMultiBuffer()
	if r.counter != nil { r.counter.Add(int64(mb.Len())) }
	return mb, err
}

type sizeStatWriter struct { buf.Writer; counter stats.Counter }
func (w *sizeStatWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
	if w.counter != nil { w.counter.Add(int64(mb.Len())) }
	return w.Writer.WriteMultiBuffer(mb)
}

func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
	outbounds := session.OutboundsFromContext(ctx)
	destination := outbounds[len(outbounds)-1].Target
	handlerFunc := GetHandler(c.name)
	if handlerFunc == nil {
		return errors.New("plugin outbound handler not registered: ", c.name)
	}

	var tag string
	if len(outbounds) > 0 { tag = outbounds[len(outbounds)-1].Tag }
	if len(tag) > 0 {
		if v := core.FromContext(ctx); v != nil {
			pm := v.GetFeature(policy.ManagerType()).(policy.Manager)
			sm := v.GetFeature(stats.ManagerType()).(stats.Manager)
			if pm.ForSystem().Stats.OutboundUplink {
				if c, _ := stats.GetOrRegisterCounter(sm, "outbound>>>"+tag+">>>traffic>>>uplink"); c != nil {
					link.Writer = &sizeStatWriter{Writer: link.Writer, counter: c}
				}
			}
			if pm.ForSystem().Stats.OutboundDownlink {
				if c, _ := stats.GetOrRegisterCounter(sm, "outbound>>>"+tag+">>>traffic>>>downlink"); c != nil {
					link.Reader = &sizeStatReader{Reader: link.Reader, counter: c}
				}
			}
		}
	}
	return handlerFunc(ctx, destination, link)
}

func init() {
	common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
		return NewClient(ctx, config.(*ClientConfig))
	}))
}

4. Integration Example

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)
})

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read the proposed entry points in proxy/plugin/plugin.go, proxy/plugin/config.proto, and proxy/plugin/client.go, then compare them with Xray-core's existing proxy configuration and outbound registration patterns. The work is done when the plugin protocol, dynamic handler registry, registration callback, configuration, and statistics-aware outbound processing are integrated consistently with the supplied API design.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend-api-design, networking
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.