Architecture

One Go binary that holds the panel, the bot and the background workers, and supervises Xray-core as a child process.

Last updated Aug 17, 2026View as Markdown

Overview

Nasnet Linux is a single Go binary that runs everything: the HTTP API, the embedded admin and subscriber panels, the Telegram bot, the background workers, and supervision of a local Xray-core process. There is no agent to install and no fleet to coordinate, because the panel and the proxy core sit on the same host.

Knowing that shape explains most of the operational advice elsewhere in these guides: why an upgrade is usually “replace the binary and restart”, why there is no message broker to keep alive, and why losing the host loses everything, which is what makes backups the thing to get right.

What the binary runs

Entry point main.gocmd/root.go, started with nasnet-panel serve.

Component What it does
HTTP server (Gin) The REST API plus the embedded admin and subscriber panels, served from a go:embeded Vite build.
Telegram bot Optional. Admin operations and user-facing subscription info.
Xray supervisor Starts Xray-core as a child process, keeps it alive with a watchdog, applies generated config.
Background services Scheduler, monitor, provisioning worker, alert engine, notification dispatcher.
Database PostgreSQL or SQLite through GORM, with automatic schema migration on boot.

How it drives Xray

The panel does not talk to Xray over a network control plane. Inbounds, outbounds, routing and balancing that you build in the panel are compiled into an Xray configuration by the config generator; the supervisor then applies it to the local core and reloads it. Stats and proxy users are read and managed through Xray’s local gRPC API, by default on 127.0.0.1:10085.

The request and response shapes exchanged with the core are defined in proto/node_agent.proto, but they are used as in-process types. There is no wire transport between the panel and the core, and therefore no mutual TLS to configure between them.

Layered code

Every feature under internal/<feature>/ follows the same four layers, which is why the codebase stays navigable as features are added:

Layer Responsibility
domain Entities and the repository/usecase interfaces. No framework imports.
repository Data access, implementing the domain interfaces against GORM.
usecase Business logic. Orchestrates repositories, enforces rules, emits events.
delivery Transport adapters, meaning the Gin handlers and Telegram bot handlers that call usecases.

Reusable libraries live in pkg/, covering JWT, ACME, metrics, scheduler, events, cache, geoip, i18n, WireGuard keys and Xray helpers. HTTP and Telegram wiring lives in transport/. Everything is constructed once at startup in cmd/bootstrap.go and injected.

Events and background work

  • Event bus. Usecases publish domain events such as subscription created, subscription expired, and Xray down or recovered. The metrics collector, the alert engine and the notification dispatcher subscribe to them.
  • Scheduler. Periodic jobs covering subscription expiry, usage aggregation, certificate renewal, retention cleanup, digests and notifications.
  • Monitor. Polls Xray and host health, raising events when state changes.
  • Provisioning worker. Applies account add and remove work against the local core from a queue.
  • Alert engine. Evaluates rules against events and emits system alerts, which the dispatcher routes to Telegram, Discord or a webhook.

What happens when a subscriber fetches their config

  1. The client requests /sub/{link_key}.
  2. The HTTP layer resolves the subscription and checks expiry and traffic, and detects the client app from the User-Agent.
  3. The subscription usecase asks the Xray provider to build per-client configs and links from the subscription’s inbounds and the server’s host data.
  4. The response goes back as a base64 feed with usage metadata in the headers.

Persistence

GORM models from every feature are registered and auto-migrated on startup, and the boot sequence runs idempotent data backfills alongside them. PostgreSQL picks up a few GIN indexes that SQLite does not support; otherwise the two behave the same.