S-UI Inbound and Outbound Registry Architecture Explained
S-UI manages proxy configurations through a dual-registry system—Inbounds for traffic listeners and Outbounds for destinations—using a layered Go architecture that separates database persistence, service logic, and runtime synchronization with the Sing-Box core.
S-UI is a web-based management interface for the Sing-Box proxy platform. The application implements a robust registry system to handle dynamic proxy configurations without requiring process restarts. This article examines the architecture of S-UI's inbound and outbound registry system based on the actual source code in the alireza0/s-ui repository.
Layered System Architecture
The registry implementation follows a strict separation of concerns across three distinct layers. The Persistence Layer stores definitions in SQLite using GORM models defined in database/model/inbounds.go and database/model/outbounds.go. The Service Layer provides CRUD operations, JSON marshaling for the Sing-Box core, and user-link enrichment through service/inbounds.go and service/outbounds.go. The Runtime Integration Layer maintains live synchronization with the running Sing-Box instance via corePtr methods including AddInbound, RemoveInbound, AddOutbound, and RemoveOutbound.
Inbound Registry Implementation
Database Models
Inbound definitions persist through the Inbound struct in database/model/inbounds.go. The schema captures fixed fields alongside flexible protocol-specific options:
type Inbound struct {
Id uint `gorm:"primaryKey;autoIncrement"`
Type string
Tag string `gorm:"unique"`
TlsId uint // FK to TLS table
Tls *Tls // optional TLS configuration
Addrs json.RawMessage
OutJson json.RawMessage
Options json.RawMessage // all other fields
}
The Options field stores dynamic configuration—such as listen addresses and shadowtls versions—allowing the system to support new protocols without database migrations. Both Inbound and Outbound models implement custom UnmarshalJSON and MarshalJSON methods to partition fixed schema fields from dynamic JSON payloads.
Service Layer Operations
The InboundService in service/inbounds.go coordinates between the database and the running core. Read APIs including Get, GetAll, and FromIds retrieve raw database rows or enriched maps containing id, type, tag, tls_id, listen, and listen_port fields.
The Save method handles lifecycle states "new", "edit", and "del" through a specific sequence:
- Parse the inbound payload using
UnmarshalJSON - For running cores, remove the old tag and invoke
corePtr.AddInbound - Persist the record via
tx.Save - Update related client tables through
UpdateClientsOnInboundAddandUpdateLinksByInboundChange
User Enrichment Process
When an inbound supports user authentication, the service triggers user enrichment through helper methods addUsers, initUsers, and fetchUsers. The hasUser function determines protocol capability, then queries the clients table to embed user JSON blobs under the users key in the final configuration. This mechanism supports protocols like VLESS, VMESS, and Shadowsocks.
Graceful Restart Mechanism
The RestartInbounds function implements hot-reload functionality without process termination. It removes existing inbounds via corePtr.RemoveInbound, closes active connections using ConnTracker().CloseConnByInbound, regenerates JSON including refreshed user data, and re-adds the configuration through corePtr.AddInbound.
Outbound Registry Structure
Outbound management mirrors the inbound pattern but excludes user-related complexity. The Outbound struct in database/model/outbounds.go maintains a simplified schema:
type Outbound struct {
Id uint `gorm:"primaryKey;autoIncrement"`
Type string
Tag string `gorm:"unique"`
Options json.RawMessage // protocol-specific options
}
The OutboundService in service/outbounds.go provides equivalent CRUD functionality. Since outbounds function purely as destination definitions, they never require user enrichment logic. The Save method handles all three operation types—"new", "edit", and "del"—by marshaling the outbound configuration, synchronizing with the core via corePtr.AddOutbound or corePtr.RemoveOutbound, and persisting changes to the database.
Runtime Synchronization Flow
The complete interaction flow demonstrates how changes propagate from API to execution:
- API Handling:
api/apiHandler.goreceives HTTP requests and delegates toInboundService.SaveorOutboundService.Save - JSON Generation: Services generate complete Sing-Box compatible JSON through
GetAllConfig, including populated TLS objects and user entries - Core Synchronization: The service layer invokes
corePtrmethods to apply changes to the running Sing-Box instance immediately - Database Persistence: Canonical representations store in SQLite via GORM models
This hot-reload capability ensures runtime state reflects configuration changes without service interruption.
Practical Configuration Examples
Adding an HTTP Inbound with TLS
payload := []byte(`{
"type":"http",
"tag":"http_in",
"tls_id":1,
"listen":"0.0.0.0",
"listen_port":8080
}`)
err := inboundService.Save(tx, "new", payload, "1,2", "my.example.com")
This call stores the inbound, associates TLS configuration, registers it with the Sing-Box core, and links client IDs 1 and 2 as authorized users.
Editing an Outbound Configuration
payload := []byte(`{
"id":10,
"type":"shadowsocks",
"tag":"ss_out",
"method":"aes-256-gcm",
"address":"1.2.3.4",
"port":8388
}`)
err := outboundService.Save(tx, "edit", payload)
The service removes the previous tag, applies the new Shadowsocks configuration to the core, and updates the database record atomically.
Bulk Restarting Inbounds
ids := []uint{3, 5, 7}
err := inboundService.RestartInbounds(tx, ids)
This removes specified inbounds from the core, terminates their connections, refreshes user data and TLS configurations, and re-registers them with the Sing-Box runtime.
Summary
- Layered Architecture: Clear separation between database models (
database/model/files), service logic (service/inbounds.goandservice/outbounds.go), and runtime integration (corePtrmethods) - Dynamic Schemas:
Optionsfields in both registries allow protocol-specific configurations without database migrations - Hot-Reload Capability: Direct manipulation of the Sing-Box core via
AddInbound,RemoveInbound,AddOutbound, andRemoveOutboundenables runtime updates - User Integration: Inbounds automatically enrich configurations with client data from the
clientstable for protocols requiring authentication - Graceful Restarts:
RestartInboundsclears connections and refreshes configurations without process termination
Frequently Asked Questions
How does S-UI handle new protocols without changing the database schema?
The registry system stores protocol-specific parameters in the Options field as json.RawMessage. This design allows arbitrary key-value configurations for new protocols while maintaining fixed database columns for common attributes like type, tag, and id. The custom MarshalJSON and UnmarshalJSON methods handle the serialization logic transparently.
What happens when an administrator edits an active inbound?
The Save method in service/inbounds.go implements an atomic update sequence. It first removes the existing inbound from the running Sing-Box core using corePtr.RemoveInbound, then adds the updated configuration via corePtr.AddInbound, and finally commits the transaction to the SQLite database. This ensures consistency between runtime state and persistent storage.
Why do inbounds require user enrichment while outbounds do not?
Inbounds accept incoming client connections and often require user authentication for protocols like VLESS, VMESS, and Shadowsocks. The service layer queries the clients table through fetchUsers and embeds user credentials in the JSON payload. Outbounds represent upstream destinations and typically authenticate using static credentials or certificates stored directly in their Options field, eliminating the need for dynamic user lookups.
How does the registry achieve hot-reload without restarting Sing-Box?
S-UI maintains a global corePtr reference to the running Sing-Box instance. Service methods call AddInbound, RemoveInbound, AddOutbound, and RemoveOutbound directly on this pointer, invoking Sing-Box's internal API for dynamic configuration changes. Combined with connection tracking via ConnTracker().CloseConnByInbound, the system updates routing rules and listeners while maintaining existing connections where possible.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →