refactor(backend): retire hysteria2 as a top-level protocol

Hysteria v2 is not a separate xray protocol — it is plain "hysteria"
with streamSettings.version = 2. The frontend already dropped hysteria2
from the protocol enum in 5a90f7e3; the backend was still carrying the
literal as a compat alias.

Removed:
- model.Hysteria2 constant
- model.IsHysteria helper (only callers were buildProxy + genHysteriaLink)
- TestIsHysteria
- "hysteria2" from the Inbound.Protocol validate oneof enum
- All `case model.Hysteria, model.Hysteria2:` and `case "hysteria",
  "hysteria2":` branches across client.go, inbound.go, outbound.go,
  xray.go, port_conflict.go, xray/api.go, subService.go,
  subJsonService.go, subClashService.go
- Stale #4081 comments

Kept (correctly — these are client-side URI/config schemes that are
independent of the xray protocol type):
- hysteria2:// share-link URI in subService.genHysteriaLink
- "hysteria2" Clash proxy type in subClashService.buildHysteriaProxy
- Comments referring to Hysteria v2 as a transport version

Note: this change does not include a DB migration. Existing rows with
protocol = 'hysteria2' will fall through to the default switch arms
after upgrade. A separate `UPDATE inbounds SET protocol = 'hysteria'
WHERE protocol = 'hysteria2'` is required for installs that still hold
legacy data.
This commit is contained in:
MHSanaei
2026-05-27 00:58:37 +02:00
parent 15787dbdfe
commit d843014461
15 changed files with 52 additions and 82 deletions

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"sync"
@@ -70,7 +71,7 @@ func clientKeyForProtocol(p model.Protocol, rec *model.ClientRecord) string {
return rec.Password
case model.Shadowsocks:
return rec.Email
case model.Hysteria, model.Hysteria2:
case model.Hysteria:
return rec.Auth
default:
return rec.UUID
@@ -478,7 +479,7 @@ func (s *ClientService) fillProtocolDefaults(c *model.Client, ib *model.Inbound)
if c.Password == "" || !validShadowsocksClientKey(method, c.Password) {
c.Password = randomShadowsocksClientKey(method)
}
case model.Hysteria, model.Hysteria2:
case model.Hysteria:
if c.Auth == "" {
c.Auth = strings.ReplaceAll(uuid.NewString(), "-", "")
}
@@ -1064,12 +1065,7 @@ func clientMatchesInbound(c ClientWithAttachments, inboundId int) bool {
if inboundId <= 0 {
return true
}
for _, id := range c.InboundIds {
if id == inboundId {
return true
}
}
return false
return slices.Contains(c.InboundIds, inboundId)
}
func clientMatchesBucket(c ClientWithAttachments, bucket string, onlineSet map[string]struct{}, nowMs, expireDiffMs, trafficDiffBytes int64) bool {
@@ -1284,10 +1280,7 @@ func (s *ClientService) BulkAdjust(inboundSvc *InboundService, emails []string,
skippedReasons[email] = "unlimited traffic"
}
} else {
next := rec.TotalGB + addBytes
if next < 0 {
next = 0
}
next := max(rec.TotalGB+addBytes, 0)
entry.applyTotal = true
entry.newTotal = next
}
@@ -1410,7 +1403,7 @@ func (s *ClientService) bulkAdjustInboundClients(
clientKey = "password"
case model.Shadowsocks:
clientKey = "email"
case model.Hysteria, model.Hysteria2:
case model.Hysteria:
clientKey = "auth"
}
@@ -1690,7 +1683,7 @@ func (s *ClientService) bulkDelInboundClients(
clientKey = "password"
case model.Shadowsocks:
clientKey = "email"
case model.Hysteria, model.Hysteria2:
case model.Hysteria:
clientKey = "auth"
}
@@ -2105,7 +2098,7 @@ func (s *ClientService) AddInboundClient(inboundSvc *InboundService, data *model
if client.Email == "" {
return false, common.NewError("empty client ID")
}
case "hysteria", "hysteria2":
case "hysteria":
if client.Auth == "" {
return false, common.NewError("empty client ID")
}
@@ -2252,7 +2245,7 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
case "shadowsocks":
oldClientId = oldClient.Email
newClientId = clients[0].Email
case "hysteria", "hysteria2":
case "hysteria":
oldClientId = oldClient.Auth
newClientId = clients[0].Auth
default:
@@ -2274,7 +2267,7 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
lookupErr = database.GetDB().Where("password = ?", clientId).First(&rec).Error
case "shadowsocks":
lookupErr = database.GetDB().Where("email = ?", clientId).First(&rec).Error
case "hysteria", "hysteria2":
case "hysteria":
lookupErr = database.GetDB().Where("auth = ?", clientId).First(&rec).Error
default:
lookupErr = database.GetDB().Where("uuid = ?", clientId).First(&rec).Error
@@ -2512,7 +2505,7 @@ func (s *ClientService) DelInboundClient(inboundSvc *InboundService, inboundId i
client_key = "password"
case "shadowsocks":
client_key = "email"
case "hysteria", "hysteria2":
case "hysteria":
client_key = "auth"
}

View File

@@ -22,7 +22,7 @@ func TestSyncInbound_PreservesCredentialsAcrossProtocols(t *testing.T) {
if err := db.Create(vlessInbound).Error; err != nil {
t.Fatalf("create vless inbound: %v", err)
}
hysteriaInbound := &model.Inbound{Tag: "hy-in", Enable: true, Port: 10002, Protocol: model.Hysteria2}
hysteriaInbound := &model.Inbound{Tag: "hy-in", Enable: true, Port: 10002, Protocol: model.Hysteria}
if err := db.Create(hysteriaInbound).Error; err != nil {
t.Fatalf("create hysteria inbound: %v", err)
}

View File

@@ -452,7 +452,6 @@ func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
model.Trojan: true,
model.Shadowsocks: true,
model.Hysteria: true,
model.Hysteria2: true,
}
if !protocolsWithStream[inbound.Protocol] {
@@ -528,7 +527,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
if client.Email == "" {
return inbound, false, common.NewError("empty client ID")
}
case "hysteria", "hysteria2":
case "hysteria":
if client.Auth == "" {
return inbound, false, common.NewError("empty client ID")
}
@@ -2913,7 +2912,7 @@ func (s *InboundService) MigrationRequirements() {
// Fix inbounds based problems
var inbounds []*model.Inbound
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan", "shadowsocks", "hysteria", "hysteria2"}).Find(&inbounds).Error
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan", "shadowsocks", "hysteria"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return
}

View File

@@ -298,9 +298,9 @@ func extractOutboundEndpoints(ob map[string]any) []outboundEndpoint {
return nil
}
// Hysteria (and hysteria2 over trojan) is QUIC/UDP. Detect it via the
// outer protocol or via streamSettings.network so trojan-with-hysteria2
// transport gets probed over UDP too. kcp and quic are also UDP-based.
// Hysteria is QUIC/UDP — detect via the outer protocol or via
// streamSettings.network so a trojan-with-hysteria transport gets
// probed over UDP too. kcp and quic are also UDP-based.
network := "tcp"
if protocol == "hysteria" || protocol == "wireguard" {
network = "udp"

View File

@@ -28,7 +28,7 @@ func (b transportBits) conflicts(o transportBits) bool { return b&o != 0 }
// the validator never gets looser than the old port-only check.
//
// the rules:
// - hysteria, hysteria2, wireguard: udp regardless of streamSettings
// - hysteria, wireguard: udp regardless of streamSettings
// - streamSettings.network=kcp: udp
// - shadowsocks: whatever settings.network says ("tcp" / "udp" / "tcp,udp")
// - mixed (socks/http combo): tcp + udp when settings.udp is true
@@ -36,7 +36,7 @@ func (b transportBits) conflicts(o transportBits) bool { return b&o != 0 }
func inboundTransports(protocol model.Protocol, streamSettings, settings string) transportBits {
// protocols that ignore streamSettings entirely.
switch protocol {
case model.Hysteria, model.Hysteria2, model.WireGuard:
case model.Hysteria, model.WireGuard:
return transportUDP
}

View File

@@ -77,7 +77,6 @@ func TestInboundTransports(t *testing.T) {
{"trojan grpc is tcp", model.Trojan, `{"network":"grpc"}`, ``, transportTCP},
{"hysteria forced udp", model.Hysteria, `{"network":"tcp"}`, ``, transportUDP},
{"hysteria2 forced udp", model.Hysteria2, ``, ``, transportUDP},
{"wireguard forced udp", model.WireGuard, ``, ``, transportUDP},
{"shadowsocks tcp,udp", model.Shadowsocks, ``, `{"network":"tcp,udp"}`, transportTCP | transportUDP},
@@ -122,7 +121,7 @@ func TestListenOverlaps(t *testing.T) {
}
// the actual case from #4103: tcp/443 vless reality and udp/443
// hysteria2 must be allowed to coexist on the same port.
// hysteria must be allowed to coexist on the same port.
func TestCheckPortConflict_TCPandUDPCoexistOnSamePort(t *testing.T) {
setupConflictDB(t)
seedInboundConflict(t, "vless-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`)
@@ -132,7 +131,7 @@ func TestCheckPortConflict_TCPandUDPCoexistOnSamePort(t *testing.T) {
Tag: "hyst2-443-udp",
Listen: "0.0.0.0",
Port: 443,
Protocol: model.Hysteria2,
Protocol: model.Hysteria,
}
exist, err := svc.checkPortConflict(hyst2, 0)
if err != nil {
@@ -169,7 +168,7 @@ func TestCheckPortConflict_TCPCollidesWithTCP(t *testing.T) {
// conflict, since they fight for the same socket.
func TestCheckPortConflict_UDPCollidesWithUDP(t *testing.T) {
setupConflictDB(t)
seedInboundConflict(t, "hyst2-443", "0.0.0.0", 443, model.Hysteria2, ``, ``)
seedInboundConflict(t, "hyst2-443", "0.0.0.0", 443, model.Hysteria, ``, ``)
svc := &InboundService{}
wg := &model.Inbound{
@@ -210,7 +209,7 @@ func TestCheckPortConflict_ShadowsocksDualListenBlocksBoth(t *testing.T) {
Tag: "hyst2-443",
Listen: "0.0.0.0",
Port: 443,
Protocol: model.Hysteria2,
Protocol: model.Hysteria,
}
if exist, err := svc.checkPortConflict(udpClash, 0); err != nil || !exist {
t.Fatalf("udp inbound should clash with shadowsocks tcp,udp; exist=%v err=%v", exist, err)
@@ -281,7 +280,7 @@ func TestGenerateInboundTag_DisambiguatesByTransportOnSamePort(t *testing.T) {
udp := &model.Inbound{
Listen: "0.0.0.0",
Port: 443,
Protocol: model.Hysteria2,
Protocol: model.Hysteria,
}
got, err := svc.generateInboundTag(udp, 0)
if err != nil {
@@ -343,7 +342,7 @@ func TestGenerateInboundTag_SpecificListenSameDisambiguation(t *testing.T) {
udp := &model.Inbound{
Listen: "1.2.3.4",
Port: 443,
Protocol: model.Hysteria2,
Protocol: model.Hysteria,
}
got, err := svc.generateInboundTag(udp, 0)
if err != nil {
@@ -403,7 +402,7 @@ func TestCheckPortConflict_NodeScope(t *testing.T) {
func TestResolveInboundTag_RespectsCallerTagWhenFree(t *testing.T) {
setupConflictDB(t)
seedInboundConflictNode(t, "inbound-5000", "0.0.0.0", 5000, model.VLESS, `{"network":"tcp"}`, `{}`, nil)
seedInboundConflictNode(t, "inbound-5000-udp", "0.0.0.0", 5000, model.Hysteria2, ``, ``, nil)
seedInboundConflictNode(t, "inbound-5000-udp", "0.0.0.0", 5000, model.Hysteria, ``, ``, nil)
svc := &InboundService{}
pushed := &model.Inbound{
@@ -458,7 +457,7 @@ func TestResolveInboundTag_RegeneratesOnCollision(t *testing.T) {
Tag: "inbound-5000-tcp",
Listen: "0.0.0.0",
Port: 5000,
Protocol: model.Hysteria2,
Protocol: model.Hysteria,
StreamSettings: ``,
Settings: ``,
}

View File

@@ -183,7 +183,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if c.Security != "" {
entry["method"] = c.Security
}
case model.Hysteria, model.Hysteria2:
case model.Hysteria:
if c.Auth != "" {
entry["auth"] = c.Auth
}