From d0d745692d68c34ccbdbee0b46726d4784cb835b Mon Sep 17 00:00:00 2001 From: Alireza Ahmadi Date: Mon, 4 Aug 2025 16:01:07 +0200 Subject: [PATCH] add ech support --- web/assets/js/model/inbound.js | 23 ++++++++++++++++++++++- web/assets/js/model/outbound.js | 9 +++++++-- web/controller/server.go | 11 +++++++++++ web/html/form/tls_settings.html | 15 +++++++++++++++ web/html/modals/inbound_modal.html | 10 ++++++++++ web/service/server.go | 24 ++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 3 deletions(-) diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index 09073593..da5522a5 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -560,6 +560,8 @@ class TlsStreamSettings extends XrayCommonClass { enableSessionResumption = false, certificates = [new TlsStreamSettings.Cert()], alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1], + echServerKeys = '', + echForceQuery = 'none', settings = new TlsStreamSettings.Settings() ) { super(); @@ -573,6 +575,8 @@ class TlsStreamSettings extends XrayCommonClass { this.enableSessionResumption = enableSessionResumption; this.certs = certificates; this.alpn = alpn; + this.echServerKeys = echServerKeys; + this.echForceQuery = echForceQuery; this.settings = settings; } @@ -592,7 +596,7 @@ class TlsStreamSettings extends XrayCommonClass { } if (!ObjectUtil.isEmpty(json.settings)) { - settings = new TlsStreamSettings.Settings(json.settings.allowInsecure, json.settings.fingerprint, json.settings.serverName, json.settings.domains); + settings = new TlsStreamSettings.Settings(json.settings.allowInsecure, json.settings.fingerprint, json.settings.echConfigList); } return new TlsStreamSettings( json.serverName, @@ -605,6 +609,8 @@ class TlsStreamSettings extends XrayCommonClass { json.enableSessionResumption, certs, json.alpn, + json.echServerKeys, + json.echForceQuery, settings, ); } @@ -621,6 +627,8 @@ class TlsStreamSettings extends XrayCommonClass { enableSessionResumption: this.enableSessionResumption, certificates: TlsStreamSettings.toJsonArray(this.certs), alpn: this.alpn, + echServerKeys: this.echServerKeys, + echForceQuery: this.echForceQuery, settings: this.settings, }; } @@ -701,21 +709,25 @@ TlsStreamSettings.Settings = class extends XrayCommonClass { constructor( allowInsecure = false, fingerprint = UTLS_FINGERPRINT.UTLS_CHROME, + echConfigList = '', ) { super(); this.allowInsecure = allowInsecure; this.fingerprint = fingerprint; + this.echConfigList = echConfigList; } static fromJson(json = {}) { return new TlsStreamSettings.Settings( json.allowInsecure, json.fingerprint, + json.echConfigList, ); } toJson() { return { allowInsecure: this.allowInsecure, fingerprint: this.fingerprint, + echConfigList: this.echConfigList }; } }; @@ -1375,6 +1387,9 @@ class Inbound extends XrayCommonClass { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { params.set("sni", this.stream.tls.sni); } + if (this.stream.tls.settings.echConfigList?.length > 0) { + params.set("ech", this.stream.tls.settings.echConfigList); + } if (type == "tcp" && !ObjectUtil.isEmpty(flow)) { params.set("flow", flow); } @@ -1474,6 +1489,9 @@ class Inbound extends XrayCommonClass { if (this.stream.tls.settings.allowInsecure) { params.set("allowInsecure", "1"); } + if (this.stream.tls.settings.echConfigList?.length > 0) { + params.set("ech", this.stream.tls.settings.echConfigList); + } if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { params.set("sni", this.stream.tls.sni); } @@ -1552,6 +1570,9 @@ class Inbound extends XrayCommonClass { if (this.stream.tls.settings.allowInsecure) { params.set("allowInsecure", "1"); } + if (this.stream.tls.settings.echConfigList?.length > 0) { + params.set("ech", this.stream.tls.settings.echConfigList); + } if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { params.set("sni", this.stream.tls.sni); } diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index 03569b00..a42c400d 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -354,13 +354,15 @@ class TlsStreamSettings extends CommonClass { serverName = '', alpn = [], fingerprint = '', - allowInsecure = false + allowInsecure = false, + echConfigList = '', ) { super(); this.serverName = serverName; this.alpn = alpn; this.fingerprint = fingerprint; this.allowInsecure = allowInsecure; + this.echConfigList = echConfigList; } static fromJson(json = {}) { @@ -369,6 +371,7 @@ class TlsStreamSettings extends CommonClass { json.alpn, json.fingerprint, json.allowInsecure, + json.echConfigList, ); } @@ -378,6 +381,7 @@ class TlsStreamSettings extends CommonClass { alpn: this.alpn, fingerprint: this.fingerprint, allowInsecure: this.allowInsecure, + echConfigList: this.echConfigList }; } } @@ -782,7 +786,8 @@ class Outbound extends CommonClass { let alpn = url.searchParams.get('alpn'); let allowInsecure = url.searchParams.get('allowInsecure'); let sni = url.searchParams.get('sni') ?? ''; - stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1); + let ech = url.searchParams.get('ech') ?? ''; + stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1, ech); } if (security == 'reality') { diff --git a/web/controller/server.go b/web/controller/server.go index 22f89f2f..8a7a2198 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -51,6 +51,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { g.POST("/importDB", a.importDB) g.POST("/getNewX25519Cert", a.getNewX25519Cert) g.POST("/getNewmldsa65", a.getNewmldsa65) + g.POST("/getNewEchCert", a.getNewEchCert) } func (a *ServerController) refreshStatus() { @@ -208,3 +209,13 @@ func (a *ServerController) getNewmldsa65(c *gin.Context) { } jsonObj(c, cert, nil) } + +func (a *ServerController) getNewEchCert(c *gin.Context) { + sni := c.PostForm("sni") + cert, err := a.serverService.GetNewEchCert(sni) + if err != nil { + jsonMsg(c, "get ech certificate", err) + return + } + jsonObj(c, cert, nil) +} diff --git a/web/html/form/tls_settings.html b/web/html/form/tls_settings.html index 0de6dae5..3e61b5a2 100644 --- a/web/html/form/tls_settings.html +++ b/web/html/form/tls_settings.html @@ -106,6 +106,21 @@ + + + + + + + + + [[ key ]] + + + + Get New ECH Cert + diff --git a/web/html/modals/inbound_modal.html b/web/html/modals/inbound_modal.html index f11df2e2..b77e74e2 100644 --- a/web/html/modals/inbound_modal.html +++ b/web/html/modals/inbound_modal.html @@ -152,6 +152,16 @@ inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed; inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify; }, + async getNewEchCert() { + inModal.loading(true); + const msg = await HttpUtil.post('/server/getNewEchCert', {sni: inModal.inbound.stream.tls.sni}); + inModal.loading(false); + if (!msg.success) { + return; + } + inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys; + inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList; + }, }, }); diff --git a/web/service/server.go b/web/service/server.go index e75a97b8..f0be46c2 100644 --- a/web/service/server.go +++ b/web/service/server.go @@ -743,3 +743,27 @@ func (s *ServerService) GetNewmldsa65() (any, error) { return keyPair, nil } + +func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) { + // Run the command + cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, err + } + + lines := strings.Split(out.String(), "\n") + if len(lines) < 4 { + return nil, common.NewError("invalid ech cert") + } + + configList := lines[1] + serverKeys := lines[3] + + return map[string]interface{}{ + "echServerKeys": serverKeys, + "echConfigList": configList, + }, nil +}