webui

package
v1.3.9 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: AGPL-3.0 Imports: 50 Imported by: 0

Documentation

Overview

Package webui serves an authenticated, dark-themed web dashboard on port 7777 showing live system metrics, tunnels and their logs.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Disable

func Disable() error

Disable stops and removes the web-panel service.

func Running

func Running() bool

Running reports whether the web-panel service is active.

func Save

func Save(c Config) error

Save persists the config (0600, root only).

func Serve

func Serve() error

Serve starts the web panel and blocks. Invoked by `hashem --webui`.

func TunnelLogs

func TunnelLogs(name string) string

TunnelLogs returns the last N journal lines for a tunnel service.

Types

type Config

type Config struct {
	Username string `json:"username,omitempty"` // Login username (default: admin)
	Password string `json:"password"`           // Login password
	Port     int    `json:"port"`

	// BasePath is the secret path segment the whole panel lives under, so it
	// answers at http://host:7777/<BasePath>/ and at nothing else.
	//
	// It is not authentication and does not pretend to be — the password is
	// still what lets anybody in. What it changes is who ever reaches the
	// password prompt. A panel on a known port at "/" is found by anything that
	// sweeps the internet within hours of being started, and from then on it is
	// answering login attempts from strangers forever. Behind a path nobody can
	// guess, those sweeps get a 404 and go away, and the login page is seen by
	// people who were told where it is.
	//
	// Generated on first use and on upgrade, so every panel has one; see
	// EnsureBasePath. "/" turns it off for an operator who wants the panel at
	// the root, which is the only way to get that now.
	BasePath string `json:"base_path,omitempty"`

	// HTTPS, when set, serves the panel over TLS instead of plain HTTP.
	//
	// It is off by default and stays that way on upgrade: a panel reached at
	// http://ip:7777 keeps working exactly as it did. Turning it on is a
	// deliberate act, because it changes the address people have bookmarked.
	//
	// TLSDomain switches to Let's Encrypt for that name, which must resolve to
	// this server; empty means the generated self-signed certificate, which
	// works on a bare IP. Certificates renew themselves either way — an ACME
	// one is reissued well before its ninety days are up and picked up on the
	// next connection, with no restart.
	HTTPS     bool   `json:"https,omitempty"`
	TLSDomain string `json:"tls_domain,omitempty"`
	TLSEmail  string `json:"tls_email,omitempty"`
	// TLSSelfHost is an optional domain or IP to add to the self-signed
	// certificate's SANs, for reaching the panel by a name that has no public
	// DNS for Let's Encrypt (an internal domain, a host that is only in the
	// operator's /etc/hosts). It changes nothing about which addresses already
	// work — every local IP and loopback are always included — it only adds one
	// the machine cannot discover on its own. Empty is the common case.
	TLSSelfHost string `json:"tls_self_host,omitempty"`
}

Config is the persisted web-panel configuration.

func EnsurePassword

func EnsurePassword() (Config, error)

EnsurePassword returns the config, generating and saving an 8-digit password and a base path if either is missing.

The base path is generated here rather than only on a fresh install, so a panel that has been running at "/" for a year gets one on the next upgrade. That does move the address: the CLI's Web Panel screen prints the whole URL, including the path, which is where an operator whose bookmark stopped working is told to look — and the address is on the machine they already have a shell on, which is the one place it can be found without being findable by anybody else.

func EnsureRunning

func EnsureRunning() (Config, error)

EnsureRunning makes sure a password exists and the web-panel systemd service is installed and running. Safe to call repeatedly (idempotent).

func Load

func Load() Config

Load reads the saved config, filling defaults for missing fields.

func RegenerateBasePath

func RegenerateBasePath() (Config, error)

RegenerateBasePath moves the panel to a new unguessable path and restarts it.

The path is not a secret that has to be rotated on a schedule — it is not authentication and nothing is signed with it. What it is for is the case where it stopped being unguessable: pasted into a chat, screenshotted, typed on a machine that was not the operator's. Then the old one is worth throwing away, and there has to be a way to do it that does not involve editing JSON.

The restart is what makes it take effect; the running server read its path once, at startup.

func RegeneratePassword

func RegeneratePassword() (Config, error)

RegeneratePassword creates a new 8-digit password and restarts the panel.

func SetBasePath

func SetBasePath(path string) (Config, error)

