feat: add universal user_manager service for external auth & traffic tracking
Some checks are pending
Build / Calculate version (push) Waiting to run
Build / Build binary (push) Blocked by required conditions
Build / Build Darwin binaries (push) Blocked by required conditions
Build / Build Windows binaries (push) Blocked by required conditions
Build / Build Android (push) Blocked by required conditions
Build / Publish Android (push) Blocked by required conditions
Build / Build Apple clients (push) Blocked by required conditions
Build / Upload builds (push) Blocked by required conditions
Some checks are pending
Build / Calculate version (push) Waiting to run
Build / Build binary (push) Blocked by required conditions
Build / Build Darwin binaries (push) Blocked by required conditions
Build / Build Windows binaries (push) Blocked by required conditions
Build / Build Android (push) Blocked by required conditions
Build / Publish Android (push) Blocked by required conditions
Build / Build Apple clients (push) Blocked by required conditions
Build / Upload builds (push) Blocked by required conditions
- New UserManager service with HTTPS auth, credential sync, traffic tracking, and kick API - Integrates into all 10 protocol inbounds (hysteria2, tuic, vless, vmess, trojan, shadowsocks, http, socks, mixed, naive) - Each inbound auto-detects user_manager from service context, falls back to static config - Auth server contract: POST /api/auth, GET /api/credentials, POST /api/traffic
This commit is contained in:
parent
17852ccaaa
commit
a3404e463f
19 changed files with 1364 additions and 88 deletions
23
adapter/user_manager.go
Normal file
23
adapter/user_manager.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package adapter
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type UserTraffic struct {
|
||||||
|
Tx int64 `json:"tx"`
|
||||||
|
Rx int64 `json:"rx"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserInfo struct {
|
||||||
|
Username string
|
||||||
|
Credential string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserManager interface {
|
||||||
|
Service
|
||||||
|
Authenticate(ctx context.Context, protocol string, credential string, addr string) (username string, err error)
|
||||||
|
GetCredentials(protocol string) ([]UserInfo, error)
|
||||||
|
ReportTraffic(username string, tx int64, rx int64)
|
||||||
|
GetTraffic(username string) (tx int64, rx int64)
|
||||||
|
ListTraffic() map[string]UserTraffic
|
||||||
|
KickUser(username string)
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ const (
|
||||||
TypeHysteriaRealm = "hysteria-realm"
|
TypeHysteriaRealm = "hysteria-realm"
|
||||||
TypeACME = "acme"
|
TypeACME = "acme"
|
||||||
TypeCloudflareOriginCA = "cloudflare-origin-ca"
|
TypeCloudflareOriginCA = "cloudflare-origin-ca"
|
||||||
|
TypeUserManager = "user_manager"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,7 @@ func ServiceRegistry() *service.Registry {
|
||||||
registerOCMService(registry)
|
registerOCMService(registry)
|
||||||
registerOOMKillerService(registry)
|
registerOOMKillerService(registry)
|
||||||
registerUSBIPServices(registry)
|
registerUSBIPServices(registry)
|
||||||
|
registerUserManagerService(registry)
|
||||||
|
|
||||||
return registry
|
return registry
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
include/usermanager.go
Normal file
10
include/usermanager.go
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package include
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/adapter/service"
|
||||||
|
"github.com/sagernet/sing-box/service/usermanager"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerUserManagerService(registry *service.Registry) {
|
||||||
|
usermanager.RegisterService(registry)
|
||||||
|
}
|
||||||
16
option/user_manager.go
Normal file
16
option/user_manager.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package option
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing/common/json/badoption"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserManagerOptions struct {
|
||||||
|
AuthServer string `json:"auth_server,omitempty"`
|
||||||
|
Timeout badoption.Duration `json:"timeout,omitempty"`
|
||||||
|
CacheTTL badoption.Duration `json:"cache_ttl,omitempty"`
|
||||||
|
RefreshInterval badoption.Duration `json:"refresh_interval,omitempty"`
|
||||||
|
ReportInterval badoption.Duration `json:"report_interval,omitempty"`
|
||||||
|
ReportTraffic bool `json:"report_traffic,omitempty"`
|
||||||
|
APISecret string `json:"api_secret,omitempty"`
|
||||||
|
APIListen string `json:"api_listen,omitempty"`
|
||||||
|
}
|
||||||
65
patches.md
Normal file
65
patches.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Upstream Changes
|
||||||
|
|
||||||
|
## UserManager — External Authentication & Traffic Tracking
|
||||||
|
|
||||||
|
A new `user_manager` service that turns sing-box into a universal proxy server for external auth servers.
|
||||||
|
|
||||||
|
### New files
|
||||||
|
|
||||||
|
- `adapter/user_manager.go` — `UserManager` interface: `Authenticate`, `GetCredentials`, `ReportTraffic`, `GetTraffic`, `ListTraffic`, `KickUser`
|
||||||
|
- `option/user_manager.go` — `UserManagerOptions`: `auth_server`, `cache_ttl`, `refresh_interval`, `report_interval`, `report_traffic`, `api_secret`, `api_listen`, `timeout`
|
||||||
|
- `service/usermanager/manager.go` — full implementation:
|
||||||
|
- On-demand HTTPS auth (`POST /api/auth`) with in-memory TTL cache
|
||||||
|
- Periodic credential sync (`GET /api/credentials`) populates per-protocol user lists
|
||||||
|
- Per-user traffic counters (`atomic.Int64`) reported via `POST /api/traffic`
|
||||||
|
- `ConnectionTracker` impl wrapping conns with byte counters (`RoutedConnection`/`RoutedPacketConnection`)
|
||||||
|
- HTTP API on configurable `api_listen`:
|
||||||
|
- `GET /traffic` — all users traffic
|
||||||
|
- `GET /traffic/{user}` — single user
|
||||||
|
- `POST /kick/{user}` — evict user from auth cache
|
||||||
|
- Bearer token auth on all API endpoints via `api_secret`
|
||||||
|
- `service/usermanager/registry.go` — service registration
|
||||||
|
- `include/usermanager.go` — include build hook
|
||||||
|
- `constant/proxy.go` — `TypeUserManager` constant
|
||||||
|
|
||||||
|
### Modified files — Protocol inbound integration
|
||||||
|
|
||||||
|
All 10 protocol inbounds detect the global `user_manager` from service context and use it when present:
|
||||||
|
|
||||||
|
| Protocol | Type | Approach |
|
||||||
|
|----------|------|----------|
|
||||||
|
| hysteria2 | Service-based | `service.UpdateUsers` with password credentials, 30s refresh loop |
|
||||||
|
| tuic | Service-based | `service.UpdateUsers` with UUID+password (format `"uuid:password"`), 30s refresh loop |
|
||||||
|
| vless | Service-based | `service.UpdateUsers` with UUID credentials, 30s refresh loop |
|
||||||
|
| vmess | Service-based | `service.UpdateUsers` with UUID credentials, 30s refresh loop |
|
||||||
|
| trojan | Service-based | `service.UpdateUsers` with password credentials, 30s refresh loop |
|
||||||
|
| shadowsocks (multi) | Service-based | `service.UpdateUsersWithPasswords` with password credentials, 30s refresh loop |
|
||||||
|
| http | Authenticator-based | `*auth.Authenticator` rebuilt from `GetCredentials`, 30s refresh loop |
|
||||||
|
| socks | Authenticator-based | `*auth.Authenticator` rebuilt from `GetCredentials`, 30s refresh loop |
|
||||||
|
| mixed | Authenticator-based | `*auth.Authenticator` rebuilt from `GetCredentials`, 30s refresh loop |
|
||||||
|
| naive | Authenticator-based | `*auth.Authenticator` rebuilt from `GetCredentials`, 30s refresh loop; skips `"missing users"` check when user_manager is configured |
|
||||||
|
|
||||||
|
Each inbound falls back to its original static config when no `user_manager` service is registered.
|
||||||
|
|
||||||
|
### Credential format per protocol
|
||||||
|
|
||||||
|
| Protocol | `UserInfo.Credential` format |
|
||||||
|
|------------|------------------------------|
|
||||||
|
| hysteria2 | password |
|
||||||
|
| tuic | `uuid:password` |
|
||||||
|
| vless | uuid |
|
||||||
|
| vmess | uuid |
|
||||||
|
| trojan | password |
|
||||||
|
| shadowsocks| password (method from static config) |
|
||||||
|
| http | password (username from `UserInfo.Username`) |
|
||||||
|
| socks | password (username from `UserInfo.Username`) |
|
||||||
|
| mixed | password (username from `UserInfo.Username`) |
|
||||||
|
| naive | password (username from `UserInfo.Username`) |
|
||||||
|
|
||||||
|
### Auth server API contract
|
||||||
|
|
||||||
|
The auth server (`auth_server`) must implement:
|
||||||
|
|
||||||
|
- `POST /api/auth` — body `{"protocol":"...", "credential":"...", "addr":"..."}`, returns `{"ok":true, "id":"username"}`
|
||||||
|
- `GET /api/credentials` — returns `{"credentials":[{"protocol":"...", "credential":"...", "username":"..."}]}`
|
||||||
|
- `POST /api/traffic` (optional, when `report_traffic=true`) — body `{"traffic":{"user1":{"tx":123,"rx":456}}}`
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
std_bufio "bufio"
|
std_bufio "bufio"
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
|
|
@ -18,6 +19,7 @@ import (
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/protocol/http"
|
"github.com/sagernet/sing/protocol/http"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterInbound(registry *inbound.Registry) {
|
func RegisterInbound(registry *inbound.Registry) {
|
||||||
|
|
@ -33,14 +35,28 @@ type Inbound struct {
|
||||||
listener *listener.Listener
|
listener *listener.Listener
|
||||||
authenticator *auth.Authenticator
|
authenticator *auth.Authenticator
|
||||||
tlsConfig tls.ServerConfig
|
tlsConfig tls.ServerConfig
|
||||||
|
userManager adapter.UserManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) {
|
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) {
|
||||||
|
authenticator := auth.NewAuthenticator(options.Users)
|
||||||
|
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||||
|
if userManager != nil {
|
||||||
|
creds, err := userManager.GetCredentials(C.TypeHTTP)
|
||||||
|
if err == nil && len(creds) > 0 {
|
||||||
|
users := make([]auth.User, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||||
|
}
|
||||||
|
authenticator = auth.NewAuthenticator(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
inbound := &Inbound{
|
inbound := &Inbound{
|
||||||
Adapter: inbound.NewAdapter(C.TypeHTTP, tag),
|
Adapter: inbound.NewAdapter(C.TypeHTTP, tag),
|
||||||
router: uot.NewRouter(router, logger),
|
router: uot.NewRouter(router, logger),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
authenticator: auth.NewAuthenticator(options.Users),
|
authenticator: authenticator,
|
||||||
|
userManager: userManager,
|
||||||
}
|
}
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{
|
tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{
|
||||||
|
|
@ -76,9 +92,28 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return E.Cause(err, "create TLS config")
|
return E.Cause(err, "create TLS config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if h.userManager != nil {
|
||||||
|
go h.credentialLoop()
|
||||||
|
}
|
||||||
return h.listener.Start()
|
return h.listener.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) credentialLoop() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
creds, err := h.userManager.GetCredentials(C.TypeHTTP)
|
||||||
|
if err != nil || len(creds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
users := make([]auth.User, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||||
|
}
|
||||||
|
h.authenticator = auth.NewAuthenticator(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Inbound) Close() error {
|
func (h *Inbound) Close() error {
|
||||||
return common.Close(
|
return common.Close(
|
||||||
h.listener,
|
h.listener,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ type Inbound struct {
|
||||||
tlsConfig tls.ServerConfig
|
tlsConfig tls.ServerConfig
|
||||||
service *hysteria2.Service[int]
|
service *hysteria2.Service[int]
|
||||||
userNameList []string
|
userNameList []string
|
||||||
|
userManager adapter.UserManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) {
|
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) {
|
||||||
|
|
@ -183,17 +184,39 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
userList := make([]int, 0, len(options.Users))
|
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||||
userNameList := make([]string, 0, len(options.Users))
|
|
||||||
userPasswordList := make([]string, 0, len(options.Users))
|
var userList []int
|
||||||
for index, user := range options.Users {
|
var userNameList []string
|
||||||
userList = append(userList, index)
|
var userPasswordList []string
|
||||||
userNameList = append(userNameList, user.Name)
|
|
||||||
userPasswordList = append(userPasswordList, user.Password)
|
if userManager != nil {
|
||||||
|
creds, err := userManager.GetCredentials(C.TypeHysteria2)
|
||||||
|
if err == nil && len(creds) > 0 {
|
||||||
|
userList = make([]int, len(creds))
|
||||||
|
userNameList = make([]string, len(creds))
|
||||||
|
userPasswordList = make([]string, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
userList[i] = i
|
||||||
|
userNameList[i] = c.Username
|
||||||
|
userPasswordList[i] = c.Credential
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userList = make([]int, 0, len(options.Users))
|
||||||
|
userNameList = make([]string, 0, len(options.Users))
|
||||||
|
userPasswordList = make([]string, 0, len(options.Users))
|
||||||
|
for index, user := range options.Users {
|
||||||
|
userList = append(userList, index)
|
||||||
|
userNameList = append(userNameList, user.Name)
|
||||||
|
userPasswordList = append(userPasswordList, user.Password)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hysteriaService.UpdateUsers(userList, userPasswordList)
|
hysteriaService.UpdateUsers(userList, userPasswordList)
|
||||||
inbound.service = hysteriaService
|
inbound.service = hysteriaService
|
||||||
inbound.userNameList = userNameList
|
inbound.userNameList = userNameList
|
||||||
|
inbound.userManager = userManager
|
||||||
return inbound, nil
|
return inbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,6 +274,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if h.userManager != nil {
|
||||||
|
go h.credentialLoop()
|
||||||
|
}
|
||||||
packetConn, err := h.listener.ListenUDP()
|
packetConn, err := h.listener.ListenUDP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -258,6 +284,27 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return h.service.Start(packetConn)
|
return h.service.Start(packetConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) credentialLoop() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
creds, err := h.userManager.GetCredentials(C.TypeHysteria2)
|
||||||
|
if err != nil || len(creds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userList := make([]int, len(creds))
|
||||||
|
userNameList := make([]string, len(creds))
|
||||||
|
userPasswordList := make([]string, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
userList[i] = i
|
||||||
|
userNameList[i] = c.Username
|
||||||
|
userPasswordList[i] = c.Credential
|
||||||
|
}
|
||||||
|
h.service.UpdateUsers(userList, userPasswordList)
|
||||||
|
h.userNameList = userNameList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Inbound) InterfaceUpdated() {
|
func (h *Inbound) InterfaceUpdated() {
|
||||||
h.service.Reset()
|
h.service.Reset()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"github.com/sagernet/sing/protocol/socks"
|
"github.com/sagernet/sing/protocol/socks"
|
||||||
"github.com/sagernet/sing/protocol/socks/socks4"
|
"github.com/sagernet/sing/protocol/socks/socks4"
|
||||||
"github.com/sagernet/sing/protocol/socks/socks5"
|
"github.com/sagernet/sing/protocol/socks/socks5"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterInbound(registry *inbound.Registry) {
|
func RegisterInbound(registry *inbound.Registry) {
|
||||||
|
|
@ -38,6 +39,7 @@ type Inbound struct {
|
||||||
authenticator *auth.Authenticator
|
authenticator *auth.Authenticator
|
||||||
tlsConfig tls.ServerConfig
|
tlsConfig tls.ServerConfig
|
||||||
udpTimeout time.Duration
|
udpTimeout time.Duration
|
||||||
|
userManager adapter.UserManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) {
|
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) {
|
||||||
|
|
@ -47,12 +49,25 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||||
} else {
|
} else {
|
||||||
udpTimeout = C.UDPTimeout
|
udpTimeout = C.UDPTimeout
|
||||||
}
|
}
|
||||||
|
authenticator := auth.NewAuthenticator(options.Users)
|
||||||
|
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||||
|
if userManager != nil {
|
||||||
|
creds, err := userManager.GetCredentials(C.TypeMixed)
|
||||||
|
if err == nil && len(creds) > 0 {
|
||||||
|
users := make([]auth.User, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||||
|
}
|
||||||
|
authenticator = auth.NewAuthenticator(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
inbound := &Inbound{
|
inbound := &Inbound{
|
||||||
Adapter: inbound.NewAdapter(C.TypeMixed, tag),
|
Adapter: inbound.NewAdapter(C.TypeMixed, tag),
|
||||||
router: uot.NewRouter(router, logger),
|
router: uot.NewRouter(router, logger),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
authenticator: auth.NewAuthenticator(options.Users),
|
authenticator: authenticator,
|
||||||
udpTimeout: udpTimeout,
|
udpTimeout: udpTimeout,
|
||||||
|
userManager: userManager,
|
||||||
}
|
}
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{
|
tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{
|
||||||
|
|
@ -88,9 +103,28 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return E.Cause(err, "create TLS config")
|
return E.Cause(err, "create TLS config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if h.userManager != nil {
|
||||||
|
go h.credentialLoop()
|
||||||
|
}
|
||||||
return h.listener.Start()
|
return h.listener.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) credentialLoop() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
creds, err := h.userManager.GetCredentials(C.TypeMixed)
|
||||||
|
if err != nil || len(creds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
users := make([]auth.User, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||||
|
}
|
||||||
|
h.authenticator = auth.NewAuthenticator(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Inbound) Close() error {
|
func (h *Inbound) Close() error {
|
||||||
return common.Close(
|
return common.Close(
|
||||||
h.listener,
|
h.listener,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
|
|
@ -24,6 +25,7 @@ import (
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
aTLS "github.com/sagernet/sing/common/tls"
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
sHttp "github.com/sagernet/sing/protocol/http"
|
sHttp "github.com/sagernet/sing/protocol/http"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
|
|
@ -51,9 +53,22 @@ type Inbound struct {
|
||||||
tlsConfig tls.ServerConfig
|
tlsConfig tls.ServerConfig
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
h3Server io.Closer
|
h3Server io.Closer
|
||||||
|
userManager adapter.UserManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (adapter.Inbound, error) {
|
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (adapter.Inbound, error) {
|
||||||
|
authenticator := auth.NewAuthenticator(options.Users)
|
||||||
|
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||||
|
if userManager != nil {
|
||||||
|
creds, err := userManager.GetCredentials(C.TypeNaive)
|
||||||
|
if err == nil && len(creds) > 0 {
|
||||||
|
users := make([]auth.User, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||||
|
}
|
||||||
|
authenticator = auth.NewAuthenticator(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
inbound := &Inbound{
|
inbound := &Inbound{
|
||||||
Adapter: inbound.NewAdapter(C.TypeNaive, tag),
|
Adapter: inbound.NewAdapter(C.TypeNaive, tag),
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
|
@ -66,14 +81,15 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||||
}),
|
}),
|
||||||
networkIsDefault: options.Network == "",
|
networkIsDefault: options.Network == "",
|
||||||
network: options.Network.Build(),
|
network: options.Network.Build(),
|
||||||
authenticator: auth.NewAuthenticator(options.Users),
|
authenticator: authenticator,
|
||||||
|
userManager: userManager,
|
||||||
}
|
}
|
||||||
if common.Contains(inbound.network, N.NetworkUDP) {
|
if common.Contains(inbound.network, N.NetworkUDP) {
|
||||||
if options.TLS == nil || !options.TLS.Enabled {
|
if options.TLS == nil || !options.TLS.Enabled {
|
||||||
return nil, E.New("TLS is required for QUIC server")
|
return nil, E.New("TLS is required for QUIC server")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(options.Users) == 0 {
|
if len(options.Users) == 0 && userManager == nil {
|
||||||
return nil, E.New("missing users")
|
return nil, E.New("missing users")
|
||||||
}
|
}
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
|
|
@ -96,6 +112,9 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return E.Cause(err, "create TLS config")
|
return E.Cause(err, "create TLS config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if n.userManager != nil {
|
||||||
|
go n.credentialLoop()
|
||||||
|
}
|
||||||
if common.Contains(n.network, N.NetworkTCP) {
|
if common.Contains(n.network, N.NetworkTCP) {
|
||||||
tcpListener, err := n.listener.ListenTCP()
|
tcpListener, err := n.listener.ListenTCP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -138,6 +157,22 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Inbound) credentialLoop() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
creds, err := n.userManager.GetCredentials(C.TypeNaive)
|
||||||
|
if err != nil || len(creds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
users := make([]auth.User, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||||
|
}
|
||||||
|
n.authenticator = auth.NewAuthenticator(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (n *Inbound) Close() error {
|
func (n *Inbound) Close() error {
|
||||||
return common.Close(
|
return common.Close(
|
||||||
n.listener,
|
n.listener,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import (
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterInbound(registry *inbound.Registry) {
|
func RegisterInbound(registry *inbound.Registry) {
|
||||||
|
|
@ -35,7 +36,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||||
} else if options.Managed && (len(options.Users) > 0 || len(options.Destinations) > 0) {
|
} else if options.Managed && (len(options.Users) > 0 || len(options.Destinations) > 0) {
|
||||||
return nil, E.New("users and destinations options are not supported in managed servers")
|
return nil, E.New("users and destinations options are not supported in managed servers")
|
||||||
}
|
}
|
||||||
if len(options.Users) > 0 || options.Managed {
|
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||||
|
if len(options.Users) > 0 || options.Managed || userManager != nil {
|
||||||
return newMultiInbound(ctx, router, logger, tag, options)
|
return newMultiInbound(ctx, router, logger, tag, options)
|
||||||
} else if len(options.Destinations) > 0 {
|
} else if len(options.Destinations) > 0 {
|
||||||
return newRelayInbound(ctx, router, logger, tag, options)
|
return newRelayInbound(ctx, router, logger, tag, options)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
sService "github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -35,13 +36,15 @@ var (
|
||||||
|
|
||||||
type MultiInbound struct {
|
type MultiInbound struct {
|
||||||
inbound.Adapter
|
inbound.Adapter
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
router adapter.ConnectionRouterEx
|
router adapter.ConnectionRouterEx
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
listener *listener.Listener
|
listener *listener.Listener
|
||||||
service shadowsocks.MultiService[int]
|
service shadowsocks.MultiService[int]
|
||||||
users []option.ShadowsocksUser
|
users []option.ShadowsocksUser
|
||||||
tracker adapter.SSMTracker
|
userNameList []string
|
||||||
|
userManager adapter.UserManager
|
||||||
|
tracker adapter.SSMTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMultiInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*MultiInbound, error) {
|
func newMultiInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*MultiInbound, error) {
|
||||||
|
|
@ -83,18 +86,35 @@ func newMultiInbound(ctx context.Context, router adapter.Router, logger log.Cont
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(options.Users) > 0 {
|
userManager := sService.FromContext[adapter.UserManager](ctx)
|
||||||
err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Users, func(index int, user option.ShadowsocksUser) int {
|
users := options.Users
|
||||||
|
if len(users) == 0 && userManager != nil {
|
||||||
|
creds, uErr := userManager.GetCredentials(C.TypeShadowsocks)
|
||||||
|
if uErr == nil && len(creds) > 0 {
|
||||||
|
users = make([]option.ShadowsocksUser, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
users[i] = option.ShadowsocksUser{Name: c.Username, Password: c.Credential}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(users) > 0 {
|
||||||
|
err = service.UpdateUsersWithPasswords(common.MapIndexed(users, func(index int, user option.ShadowsocksUser) int {
|
||||||
return index
|
return index
|
||||||
}), common.Map(options.Users, func(user option.ShadowsocksUser) string {
|
}), common.Map(users, func(user option.ShadowsocksUser) string {
|
||||||
return user.Password
|
return user.Password
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
userNameList := make([]string, len(users))
|
||||||
|
for i, u := range users {
|
||||||
|
userNameList[i] = u.Name
|
||||||
|
}
|
||||||
inbound.service = service
|
inbound.service = service
|
||||||
inbound.users = options.Users
|
inbound.users = users
|
||||||
|
inbound.userNameList = userNameList
|
||||||
|
inbound.userManager = userManager
|
||||||
inbound.listener = listener.New(listener.Options{
|
inbound.listener = listener.New(listener.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
|
|
@ -111,9 +131,33 @@ func (h *MultiInbound) Start(stage adapter.StartStage) error {
|
||||||
if stage != adapter.StartStateStart {
|
if stage != adapter.StartStateStart {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if h.userManager != nil {
|
||||||
|
go h.credentialLoop()
|
||||||
|
}
|
||||||
return h.listener.Start()
|
return h.listener.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *MultiInbound) credentialLoop() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
creds, err := h.userManager.GetCredentials(C.TypeShadowsocks)
|
||||||
|
if err != nil || len(creds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userIndices := make([]int, len(creds))
|
||||||
|
userPasswords := make([]string, len(creds))
|
||||||
|
userNameList := make([]string, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
userIndices[i] = i
|
||||||
|
userPasswords[i] = c.Credential
|
||||||
|
userNameList[i] = c.Username
|
||||||
|
}
|
||||||
|
h.service.UpdateUsersWithPasswords(userIndices, userPasswords)
|
||||||
|
h.userNameList = userNameList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *MultiInbound) Close() error {
|
func (h *MultiInbound) Close() error {
|
||||||
return h.listener.Close()
|
return h.listener.Close()
|
||||||
}
|
}
|
||||||
|
|
@ -163,7 +207,7 @@ func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadat
|
||||||
if !loaded {
|
if !loaded {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
user := h.users[userIndex].Name
|
user := h.userNameList[userIndex]
|
||||||
if user == "" {
|
if user == "" {
|
||||||
user = F.ToString(userIndex)
|
user = F.ToString(userIndex)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -186,7 +230,7 @@ func (h *MultiInbound) newPacketConnection(ctx context.Context, conn N.PacketCon
|
||||||
if !loaded {
|
if !loaded {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
user := h.users[userIndex].Name
|
user := h.userNameList[userIndex]
|
||||||
if user == "" {
|
if user == "" {
|
||||||
user = F.ToString(userIndex)
|
user = F.ToString(userIndex)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/protocol/socks"
|
"github.com/sagernet/sing/protocol/socks"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterInbound(registry *inbound.Registry) {
|
func RegisterInbound(registry *inbound.Registry) {
|
||||||
|
|
@ -33,6 +34,7 @@ type Inbound struct {
|
||||||
listener *listener.Listener
|
listener *listener.Listener
|
||||||
authenticator *auth.Authenticator
|
authenticator *auth.Authenticator
|
||||||
udpTimeout time.Duration
|
udpTimeout time.Duration
|
||||||
|
userManager adapter.UserManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SocksInboundOptions) (adapter.Inbound, error) {
|
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SocksInboundOptions) (adapter.Inbound, error) {
|
||||||
|
|
@ -42,12 +44,25 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||||
} else {
|
} else {
|
||||||
udpTimeout = C.UDPTimeout
|
udpTimeout = C.UDPTimeout
|
||||||
}
|
}
|
||||||
|
authenticator := auth.NewAuthenticator(options.Users)
|
||||||
|
userManager := service.FromContext[adapter.UserManager](ctx)
|
||||||
|
if userManager != nil {
|
||||||
|
creds, err := userManager.GetCredentials(C.TypeSOCKS)
|
||||||
|
if err == nil && len(creds) > 0 {
|
||||||
|
users := make([]auth.User, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||||
|
}
|
||||||
|
authenticator = auth.NewAuthenticator(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
inbound := &Inbound{
|
inbound := &Inbound{
|
||||||
Adapter: inbound.NewAdapter(C.TypeSOCKS, tag),
|
Adapter: inbound.NewAdapter(C.TypeSOCKS, tag),
|
||||||
router: uot.NewRouter(router, logger),
|
router: uot.NewRouter(router, logger),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
authenticator: auth.NewAuthenticator(options.Users),
|
authenticator: authenticator,
|
||||||
udpTimeout: udpTimeout,
|
udpTimeout: udpTimeout,
|
||||||
|
userManager: userManager,
|
||||||
}
|
}
|
||||||
inbound.listener = listener.New(listener.Options{
|
inbound.listener = listener.New(listener.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
|
|
@ -63,9 +78,28 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
if stage != adapter.StartStateStart {
|
if stage != adapter.StartStateStart {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if h.userManager != nil {
|
||||||
|
go h.credentialLoop()
|
||||||
|
}
|
||||||
return h.listener.Start()
|
return h.listener.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) credentialLoop() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
creds, err := h.userManager.GetCredentials(C.TypeSOCKS)
|
||||||
|
if err != nil || len(creds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
users := make([]auth.User, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
users[i] = auth.User{Username: c.Username, Password: c.Credential}
|
||||||
|
}
|
||||||
|
h.authenticator = auth.NewAuthenticator(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Inbound) Close() error {
|
func (h *Inbound) Close() error {
|
||||||
return h.listener.Close()
|
return h.listener.Close()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
|
|
@ -21,6 +22,7 @@ import (
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
sService "github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterInbound(registry *inbound.Registry) {
|
func RegisterInbound(registry *inbound.Registry) {
|
||||||
|
|
@ -36,6 +38,8 @@ type Inbound struct {
|
||||||
listener *listener.Listener
|
listener *listener.Listener
|
||||||
service *trojan.Service[int]
|
service *trojan.Service[int]
|
||||||
users []option.TrojanUser
|
users []option.TrojanUser
|
||||||
|
userNameList []string
|
||||||
|
userManager adapter.UserManager
|
||||||
tlsConfig tls.ServerConfig
|
tlsConfig tls.ServerConfig
|
||||||
fallbackAddr M.Socksaddr
|
fallbackAddr M.Socksaddr
|
||||||
fallbackAddrTLSNextProto map[string]M.Socksaddr
|
fallbackAddrTLSNextProto map[string]M.Socksaddr
|
||||||
|
|
@ -87,13 +91,40 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||||
fallbackHandler = adapter.NewUpstreamContextHandler(inbound.fallbackConnection, nil)
|
fallbackHandler = adapter.NewUpstreamContextHandler(inbound.fallbackConnection, nil)
|
||||||
}
|
}
|
||||||
service := trojan.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection), fallbackHandler, logger)
|
service := trojan.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection), fallbackHandler, logger)
|
||||||
err := service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.TrojanUser) int {
|
var err error
|
||||||
return index
|
userManager := sService.FromContext[adapter.UserManager](ctx)
|
||||||
}), common.Map(options.Users, func(it option.TrojanUser) string {
|
|
||||||
return it.Password
|
var userNameList []string
|
||||||
}))
|
if userManager != nil {
|
||||||
if err != nil {
|
creds, uErr := userManager.GetCredentials(C.TypeTrojan)
|
||||||
return nil, err
|
if uErr == nil && len(creds) > 0 {
|
||||||
|
userIndices := make([]int, len(creds))
|
||||||
|
userPasswords := make([]string, len(creds))
|
||||||
|
userNameList = make([]string, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
userIndices[i] = i
|
||||||
|
userPasswords[i] = c.Credential
|
||||||
|
userNameList[i] = c.Username
|
||||||
|
}
|
||||||
|
uErr = service.UpdateUsers(userIndices, userPasswords)
|
||||||
|
if uErr != nil {
|
||||||
|
return nil, uErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inbound.userManager = userManager
|
||||||
|
} else {
|
||||||
|
userNameList = make([]string, len(options.Users))
|
||||||
|
for i, u := range options.Users {
|
||||||
|
userNameList[i] = u.Name
|
||||||
|
}
|
||||||
|
err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.TrojanUser) int {
|
||||||
|
return index
|
||||||
|
}), common.Map(options.Users, func(it option.TrojanUser) string {
|
||||||
|
return it.Password
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if options.Transport != nil {
|
if options.Transport != nil {
|
||||||
inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound))
|
inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound))
|
||||||
|
|
@ -106,6 +137,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
inbound.service = service
|
inbound.service = service
|
||||||
|
inbound.userNameList = userNameList
|
||||||
inbound.listener = listener.New(listener.Options{
|
inbound.listener = listener.New(listener.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
|
|
@ -126,6 +158,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return E.Cause(err, "create TLS config")
|
return E.Cause(err, "create TLS config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if h.userManager != nil {
|
||||||
|
go h.credentialLoop()
|
||||||
|
}
|
||||||
if h.transport == nil {
|
if h.transport == nil {
|
||||||
return h.listener.Start()
|
return h.listener.Start()
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +191,27 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) credentialLoop() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
creds, err := h.userManager.GetCredentials(C.TypeTrojan)
|
||||||
|
if err != nil || len(creds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userIndices := make([]int, len(creds))
|
||||||
|
userPasswords := make([]string, len(creds))
|
||||||
|
userNameList := make([]string, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
userIndices[i] = i
|
||||||
|
userPasswords[i] = c.Credential
|
||||||
|
userNameList[i] = c.Username
|
||||||
|
}
|
||||||
|
h.service.UpdateUsers(userIndices, userPasswords)
|
||||||
|
h.userNameList = userNameList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Inbound) Close() error {
|
func (h *Inbound) Close() error {
|
||||||
return common.Close(
|
return common.Close(
|
||||||
h.listener,
|
h.listener,
|
||||||
|
|
@ -189,7 +245,7 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada
|
||||||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := h.users[userIndex].Name
|
user := h.userNameList[userIndex]
|
||||||
if user == "" {
|
if user == "" {
|
||||||
user = F.ToString(userIndex)
|
user = F.ToString(userIndex)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -207,7 +263,7 @@ func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, me
|
||||||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := h.users[userIndex].Name
|
user := h.userNameList[userIndex]
|
||||||
if user == "" {
|
if user == "" {
|
||||||
user = F.ToString(userIndex)
|
user = F.ToString(userIndex)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package tuic
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
|
@ -20,6 +21,7 @@ import (
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
sService "github.com/sagernet/sing/service"
|
||||||
|
|
||||||
"github.com/gofrs/uuid/v5"
|
"github.com/gofrs/uuid/v5"
|
||||||
)
|
)
|
||||||
|
|
@ -36,6 +38,7 @@ type Inbound struct {
|
||||||
tlsConfig tls.ServerConfig
|
tlsConfig tls.ServerConfig
|
||||||
server *tuic.Service[int]
|
server *tuic.Service[int]
|
||||||
userNameList []string
|
userNameList []string
|
||||||
|
userManager adapter.UserManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) {
|
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) {
|
||||||
|
|
@ -87,26 +90,61 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var userList []int
|
userManager := sService.FromContext[adapter.UserManager](ctx)
|
||||||
var userNameList []string
|
|
||||||
var userUUIDList [][16]byte
|
if userManager != nil {
|
||||||
var userPasswordList []string
|
creds, err := userManager.GetCredentials(C.TypeTUIC)
|
||||||
for index, user := range options.Users {
|
if err == nil && len(creds) > 0 {
|
||||||
if user.UUID == "" {
|
userList := make([]int, 0, len(creds))
|
||||||
return nil, E.New("missing uuid for user ", index)
|
userNameList := make([]string, 0, len(creds))
|
||||||
|
userUUIDList := make([][16]byte, 0, len(creds))
|
||||||
|
userPasswordList := make([]string, 0, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
parts := strings.SplitN(c.Credential, ":", 2)
|
||||||
|
if len(parts) < 1 || parts[0] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userUUID, pErr := uuid.FromString(parts[0])
|
||||||
|
if pErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
password := ""
|
||||||
|
if len(parts) == 2 {
|
||||||
|
password = parts[1]
|
||||||
|
}
|
||||||
|
userList = append(userList, i)
|
||||||
|
userNameList = append(userNameList, c.Username)
|
||||||
|
userUUIDList = append(userUUIDList, userUUID)
|
||||||
|
userPasswordList = append(userPasswordList, password)
|
||||||
|
}
|
||||||
|
if len(userList) > 0 {
|
||||||
|
service.UpdateUsers(userList, userUUIDList, userPasswordList)
|
||||||
|
inbound.userNameList = userNameList
|
||||||
|
}
|
||||||
}
|
}
|
||||||
userUUID, err := uuid.FromString(user.UUID)
|
inbound.userManager = userManager
|
||||||
if err != nil {
|
} else {
|
||||||
return nil, E.Cause(err, "invalid uuid for user ", index)
|
userList := make([]int, 0, len(options.Users))
|
||||||
|
userNameList := make([]string, 0, len(options.Users))
|
||||||
|
userUUIDList := make([][16]byte, 0, len(options.Users))
|
||||||
|
userPasswordList := make([]string, 0, len(options.Users))
|
||||||
|
for index, user := range options.Users {
|
||||||
|
if user.UUID == "" {
|
||||||
|
return nil, E.New("missing uuid for user ", index)
|
||||||
|
}
|
||||||
|
userUUID, err := uuid.FromString(user.UUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "invalid uuid for user ", index)
|
||||||
|
}
|
||||||
|
userList = append(userList, index)
|
||||||
|
userNameList = append(userNameList, user.Name)
|
||||||
|
userUUIDList = append(userUUIDList, userUUID)
|
||||||
|
userPasswordList = append(userPasswordList, user.Password)
|
||||||
}
|
}
|
||||||
userList = append(userList, index)
|
service.UpdateUsers(userList, userUUIDList, userPasswordList)
|
||||||
userNameList = append(userNameList, user.Name)
|
inbound.userNameList = userNameList
|
||||||
userUUIDList = append(userUUIDList, userUUID)
|
|
||||||
userPasswordList = append(userPasswordList, user.Password)
|
|
||||||
}
|
}
|
||||||
service.UpdateUsers(userList, userUUIDList, userPasswordList)
|
|
||||||
inbound.server = service
|
inbound.server = service
|
||||||
inbound.userNameList = userNameList
|
|
||||||
return inbound, nil
|
return inbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,6 +202,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if h.userManager != nil {
|
||||||
|
go h.credentialLoop()
|
||||||
|
}
|
||||||
packetConn, err := h.listener.ListenUDP()
|
packetConn, err := h.listener.ListenUDP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -171,6 +212,43 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return h.server.Start(packetConn)
|
return h.server.Start(packetConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) credentialLoop() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
creds, err := h.userManager.GetCredentials(C.TypeTUIC)
|
||||||
|
if err != nil || len(creds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userList := make([]int, 0, len(creds))
|
||||||
|
userNameList := make([]string, 0, len(creds))
|
||||||
|
userUUIDList := make([][16]byte, 0, len(creds))
|
||||||
|
userPasswordList := make([]string, 0, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
parts := strings.SplitN(c.Credential, ":", 2)
|
||||||
|
if len(parts) < 1 || parts[0] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userUUID, pErr := uuid.FromString(parts[0])
|
||||||
|
if pErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
password := ""
|
||||||
|
if len(parts) == 2 {
|
||||||
|
password = parts[1]
|
||||||
|
}
|
||||||
|
userList = append(userList, i)
|
||||||
|
userNameList = append(userNameList, c.Username)
|
||||||
|
userUUIDList = append(userUUIDList, userUUID)
|
||||||
|
userPasswordList = append(userPasswordList, password)
|
||||||
|
}
|
||||||
|
if len(userList) > 0 {
|
||||||
|
h.server.UpdateUsers(userList, userUUIDList, userPasswordList)
|
||||||
|
h.userNameList = userNameList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Inbound) Close() error {
|
func (h *Inbound) Close() error {
|
||||||
return common.Close(
|
return common.Close(
|
||||||
h.listener,
|
h.listener,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
|
|
@ -25,6 +26,7 @@ import (
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
sService "github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterInbound(registry *inbound.Registry) {
|
func RegisterInbound(registry *inbound.Registry) {
|
||||||
|
|
@ -35,14 +37,16 @@ var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
|
||||||
|
|
||||||
type Inbound struct {
|
type Inbound struct {
|
||||||
inbound.Adapter
|
inbound.Adapter
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
router adapter.ConnectionRouterEx
|
router adapter.ConnectionRouterEx
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
listener *listener.Listener
|
listener *listener.Listener
|
||||||
users []option.VLESSUser
|
users []option.VLESSUser
|
||||||
service *vless.Service[int]
|
userNameList []string
|
||||||
tlsConfig tls.ServerConfig
|
userManager adapter.UserManager
|
||||||
transport adapter.V2RayServerTransport
|
service *vless.Service[int]
|
||||||
|
tlsConfig tls.ServerConfig
|
||||||
|
transport adapter.V2RayServerTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (adapter.Inbound, error) {
|
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (adapter.Inbound, error) {
|
||||||
|
|
@ -59,14 +63,40 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
service := vless.NewService[int](logger, adapter.NewUpstreamContextHandler(inbound.newConnectionEx, inbound.newPacketConnectionEx))
|
service := vless.NewService[int](logger, adapter.NewUpstreamContextHandler(inbound.newConnectionEx, inbound.newPacketConnectionEx))
|
||||||
service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int {
|
userManager := sService.FromContext[adapter.UserManager](ctx)
|
||||||
return index
|
|
||||||
}), common.Map(inbound.users, func(it option.VLESSUser) string {
|
var userNameList []string
|
||||||
return it.UUID
|
if userManager != nil {
|
||||||
}), common.Map(inbound.users, func(it option.VLESSUser) string {
|
creds, err := userManager.GetCredentials(C.TypeVLESS)
|
||||||
return it.Flow
|
if err == nil && len(creds) > 0 {
|
||||||
}))
|
userIndices := make([]int, len(creds))
|
||||||
|
userUUIDs := make([]string, len(creds))
|
||||||
|
userFlows := make([]string, len(creds))
|
||||||
|
userNameList = make([]string, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
userIndices[i] = i
|
||||||
|
userUUIDs[i] = c.Credential
|
||||||
|
userFlows[i] = ""
|
||||||
|
userNameList[i] = c.Username
|
||||||
|
}
|
||||||
|
service.UpdateUsers(userIndices, userUUIDs, userFlows)
|
||||||
|
}
|
||||||
|
inbound.userManager = userManager
|
||||||
|
} else {
|
||||||
|
userNameList = make([]string, len(inbound.users))
|
||||||
|
for i, u := range inbound.users {
|
||||||
|
userNameList[i] = u.Name
|
||||||
|
}
|
||||||
|
service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int {
|
||||||
|
return index
|
||||||
|
}), common.Map(inbound.users, func(it option.VLESSUser) string {
|
||||||
|
return it.UUID
|
||||||
|
}), common.Map(inbound.users, func(it option.VLESSUser) string {
|
||||||
|
return it.Flow
|
||||||
|
}))
|
||||||
|
}
|
||||||
inbound.service = service
|
inbound.service = service
|
||||||
|
inbound.userNameList = userNameList
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
inbound.tlsConfig, err = tls.NewServerWithOptions(tls.ServerOptions{
|
inbound.tlsConfig, err = tls.NewServerWithOptions(tls.ServerOptions{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
|
|
@ -108,6 +138,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if h.userManager != nil {
|
||||||
|
go h.credentialLoop()
|
||||||
|
}
|
||||||
if h.transport == nil {
|
if h.transport == nil {
|
||||||
return h.listener.Start()
|
return h.listener.Start()
|
||||||
}
|
}
|
||||||
|
|
@ -138,6 +171,29 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) credentialLoop() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
creds, err := h.userManager.GetCredentials(C.TypeVLESS)
|
||||||
|
if err != nil || len(creds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userIndices := make([]int, len(creds))
|
||||||
|
userUUIDs := make([]string, len(creds))
|
||||||
|
userFlows := make([]string, len(creds))
|
||||||
|
userNameList := make([]string, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
userIndices[i] = i
|
||||||
|
userUUIDs[i] = c.Credential
|
||||||
|
userFlows[i] = ""
|
||||||
|
userNameList[i] = c.Username
|
||||||
|
}
|
||||||
|
h.service.UpdateUsers(userIndices, userUUIDs, userFlows)
|
||||||
|
h.userNameList = userNameList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Inbound) Close() error {
|
func (h *Inbound) Close() error {
|
||||||
return common.Close(
|
return common.Close(
|
||||||
h.service,
|
h.service,
|
||||||
|
|
@ -172,7 +228,7 @@ func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata a
|
||||||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := h.users[userIndex].Name
|
user := h.userNameList[userIndex]
|
||||||
if user == "" {
|
if user == "" {
|
||||||
user = F.ToString(userIndex)
|
user = F.ToString(userIndex)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -190,7 +246,7 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
||||||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := h.users[userIndex].Name
|
user := h.userNameList[userIndex]
|
||||||
if user == "" {
|
if user == "" {
|
||||||
user = F.ToString(userIndex)
|
user = F.ToString(userIndex)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
|
|
@ -26,6 +27,7 @@ import (
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
sService "github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterInbound(registry *inbound.Registry) {
|
func RegisterInbound(registry *inbound.Registry) {
|
||||||
|
|
@ -36,14 +38,16 @@ var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
|
||||||
|
|
||||||
type Inbound struct {
|
type Inbound struct {
|
||||||
inbound.Adapter
|
inbound.Adapter
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
router adapter.ConnectionRouterEx
|
router adapter.ConnectionRouterEx
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
listener *listener.Listener
|
listener *listener.Listener
|
||||||
service *vmess.Service[int]
|
service *vmess.Service[int]
|
||||||
users []option.VMessUser
|
users []option.VMessUser
|
||||||
tlsConfig tls.ServerConfig
|
userNameList []string
|
||||||
transport adapter.V2RayServerTransport
|
userManager adapter.UserManager
|
||||||
|
tlsConfig tls.ServerConfig
|
||||||
|
transport adapter.V2RayServerTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessInboundOptions) (adapter.Inbound, error) {
|
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessInboundOptions) (adapter.Inbound, error) {
|
||||||
|
|
@ -67,17 +71,46 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||||
serviceOptions = append(serviceOptions, vmess.ServiceWithDisableHeaderProtection())
|
serviceOptions = append(serviceOptions, vmess.ServiceWithDisableHeaderProtection())
|
||||||
}
|
}
|
||||||
service := vmess.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnectionEx, inbound.newPacketConnectionEx), serviceOptions...)
|
service := vmess.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnectionEx, inbound.newPacketConnectionEx), serviceOptions...)
|
||||||
inbound.service = service
|
userManager := sService.FromContext[adapter.UserManager](ctx)
|
||||||
err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int {
|
|
||||||
return index
|
var userNameList []string
|
||||||
}), common.Map(options.Users, func(it option.VMessUser) string {
|
if userManager != nil {
|
||||||
return it.UUID
|
creds, uErr := userManager.GetCredentials(C.TypeVMess)
|
||||||
}), common.Map(options.Users, func(it option.VMessUser) int {
|
if uErr == nil && len(creds) > 0 {
|
||||||
return it.AlterId
|
userIndices := make([]int, len(creds))
|
||||||
}))
|
userUUIDs := make([]string, len(creds))
|
||||||
if err != nil {
|
userAlterIds := make([]int, len(creds))
|
||||||
return nil, err
|
userNameList = make([]string, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
userIndices[i] = i
|
||||||
|
userUUIDs[i] = c.Credential
|
||||||
|
userAlterIds[i] = 0
|
||||||
|
userNameList[i] = c.Username
|
||||||
|
}
|
||||||
|
uErr = service.UpdateUsers(userIndices, userUUIDs, userAlterIds)
|
||||||
|
if uErr != nil {
|
||||||
|
return nil, uErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inbound.userManager = userManager
|
||||||
|
} else {
|
||||||
|
userNameList = make([]string, len(options.Users))
|
||||||
|
for i, u := range options.Users {
|
||||||
|
userNameList[i] = u.Name
|
||||||
|
}
|
||||||
|
err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int {
|
||||||
|
return index
|
||||||
|
}), common.Map(options.Users, func(it option.VMessUser) string {
|
||||||
|
return it.UUID
|
||||||
|
}), common.Map(options.Users, func(it option.VMessUser) int {
|
||||||
|
return it.AlterId
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
inbound.service = service
|
||||||
|
inbound.userNameList = userNameList
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
inbound.tlsConfig, err = tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
inbound.tlsConfig, err = tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -114,6 +147,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if h.userManager != nil {
|
||||||
|
go h.credentialLoop()
|
||||||
|
}
|
||||||
if h.transport == nil {
|
if h.transport == nil {
|
||||||
return h.listener.Start()
|
return h.listener.Start()
|
||||||
}
|
}
|
||||||
|
|
@ -144,6 +180,29 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) credentialLoop() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
creds, err := h.userManager.GetCredentials(C.TypeVMess)
|
||||||
|
if err != nil || len(creds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userIndices := make([]int, len(creds))
|
||||||
|
userUUIDs := make([]string, len(creds))
|
||||||
|
userAlterIds := make([]int, len(creds))
|
||||||
|
userNameList := make([]string, len(creds))
|
||||||
|
for i, c := range creds {
|
||||||
|
userIndices[i] = i
|
||||||
|
userUUIDs[i] = c.Credential
|
||||||
|
userAlterIds[i] = 0
|
||||||
|
userNameList[i] = c.Username
|
||||||
|
}
|
||||||
|
h.service.UpdateUsers(userIndices, userUUIDs, userAlterIds)
|
||||||
|
h.userNameList = userNameList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Inbound) Close() error {
|
func (h *Inbound) Close() error {
|
||||||
return common.Close(
|
return common.Close(
|
||||||
h.service,
|
h.service,
|
||||||
|
|
@ -178,7 +237,7 @@ func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata a
|
||||||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := h.users[userIndex].Name
|
user := h.userNameList[userIndex]
|
||||||
if user == "" {
|
if user == "" {
|
||||||
user = F.ToString(userIndex)
|
user = F.ToString(userIndex)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -196,7 +255,7 @@ func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
||||||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := h.users[userIndex].Name
|
user := h.userNameList[userIndex]
|
||||||
if user == "" {
|
if user == "" {
|
||||||
user = F.ToString(userIndex)
|
user = F.ToString(userIndex)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
662
service/usermanager/manager.go
Normal file
662
service/usermanager/manager.go
Normal file
|
|
@ -0,0 +1,662 @@
|
||||||
|
package usermanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.UserManager = (*Manager)(nil)
|
||||||
|
var _ adapter.ConnectionTracker = (*Manager)(nil)
|
||||||
|
|
||||||
|
type authCacheEntry struct {
|
||||||
|
username string
|
||||||
|
expiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type userConnCounter struct {
|
||||||
|
username string
|
||||||
|
tx int64
|
||||||
|
rx int64
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
boxService.Adapter
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
logger log.ContextLogger
|
||||||
|
|
||||||
|
authServer string
|
||||||
|
timeout time.Duration
|
||||||
|
cacheTTL time.Duration
|
||||||
|
refreshInt time.Duration
|
||||||
|
reportInt time.Duration
|
||||||
|
reportTraffic bool
|
||||||
|
apiSecret string
|
||||||
|
apiListen string
|
||||||
|
|
||||||
|
httpClient *http.Client
|
||||||
|
|
||||||
|
access sync.RWMutex
|
||||||
|
authCache map[string]*authCacheEntry
|
||||||
|
userCreds map[string]string
|
||||||
|
userConns map[string]map[*userConnCounter]struct{}
|
||||||
|
|
||||||
|
protocolUsers map[string][]adapter.UserInfo
|
||||||
|
protocolLastSeq map[string]int64
|
||||||
|
|
||||||
|
trafficAccess sync.Mutex
|
||||||
|
traffic map[string]*UserTrafficAtomic
|
||||||
|
|
||||||
|
stopRefresh chan struct{}
|
||||||
|
stopReport chan struct{}
|
||||||
|
stopAPI chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserTrafficAtomic struct {
|
||||||
|
Tx atomic.Int64
|
||||||
|
Rx atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, logger log.ContextLogger, tag string, options option.UserManagerOptions) (*Manager, error) {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
m := &Manager{
|
||||||
|
Adapter: boxService.NewAdapter(C.TypeUserManager, tag),
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
logger: logger,
|
||||||
|
authServer: options.AuthServer,
|
||||||
|
timeout: time.Duration(options.Timeout),
|
||||||
|
cacheTTL: time.Duration(options.CacheTTL),
|
||||||
|
refreshInt: time.Duration(options.RefreshInterval),
|
||||||
|
reportInt: time.Duration(options.ReportInterval),
|
||||||
|
reportTraffic: options.ReportTraffic,
|
||||||
|
apiSecret: options.APISecret,
|
||||||
|
apiListen: options.APIListen,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: time.Duration(options.Timeout),
|
||||||
|
},
|
||||||
|
authCache: make(map[string]*authCacheEntry),
|
||||||
|
userCreds: make(map[string]string),
|
||||||
|
userConns: make(map[string]map[*userConnCounter]struct{}),
|
||||||
|
protocolUsers: make(map[string][]adapter.UserInfo),
|
||||||
|
protocolLastSeq: make(map[string]int64),
|
||||||
|
traffic: make(map[string]*UserTrafficAtomic),
|
||||||
|
stopRefresh: make(chan struct{}),
|
||||||
|
stopReport: make(chan struct{}),
|
||||||
|
stopAPI: make(chan struct{}),
|
||||||
|
}
|
||||||
|
if m.timeout <= 0 {
|
||||||
|
m.timeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
if m.cacheTTL <= 0 {
|
||||||
|
m.cacheTTL = 5 * time.Minute
|
||||||
|
}
|
||||||
|
if m.refreshInt <= 0 {
|
||||||
|
m.refreshInt = 30 * time.Second
|
||||||
|
}
|
||||||
|
if m.reportInt <= 0 {
|
||||||
|
m.reportInt = 60 * time.Second
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStateStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.logger.Info("starting user manager")
|
||||||
|
|
||||||
|
if err := m.refreshCredentials(); err != nil {
|
||||||
|
m.logger.Warn("initial credential fetch failed: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := service.FromContext[adapter.Router](m.ctx)
|
||||||
|
if router != nil {
|
||||||
|
router.AppendTracker(m)
|
||||||
|
m.logger.Debug("registered as connection tracker")
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(m.refreshInt)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := m.refreshCredentials(); err != nil {
|
||||||
|
m.logger.Warn("credential refresh failed: ", err)
|
||||||
|
}
|
||||||
|
case <-m.stopRefresh:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if m.reportTraffic && m.authServer != "" {
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(m.reportInt)
|
||||||
|
defer ticker.Stop()
|
||||||
|
select {
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
case <-m.stopReport:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
m.reportTrafficToServer()
|
||||||
|
case <-m.stopReport:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.apiListen != "" {
|
||||||
|
go m.serveAPI()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() error {
|
||||||
|
m.cancel()
|
||||||
|
close(m.stopRefresh)
|
||||||
|
close(m.stopReport)
|
||||||
|
close(m.stopAPI)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Authenticate(ctx context.Context, protocol string, credential string, addr string) (string, error) {
|
||||||
|
if credential == "" {
|
||||||
|
return "", E.New("empty credential")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.access.RLock()
|
||||||
|
entry, cached := m.authCache[credential]
|
||||||
|
m.access.RUnlock()
|
||||||
|
if cached && time.Now().Before(entry.expiresAt) {
|
||||||
|
return entry.username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
username, err := m.remoteAuth(protocol, credential, addr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.access.Lock()
|
||||||
|
m.authCache[credential] = &authCacheEntry{
|
||||||
|
username: username,
|
||||||
|
expiresAt: time.Now().Add(m.cacheTTL),
|
||||||
|
}
|
||||||
|
m.userCreds[username] = credential
|
||||||
|
m.access.Unlock()
|
||||||
|
|
||||||
|
return username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type authRequest struct {
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Credential string `json:"credential"`
|
||||||
|
Addr string `json:"addr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type authResponse struct {
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) remoteAuth(protocol string, credential string, addr string) (string, error) {
|
||||||
|
if m.authServer == "" {
|
||||||
|
return "", E.New("auth server not configured")
|
||||||
|
}
|
||||||
|
req := authRequest{
|
||||||
|
Protocol: protocol,
|
||||||
|
Credential: credential,
|
||||||
|
Addr: addr,
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", E.Cause(err, "marshal auth request")
|
||||||
|
}
|
||||||
|
httpReq, err := http.NewRequestWithContext(m.ctx, "POST", m.authServer+"/api/auth", bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return "", E.Cause(err, "create auth request")
|
||||||
|
}
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := m.httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return "", E.Cause(err, "auth request failed")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden {
|
||||||
|
return "", E.New("authentication rejected")
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", E.New("auth server returned ", resp.StatusCode)
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", E.Cause(err, "read auth response")
|
||||||
|
}
|
||||||
|
var authResp authResponse
|
||||||
|
if err := json.Unmarshal(body, &authResp); err != nil {
|
||||||
|
return "", E.Cause(err, "parse auth response")
|
||||||
|
}
|
||||||
|
if !authResp.Ok {
|
||||||
|
return "", E.New("authentication rejected")
|
||||||
|
}
|
||||||
|
if authResp.ID == "" {
|
||||||
|
return "", E.New("auth response missing user ID")
|
||||||
|
}
|
||||||
|
return authResp.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetCredentials(protocol string) ([]adapter.UserInfo, error) {
|
||||||
|
m.access.RLock()
|
||||||
|
defer m.access.RUnlock()
|
||||||
|
users := m.protocolUsers[protocol]
|
||||||
|
if users == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
result := make([]adapter.UserInfo, len(users))
|
||||||
|
copy(result, users)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type credentialsResponse struct {
|
||||||
|
Credentials []credentialEntry `json:"credentials"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type credentialEntry struct {
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Credential string `json:"credential"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) refreshCredentials() error {
|
||||||
|
if m.authServer == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
httpReq, err := http.NewRequestWithContext(m.ctx, "GET", m.authServer+"/api/credentials", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := m.httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "fetch credentials")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return E.New("credentials server returned ", resp.StatusCode)
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read credentials response")
|
||||||
|
}
|
||||||
|
var credResp credentialsResponse
|
||||||
|
if err := json.Unmarshal(body, &credResp); err != nil {
|
||||||
|
return E.Cause(err, "parse credentials response")
|
||||||
|
}
|
||||||
|
|
||||||
|
newCache := make(map[string]*authCacheEntry)
|
||||||
|
newUserCreds := make(map[string]string)
|
||||||
|
byProtocol := make(map[string][]adapter.UserInfo)
|
||||||
|
|
||||||
|
for _, c := range credResp.Credentials {
|
||||||
|
newCache[c.Credential] = &authCacheEntry{
|
||||||
|
username: c.Username,
|
||||||
|
expiresAt: time.Now().Add(m.cacheTTL),
|
||||||
|
}
|
||||||
|
newUserCreds[c.Username] = c.Credential
|
||||||
|
byProtocol[c.Protocol] = append(byProtocol[c.Protocol], adapter.UserInfo{
|
||||||
|
Username: c.Username,
|
||||||
|
Credential: c.Credential,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
m.access.Lock()
|
||||||
|
for k, v := range newCache {
|
||||||
|
if _, exists := m.authCache[k]; !exists {
|
||||||
|
m.authCache[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, v := range newUserCreds {
|
||||||
|
m.userCreds[k] = v
|
||||||
|
}
|
||||||
|
m.protocolUsers = byProtocol
|
||||||
|
m.access.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ReportTraffic(username string, tx int64, rx int64) {
|
||||||
|
if username == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.trafficAccess.Lock()
|
||||||
|
t, ok := m.traffic[username]
|
||||||
|
if !ok {
|
||||||
|
t = &UserTrafficAtomic{}
|
||||||
|
m.traffic[username] = t
|
||||||
|
}
|
||||||
|
m.trafficAccess.Unlock()
|
||||||
|
if tx > 0 {
|
||||||
|
t.Tx.Add(tx)
|
||||||
|
}
|
||||||
|
if rx > 0 {
|
||||||
|
t.Rx.Add(rx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetTraffic(username string) (int64, int64) {
|
||||||
|
m.trafficAccess.Lock()
|
||||||
|
t, ok := m.traffic[username]
|
||||||
|
m.trafficAccess.Unlock()
|
||||||
|
if !ok {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
return t.Tx.Load(), t.Rx.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ListTraffic() map[string]adapter.UserTraffic {
|
||||||
|
m.trafficAccess.Lock()
|
||||||
|
defer m.trafficAccess.Unlock()
|
||||||
|
result := make(map[string]adapter.UserTraffic, len(m.traffic))
|
||||||
|
for username, t := range m.traffic {
|
||||||
|
result[username] = adapter.UserTraffic{
|
||||||
|
Tx: t.Tx.Load(),
|
||||||
|
Rx: t.Rx.Load(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) KickUser(username string) {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
cred, ok := m.userCreds[username]
|
||||||
|
if ok {
|
||||||
|
delete(m.authCache, cred)
|
||||||
|
delete(m.userCreds, username)
|
||||||
|
}
|
||||||
|
for proto, users := range m.protocolUsers {
|
||||||
|
filtered := make([]adapter.UserInfo, 0, len(users))
|
||||||
|
for _, u := range users {
|
||||||
|
if u.Username != username {
|
||||||
|
filtered = append(filtered, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.protocolUsers[proto] = filtered
|
||||||
|
}
|
||||||
|
m.logger.Info("kicked user ", username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionTracker implementation
|
||||||
|
|
||||||
|
func (m *Manager) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn {
|
||||||
|
username := metadata.User
|
||||||
|
if username == "" {
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
counter := &userConnCounter{
|
||||||
|
username: username,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
m.access.Lock()
|
||||||
|
if m.userConns[username] == nil {
|
||||||
|
m.userConns[username] = make(map[*userConnCounter]struct{})
|
||||||
|
}
|
||||||
|
m.userConns[username][counter] = struct{}{}
|
||||||
|
m.access.Unlock()
|
||||||
|
|
||||||
|
wrapped := bufio.NewCounterConn(conn,
|
||||||
|
[]N.CountFunc{func(n int64) { counter.tx += n }},
|
||||||
|
[]N.CountFunc{func(n int64) { counter.rx += n }},
|
||||||
|
)
|
||||||
|
|
||||||
|
return &trackedConn{
|
||||||
|
ExtendedConn: wrapped,
|
||||||
|
onClose: func() {
|
||||||
|
m.ReportTraffic(username, counter.tx, counter.rx)
|
||||||
|
m.access.Lock()
|
||||||
|
if conns, ok := m.userConns[username]; ok {
|
||||||
|
delete(conns, counter)
|
||||||
|
if len(conns) == 0 {
|
||||||
|
delete(m.userConns, username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.access.Unlock()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn {
|
||||||
|
username := metadata.User
|
||||||
|
if username == "" {
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
counter := &userConnCounter{
|
||||||
|
username: username,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
m.access.Lock()
|
||||||
|
if m.userConns[username] == nil {
|
||||||
|
m.userConns[username] = make(map[*userConnCounter]struct{})
|
||||||
|
}
|
||||||
|
m.userConns[username][counter] = struct{}{}
|
||||||
|
m.access.Unlock()
|
||||||
|
|
||||||
|
wrapped := bufio.NewCounterPacketConn(conn,
|
||||||
|
[]N.CountFunc{func(n int64) { counter.tx += n }},
|
||||||
|
[]N.CountFunc{func(n int64) { counter.rx += n }},
|
||||||
|
)
|
||||||
|
|
||||||
|
return &trackedPacketConn{
|
||||||
|
PacketConn: wrapped,
|
||||||
|
onClose: func() {
|
||||||
|
m.ReportTraffic(username, counter.tx, counter.rx)
|
||||||
|
m.access.Lock()
|
||||||
|
if conns, ok := m.userConns[username]; ok {
|
||||||
|
delete(conns, counter)
|
||||||
|
if len(conns) == 0 {
|
||||||
|
delete(m.userConns, username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.access.Unlock()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type trackedConn struct {
|
||||||
|
N.ExtendedConn
|
||||||
|
onClose func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *trackedConn) Close() error {
|
||||||
|
c.onClose()
|
||||||
|
return c.ExtendedConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *trackedConn) Upstream() any {
|
||||||
|
return c.ExtendedConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *trackedConn) ReaderReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *trackedConn) WriterReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type trackedPacketConn struct {
|
||||||
|
N.PacketConn
|
||||||
|
onClose func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *trackedPacketConn) Close() error {
|
||||||
|
c.onClose()
|
||||||
|
return c.PacketConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *trackedPacketConn) Upstream() any {
|
||||||
|
return c.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *trackedPacketConn) ReaderReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *trackedPacketConn) WriterReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traffic report
|
||||||
|
|
||||||
|
type trafficReport struct {
|
||||||
|
Traffic map[string]adapter.UserTraffic `json:"traffic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) reportTrafficToServer() {
|
||||||
|
traffic := m.ListTraffic()
|
||||||
|
if len(traffic) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
report := trafficReport{Traffic: traffic}
|
||||||
|
data, err := json.Marshal(report)
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Warn("marshal traffic report: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpReq, err := http.NewRequestWithContext(m.ctx, "POST", m.authServer+"/api/traffic", bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Warn("create traffic report: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := m.httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Warn("traffic report failed: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
m.logger.Debug("traffic reported, users=", len(traffic))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP API
|
||||||
|
|
||||||
|
func (m *Manager) serveAPI() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/traffic", m.handleTraffic)
|
||||||
|
mux.HandleFunc("/traffic/", m.handleTrafficUser)
|
||||||
|
mux.HandleFunc("/kick/", m.handleKick)
|
||||||
|
|
||||||
|
apiSecret := m.apiSecret
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if apiSecret != "" {
|
||||||
|
auth := r.Header.Get("Authorization")
|
||||||
|
expected := "Bearer " + apiSecret
|
||||||
|
if subtle.ConstantTimeCompare([]byte(auth), []byte(expected)) != 1 {
|
||||||
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mux.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: m.apiListen,
|
||||||
|
Handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", m.apiListen)
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Error("api listen: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.logger.Info("user manager API listening on ", m.apiListen)
|
||||||
|
|
||||||
|
err = server.Serve(listener)
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
m.logger.Error("api serve: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) handleTraffic(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
traffic := m.ListTraffic()
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"users": traffic})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) handleTrafficUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := r.URL.Path[len("/traffic/"):]
|
||||||
|
if username == "" {
|
||||||
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tx, rx := m.GetTraffic(username)
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"tx": tx, "rx": rx})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) handleKick(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := r.URL.Path[len("/kick/"):]
|
||||||
|
if username == "" {
|
||||||
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.KickUser(username)
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetCachedUsers() []string {
|
||||||
|
m.access.RLock()
|
||||||
|
defer m.access.RUnlock()
|
||||||
|
users := make([]string, 0, len(m.userCreds))
|
||||||
|
for u := range m.userCreds {
|
||||||
|
users = append(users, u)
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = fmt.Sprintf
|
||||||
|
var _ = io.Discard
|
||||||
18
service/usermanager/registry.go
Normal file
18
service/usermanager/registry.go
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package usermanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/adapter/service"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterService(registry *service.Registry) {
|
||||||
|
service.Register[option.UserManagerOptions](registry, "user_manager", NewUserManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserManager(ctx context.Context, logger log.ContextLogger, tag string, options option.UserManagerOptions) (adapter.Service, error) {
|
||||||
|
return New(ctx, logger, tag, options)
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue