feat: audit api, sdwan persist, relay fallback updates
This commit is contained in:
@@ -25,8 +25,9 @@ func main() {
|
||||
user := flag.String("user", "", "Username for token generation")
|
||||
pass := flag.String("password", "", "Password for token generation")
|
||||
flag.BoolVar(&cfg.Insecure, "insecure", false, "Skip TLS verification")
|
||||
flag.BoolVar(&cfg.RelayEnabled, "relay", false, "Enable relay capability")
|
||||
flag.BoolVar(&cfg.SuperRelay, "super", false, "Register as super relay node (implies -relay)")
|
||||
flag.BoolVar(&cfg.RelayEnabled, "relay", cfg.RelayEnabled, "Enable relay capability")
|
||||
flag.BoolVar(&cfg.SuperRelay, "super", cfg.SuperRelay, "Register as super relay node (implies -relay)")
|
||||
flag.BoolVar(&cfg.RelayOfficial, "official-relay", cfg.RelayOfficial, "Register as official relay node")
|
||||
flag.IntVar(&cfg.RelayPort, "relay-port", cfg.RelayPort, "Relay listen port")
|
||||
flag.IntVar(&cfg.MaxRelayLoad, "relay-max", cfg.MaxRelayLoad, "Max concurrent relay sessions")
|
||||
flag.IntVar(&cfg.ShareBandwidth, "bw", cfg.ShareBandwidth, "Share bandwidth (Mbps)")
|
||||
@@ -49,9 +50,7 @@ func main() {
|
||||
// Load config file first (unless -newconfig)
|
||||
if !*newConfig {
|
||||
if data, err := os.ReadFile(*configFile); err == nil {
|
||||
var fileCfg config.ClientConfig
|
||||
if err := json.Unmarshal(data, &fileCfg); err == nil {
|
||||
cfg = fileCfg
|
||||
if err := json.Unmarshal(data, &cfg); err == nil {
|
||||
// fill defaults for missing fields
|
||||
if cfg.ServerPort == 0 {
|
||||
cfg.ServerPort = config.DefaultWSPort
|
||||
@@ -101,6 +100,9 @@ func main() {
|
||||
case "super":
|
||||
cfg.SuperRelay = true
|
||||
cfg.RelayEnabled = true // super implies relay
|
||||
case "official-relay":
|
||||
cfg.RelayOfficial = true
|
||||
cfg.RelayEnabled = true
|
||||
case "bw":
|
||||
fmt.Sscanf(f.Value.String(), "%d", &cfg.ShareBandwidth)
|
||||
}
|
||||
|
||||
@@ -12,16 +12,58 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/openp2p-cn/inp2p/internal/server"
|
||||
"github.com/openp2p-cn/inp2p/internal/store"
|
||||
"github.com/openp2p-cn/inp2p/pkg/auth"
|
||||
"github.com/openp2p-cn/inp2p/pkg/config"
|
||||
"github.com/openp2p-cn/inp2p/pkg/nat"
|
||||
"github.com/openp2p-cn/inp2p/pkg/protocol"
|
||||
)
|
||||
|
||||
type rateLimiter struct {
|
||||
mu sync.Mutex
|
||||
m map[string]*rateEntry
|
||||
max int
|
||||
window time.Duration
|
||||
}
|
||||
|
||||
type rateEntry struct {
|
||||
count int
|
||||
reset time.Time
|
||||
}
|
||||
|
||||
func newRateLimiter(max int, window time.Duration) *rateLimiter {
|
||||
return &rateLimiter{m: make(map[string]*rateEntry), max: max, window: window}
|
||||
}
|
||||
|
||||
func (r *rateLimiter) Allow(key string) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
e, ok := r.m[key]
|
||||
now := time.Now()
|
||||
if !ok || now.After(e.reset) {
|
||||
r.m[key] = &rateEntry{count: 1, reset: now.Add(r.window)}
|
||||
return true
|
||||
}
|
||||
if e.count >= r.max {
|
||||
return false
|
||||
}
|
||||
e.count++
|
||||
return true
|
||||
}
|
||||
|
||||
func clientIP(addr string) string {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return addr
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config.DefaultServerConfig()
|
||||
|
||||
@@ -38,6 +80,8 @@ func main() {
|
||||
token := flag.Uint64("token", 0, "Master authentication token (uint64)")
|
||||
user := flag.String("user", "", "Username for token generation (requires -password)")
|
||||
pass := flag.String("password", "", "Password for token generation")
|
||||
bootstrapAdmin := flag.String("bootstrap-admin", "", "Bootstrap system admin username (letters only, >=6)")
|
||||
bootstrapPass := flag.String("bootstrap-password", "", "Bootstrap system admin password")
|
||||
version := flag.Bool("version", false, "Print version and exit")
|
||||
flag.Parse()
|
||||
|
||||
@@ -47,6 +91,46 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Bootstrap system admin (optional)
|
||||
if *bootstrapAdmin != "" {
|
||||
if !server.IsValidGlobalUsername(*bootstrapAdmin) {
|
||||
log.Fatalf("[main] invalid bootstrap-admin username (letters only, >=6)")
|
||||
}
|
||||
if *bootstrapPass == "" {
|
||||
log.Fatalf("[main] bootstrap-password required")
|
||||
}
|
||||
st, err := store.Open(cfg.DBPath)
|
||||
if err != nil {
|
||||
log.Fatalf("[main] open store failed: %v", err)
|
||||
}
|
||||
_ = st.SetSetting("bootstrapped_admin", "1")
|
||||
// ensure default tenant exists
|
||||
if _, gErr := st.GetTenantByID(1); gErr != nil {
|
||||
_, _, _, _ = st.CreateTenantWithUsers("default", "admin", "admin")
|
||||
}
|
||||
// update/create admin user in tenant 1
|
||||
users, _ := st.ListUsers(1)
|
||||
var adminID int64
|
||||
for _, u := range users {
|
||||
if u.Role == "admin" {
|
||||
adminID = u.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if adminID > 0 {
|
||||
_ = st.UpdateUserEmail(adminID, *bootstrapAdmin)
|
||||
_ = st.UpdateUserPassword(adminID, *bootstrapPass)
|
||||
log.Printf("[main] bootstrapped admin updated: %s", *bootstrapAdmin)
|
||||
} else {
|
||||
_, err = st.CreateUser(1, "admin", *bootstrapAdmin, *bootstrapPass, 1)
|
||||
if err != nil {
|
||||
log.Fatalf("[main] bootstrap admin create failed: %v", err)
|
||||
}
|
||||
log.Printf("[main] bootstrapped admin created: %s", *bootstrapAdmin)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Token: either direct value or generated from user+password
|
||||
if *token > 0 {
|
||||
cfg.Token = *token
|
||||
@@ -91,7 +175,7 @@ func main() {
|
||||
srv := server.New(cfg)
|
||||
srv.StartCleanup()
|
||||
|
||||
// Admin-only Middleware (master token only)
|
||||
// Admin-only Middleware (System Admin session only)
|
||||
adminMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/v1/auth/login" {
|
||||
@@ -99,17 +183,18 @@ func main() {
|
||||
return
|
||||
}
|
||||
ac, ok := srv.ResolveAccess(r, cfg.Token)
|
||||
if !ok || ac.Kind != "master" {
|
||||
if !ok || ac.Kind != "session" || ac.Role != "admin" || ac.TenantID != 1 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprintf(w, `{"error":401,"message":"unauthorized"}`)
|
||||
return
|
||||
}
|
||||
r = r.WithContext(context.WithValue(r.Context(), server.ServerCtxKeyAccess{}, ac))
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Tenant or Admin Middleware (session/apikey/master)
|
||||
// Tenant Middleware (session/apikey only, no operator role)
|
||||
tenantMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/v1/auth/login" {
|
||||
@@ -123,15 +208,14 @@ func main() {
|
||||
fmt.Fprintf(w, `{"error":401,"message":"unauthorized"}`)
|
||||
return
|
||||
}
|
||||
if ac.Kind == "session" && ac.Role == "operator" {
|
||||
path := r.URL.Path
|
||||
if path != "/api/v1/nodes" && path != "/api/v1/sdwans" && path != "/api/v1/sdwan/edit" && path != "/api/v1/connect" && path != "/api/v1/nodes/apps" && path != "/api/v1/nodes/kick" && path != "/api/v1/nodes/alias" && path != "/api/v1/nodes/ip" && path != "/api/v1/stats" && path != "/api/v1/health" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
fmt.Fprintf(w, `{"error":403,"message":"forbidden"}`)
|
||||
return
|
||||
}
|
||||
// reject master token for tenant APIs
|
||||
if ac.Kind == "master" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprintf(w, `{"error":401,"message":"unauthorized"}`)
|
||||
return
|
||||
}
|
||||
r = r.WithContext(context.WithValue(r.Context(), server.ServerCtxKeyAccess{}, ac))
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
@@ -192,9 +276,31 @@ func main() {
|
||||
mux.HandleFunc("/api/v1/admin/tenants/", adminMiddleware(srv.HandleAdminCreateAPIKey))
|
||||
mux.HandleFunc("/api/v1/admin/users", adminMiddleware(srv.HandleAdminUsers))
|
||||
mux.HandleFunc("/api/v1/admin/users/", adminMiddleware(srv.HandleAdminUsers))
|
||||
mux.HandleFunc("/api/v1/admin/settings", adminMiddleware(srv.HandleAdminSettings))
|
||||
mux.HandleFunc("/api/v1/admin/audit", adminMiddleware(srv.HandleAdminAudit))
|
||||
mux.HandleFunc("/api/v1/tenants/enroll", srv.HandleTenantEnroll)
|
||||
mux.HandleFunc("/api/v1/enroll/consume", srv.HandleEnrollConsume)
|
||||
mux.HandleFunc("/api/v1/enroll/consume/", srv.HandleEnrollConsume)
|
||||
// enroll consume with rate-limit
|
||||
rl := newRateLimiter(10, time.Minute)
|
||||
mux.HandleFunc("/api/v1/enroll/consume", func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := clientIP(r.RemoteAddr)
|
||||
if !rl.Allow(ip) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
fmt.Fprintf(w, `{"error":1,"message":"too many requests"}`)
|
||||
return
|
||||
}
|
||||
srv.HandleEnrollConsume(w, r)
|
||||
})
|
||||
mux.HandleFunc("/api/v1/enroll/consume/", func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := clientIP(r.RemoteAddr)
|
||||
if !rl.Allow(ip) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
fmt.Fprintf(w, `{"error":1,"message":"too many requests"}`)
|
||||
return
|
||||
}
|
||||
srv.HandleEnrollConsume(w, r)
|
||||
})
|
||||
mux.HandleFunc("/api/v1/nodes/alias", tenantMiddleware(srv.HandleNodeMeta))
|
||||
mux.HandleFunc("/api/v1/nodes/ip", tenantMiddleware(srv.HandleNodeMeta))
|
||||
|
||||
@@ -203,97 +309,63 @@ func main() {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
// Support two modes:
|
||||
// 1) token login: {"token":"xxxx"} (admin/master only, backward compatible)
|
||||
// 2) user login: {"tenant":1,"username":"admin","password":"pass"}
|
||||
var reqTok struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
// single mode: username/password login
|
||||
var reqUser struct {
|
||||
TenantID int64 `json:"tenant"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
_ = json.Unmarshal(body, &reqTok)
|
||||
_ = json.Unmarshal(body, &reqUser)
|
||||
|
||||
// --- user login (session token) ---
|
||||
if reqUser.TenantID > 0 && reqUser.Username != "" && reqUser.Password != "" {
|
||||
if srv.Store() == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, `{"error":1,"message":"store not ready"}`)
|
||||
return
|
||||
}
|
||||
u, err := srv.Store().VerifyUserPassword(reqUser.TenantID, reqUser.Username, reqUser.Password)
|
||||
if err != nil || u == nil || u.Status != 1 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprintf(w, `{"error":1,"message":"invalid credentials"}`)
|
||||
return
|
||||
}
|
||||
sessionToken, exp, err := srv.Store().CreateSessionToken(u.ID, reqUser.TenantID, u.Role, 24*time.Hour)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, `{"error":1,"message":"create session failed"}`)
|
||||
return
|
||||
}
|
||||
ten, _ := srv.Store().GetTenantByID(reqUser.TenantID)
|
||||
resp := struct {
|
||||
Error int `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Token string `json:"token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Role string `json:"role"`
|
||||
Status int `json:"status"`
|
||||
Subnet string `json:"subnet"`
|
||||
}{0, "ok", sessionToken, "session", exp, u.Role, u.Status, ""}
|
||||
if ten != nil {
|
||||
resp.Subnet = ten.Subnet
|
||||
}
|
||||
b, _ := json.Marshal(resp)
|
||||
if reqUser.Username == "" || reqUser.Password == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(b)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, `{"error":1,"message":"username and password required"}`)
|
||||
return
|
||||
}
|
||||
|
||||
// --- token login (legacy/admin) ---
|
||||
valid := false
|
||||
role := "admin"
|
||||
status := 1
|
||||
if reqTok.Token != "" {
|
||||
// support numeric token as string
|
||||
if reqTok.Token == fmt.Sprintf("%d", cfg.Token) {
|
||||
valid = true
|
||||
} else {
|
||||
for _, t := range cfg.Tokens {
|
||||
if reqTok.Token == fmt.Sprintf("%d", t) {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !server.IsValidGlobalUsername(reqUser.Username) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, `{"error":1,"message":"username must be letters only and >=6"}`)
|
||||
return
|
||||
}
|
||||
if !valid {
|
||||
if srv.Store() == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, `{"error":1,"message":"store not ready"}`)
|
||||
return
|
||||
}
|
||||
u, err := srv.Store().VerifyUserPasswordGlobal(reqUser.Username, reqUser.Password)
|
||||
if err != nil || u == nil || u.Status != 1 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprintf(w, `{"error":1,"message":"invalid token"}`)
|
||||
fmt.Fprintf(w, `{"error":1,"message":"invalid credentials"}`)
|
||||
return
|
||||
}
|
||||
if srv.Store() != nil {
|
||||
if u, err := srv.Store().GetUserByTenant(0); err == nil && u != nil {
|
||||
if u.Role != "" {
|
||||
role = u.Role
|
||||
}
|
||||
status = u.Status
|
||||
}
|
||||
sessionToken, exp, err := srv.Store().CreateSessionToken(u.ID, u.TenantID, u.Role, 24*time.Hour)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, `{"error":1,"message":"create session failed"}`)
|
||||
return
|
||||
}
|
||||
ten, _ := srv.Store().GetTenantByID(u.TenantID)
|
||||
resp := struct {
|
||||
Error int `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Token string `json:"token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Role string `json:"role"`
|
||||
Status int `json:"status"`
|
||||
Subnet string `json:"subnet"`
|
||||
}{0, "ok", sessionToken, "session", exp, u.Role, u.Status, ""}
|
||||
if ten != nil {
|
||||
resp.Subnet = ten.Subnet
|
||||
}
|
||||
b, _ := json.Marshal(resp)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"error":0,"token":"%d","role":"%s","status":%d}`, cfg.Token, role, status)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(b)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/v1/health", tenantMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -347,10 +419,35 @@ func main() {
|
||||
http.Error(w, "hub mode requires hubNode", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// subnet proxy validation (basic)
|
||||
for _, sp := range req.SubnetProxies {
|
||||
if sp.Node == "" || sp.LocalCIDR == "" || sp.VirtualCIDR == "" {
|
||||
http.Error(w, "subnet proxy requires node/localCIDR/virtualCIDR", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, lnet, lerr := net.ParseCIDR(sp.LocalCIDR)
|
||||
_, vnet, verr := net.ParseCIDR(sp.VirtualCIDR)
|
||||
if lerr != nil || verr != nil || lnet == nil || vnet == nil {
|
||||
http.Error(w, "subnet proxy CIDR invalid", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
lOnes, _ := lnet.Mask.Size()
|
||||
vOnes, _ := vnet.Mask.Size()
|
||||
if lOnes != vOnes {
|
||||
http.Error(w, "subnet proxy CIDR mask mismatch", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
// tenant filter by session/apikey
|
||||
tenantID := getTenantID(r)
|
||||
if tenantID > 0 {
|
||||
if err := srv.SetSDWANTenant(tenantID, req); err != nil {
|
||||
ac := server.GetAccessContext(r)
|
||||
actorType, actorID := "", ""
|
||||
if ac != nil {
|
||||
actorType = ac.Kind
|
||||
actorID = fmt.Sprintf("%d", ac.UserID)
|
||||
}
|
||||
if err := srv.SetSDWANTenant(tenantID, req, actorType, actorID, r.RemoteAddr); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user