SetBasePath persists a path the operator chose, or "/" to put the panel back at the root, and restarts it.

func SetCredentials

func SetCredentials(user, pw string) (Config, error)

SetCredentials persists both username and password and restarts the panel service.

func SetPassword

func SetPassword(pw string) (Config, error)

SetPassword persists a custom password and restarts the panel service so the change takes effect. Used from the CLI (a separate process from the server).

func SetPort

func SetPort(port int) (Config, error)

SetPort persists a new panel port and restarts the panel service so it listens there. Used from the CLI (a separate process from the server).

func SetUsername

func SetUsername(user string) (Config, error)

SetUsername persists a custom username and restarts the panel service so the change takes effect. Used from the CLI (a separate process from the server).

func (Config) PathPrefix

func (c Config) PathPrefix() string

PathPrefix is the panel's base path as a URL prefix: "/x7Kq2p" or "" when the panel is at the root. Always without a trailing slash, so callers build addresses by appending.

func (Config) Scheme

func (c Config) Scheme() string

Scheme is the URL scheme the panel answers on.

func (Config) URL

func (c Config) URL(host string) string

URL is where this panel answers, given the host to reach it at.

type RatePoint

type RatePoint struct {
	T   int64   `json:"t"`   // unix seconds
	In  float64 `json:"in"`  // bytes/s received over the tunnel
	Out float64 `json:"out"` // bytes/s sent over the tunnel
}

RatePoint is one sparkline sample: bytes per second at a moment in time.

type SessionEntry

type SessionEntry struct {
	ID      string `json:"id"`
	IP      string `json:"ip"`
	Created string `json:"created"`
	Current bool   `json:"current"`
}

SessionEntry is one row of the Settings session list.

type SystemStats

type SystemStats struct {
	Hostname string `json:"hostname"`
	OS       string `json:"os"`
	Uptime   string `json:"uptime"`

	IPv4 string `json:"ipv4"`
	IPv6 string `json:"ipv6"`
	// Where IPv4 was decided from — "interface" when the machine holds the
	// address itself, "echo" when it had to be inferred from how this host
	// appears to an outside service. Location and ISP are looked up from this
	// address, so when it is the inferred one the panel says so rather than
	// presenting a guess as a fact.
	IPv4Source string `json:"ipv4Source"`
	Location   string `json:"location"`
	ISP        string `json:"isp"`

	CPUPercent float64 `json:"cpuPercent"`
	CPUCores   int     `json:"cpuCores"`
	Load       string  `json:"load"`

	MemUsed    string  `json:"memUsed"`
	MemTotal   string  `json:"memTotal"`
	MemPercent float64 `json:"memPercent"`

	SwapUsed    string  `json:"swapUsed"`
	SwapTotal   string  `json:"swapTotal"`
	SwapPercent float64 `json:"swapPercent"`

	DiskUsed    string  `json:"diskUsed"`
	DiskTotal   string  `json:"diskTotal"`
	DiskPercent float64 `json:"diskPercent"`

	// Traffic carried by the tunnels — every tunnel's persisted counters added
	// up, so the headline figure is the sum of what the cards show rather than a
	// larger number nothing on the page accounts for. It used to be the machine's
	// NIC counters, which also include ssh, apt and the panel itself, and which
	// reset on reboot while the per-tunnel counters survive one.
	TotalSent    string `json:"totalSent"`
	TotalRecv    string `json:"totalRecv"`
	TotalTraffic string `json:"totalTraffic"`
	// Speed stays on the interface counters: it answers "what is this box doing
	// right now", which is the question a live rate is read for, and a tunnel's
	// own rate is already on its card.
	UpSpeed   string `json:"upSpeed"`
	DownSpeed string `json:"downSpeed"`

	// The same five measurements as plain numbers — bytes, and bytes per
	// second.
	//
	// The strings above are formatted for a reader and are what the classic
	// panel prints. Anything that has to compute rather than print needs the
	// number: the new panel scales a column history against a peak, works out
	// each tunnel's share of the total, and animates the headline figure up to
	// its value. Number("873 B/s") is NaN, so every one of those silently
	// became zero — a page reporting no traffic on a link that was carrying
	// it. Sending both is the honest fix; parsing a formatted string back into
	// a number in the browser is guesswork about units that will be wrong the
	// first time the formatter changes.
	UpBps             float64 `json:"upBps"`
	DownBps           float64 `json:"downBps"`
	TotalSentBytes    uint64  `json:"totalSentBytes"`
	TotalRecvBytes    uint64  `json:"totalRecvBytes"`
	TotalTrafficBytes uint64  `json:"totalTrafficBytes"`

	TunnelsTotal   int `json:"tunnelsTotal"`
	TunnelsRunning int `json:"tunnelsRunning"`

	// MonitorRunning reports the hashem-monitor service — the watchdog, the
	// Telegram bot and the alerts live there, not in this panel. When it is
	// down, dropped tunnels are not restarted and no alert fires, and nothing
	// else visibly breaks — which is exactly why the panel must say so.
	MonitorRunning bool `json:"monitorRunning"`

	// Version is what is running here, so the update notice can say what it is
	// asking the operator to move away from rather than only where to.
	Version string `json:"version,omitempty"`

	// UpdateTag is the newer release the cached background check knows about,
	// empty when this version is current. Same source as the CLI's notice and
	// the Telegram announcement, so the three can never disagree.
	UpdateTag string `json:"updateTag,omitempty"`

	// The built-in proxy, when the operator has turned it on. It is off by
	// default, so all of this stays empty and the panel shows nothing.
	//
	// ProxyEnabled and ProxyRunning are deliberately separate. The proxy is a
	// service of its own, and a tunnel can be forwarding a port to it while it
	// is dead: the tunnel is up, the panel is green, and every connection
	// through that port is refused at the far end. Only the two together say
	// whether the thing actually answers.
	ProxyEnabled bool   `json:"proxyEnabled,omitempty"`
	ProxyRunning bool   `json:"proxyRunning,omitempty"`
	ProxyType    string `json:"proxyType,omitempty"`
	ProxyPort    int    `json:"proxyPort,omitempty"`

	// Congestion is the TCP congestion control the tunnel's own sockets run
	// under; CongestionWanted is what they ask for. They differ when the kernel
	// does not have the requested algorithm, and the request is silently
	// dropped by design — the connection still works, just not as fast on a
	// long lossy path, and the presets were tuned expecting it to be there.
	// Empty means the question has no answer here (not Linux), so the panel
	// says nothing rather than guessing.
	Congestion       string `json:"congestion,omitempty"`
	CongestionWanted string `json:"congestionWanted,omitempty"`
}

