add ech support (#3310)

Co-authored-by: Alireza Ahmadi <alireza7@gmail.com>
This commit is contained in:
Sanaei 2025-08-04 16:27:57 +02:00 committed by GitHub
parent 6ff555c8bb
commit e4ba5ba53a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 89 additions and 3 deletions

View file

@ -560,6 +560,8 @@ class TlsStreamSettings extends XrayCommonClass {
enableSessionResumption = false, enableSessionResumption = false,
certificates = [new TlsStreamSettings.Cert()], certificates = [new TlsStreamSettings.Cert()],
alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1], alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1],
echServerKeys = '',
echForceQuery = 'none',
settings = new TlsStreamSettings.Settings() settings = new TlsStreamSettings.Settings()
) { ) {
super(); super();
@ -573,6 +575,8 @@ class TlsStreamSettings extends XrayCommonClass {
this.enableSessionResumption = enableSessionResumption; this.enableSessionResumption = enableSessionResumption;
this.certs = certificates; this.certs = certificates;
this.alpn = alpn; this.alpn = alpn;
this.echServerKeys = echServerKeys;
this.echForceQuery = echForceQuery;
this.settings = settings; this.settings = settings;
} }
@ -592,7 +596,7 @@ class TlsStreamSettings extends XrayCommonClass {
} }
if (!ObjectUtil.isEmpty(json.settings)) { 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( return new TlsStreamSettings(
json.serverName, json.serverName,
@ -605,6 +609,8 @@ class TlsStreamSettings extends XrayCommonClass {
json.enableSessionResumption, json.enableSessionResumption,
certs, certs,
json.alpn, json.alpn,
json.echServerKeys,
json.echForceQuery,
settings, settings,
); );
} }
@ -621,6 +627,8 @@ class TlsStreamSettings extends XrayCommonClass {
enableSessionResumption: this.enableSessionResumption, enableSessionResumption: this.enableSessionResumption,
certificates: TlsStreamSettings.toJsonArray(this.certs), certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn, alpn: this.alpn,
echServerKeys: this.echServerKeys,
echForceQuery: this.echForceQuery,
settings: this.settings, settings: this.settings,
}; };
} }
@ -701,21 +709,25 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor( constructor(
allowInsecure = false, allowInsecure = false,
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME, fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
echConfigList = '',
) { ) {
super(); super();
this.allowInsecure = allowInsecure; this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.echConfigList = echConfigList;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new TlsStreamSettings.Settings( return new TlsStreamSettings.Settings(
json.allowInsecure, json.allowInsecure,
json.fingerprint, json.fingerprint,
json.echConfigList,
); );
} }
toJson() { toJson() {
return { return {
allowInsecure: this.allowInsecure, allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
echConfigList: this.echConfigList
}; };
} }
}; };
@ -1375,6 +1387,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", 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)) { if (type == "tcp" && !ObjectUtil.isEmpty(flow)) {
params.set("flow", flow); params.set("flow", flow);
} }
@ -1474,6 +1489,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.tls.settings.allowInsecure) { if (this.stream.tls.settings.allowInsecure) {
params.set("allowInsecure", "1"); 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)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", 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) { if (this.stream.tls.settings.allowInsecure) {
params.set("allowInsecure", "1"); 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)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni); params.set("sni", this.stream.tls.sni);
} }

View file

@ -354,13 +354,15 @@ class TlsStreamSettings extends CommonClass {
serverName = '', serverName = '',
alpn = [], alpn = [],
fingerprint = '', fingerprint = '',
allowInsecure = false allowInsecure = false,
echConfigList = '',
) { ) {
super(); super();
this.serverName = serverName; this.serverName = serverName;
this.alpn = alpn; this.alpn = alpn;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.allowInsecure = allowInsecure; this.allowInsecure = allowInsecure;
this.echConfigList = echConfigList;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@ -369,6 +371,7 @@ class TlsStreamSettings extends CommonClass {
json.alpn, json.alpn,
json.fingerprint, json.fingerprint,
json.allowInsecure, json.allowInsecure,
json.echConfigList,
); );
} }
@ -378,6 +381,7 @@ class TlsStreamSettings extends CommonClass {
alpn: this.alpn, alpn: this.alpn,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
allowInsecure: this.allowInsecure, allowInsecure: this.allowInsecure,
echConfigList: this.echConfigList
}; };
} }
} }
@ -782,7 +786,8 @@ class Outbound extends CommonClass {
let alpn = url.searchParams.get('alpn'); let alpn = url.searchParams.get('alpn');
let allowInsecure = url.searchParams.get('allowInsecure'); let allowInsecure = url.searchParams.get('allowInsecure');
let sni = url.searchParams.get('sni') ?? ''; 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') { if (security == 'reality') {

View file

@ -51,6 +51,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/importDB", a.importDB) g.POST("/importDB", a.importDB)
g.POST("/getNewX25519Cert", a.getNewX25519Cert) g.POST("/getNewX25519Cert", a.getNewX25519Cert)
g.POST("/getNewmldsa65", a.getNewmldsa65) g.POST("/getNewmldsa65", a.getNewmldsa65)
g.POST("/getNewEchCert", a.getNewEchCert)
} }
func (a *ServerController) refreshStatus() { func (a *ServerController) refreshStatus() {
@ -208,3 +209,13 @@ func (a *ServerController) getNewmldsa65(c *gin.Context) {
} }
jsonObj(c, cert, nil) 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)
}

View file

@ -106,6 +106,21 @@
<a-switch v-model="cert.buildChain"></a-switch> <a-switch v-model="cert.buildChain"></a-switch>
</a-form-item> </a-form-item>
</template> </template>
<a-form-item label='ECH key'>
<a-input v-model="inbound.stream.tls.echServerKeys"></a-input>
</a-form-item>
<a-form-item label='ECH config'>
<a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
</a-form-item>
<a-form-item label='ECH force query'>
<a-select v-model="inbound.stream.tls.echForceQuery"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
</a-form-item>
</template> </template>
<!-- reality settings --> <!-- reality settings -->

View file

@ -152,6 +152,16 @@
inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed; inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify; 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;
},
}, },
}); });

View file

@ -743,3 +743,27 @@ func (s *ServerService) GetNewmldsa65() (any, error) {
return keyPair, nil 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
}