DSH-2Xray: Integrate xray-core into the DSH process for global traffic takeover

Introduction

DSH itself is an HTTP(S) exit point, but agents often need stronger exit strategies during runtime: switching nodes, splitting traffic by domain, or using fixed Xray configurations. The conventional approach is to configure proxy environment variables at the system level or to route the entire DSH request separately. The former has a wide impact and relies on the environment; the latter easily misses calls like web_fetch that only ask the proxy strategy layer, leading to a split where “Model API goes through the proxy, but web scraping connects directly.”

Below is an introduction to the DSH plugin @twinsearth/dsh-2xray maintained by TwinsEarth (repository: TwinsEarth/DSH-2Xray). It is positioned as a Host-only bundle, acting as the xray-core startup daemon and traffic takeover within the DSH process, requiring no additional build.

What is it

The plugin plays two roles simultaneously: the process manager for the xray-core client, and the router for HTTP(S) traffic within the DSH process. It generates xray-core client configuration, starts and guards the xray subprocess; once the xray local inbound is confirmed available, it connects the DSH process’s global HTTP(S) exit to that inbound; optionally, it performs multi-node switching and traffic statistics via a zero-dependency gRPC control plane.

Maintainer: TwinsEarth. Package name: @twinsearth/dsh-2xray, current version 0.8.0, license MIT, requires Node >= 24.9.0. 0.8.0 adapts to DSH 0.2.2-rc.1; DSH 0.2.1 and earlier need to use 0.7.0. Apart from the control plane, there are no runtime dependencies; it is a Host-only bundle.

Core Features

Regarding the integration method, the plugin calls installProxyFromEnvironment() from @deepseek-ai/dsh-http-proxy instead of installing setGlobalDispatcher() directly. This is because DSH has already installed the policy dispatcher before the first plugin mounts, and web_fetch only asks proxyRouteFor(). Installing directly would break the consistency of the exit.

The startup and takeover follow a fixed sequence: write configuration -> start process -> TCP ready -> 204 feature probe -> takeover dispatcher. Takeover only occurs after the probe passes to avoid the overall ECONNREFUSED caused by the dispatcher pointing to a port that hasn’t started listening. When stopping, it unmounts the route first, then terminates, and finally waits for the exit.

The port policy is a fixed port + fail if occupied; no silent port switching is performed. After switching ports, DSH still points to the old port, manifesting as persistent connection failures, making it impossible for the user to determine the cause.

The subprocess prefers ctx.subprocess. On Windows, this is a Job Object with KILL_ON_JOB_CLOSE, which can avoid orphaned xray processes occupying ports after Electron closes. If there is no Job Object, the cordis dispose may not execute when the process is forcibly killed.

In multi-node scenarios, the plugin generates one outbound and one balancer for each node. Switching nodes only changes RoutingService.OverrideBalancerTarget, leaving the process, inbound, and dispatcher untouched. The control plane uses node:http2 + hand-written protobuf, not introducing @grpc/grpc-js, with the goal of reducing dependency and supply chain surface for each profile.

The restart strategy applies only to unexpected crashes: exit code 23 (configuration error) does not trigger a restart, port occupation does not trigger a restart, and upstream unreachability does not trigger a restart, because the process itself is healthy.

Installation and Enablement

The plugin is a Host-only bundle and requires no build. The installation command is handled by the plugin manager; no CLI plugin addition form is provided.

npm i @twinsearth/dsh-2xray

After installation, enable it via the plugin manager; or select “Install from directory” in the DSH plugin page and point it to the local bundle directory, for example, E:\GPT\dsh-2xray.

Next, you need to place the xray executable file in one of the following locations, parsed in order of priority from high to low:

1. Plugin's own bin/xray(.exe) (used when distributed with the bundle)
2. Configuration field binary.path
3. Environment variable DSH_XRAY_BIN
4. <DSH_HOME>\xray-proxy\bin\xray.exe (DSH_HOME defaults to %USERPROFILE%\.dsh)
5. Put into PATH

The third step is to edit the profile’s cordis.patch.yml (or the bundle’s default configuration), fill in server.* or nodes[], change enabled to true, and then restart the profile. The bundle’s default configuration is enabled: false to avoid errors immediately upon installation. It is worth noting that the bundle’s cordis.patch.yml is a complete block of configuration; when overriding at the profile layer, the fields to be retained must be restated. The name in the registration line must equal the package name @twinsearth/dsh-2xray; otherwise, the patch will be skipped due to “name mismatch.”

