Documentation
¶
Overview ¶
Package model holds the core domain types shared across the panel.
Index ¶
- Constants
- Variables
- func AdminAuditActionsIn(category string) []string
- func AdminAuditLabel(action string) string
- func AudienceDays(audience, prefix string) (int, bool)
- func BuiltinLanes() []string
- func GrantableRole(role string) bool
- func RoleAtLeast(role, tier string) bool
- func RoleLabel(role string) string
- func UserEmail(id int64) string
- func ValidAudience(a string) bool
- func ValidFingerprint(fp string) bool
- func ValidLaneID(id string) bool
- func ValidUserEvent(k string) bool
- func ValidWebhookEvent(k string) bool
- func ValidWebhookURL(raw string) error
- type APIKey
- type Admin
- type AdminAudit
- type AdminAuditEntry
- type Broadcast
- type BroadcastButton
- type Connection
- type DailyPoint
- type EgressLane
- type Node
- type NodeConnections
- type NodeStatusUpdate
- type PaymentOrder
- type PaymentProvider
- type PaymentStats
- type ProviderStat
- type ProxyEndpoint
- type RegistrationRequest
- type RoutingConfig
- type Settings
- func (s *Settings) AdminEventEnabled(bit int64) bool
- func (s *Settings) ExpiringDays() int
- func (s *Settings) OperaCountryOr() string
- func (s *Settings) OperaPortOr() int
- func (s *Settings) ProtoLabel(proto string) string
- func (s *Settings) RealityFP() string
- func (s *Settings) RealitySID() string
- func (s *Settings) RealitySNI() string
- func (s *Settings) RealityServerNames() []string
- func (s *Settings) RegMode() string
- func (s *Settings) RegistrationActivates() bool
- func (s *Settings) RegistrationOpen() bool
- func (s *Settings) SubPathOr() string
- func (s *Settings) SupportLink() string
- func (s *Settings) TelegramAuthorized(id int64) bool
- func (s *Settings) TelegramChatIDs() []int64
- func (s *Settings) TrojanFP() string
- func (s *Settings) UserNotifyEnabled(bit int64) bool
- func (s *Settings) VLESSFP() string
- func (s *Settings) WarpRegistered() bool
- type Subscriber
- type SupportGroup
- type TariffPlan
- type User
- type UserEvent
- type UserTotal
- type Webhook
Constants ¶
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.
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" // Mass broadcasts. Kept as their own actions rather than folded into // AuditSettings: "who sent a message to every user, and what was in it" is the // question this journal exists to answer, and it must not need reading a // settings-changed row to find. AuditBroadcastStarted = "broadcast.started" AuditBroadcastChanged = "broadcast.changed" AuditBroadcastTest = "broadcast.test" AuditUserMessaged = "broadcast.user_messaged" // 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.
const ( AuditCatSession = "session" AuditCatAdmins = "admins" AuditCatSettings = "settings" AuditCatPlans = "plans" AuditCatAPI = "api" AuditCatBroadcast = "broadcast" 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.
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.
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.
const ( ACMEProviderLE = "letsencrypt" // default; no EAB required ACMEProviderZeroSSL = "zerossl" // requires EAB credentials from zerossl.com )
ACME CA providers.
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.
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).
const ( RegOff = "off" // registration closed RegOpen = "open" // account created and active immediately RegModeration = "moderation" // account created disabled; an admin approves it RegInvite = "invite" // an invite code is required, then active )
Self-registration modes (Settings.TGUserRegMode).
const ( BroadcastRunning = "running" BroadcastPaused = "paused" BroadcastDone = "done" BroadcastCancelled = "cancelled" )
Broadcast statuses. There is no "draft": a broadcast row exists only once it has been launched, so nothing half-composed can be mistaken for something queued.
const ( TargetPending = "pending" TargetSent = "sent" TargetFailed = "failed" TargetBlocked = "blocked" // TargetSkipped is someone who unsubscribed after the audience was frozen. The // snapshot is what keeps the total steady, but honouring an opt-out only until // the run starts would deliver marketing to a person the bot has just told they // are unsubscribed — which is what makes people block it outright. TargetSkipped = "skipped" )
Per-recipient delivery states. "blocked" is kept apart from "failed" because it is permanent and not the operator's problem to retry — it means Telegram will never accept a message for that chat again.
const ( AudienceAll = "all" AudienceLinked = "linked" // has a panel account AudienceUnlinked = "unlinked" // never registered, or the account was deleted AudienceActive = "active" // account in the active state AudienceExpired = "expired" // subscription ran out AudienceNever = "never" // linked account that has never connected AudienceSeenPrefix = "seen:" // connected within the last N days AudienceUnseenPrefix = "unseen:" // not seen for N days (never counts) AudienceExpiringPrefix = "expiring:" // subscription runs out within N days )
Broadcast audiences, resolved to a chat list once at launch.
The parameterised ones carry their argument in the value itself ("seen:7") rather than in another column: an audience is written once at launch and read back only to be shown, so a second field would exist purely to be kept in sync with this one.
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.
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.
const ( UserNotifyExpiring int64 = 1 << 0 // subscription runs out soon UserNotifyExpired int64 = 1 << 1 // subscription has run out UserNotifyTrafficLow int64 = 1 << 2 // most of the quota is spent UserNotifyLimited int64 = 1 << 3 // traffic quota exhausted UserNotifyDeviceLimited int64 = 1 << 4 // too many devices at once UserNotifyDisabled int64 = 1 << 5 // access switched off by an operator UserNotifyPayment int64 = 1 << 6 // payment confirmed, plan activated UserNotifyRegistration int64 = 1 << 7 // moderated signup approved or rejected )
User notification categories (bitmask flags stored in Settings.TGUserEvents). Named UserNotify* rather than UserEvent*, which the user journal already uses for something else entirely — one records what happened, this decides what gets said. Separate from the AdminEvent* set even where the subject matches: "чей-то трафик закончился" and "ваш трафик закончился" are different messages to different people, and an operator watching their own alerts should not thereby be writing to customers.
const AdminAuditRetentionDays = 90
AdminAuditRetentionDays matches the user journal's window.
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.
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).
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.
const LocalNodeID int64 = 0
LocalNodeID is the virtual node the panel's own Xray runs as. It has no row in `nodes` — its identity is the settings singleton — but it carries an ID so that traffic history, link generation and the UI can treat every server uniformly.
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.
const NodeOnlineWindow int64 = 120
NodeOnlineWindow is how long after its last sync a node still counts as online. Generous next to the node's own poll cadence (a held poll returns at least every 45s), so one slow round trip doesn't flap the badge.
const TLSModeACME = "acme"
TLSModeACME is the only TLS mode: ACME (Let's Encrypt or ZeroSSL) for a domain or IP.
const TelegramLinkCodeTTL = 15 * time.Minute
TelegramLinkCodeTTL is how long a one-time Telegram bind code stays valid.
const TrafficDailyRetentionDays = 365
TrafficDailyRetentionDays is how long per-day traffic history is kept. It sits well above the journals' 30/90 days because this is reporting data rather than a log — the stats page offers ranges up to a year — but it is bounded all the same: traffic_daily grows at users × nodes × days, so without a sweep it is the one table that never stops.
const TrafficWarnPercent = 80
TrafficWarnPercent is how much of the quota must be spent before the "running low" notice goes out. Fixed rather than configurable: the useful range is narrow, and one more knob to explain buys nothing.
const UserEventRetentionDays = 90
UserEventRetentionDays is how long audit rows are kept before the retention sweep drops them.
Variables ¶
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}, {AuditBroadcastStarted, "Рассылка запущена", AuditCatBroadcast}, {AuditBroadcastChanged, "Рассылка: пауза, отмена или повтор", AuditCatBroadcast}, {AuditBroadcastTest, "Тестовая отправка рассылки", AuditCatBroadcast}, {AuditUserMessaged, "Сообщение пользователю в Telegram", AuditCatBroadcast}, {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.
var AdminAuditCategories = []struct{ Key, Label string }{ {AuditCatSession, "Входы"}, {AuditCatAdmins, "Администраторы"}, {AuditCatSettings, "Настройки"}, {AuditCatPlans, "Тарифы"}, {AuditCatAPI, "API и вебхуки"}, {AuditCatBroadcast, "Рассылки"}, {AuditCatPanel, "Панель"}, }
AdminAuditCategories is the filter's list, in the order it renders.
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.
var Fingerprints = []string{
"firefox", "chrome", "safari", "edge", "ios", "android", "random", "randomized",
}
Fingerprints are the uTLS ClientHello fingerprints offered in the UI.
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.
var OperaCountries = []string{"EU", "AS", "AM"}
OperaCountries are the Opera VPN regions opera-proxy accepts.
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.
var UserNotifyCatalog = []struct { Key string Bit int64 }{ {"expiring", UserNotifyExpiring}, {"expired", UserNotifyExpired}, {"traffic_low", UserNotifyTrafficLow}, {"limited", UserNotifyLimited}, {"device_limited", UserNotifyDeviceLimited}, {"disabled", UserNotifyDisabled}, {"payment", UserNotifyPayment}, {"registration", UserNotifyRegistration}, }
UserNotifyCatalog is the stable key→flag mapping the settings API and UI iterate over. Appending is safe; renaming a key silently resets that toggle.
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
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
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 AudienceDays ¶ added in v1.2.0
AudienceDays extracts the N from a parameterised audience, and reports whether the value is one. Bounded so a typo can't turn "not seen for 90 days" into a filter that quietly matches nobody or everybody.
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
GrantableRole reports whether role is one an owner may assign to someone else.
func RoleAtLeast ¶ added in v0.14.0
RoleAtLeast reports whether role clears the given tier.
func UserEmail ¶
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 ValidAudience ¶ added in v1.2.0
ValidAudience reports whether an audience string is one the panel knows how to resolve.
func ValidFingerprint ¶
ValidFingerprint reports whether fp is an offered uTLS fingerprint.
func ValidLaneID ¶ added in v0.14.0
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
ValidUserEvent reports whether k is a known audit action key.
func ValidWebhookEvent ¶ added in v0.10.0
ValidWebhookEvent reports whether k is a known webhook event key.
func ValidWebhookURL ¶ added in v0.10.0
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.
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 Broadcast ¶ added in v1.2.0
type Broadcast struct {
ID int64 `json:"id"`
CreatedBy string `json:"created_by"`
Text string `json:"text"`
MediaKind string `json:"media_kind"` // "" | "photo" | "document"
MediaFileID string `json:"-"`
MediaName string `json:"media_name"`
Buttons []BroadcastButton `json:"buttons"`
Audience string `json:"audience"`
Status string `json:"status"`
CreatedAt int64 `json:"created_at"`
StartedAt int64 `json:"started_at"`
FinishedAt int64 `json:"finished_at"`
Total int `json:"total"`
Sent int `json:"sent"`
Failed int `json:"failed"`
Blocked int `json:"blocked"`
Skipped int `json:"skipped"`
}
Broadcast is one mass message and its progress. The counters are derived from the per-recipient rows on read rather than stored, so they cannot drift from reality.
type BroadcastButton ¶ added in v1.2.0
BroadcastButton is one URL button under a broadcast. Only URL buttons are offered: a callback button would need a handler in the bot, and one attached to a message sent months ago outlives whatever it was meant to do.
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 Node ¶ added in v1.0.0
type Node struct {
ID int64 `json:"id"`
Name string `json:"name"`
Host string `json:"host"`
Enabled bool `json:"enabled"`
// Per-node REALITY identity. RealityPrivateKey is encrypted at rest and never
// serialized to any client. RealityDest is the node's own masquerade donor SNI
// (empty ⇒ inherit the panel's donor).
RealityPrivateKey string `json:"-"`
RealityPublicKey string `json:"-"`
RealityShortID string `json:"-"`
RealityServiceName string `json:"-"`
RealityDest string `json:"-"`
// Per-node protocols (the node's OWN — no inheritance from the master). A stored
// nil is treated as off; every write sets an explicit value.
VLESSEnabled *bool `json:"vless_enabled"`
TrojanEnabled *bool `json:"trojan_enabled"`
HysteriaEnabled *bool `json:"hysteria_enabled"`
RealityEnabled *bool `json:"reality_enabled"`
DecoyTemplate string `json:"decoy_template"`
// Routing is the node's own routing override: nil ⇒ inherit the panel's routing.
// A node's egress lanes (proxy pools) live in Routing.Lanes and resolve against
// the node's OWN proxy pool; WARP/Opera below are the node's own too.
Routing *RoutingConfig `json:"routing,omitempty"`
// XrayDNS is the node's own upstream DNS override: nil ⇒ inherit the panel's DNS.
XrayDNS *string `json:"xray_dns,omitempty"`
// Per-node egress backends (independent of the master; all off by default).
// WARP is a per-node Cloudflare registration (WireGuard); Opera runs a local
// helper on the node. Proxy lanes live in Routing.Lanes.
WarpEnabled bool `json:"warp_enabled"`
WarpPrivateKey string `json:"-"` // encrypted at rest
WarpPublicKey string `json:"-"`
WarpEndpoint string `json:"-"`
WarpAddressV4 string `json:"-"`
WarpAddressV6 string `json:"-"`
WarpReserved string `json:"-"`
OperaEnabled bool `json:"opera_enabled"`
OperaCountry string `json:"opera_country"`
// Connections is the node's own transport override (nil ⇒ inherit the master's).
Connections *NodeConnections `json:"-"`
// Per-node ACME (empty ⇒ inherit the panel's). ZeroSSLEABHMAC is encrypted at rest.
ACMEEmail string `json:"-"`
ACMEProvider string `json:"-"`
ZeroSSLEABKID string `json:"-"`
ZeroSSLEABHMAC string `json:"-"`
// Reported by the node on each sync.
LastSeen int64 `json:"last_seen"`
NodeVersion string `json:"node_version"`
XrayVersion string `json:"xray_version"`
XrayRunning bool `json:"xray_running"`
CertSHA256 string `json:"-"`
CertSelfSigned bool `json:"-"`
CertIssuer string `json:"-"` // ≈ ACME provider that signed the node's cert
CertExpiresAt int64 `json:"-"` // unix; 0 ⇒ unknown
// GeoRefreshHours is the node's OWN geo auto-refresh cadence (hours; 0 ⇒ never —
// its own, not inherited from the master).
GeoRefreshHours int `json:"-"`
ConfigHash string `json:"-"`
LastReportID int64 `json:"-"`
CreatedAt int64 `json:"created_at"`
// DeletedAt is the tombstone timestamp: non-zero ⇒ the node was deleted and is
// kept only so its next sync can be answered Revoked before the row is purged.
DeletedAt int64 `json:"-"`
// JoinExpiresAt is when the pending one-time join token lapses (0 ⇒ the node has
// already joined, or its token expired and was cleared).
JoinExpiresAt int64 `json:"join_expires_at"`
// RawJoinToken is populated ONLY by CreateNode/RegenJoinToken, and shown to the
// operator exactly once (it is the credential in the install command). It is
// never stored in clear and never read back.
RawJoinToken string `json:"join_token,omitempty"`
}
Node is a remote VPN server managed by this panel. It runs the same rospanel binary in node mode: it holds an outbound long-poll to the panel, applies the Xray config the panel generates for it, and reports traffic back.
A node inherits every setting from the global settings row except the fields below — its own address, TLS/REALITY identity, protocol overrides, and its OWN routing/DNS/egress (proxy lanes, WARP, Opera), independent of the master. See core.nodeSettings, which materializes exactly that.
func (*Node) Joined ¶ added in v1.0.0
Joined reports whether the node has exchanged its join token for a permanent one — i.e. whether the install command has actually been run on a server.
func (*Node) Online ¶ added in v1.0.0
Online reports whether the node has synced within NodeOnlineWindow of now.
func (*Node) WarpRegistered ¶ added in v1.0.0
WarpRegistered reports whether the node has a WARP account provisioned.
type NodeConnections ¶ added in v1.0.0
type NodeConnections struct {
WSPath string `json:"ws_path"`
HysteriaPort int `json:"hysteria_port"`
HopStart int `json:"hop_start"`
HopEnd int `json:"hop_end"`
HopInterval string `json:"hop_interval"`
RealityPort int `json:"reality_port"`
RealityMaxTimeDiff int `json:"reality_max_time_diff"` // >0 ⇒ anti-replay on
TLSFragment bool `json:"tls_fragment"`
TLSMin13 bool `json:"tls_min13"`
BlockQUIC bool `json:"block_quic"`
VLESSFp string `json:"vless_fp"`
TrojanFp string `json:"trojan_fp"`
RealityFp string `json:"reality_fp"`
VLESSName string `json:"vless_name"`
TrojanName string `json:"trojan_name"`
RealityName string `json:"reality_name"`
HysteriaName string `json:"hysteria_name"`
}
NodeConnections is a node's own connection transport, overriding the master's when present (nil ⇒ inherit the master's). Protocol on/off and the REALITY donor/keys are separate per-node fields; this holds the ports, port-hopping, WS path, REALITY port + anti-replay, uTLS fingerprints, connection display names, and anti-DPI.
type NodeStatusUpdate ¶ added in v1.0.0
type NodeStatusUpdate struct {
LastSeen int64
NodeVersion string
XrayVersion string
XrayRunning bool
CertSHA256 string
CertSelfSigned bool
CertIssuer string
CertExpiresAt int64
ConfigHash string
}
NodeStatusUpdate is what a node reports on each sync, persisted by Store.UpdateNodeStatus.
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) or a payments registry key
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 PaymentProvider ¶ added in v1.0.0
PaymentProvider is one payment provider's saved setup: whether it's on, plus the credentials for the fields its registry entry declares (internal/payments). The config holds API keys, so it never leaves the server — the panel is told only which fields are set, never their values.
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 ¶
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 RegistrationRequest ¶ added in v1.0.0
type RegistrationRequest struct {
ID int64 `json:"id"`
ChatID int64 `json:"chat_id"`
Name string `json:"name"`
CreatedAt int64 `json:"created_at"`
}
RegistrationRequest is a moderated self-registration awaiting an admin decision. No user exists yet — approval creates one and links ChatID; rejection just drops the request.
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:"-"` // derived mirror of RegMode != off (kept for old readers)
// TGUserRegMode is how self-registration behaves: RegOff | RegOpen | RegModeration
// | RegInvite. TGUserRegCode is the invite code required in RegInvite mode.
TGUserRegMode string `json:"-"`
TGUserRegCode string `json:"-"`
// 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:"-"`
// TGUserEvents is the same idea aimed the other way: which UserEvent* categories
// the USER bot pushes to the person themselves. TGUserExpiringDays is how many
// days ahead the expiry warning goes out.
TGUserEvents int64 `json:"-"`
TGUserExpiringDays int `json:"-"`
// Support relay (Settings → Telegram → Поддержка): a third bot whose only job is
// to carry messages between a user and a per-user topic in TGSupportGroupID, a
// forum supergroup the operator's admins answer in. It is separate from the user
// bot precisely so that everything sent to it is unambiguously a support request.
// TGSupportBotUsername is the cached @username the user bot links to.
TGSupportEnabled bool `json:"-"`
TGSupportBotToken string `json:"-"`
TGSupportBotUsername string `json:"-"`
TGSupportGroupID int64 `json:"-"`
TGSupportGreeting string `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 plan, free-tier fallback. The
// trial's length is the trial plan's own period_days, not a separate setting.
BillingEnabled bool `json:"-"`
BillingFreePlanID int64 `json:"-"`
BillingTrialPlanID int64 `json:"-"`
BillingPaymentNote string `json:"-"`
// PaymentWebhookSecret is the random URL segment the provider webhooks are
// mounted under (/<secret>/<provider>), so the callback path is fixed yet
// unguessable and doesn't reveal the hidden panel. Provider credentials
// themselves live in the payment_providers table, one row per provider — see
// PaymentProvider and internal/payments.
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:"-"`
// NodeAPIPath is the unguessable URL segment the node sync API is mounted under
// (/<node_api_path>/v1/{join,sync}). Empty ⇒ no nodes exist yet and the surface
// falls through to the decoy. Kept separate from APIPath and the panel secret so
// rotating either never orphans a joined node.
NodeAPIPath string `json:"-"`
// MasterLabel is the panel server's display name in share-link / subscription
// config labels (multi-node: "<master> · VLESS"). Empty ⇒ no prefix.
MasterLabel string `json:"-"`
// GeoRefreshHours is how often to auto-refresh the geo databases (hours; 0 ⇒
// never — manual only). Applies to the master and is pushed to nodes.
GeoRefreshHours int `json:"-"`
// IPListRefreshHours is how often to auto-refresh the iplist databases (hours;
// 0 ⇒ never — manual only). Its own cadence, not the geo one: the lists sit on a
// different clock (their services re-resolve every ~12h) and, unlike the .dat
// files, they are read only by the panel and never pushed to nodes.
IPListRefreshHours int `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:"-"`
// NodeLabel is computed per request for multi-node subscriptions: when set, it's
// appended to every protocol label ("VLESS · Нидерланды") so a client shows which
// node each entry belongs to. Empty for the local server / single-node installs.
NodeLabel 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
AdminEventEnabled reports whether the given AdminEvent* flag is enabled.
func (*Settings) ExpiringDays ¶ added in v1.2.0
ExpiringDays is the warning horizon, clamped to something a person would call a warning: zero would fire at the moment of expiry (which the expired notice already covers) and a huge value would warn about a subscription that is barely used.
func (*Settings) OperaCountryOr ¶
OperaCountryOr returns the configured Opera VPN region, defaulting to "EU" for an empty or unknown value.
func (*Settings) OperaPortOr ¶
OperaPortOr returns the local Opera proxy port, defaulting to 18080.
func (*Settings) ProtoLabel ¶ added in v0.11.0
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) RealitySID ¶
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 ¶
RealitySNI returns the primary (first) donor domain — used as the dialed dest and the sni= in share links.
func (*Settings) RealityServerNames ¶
RealityServerNames returns the donor SNIs the REALITY inbound accepts (RealityDest stores a comma-separated set; the first is the primary).
func (*Settings) RegMode ¶ added in v1.0.0
RegMode is the normalised self-registration mode. It falls back to the legacy TGUserRegEnabled bool for rows written before the mode column existed.
func (*Settings) RegistrationActivates ¶ added in v1.0.0
func (*Settings) RegistrationOpen ¶ added in v1.0.0
RegistrationOpen reports whether new signups are accepted at all (any mode but off). RegistrationActivates reports whether a successful signup is active at once (open/invite) rather than held for moderation.
func (*Settings) SupportLink ¶ added in v1.2.0
SupportLink is the t.me URL of the support bot, or "" when support is off or the bot's @username was never resolved. Callers render the entry point only for a non-empty result, so a half-configured relay shows no dead button — the same contract subWebAppURL has for the subscription Mini App.
func (*Settings) TelegramAuthorized ¶ added in v0.2.0
TelegramAuthorized reports whether the given chat ID is linked to the bot.
func (*Settings) TelegramChatIDs ¶ added in v0.2.0
TelegramChatIDs parses the comma-separated authorized chat IDs into int64s, skipping blanks and unparseable entries.
func (*Settings) UserNotifyEnabled ¶ added in v1.2.0
UserNotifyEnabled reports whether the given UserEvent* flag is enabled.
func (*Settings) VLESSFP ¶
VLESSFP / TrojanFP / RealityFP return the per-connection uTLS fingerprint for share links, each defaulting to "firefox".
func (*Settings) WarpRegistered ¶
WarpRegistered reports whether a WARP account has been provisioned.
type Subscriber ¶ added in v1.2.0
type Subscriber struct {
ChatID int64 `json:"chat_id"`
UserID int64 `json:"user_id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
Lang string `json:"lang"`
Active bool `json:"active"`
OptOut bool `json:"opt_out"`
BlockedAt int64 `json:"blocked_at"`
StartedAt int64 `json:"started_at"`
}
Subscriber is a chat that has opened the user bot — the audience a broadcast is addressed to. Wider than the user roster on purpose: it also holds people who never completed registration and people whose account was deleted, both of whom a broadcast may still need to reach. UserID is nil-able in SQL and 0 here when the chat isn't tied to an account.
Active and OptOut mean different things and must not be conflated: Active=false is Telegram refusing delivery (blocked or deactivated), OptOut=true is the person choosing not to receive broadcasts while still getting service messages.
type SupportGroup ¶ added in v1.2.0
type SupportGroup struct {
ChatID int64 `json:"chat_id"`
Title string `json:"title"`
IsForum bool `json:"is_forum"`
IsAdmin bool `json:"is_admin"`
}
SupportGroup is a group the support bot has been added to — an option in the settings picker, never applied on its own. IsForum and IsAdmin are shown so the operator can see at a glance which candidate is actually usable.
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:"-"`
// NotifiedExpireAt is the ExpireAt a "runs out soon" warning was already sent for.
// Holding the expiry rather than a timestamp re-arms the warning for free: a
// renewal moves ExpireAt, the stored value stops matching, and the next cycle is
// eligible again.
NotifiedExpireAt int64 `json:"-"`
// NotifiedQuotaAt marks that a "running low on traffic" notice was already sent
// (0 = not yet). Cleared once usage falls back under the threshold, which is what
// a quota reset or a plan change does — so the warning re-arms on its own.
NotifiedQuotaAt int64 `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
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
Subscribed reports whether this webhook wants the given event. An empty Events set means "all events".