mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-15 19:02:18 +03:00
Offer monthly calendar subscriptions an explicit last-valid-second display without moving their real billing boundary or spending renewal allowances. Keep the option off by default and limit conversion to a shared fixed day-1 midnight cutoff at an actual month transition in the panel timezone. Use the authoritative client calendar mode when aggregating node traffic, and share the header formatter across raw, JSON, and Clash exports. Expose the setting in the existing settings API/UI, regenerate its schemas, and document that clients may report expiry one second early or format the date differently in another timezone. Add HTTP, settings, and DST coverage. Stored deadlines, access enforcement, info/remark expiry values, and renewal accounting remain unchanged. Refs: #6516 Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com>
400 lines
19 KiB
Go
400 lines
19 KiB
Go
package entity
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"math"
|
|
"net"
|
|
"net/mail"
|
|
"net/netip"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
|
)
|
|
|
|
type Msg struct {
|
|
Success bool `json:"success"`
|
|
Msg string `json:"msg"`
|
|
Obj any `json:"obj"`
|
|
}
|
|
|
|
type AllSetting struct {
|
|
WebListen string `json:"webListen" form:"webListen"`
|
|
WebDomain string `json:"webDomain" form:"webDomain"`
|
|
WebPort int `json:"webPort" form:"webPort" validate:"gte=1,lte=65535"`
|
|
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
|
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
|
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
|
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge" validate:"gte=1,lte=525600"`
|
|
TrustedProxyCIDRs string `json:"trustedProxyCIDRs" form:"trustedProxyCIDRs"`
|
|
RealityScanCandidates string `json:"realityScanCandidates" form:"realityScanCandidates"`
|
|
IpLimitAllowlist string `json:"ipLimitAllowlist" form:"ipLimitAllowlist"`
|
|
PanelOutbound string `json:"panelOutbound" form:"panelOutbound"`
|
|
|
|
PageSize int `json:"pageSize" form:"pageSize" validate:"gte=0,lte=1000"`
|
|
ExpireDiff int `json:"expireDiff" form:"expireDiff" validate:"gte=0"`
|
|
TrafficDiff int `json:"trafficDiff" form:"trafficDiff" validate:"gte=0,lte=100"`
|
|
RemarkTemplate string `json:"remarkTemplate" form:"remarkTemplate"`
|
|
SubShowIdentityOnAllLinks bool `json:"subShowIdentityOnAllLinks" form:"subShowIdentityOnAllLinks"`
|
|
SubInfoNodeEnable bool `json:"subInfoNodeEnable" form:"subInfoNodeEnable"`
|
|
SubCalendarExpireInclusive bool `json:"subCalendarExpireInclusive" form:"subCalendarExpireInclusive"`
|
|
SubExpiredTemplate string `json:"subExpiredTemplate" form:"subExpiredTemplate"`
|
|
SubTrafficDepletedTemplate string `json:"subTrafficDepletedTemplate" form:"subTrafficDepletedTemplate"`
|
|
Datepicker string `json:"datepicker" form:"datepicker"`
|
|
|
|
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
|
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
|
TgBotProxy string `json:"tgBotProxy" form:"tgBotProxy"`
|
|
TgBotAPIServer string `json:"tgBotAPIServer" form:"tgBotAPIServer"`
|
|
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
|
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
|
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
|
TgCpu int `json:"tgCpu" form:"tgCpu" validate:"gte=0,lte=100"`
|
|
TgMemory int `json:"tgMemory" form:"tgMemory" validate:"gte=0,lte=100"`
|
|
TgLang string `json:"tgLang" form:"tgLang"`
|
|
TgEnabledEvents string `json:"tgEnabledEvents" form:"tgEnabledEvents"`
|
|
|
|
SmtpEnable bool `json:"smtpEnable" form:"smtpEnable"`
|
|
SmtpHost string `json:"smtpHost" form:"smtpHost"`
|
|
SmtpPort int `json:"smtpPort" form:"smtpPort" validate:"gte=1,lte=65535"`
|
|
SmtpUsername string `json:"smtpUsername" form:"smtpUsername"`
|
|
SmtpPassword string `json:"smtpPassword" form:"smtpPassword"`
|
|
SmtpFrom string `json:"smtpFrom" form:"smtpFrom"`
|
|
SmtpFromName string `json:"smtpFromName" form:"smtpFromName"`
|
|
SmtpTo string `json:"smtpTo" form:"smtpTo"`
|
|
SmtpEncryptionType string `json:"smtpEncryptionType" form:"smtpEncryptionType"`
|
|
SmtpEnabledEvents string `json:"smtpEnabledEvents" form:"smtpEnabledEvents"`
|
|
SmtpCpu int `json:"smtpCpu" form:"smtpCpu" validate:"gte=0,lte=100"`
|
|
SmtpMemory int `json:"smtpMemory" form:"smtpMemory" validate:"gte=0,lte=100"`
|
|
|
|
DiscordBotEnable bool `json:"discordBotEnable" form:"discordBotEnable"`
|
|
DiscordBotToken string `json:"discordBotToken" form:"discordBotToken"`
|
|
DiscordChannelId string `json:"discordChannelId" form:"discordChannelId"`
|
|
DiscordAdminIds string `json:"discordAdminIds" form:"discordAdminIds"`
|
|
DiscordRunTime string `json:"discordRunTime" form:"discordRunTime"`
|
|
DiscordBotBackup bool `json:"discordBotBackup" form:"discordBotBackup"`
|
|
DiscordCpu int `json:"discordCpu" form:"discordCpu" validate:"gte=0,lte=100"`
|
|
DiscordMemory int `json:"discordMemory" form:"discordMemory" validate:"gte=0,lte=100"`
|
|
DiscordLang string `json:"discordLang" form:"discordLang"`
|
|
DiscordEnabledEvents string `json:"discordEnabledEvents" form:"discordEnabledEvents"`
|
|
|
|
OutboundDownThreshold int `json:"outboundDownThreshold" form:"outboundDownThreshold" validate:"gte=1,lte=100"`
|
|
|
|
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
|
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
|
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
|
|
|
HappLinkEnable bool `json:"happLinkEnable" form:"happLinkEnable"`
|
|
SubEnable bool `json:"subEnable" form:"subEnable"`
|
|
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"`
|
|
SubJsonAutoDetect bool `json:"subJsonAutoDetect" form:"subJsonAutoDetect"`
|
|
SubJsonAlwaysArray bool `json:"subJsonAlwaysArray" form:"subJsonAlwaysArray"`
|
|
SubJsonUserAgentRegex string `json:"subJsonUserAgentRegex" form:"subJsonUserAgentRegex"`
|
|
SubClashAutoDetect bool `json:"subClashAutoDetect" form:"subClashAutoDetect"`
|
|
SubClashUserAgentRegex string `json:"subClashUserAgentRegex" form:"subClashUserAgentRegex"`
|
|
SubTitle string `json:"subTitle" form:"subTitle"`
|
|
SubSupportUrl string `json:"subSupportUrl" form:"subSupportUrl"`
|
|
SubProfileUrl string `json:"subProfileUrl" form:"subProfileUrl"`
|
|
SubAnnounce string `json:"subAnnounce" form:"subAnnounce"`
|
|
SubEnableRouting bool `json:"subEnableRouting" form:"subEnableRouting"`
|
|
SubRoutingRules string `json:"subRoutingRules" form:"subRoutingRules"`
|
|
SubIncyEnableRouting bool `json:"subIncyEnableRouting" form:"subIncyEnableRouting"`
|
|
SubIncyRoutingRules string `json:"subIncyRoutingRules" form:"subIncyRoutingRules"`
|
|
SubListen string `json:"subListen" form:"subListen"`
|
|
SubPort int `json:"subPort" form:"subPort" validate:"gte=1,lte=65535"`
|
|
SubPath string `json:"subPath" form:"subPath"`
|
|
SubDomain string `json:"subDomain" form:"subDomain"`
|
|
SubCertFile string `json:"subCertFile" form:"subCertFile"`
|
|
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
|
|
SubUpdates int `json:"subUpdates" form:"subUpdates" validate:"gte=0,lte=525600"`
|
|
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"`
|
|
ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"`
|
|
RestartXrayOnClientDisable bool `json:"restartXrayOnClientDisable" form:"restartXrayOnClientDisable"`
|
|
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
|
|
SubURI string `json:"subURI" form:"subURI"`
|
|
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
|
|
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
|
|
SubClashEnable bool `json:"subClashEnable" form:"subClashEnable"`
|
|
SubClashPath string `json:"subClashPath" form:"subClashPath"`
|
|
SubClashURI string `json:"subClashURI" form:"subClashURI"`
|
|
SubClashEnableRouting bool `json:"subClashEnableRouting" form:"subClashEnableRouting"`
|
|
SubClashRules string `json:"subClashRules" form:"subClashRules"`
|
|
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
|
|
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
|
SubJsonRoutingRules string `json:"subJsonRoutingRules" form:"subJsonRoutingRules"`
|
|
SubJsonDns string `json:"subJsonDns" form:"subJsonDns"`
|
|
SubJsonFinalMask string `json:"subJsonFinalMask" form:"subJsonFinalMask"`
|
|
SubJsonObservatory string `json:"subJsonObservatory" form:"subJsonObservatory"`
|
|
SubThemeDir string `json:"subThemeDir" form:"subThemeDir"`
|
|
SubHideSettings bool `json:"subHideSettings" form:"subHideSettings"`
|
|
|
|
// Happ client customization settings (app-management / routing / UX).
|
|
SubHappAutoDetect bool `json:"subHappAutoDetect" form:"subHappAutoDetect"`
|
|
SubHappProviderId string `json:"subHappProviderId" form:"subHappProviderId"`
|
|
SubHappNewUrl string `json:"subHappNewUrl" form:"subHappNewUrl"`
|
|
SubHappFallbackUrl string `json:"subHappFallbackUrl" form:"subHappFallbackUrl"`
|
|
SubHappSubInfoColor string `json:"subHappSubInfoColor" form:"subHappSubInfoColor"`
|
|
SubHappSubInfoText string `json:"subHappSubInfoText" form:"subHappSubInfoText"`
|
|
SubHappSubInfoButtonText string `json:"subHappSubInfoButtonText" form:"subHappSubInfoButtonText"`
|
|
SubHappSubInfoButtonLink string `json:"subHappSubInfoButtonLink" form:"subHappSubInfoButtonLink"`
|
|
SubHappSubExpire bool `json:"subHappSubExpire" form:"subHappSubExpire"`
|
|
SubHappSubExpireButtonLink string `json:"subHappSubExpireButtonLink" form:"subHappSubExpireButtonLink"`
|
|
SubHappNotificationExpire bool `json:"subHappNotificationExpire" form:"subHappNotificationExpire"`
|
|
SubHappNoLimit bool `json:"subHappNoLimit" form:"subHappNoLimit"`
|
|
SubHappAlwaysHwid bool `json:"subHappAlwaysHwid" form:"subHappAlwaysHwid"`
|
|
SubHappTunMode string `json:"subHappTunMode" form:"subHappTunMode"`
|
|
SubHappTunType string `json:"subHappTunType" form:"subHappTunType"`
|
|
SubHappExcludeRoutes string `json:"subHappExcludeRoutes" form:"subHappExcludeRoutes"`
|
|
SubHappExcludeApns bool `json:"subHappExcludeApns" form:"subHappExcludeApns"`
|
|
SubHappColorProfile string `json:"subHappColorProfile" form:"subHappColorProfile"`
|
|
SubHappPingType string `json:"subHappPingType" form:"subHappPingType"`
|
|
SubHappAutoConnect bool `json:"subHappAutoConnect" form:"subHappAutoConnect"`
|
|
SubHappAutoConnectType string `json:"subHappAutoConnectType" form:"subHappAutoConnectType"`
|
|
SubHappPerAppMode string `json:"subHappPerAppMode" form:"subHappPerAppMode"`
|
|
SubHappPerAppList string `json:"subHappPerAppList" form:"subHappPerAppList"`
|
|
|
|
LdapEnable bool `json:"ldapEnable" form:"ldapEnable"`
|
|
LdapHost string `json:"ldapHost" form:"ldapHost"`
|
|
LdapPort int `json:"ldapPort" form:"ldapPort" validate:"gte=0,lte=65535"`
|
|
LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"`
|
|
LdapInsecureSkipVerify bool `json:"ldapInsecureSkipVerify" form:"ldapInsecureSkipVerify"`
|
|
LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"`
|
|
LdapPassword string `json:"ldapPassword" form:"ldapPassword"`
|
|
LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"`
|
|
LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"`
|
|
LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"`
|
|
LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"`
|
|
LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"`
|
|
LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"`
|
|
LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"`
|
|
LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"`
|
|
LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"`
|
|
LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"`
|
|
LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"`
|
|
LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB" validate:"gte=0"`
|
|
LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays" validate:"gte=0"`
|
|
LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP" validate:"gte=0"`
|
|
|
|
WarpUpdateInterval int `json:"warpUpdateInterval" form:"warpUpdateInterval" validate:"gte=0"`
|
|
}
|
|
|
|
type AllSettingView struct {
|
|
AllSetting
|
|
|
|
HasTgBotToken bool `json:"hasTgBotToken"`
|
|
HasTwoFactorToken bool `json:"hasTwoFactorToken"`
|
|
HasLdapPassword bool `json:"hasLdapPassword"`
|
|
HasApiToken bool `json:"hasApiToken"`
|
|
HasWarpSecret bool `json:"hasWarpSecret"`
|
|
HasNordSecret bool `json:"hasNordSecret"`
|
|
HasSmtpPassword bool `json:"hasSmtpPassword"`
|
|
HasDiscordBotToken bool `json:"hasDiscordBotToken"`
|
|
}
|
|
|
|
func pathHasForbiddenChar(s string) bool {
|
|
for _, r := range s {
|
|
if r == '\\' || r == ' ' || r < 0x20 || r == 0x7f {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CheckNetipAddrOrPrefixList mirrors parseIpLimitAllowlist exactly: net and netip
|
|
// disagree (net accepts "/024", netip does not), so save and scan must share rules.
|
|
func CheckNetipAddrOrPrefixList(list, message string) error {
|
|
for entry := range strings.SplitSeq(list, ",") {
|
|
entry = strings.TrimSpace(entry)
|
|
if entry == "" {
|
|
continue
|
|
}
|
|
if _, err := netip.ParseAddr(entry); err == nil {
|
|
continue
|
|
}
|
|
if _, err := netip.ParsePrefix(entry); err != nil {
|
|
return common.NewError(message, entry)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkIPOrCIDRList rejects the first comma-separated entry that is neither a
|
|
// bare address nor a CIDR, naming it with the caller's message.
|
|
func checkIPOrCIDRList(list, message string) error {
|
|
for entry := range strings.SplitSeq(list, ",") {
|
|
entry = strings.TrimSpace(entry)
|
|
if entry == "" {
|
|
continue
|
|
}
|
|
if ip := net.ParseIP(entry); ip != nil {
|
|
continue
|
|
}
|
|
if _, _, err := net.ParseCIDR(entry); err != nil {
|
|
return common.NewError(message, entry)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *AllSetting) CheckValid() error {
|
|
if s.WebListen != "" {
|
|
ip := net.ParseIP(s.WebListen)
|
|
if ip == nil {
|
|
return common.NewError("web listen is not valid ip:", s.WebListen)
|
|
}
|
|
}
|
|
|
|
if s.SubListen != "" {
|
|
ip := net.ParseIP(s.SubListen)
|
|
if ip == nil {
|
|
return common.NewError("Sub listen is not valid ip:", s.SubListen)
|
|
}
|
|
}
|
|
|
|
if s.WebPort <= 0 || s.WebPort > math.MaxUint16 {
|
|
return common.NewError("web port is not a valid port:", s.WebPort)
|
|
}
|
|
|
|
if s.SubPort <= 0 || s.SubPort > math.MaxUint16 {
|
|
return common.NewError("Sub port is not a valid port:", s.SubPort)
|
|
}
|
|
|
|
if (s.SubPort == s.WebPort) && listenAddressesConflict(s.WebListen, s.SubListen) {
|
|
return common.NewError("Sub and Web could not use same ip:port, ", s.SubListen, ":", s.SubPort, " & ", s.WebListen, ":", s.WebPort)
|
|
}
|
|
|
|
if s.WebCertFile != "" || s.WebKeyFile != "" {
|
|
_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
|
|
if err != nil {
|
|
return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.WebCertFile, s.WebKeyFile, err)
|
|
}
|
|
}
|
|
|
|
if s.SubCertFile != "" || s.SubKeyFile != "" {
|
|
_, err := tls.LoadX509KeyPair(s.SubCertFile, s.SubKeyFile)
|
|
if err != nil {
|
|
return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.SubCertFile, s.SubKeyFile, err)
|
|
}
|
|
}
|
|
|
|
for _, p := range []struct {
|
|
name string
|
|
value string
|
|
}{
|
|
{"web base path", s.WebBasePath},
|
|
{"subscription path", s.SubPath},
|
|
{"subscription JSON path", s.SubJsonPath},
|
|
{"subscription Clash path", s.SubClashPath},
|
|
} {
|
|
if pathHasForbiddenChar(p.value) {
|
|
return common.NewError("URI path contains an invalid character:", p.name)
|
|
}
|
|
}
|
|
|
|
if !strings.HasPrefix(s.WebBasePath, "/") {
|
|
s.WebBasePath = "/" + s.WebBasePath
|
|
}
|
|
if !strings.HasSuffix(s.WebBasePath, "/") {
|
|
s.WebBasePath += "/"
|
|
}
|
|
if !strings.HasPrefix(s.SubPath, "/") {
|
|
s.SubPath = "/" + s.SubPath
|
|
}
|
|
if !strings.HasSuffix(s.SubPath, "/") {
|
|
s.SubPath += "/"
|
|
}
|
|
|
|
if !strings.HasPrefix(s.SubJsonPath, "/") {
|
|
s.SubJsonPath = "/" + s.SubJsonPath
|
|
}
|
|
if !strings.HasSuffix(s.SubJsonPath, "/") {
|
|
s.SubJsonPath += "/"
|
|
}
|
|
|
|
if !strings.HasPrefix(s.SubClashPath, "/") {
|
|
s.SubClashPath = "/" + s.SubClashPath
|
|
}
|
|
if !strings.HasSuffix(s.SubClashPath, "/") {
|
|
s.SubClashPath += "/"
|
|
}
|
|
|
|
if err := checkIPOrCIDRList(s.TrustedProxyCIDRs, "trusted proxy CIDR is not valid:"); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Rejected here rather than skipped at scan time: a typo in an allowlist
|
|
// entry silently leaves the address unprotected until a trusted network gets banned.
|
|
if err := CheckNetipAddrOrPrefixList(s.IpLimitAllowlist, "IP limit allowlist entry is not valid:"); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err := time.LoadLocation(s.TimeLocation)
|
|
if err != nil {
|
|
return common.NewError("time location not exist:", s.TimeLocation)
|
|
}
|
|
|
|
if s.SmtpFrom != "" {
|
|
if _, err := mail.ParseAddress(s.SmtpFrom); err != nil {
|
|
return common.NewError("SMTP from address is not valid:", s.SmtpFrom)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// listenAddressesConflict reports whether two listen addresses on the same port
|
|
// would collide at bind time. A wildcard listen ("", "0.0.0.0", "::") overlaps
|
|
// every address, so it conflicts with anything on that port; two specific
|
|
// addresses conflict only when identical.
|
|
func listenAddressesConflict(a, b string) bool {
|
|
if a == b {
|
|
return true
|
|
}
|
|
return isWildcardListen(a) || isWildcardListen(b)
|
|
}
|
|
|
|
func isWildcardListen(listen string) bool {
|
|
if listen == "" {
|
|
return true
|
|
}
|
|
if ip := net.ParseIP(listen); ip != nil {
|
|
return ip.IsUnspecified()
|
|
}
|
|
return false
|
|
}
|
|
|
|
type HostGroup struct {
|
|
GroupId string `json:"groupId"`
|
|
InboundIds []int `json:"inboundIds" validate:"required,min=1"`
|
|
Hosts []string `json:"hosts" validate:"omitempty"`
|
|
|
|
SortOrder int `json:"sortOrder"`
|
|
Remark string `json:"remark" validate:"required,max=256"`
|
|
ServerDescription string `json:"serverDescription" validate:"omitempty,max=64"`
|
|
IsDisabled bool `json:"isDisabled"`
|
|
IsHidden bool `json:"isHidden"`
|
|
Tags []string `json:"tags"`
|
|
Port int `json:"port" validate:"gte=0,lte=65535"`
|
|
Security string `json:"security" validate:"omitempty,oneof=same tls none reality"`
|
|
Sni string `json:"sni"`
|
|
HostHeader string `json:"hostHeader"`
|
|
Path string `json:"path"`
|
|
Alpn []string `json:"alpn"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
OverrideSniFromAddress bool `json:"overrideSniFromAddress"`
|
|
KeepSniBlank bool `json:"keepSniBlank"`
|
|
PinnedPeerCertSha256 []string `json:"pinnedPeerCertSha256"`
|
|
VerifyPeerCertByName string `json:"verifyPeerCertByName"`
|
|
AllowInsecure bool `json:"allowInsecure"`
|
|
EchConfigList string `json:"echConfigList"`
|
|
MuxParams string `json:"muxParams"`
|
|
SockoptParams string `json:"sockoptParams"`
|
|
FinalMask string `json:"finalMask"`
|
|
VlessRoute string `json:"vlessRoute"`
|
|
ExcludeFromSubTypes []string `json:"excludeFromSubTypes"`
|
|
NodeGuids []string `json:"nodeGuids"`
|
|
MihomoIpVersion string `json:"mihomoIpVersion" validate:"omitempty,oneof=dual ipv4 ipv6 ipv4-prefer ipv6-prefer"`
|
|
MihomoX25519 bool `json:"mihomoX25519"`
|
|
ShuffleHost bool `json:"shuffleHost"`
|
|
}
|