Single-node configuration example:

- id: xray-proxy
  name: '@twinsearth/dsh-2xray'
  config:
    enabled: true
    server:
      address: example.com
      port: 443
      uuid: 00000000-0000-0000-0000-000000000000
      flow: xtls-rprx-vision
      serverName: www.example.com
      password: REPLACE_WITH_43_CHAR_BASE64URL_X25519_PUBLIC_KEY
      shortId: ''
      fingerprint: chrome
      spiderX: /
    inbound:
      protocol: http
      host: 127.0.0.1
      port: 20808
    takeover: auto

Multi-node configuration example:

- id: xray-proxy
  name: '@twinsearth/dsh-2xray'
  config:
    enabled: true
    activeNode: hk-01
    nodes:
      - id: hk-01
        address: hk.example.com
        port: 443
        uuid: 11111111-2222-4333-8444-555555555555
        flow: xtls-rprx-vision
        serverName: www.example.com
        password: REPLACE_WITH_43_CHAR_BASE64URL_X25519_PUBLIC_KEY
        shortId: ''
      - id: jp-01
        address: jp.example.com
        port: 443
        uuid: 11111111-2222-4333-8444-555555555555
        flow: xtls-rprx-vision
        serverName: www.example.com
        password: REPLACE_WITH_43_CHAR_BASE64URL_X25519_PUBLIC_KEY
        shortId: ''
    control:
      enabled: true
      port: 10085
      stats: true

Choose either nodes[] or server.*. Each node requires an id/tag and the fields under server.*. activeNode specifies the node id or outbound tag to pin at startup. control.enabled is required to start the gRPC control plane; control.stats enables traffic counting for stats and policy.system.

Typical Usage

In the default single-node scenario, the most direct usage is: fill in server.* as per the example above, keep inbound.protocol as http, and use takeover with the default auto. This way, the plugin will take over the DSH process’s global exit after the probe passes, without the need to manually set proxy environment variables.

After enabling the gRPC control plane, you can switch nodes: the startup sequence is write configuration -> start process -> TCP ready -> 204 feature probe -> takeover dispatcher -> OverrideBalancerTarget(activeNode) to pin the exit. Subsequent node switching only changes the balancer override without moving the process or inbound. The plugin also provides three optional model tools: xray_status (same state snapshot as status.json), xray_switch_node, and xray_stats; they can be optionally registered via ctx.inject(['tools']). Tool registration failure does not affect the proxy’s operation itself, avoiding the proxy being dragged down by tool availability.

Regarding the inbound protocol, the default is HTTP; SOCKS is only an optional addition because DSH’s proxy policy explicitly rejects socks5:// URLs, and SOCKS can only go through the built-in dispatcher, losing the coverage of web_fetch. SOCKS is suitable for non-Node clients.

The feature probe defaults to requesting http://www.gstatic.com/generate_204. This is the only check that can prove the availability of uuid, flow, SNI, and REALITY across the entire chain, with a default timeout of 5000ms.

Applicable Scenarios and Notes

Suitable for developers who need to manage the xray exit uniformly within DSH, perform multi-node switching, or wish to see clear status and logs within the process. If you have already solved the exit issue well through system proxy or environment variables, there is no need to introduce an extra layer.

Points to note:

  1. The plugin runs with the permissions of the current DSH process; you should check the source code and license before installing. The license is MIT, and the repository provides a LICENSE file; the Xray binary itself has a separate license description (see bin/README.md).
  2. The version must match DSH: 0.8.0 adapts to DSH 0.2.2-rc.1, while DSH 0.2.1 and earlier require 0.7.0.
  3. The port policy is a fixed port; if there is a conflict, it fails. You must ensure that inbound.port is not occupied and does not conflict with control.port.
  4. takeover is optional with auto/force/off. If an existing proxy policy exists, try auto first, which preserves the existing strategy; only use force if you explicitly need to override.
  5. noProxy does not support CIDR; intranet suffixes are configured via routing.bypassDomains.

The plugin directory page is on SkillHub (a community directory with no official affiliation with DeepSeek or Quant). Source code and documentation are on GitHub:

  • https://www.skillhub.cn/plugins/TwinsEarth/DSH-2Xray
  • https://github.com/TwinsEarth/DSH-2Xray
羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye