model

package
v0.15.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

Documentation

Overview

Package model holds the core domain types shared across the panel.

Index

Constants

View Source
const (
	RoleOperator = "operator" // end users, stats, journal — no settings, backups or API
	RoleAdmin    = "admin"    // everything except the admin roster
	RoleOwner    = "owner"    // everything, plus the roster; exactly one, undeletable
)

Roles form a ladder, not a permission matrix: each role can do everything the one below it can, plus more. That keeps the check on a route a rank comparison instead of a set lookup, and leaves no combination of checkboxes that locks the owner out of their own panel.

View Source
const (
	// Sessions.
	AuditLogin       = "admin.login"
	AuditLoginFailed = "admin.login_failed"
	AuditLogout      = "admin.logout"

	// The roster.
	AuditAdminCreated       = "admin.created"
	AuditAdminDeleted       = "admin.deleted"
	AuditAdminRoleChanged   = "admin.role_changed"
	AuditAdminPasswordReset = "admin.password_reset"

	// Your own account.
	AuditPasswordChanged    = "admin.password_changed"
	AuditCredentialsChanged = "admin.credentials_changed"

	// Settings — one action for all of them. Which section was touched goes in the
	// row's Target ("Маршрутизация", "DNS", …), not into a key of its own: a filter
	// with twenty near-identical entries is a filter nobody uses, and the answer the
	// owner wants ("who has been changing settings?") is one row type, not twenty.
	AuditSettings = "settings.changed"

	// Tariff plans.
	AuditPlanSaved    = "plan.saved"
	AuditPlanDeleted  = "plan.deleted"
	AuditPlanMigrated = "plan.migrated"

	// The API surface.
	AuditAPIKeyCreated  = "apikey.created"
	AuditAPIKeyRevoked  = "apikey.revoked"
	AuditWebhookCreated = "webhook.created"
	AuditWebhookUpdated = "webhook.updated"
	AuditWebhookDeleted = "webhook.deleted"

	// The panel itself.
	AuditXrayRestarted = "panel.xray_restarted"
	AuditStatsReset    = "panel.stats_reset"
	AuditBackupTaken   = "panel.backup_downloaded"
	AuditRestored      = "panel.restored"
	AuditFactoryReset  = "panel.factory_reset"
	AuditUpdated       = "panel.updated"
)

Audit action keys. Stable strings persisted in admin_audit.action — never renamed once shipped, or old rows lose their label.

View Source
const (
	AuditCatSession  = "session"
	AuditCatAdmins   = "admins"
	AuditCatSettings = "settings"
	AuditCatPlans    = "plans"
	AuditCatAPI      = "api"
	AuditCatPanel    = "panel"
)

Audit categories. What the journal is FILTERED by — a handful of areas instead of two dozen near-identical actions.

The actions themselves stay precise: "администратор удалён" and "администратор создан" are not the same event, and folding them into one key to shorten a dropdown would throw away the only thing the row is for. So the filter is unified, not the events: pick an area, read the exact action on each row.

View Source
const (
	ActorAdmin    = "admin"    // a panel session
	ActorAPIKey   = "apikey"   // the external REST API
	ActorTelegram = "telegram" // the admin Telegram bot
	ActorUser     = "user"     // the VPN user themself (user bot / subscription page)
	ActorSystem   = "system"   // the panel itself (cron, provider webhook)
)

Actor kinds — who performed the action.

View Source
const (
	EventUserCreated    = "user.created"
	EventUserRegistered = "user.registered" // self-registered via the user bot
	EventUserDeleted    = "user.deleted"
	EventUserRenamed    = "user.renamed"
	EventUserEnabled    = "user.enabled"
	EventUserDisabled   = "user.disabled"
	EventUserLimits     = "user.limits_changed"
	EventTrafficReset   = "user.traffic_reset"  // admin zeroed the counters
	EventQuotaReset     = "user.quota_reset"    // the automatic period rollover did
	EventResetPeriod    = "user.reset_period"   // autoreset period changed
	EventSubRotated     = "user.sub_rotated"    // subscription link reissued
	EventUserExpired    = "user.expired"        // system: subscription lapsed
	EventUserLimited    = "user.limited"        // system: quota exhausted
	EventDeviceLimited  = "user.device_limited" // system: too many devices
	EventTelegramLinked = "user.telegram_linked"
	EventTelegramUnlink = "user.telegram_unlinked"

	EventPlanChanged    = "plan.changed"
	EventPlanDowngraded = "plan.downgraded" // system: paid period ended → free plan
	EventPlanCancelled  = "plan.cancelled"

	EventPaymentCreated   = "payment.created"
	EventPaymentPaid      = "payment.paid"
	EventPaymentCancelled = "payment.cancelled"
)

Audit event keys. Stable strings persisted in user_events.action — never renamed once shipped, or old rows lose their label.

View Source
const (
	ACMEProviderLE      = "letsencrypt" // default; no EAB required
	ACMEProviderZeroSSL = "zerossl"     // requires EAB credentials from zerossl.com
)

ACME CA providers.

View Source
const (
	ProtoVLESS    = "VLESS-TCP-TLS"
	ProtoReality  = "VLESS-GRPC-REALITY"
	ProtoTrojan   = "TROJAN-WS"
	ProtoHysteria = "HYSTERIA-UDP"
)

Protocol display names. These appear in lockstep across the share-link "#label", the sing-box/Clash node tag/name, the subscription page, and the Connections UI; keeping them here as the single source stops those copies from drifting apart.

View Source
const (
	StatusActive        = "active"
	StatusDisabled      = "disabled"
	StatusExpired       = "expired"
	StatusLimited       = "limited"        // traffic quota exhausted
	StatusDeviceLimited = "device_limited" // too many concurrent devices
)

User status values derived on read (not stored).

View Source
const (
	WebhookUserCreated      = "user.created"        // created via panel or API
	WebhookUserDeleted      = "user.deleted"        //
	WebhookUserRegistered   = "user.registered"     // self-registered via the user bot
	WebhookUserExpired      = "user.expired"        // subscription lapsed
	WebhookUserLimited      = "user.limited"        // traffic quota exhausted
	WebhookUserDeviceLimit  = "user.device_limited" //
	WebhookPaymentCreated   = "payment.created"     // order opened
	WebhookPaymentPaid      = "payment.paid"        // order paid, plan applied
	WebhookPaymentCancelled = "payment.cancelled"   //
)

Webhook event keys. Stable strings sent in the payload's "event" field and the X-RosPanel-Event header; never renumbered/renamed once shipped.

View Source
const (
	AdminEventRegistered    int64 = 1 << 0 // a new user self-registered via the user bot
	AdminEventExpired       int64 = 1 << 1 // a user's subscription expired
	AdminEventLimited       int64 = 1 << 2 // a user exhausted their traffic quota
	AdminEventDeviceLimited int64 = 1 << 3 // a user exceeded their device limit
	AdminEventXrayDown      int64 = 1 << 4 // Xray crashed and is being restarted
	AdminEventCert          int64 = 1 << 5 // TLS certificate renewed or renewal failed
	AdminEventPayment       int64 = 1 << 6 // payment lifecycle (order created / paid)
)

Admin notification categories (bitmask flags stored in Settings.TGAdminEvents). The admin bot only pushes an event whose flag is set. New flags must be appended (never renumbered) so existing saved masks keep their meaning.

View Source
const AdminAuditRetentionDays = 90

AdminAuditRetentionDays matches the user journal's window.

View Source
const ConnectionRetentionDays = 30

ConnectionRetentionDays is how long a connections row outlives its last sighting. Only DeviceOnlineWindow matters for the device limit; the rest of the history exists purely for the per-user IP list in the UI, and a roaming mobile client accrues a row per IP indefinitely without a sweep.

View Source
const DeviceOnlineWindow int64 = 120

DeviceOnlineWindow is how long (seconds) a source IP counts as an active device. Matches the panel's online indicator (stats poll ~60s + access-log writes).

View Source
const LegacyProxyLaneID = "proxy"

LegacyProxyLaneID is the ID the pre-lanes proxy pool migrates into. It is deliberately the literal "proxy" — the string a pre-lanes RoutingOrder already uses for the pool — so a saved precedence keeps pointing at the same lane across the upgrade with no rewriting.

View Source
const MaxEgressLanes = 16

MaxEgressLanes caps how many lanes one config may define. Every active lane costs an Xray balancer plus an Observatory probe subject, so the ceiling keeps a hand-edited config from melting the box.

View Source
const TLSModeACME = "acme"

