Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- runner: ubuntu-24.04
target: android
artifact: android
- runner: windows-2022
- runner: windows-2025
target: windows
arch: x64
goarch: amd64
Expand All @@ -57,13 +57,13 @@ jobs:
# Checkout
# =========================
- name: Checkout source
uses: actions/checkout@v6
uses: actions/checkout@v7

# =========================
# Python (build scripts)
# =========================
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: "3.x"
check-latest: true
Expand All @@ -76,7 +76,7 @@ jobs:
# Go setup
# =========================
- name: Setup Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: stable
check-latest: true
Expand Down Expand Up @@ -273,7 +273,7 @@ jobs:
done

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
name: libXray ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/libxray-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: stable
check-latest: true
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-go-mirror.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
contents: write
steps:
- name: Checkout (full history + tags)
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7

- uses: actions/setup-go@v6
- uses: actions/setup-go@v7
with:
go-version: stable
check-latest: true
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ Design notes:
6. `convertShareLinksToXrayJson` validates each parsed outbound with the current
Xray-core config builder. Invalid outbounds are omitted, and the method fails
if none remain. Validation does not create or start an Xray instance.
Xray JSON input is treated as a node source: only its root `outbounds` are
retained, and all other root fields are ignored. The response contains only
fields supported by libXray share links; unsupported and generated empty
fields are omitted. Opaque XHTTP `extra` and FinalMask mask `settings` JSON
remain unchanged.
Its optional `age.secretKey` decrypts official age ASCII armor in memory
before the existing parser runs. Plaintext input remains unchanged.
7. Xray-core keeps its system dialer DNS client and outbound manager in
Expand Down
8 changes: 6 additions & 2 deletions invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,12 @@ func invokeConvertShareLinksToXrayJson(payload json.RawMessage) string {
if request.Age != nil {
secretKey = request.Age.SecretKey
}
xrayJson, err := share.ConvertShareLinksToXrayJsonWithAge(request.Text, secretKey)
return encodeInvokeResponse(xrayJson, err)
config, err := share.ConvertShareLinksToXrayJsonWithAge(request.Text, secretKey)
if err != nil {
return encodeInvokeResponse(nil, err)
}
xrayJSON, err := share.MarshalShareConfigJSON(config)
return encodeInvokeResponse(xrayJSON, err)
}

func invokeGenerateAgeKeyPair(payload json.RawMessage) string {
Expand Down
40 changes: 40 additions & 0 deletions invoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,46 @@ func TestInvokeConvertShareLinksFiltersBuildInvalidOutbounds(t *testing.T) {
}
}

func TestInvokeConvertShareLinksReturnsProjectedObject(t *testing.T) {
const publicKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
link := "vless://12345678-abcd-abcd-abcd-123456789abc@example.com:443" +
"?encryption=none&type=xhttp&host=cdn.example.com&path=%2Fx&mode=stream-up" +
"&security=reality&sni=example.com&fp=chrome&pbk=" + publicKey + "&sid=abcd"
response := invokeForTest(
t,
LibXrayMethodConvertShareLinksToXrayJson,
ConvertShareLinksToXrayJsonRequest{Text: link},
)
if !response.Success {
t.Fatalf("ConvertShareLinksToXrayJson failed: %s", response.Err)
}

var root map[string]json.RawMessage
if err := json.Unmarshal(response.Data, &root); err != nil {
t.Fatalf("data is not an object: %s", response.Data)
}
if len(root) != 1 || root["outbounds"] == nil {
t.Fatalf("data root = %s, want only outbounds", response.Data)
}
for _, field := range []string{"publicKey", "target", "dest", "proxySettings", "sockopt"} {
if bytes.Contains(response.Data, []byte(`"`+field+`"`)) {
t.Fatalf("data contains unsupported field %q: %s", field, response.Data)
}
}
if !bytes.Contains(response.Data, []byte(`"password":"`+publicKey+`"`)) {
t.Fatalf("data did not canonicalize REALITY password: %s", response.Data)
}

config := decodeDataObject[conf.Config](t, response)
if len(config.OutboundConfigs) != 1 {
t.Fatalf("outbounds = %d, want 1", len(config.OutboundConfigs))
}
config.OutboundConfigs[0].SendThrough = nil
if _, err := config.OutboundConfigs[0].Build(); err != nil {
t.Fatalf("projected outbound does not build: %v", err)
}
}