SystemStats is the payload for /api/stats.

func GatherSystem

func GatherSystem() SystemStats

GatherSystem collects the current system statistics.

type TunnelInfo

type TunnelInfo struct {
	Kind string `json:"kind"`
	Name string `json:"name"`
	Role string `json:"role"`

	// Transport is the raw value the rest of the panel keys off — "tcp",
	// "l3/pck", "direct/wss". Kept exactly as it was, because the edit form and
	// several capability checks compare against it.
	Transport string `json:"transport"`

	// Direction and Carrier are the same thing split for the card, which has
	// two badges rather than one: whether the tunnel is dialled from Iran or
	// from kharej, and what carries it.
	//
	// Split here rather than in the browser because the browser would have to
	// know which prefixes mean what — and would then be a second place that
	// has to learn about every new tunnel kind, and the place nobody remembers
	// to update. "l3/pck" on a card was the symptom: an internal name, leaking
	// out because there was one field where there are two facts.
	Direction    string `json:"direction"`
	Carrier      string `json:"carrier"`
	Addr         string `json:"addr"`
	Ports        string `json:"ports"`
	State        string `json:"state"`
	Ping         int    `json:"ping"` // milliseconds, -1 = n/a
	PeerLocation string `json:"peerLocation"`
	PeerISP      string `json:"peerISP"`
	BotRelay     bool   `json:"botRelay"` // has a hidden port used for the Telegram relay
	// BotRelayPort is the loopback port that relay listens on. It is shown on
	// its own, under its own name, rather than as the raw mapping: the mapping
	// reads like something the operator set up and can therefore tidy away,
	// and removing it stops the bot for a reason that looks unconnected.
	BotRelayPort int    `json:"botRelayPort,omitempty"`
	Country      string `json:"country"` // user-chosen ISO country code (label)
	// PeerCountry is the ISO code detected from the peer's address, used for
	// the flag. It is separate from Country so a label the user set by hand is
	// never silently overwritten by a lookup.
	PeerCountry string `json:"peerCountry"`
	// TunnelPort is the port clients dial, pulled out of the bind address
	// because ":1231" is what matters and "0.0.0.0:1231" is noise.
	TunnelPort string `json:"tunnelPort"`

	// ServiceDown says the tunnel is up and delivering into nothing: the
	// service it forwards to, on the machine at the other end, is refusing
	// every connection.
	//
	// State stays "online", because the tunnel is. This is the sentence beside
	// it that says why nothing works anyway — the reading an operator used to
	// have to go and find in the far machine's journal.
	ServiceDown string `json:"serviceDown,omitempty"`

	// Node is the managed server holding this tunnel's other end, when this
	// panel built both. Empty for a tunnel whose far end was set up by hand,
	// which is a real and ordinary thing to have.
	//
	// The panel needs it to know there is a second side it can act on at all —
	// a log to read there, a speed test that can start a receiver — without
	// asking the fleet about every tunnel on every poll.
	Node     string `json:"node,omitempty"`
	PeerName string `json:"peerName,omitempty"`

	// From the tunnel's metrics snapshot (empty when none has been written yet).
	Uptime   string `json:"uptime,omitempty"`
	BytesIn  string `json:"bytesIn,omitempty"`
	BytesOut string `json:"bytesOut,omitempty"`
	// InBytes/OutBytes/TotalBytes are the same three as numbers, for the same
	// reason as the system totals above: the panel sorts tunnels by what they
	// have carried and draws each one's share of the busiest, and neither is
	// possible with "200.0 MiB".
	InBytes    uint64 `json:"inBytes,omitempty"`
	OutBytes   uint64 `json:"outBytes,omitempty"`
	TotalBytes uint64 `json:"totalBytes,omitempty"`
	// BytesTotal is the two added. The card shows all three on one line, and a
	// sum of two already-formatted strings is not something the browser can do.
	BytesTotal string `json:"bytesTotal,omitempty"`
	/* TrafficMeasured says whether the figures here are a reading or a
	  silence.
	*
	* A measured zero is a fact - the tunnel is up and has carried nothing yet
	* - and no figures at all is a different fact, and the two are the same
	* JSON: omitempty drops a zero. The strings above do distinguish them
	* ("0 B" is present, absence is not), but only a reader that knows to look
	* can tell, so the reading is stated rather than implied. Absent whenever
	* nothing could be measured. */
	TrafficMeasured bool `json:"trafficMeasured,omitempty"`
	// KCP link-quality counters; nil on every other transport.
	KCP *metrics.KCPStats `json:"kcp,omitempty"`
	// KCPLossPercent is derived from the counters above: how much of the sent
	// traffic needed resending — the honest answer to "is this link lossy?".
	KCPLossPercent float64 `json:"kcpLossPercent,omitempty"`
	// Pool is the client's connection pool; nil on a server tunnel and on the
	// transports that do not keep one.
	Pool *metrics.PoolStats `json:"pool,omitempty"`

	// From the tunnel's own config.
	Preset         string   `json:"preset,omitempty"`         // display label: Balance / Turbo / Aggressive / Custom
	MaxConnections int      `json:"maxConnections,omitempty"` // 0 = unlimited
	BandwidthMbps  int      `json:"bandwidthMbps,omitempty"`  // 0 = unlimited
	ProxyProtocol  bool     `json:"proxyProtocol,omitempty"`
	LoadBalance    bool     `json:"loadBalance,omitempty"`
	FallbackAddrs  []string `json:"fallbackAddrs,omitempty"`
	// CertType is "letsencrypt" or "self-signed", only for wss/wssmux servers.
	CertDomain string `json:"certDomain,omitempty"`
	CertType   string `json:"certType,omitempty"`
	// CertExpiry is the NotAfter date of the certificate on disk, when it can
	// be read. ACME certificates renew themselves, so no expiry is shown.
	CertExpiry string `json:"certExpiry,omitempty"`

	// Rates is the recent transfer speed of this tunnel, oldest first, for the
	// dashboard's sparkline. Derived from successive metrics snapshots.
	Rates []RatePoint `json:"rates,omitempty"`
}

TunnelInfo is one row for /api/tunnels.

Jump to

Keyboard shortcuts

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