TLSModeACME is the only TLS mode: ACME (Let's Encrypt or ZeroSSL) for a domain or IP.

View Source
const TelegramLinkCodeTTL = 15 * time.Minute

TelegramLinkCodeTTL is how long a one-time Telegram bind code stays valid.

View Source
const UserEventRetentionDays = 90

UserEventRetentionDays is how long audit rows are kept before the retention sweep drops them.

Variables

View Source
var AdminAuditCatalog = []AdminAuditEntry{
	{AuditLogin, "Вход в панель", AuditCatSession},
	{AuditLoginFailed, "Неудачный вход", AuditCatSession},
	{AuditLogout, "Выход", AuditCatSession},

	{AuditAdminCreated, "Администратор создан", AuditCatAdmins},
	{AuditAdminDeleted, "Администратор удалён", AuditCatAdmins},
	{AuditAdminRoleChanged, "Роль изменена", AuditCatAdmins},
	{AuditAdminPasswordReset, "Пароль сброшен", AuditCatAdmins},
	{AuditPasswordChanged, "Смена своего пароля", AuditCatAdmins},
	{AuditCredentialsChanged, "Смена своих учётных данных", AuditCatAdmins},

	{AuditSettings, "Изменение настроек", AuditCatSettings},

	{AuditPlanSaved, "Тариф сохранён", AuditCatPlans},
	{AuditPlanDeleted, "Тариф удалён", AuditCatPlans},
	{AuditPlanMigrated, "Перенос пользователей тарифа", AuditCatPlans},

	{AuditAPIKeyCreated, "Ключ API создан", AuditCatAPI},
	{AuditAPIKeyRevoked, "Ключ API отозван", AuditCatAPI},
	{AuditWebhookCreated, "Вебхук создан", AuditCatAPI},
	{AuditWebhookUpdated, "Вебхук изменён", AuditCatAPI},
	{AuditWebhookDeleted, "Вебхук удалён", AuditCatAPI},

	{AuditXrayRestarted, "Перезапуск Xray", AuditCatPanel},
	{AuditStatsReset, "Сброс статистики", AuditCatPanel},
	{AuditBackupTaken, "Бэкап скачан", AuditCatPanel},
	{AuditRestored, "Восстановление из бэкапа", AuditCatPanel},
	{AuditFactoryReset, "Сброс к заводским", AuditCatPanel},
	{AuditUpdated, "Обновление панели", AuditCatPanel},
}

AdminAuditCatalog is the stable action list the journal UI iterates over (to render an action name, and to expand a category filter). Adding an event appends here.

View Source
var AdminAuditCategories = []struct{ Key, Label string }{
	{AuditCatSession, "Входы"},
	{AuditCatAdmins, "Администраторы"},
	{AuditCatSettings, "Настройки"},
	{AuditCatPlans, "Тарифы"},
	{AuditCatAPI, "API и вебхуки"},
	{AuditCatPanel, "Панель"},
}

AdminAuditCategories is the filter's list, in the order it renders.

View Source
var AdminEventCatalog = []struct {
	Key string
	Bit int64
}{
	{"registered", AdminEventRegistered},
	{"expired", AdminEventExpired},
	{"limited", AdminEventLimited},
	{"device_limited", AdminEventDeviceLimited},
	{"xray_down", AdminEventXrayDown},
	{"cert", AdminEventCert},
	{"payment", AdminEventPayment},
}

AdminEventCatalog is the stable key→flag mapping the settings API/UI iterate over. Order here is the display order in the panel.

View Source
var Fingerprints = []string{
	"firefox", "chrome", "safari", "edge", "ios", "android", "random", "randomized",
}

Fingerprints are the uTLS ClientHello fingerprints offered in the UI.

View Source
var GrantableRoles = []string{RoleAdmin, RoleOperator}

GrantableRoles are the roles an owner may hand out. RoleOwner is deliberately absent: ownership is singular and moves by transfer, never by grant, so there is no path that quietly produces a second owner.

View Source
var OperaCountries = []string{"EU", "AS", "AM"}

OperaCountries are the Opera VPN regions opera-proxy accepts.

View Source
var UserEventCatalog = []struct{ Key, Label string }{
	{EventUserCreated, "Пользователь создан"},
	{EventUserRegistered, "Саморегистрация"},
	{EventUserDeleted, "Пользователь удалён"},
	{EventUserRenamed, "Переименован"},
	{EventUserEnabled, "Включён"},
	{EventUserDisabled, "Отключён"},
	{EventUserLimits, "Изменены лимиты"},
	{EventTrafficReset, "Сброшен трафик"},
	{EventQuotaReset, "Автосброс квоты"},
	{EventResetPeriod, "Изменён период автосброса"},
	{EventSubRotated, "Обновлена ссылка подписки"},
	{EventUserExpired, "Подписка истекла"},
	{EventUserLimited, "Исчерпан трафик"},
	{EventDeviceLimited, "Превышен лимит устройств"},
	{EventTelegramLinked, "Telegram привязан"},
	{EventTelegramUnlink, "Telegram отвязан"},
	{EventPlanChanged, "Изменён тариф"},
	{EventPlanDowngraded, "Переведён на бесплатный тариф"},
	{EventPlanCancelled, "Подписка отменена"},
	{EventPaymentCreated, "Заказ создан"},
	{EventPaymentPaid, "Оплачено"},
	{EventPaymentCancelled, "Заказ отменён"},
}

UserEventCatalog is the stable key→label list the journal UI iterates over (for the filter dropdown and for rendering an action name). Adding an event appends here.

View Source
var WebhookEventCatalog = []struct{ Key, Label string }{
	{WebhookUserCreated, "Пользователь создан"},
	{WebhookUserDeleted, "Пользователь удалён"},
	{WebhookUserRegistered, "Саморегистрация"},
	{WebhookUserExpired, "Подписка истекла"},
	{WebhookUserLimited, "Исчерпан трафик"},
	{WebhookUserDeviceLimit, "Превышен лимит устройств"},
	{WebhookPaymentCreated, "Заказ создан"},
	{WebhookPaymentPaid, "Оплачено"},
	{WebhookPaymentCancelled, "Заказ отменён"},
}

WebhookEventCatalog is the stable key→label list the settings UI iterates over (display order). Adding an event appends here.

Functions

func AdminAuditActionsIn added in v0.14.0

func AdminAuditActionsIn(category string) []string

AdminAuditActionsIn returns every action key in a category — what a category filter expands to. An unknown category yields nothing, which the store reads as "match nothing" rather than "match everything": a filter that silently ignores itself and dumps the whole trail is worse than an empty page.

func AdminAuditLabel added in v0.14.0

func AdminAuditLabel(action string) string

AdminAuditLabel returns the human label for an action key, falling back to the key itself so a row written by a newer build still renders as something.

func BuiltinLanes added in v0.14.0

func BuiltinLanes() []string

BuiltinLanes returns the always-present egress lanes, in default precedence (the last one, "direct", is the default catch-all).

func GrantableRole added in v0.14.0

func GrantableRole(role string) bool

GrantableRole reports whether role is one an owner may assign to someone else.

func RoleAtLeast added in v0.14.0

func RoleAtLeast(role, tier string) bool

RoleAtLeast reports whether role clears the given tier.

func RoleLabel added in v0.14.0

func RoleLabel(role string) string

RoleLabel is the Russian name shown in the panel.

func UserEmail

func UserEmail(id int64) string

UserEmail returns the identifier a user is keyed by inside Xray — "u<id>" — which appears in access logs, per-user stats, and every protocol's client entry. This is the single source of that format.

func ValidFingerprint

func ValidFingerprint(fp string) bool

ValidFingerprint reports whether fp is an offered uTLS fingerprint.

func ValidLaneID added in v0.14.0

func ValidLaneID(id string) bool

ValidLaneID reports whether id is usable as a lane ID: 1–16 lowercase alphanumerics, no dashes, and not a built-in lane name.

The no-dash rule is load-bearing, not cosmetic. An Xray balancer selects its members by TAG PREFIX, and a lane's members are tagged "proxy-<id>-<n>". Were "-" allowed in an ID, lane "ru" (selector "proxy-ru-") would also select the members of lane "ru-x" (tagged "proxy-ru-x-0") and silently steal its proxies. Barring dashes from IDs makes the trailing "-" of the selector an unambiguous terminator.

func ValidUserEvent added in v0.13.0

func ValidUserEvent(k string) bool

ValidUserEvent reports whether k is a known audit action key.

func ValidWebhookEvent added in v0.10.0

func ValidWebhookEvent(k string) bool

ValidWebhookEvent reports whether k is a known webhook event key.

func ValidWebhookURL added in v0.10.0

func ValidWebhookURL(raw string) error

ValidWebhookURL checks a webhook target: an http or https URL with a host. Unlike the SSRF-guarded fetch surfaces (proxy lists, routing templates, whose URLs may come from less-trusted places), a webhook target is set by the authenticated admin and only ever receives a blind POST, so private/localhost hosts are allowed — the receiver is often the operator's own internal service.

Types

type APIKey added in v0.10.0

type APIKey struct {
	ID         int64  `json:"id"`
	Name       string `json:"name"`
	Prefix     string `json:"prefix"`            // leading clear part, e.g. "rp_A1b2C3"
	CreatedAt  int64  `json:"created_at"`        // unix seconds
	LastUsedAt int64  `json:"last_used_at"`      // unix seconds, 0 = never used
	RevokedAt  int64  `json:"revoked_at"`        // unix seconds, 0 = active
	RawKey     string `json:"raw_key,omitempty"` // populated only on creation
}

APIKey is a named credential for the external REST API. The raw key is only ever returned once (at creation, in RawKey); the stored record keeps just its HMAC hash and the clear Prefix so the operator can identify it in the UI.

func (APIKey) Active added in v0.10.0

func (k APIKey) Active() bool

Active reports whether the key has not been revoked.

type Admin added in v0.14.0

type Admin struct {
	ID                 int64  `json:"id"`
	Username           string `json:"username"`
	Role               string `json:"role"`
	MustChangePassword bool   `json:"must_change_password"`
	CreatedAt          int64  `json:"created_at"`
	LastLoginAt        int64  `json:"last_login_at"` // 0 — ни разу не входил
}

Admin is one row of the admin roster. The password hash never leaves the store.

type AdminAudit added in v0.14.0

type AdminAudit struct {
	ID        int64  `json:"id"`
	Action    string `json:"action"` // one of the Audit* keys below
	Target    string `json:"target"` // what it was done TO (an admin login, a key name); "" when the action says it all
	ActorKind string `json:"actor_kind"`
	ActorName string `json:"actor_name"` // admin login; "" for an anonymous failed sign-in
	IP        string `json:"ip"`         // where it came from — the whole point of a sign-in row
	Details   any    `json:"details"`    // decoded JSON object, nil when the row carried none
	CreatedAt int64  `json:"created_at"`
}

AdminAudit is one row of the admin trail.

type AdminAuditEntry added in v0.14.0

type AdminAuditEntry struct {
	Key      string `json:"key"`
	Label    string `json:"label"`
	Category string `json:"category"`
}

AdminAuditEntry is one action: its stable key, how it reads, and the area it belongs to.

type Connection

type Connection struct {
	IP       string `json:"ip"`
	LastSeen int64  `json:"last_seen"`
	Count    int64  `json:"count"`
}

Connection is a per-source-IP record of a user's connections.

type DailyPoint

type DailyPoint struct {
	Day  string `json:"day"` // YYYY-MM-DD (UTC)
	Up   int64  `json:"up"`
	Down int64  `json:"down"`
}

DailyPoint is one day's traffic total (for charts).

type EgressLane added in v0.14.0

type EgressLane struct {
	// ID is the stable slug the routing order references and the Xray outbound /
	// balancer tags are derived from. See ValidLaneID for the charset.
	ID      string   `json:"id"`
	Name    string   `json:"name"`    // display name ("Зона .ru")
	Enabled bool     `json:"enabled"` // off ⇒ the lane emits nothing at all
	URLs    []string `json:"urls"`    // proxy-list sources, one proxy per line
	Manual  []string `json:"manual"`  // "scheme://[user:pass@]host:port" entries
	Domains []string `json:"domains"` // destinations routed through this lane
	IPs     []string `json:"ips"`     // CIDRs or "geoip:xx"
}

EgressLane is one named proxy egress: a set of upstream proxies traffic is load-balanced across, plus the destinations that should take it. Traffic matching Domains/IPs leaves through this lane's proxies; a lane with no live proxies is skipped entirely, so its traffic falls through to the next lane.

type PaymentOrder added in v0.4.0

type PaymentOrder struct {
	ID         int64  `json:"id"`
	UserID     int64  `json:"user_id"`
	UserName   string `json:"user_name,omitempty"`
	PlanID     int64  `json:"plan_id"`
	PlanName   string `json:"plan_name,omitempty"`
	AmountRub  int    `json:"amount_rub"`
	Status     string `json:"status"`                // pending | paid | cancelled
	Provider   string `json:"provider"`              // "" (manual) | yookassa | cryptobot
	ProviderID string `json:"provider_id,omitempty"` // external payment/invoice id (admin-only view)
	PayURL     string `json:"pay_url,omitempty"`     // hosted payment URL for the user
	CreatedAt  int64  `json:"created_at"`
	PaidAt     int64  `json:"paid_at"`
}

PaymentOrder is a user payment request (manual or via a payment provider).

type PaymentStats added in v0.10.0

type PaymentStats struct {
	TotalPaid    int            `json:"total_paid"`    // all-time paid revenue (₽)
	PaidCount    int            `json:"paid_count"`    // number of paid orders
	EarnedToday  int            `json:"earned_today"`  // paid revenue since local midnight
	EarnedMonth  int            `json:"earned_month"`  // paid revenue since the 1st (local)
	PendingCount int            `json:"pending_count"` // orders awaiting payment
	PendingSum   int            `json:"pending_sum"`   // their total (₽)
	ByProvider   []ProviderStat `json:"by_provider"`   // paid revenue split by provider
}

PaymentStats is the revenue dashboard shown on the Payments page.

type ProviderStat added in v0.10.0

type ProviderStat struct {
	Provider string `json:"provider"`
	Count    int    `json:"count"`
	Sum      int    `json:"sum"` // rubles
}

ProviderStat is paid-order revenue for one payment provider ("" = manual).

type ProxyEndpoint

type ProxyEndpoint struct {
	Protocol string
	Address  string
	Port     int
	User     string
	Pass     string
}

ProxyEndpoint is one outbound proxy in the pool (parsed from a "scheme:// [user:pass@]host:port" line). Protocol is normalized to "socks" or "http".

type RoutingConfig

type RoutingConfig struct {
	BlockBittorrent bool     `json:"block_bittorrent"`
	BlockAds        bool     `json:"block_ads"` // block geosite:category-ads-all
	BlockIPs        []string `json:"block_ips"` // CIDRs or geoip:xx
	BlockDomains    []string `json:"block_domains"`
	WarpDomains     []string `json:"warp_domains"`  // routed through Cloudflare WARP
	WarpIPs         []string `json:"warp_ips"`      // CIDRs or geoip:xx, via WARP
	OperaDomains    []string `json:"opera_domains"` // routed through Opera VPN
	OperaIPs        []string `json:"opera_ips"`     // CIDRs or geoip:xx, via Opera VPN
	DirectDomains   []string `json:"direct_domains"`
	DirectIPs       []string `json:"direct_ips"`

	// RoutingOrder is the precedence of the egress lanes; first-match-wins. It is a
	// permutation of the built-in lanes ("warp"/"opera"/"direct") plus the ID of
	// every proxy lane in Lanes. The LAST lane is the catch-all ("everything else")
	// — its specific rules are subsumed by a final rule. A config saved before a
	// lane existed simply omits it; the generator back-fills any missing lane rather
	// than dropping it, and drops IDs of lanes that no longer exist.
	RoutingOrder []string `json:"routing_order"`

	// Lanes are the operator-defined proxy egress lanes. Each has its own upstream
	// proxies and its own match rules, so different destinations can leave through
	// different proxies (e.g. a ".ru" lane and a ".com" lane).
	Lanes []EgressLane `json:"lanes"`

	// ProxyRefreshMinutes is how often the URL-sourced proxy lists are re-fetched.
	// 0 means the default (30 min) — kept so configs saved before this was
	// selectable keep auto-refreshing; a negative value means "never".
	ProxyRefreshMinutes int `json:"proxy_refresh_minutes"`

	// Deprecated: the pre-lanes single proxy pool. Only read, never written —
	// MigrateLanes folds these into a Lanes entry on load. Kept so a config saved
	// by an older build still upgrades cleanly.
	ProxyURLs    []string `json:"proxy_urls,omitempty"`
	ProxyManual  []string `json:"proxy_manual,omitempty"`
	ProxyDomains []string `json:"proxy_domains,omitempty"`
	ProxyIPs     []string `json:"proxy_ips,omitempty"`
}

RoutingConfig is the structured routing configuration (Settings → Роутинг). Each field is a category of destinations handled the same way; domain entries are raw Xray matchers (plain host, "domain:", "keyword:", "regexp:", "geosite:", "ext:file:tag") and IP entries are CIDRs or "geoip:xx".

func (*RoutingConfig) LaneIDs added in v0.14.0

func (rc *RoutingConfig) LaneIDs() []string

LaneIDs returns the IDs of the configured proxy lanes, in config order.

func (*RoutingConfig) MigrateLanes added in v0.14.0

func (rc *RoutingConfig) MigrateLanes()

MigrateLanes upgrades a config saved before egress lanes existed: the single proxy pool becomes one lane (ID "proxy"), so its proxies, rules and place in the routing order all survive. It also clears the deprecated fields on a config that already has lanes, so they are never written back.

func (*RoutingConfig) ValidateLanes added in v0.14.0

func (rc *RoutingConfig) ValidateLanes() error

ValidateLanes checks the operator-supplied lanes before they are persisted. Messages are user-facing (shown in the panel).

type Settings

type Settings struct {
	ID   int64  `json:"-"`
	Host string `json:"host"` // public domain or IP used in share links
	SNI  string `json:"sni"`  // TLS server name (link + cert SAN)
	// TLSMode is vestigial: self-signed and custom-upload were removed as operator
	// choices, so ACME is the only mode and this is always TLSModeACME. It survives
	// because the column is still written by every TLS path and read as a guard
	// before renewal — and because it's the seam where a second mode would go back
	// in. Don't branch new behaviour on it; treat ACME as given.
	TLSMode         string    `json:"tls_mode"`
	ACMEEmail       string    `json:"acme_email"`
	ACMEProvider    string    `json:"acme_provider"` // "letsencrypt" | "zerossl"
	ZeroSSLEABKID   string    `json:"-"`             // ZeroSSL External Account Binding KID
	ZeroSSLEABHMAC  string    `json:"-"`             // ZeroSSL EAB HMAC key (base64url)
	CertPath        string    `json:"cert_path"`
	KeyPath         string    `json:"key_path"`
	VLESSPort       int       `json:"vless_port"`
	ConfigRevision  int64     `json:"config_revision"`
	LastConfigError string    `json:"last_config_error"`
	UpdatedAt       time.Time `json:"updated_at"`
	PanelSecretPath string    `json:"-"` // never serialized to clients
	// Branding: custom panel display name + colour theme (empty ⇒ defaults). A
	// custom logo lives as a file under <dataDir>/branding/, not here.
	PanelName     string `json:"-"`
	PanelTheme    string `json:"-"` // JSON {accent,text,muted,bg,surface}, empty ⇒ defaults
	DecoyTemplate string `json:"decoy_template"`
	WSPath        string `json:"-"` // Trojan-WS path matched by VLESS fallbacks
	TrojanPort    int    `json:"-"` // loopback Trojan inbound port
	HysteriaPort  int    `json:"hysteria_port"`
	HopStart      int    `json:"hop_start"`
	HopEnd        int    `json:"hop_end"`
	// HopInterval is the port-hopping rotation interval in seconds ("min-max"),
	// embedded in the Hysteria2 share link's quicParams.
	HopInterval string `json:"-"`

	// Per-protocol toggles for the Connections panel. A disabled protocol drops
	// out of user subscriptions/share links and its clients are removed from the
	// Xray inbound (the listener stays up but rejects everyone).
	VLESSEnabled    bool `json:"-"`
	TrojanEnabled   bool `json:"-"`
	HysteriaEnabled bool `json:"-"`

	// VLESS + gRPC + REALITY inbound (separate port). REALITY borrows the TLS of a
	// real site (RealityDest) instead of our cert. Keys/shortId/serviceName are
	// generated by the panel; the public key + shortId go into share links.
	RealityEnabled     bool   `json:"-"`
	RealityPort        int    `json:"-"`
	RealityDest        string `json:"-"` // target site / SNI(s), e.g. "max.ru"
	RealityPrivateKey  string `json:"-"` // X25519 private (base64 raw-url)
	RealityPublicKey   string `json:"-"` // X25519 public (base64 raw-url), in links (pbk)
	RealityShortID     string `json:"-"` // hex shortId, in links (sid)
	RealityServiceName string `json:"-"` // gRPC service name

	// Proxy mode: a socks/http forward-proxy inbound so other RosPanel servers can
	// chain their egress through this one (point their proxy pool at host:port).
	ProxyModeEnabled bool   `json:"-"`
	ProxyModeType    string `json:"-"` // "socks" | "http"
	ProxyModePort    int    `json:"-"`
	ProxyModeUser    string `json:"-"`
	ProxyModePass    string `json:"-"`

	// First-run wizard state. SetupDone gates the wizard; Timezone is the IANA
	// zone anchoring the local-day boundary for stats (empty ⇒ server local).
	SetupDone bool   `json:"-"`
	Timezone  string `json:"-"`

	// Subscription delivery settings (Settings → Подписки).
	SubPath           string `json:"-"` // public subscription URL prefix /<sub_path>/<token>
	SubBase64         bool   `json:"-"` // base64-encode the universal link list
	SubNameInTitle    bool   `json:"-"` // append the user name to Profile-Title / group name
	SubTitle          string `json:"-"` // profile title base (empty ⇒ «РосПанель»)
	SubRouting        bool   `json:"-"` // attach auto-routing headers
	SubRoutingHapp    string `json:"-"` // Happ routing config URL
	SubRoutingIncy    string `json:"-"` // INCY routing config URL
	SubRoutingMihomo  string `json:"-"` // Mihomo (Clash Meta) routing config URL
	SubUpdateInterval int    `json:"-"` // subscription auto-update interval (hours)
	// SubAnnounce is a short broadcast shown inside the VPN client itself (Happ,
	// v2RayTun) via the subscription's Announce header. Empty ⇒ no announcement.
	// Clients only render the first 200 characters; the panel enforces that limit.
	SubAnnounce string `json:"-"`

	// UserAutoDeleteDays deletes an expired user this many days after their expiry
	// date. 0 ⇒ never (default): expired users pile up but nothing is ever destroyed
	// behind the operator's back.
	UserAutoDeleteDays int `json:"-"`

	XrayDNS string `json:"-"` // upstream DNS servers for Xray (newline/comma separated)

	// Per-connection uTLS fingerprint embedded in share links (fp=). Hysteria2
	// (QUIC) has none.
	VLESSFp   string `json:"-"`
	TrojanFp  string `json:"-"`
	RealityFp string `json:"-"`

	// Per-connection display names shown in VPN clients / on the subscription page
	// (the node label after '#' and the sing-box/Clash node tag). Empty ⇒ the
	// default protocol label (ProtoVLESS, ProtoReality, …). See Settings.ProtoLabel.
	VLESSName    string `json:"-"`
	RealityName  string `json:"-"`
	TrojanName   string `json:"-"`
	HysteriaName string `json:"-"`

	// Anti-DPI / anti-censorship transport hardening (Settings → Подключения).
	// TLSFragment / BlockQUIC shape the GENERATED client configs (sing-box only —
	// no server change); TLSMin13 / RealityMaxTimeDiff / RealityDestPort change the
	// SERVER inbound config.
	TLSFragment        bool `json:"-"` // sing-box ClientHello fragmentation (Vision + Trojan-WS)
	TLSMin13           bool `json:"-"` // require TLS 1.3 on the :443 inbound
	BlockQUIC          bool `json:"-"` // drop untunneled browser QUIC (UDP/443) in client configs
	RealityMaxTimeDiff int  `json:"-"` // REALITY anti-replay window in ms (0 = off)

	// Opera VPN egress: the opera-proxy helper binary exposes a local HTTP proxy
	// (127.0.0.1:OperaPort) we add as the "opera" routing lane. Country is the
	// Opera VPN region (EU|AS|AM).
	OperaEnabled bool   `json:"-"`
	OperaCountry string `json:"-"`
	OperaPort    int    `json:"-"`

	// Cloudflare WARP outbound (WireGuard). When enabled and registered, routing
	// rules with action "warp" egress through it.
	WarpEnabled    bool   `json:"-"`
	WarpPrivateKey string `json:"-"` // our WG secret key (base64)
	WarpPublicKey  string `json:"-"` // Cloudflare's WG public key (base64)
	WarpEndpoint   string `json:"-"` // host:port of the WARP peer
	WarpAddressV4  string `json:"-"` // assigned interface IPv4
	WarpAddressV6  string `json:"-"` // assigned interface IPv6
	WarpReserved   string `json:"-"` // client id as "a,b,c"

	// Telegram bot (Settings → Telegram). An authorized admin chat can view/add/
	// remove users via the bot, and scheduled backups are pushed to it. TGChatIDs
	// is the comma-separated set of authorized chats; TGLinkCode is the pending
	// one-time code an admin sends as "/start <code>" to link their chat.
	TGBotEnabled bool   `json:"-"`
	TGBotToken   string `json:"-"`
	TGChatIDs    string `json:"-"` // comma-separated authorized chat IDs
	TGLinkCode   string `json:"-"` // pending one-time linking code (cleared once used)
	TGBackupCron string `json:"-"` // 5-field cron (operator TZ) for scheduled backups; empty = off

	// Separate public user bot (Settings → Telegram): VPN clients self-register and
	// self-serve their subscription. Must use a different token than the admin bot.
	TGUserBotEnabled bool   `json:"-"`
	TGUserBotToken   string `json:"-"`
	TGUserRegEnabled bool   `json:"-"` // allow /start self-registration (creates a new VPN account)

	// TGAdminEvents is a bitmask of the AdminEvent* categories the admin bot pushes
	// to the authorized chats. Default -1 (all on); see AdminEventEnabled.
	TGAdminEvents int64 `json:"-"`

	// Local scheduled backups, independent of Telegram: archives are written to
	// <dataDir>/backups and the newest LocalBackupKeep are retained. Same 5-field
	// cron dialect and operator timezone as TGBackupCron; empty = off.
	LocalBackupCron string `json:"local_backup_cron"`
	LocalBackupKeep int    `json:"local_backup_keep"`

	// Billing (Settings → Оплата): plans, trial period, free-tier fallback.
	BillingEnabled     bool   `json:"-"`
	BillingTrialDays   int    `json:"-"`
	BillingFreePlanID  int64  `json:"-"`
	BillingTrialPlanID int64  `json:"-"`
	BillingPaymentNote string `json:"-"`

	// Payment providers (Settings → Оплата). Secret key / token are stored
	// encrypted at rest. PaymentWebhookSecret is a random URL segment for the
	// provider webhook path so it's unguessable yet fixed.
	YooKassaEnabled      bool   `json:"-"`
	YooKassaShopID       string `json:"-"`
	YooKassaSecretKey    string `json:"-"`
	YooKassaTest         bool   `json:"-"` // using a YooKassa test shop
	CryptoBotEnabled     bool   `json:"-"`
	CryptoBotToken       string `json:"-"`
	CryptoBotTestnet     bool   `json:"-"` // use the CryptoBot testnet endpoint
	PaymentWebhookSecret string `json:"-"`

	// External REST API: the stable, unguessable URL segment the API is mounted
	// under (/<api_path>/v1/...). Empty ⇒ the API surface is disabled. Kept
	// separate from PanelSecretPath so rotating the panel secret never breaks
	// integrations. Keys themselves live in the api_keys table.
	APIPath string `json:"-"`

	Routing RoutingConfig `json:"-"` // structured routing config (Settings → Роутинг)

	// Computed per request (NOT stored). When the active cert isn't CA-trusted (a
	// self-signed fallback), TLSInsecure is set and TLSPinSHA256 carries the hex
	// SHA-256 of that cert so Xray links can pin it (pinnedPeerCertSha256) — clients
	// then trust this exact cert. sing-box/clash use TLSInsecure (skip verify).
	TLSInsecure  bool   `json:"-"`
	TLSPinSHA256 string `json:"-"`
}

Settings is the singleton (id=1) panel configuration. The DB is the source of truth; the Xray config.json is always derived from it.

func (*Settings) AdminEventEnabled added in v0.7.0

func (s *Settings) AdminEventEnabled(bit int64) bool

AdminEventEnabled reports whether the given AdminEvent* flag is enabled.

func (*Settings) OperaCountryOr

func (s *Settings) OperaCountryOr() string

OperaCountryOr returns the configured Opera VPN region, defaulting to "EU" for an empty or unknown value.

func (*Settings) OperaPortOr

func (s *Settings) OperaPortOr() int

OperaPortOr returns the local Opera proxy port, defaulting to 18080.

func (*Settings) ProtoLabel added in v0.11.0

func (s *Settings) ProtoLabel(proto string) string

ProtoLabel returns the display name for a protocol constant (ProtoVLESS, …): the admin-configured custom name when set, otherwise the constant itself. Used for the share-link node label and the sing-box/Clash node tag. A nil receiver falls back to the constant so link builders stay safe.

func (*Settings) RealityFP

func (s *Settings) RealityFP() string

func (*Settings) RealitySID

func (s *Settings) RealitySID() string

RealitySID returns the primary (first) REALITY shortId — the one embedded in share links and client configs (RealityShortID stores a comma-separated set the server accepts).

func (*Settings) RealitySNI

func (s *Settings) RealitySNI() string

RealitySNI returns the primary (first) donor domain — used as the dialed dest and the sni= in share links.

func (*Settings) RealityServerNames

func (s *Settings) RealityServerNames() []string

RealityServerNames returns the donor SNIs the REALITY inbound accepts (RealityDest stores a comma-separated set; the first is the primary).

func (*Settings) SubPathOr

func (s *Settings) SubPathOr() string

SubPathOr returns the subscription URL prefix, defaulting to "sub".

func (*Settings) TelegramAuthorized added in v0.2.0

func (s *Settings) TelegramAuthorized(id int64) bool

TelegramAuthorized reports whether the given chat ID is linked to the bot.

func (*Settings) TelegramChatIDs added in v0.2.0

func (s *Settings) TelegramChatIDs() []int64

TelegramChatIDs parses the comma-separated authorized chat IDs into int64s, skipping blanks and unparseable entries.

func (*Settings) TrojanFP

func (s *Settings) TrojanFP() string

func (*Settings) VLESSFP

func (s *Settings) VLESSFP() string

VLESSFP / TrojanFP / RealityFP return the per-connection uTLS fingerprint for share links, each defaulting to "firefox".

func (*Settings) WarpRegistered

func (s *Settings) WarpRegistered() bool

WarpRegistered reports whether a WARP account has been provisioned.

type TariffPlan added in v0.4.0

type TariffPlan struct {
	ID          int64  `json:"id"`
	Slug        string `json:"slug"`
	Name        string `json:"name"`
	PriceRub    int    `json:"price_rub"`
	PeriodDays  int    `json:"period_days"`
	DataLimit   int64  `json:"data_limit"`
	DeviceLimit int    `json:"device_limit"`
	SortOrder   int    `json:"sort_order"`
	Enabled     bool   `json:"enabled"`
}

TariffPlan is a billing tier (free, trial template, or paid).

func (TariffPlan) IsFree added in v0.4.0

func (p TariffPlan) IsFree() bool

IsFree reports whether this is a free plan. A plan is free iff it has no price: free plans never expire and refill their quota every срок действия, while paid plans (price > 0) expire after their period and must be renewed.

type User

type User struct {
	ID        int64     `json:"id"`
	Name      string    `json:"name"`
	UUID      string    `json:"uuid"`       // VLESS
	Password  string    `json:"-"`          // Trojan + Hysteria2 (shared); embedded in links only
	SubToken  string    `json:"-"`          // subscription capability token
	Status    string    `json:"status"`     // active | disabled | expired | limited | device_limited
	Enabled   bool      `json:"enabled"`    // manual on/off toggle (independent of Status)
	DataLimit int64     `json:"data_limit"` // bytes, 0 = unlimited
	ExpireAt  int64     `json:"expire_at"`  // unix seconds, 0 = never
	UsedUp    int64     `json:"used_up"`
	UsedDown  int64     `json:"used_down"`
	LastUp    int64     `json:"-"` // last raw Xray uplink counter
	LastDown  int64     `json:"-"` // last raw Xray downlink counter
	CreatedAt time.Time `json:"created_at"`

	ResetPeriod string `json:"reset_period"` // none | daily | weekly | monthly | yearly
	LastResetAt int64  `json:"-"`            // unix of the last automatic quota reset
	LastSeen    int64  `json:"last_seen"`    // unix of last activity (0 = never); 0 ⇒ offline

	DeviceLimit   int `json:"device_limit"`   // max concurrent devices (unique IPs), 0 = unlimited
	ActiveDevices int `json:"active_devices"` // computed: distinct IPs seen within DeviceOnlineWindow

	TgChatID int64 `json:"tg_chat_id"` // linked Telegram chat for the user bot (0 = not linked)

	TgLinkCode   string `json:"-"` // pending one-time Telegram bind code (replaces sub-token deep links)
	TgLinkCodeAt int64  `json:"-"` // unix when TgLinkCode was issued (0 = none)

	PlanID    int64  `json:"plan_id"`             // active tariff (0 = manual limits)
	TrialUsed bool   `json:"trial_used"`          // trial period already consumed
	PlanName  string `json:"plan_name,omitempty"` // computed for API views (not stored)

	// NotifiedStatus is the last Status the operator/user was ALERTED about (admin
	// push, webhook, audit row). Persisted so a panel restart cannot lose a transition
	// that happened while it was down. "" = never alerted about.
	NotifiedStatus string `json:"-"`
}

User is a VPN user. In v1 one user = one credential set applied across all enabled protocols (M1 only wires VLESS).

func (User) UserTgLinkCodeValid added in v0.4.0

func (u User) UserTgLinkCodeValid() bool

UserTgLinkCodeValid reports whether the user's pending Telegram bind code exists and has not expired.

type UserEvent added in v0.13.0

type UserEvent struct {
	ID        int64  `json:"id"`
	UserID    int64  `json:"user_id"`
	UserName  string `json:"user_name"` // denormalized: the event outlives the user row
	Action    string `json:"action"`    // one of the Event* keys below
	ActorKind string `json:"actor_kind"`
	ActorName string `json:"actor_name"` // admin login, API key name, Telegram @username; "" for system
	Details   any    `json:"details"`    // decoded JSON object, nil when the row carried none
	CreatedAt int64  `json:"created_at"`
}

UserEvent is one audit-log row.

type UserTotal

type UserTotal struct {
	UserID int64  `json:"user_id"`
	Name   string `json:"name"`
	Up     int64  `json:"up"`
	Down   int64  `json:"down"`
}

UserTotal is a user's traffic total over a period (for the per-user table).

type Webhook added in v0.10.0

type Webhook struct {
	ID            int64    `json:"id"`
	URL           string   `json:"url"`
	Secret        string   `json:"secret"`
	Events        []string `json:"events"`
	Enabled       bool     `json:"enabled"`
	CreatedAt     int64    `json:"created_at"`
	LastStatus    int      `json:"last_status"`     // last HTTP status (0 = never/connection error)
	LastAttemptAt int64    `json:"last_attempt_at"` // unix seconds, 0 = never delivered
	LastError     string   `json:"last_error"`      // last failure reason ("" = ok/never)
}

Webhook is an outbound HTTP endpoint the panel POSTs lifecycle events to. The Secret is a symmetric key both sides hold: the panel signs each delivery with HMAC-SHA256 and the receiver verifies it (so unlike an API key it stays readable in the UI). Events is the subscribed set; empty ⇒ every event.

func (Webhook) Subscribed added in v0.10.0

func (h Webhook) Subscribed(event string) bool

Subscribed reports whether this webhook wants the given event. An empty Events set means "all events".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL