mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-18 16:17:16 +00:00
feat(hosts): add a cipher suites override and accept custom suites
The inbound TLS form offered cipherSuites as a closed single-choice list, but xray reads the value as a colon-separated list and accepts any name Go knows, so several suites or one missing from the list could not be set. Both the inbound and the new host field now use a tag picker that keeps the stored value as the colon-joined string xray expects; old single values open unchanged. A host's cipher suites replace the inbound's in the JSON subscription stream, and a blank field inherits them. Share links and Clash carry no cipher suite parameter, so their output is unchanged.
This commit is contained in:
@@ -1083,6 +1083,7 @@ type Host struct {
|
||||
Path string `json:"path" form:"path"`
|
||||
Alpn []string `json:"alpn" form:"alpn" gorm:"serializer:json"`
|
||||
Fingerprint string `json:"fingerprint" form:"fingerprint"`
|
||||
CipherSuites string `json:"cipherSuites" form:"cipherSuites" gorm:"column:cipher_suites"`
|
||||
OverrideSniFromAddress bool `json:"overrideSniFromAddress" form:"overrideSniFromAddress" gorm:"column:override_sni_from_address"`
|
||||
KeepSniBlank bool `json:"keepSniBlank" form:"keepSniBlank" gorm:"column:keep_sni_blank"`
|
||||
PinnedPeerCertSha256 []string `json:"pinnedPeerCertSha256" form:"pinnedPeerCertSha256" gorm:"serializer:json;column:pinned_peer_cert_sha256"`
|
||||
|
||||
@@ -71,6 +71,9 @@ func hostToExternalProxyMap(h *model.Host, defaultDest string, defaultPort int)
|
||||
if h.Fingerprint != "" {
|
||||
ep["fingerprint"] = h.Fingerprint
|
||||
}
|
||||
if h.CipherSuites != "" {
|
||||
ep["cipherSuites"] = h.CipherSuites
|
||||
}
|
||||
if len(h.Alpn) > 0 {
|
||||
ep["alpn"] = stringsToAnySlice(h.Alpn)
|
||||
}
|
||||
|
||||
@@ -442,3 +442,31 @@ func TestSub_HostTlsOverRealityDropsRealityParams(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A host's cipher suites override the inbound's own in the JSON subscription,
|
||||
// while a host that leaves the field blank inherits them.
|
||||
func TestSub_HostCipherSuitesJSON(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
ib := seedSubInbound(t, "s1", "cs", 4462, 1,
|
||||
`{"network":"tcp","security":"tls","tlsSettings":{"serverName":"base.sni","cipherSuites":"TLS_CHACHA20_POLY1305_SHA256"}}`)
|
||||
seedHost(t, &model.Host{
|
||||
InboundId: ib.Id, SortOrder: 0, Remark: "CS", Address: "cs.cdn.com", Port: 8443, Security: "tls",
|
||||
CipherSuites: "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256",
|
||||
})
|
||||
seedHost(t, &model.Host{
|
||||
InboundId: ib.Id, SortOrder: 1, Remark: "INHERIT", Address: "inh.cdn.com", Port: 8443, Security: "tls",
|
||||
})
|
||||
|
||||
out, _, err := NewSubJsonService("", "", "", "", NewSubService("")).GetJson("s1", "req.example.com", false)
|
||||
if err != nil {
|
||||
t.Fatalf("GetJson: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, `"cipherSuites": "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"`) &&
|
||||
!strings.Contains(out, `"cipherSuites":"TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"`) {
|
||||
t.Fatalf("json tlsSettings should carry the host's cipher suites:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, `"cipherSuites": "TLS_CHACHA20_POLY1305_SHA256"`) &&
|
||||
!strings.Contains(out, `"cipherSuites":"TLS_CHACHA20_POLY1305_SHA256"`) {
|
||||
t.Fatalf("a host with no cipher suites should inherit the inbound's:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2088,6 +2088,9 @@ func applyExternalProxyTLSToStream(ep map[string]any, stream map[string]any, sec
|
||||
if alpn, ok := externalProxyALPNList(ep["alpn"]); ok {
|
||||
tlsSettings["alpn"] = alpn
|
||||
}
|
||||
if cs, ok := ep["cipherSuites"].(string); ok && cs != "" {
|
||||
tlsSettings["cipherSuites"] = cs
|
||||
}
|
||||
if pins, ok := externalProxyPins(ep["pinnedPeerCertSha256"]); ok {
|
||||
settings, _ := tlsSettings["settings"].(map[string]any)
|
||||
if settings == nil {
|
||||
|
||||
@@ -382,6 +382,7 @@ type HostGroup struct {
|
||||
Path string `json:"path"`
|
||||
Alpn []string `json:"alpn"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
CipherSuites string `json:"cipherSuites"`
|
||||
OverrideSniFromAddress bool `json:"overrideSniFromAddress"`
|
||||
KeepSniBlank bool `json:"keepSniBlank"`
|
||||
PinnedPeerCertSha256 []string `json:"pinnedPeerCertSha256"`
|
||||
|
||||
@@ -45,6 +45,7 @@ func newHostGroup(h *model.Host, groupId string) *entity.HostGroup {
|
||||
Path: h.Path,
|
||||
Alpn: h.Alpn,
|
||||
Fingerprint: h.Fingerprint,
|
||||
CipherSuites: h.CipherSuites,
|
||||
OverrideSniFromAddress: h.OverrideSniFromAddress,
|
||||
KeepSniBlank: h.KeepSniBlank,
|
||||
PinnedPeerCertSha256: h.PinnedPeerCertSha256,
|
||||
@@ -133,6 +134,7 @@ func buildHostRows(groupId string, req *entity.HostGroup) []*model.Host {
|
||||
Path: req.Path,
|
||||
Alpn: req.Alpn,
|
||||
Fingerprint: req.Fingerprint,
|
||||
CipherSuites: req.CipherSuites,
|
||||
OverrideSniFromAddress: req.OverrideSniFromAddress,
|
||||
KeepSniBlank: req.KeepSniBlank,
|
||||
PinnedPeerCertSha256: req.PinnedPeerCertSha256,
|
||||
|
||||
@@ -365,3 +365,27 @@ func TestUpdateHostGroup_ValidateBeforeDelete(t *testing.T) {
|
||||
t.Fatalf("remark not updated: %s", got2.Remark)
|
||||
}
|
||||
}
|
||||
|
||||
// Host fields are copied by hand in buildHostRows and newHostGroup; a missed
|
||||
// copy on either side silently blanks the value on the next edit-and-save.
|
||||
func TestHostGroup_CipherSuitesRoundTrip(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
svc := &HostService{}
|
||||
ib := mkInbound(t, 443, model.VLESS, `{"clients":[]}`)
|
||||
const suites = "TLS_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
||||
|
||||
created, err := svc.AddHostGroup(&entity.HostGroup{
|
||||
InboundIds: []int{ib.Id}, Remark: "cs", Hosts: []string{"cs.example.com"},
|
||||
Security: "tls", CipherSuites: suites,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("AddHostGroup: %v", err)
|
||||
}
|
||||
g, err := svc.GetHostGroup(created[0].GroupId)
|
||||
if err != nil {
|
||||
t.Fatalf("GetHostGroup: %v", err)
|
||||
}
|
||||
if g.CipherSuites != suites {
|
||||
t.Fatalf("CipherSuites = %q, want %q", g.CipherSuites, suites)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +596,7 @@
|
||||
"customSockopt": "sockopt مخصص",
|
||||
"addCustomOption": "إضافة خيار مخصص",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Cipher Suites",
|
||||
"cipherSuites": "مجموعات التشفير",
|
||||
"autoOption": "تلقائي",
|
||||
"minMaxVersion": "إصدار أدنى/أقصى",
|
||||
"rejectUnknownSni": "رفض SNI غير معروف",
|
||||
|
||||
@@ -596,7 +596,7 @@
|
||||
"customSockopt": "Sockopt personalizado",
|
||||
"addCustomOption": "Añadir opción personalizada",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Cipher Suites",
|
||||
"cipherSuites": "Conjuntos de cifrado",
|
||||
"autoOption": "Auto",
|
||||
"minMaxVersion": "Versión mín/máx",
|
||||
"rejectUnknownSni": "Rechazar SNI desconocido",
|
||||
|
||||
@@ -596,7 +596,7 @@
|
||||
"customSockopt": "Sockopt kustom",
|
||||
"addCustomOption": "Tambah opsi kustom",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Cipher Suites",
|
||||
"cipherSuites": "Rangkaian Sandi",
|
||||
"autoOption": "Otomatis",
|
||||
"minMaxVersion": "Versi Min/Maks",
|
||||
"rejectUnknownSni": "Tolak SNI tidak dikenal",
|
||||
|
||||
@@ -617,7 +617,7 @@
|
||||
"customSockopt": "カスタム sockopt",
|
||||
"addCustomOption": "カスタムオプション追加",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Cipher Suites",
|
||||
"cipherSuites": "暗号スイート",
|
||||
"autoOption": "自動",
|
||||
"minMaxVersion": "最小/最大バージョン",
|
||||
"rejectUnknownSni": "未知の SNI を拒否",
|
||||
|
||||
@@ -617,7 +617,7 @@
|
||||
"customSockopt": "Sockopt personalizado",
|
||||
"addCustomOption": "Adicionar opção personalizada",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Cipher Suites",
|
||||
"cipherSuites": "Conjuntos de cifras",
|
||||
"autoOption": "Auto",
|
||||
"minMaxVersion": "Versão mín/máx",
|
||||
"rejectUnknownSni": "Rejeitar SNI desconhecido",
|
||||
|
||||
@@ -619,7 +619,7 @@
|
||||
"customSockopt": "Пользовательский sockopt",
|
||||
"addCustomOption": "Добавить опцию",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Cipher Suites",
|
||||
"cipherSuites": "Наборы шифров",
|
||||
"autoOption": "Авто",
|
||||
"minMaxVersion": "Мин/Макс версия",
|
||||
"rejectUnknownSni": "Отклонить неизвестный SNI",
|
||||
|
||||
@@ -596,7 +596,7 @@
|
||||
"customSockopt": "Користувацький sockopt",
|
||||
"addCustomOption": "Додати опцію",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Cipher Suites",
|
||||
"cipherSuites": "Набори шифрів",
|
||||
"autoOption": "Авто",
|
||||
"minMaxVersion": "Мін/Макс версія",
|
||||
"rejectUnknownSni": "Відхиляти невідомий SNI",
|
||||
|
||||
@@ -617,7 +617,7 @@
|
||||
"customSockopt": "Sockopt tùy chỉnh",
|
||||
"addCustomOption": "Thêm tùy chọn",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Cipher Suites",
|
||||
"cipherSuites": "Bộ mật mã",
|
||||
"autoOption": "Tự động",
|
||||
"minMaxVersion": "Phiên bản Min/Max",
|
||||
"rejectUnknownSni": "Từ chối SNI lạ",
|
||||
|
||||
@@ -616,7 +616,7 @@
|
||||
"customSockopt": "自定义 sockopt",
|
||||
"addCustomOption": "添加自定义选项",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Cipher Suites",
|
||||
"cipherSuites": "密码套件",
|
||||
"autoOption": "自动",
|
||||
"minMaxVersion": "最小/最大版本",
|
||||
"rejectUnknownSni": "拒绝未知 SNI",
|
||||
|
||||
@@ -596,7 +596,7 @@
|
||||
"customSockopt": "自訂 sockopt",
|
||||
"addCustomOption": "新增自訂選項",
|
||||
"serverNameIndication": "SNI",
|
||||
"cipherSuites": "Cipher Suites",
|
||||
"cipherSuites": "加密套件",
|
||||
"autoOption": "自動",
|
||||
"minMaxVersion": "最小/最大版本",
|
||||
"rejectUnknownSni": "拒絕未知 SNI",
|
||||
|
||||
Reference in New Issue
Block a user