Proposal: Plugin Outbound Protocol
Nobody has claimed this yet.
- 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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