func TestInvokeAgeKeyGenerationAndConversion(t *testing.T) {
generated := invokeForTest(
t,
Expand Down
2 changes: 1 addition & 1 deletion readme/README.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ void CGoFree(char* value);
3. `SetTunFd` 已删除。如果 fd 只能在运行时获得,请在调用 `runXray` 前把 `xray.tun.fd` 写入 Xray 配置根 `env` 对象。
4. `countGeoData` 不依赖 Xray 配置,因此通过 method payload 的 `datDir` 传入数据目录。
5. 完整的 UTF-8 编码 Invoke 请求和响应 JSON 包体限制为 16 MiB。任一方向超过限制时,Invoke 将返回 `success: false`、`data: null` 和对应的大小限制错误。
6. `convertShareLinksToXrayJson` 会使用当前 Xray-core 配置构建器校验每个已解析的 outbound。无效 outbound 会被忽略;如果没有剩余的有效 outbound,该方法返回失败。校验不会创建或启动 Xray instance。可选的 `age.secretKey` 会在现有解析流程前于内存中解密官方 age ASCII armor;明文输入保持原有行为。
6. `convertShareLinksToXrayJson` 会使用当前 Xray-core 配置构建器校验每个已解析的 outbound。无效 outbound 会被忽略;如果没有剩余的有效 outbound,该方法返回失败。校验不会创建或启动 Xray instance。Xray JSON 输入仅作为节点来源,只保留根级 `outbounds`,忽略其他根字段。响应仅包含 libXray 分享链接支持的字段,不支持的字段和生成的空字段会被省略;XHTTP `extra` 与 FinalMask mask `settings` 中的原始 JSON 保持不变。可选的 `age.secretKey` 会在现有解析流程前于内存中解密官方 age ASCII armor;明文输入保持原有行为。
7. Xray-core 的系统拨号 DNS client 和 outbound manager 属于进程级状态。当 `runXray` 正在运行时,通过 `pingBatch`、`testXray` 或导出的 Go API 创建另一个 Xray instance,可能覆盖这些状态并影响正在运行的 instance。关闭临时 instance 不会恢复之前的状态。libXray 不对并发 instance 进行串行化、隔离或状态恢复;调用方如需同时运行多个 instance,必须将它们放在不同进程中。

支持的 method:
Expand Down
29 changes: 11 additions & 18 deletions share/clash_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,10 @@ type ClashProxy struct {
Udp bool `yaml:"udp,omitempty"`
UdpOverTcp bool `yaml:"udp-over-tcp,omitempty"`

Tls bool `yaml:"tls,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Servername string `yaml:"servername,omitempty"`
Sni string `yaml:"sni,omitempty"`
Alpn []string `yaml:"alpn,omitempty"`
Tls bool `yaml:"tls,omitempty"`
Servername string `yaml:"servername,omitempty"`
Sni string `yaml:"sni,omitempty"`
Alpn []string `yaml:"alpn,omitempty"`

Fingerprint string `yaml:"fingerprint,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
Expand All @@ -66,14 +65,13 @@ type ClashProxyRealityOpts struct {
}

type ClashProxyPluginOpts struct {
Mode string `yaml:"mode,omitempty"`
Tls bool `yaml:"tls,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
EchOpts *ClashProxyEchOpts `yaml:"ech-opts,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Host string `yaml:"host,omitempty"`
Path string `yaml:"path,omitempty"`
Mux bool `yaml:"mux,omitempty"`
Mode string `yaml:"mode,omitempty"`
Tls bool `yaml:"tls,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
EchOpts *ClashProxyEchOpts `yaml:"ech-opts,omitempty"`
Host string `yaml:"host,omitempty"`
Path string `yaml:"path,omitempty"`
Mux bool `yaml:"mux,omitempty"`
}

type ClashProxyWsOpts struct {
Expand Down Expand Up @@ -125,7 +123,6 @@ type ClashProxyXhttpOptsDownloadSettings struct {
Alpn []string `yaml:"alpn,omitempty"`
EchOpts *ClashProxyEchOpts `yaml:"ech-opts,omitempty"`
RealityOpts *ClashProxyRealityOpts `yaml:"reality-opts,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
Servername string `yaml:"servername,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
Expand Down Expand Up @@ -500,8 +497,6 @@ func (proxy ClashProxy) parseSecurity(streamSettings *conf.StreamConfig, outboun
realitySettings.Fingerprint = proxy.ClientFingerprint
}

tlsSettings.AllowInsecure = proxy.SkipCertVerify

if (outbound.Protocol == "trojan" || outbound.Protocol == "hysteria") && len(streamSettings.Security) == 0 {
streamSettings.Security = "tls"
}
Expand Down Expand Up @@ -695,8 +690,6 @@ func parseXHTTPDownloadSettingsSecurity(streamSettings *conf.StreamConfig, proxy
realitySettings.Fingerprint = proxy.ClientFingerprint
}

tlsSettings.AllowInsecure = proxy.SkipCertVerify

switch streamSettings.Security {
case "tls":
streamSettings.TLSSettings = tlsSettings
Expand Down
5 changes: 1 addition & 4 deletions share/clash_meta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,6 @@ func TestClashVless_XhttpExtraDownloadAndRanges(t *testing.T) {
ech-opts:
enable: true
config: echcfg123
skip-cert-verify: true
xhttp-opts:
path: /xh
host: xh.h
Expand Down Expand Up @@ -342,15 +341,13 @@ func TestClashTrojan_TlsFromType(t *testing.T) {
server: tr.host
port: 443
password: trpass
sni: tr.host
skip-cert-verify: true`
sni: tr.host`
cfg := parseClashYAML(t, yaml)
require.Len(t, cfg.OutboundConfigs, 1)
ss := cfg.OutboundConfigs[0].StreamSetting
require.NotNil(t, ss)
assert.Equal(t, "tls", ss.Security)
require.NotNil(t, ss.TLSSettings)
assert.True(t, ss.TLSSettings.AllowInsecure)
}

func TestClashVless_Reality(t *testing.T) {
Expand Down
29 changes: 0 additions & 29 deletions share/generate_share.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,6 @@ func streamSettingsQuery(proxy conf.OutboundDetourConfig, link *url.URL) {
if len(vcn) > 0 {
query = addQuery(query, "vcn", vcn)
}
if streamSettings.TLSSettings.AllowInsecure {
query = addQuery(query, "insecure", "1")
}
}

// QuicParams (bandwidth + port-hopping)
Expand Down Expand Up @@ -353,29 +350,6 @@ func streamSettingsQuery(proxy conf.OutboundDetourConfig, link *url.URL) {
query = addQuery(query, "host", strings.Join(host, ","))
}
}
case "kcp":
if streamSettings.KCPSettings == nil {
break
}
seed := streamSettings.KCPSettings.Seed
if seed != nil && len(*seed) > 0 {
query = addQuery(query, "seed", *seed)
}

headerConfig := streamSettings.KCPSettings.HeaderConfig
if headerConfig == nil {
break
}
var header XrayFakeHeader
err := json.Unmarshal(headerConfig, &header)
if err != nil {
break
}

headerType := header.Type
if len(headerType) > 0 {
query = addQuery(query, "headerType", headerType)
}
case "ws":
if streamSettings.WSSettings == nil {
break
Expand Down Expand Up @@ -476,9 +450,6 @@ func streamSettingsQuery(proxy conf.OutboundDetourConfig, link *url.URL) {
if len(vcn) > 0 {
query = addQuery(query, "vcn", vcn)
}
if streamSettings.TLSSettings.AllowInsecure {
query = addQuery(query, "insecure", "1")
}
case "reality":
if streamSettings.REALITYSettings == nil {
break
Expand Down
20 changes: 20 additions & 0 deletions share/generate_share_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,26 @@ func TestConvertXrayJsonToShareLinks_RoundTripProtocols(t *testing.T) {
}
}

func TestGenerate_KCPIgnoresSeedAndHeader(t *testing.T) {
config, err := ConvertShareLinksToXrayJson(
"vless://" + testShareUUID + "@kcp.example:443?encryption=none&type=kcp",
)
require.NoError(t, err)

seed := "legacy-seed"
header := json.RawMessage(`{"type":"srtp"}`)
config.OutboundConfigs[0].StreamSetting.KCPSettings = &conf.KCPConfig{
Seed: &seed,
HeaderConfig: header,
}

link, err := shareLink(config.OutboundConfigs[0])
require.NoError(t, err)
assert.Equal(t, "kcp", link.Query().Get("type"))
assert.Empty(t, link.Query().Get("seed"))
assert.Empty(t, link.Query().Get("headerType"))
}

func TestGenerate_ShadowsocksAEAD2022PlainUserInfo(t *testing.T) {
const original = "ss://2022-blake3-aes-256-gcm:" +
"YctPZ6U7xPPcU%2Bgp3u%2B0tx%2FtRizJN9K8y%2BuKlW2qjlI%3D" +
Expand Down
Loading