From bb1b1bc7c5d1a0a5da9bac6ebc90a574eb3b8889 Mon Sep 17 00:00:00 2001 From: yiguo Date: Tue, 25 Aug 2026 21:34:39 +0800 Subject: [PATCH 1/7] Remove deprecated AllowInsecure share handling --- share/clash_meta.go | 29 +++++++++++------------------ share/clash_meta_test.go | 5 +---- share/generate_share.go | 6 ------ share/stream.go | 4 ---- 4 files changed, 12 insertions(+), 32 deletions(-) diff --git a/share/clash_meta.go b/share/clash_meta.go index ce8d3022..315f9ae7 100644 --- a/share/clash_meta.go +++ b/share/clash_meta.go @@ -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"` @@ -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 { @@ -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"` @@ -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" } @@ -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 diff --git a/share/clash_meta_test.go b/share/clash_meta_test.go index 4733eccc..9563f448 100644 --- a/share/clash_meta_test.go +++ b/share/clash_meta_test.go @@ -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 @@ -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) { diff --git a/share/generate_share.go b/share/generate_share.go index 340654e0..f46ab127 100644 --- a/share/generate_share.go +++ b/share/generate_share.go @@ -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) @@ -476,9 +473,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 diff --git a/share/stream.go b/share/stream.go index b488f272..d6b822ee 100644 --- a/share/stream.go +++ b/share/stream.go @@ -47,10 +47,6 @@ func (proxy xrayShareLink) parseSecurityFromURL(link *url.URL, streamSettings *c tlsSettings.ALPN = new(conf.StringList(strings.Split(alpn, ","))) } - if query.Get("insecure") == "1" { - tlsSettings.AllowInsecure = true - } - pbk := query.Get("pbk") realitySettings.Password = pbk realitySettings.PublicKey = pbk From bc536c7d754a30e77c067da99ac17742af19c5b6 Mon Sep 17 00:00:00 2001 From: yiguo Date: Wed, 26 Aug 2026 15:36:08 +0800 Subject: [PATCH 2/7] Keep only outbounds from Xray JSON imports --- README.md | 2 ++ readme/README.zh_CN.md | 2 +- share/parse_share.go | 4 ++-- share/parse_share_test.go | 19 +++++++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e5fa0026..97f7c5de 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,8 @@ 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. 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 diff --git a/readme/README.zh_CN.md b/readme/README.zh_CN.md index 87ca9e54..11cb31ab 100644 --- a/readme/README.zh_CN.md +++ b/readme/README.zh_CN.md @@ -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`,忽略其他根字段。可选的 `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: diff --git a/share/parse_share.go b/share/parse_share.go index 9cfcb005..7a24af6e 100644 --- a/share/parse_share.go +++ b/share/parse_share.go @@ -44,7 +44,7 @@ func decodeBase64Text(text string) (string, error) { // https://github.com/XTLS/Xray-core/discussions/716 // ConvertShareLinksToXrayJson parses: -// - a single Xray JSON object (starts with '{') +// - a single Xray JSON object (starts with '{'; only root outbounds are retained) // - plain v2rayN-style lines (vless/vmess/ss/socks/trojan/hy2…) // - one base64 blob that decodes to Xray JSON, share lines, or Clash YAML // - Clash / Clash.Meta YAML (proxies:) @@ -87,7 +87,7 @@ func parseXrayJSONConfig(text string) (*conf.Config, error) { if len(xray.OutboundConfigs) == 0 { return nil, fmt.Errorf("no valid outbounds") } - return xray, nil + return &conf.Config{OutboundConfigs: xray.OutboundConfigs}, nil } var shareSchemes = []string{ diff --git a/share/parse_share_test.go b/share/parse_share_test.go index f97e2584..79a59e21 100644 --- a/share/parse_share_test.go +++ b/share/parse_share_test.go @@ -173,6 +173,25 @@ func TestConvertShareLinksToXrayJson_XrayJSONRoundTrip(t *testing.T) { assert.Equal(t, orig.OutboundConfigs[0].Protocol, again.OutboundConfigs[0].Protocol) } +func TestConvertShareLinksToXrayJson_XrayJSONKeepsOnlyOutbounds(t *testing.T) { + config, err := ConvertShareLinksToXrayJson(`{ + "env": {"TEST_ENV": "value"}, + "log": {"loglevel": "debug"}, + "routing": {"rules": []}, + "dns": {"servers": ["8.8.8.8"]}, + "inbounds": [{"protocol": "http", "listen": "127.0.0.1", "port": 1080, "settings": {}}], + "outbounds": [{"protocol": "freedom", "tag": "direct", "settings": {}}], + "policy": {"levels": {}}, + "metrics": {"tag": "metrics"}, + "stats": {}, + "fakeDns": {"ipPool": "198.18.0.0/15", "poolSize": 65535} + }`) + require.NoError(t, err) + require.Len(t, config.OutboundConfigs, 1) + assert.Equal(t, "direct", config.OutboundConfigs[0].Tag) + assert.Equal(t, &conf.Config{OutboundConfigs: config.OutboundConfigs}, config) +} + func TestConvertShareLinksToXrayJson_XrayJSONInvalid(t *testing.T) { _, err := ConvertShareLinksToXrayJson("{not json") require.Error(t, err) From 17a9790a6da8bdbaa28975f9f339696795e150a5 Mon Sep 17 00:00:00 2001 From: yiguo Date: Wed, 26 Aug 2026 16:10:48 +0800 Subject: [PATCH 3/7] Remove KCP seed and header share handling --- share/generate_share.go | 23 ----------------------- share/generate_share_test.go | 20 ++++++++++++++++++++ share/parse_share_test.go | 18 ++++++++++++------ share/transport_build.go | 32 +++++++++++--------------------- share/xray_json.go | 4 ---- 5 files changed, 43 insertions(+), 54 deletions(-) diff --git a/share/generate_share.go b/share/generate_share.go index f46ab127..8f32f009 100644 --- a/share/generate_share.go +++ b/share/generate_share.go @@ -350,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 diff --git a/share/generate_share_test.go b/share/generate_share_test.go index 0b9c844e..a4ab340b 100644 --- a/share/generate_share_test.go +++ b/share/generate_share_test.go @@ -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"}`) + kcp := config.OutboundConfigs[0].StreamSetting.KCPSettings + require.NotNil(t, kcp) + kcp.Seed = &seed + kcp.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" + diff --git a/share/parse_share_test.go b/share/parse_share_test.go index 79a59e21..16b5ce90 100644 --- a/share/parse_share_test.go +++ b/share/parse_share_test.go @@ -384,9 +384,12 @@ func TestConvertShareLinksToXrayJson_VmessBase64QR(t *testing.T) { func TestConvertShareLinksToXrayJson_TransportKcpGrpcHttpUpgradeXhttp(t *testing.T) { t.Run("kcp", func(t *testing.T) { link := "vless://" + testShareUUID + "@k.example:443?encryption=none&type=kcp&headerType=srtp&seed=myseed" - _, err := ConvertShareLinksToXrayJson(link) - require.Error(t, err) - assert.Contains(t, err.Error(), "mkcp header & seed has been removed") + cfg, err := ConvertShareLinksToXrayJson(link) + require.NoError(t, err) + kcp := cfg.OutboundConfigs[0].StreamSetting.KCPSettings + require.NotNil(t, kcp) + assert.Nil(t, kcp.HeaderConfig) + assert.Nil(t, kcp.Seed) }) t.Run("grpc", func(t *testing.T) { @@ -546,9 +549,12 @@ func TestConvertShareLinksToXrayJson_VmessQRGrpcAndKcp(t *testing.T) { t.Run("kcp", func(t *testing.T) { qr := `{"ps":"k","add":"kcp.host","port":"8391","id":"` + testShareUUID + `","net":"kcp","path":"seedval","type":"wireguard"}` link := "vmess://" + base64.StdEncoding.EncodeToString([]byte(qr)) - _, err := ConvertShareLinksToXrayJson(link) - require.Error(t, err) - assert.Contains(t, err.Error(), "mkcp header & seed has been removed") + cfg, err := ConvertShareLinksToXrayJson(link) + require.NoError(t, err) + kcp := cfg.OutboundConfigs[0].StreamSetting.KCPSettings + require.NotNil(t, kcp) + assert.Nil(t, kcp.HeaderConfig) + assert.Nil(t, kcp.Seed) }) } diff --git a/share/transport_build.go b/share/transport_build.go index 11f9d6ba..0d9e2210 100644 --- a/share/transport_build.go +++ b/share/transport_build.go @@ -14,7 +14,6 @@ type shareTransportFields struct { HeaderType string Path string Host string - Seed string // KCP seed (URL query or VMess QR path-as-seed) GrpcAuthority string GrpcServiceName string GrpcMultiMode bool @@ -28,12 +27,10 @@ func transportFieldsFromURLQuery(q url.Values) shareTransportFields { if network == "" { network = "raw" } - return shareTransportFields{ + fields := shareTransportFields{ Network: network, - HeaderType: q.Get("headerType"), Path: q.Get("path"), Host: q.Get("host"), - Seed: q.Get("seed"), GrpcAuthority: q.Get("authority"), GrpcServiceName: q.Get("serviceName"), GrpcMultiMode: q.Get("mode") == "multi", @@ -41,6 +38,10 @@ func transportFieldsFromURLQuery(q url.Values) shareTransportFields { ExtraJSON: q.Get("extra"), FMJSON: q.Get("fm"), } + if network == "raw" || network == "tcp" { + fields.HeaderType = q.Get("headerType") + } + return fields } func transportFieldsFromVmessQR(p vmessQrCode) shareTransportFields { @@ -49,17 +50,16 @@ func transportFieldsFromVmessQR(p vmessQrCode) shareTransportFields { network = "raw" } t := shareTransportFields{ - Network: network, - HeaderType: p.Type, - Path: p.Path, - Host: p.Host, + Network: network, + Path: p.Path, + Host: p.Host, } switch network { + case "raw", "tcp": + t.HeaderType = p.Type case "grpc", "gun": t.GrpcServiceName = p.Path t.GrpcMultiMode = p.Type == "multi" - case "kcp", "mkcp": - t.Seed = p.Path } if network == "xhttp" || network == "splithttp" { t.XHTTPMode = p.Type @@ -96,17 +96,7 @@ func buildStreamFromTransportFields(t shareTransportFields) (*conf.StreamConfig, streamSettings.RAWSettings = rawSettings } case "kcp", "mkcp": - kcpSettings := &conf.KCPConfig{} - if t.HeaderType != "" { - header := XrayFakeHeader{Type: t.HeaderType} - headerRawMessage, err := convertJsonToRawMessage(header) - if err != nil { - return nil, err - } - kcpSettings.HeaderConfig = headerRawMessage - } - kcpSettings.Seed = new(t.Seed) - streamSettings.KCPSettings = kcpSettings + streamSettings.KCPSettings = &conf.KCPConfig{} case "ws", "websocket": streamSettings.WSSettings = &conf.WebSocketConfig{Path: t.Path, Host: t.Host} case "grpc", "gun": diff --git a/share/xray_json.go b/share/xray_json.go index fd29bbd0..78589109 100644 --- a/share/xray_json.go +++ b/share/xray_json.go @@ -21,10 +21,6 @@ type XrayRawSettingsHeaderRequestHeaders struct { Host []string `json:"Host,omitempty"` } -type XrayFakeHeader struct { - Type string `json:"type,omitempty"` -} - func setOutboundName(outbound *conf.OutboundDetourConfig, name string) { outbound.SendThrough = &name } From 1f2cb965ce744f751fce08f5b53e608482849f8b Mon Sep 17 00:00:00 2001 From: yiguo Date: Wed, 26 Aug 2026 16:43:42 +0800 Subject: [PATCH 4/7] Stop generating empty KCP share settings --- share/generate_share_test.go | 8 ++++---- share/parse_share_test.go | 10 ++-------- share/transport_build.go | 2 -- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/share/generate_share_test.go b/share/generate_share_test.go index a4ab340b..f8b8e21e 100644 --- a/share/generate_share_test.go +++ b/share/generate_share_test.go @@ -181,10 +181,10 @@ func TestGenerate_KCPIgnoresSeedAndHeader(t *testing.T) { seed := "legacy-seed" header := json.RawMessage(`{"type":"srtp"}`) - kcp := config.OutboundConfigs[0].StreamSetting.KCPSettings - require.NotNil(t, kcp) - kcp.Seed = &seed - kcp.HeaderConfig = header + config.OutboundConfigs[0].StreamSetting.KCPSettings = &conf.KCPConfig{ + Seed: &seed, + HeaderConfig: header, + } link, err := shareLink(config.OutboundConfigs[0]) require.NoError(t, err) diff --git a/share/parse_share_test.go b/share/parse_share_test.go index 16b5ce90..6ccdc1ee 100644 --- a/share/parse_share_test.go +++ b/share/parse_share_test.go @@ -386,10 +386,7 @@ func TestConvertShareLinksToXrayJson_TransportKcpGrpcHttpUpgradeXhttp(t *testing link := "vless://" + testShareUUID + "@k.example:443?encryption=none&type=kcp&headerType=srtp&seed=myseed" cfg, err := ConvertShareLinksToXrayJson(link) require.NoError(t, err) - kcp := cfg.OutboundConfigs[0].StreamSetting.KCPSettings - require.NotNil(t, kcp) - assert.Nil(t, kcp.HeaderConfig) - assert.Nil(t, kcp.Seed) + assert.Nil(t, cfg.OutboundConfigs[0].StreamSetting.KCPSettings) }) t.Run("grpc", func(t *testing.T) { @@ -551,10 +548,7 @@ func TestConvertShareLinksToXrayJson_VmessQRGrpcAndKcp(t *testing.T) { link := "vmess://" + base64.StdEncoding.EncodeToString([]byte(qr)) cfg, err := ConvertShareLinksToXrayJson(link) require.NoError(t, err) - kcp := cfg.OutboundConfigs[0].StreamSetting.KCPSettings - require.NotNil(t, kcp) - assert.Nil(t, kcp.HeaderConfig) - assert.Nil(t, kcp.Seed) + assert.Nil(t, cfg.OutboundConfigs[0].StreamSetting.KCPSettings) }) } diff --git a/share/transport_build.go b/share/transport_build.go index 0d9e2210..060b29b4 100644 --- a/share/transport_build.go +++ b/share/transport_build.go @@ -95,8 +95,6 @@ func buildStreamFromTransportFields(t shareTransportFields) (*conf.StreamConfig, rawSettings.HeaderConfig = headerRawMessage streamSettings.RAWSettings = rawSettings } - case "kcp", "mkcp": - streamSettings.KCPSettings = &conf.KCPConfig{} case "ws", "websocket": streamSettings.WSSettings = &conf.WebSocketConfig{Path: t.Path, Host: t.Host} case "grpc", "gun": From a462db689cee8cf6ddf7a7ad2e2172fc25388935 Mon Sep 17 00:00:00 2001 From: yiguo Date: Wed, 26 Aug 2026 17:09:31 +0800 Subject: [PATCH 5/7] Add share config JSON projection --- README.md | 5 +- invoke.go | 8 +- invoke_test.go | 40 ++++ readme/README.zh_CN.md | 2 +- share/marshal_share.go | 368 ++++++++++++++++++++++++++++++++++++ share/marshal_share_test.go | 141 ++++++++++++++ 6 files changed, 560 insertions(+), 4 deletions(-) create mode 100644 share/marshal_share.go create mode 100644 share/marshal_share_test.go diff --git a/README.md b/README.md index 97f7c5de..10eea4db 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,10 @@ Design notes: 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. + 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 diff --git a/invoke.go b/invoke.go index 4abcf6cc..f2134fe3 100644 --- a/invoke.go +++ b/invoke.go @@ -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 { diff --git a/invoke_test.go b/invoke_test.go index 45e10f41..78cd23f3 100644 --- a/invoke_test.go +++ b/invoke_test.go @@ -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, diff --git a/readme/README.zh_CN.md b/readme/README.zh_CN.md index 11cb31ab..a9e605c7 100644 --- a/readme/README.zh_CN.md +++ b/readme/README.zh_CN.md @@ -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。Xray JSON 输入仅作为节点来源,只保留根级 `outbounds`,忽略其他根字段。可选的 `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: diff --git a/share/marshal_share.go b/share/marshal_share.go new file mode 100644 index 00000000..729cd64d --- /dev/null +++ b/share/marshal_share.go @@ -0,0 +1,368 @@ +package share + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/xtls/xray-core/infra/conf" +) + +// MarshalShareConfigJSON returns the Xray JSON subset supported by share links. +func MarshalShareConfigJSON(config *conf.Config) (json.RawMessage, error) { + if config == nil { + return nil, fmt.Errorf("no valid outbound found") + } + + outbounds := make([]map[string]any, 0, len(config.OutboundConfigs)) + var firstBuildError error + for _, outbound := range config.OutboundConfigs { + source, err := marshalShareJSONObject(outbound) + if err != nil { + return nil, err + } + projected, supported := projectShareOutbound(source) + if !supported { + continue + } + if err := validateProjectedShareOutbound(projected); err != nil { + if firstBuildError == nil { + firstBuildError = err + } + continue + } + outbounds = append(outbounds, projected) + } + + if len(outbounds) == 0 { + if firstBuildError != nil { + return nil, fmt.Errorf("no valid outbound found: %w", firstBuildError) + } + return nil, fmt.Errorf("no valid outbound found") + } + + raw, err := json.Marshal(map[string]any{"outbounds": outbounds}) + if err != nil { + return nil, fmt.Errorf("failed to marshal share config: %w", err) + } + return raw, nil +} + +func marshalShareJSONObject(value any) (map[string]any, error) { + raw, err := json.Marshal(value) + if err != nil { + return nil, fmt.Errorf("failed to marshal share config: %w", err) + } + decoder := json.NewDecoder(bytes.NewReader(raw)) + decoder.UseNumber() + var object map[string]any + if err := decoder.Decode(&object); err != nil { + return nil, fmt.Errorf("failed to marshal share config: %w", err) + } + return object, nil +} + +func projectShareOutbound(source map[string]any) (map[string]any, bool) { + protocol := strings.ToLower(shareString(source["protocol"])) + settings, _ := shareObject(source["settings"]) + + projectedSettings := map[string]any{} + switch protocol { + case "shadowsocks": + copyShareFields(projectedSettings, settings, "address", "port", "method", "password") + case "vmess": + copyShareFields(projectedSettings, settings, "address", "port", "id", "security") + case "vless": + copyShareFields(projectedSettings, settings, "address", "port", "id", "flow", "encryption") + case "socks": + copyShareFields(projectedSettings, settings, "address", "port", "user", "pass") + case "trojan": + copyShareFields(projectedSettings, settings, "address", "port", "password") + case "hysteria": + copyShareFields(projectedSettings, settings, "version", "address", "port") + default: + return nil, false + } + + projected := map[string]any{ + "protocol": protocol, + "settings": projectedSettings, + } + copyShareFields(projected, source, "sendThrough", "tag") + + if stream, ok := shareObject(source["streamSettings"]); ok { + projectedStream, supported := projectShareStream(stream) + if !supported { + return nil, false + } + if len(projectedStream) > 0 { + projected["streamSettings"] = projectedStream + } + } + return projected, true +} + +func projectShareStream(source map[string]any) (map[string]any, bool) { + networkValue := shareString(source["network"]) + if method := shareString(source["method"]); method != "" { + networkValue = method + } + network, supported := canonicalShareNetwork(networkValue) + if !supported { + return nil, false + } + + securityValue := shareString(source["security"]) + security := strings.ToLower(securityValue) + if security != "" && security != "none" && security != "tls" && security != "reality" { + return nil, false + } + + projected := map[string]any{} + if networkValue != "" { + projected["network"] = network + } + if securityValue != "" { + projected["security"] = security + } + + switch network { + case "raw": + if settings, ok := firstShareObject(source, "rawSettings", "tcpSettings"); ok { + if value := projectShareRAWSettings(settings); len(value) > 0 { + projected["rawSettings"] = value + } + } + case "kcp": + // KCP share links carry only the transport discriminator. + case "ws": + projectShareTransportSettings(projected, source, "wsSettings", "host", "path") + case "grpc": + projectShareTransportSettings(projected, source, "grpcSettings", "authority", "serviceName", "multiMode") + case "httpupgrade": + projectShareTransportSettings(projected, source, "httpupgradeSettings", "host", "path") + case "xhttp": + if settings, ok := firstShareObject(source, "xhttpSettings", "splithttpSettings"); ok { + value := map[string]any{} + copyShareFields(value, settings, "host", "path", "mode", "extra") + if len(value) > 0 { + projected["xhttpSettings"] = value + } + } + case "hysteria": + projectShareTransportSettings(projected, source, "hysteriaSettings", "version", "auth") + } + + switch security { + case "tls": + if settings, ok := shareObject(source["tlsSettings"]); ok { + value := map[string]any{} + copyShareFields(value, settings, + "serverName", "alpn", "fingerprint", "echConfigList", + "pinnedPeerCertSha256", "verifyPeerCertByName", + ) + if len(value) > 0 { + projected["tlsSettings"] = value + } + } + case "reality": + if settings, ok := shareObject(source["realitySettings"]); ok { + value := map[string]any{} + copyShareFields(value, settings, + "serverName", "fingerprint", "shortId", "mldsa65Verify", "spiderX", + ) + if password := shareString(settings["password"]); password != "" { + value["password"] = password + } else if publicKey := shareString(settings["publicKey"]); publicKey != "" { + value["password"] = publicKey + } + if len(value) > 0 { + projected["realitySettings"] = value + } + } + } + + if finalMask, ok := shareObject(source["finalmask"]); ok { + if value := projectShareFinalMask(finalMask); len(value) > 0 { + projected["finalmask"] = value + } + } + return projected, true +} + +func canonicalShareNetwork(network string) (string, bool) { + switch strings.ToLower(network) { + case "", "raw", "tcp": + return "raw", true + case "kcp", "mkcp": + return "kcp", true + case "ws", "websocket": + return "ws", true + case "grpc": + return "grpc", true + case "httpupgrade": + return "httpupgrade", true + case "xhttp", "splithttp": + return "xhttp", true + case "hysteria": + return "hysteria", true + default: + return "", false + } +} + +func projectShareRAWSettings(source map[string]any) map[string]any { + header, ok := shareObject(source["header"]) + if !ok || strings.ToLower(shareString(header["type"])) != "http" { + return nil + } + + projectedHeader := map[string]any{"type": "http"} + if request, ok := shareObject(header["request"]); ok { + projectedRequest := map[string]any{} + copyShareFields(projectedRequest, request, "path") + if headers, ok := shareObject(request["headers"]); ok { + projectedHeaders := map[string]any{} + copyShareFields(projectedHeaders, headers, "Host") + if len(projectedHeaders) > 0 { + projectedRequest["headers"] = projectedHeaders + } + } + if len(projectedRequest) > 0 { + projectedHeader["request"] = projectedRequest + } + } + return map[string]any{"header": projectedHeader} +} + +func projectShareTransportSettings(projected, source map[string]any, field string, keys ...string) { + settings, ok := shareObject(source[field]) + if !ok { + return + } + value := map[string]any{} + copyShareFields(value, settings, keys...) + if len(value) > 0 { + projected[field] = value + } +} + +func projectShareFinalMask(source map[string]any) map[string]any { + projected := map[string]any{} + for _, field := range []string{"tcp", "udp"} { + masks, ok := source[field].([]any) + if !ok { + continue + } + projectedMasks := make([]map[string]any, 0, len(masks)) + for _, item := range masks { + mask, ok := shareObject(item) + if !ok { + continue + } + maskType := shareString(mask["type"]) + if maskType == "" { + continue + } + projectedMask := map[string]any{"type": maskType} + if settings, exists := mask["settings"]; exists && !emptyShareValue(settings) { + projectedMask["settings"] = settings + } + projectedMasks = append(projectedMasks, projectedMask) + } + if len(projectedMasks) > 0 { + projected[field] = projectedMasks + } + } + + if params, ok := shareObject(source["quicParams"]); ok { + projectedParams := map[string]any{} + copyShareFields(projectedParams, params, "congestion", "brutalUp", "brutalDown") + if udpHop, ok := shareObject(params["udpHop"]); ok { + projectedUDPHop := map[string]any{} + copyShareFields(projectedUDPHop, udpHop, "ports") + if interval, exists := udpHop["interval"]; exists && !emptyShareRange(interval) { + projectedUDPHop["interval"] = interval + } + if len(projectedUDPHop) > 0 { + projectedParams["udpHop"] = projectedUDPHop + } + } + if len(projectedParams) > 0 { + projected["quicParams"] = projectedParams + } + } + return projected +} + +func validateProjectedShareOutbound(projected map[string]any) error { + raw, err := json.Marshal(projected) + if err != nil { + return err + } + var outbound conf.OutboundDetourConfig + if err := json.Unmarshal(raw, &outbound); err != nil { + return err + } + // sendThrough stores the node display name during share conversion. + outbound.SendThrough = nil + _, err = outbound.Build() + return err +} + +func copyShareFields(target, source map[string]any, fields ...string) { + for _, field := range fields { + if value, ok := source[field]; ok && !emptyShareValue(value) { + target[field] = value + } + } +} + +func firstShareObject(source map[string]any, fields ...string) (map[string]any, bool) { + for _, field := range fields { + if object, ok := shareObject(source[field]); ok { + return object, true + } + } + return nil, false +} + +func shareObject(value any) (map[string]any, bool) { + object, ok := value.(map[string]any) + return object, ok +} + +func shareString(value any) string { + text, _ := value.(string) + return text +} + +func emptyShareValue(value any) bool { + switch value := value.(type) { + case nil: + return true + case string: + return value == "" + case bool: + return !value + case json.Number: + number, err := strconv.ParseFloat(value.String(), 64) + return err == nil && number == 0 + case []any: + return len(value) == 0 + case map[string]any: + return len(value) == 0 + default: + return false + } +} + +func emptyShareRange(value any) bool { + if text, ok := value.(string); ok { + number, err := strconv.ParseInt(text, 10, 32) + return err == nil && number == 0 + } + return emptyShareValue(value) +} diff --git a/share/marshal_share_test.go b/share/marshal_share_test.go new file mode 100644 index 00000000..e915bcba --- /dev/null +++ b/share/marshal_share_test.go @@ -0,0 +1,141 @@ +package share + +import ( + "encoding/base64" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xtls/xray-core/infra/conf" +) + +func TestMarshalShareConfigJSONProjectsSupportedFields(t *testing.T) { + const input = `{ + "log":{"loglevel":"warning"}, + "outbounds":[ + {"protocol":"freedom","settings":{}}, + { + "protocol":"vless", + "settings":{"address":"drop.example","port":443,"id":"12345678-abcd-abcd-abcd-123456789abc","encryption":"none"}, + "streamSettings":{"network":"quic","security":"tls","tlsSettings":{"fingerprint":"chrome"}} + }, + { + "protocol":"vless", + "settings":{"address":"drop.example","port":443,"id":"12345678-abcd-abcd-abcd-123456789abc","encryption":"none"}, + "streamSettings":{"network":"raw","security":"xtls"} + }, + { + "protocol":"VLESS", + "sendThrough":"Node name", + "tag":"tag fallback", + "settings":{ + "address":"example.com","port":443,"id":"12345678-abcd-abcd-abcd-123456789abc", + "flow":"","encryption":"none","level":1,"email":"drop@example.com","seed":"drop","reverse":{} + }, + "streamSettings":{ + "address":"drop.example","port":8443,"network":"splithttp","security":"REALITY", + "xhttpSettings":{ + "host":"cdn.example.com","path":"/xhttp","mode":"stream-up","headers":{"X-Drop":"yes"}, + "extra":{"maxConcurrency":"1-2","empty":null} + }, + "realitySettings":{ + "show":false,"target":null,"dest":null,"serverNames":null,"privateKey":"", + "serverName":"example.com","fingerprint":"chrome","password":"", + "publicKey":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","shortId":"abcd","spiderX":"/" + }, + "sockopt":{"dialerProxy":"drop"} + }, + "proxySettings":{"tag":"drop"},"mux":{"enabled":true},"targetStrategy":"UseIP" + } + ] + }` + + var config conf.Config + require.NoError(t, json.Unmarshal([]byte(input), &config)) + raw, err := MarshalShareConfigJSON(&config) + require.NoError(t, err) + + const expected = `{ + "outbounds":[{ + "protocol":"vless", + "sendThrough":"Node name", + "tag":"tag fallback", + "settings":{"address":"example.com","port":443,"id":"12345678-abcd-abcd-abcd-123456789abc","encryption":"none"}, + "streamSettings":{ + "network":"xhttp","security":"reality", + "xhttpSettings":{"host":"cdn.example.com","path":"/xhttp","mode":"stream-up","extra":{"maxConcurrency":"1-2","empty":null}}, + "realitySettings":{"serverName":"example.com","fingerprint":"chrome","password":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","shortId":"abcd","spiderX":"/"} + } + }] + }` + assert.JSONEq(t, expected, string(raw)) + requireProjectedOutboundsBuild(t, raw) +} + +func TestMarshalShareConfigJSONPreservesHysteriaPortHopping(t *testing.T) { + config, err := ConvertShareLinksToXrayJson( + "hy2://auth@host:443?up=50+mbps&down=100+mbps&ports=20000-40000&hop-interval=30&sni=example.com&fp=chrome", + ) + require.NoError(t, err) + raw, err := MarshalShareConfigJSON(config) + require.NoError(t, err) + + var document map[string]any + require.NoError(t, json.Unmarshal(raw, &document)) + outbound := document["outbounds"].([]any)[0].(map[string]any) + stream := outbound["streamSettings"].(map[string]any) + finalMask := stream["finalmask"].(map[string]any) + quicParams := finalMask["quicParams"].(map[string]any) + udpHop := quicParams["udpHop"].(map[string]any) + assert.Equal(t, "20000-40000", udpHop["ports"]) + assert.Equal(t, "30", udpHop["interval"]) + requireProjectedOutboundsBuild(t, raw) +} + +func TestMarshalShareConfigJSONKeepsKCPWithoutSettings(t *testing.T) { + qr := `{"ps":"k","add":"kcp.host","port":"8391","id":"` + testShareUUID + `","net":"kcp","path":"seedval","type":"wireguard"}` + link := "vmess://" + base64.StdEncoding.EncodeToString([]byte(qr)) + config, err := ConvertShareLinksToXrayJson(link) + require.NoError(t, err) + raw, err := MarshalShareConfigJSON(config) + require.NoError(t, err) + + assert.Contains(t, string(raw), `"network":"kcp"`) + assert.NotContains(t, string(raw), "kcpSettings") + assert.NotContains(t, string(raw), "seedval") + assert.NotContains(t, string(raw), "wireguard") + requireProjectedOutboundsBuild(t, raw) +} + +func TestMarshalShareConfigJSONSupportedProtocolsBuild(t *testing.T) { + tests := map[string]string{ + "shadowsocks": "ss://" + ssUserB64("chacha20-ietf-poly1305", "password") + "@10.0.0.1:8388", + "vmess": "vmess://" + testShareUUID + "@vm.example:443?encryption=auto&type=raw", + "vless": "vless://" + testShareUUID + "@10.0.0.1:443?encryption=none&security=none", + "socks": "socks://" + base64.StdEncoding.EncodeToString([]byte("user:password")) + "@127.0.0.1:1080", + "trojan": "trojan://password@trojan.example:443?sni=trojan.example", + "hysteria": "hy2://auth@hysteria.example:443?sni=hysteria.example&fp=chrome", + } + for protocol, link := range tests { + t.Run(protocol, func(t *testing.T) { + config, err := ConvertShareLinksToXrayJson(link) + require.NoError(t, err) + raw, err := MarshalShareConfigJSON(config) + require.NoError(t, err) + requireProjectedOutboundsBuild(t, raw) + }) + } +} + +func requireProjectedOutboundsBuild(t *testing.T, raw json.RawMessage) { + t.Helper() + var config conf.Config + require.NoError(t, json.Unmarshal(raw, &config)) + require.NotEmpty(t, config.OutboundConfigs) + for index := range config.OutboundConfigs { + config.OutboundConfigs[index].SendThrough = nil + _, err := config.OutboundConfigs[index].Build() + require.NoError(t, err) + } +} From 18ea5babb26de012bdba82a6a0b399120843176a Mon Sep 17 00:00:00 2001 From: yiguo Date: Wed, 26 Aug 2026 19:03:27 +0800 Subject: [PATCH 6/7] Update libXray build workflow --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49c04183..79bbc19b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 @@ -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 @@ -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 From fda2260d76c92f0141f9fa60a19d6886060665c7 Mon Sep 17 00:00:00 2001 From: yiguo Date: Wed, 26 Aug 2026 19:06:01 +0800 Subject: [PATCH 7/7] Update workflow action versions --- .github/workflows/build.yml | 2 +- .github/workflows/libxray-test.yml | 4 ++-- .github/workflows/release-go-mirror.yml | 2 +- .github/workflows/validate.yml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 79bbc19b..36bdc62f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }} diff --git a/.github/workflows/libxray-test.yml b/.github/workflows/libxray-test.yml index 044b999c..32dcc8a8 100644 --- a/.github/workflows/libxray-test.yml +++ b/.github/workflows/libxray-test.yml @@ -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 diff --git a/.github/workflows/release-go-mirror.yml b/.github/workflows/release-go-mirror.yml index d45255c5..a24f2ed5 100644 --- a/.github/workflows/release-go-mirror.yml +++ b/.github/workflows/release-go-mirror.yml @@ -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 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6fd42472..7f67360e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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